summaryrefslogtreecommitdiff
path: root/spec/services/ci
diff options
context:
space:
mode:
Diffstat (limited to 'spec/services/ci')
-rw-r--r--spec/services/ci/compare_accessibility_reports_service_spec.rb30
-rw-r--r--spec/services/ci/compare_codequality_reports_service_spec.rb32
-rw-r--r--spec/services/ci/compare_reports_base_service_spec.rb38
-rw-r--r--spec/services/ci/compare_test_reports_service_spec.rb32
-rw-r--r--spec/services/ci/create_job_artifacts_service_spec.rb2
-rw-r--r--spec/services/ci/create_pipeline_service/merge_requests_spec.rb53
-rw-r--r--spec/services/ci/create_pipeline_service/rules_spec.rb142
-rw-r--r--spec/services/ci/generate_coverage_reports_service_spec.rb30
-rw-r--r--spec/services/ci/generate_terraform_reports_service_spec.rb29
-rw-r--r--spec/services/ci/list_config_variables_service_spec.rb47
-rw-r--r--spec/services/ci/pipelines/create_artifact_service_spec.rb3
-rw-r--r--spec/services/ci/test_cases_service_spec.rb94
-rw-r--r--spec/services/ci/test_failure_history_service_spec.rb192
-rw-r--r--spec/services/ci/update_build_state_service_spec.rb70
14 files changed, 574 insertions, 220 deletions
diff --git a/spec/services/ci/compare_accessibility_reports_service_spec.rb b/spec/services/ci/compare_accessibility_reports_service_spec.rb
index 6903a633eeb..e0b84219834 100644
--- a/spec/services/ci/compare_accessibility_reports_service_spec.rb
+++ b/spec/services/ci/compare_accessibility_reports_service_spec.rb
@@ -29,34 +29,4 @@ RSpec.describe Ci::CompareAccessibilityReportsService do
end
end
end
-
- describe '#latest?' do
- subject { service.latest?(base_pipeline, head_pipeline, data) }
-
- let!(:base_pipeline) { nil }
- let!(:head_pipeline) { create(:ci_pipeline, :with_accessibility_reports, project: project) }
- let!(:key) { service.send(:key, base_pipeline, head_pipeline) }
-
- context 'when cache key is latest' do
- let(:data) { { key: key } }
-
- it { is_expected.to be_truthy }
- end
-
- context 'when cache key is outdated' do
- before do
- head_pipeline.update_column(:updated_at, 10.minutes.ago)
- end
-
- let(:data) { { key: key } }
-
- it { is_expected.to be_falsy }
- end
-
- context 'when cache key is empty' do
- let(:data) { { key: nil } }
-
- it { is_expected.to be_falsy }
- end
- end
end
diff --git a/spec/services/ci/compare_codequality_reports_service_spec.rb b/spec/services/ci/compare_codequality_reports_service_spec.rb
new file mode 100644
index 00000000000..ef762a2e9ad
--- /dev/null
+++ b/spec/services/ci/compare_codequality_reports_service_spec.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ci::CompareCodequalityReportsService do
+ let(:service) { described_class.new(project) }
+ let(:project) { create(:project, :repository) }
+
+ describe '#execute' do
+ subject { service.execute(base_pipeline, head_pipeline) }
+
+ context 'when head pipeline has a codequality report' do
+ let(:base_pipeline) { nil }
+ let(:head_pipeline) { create(:ci_pipeline, :with_codequality_reports, project: project) }
+
+ it 'returns status and data' do
+ expect(subject[:status]).to eq(:parsed)
+ expect(subject[:data]).to match_schema('entities/codequality_reports_comparer')
+ end
+ end
+
+ context 'when base and head pipelines have codequality reports' do
+ let(:base_pipeline) { create(:ci_pipeline, :with_codequality_reports, project: project) }
+ let(:head_pipeline) { create(:ci_pipeline, :with_codequality_reports, project: project) }
+
+ it 'returns status and data' do
+ expect(subject[:status]).to eq(:parsed)
+ expect(subject[:data]).to match_schema('entities/codequality_reports_comparer')
+ end
+ end
+ end
+end
diff --git a/spec/services/ci/compare_reports_base_service_spec.rb b/spec/services/ci/compare_reports_base_service_spec.rb
new file mode 100644
index 00000000000..9ce58c4972d
--- /dev/null
+++ b/spec/services/ci/compare_reports_base_service_spec.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ci::CompareReportsBaseService do
+ let(:service) { described_class.new(project) }
+ let(:project) { create(:project, :repository) }
+
+ describe '#latest?' do
+ subject { service.latest?(base_pipeline, head_pipeline, data) }
+
+ let!(:base_pipeline) { nil }
+ let!(:head_pipeline) { create(:ci_pipeline, :with_test_reports, project: project) }
+ let!(:key) { service.send(:key, base_pipeline, head_pipeline) }
+
+ context 'when cache key is latest' do
+ let(:data) { { key: key } }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when cache key is outdated' do
+ before do
+ head_pipeline.update_column(:updated_at, 10.minutes.ago)
+ end
+
+ let(:data) { { key: key } }
+
+ it { is_expected.to be_falsy }
+ end
+
+ context 'when cache key is empty' do
+ let(:data) { { key: nil } }
+
+ it { is_expected.to be_falsy }
+ end
+ end
+end
diff --git a/spec/services/ci/compare_test_reports_service_spec.rb b/spec/services/ci/compare_test_reports_service_spec.rb
index 377c801b008..01d58b2095f 100644
--- a/spec/services/ci/compare_test_reports_service_spec.rb
+++ b/spec/services/ci/compare_test_reports_service_spec.rb
@@ -67,7 +67,7 @@ RSpec.describe Ci::CompareTestReportsService do
# The JUnit fixture for the given build has 3 failures.
# This service will create 1 test case failure record for each.
- Ci::TestCasesService.new.execute(build)
+ Ci::TestFailureHistoryService.new(head_pipeline).execute
end
it 'loads recent failures on limited test cases to avoid building up a huge DB query', :aggregate_failures do
@@ -80,34 +80,4 @@ RSpec.describe Ci::CompareTestReportsService do
end
end
end
-
- describe '#latest?' do
- subject { service.latest?(base_pipeline, head_pipeline, data) }
-
- let!(:base_pipeline) { nil }
- let!(:head_pipeline) { create(:ci_pipeline, :with_test_reports, project: project) }
- let!(:key) { service.send(:key, base_pipeline, head_pipeline) }
-
- context 'when cache key is latest' do
- let(:data) { { key: key } }
-
- it { is_expected.to be_truthy }
- end
-
- context 'when cache key is outdated' do
- before do
- head_pipeline.update_column(:updated_at, 10.minutes.ago)
- end
-
- let(:data) { { key: key } }
-
- it { is_expected.to be_falsy }
- end
-
- context 'when cache key is empty' do
- let(:data) { { key: nil } }
-
- it { is_expected.to be_falsy }
- end
- end
end
diff --git a/spec/services/ci/create_job_artifacts_service_spec.rb b/spec/services/ci/create_job_artifacts_service_spec.rb
index 72b0d220b11..29e51a23dea 100644
--- a/spec/services/ci/create_job_artifacts_service_spec.rb
+++ b/spec/services/ci/create_job_artifacts_service_spec.rb
@@ -24,7 +24,7 @@ RSpec.describe Ci::CreateJobArtifactsService do
upload = Tempfile.new('upload')
FileUtils.copy(path, upload.path)
- UploadedFile.new(upload.path, params)
+ UploadedFile.new(upload.path, **params)
end
describe '#execute' do
diff --git a/spec/services/ci/create_pipeline_service/merge_requests_spec.rb b/spec/services/ci/create_pipeline_service/merge_requests_spec.rb
new file mode 100644
index 00000000000..e5347faed6a
--- /dev/null
+++ b/spec/services/ci/create_pipeline_service/merge_requests_spec.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ci::CreatePipelineService do
+ context 'merge requests handling' do
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:user) { project.owner }
+
+ let(:ref) { 'refs/heads/feature' }
+ let(:source) { :push }
+ let(:service) { described_class.new(project, user, { ref: ref }) }
+ let(:pipeline) { service.execute(source) }
+
+ before do
+ stub_ci_pipeline_yaml_file <<-EOS
+ workflow:
+ rules:
+ # do not create pipelines if merge requests are opened
+ - if: $CI_OPEN_MERGE_REQUESTS
+ when: never
+
+ - if: $CI_COMMIT_BRANCH
+
+ rspec:
+ script: echo Hello World
+ EOS
+ end
+
+ context 'when pushing a change' do
+ context 'when a merge request already exists' do
+ let!(:merge_request) do
+ create(:merge_request,
+ source_project: project,
+ source_branch: 'feature',
+ target_project: project,
+ target_branch: 'master')
+ end
+
+ it 'does not create a pipeline' do
+ expect(pipeline).not_to be_persisted
+ end
+ end
+
+ context 'when no merge request exists' do
+ it 'does create a pipeline' do
+ expect(pipeline.errors).to be_empty
+ expect(pipeline).to be_persisted
+ end
+ end
+ end
+ end
+end
diff --git a/spec/services/ci/create_pipeline_service/rules_spec.rb b/spec/services/ci/create_pipeline_service/rules_spec.rb
index a0ff2fff0ef..ac6c4c188e4 100644
--- a/spec/services/ci/create_pipeline_service/rules_spec.rb
+++ b/spec/services/ci/create_pipeline_service/rules_spec.rb
@@ -93,6 +93,148 @@ RSpec.describe Ci::CreatePipelineService do
end
end
end
+
+ context 'with allow_failure and exit_codes', :aggregate_failures do
+ def find_job(name)
+ pipeline.builds.find_by(name: name)
+ end
+
+ let(:config) do
+ <<-EOY
+ job-1:
+ script: exit 42
+ allow_failure:
+ exit_codes: 42
+ rules:
+ - if: $CI_COMMIT_REF_NAME == "master"
+ allow_failure: false
+
+ job-2:
+ script: exit 42
+ allow_failure:
+ exit_codes: 42
+ rules:
+ - if: $CI_COMMIT_REF_NAME == "master"
+ allow_failure: true
+
+ job-3:
+ script: exit 42
+ allow_failure:
+ exit_codes: 42
+ rules:
+ - if: $CI_COMMIT_REF_NAME == "master"
+ when: manual
+ EOY
+ end
+
+ it 'creates a pipeline' do
+ expect(pipeline).to be_persisted
+ expect(build_names).to contain_exactly(
+ 'job-1', 'job-2', 'job-3'
+ )
+ end
+
+ it 'assigns job:allow_failure values to the builds' do
+ expect(find_job('job-1').allow_failure).to eq(false)
+ expect(find_job('job-2').allow_failure).to eq(true)
+ expect(find_job('job-3').allow_failure).to eq(false)
+ end
+
+ it 'removes exit_codes if allow_failure is specified' do
+ expect(find_job('job-1').options.dig(:allow_failure_criteria)).to be_nil
+ expect(find_job('job-2').options.dig(:allow_failure_criteria)).to be_nil
+ expect(find_job('job-3').options.dig(:allow_failure_criteria, :exit_codes)).to eq([42])
+ end
+
+ context 'with ci_allow_failure_with_exit_codes disabled' do
+ before do
+ stub_feature_flags(ci_allow_failure_with_exit_codes: false)
+ end
+
+ it 'does not persist allow_failure_criteria' do
+ expect(pipeline).to be_persisted
+
+ expect(find_job('job-1').options.key?(:allow_failure_criteria)).to be_falsey
+ expect(find_job('job-2').options.key?(:allow_failure_criteria)).to be_falsey
+ expect(find_job('job-3').options.key?(:allow_failure_criteria)).to be_falsey
+ end
+ end
+ end
+
+ context 'if:' do
+ context 'variables:' do
+ let(:config) do
+ <<-EOY
+ job:
+ script: "echo job1"
+ variables:
+ VAR1: my var 1
+ VAR2: my var 2
+ rules:
+ - if: $CI_COMMIT_REF_NAME =~ /master/
+ variables:
+ VAR1: overridden var 1
+ - if: $CI_COMMIT_REF_NAME =~ /feature/
+ variables:
+ VAR2: overridden var 2
+ VAR3: new var 3
+ - when: on_success
+ EOY
+ end
+
+ let(:job) { pipeline.builds.find_by(name: 'job') }
+
+ context 'when matching to the first rule' do
+ let(:ref) { 'refs/heads/master' }
+
+ it 'overrides VAR1' do
+ variables = job.scoped_variables_hash
+
+ expect(variables['VAR1']).to eq('overridden var 1')
+ expect(variables['VAR2']).to eq('my var 2')
+ expect(variables['VAR3']).to be_nil
+ end
+
+ context 'when FF ci_rules_variables is disabled' do
+ before do
+ stub_feature_flags(ci_rules_variables: false)
+ end
+
+ it 'does not affect variables' do
+ variables = job.scoped_variables_hash
+
+ expect(variables['VAR1']).to eq('my var 1')
+ expect(variables['VAR2']).to eq('my var 2')
+ expect(variables['VAR3']).to be_nil
+ end
+ end
+ end
+
+ context 'when matching to the second rule' do
+ let(:ref) { 'refs/heads/feature' }
+
+ it 'overrides VAR2 and adds VAR3' do
+ variables = job.scoped_variables_hash
+
+ expect(variables['VAR1']).to eq('my var 1')
+ expect(variables['VAR2']).to eq('overridden var 2')
+ expect(variables['VAR3']).to eq('new var 3')
+ end
+ end
+
+ context 'when no match' do
+ let(:ref) { 'refs/heads/wip' }
+
+ it 'does not affect vars' do
+ variables = job.scoped_variables_hash
+
+ expect(variables['VAR1']).to eq('my var 1')
+ expect(variables['VAR2']).to eq('my var 2')
+ expect(variables['VAR3']).to be_nil
+ end
+ end
+ end
+ end
end
context 'when workflow:rules are used' do
diff --git a/spec/services/ci/generate_coverage_reports_service_spec.rb b/spec/services/ci/generate_coverage_reports_service_spec.rb
index d39053adebc..d12a9268e7e 100644
--- a/spec/services/ci/generate_coverage_reports_service_spec.rb
+++ b/spec/services/ci/generate_coverage_reports_service_spec.rb
@@ -52,34 +52,4 @@ RSpec.describe Ci::GenerateCoverageReportsService do
end
end
end
-
- describe '#latest?' do
- subject { service.latest?(base_pipeline, head_pipeline, data) }
-
- let!(:base_pipeline) { nil }
- let!(:head_pipeline) { create(:ci_pipeline, :with_test_reports, project: project) }
- let!(:key) { service.send(:key, base_pipeline, head_pipeline) }
-
- context 'when cache key is latest' do
- let(:data) { { key: key } }
-
- it { is_expected.to be_truthy }
- end
-
- context 'when cache key is outdated' do
- before do
- head_pipeline.update_column(:updated_at, 10.minutes.ago)
- end
-
- let(:data) { { key: key } }
-
- it { is_expected.to be_falsy }
- end
-
- context 'when cache key is empty' do
- let(:data) { { key: nil } }
-
- it { is_expected.to be_falsy }
- end
- end
end
diff --git a/spec/services/ci/generate_terraform_reports_service_spec.rb b/spec/services/ci/generate_terraform_reports_service_spec.rb
index 25bf96035b2..c9ac74e050c 100644
--- a/spec/services/ci/generate_terraform_reports_service_spec.rb
+++ b/spec/services/ci/generate_terraform_reports_service_spec.rb
@@ -64,33 +64,4 @@ RSpec.describe Ci::GenerateTerraformReportsService do
end
end
end
-
- describe '#latest?' do
- let_it_be(:head_pipeline) { create(:ci_pipeline, :with_test_reports, project: project) }
-
- subject { described_class.new(project) }
-
- it 'returns true when cache key is latest' do
- cache_key = subject.send(:key, nil, head_pipeline)
-
- result = subject.latest?(nil, head_pipeline, key: cache_key)
-
- expect(result).to eq(true)
- end
-
- it 'returns false when cache key is outdated' do
- cache_key = subject.send(:key, nil, head_pipeline)
- head_pipeline.update_column(:updated_at, 10.minutes.ago)
-
- result = subject.latest?(nil, head_pipeline, key: cache_key)
-
- expect(result).to eq(false)
- end
-
- it 'returns false when cache key is nil' do
- result = subject.latest?(nil, head_pipeline, key: nil)
-
- expect(result).to eq(false)
- end
- end
end
diff --git a/spec/services/ci/list_config_variables_service_spec.rb b/spec/services/ci/list_config_variables_service_spec.rb
index 95c98c2b5ef..1735f4cfc97 100644
--- a/spec/services/ci/list_config_variables_service_spec.rb
+++ b/spec/services/ci/list_config_variables_service_spec.rb
@@ -2,7 +2,9 @@
require 'spec_helper'
-RSpec.describe Ci::ListConfigVariablesService do
+RSpec.describe Ci::ListConfigVariablesService, :use_clean_rails_memory_store_caching do
+ include ReactiveCachingHelpers
+
let(:project) { create(:project, :repository) }
let(:user) { project.creator }
let(:service) { described_class.new(project, user) }
@@ -31,6 +33,10 @@ RSpec.describe Ci::ListConfigVariablesService do
}
end
+ before do
+ synchronous_reactive_cache(service)
+ end
+
it 'returns variable list' do
expect(subject['KEY1']).to eq({ value: 'val 1', description: 'description 1' })
expect(subject['KEY2']).to eq({ value: 'val 2', description: '' })
@@ -65,6 +71,8 @@ RSpec.describe Ci::ListConfigVariablesService do
HEREDOC
end
end
+
+ synchronous_reactive_cache(service)
end
it 'returns variable list' do
@@ -77,6 +85,10 @@ RSpec.describe Ci::ListConfigVariablesService do
let(:sha) { 'invalid-sha' }
let(:ci_config) { nil }
+ before do
+ synchronous_reactive_cache(service)
+ end
+
it 'returns empty json' do
expect(subject).to eq({})
end
@@ -96,11 +108,44 @@ RSpec.describe Ci::ListConfigVariablesService do
}
end
+ before do
+ synchronous_reactive_cache(service)
+ end
+
it 'returns empty result' do
expect(subject).to eq({})
end
end
+ context 'when reading from cache' do
+ let(:sha) { 'master' }
+ let(:ci_config) { {} }
+ let(:reactive_cache_params) { [sha] }
+ let(:return_value) { { 'KEY1' => { value: 'val 1', description: 'description 1' } } }
+
+ before do
+ stub_reactive_cache(service, return_value, reactive_cache_params)
+ end
+
+ it 'returns variable list' do
+ expect(subject).to eq(return_value)
+ end
+ end
+
+ context 'when the cache is empty' do
+ let(:sha) { 'master' }
+ let(:ci_config) { {} }
+ let(:reactive_cache_params) { [sha] }
+
+ it 'returns nil and enquques the worker to fill cache' do
+ expect(ExternalServiceReactiveCachingWorker)
+ .to receive(:perform_async)
+ .with(service.class, service.id, *reactive_cache_params)
+
+ expect(subject).to be_nil
+ end
+ end
+
private
def stub_gitlab_ci_yml_for_sha(sha, result)
diff --git a/spec/services/ci/pipelines/create_artifact_service_spec.rb b/spec/services/ci/pipelines/create_artifact_service_spec.rb
index 6f177889ed3..4e9248d9d1a 100644
--- a/spec/services/ci/pipelines/create_artifact_service_spec.rb
+++ b/spec/services/ci/pipelines/create_artifact_service_spec.rb
@@ -7,7 +7,8 @@ RSpec.describe ::Ci::Pipelines::CreateArtifactService do
subject { described_class.new.execute(pipeline) }
context 'when pipeline has coverage reports' do
- let(:pipeline) { create(:ci_pipeline, :with_coverage_reports) }
+ let(:project) { create(:project, :repository) }
+ let(:pipeline) { create(:ci_pipeline, :with_coverage_reports, project: project) }
context 'when pipeline is finished' do
it 'creates a pipeline artifact' do
diff --git a/spec/services/ci/test_cases_service_spec.rb b/spec/services/ci/test_cases_service_spec.rb
deleted file mode 100644
index b61d308640f..00000000000
--- a/spec/services/ci/test_cases_service_spec.rb
+++ /dev/null
@@ -1,94 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Ci::TestCasesService, :aggregate_failures do
- describe '#execute' do
- subject(:execute_service) { described_class.new.execute(build) }
-
- context 'when build has test reports' do
- let(:build) { create(:ci_build, :success, :test_reports) } # The test report has 2 test case failures
-
- it 'creates test case failures records' do
- execute_service
-
- expect(Ci::TestCase.count).to eq(2)
- expect(Ci::TestCaseFailure.count).to eq(2)
- end
-
- context 'when feature flag for test failure history is disabled' do
- before do
- stub_feature_flags(test_failure_history: false)
- end
-
- it 'does not persist data' do
- execute_service
-
- expect(Ci::TestCase.count).to eq(0)
- expect(Ci::TestCaseFailure.count).to eq(0)
- end
- end
-
- context 'when build is not for the default branch' do
- before do
- build.update_column(:ref, 'new-feature')
- end
-
- it 'does not persist data' do
- execute_service
-
- expect(Ci::TestCase.count).to eq(0)
- expect(Ci::TestCaseFailure.count).to eq(0)
- end
- end
-
- context 'when test failure data have already been persisted with the same exact attributes' do
- before do
- execute_service
- end
-
- it 'does not fail but does not persist new data' do
- expect { described_class.new.execute(build) }.not_to raise_error
-
- expect(Ci::TestCase.count).to eq(2)
- expect(Ci::TestCaseFailure.count).to eq(2)
- end
- end
-
- context 'when test failure data have duplicates within the same payload (happens when the JUnit report has duplicate test case names but have different failures)' do
- let(:build) { create(:ci_build, :success, :test_reports_with_duplicate_failed_test_names) } # The test report has 2 test case failures but with the same test case keys
-
- it 'does not fail but does not persist duplicate data' do
- expect { described_class.new.execute(build) }.not_to raise_error
-
- expect(Ci::TestCase.count).to eq(1)
- expect(Ci::TestCaseFailure.count).to eq(1)
- end
- end
-
- context 'when number of failed test cases exceed the limit' do
- before do
- stub_const("#{described_class.name}::MAX_TRACKABLE_FAILURES", 1)
- end
-
- it 'does not persist data' do
- execute_service
-
- expect(Ci::TestCase.count).to eq(0)
- expect(Ci::TestCaseFailure.count).to eq(0)
- end
- end
- end
-
- context 'when build has no test reports' do
- let(:build) { create(:ci_build, :running) }
-
- it 'does not persist data' do
- execute_service
-
- expect(Ci::TestCase.count).to eq(0)
- expect(Ci::TestCaseFailure.count).to eq(0)
- end
- end
- end
-end
diff --git a/spec/services/ci/test_failure_history_service_spec.rb b/spec/services/ci/test_failure_history_service_spec.rb
new file mode 100644
index 00000000000..e858c85490d
--- /dev/null
+++ b/spec/services/ci/test_failure_history_service_spec.rb
@@ -0,0 +1,192 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ci::TestFailureHistoryService, :aggregate_failures do
+ describe '#execute' do
+ let(:project) { create(:project) }
+ let(:pipeline) { create(:ci_empty_pipeline, status: :created, project: project) }
+
+ subject(:execute_service) { described_class.new(pipeline).execute }
+
+ context 'when pipeline has failed builds with test reports' do
+ before do
+ # The test report has 2 test case failures
+ create(:ci_build, :failed, :test_reports, pipeline: pipeline, project: project)
+ end
+
+ it 'creates test case failures records' do
+ execute_service
+
+ expect(Ci::TestCase.count).to eq(2)
+ expect(Ci::TestCaseFailure.count).to eq(2)
+ end
+
+ context 'when feature flag for test failure history is disabled' do
+ before do
+ stub_feature_flags(test_failure_history: false)
+ end
+
+ it 'does not persist data' do
+ execute_service
+
+ expect(Ci::TestCase.count).to eq(0)
+ expect(Ci::TestCaseFailure.count).to eq(0)
+ end
+ end
+
+ context 'when pipeline is not for the default branch' do
+ before do
+ pipeline.update_column(:ref, 'new-feature')
+ end
+
+ it 'does not persist data' do
+ execute_service
+
+ expect(Ci::TestCase.count).to eq(0)
+ expect(Ci::TestCaseFailure.count).to eq(0)
+ end
+ end
+
+ context 'when test failure data have already been persisted with the same exact attributes' do
+ before do
+ execute_service
+ end
+
+ it 'does not fail but does not persist new data' do
+ expect { described_class.new(pipeline).execute }.not_to raise_error
+
+ expect(Ci::TestCase.count).to eq(2)
+ expect(Ci::TestCaseFailure.count).to eq(2)
+ end
+ end
+
+ context 'when number of failed test cases exceed the limit' do
+ before do
+ stub_const("#{described_class.name}::MAX_TRACKABLE_FAILURES", 1)
+ end
+
+ it 'does not persist data' do
+ execute_service
+
+ expect(Ci::TestCase.count).to eq(0)
+ expect(Ci::TestCaseFailure.count).to eq(0)
+ end
+ end
+
+ context 'when number of failed test cases across multiple builds exceed the limit' do
+ before do
+ stub_const("#{described_class.name}::MAX_TRACKABLE_FAILURES", 2)
+
+ # This other test report has 1 unique test case failure which brings us to 3 total failures across all builds
+ # thus exceeding the limit of 2 for MAX_TRACKABLE_FAILURES
+ create(:ci_build, :failed, :test_reports_with_duplicate_failed_test_names, pipeline: pipeline, project: project)
+ end
+
+ it 'does not persist data' do
+ execute_service
+
+ expect(Ci::TestCase.count).to eq(0)
+ expect(Ci::TestCaseFailure.count).to eq(0)
+ end
+ end
+ end
+
+ context 'when test failure data have duplicates within the same payload (happens when the JUnit report has duplicate test case names but have different failures)' do
+ before do
+ # The test report has 2 test case failures but with the same test case keys
+ create(:ci_build, :failed, :test_reports_with_duplicate_failed_test_names, pipeline: pipeline, project: project)
+ end
+
+ it 'does not fail but does not persist duplicate data' do
+ expect { execute_service }.not_to raise_error
+
+ expect(Ci::TestCase.count).to eq(1)
+ expect(Ci::TestCaseFailure.count).to eq(1)
+ end
+ end
+
+ context 'when pipeline has no failed builds with test reports' do
+ before do
+ create(:ci_build, :test_reports, pipeline: pipeline, project: project)
+ create(:ci_build, :failed, pipeline: pipeline, project: project)
+ end
+
+ it 'does not persist data' do
+ execute_service
+
+ expect(Ci::TestCase.count).to eq(0)
+ expect(Ci::TestCaseFailure.count).to eq(0)
+ end
+ end
+ end
+
+ describe '#should_track_failures?' do
+ let(:project) { create(:project, :repository) }
+ let(:pipeline) { create(:ci_empty_pipeline, status: :created, project: project, ref: project.default_branch) }
+
+ subject { described_class.new(pipeline).should_track_failures? }
+
+ before do
+ create(:ci_build, :test_reports, :failed, pipeline: pipeline, project: project)
+ create(:ci_build, :test_reports, :failed, pipeline: pipeline, project: project)
+ end
+
+ context 'when feature flag is enabled and pipeline ref is the default branch' do
+ it { is_expected.to eq(true) }
+ end
+
+ context 'when feature flag is disabled' do
+ before do
+ stub_feature_flags(test_failure_history: false)
+ end
+
+ it { is_expected.to eq(false) }
+ end
+
+ context 'when pipeline is not equal to the project default branch' do
+ before do
+ pipeline.update_column(:ref, 'some-other-branch')
+ end
+
+ it { is_expected.to eq(false) }
+ end
+
+ context 'when total number of builds with failed tests exceeds the max number of trackable failures' do
+ before do
+ stub_const("#{described_class.name}::MAX_TRACKABLE_FAILURES", 1)
+ end
+
+ it { is_expected.to eq(false) }
+ end
+ end
+
+ describe '#async' do
+ let(:pipeline) { double(id: 1) }
+ let(:service) { described_class.new(pipeline) }
+
+ context 'when service should track failures' do
+ before do
+ allow(service).to receive(:should_track_failures?).and_return(true)
+ end
+
+ it 'enqueues the worker when #perform_if_needed is called' do
+ expect(Ci::TestFailureHistoryWorker).to receive(:perform_async).with(pipeline.id)
+
+ service.async.perform_if_needed
+ end
+ end
+
+ context 'when service should not track failures' do
+ before do
+ allow(service).to receive(:should_track_failures?).and_return(false)
+ end
+
+ it 'does not enqueue the worker when #perform_if_needed is called' do
+ expect(Ci::TestFailureHistoryWorker).not_to receive(:perform_async)
+
+ service.async.perform_if_needed
+ end
+ end
+ end
+end
diff --git a/spec/services/ci/update_build_state_service_spec.rb b/spec/services/ci/update_build_state_service_spec.rb
index 2545909bf56..3112e5dda1b 100644
--- a/spec/services/ci/update_build_state_service_spec.rb
+++ b/spec/services/ci/update_build_state_service_spec.rb
@@ -80,7 +80,11 @@ RSpec.describe Ci::UpdateBuildStateService do
context 'when build has a checksum' do
let(:params) do
- { checksum: 'crc32:12345678', state: 'failed', failure_reason: 'script_failure' }
+ {
+ output: { checksum: 'crc32:12345678', bytesize: 123 },
+ failure_reason: 'script_failure',
+ state: 'failed'
+ }
end
context 'when build does not have associated trace chunks' do
@@ -154,14 +158,74 @@ RSpec.describe Ci::UpdateBuildStateService do
end
context 'when trace checksum is valid' do
- let(:params) { { checksum: 'crc32:ed82cd11', state: 'success' } }
+ let(:params) do
+ { output: { checksum: 'crc32:ed82cd11', bytesize: 4 }, state: 'success' }
+ end
- it 'does not increment invalid trace metric' do
+ it 'does not increment invalid or corrupted trace metric' do
execute_with_stubbed_metrics!
expect(metrics)
.not_to have_received(:increment_trace_operation)
.with(operation: :invalid)
+
+ expect(metrics)
+ .not_to have_received(:increment_trace_operation)
+ .with(operation: :corrupted)
+ end
+
+ context 'when using deprecated parameters' do
+ let(:params) do
+ { checksum: 'crc32:ed82cd11', state: 'success' }
+ end
+
+ it 'does not increment invalid or corrupted trace metric' do
+ execute_with_stubbed_metrics!
+
+ expect(metrics)
+ .not_to have_received(:increment_trace_operation)
+ .with(operation: :invalid)
+
+ expect(metrics)
+ .not_to have_received(:increment_trace_operation)
+ .with(operation: :corrupted)
+ end
+ end
+ end
+
+ context 'when trace checksum is invalid and the log is corrupted' do
+ let(:params) do
+ { output: { checksum: 'crc32:12345678', bytesize: 1 }, state: 'success' }
+ end
+
+ it 'increments invalid and corrupted trace metrics' do
+ execute_with_stubbed_metrics!
+
+ expect(metrics)
+ .to have_received(:increment_trace_operation)
+ .with(operation: :invalid)
+
+ expect(metrics)
+ .to have_received(:increment_trace_operation)
+ .with(operation: :corrupted)
+ end
+ end
+
+ context 'when trace checksum is invalid but the log seems fine' do
+ let(:params) do
+ { output: { checksum: 'crc32:12345678', bytesize: 4 }, state: 'success' }
+ end
+
+ it 'does not increment corrupted trace metric' do
+ execute_with_stubbed_metrics!
+
+ expect(metrics)
+ .to have_received(:increment_trace_operation)
+ .with(operation: :invalid)
+
+ expect(metrics)
+ .not_to have_received(:increment_trace_operation)
+ .with(operation: :corrupted)
end
end