diff options
Diffstat (limited to 'qa/spec')
-rw-r--r-- | qa/spec/resource/base_spec.rb | 100 | ||||
-rw-r--r-- | qa/spec/resource/reusable_collection_spec.rb | 110 | ||||
-rw-r--r-- | qa/spec/spec_helper.rb | 19 | ||||
-rw-r--r-- | qa/spec/specs/helpers/quarantine_spec.rb | 14 | ||||
-rw-r--r-- | qa/spec/specs/runner_spec.rb | 38 | ||||
-rw-r--r-- | qa/spec/support/formatters/test_stats_formatter_spec.rb | 60 | ||||
-rw-r--r-- | qa/spec/support/shared_contexts/packages_registry_shared_context.rb | 2 | ||||
-rw-r--r-- | qa/spec/support/shared_examples/merge_with_code_owner_shared_examples.rb | 18 | ||||
-rw-r--r-- | qa/spec/tools/reliable_report_spec.rb | 2 | ||||
-rw-r--r-- | qa/spec/tools/test_resources_data_processor_spec.rb | 63 |
10 files changed, 350 insertions, 76 deletions
diff --git a/qa/spec/resource/base_spec.rb b/qa/spec/resource/base_spec.rb index 2dd25f983bf..eab205ec5d1 100644 --- a/qa/spec/resource/base_spec.rb +++ b/qa/spec/resource/base_spec.rb @@ -3,10 +3,45 @@ RSpec.describe QA::Resource::Base do include QA::Support::Helpers::StubEnv - let(:resource) { spy('resource', username: 'qa') } + let(:resource) { spy('resource') } let(:location) { 'http://location' } let(:log_regex) { %r{==> Built a MyResource with username 'qa' via #{method} in [\d.\-e]+ seconds+} } + before do + allow(QA::Tools::TestResourceDataProcessor).to receive(:collect) + allow(QA::Tools::TestResourceDataProcessor).to receive(:write_to_file) + end + + shared_context 'with simple resource' do + subject do + Class.new(QA::Resource::Base) do + def self.name + 'MyResource' + end + + attribute :test do + 'block' + end + + attribute :username do + 'qa' + end + + attribute :no_block + + def fabricate!(*args) + 'any' + end + + def self.current_url + 'http://stub' + end + end + end + + let(:resource) { subject.new } + end + shared_context 'with fabrication context' do subject do Class.new(described_class) do @@ -56,23 +91,29 @@ RSpec.describe QA::Resource::Base do end describe '.fabricate_via_api!' do - include_context 'with fabrication context' + context 'when fabricating' do + include_context 'with fabrication context' - it_behaves_like 'fabrication method', :fabricate_via_api! + it_behaves_like 'fabrication method', :fabricate_via_api! - it 'instantiates the resource, calls resource method returns the resource' do - expect(resource).to receive(:fabricate_via_api!).and_return(location) + it 'instantiates the resource, calls resource method returns the resource' do + expect(resource).to receive(:fabricate_via_api!).and_return(location) - result = subject.fabricate_via_api!(resource: resource, parents: []) + result = subject.fabricate_via_api!(resource: resource, parents: []) - expect(result).to eq(resource) + expect(result).to eq(resource) + end end context "with debug log level" do + include_context 'with simple resource' + let(:method) { 'api' } before do allow(QA::Runtime::Logger).to receive(:debug) + allow(resource).to receive(:api_support?).and_return(true) + allow(resource).to receive(:fabricate_via_api!) end it 'logs the resource and build method' do @@ -88,27 +129,32 @@ RSpec.describe QA::Resource::Base do end describe '.fabricate_via_browser_ui!' do - include_context 'with fabrication context' + context 'when fabricating' do + include_context 'with fabrication context' - it_behaves_like 'fabrication method', :fabricate_via_browser_ui!, :fabricate! + it_behaves_like 'fabrication method', :fabricate_via_browser_ui!, :fabricate! - it 'instantiates the resource and calls resource method' do - subject.fabricate_via_browser_ui!('something', resource: resource, parents: []) + it 'instantiates the resource and calls resource method' do + subject.fabricate_via_browser_ui!('something', resource: resource, parents: []) - expect(resource).to have_received(:fabricate!).with('something') - end + expect(resource).to have_received(:fabricate!).with('something') + end - it 'returns fabrication resource' do - result = subject.fabricate_via_browser_ui!('something', resource: resource, parents: []) + it 'returns fabrication resource' do + result = subject.fabricate_via_browser_ui!('something', resource: resource, parents: []) - expect(result).to eq(resource) + expect(result).to eq(resource) + end end context "with debug log level" do + include_context 'with simple resource' + let(:method) { 'browser_ui' } before do allow(QA::Runtime::Logger).to receive(:debug) + # allow(resource).to receive(:fabricate!) end it 'logs the resource and build method' do @@ -123,28 +169,6 @@ RSpec.describe QA::Resource::Base do end end - shared_context 'with simple resource' do - subject do - Class.new(QA::Resource::Base) do - attribute :test do - 'block' - end - - attribute :no_block - - def fabricate! - 'any' - end - - def self.current_url - 'http://stub' - end - end - end - - let(:resource) { subject.new } - end - describe '.attribute' do include_context 'with simple resource' diff --git a/qa/spec/resource/reusable_collection_spec.rb b/qa/spec/resource/reusable_collection_spec.rb new file mode 100644 index 00000000000..9116462b396 --- /dev/null +++ b/qa/spec/resource/reusable_collection_spec.rb @@ -0,0 +1,110 @@ +# frozen_string_literal: true + +RSpec.describe QA::Resource::ReusableCollection do + let(:reusable_resource_class) do + Class.new do + prepend QA::Resource::Reusable + + attr_reader :removed + + def self.name + 'FooReusableResource' + end + + def comparable + self.class.name + end + + def remove_via_api! + @removed = true + end + + def exists?() end + end + end + + let(:another_reusable_resource_class) do + Class.new(reusable_resource_class) do + def self.name + 'BarReusableResource' + end + end + end + + let(:a_resource_instance) { reusable_resource_class.new } + let(:another_resource_instance) { another_reusable_resource_class.new } + + it 'is a singleton class' do + expect { described_class.new }.to raise_error(NoMethodError) + end + + subject(:collection) do + described_class.instance + end + + before do + described_class.register_resource_classes do |c| + reusable_resource_class.register(c) + another_reusable_resource_class.register(c) + end + + collection.resource_classes = { + 'FooReusableResource' => { + reuse_as_identifier: { + resource: a_resource_instance + } + }, + 'BarReusableResource' => { + another_reuse_as_identifier: { + resource: another_resource_instance + } + } + } + + allow(a_resource_instance).to receive(:validate_reuse) + allow(another_resource_instance).to receive(:validate_reuse) + end + + after do + collection.resource_classes = {} + end + + describe '#each_resource' do + it 'yields each resource and reuse_as identifier in the collection' do + expect { |blk| collection.each_resource(&blk) } + .to yield_successive_args( + [:reuse_as_identifier, a_resource_instance], + [:another_reuse_as_identifier, another_resource_instance] + ) + end + end + + describe '.remove_all_via_api!' do + before do + allow(a_resource_instance).to receive(:exists?).and_return(true) + allow(another_resource_instance).to receive(:exists?).and_return(true) + end + + it 'removes each instance of each resource class' do + described_class.remove_all_via_api! + + expect(a_resource_instance.removed).to be true + expect(another_resource_instance.removed).to be true + end + end + + describe '.validate_resource_reuse' do + it 'validates each instance of each resource class' do + expect(a_resource_instance).to receive(:validate_reuse) + expect(another_resource_instance).to receive(:validate_reuse) + + described_class.validate_resource_reuse + end + end + + describe '.register_resource_classes' do + it 'yields the hash of resource classes in the collection' do + expect { |blk| described_class.register_resource_classes(&blk) }.to yield_with_args(collection.resource_classes) + end + end +end diff --git a/qa/spec/spec_helper.rb b/qa/spec/spec_helper.rb index 47791f76970..2a6acd6d014 100644 --- a/qa/spec/spec_helper.rb +++ b/qa/spec/spec_helper.rb @@ -6,6 +6,7 @@ require 'securerandom' require 'pathname' require 'active_support/core_ext/hash' require 'active_support/core_ext/object/blank' +require 'rainbow/refinement' require_relative 'qa_deprecation_toolkit_env' QaDeprecationToolkitEnv.configure! @@ -27,8 +28,16 @@ RSpec.configure do |config| config.add_formatter QA::Support::Formatters::QuarantineFormatter config.add_formatter QA::Support::Formatters::TestStatsFormatter if QA::Runtime::Env.export_metrics? + config.before(:suite) do |suite| + QA::Resource::ReusableCollection.register_resource_classes do |collection| + QA::Resource::ReusableProject.register(collection) + QA::Resource::ReusableGroup.register(collection) + end + end + config.prepend_before do |example| QA::Runtime::Logger.debug("\nStarting test: #{example.full_description}\n") + QA::Runtime::Example.current = example # Reset fabrication counters tracked in resource base Thread.current[:api_fabrication] = 0 @@ -65,11 +74,15 @@ RSpec.configure do |config| end config.after(:suite) do |suite| - # If any tests failed, leave the resources behind to help troubleshoot - QA::Resource::ReusableProject.remove_all_via_api! unless suite.reporter.failed_examples.present? - # Write all test created resources to JSON file QA::Tools::TestResourceDataProcessor.write_to_file + + # If requested, confirm that resources were used appropriately (e.g., not left with changes that interfere with + # further reuse) + QA::Resource::ReusableCollection.validate_resource_reuse if QA::Runtime::Env.validate_resource_reuse? + + # If any tests failed, leave the resources behind to help troubleshoot, otherwise remove them. + QA::Resource::ReusableCollection.remove_all_via_api! unless suite.reporter.failed_examples.present? end config.append_after(:suite) do diff --git a/qa/spec/specs/helpers/quarantine_spec.rb b/qa/spec/specs/helpers/quarantine_spec.rb index 8ea375cdb05..0f2d592d771 100644 --- a/qa/spec/specs/helpers/quarantine_spec.rb +++ b/qa/spec/specs/helpers/quarantine_spec.rb @@ -104,6 +104,20 @@ RSpec.describe QA::Specs::Helpers::Quarantine do end describe '.skip_or_run_quarantined_tests_or_contexts' do + context 'with explicitly disabled quarantine' do + before do + stub_env('DISABLE_QUARANTINE', 'true') + end + + it 'runs quarantined test' do + group = describe_successfully do + it('is pending', :quarantine) {} + end + + expect(group.examples.first.execution_result.status).to eq(:passed) + end + end + context 'with no tag focused' do it 'skips quarantined tests' do group = describe_successfully do diff --git a/qa/spec/specs/runner_spec.rb b/qa/spec/specs/runner_spec.rb index 5cc9ff403cd..e52ca1fb17c 100644 --- a/qa/spec/specs/runner_spec.rb +++ b/qa/spec/specs/runner_spec.rb @@ -14,6 +14,7 @@ RSpec.describe QA::Specs::Runner do allow(QA::Runtime::Browser).to receive(:configure!) QA::Runtime::Scenario.define(:gitlab_address, "http://gitlab.test") + QA::Runtime::Scenario.define(:klass, "QA::Scenario::Test::Instance::All") end it_behaves_like 'excludes orchestrated, transient, and geo' @@ -43,6 +44,43 @@ RSpec.describe QA::Specs::Runner do subject.perform end + it 'writes to file when examples are more than zero' do + allow(RSpec::Core::Runner).to receive(:run).and_return(0) + + expect(File).to receive(:open).with('no_of_examples/test_instance_all.txt', 'w') { '22' } + + subject.perform + end + + it 'does not write to file when zero examples' do + out.string = '0 examples,' + allow(RSpec::Core::Runner).to receive(:run).and_return(0) + + expect(File).not_to receive(:open) + + subject.perform + end + + it 'raises error when Rspec output does not match regex' do + out.string = '0' + allow(RSpec::Core::Runner).to receive(:run).and_return(0) + + expect { subject.perform } + .to raise_error(QA::Specs::Runner::RegexMismatchError, 'Rspec output did not match regex') + end + + context 'when --tag is specified as an option' do + subject { described_class.new.tap { |runner| runner.options = %w[--tag actioncable] } } + + it 'includes the option value in the file name' do + expect_rspec_runner_arguments(['--dry-run', '--tag', '~geo', '--tag', 'actioncable', *described_class::DEFAULT_TEST_PATH_ARGS], [$stderr, anything]) + + expect(File).to receive(:open).with('no_of_examples/test_instance_all_actioncable.txt', 'w') { '22' } + + subject.perform + end + end + after do QA::Runtime::Scenario.attributes.delete(:count_examples_only) end diff --git a/qa/spec/support/formatters/test_stats_formatter_spec.rb b/qa/spec/support/formatters/test_stats_formatter_spec.rb index 2bfd7863653..84fc3b83185 100644 --- a/qa/spec/support/formatters/test_stats_formatter_spec.rb +++ b/qa/spec/support/formatters/test_stats_formatter_spec.rb @@ -1,10 +1,12 @@ # frozen_string_literal: true require 'rspec/core/sandbox' +require 'active_support/testing/time_helpers' describe QA::Support::Formatters::TestStatsFormatter do include QA::Support::Helpers::StubEnv include QA::Specs::Helpers::RSpec + include ActiveSupport::Testing::TimeHelpers let(:url) { "http://influxdb.net" } let(:token) { "token" } @@ -22,6 +24,7 @@ describe QA::Support::Formatters::TestStatsFormatter do let(:file_path) { "./qa/specs/features/#{stage}/subfolder/some_spec.rb" } let(:ui_fabrication) { 0 } let(:api_fabrication) { 0 } + let(:fabrication_resources) { {} } let(:influx_client_args) do { @@ -45,7 +48,8 @@ describe QA::Support::Formatters::TestStatsFormatter do job_name: "test-job", merge_request: "false", run_type: run_type, - stage: stage.match(%r{\d{1,2}_(\w+)}).captures.first + stage: stage.match(%r{\d{1,2}_(\w+)}).captures.first, + testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/1234' }, fields: { id: './spec/support/formatters/test_stats_formatter_spec.rb[1:1]', @@ -56,8 +60,7 @@ describe QA::Support::Formatters::TestStatsFormatter do retry_attempts: 0, job_url: ci_job_url, pipeline_url: ci_pipeline_url, - pipeline_id: ci_pipeline_id, - testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/1234' + pipeline_id: ci_pipeline_id } } end @@ -88,6 +91,7 @@ describe QA::Support::Formatters::TestStatsFormatter do before do allow(InfluxDB2::Client).to receive(:new).with(url, token, **influx_client_args) { influx_client } + allow(QA::Tools::TestResourceDataProcessor).to receive(:resources) { fabrication_resources } end context "without influxdb variables configured" do @@ -135,6 +139,7 @@ describe QA::Support::Formatters::TestStatsFormatter do it('spec', :reliable, testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/1234') {} end + expect(influx_write_api).to have_received(:write).once expect(influx_write_api).to have_received(:write).with(data: [data]) end end @@ -147,6 +152,7 @@ describe QA::Support::Formatters::TestStatsFormatter do it('spec', :quarantine, testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/1234') {} end + expect(influx_write_api).to have_received(:write).once expect(influx_write_api).to have_received(:write).with(data: [data]) end end @@ -162,6 +168,7 @@ describe QA::Support::Formatters::TestStatsFormatter do it 'exports data to influxdb with correct run type' do run_spec + expect(influx_write_api).to have_received(:write).once expect(influx_write_api).to have_received(:write).with(data: [data]) end end @@ -179,6 +186,7 @@ describe QA::Support::Formatters::TestStatsFormatter do it 'exports data to influxdb with correct run type' do run_spec + expect(influx_write_api).to have_received(:write).once expect(influx_write_api).to have_received(:write).with(data: [data]) end end @@ -195,8 +203,54 @@ describe QA::Support::Formatters::TestStatsFormatter do it 'exports data to influxdb with fabrication times' do run_spec + expect(influx_write_api).to have_received(:write).once expect(influx_write_api).to have_received(:write).with(data: [data]) end end + + context 'with fabrication resources' do + let(:fabrication_resources) do + { + 'QA::Resource::Project' => [{ + info: "with id '1'", + api_path: '/project', + fabrication_method: :api, + fabrication_time: 1, + http_method: :post, + timestamp: Time.now.to_s + }] + } + end + + let(:fabrication_data) do + { + name: 'fabrication-stats', + time: DateTime.strptime(ci_timestamp).to_time, + tags: { + resource: 'QA::Resource::Project', + fabrication_method: :api, + http_method: :post, + run_type: run_type, + merge_request: "false" + }, + fields: { + fabrication_time: 1, + info: "with id '1'", + job_url: ci_job_url, + timestamp: Time.now.to_s + } + } + end + + around do |example| + freeze_time { example.run } + end + + it 'exports fabrication stats data to influxdb' do + run_spec + + expect(influx_write_api).to have_received(:write).with(data: [fabrication_data]) + end + end end end diff --git a/qa/spec/support/shared_contexts/packages_registry_shared_context.rb b/qa/spec/support/shared_contexts/packages_registry_shared_context.rb index 348176d264b..73a6c2bd99e 100644 --- a/qa/spec/support/shared_contexts/packages_registry_shared_context.rb +++ b/qa/spec/support/shared_contexts/packages_registry_shared_context.rb @@ -32,7 +32,7 @@ module QA runner.name = "qa-runner-#{Time.now.to_i}" runner.tags = ["runner-for-#{package_project.group.name}"] runner.executor = :docker - runner.token = package_project.group.runners_token + runner.token = package_project.group.reload!.runners_token end end diff --git a/qa/spec/support/shared_examples/merge_with_code_owner_shared_examples.rb b/qa/spec/support/shared_examples/merge_with_code_owner_shared_examples.rb index 40a8be8202a..4bbad9bf3e5 100644 --- a/qa/spec/support/shared_examples/merge_with_code_owner_shared_examples.rb +++ b/qa/spec/support/shared_examples/merge_with_code_owner_shared_examples.rb @@ -31,19 +31,13 @@ module QA end # Require approval from code owners on the default branch - # The default branch is already protected, and we can't update a protected branch via the API (yet) - # so we unprotect it first and then protect it again with the desired parameters - Resource::ProtectedBranch.unprotect_via_api! do |protected_branch| - protected_branch.project = project - protected_branch.branch_name = project.default_branch - end - - Resource::ProtectedBranch.fabricate_via_api! do |protected_branch| - protected_branch.project = project - protected_branch.branch_name = project.default_branch - protected_branch.new_branch = false - protected_branch.require_code_owner_approval = true + protected_branch = Resource::ProtectedBranch.fabricate_via_api! do |branch| + branch.project = project + branch.branch_name = project.default_branch + branch.new_branch = false + branch.require_code_owner_approval = true end + protected_branch.set_require_code_owner_approval # Push a change to the file with a CODEOWNERS rule Resource::Repository::Push.fabricate! do |push| diff --git a/qa/spec/tools/reliable_report_spec.rb b/qa/spec/tools/reliable_report_spec.rb index 1ff62df34e0..85b2590d3aa 100644 --- a/qa/spec/tools/reliable_report_spec.rb +++ b/qa/spec/tools/reliable_report_spec.rb @@ -167,7 +167,7 @@ describe QA::Tools::ReliableReport do payload: { title: "Reliable e2e test report", description: issue_body, - labels: "Quality,test,type::maintenance,reliable test report" + labels: "Quality,test,type::maintenance,reliable test report,automation:devops-mapping-disable" } ) expect(slack_notifier).to have_received(:post).with( diff --git a/qa/spec/tools/test_resources_data_processor_spec.rb b/qa/spec/tools/test_resources_data_processor_spec.rb index 6a8c0fd06a4..5117d1d367f 100644 --- a/qa/spec/tools/test_resources_data_processor_spec.rb +++ b/qa/spec/tools/test_resources_data_processor_spec.rb @@ -1,33 +1,60 @@ # frozen_string_literal: true +require 'active_support/testing/time_helpers' + RSpec.describe QA::Tools::TestResourceDataProcessor do + include QA::Support::Helpers::StubEnv + include ActiveSupport::Testing::TimeHelpers + + subject(:processor) { Class.new(described_class).instance } + let(:info) { 'information' } - let(:api_path) { '/foo' } - let(:result) { [{ info: info, api_path: api_path }] } + let(:api_response) { {} } + let(:method) { :api } + let(:time) { 2 } + let(:api_path) { resource.api_delete_path } + let(:resource) { QA::Resource::Project.init { |project| project.id = 1 } } - describe '.collect' do - context 'when resource is not restricted' do - let(:resource) { instance_double(QA::Resource::Project, api_delete_path: '/foo', api_response: 'foo') } + let(:result) do + { + 'QA::Resource::Project' => [{ + info: info, + api_path: api_path, + fabrication_method: method, + fabrication_time: time, + http_method: :post, + timestamp: Time.now.to_s + }] + } + end + + before do + processor.collect(resource: resource, info: info, fabrication_method: method, fabrication_time: time) + end + + around do |example| + freeze_time { example.run } + end - it 'collects resource' do - expect(described_class.collect(resource, info)).to eq(result) - end + describe '.collect' do + it 'collects and stores resource' do + expect(processor.resources).to eq(result) end + end + + describe '.write_to_file' do + let(:resources_file) { Pathname.new(Faker::File.file_name(dir: 'tmp', ext: 'json')) } - context 'when resource api response is nil' do - let(:resource) { double(QA::Resource::Project, api_delete_path: '/foo', api_response: nil) } + before do + stub_env('QA_TEST_RESOURCES_CREATED_FILEPATH', resources_file) - it 'does not collect resource' do - expect(described_class.collect(resource, info)).to eq(nil) - end + allow(File).to receive(:write) end - context 'when resource is restricted' do - let(:resource) { double(QA::Resource::Sandbox, api_delete_path: '/foo', api_response: 'foo') } + it 'writes applicable resources to file' do + processor.write_to_file - it 'does not collect resource' do - expect(described_class.collect(resource, info)).to eq(nil) - end + expect(File).to have_received(:write).with(resources_file, JSON.pretty_generate(result)) end end end |