diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-12-17 14:59:07 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-12-17 14:59:07 +0300 |
commit | 8b573c94895dc0ac0e1d9d59cf3e8745e8b539ca (patch) | |
tree | 544930fb309b30317ae9797a9683768705d664c4 /spec/models/ci | |
parent | 4b1de649d0168371549608993deac953eb692019 (diff) |
Add latest changes from gitlab-org/gitlab@13-7-stable-eev13.7.0-rc42
Diffstat (limited to 'spec/models/ci')
-rw-r--r-- | spec/models/ci/bridge_spec.rb | 16 | ||||
-rw-r--r-- | spec/models/ci/build_dependencies_spec.rb | 202 | ||||
-rw-r--r-- | spec/models/ci/build_spec.rb | 194 | ||||
-rw-r--r-- | spec/models/ci/build_trace_chunk_spec.rb | 2 | ||||
-rw-r--r-- | spec/models/ci/build_trace_chunks/fog_spec.rb | 46 | ||||
-rw-r--r-- | spec/models/ci/job_artifact_spec.rb | 16 | ||||
-rw-r--r-- | spec/models/ci/pipeline_spec.rb | 575 |
7 files changed, 920 insertions, 131 deletions
diff --git a/spec/models/ci/bridge_spec.rb b/spec/models/ci/bridge_spec.rb index 51e82061d97..11dcecd50ca 100644 --- a/spec/models/ci/bridge_spec.rb +++ b/spec/models/ci/bridge_spec.rb @@ -330,14 +330,6 @@ RSpec.describe Ci::Bridge do subject { build_stubbed(:ci_bridge, :manual).playable? } it { is_expected.to be_truthy } - - context 'when FF ci_manual_bridges is disabled' do - before do - stub_feature_flags(ci_manual_bridges: false) - end - - it { is_expected.to be_falsey } - end end context 'when build is not a manual action' do @@ -352,14 +344,6 @@ RSpec.describe Ci::Bridge do subject { build_stubbed(:ci_bridge, :manual).action? } it { is_expected.to be_truthy } - - context 'when FF ci_manual_bridges is disabled' do - before do - stub_feature_flags(ci_manual_bridges: false) - end - - it { is_expected.to be_falsey } - end end context 'when build is not a manual action' do diff --git a/spec/models/ci/build_dependencies_spec.rb b/spec/models/ci/build_dependencies_spec.rb index 4fa1b3eb5a5..c5f56dbe5bc 100644 --- a/spec/models/ci/build_dependencies_spec.rb +++ b/spec/models/ci/build_dependencies_spec.rb @@ -146,6 +146,204 @@ RSpec.describe Ci::BuildDependencies do end end + describe '#cross_pipeline' do + let!(:job) do + create(:ci_build, + pipeline: pipeline, + name: 'build_with_pipeline_dependency', + options: { cross_dependencies: dependencies }) + end + + subject { described_class.new(job) } + + let(:cross_pipeline_deps) { subject.cross_pipeline } + + context 'when dependency specifications are valid' do + context 'when pipeline exists in the hierarchy' do + let!(:pipeline) { create(:ci_pipeline, child_of: parent_pipeline) } + let!(:parent_pipeline) { create(:ci_pipeline, project: project) } + + context 'when job exists' do + let(:dependencies) do + [{ pipeline: parent_pipeline.id.to_s, job: upstream_job.name, artifacts: true }] + end + + let!(:upstream_job) { create(:ci_build, :success, pipeline: parent_pipeline) } + + it { expect(cross_pipeline_deps).to contain_exactly(upstream_job) } + it { is_expected.to be_valid } + + context 'when pipeline and job are specified via variables' do + let(:dependencies) do + [{ pipeline: '$parent_pipeline_ID', job: '$UPSTREAM_JOB', artifacts: true }] + end + + before do + job.yaml_variables.push(key: 'parent_pipeline_ID', value: parent_pipeline.id.to_s, public: true) + job.yaml_variables.push(key: 'UPSTREAM_JOB', value: upstream_job.name, public: true) + job.save! + end + + it { expect(cross_pipeline_deps).to contain_exactly(upstream_job) } + it { is_expected.to be_valid } + end + + context 'when feature flag `ci_cross_pipeline_artifacts_download` is disabled' do + before do + stub_feature_flags(ci_cross_pipeline_artifacts_download: false) + end + + it { expect(cross_pipeline_deps).to be_empty } + it { is_expected.to be_valid } + end + end + + context 'when same job names exist in other pipelines in the hierarchy' do + let(:cross_pipeline_limit) do + ::Gitlab::Ci::Config::Entry::Needs::NEEDS_CROSS_PIPELINE_DEPENDENCIES_LIMIT + end + + let(:sibling_pipeline) { create(:ci_pipeline, child_of: parent_pipeline) } + + before do + cross_pipeline_limit.times do |index| + create(:ci_build, :success, + pipeline: parent_pipeline, name: "dependency-#{index}", + stage_idx: 1, stage: 'build', user: user + ) + + create(:ci_build, :success, + pipeline: sibling_pipeline, name: "dependency-#{index}", + stage_idx: 1, stage: 'build', user: user + ) + end + end + + let(:dependencies) do + [ + { pipeline: parent_pipeline.id.to_s, job: 'dependency-0', artifacts: true }, + { pipeline: parent_pipeline.id.to_s, job: 'dependency-1', artifacts: true }, + { pipeline: parent_pipeline.id.to_s, job: 'dependency-2', artifacts: true }, + { pipeline: sibling_pipeline.id.to_s, job: 'dependency-3', artifacts: true }, + { pipeline: sibling_pipeline.id.to_s, job: 'dependency-4', artifacts: true }, + { pipeline: sibling_pipeline.id.to_s, job: 'dependency-5', artifacts: true } + ] + end + + it 'returns a limited number of dependencies with the right match' do + expect(job.options[:cross_dependencies].size).to eq(cross_pipeline_limit.next) + expect(cross_pipeline_deps.size).to eq(cross_pipeline_limit) + expect(cross_pipeline_deps.map { |dep| [dep.pipeline_id, dep.name] }).to contain_exactly( + [parent_pipeline.id, 'dependency-0'], + [parent_pipeline.id, 'dependency-1'], + [parent_pipeline.id, 'dependency-2'], + [sibling_pipeline.id, 'dependency-3'], + [sibling_pipeline.id, 'dependency-4']) + end + end + + context 'when job does not exist' do + let(:dependencies) do + [{ pipeline: parent_pipeline.id.to_s, job: 'non-existent', artifacts: true }] + end + + it { expect(cross_pipeline_deps).to be_empty } + it { is_expected.not_to be_valid } + end + end + + context 'when pipeline does not exist' do + let(:dependencies) do + [{ pipeline: '123', job: 'non-existent', artifacts: true }] + end + + it { expect(cross_pipeline_deps).to be_empty } + it { is_expected.not_to be_valid } + end + + context 'when jobs exist in different pipelines in the hierarchy' do + let!(:pipeline) { create(:ci_pipeline, child_of: parent_pipeline) } + let!(:parent_pipeline) { create(:ci_pipeline, project: project) } + let!(:parent_job) { create(:ci_build, :success, name: 'parent_job', pipeline: parent_pipeline) } + + let!(:sibling_pipeline) { create(:ci_pipeline, child_of: parent_pipeline) } + let!(:sibling_job) { create(:ci_build, :success, name: 'sibling_job', pipeline: sibling_pipeline) } + + context 'when pipeline and jobs dependencies are mismatched' do + let(:dependencies) do + [ + { pipeline: parent_pipeline.id.to_s, job: sibling_job.name, artifacts: true }, + { pipeline: sibling_pipeline.id.to_s, job: parent_job.name, artifacts: true } + ] + end + + it { expect(cross_pipeline_deps).to be_empty } + it { is_expected.not_to be_valid } + + context 'when dependencies contain a valid pair' do + let(:dependencies) do + [ + { pipeline: parent_pipeline.id.to_s, job: sibling_job.name, artifacts: true }, + { pipeline: sibling_pipeline.id.to_s, job: parent_job.name, artifacts: true }, + { pipeline: sibling_pipeline.id.to_s, job: sibling_job.name, artifacts: true } + ] + end + + it 'filters out the invalid ones' do + expect(cross_pipeline_deps).to contain_exactly(sibling_job) + end + + it { is_expected.not_to be_valid } + end + end + end + + context 'when job and pipeline exist outside the hierarchy' do + let!(:pipeline) { create(:ci_pipeline, project: project) } + let!(:another_pipeline) { create(:ci_pipeline, project: project) } + let!(:dependency) { create(:ci_build, :success, pipeline: another_pipeline) } + + let(:dependencies) do + [{ pipeline: another_pipeline.id.to_s, job: dependency.name, artifacts: true }] + end + + it 'ignores jobs outside the pipeline hierarchy' do + expect(cross_pipeline_deps).to be_empty + end + + it { is_expected.not_to be_valid } + end + + context 'when current pipeline is specified' do + let!(:pipeline) { create(:ci_pipeline, project: project) } + let!(:dependency) { create(:ci_build, :success, pipeline: pipeline) } + + let(:dependencies) do + [{ pipeline: pipeline.id.to_s, job: dependency.name, artifacts: true }] + end + + it 'ignores jobs from the current pipeline as simple needs should be used instead' do + expect(cross_pipeline_deps).to be_empty + end + + it { is_expected.not_to be_valid } + end + end + + context 'when artifacts:false' do + let!(:pipeline) { create(:ci_pipeline, child_of: parent_pipeline) } + let!(:parent_pipeline) { create(:ci_pipeline, project: project) } + let!(:parent_job) { create(:ci_build, :success, name: 'parent_job', pipeline: parent_pipeline) } + + let(:dependencies) do + [{ pipeline: parent_pipeline.id.to_s, job: parent_job.name, artifacts: false }] + end + + it { expect(cross_pipeline_deps).to be_empty } + it { is_expected.to be_valid } # we simply ignore it + end + end + describe '#all' do let!(:job) do create(:ci_build, pipeline: pipeline, name: 'deploy', stage_idx: 3, stage: 'deploy') @@ -155,9 +353,9 @@ RSpec.describe Ci::BuildDependencies do subject { dependencies.all } - it 'returns the union of all local dependencies and any cross pipeline dependencies' do + it 'returns the union of all local dependencies and any cross project dependencies' do expect(dependencies).to receive(:local).and_return([1, 2, 3]) - expect(dependencies).to receive(:cross_pipeline).and_return([3, 4]) + expect(dependencies).to receive(:cross_project).and_return([3, 4]) expect(subject).to contain_exactly(1, 2, 3, 4) end diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index 5ff9b4dd493..9f412d64d56 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -14,7 +14,7 @@ RSpec.describe Ci::Build do status: 'success') end - let(:build) { create(:ci_build, pipeline: pipeline) } + let_it_be(:build, refind: true) { create(:ci_build, pipeline: pipeline) } it { is_expected.to belong_to(:runner) } it { is_expected.to belong_to(:trigger_request) } @@ -307,8 +307,6 @@ RSpec.describe Ci::Build do end describe '.without_needs' do - let!(:build) { create(:ci_build) } - subject { described_class.without_needs } context 'when no build_need is created' do @@ -1151,12 +1149,26 @@ RSpec.describe Ci::Build do end context 'when transits to skipped' do - before do - build.skip! + context 'when cd_skipped_deployment_status is disabled' do + before do + stub_feature_flags(cd_skipped_deployment_status: false) + build.skip! + end + + it 'transits deployment status to canceled' do + expect(deployment).to be_canceled + end end - it 'transits deployment status to canceled' do - expect(deployment).to be_canceled + context 'when cd_skipped_deployment_status is enabled' do + before do + stub_feature_flags(cd_skipped_deployment_status: project) + build.skip! + end + + it 'transits deployment status to skipped' do + expect(deployment).to be_skipped + end end end @@ -2005,6 +2017,8 @@ RSpec.describe Ci::Build do end context 'when ci_build_metadata_config is disabled' do + let(:build) { create(:ci_build, pipeline: pipeline) } + before do stub_feature_flags(ci_build_metadata_config: false) end @@ -2392,6 +2406,7 @@ RSpec.describe Ci::Build do before do stub_container_registry_config(enabled: container_registry_enabled, host_port: 'registry.example.com') + stub_config(dependency_proxy: { enabled: true }) end subject { build.variables } @@ -2409,6 +2424,8 @@ RSpec.describe Ci::Build do { key: 'CI_REGISTRY_USER', value: 'gitlab-ci-token', public: true, masked: false }, { key: 'CI_REGISTRY_PASSWORD', value: 'my-token', public: false, masked: true }, { key: 'CI_REPOSITORY_URL', value: build.repo_url, public: false, masked: false }, + { key: 'CI_DEPENDENCY_PROXY_USER', value: 'gitlab-ci-token', public: true, masked: false }, + { key: 'CI_DEPENDENCY_PROXY_PASSWORD', value: 'my-token', public: false, masked: true }, { key: 'CI_JOB_JWT', value: 'ci.job.jwt', public: false, masked: true }, { key: 'CI_JOB_NAME', value: 'test', public: true, masked: false }, { key: 'CI_JOB_STAGE', value: 'test', public: true, masked: false }, @@ -2441,6 +2458,11 @@ RSpec.describe Ci::Build do { key: 'CI_DEFAULT_BRANCH', value: project.default_branch, public: true, masked: false }, { key: 'CI_PAGES_DOMAIN', value: Gitlab.config.pages.host, public: true, masked: false }, { key: 'CI_PAGES_URL', value: project.pages_url, public: true, masked: false }, + { key: 'CI_DEPENDENCY_PROXY_SERVER', value: "#{Gitlab.config.gitlab.host}:#{Gitlab.config.gitlab.port}", public: true, masked: false }, + { key: 'CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX', + value: "#{Gitlab.config.gitlab.host}:#{Gitlab.config.gitlab.port}/#{project.namespace.root_ancestor.path}#{DependencyProxy::URL_SUFFIX}", + public: true, + masked: false }, { key: 'CI_API_V4_URL', value: 'http://localhost/api/v4', public: true, masked: false }, { key: 'CI_PIPELINE_IID', value: pipeline.iid.to_s, public: true, masked: false }, { key: 'CI_PIPELINE_SOURCE', value: pipeline.source, public: true, masked: false }, @@ -2502,6 +2524,7 @@ RSpec.describe Ci::Build do let(:project_pre_var) { { key: 'project', value: 'value', public: true, masked: false } } let(:pipeline_pre_var) { { key: 'pipeline', value: 'value', public: true, masked: false } } let(:build_yaml_var) { { key: 'yaml', value: 'value', public: true, masked: false } } + let(:dependency_proxy_var) { { key: 'dependency_proxy', value: 'value', public: true, masked: false } } let(:job_jwt_var) { { key: 'CI_JOB_JWT', value: 'ci.job.jwt', public: false, masked: true } } let(:job_dependency_var) { { key: 'job_dependency', value: 'value', public: true, masked: false } } @@ -2511,6 +2534,7 @@ RSpec.describe Ci::Build do allow(build).to receive(:persisted_variables) { [] } allow(build).to receive(:job_jwt_variables) { [job_jwt_var] } allow(build).to receive(:dependency_variables) { [job_dependency_var] } + allow(build).to receive(:dependency_proxy_variables) { [dependency_proxy_var] } allow(build.project) .to receive(:predefined_variables) { [project_pre_var] } @@ -2523,7 +2547,8 @@ RSpec.describe Ci::Build do it 'returns variables in order depending on resource hierarchy' do is_expected.to eq( - [job_jwt_var, + [dependency_proxy_var, + job_jwt_var, build_pre_var, project_pre_var, pipeline_pre_var, @@ -2730,7 +2755,11 @@ RSpec.describe Ci::Build do pipeline.update!(tag: true) end - it { is_expected.to include(tag_variable) } + it do + build.reload + + expect(subject).to include(tag_variable) + end end context 'when CI variable is defined' do @@ -2964,8 +2993,11 @@ RSpec.describe Ci::Build do end context 'when pipeline variable overrides build variable' do + let(:build) do + create(:ci_build, pipeline: pipeline, yaml_variables: [{ key: 'MYVAR', value: 'myvar', public: true }]) + end + before do - build.yaml_variables = [{ key: 'MYVAR', value: 'myvar', public: true }] pipeline.variables.build(key: 'MYVAR', value: 'pipeline value') end @@ -3281,9 +3313,12 @@ RSpec.describe Ci::Build do end context 'when overriding user-provided variables' do + let(:build) do + create(:ci_build, pipeline: pipeline, yaml_variables: [{ key: 'MY_VAR', value: 'myvar', public: true }]) + end + before do pipeline.variables.build(key: 'MY_VAR', value: 'pipeline value') - build.yaml_variables = [{ key: 'MY_VAR', value: 'myvar', public: true }] end it 'returns a hash including variable with higher precedence' do @@ -3640,7 +3675,7 @@ RSpec.describe Ci::Build do end it 'handles raised exception' do - expect { subject.drop! }.not_to raise_exception(Gitlab::Access::AccessDeniedError) + expect { subject.drop! }.not_to raise_error end it 'logs the error' do @@ -4045,13 +4080,72 @@ RSpec.describe Ci::Build do end end + context 'when there is a Cobertura coverage report with class filename paths not relative to project root' do + before do + allow(build.project).to receive(:full_path).and_return('root/javademo') + allow(build.pipeline).to receive(:all_worktree_paths).and_return(['src/main/java/com/example/javademo/User.java']) + + create(:ci_job_artifact, :coverage_with_paths_not_relative_to_project_root, job: build, project: build.project) + end + + it 'parses blobs and add the results to the coverage report with corrected paths' do + expect { subject }.not_to raise_error + + expect(coverage_report.files.keys).to match_array(['src/main/java/com/example/javademo/User.java']) + end + + context 'and smart_cobertura_parser feature flag is disabled' do + before do + stub_feature_flags(smart_cobertura_parser: false) + end + + it 'parses blobs and add the results to the coverage report with unmodified paths' do + expect { subject }.not_to raise_error + + expect(coverage_report.files.keys).to match_array(['com/example/javademo/User.java']) + end + end + end + context 'when there is a corrupted Cobertura coverage report' do before do create(:ci_job_artifact, :coverage_with_corrupted_data, job: build, project: build.project) end it 'raises an error' do - expect { subject }.to raise_error(Gitlab::Ci::Parsers::Coverage::Cobertura::CoberturaParserError) + expect { subject }.to raise_error(Gitlab::Ci::Parsers::Coverage::Cobertura::InvalidLineInformationError) + end + end + end + end + + describe '#collect_codequality_reports!' do + subject(:codequality_report) { build.collect_codequality_reports!(Gitlab::Ci::Reports::CodequalityReports.new) } + + it { expect(codequality_report.degradations).to eq({}) } + + context 'when build has a codequality report' do + context 'when there is a codequality report' do + before do + create(:ci_job_artifact, :codequality, job: build, project: build.project) + end + + it 'parses blobs and add the results to the codequality report' do + expect { codequality_report }.not_to raise_error + + expect(codequality_report.degradations_count).to eq(3) + end + end + + context 'when there is an codequality report without errors' do + before do + create(:ci_job_artifact, :codequality_without_errors, job: build, project: build.project) + end + + it 'parses blobs and add the results to the codequality report' do + expect { codequality_report }.not_to raise_error + + expect(codequality_report.degradations_count).to eq(0) end end end @@ -4639,4 +4733,78 @@ RSpec.describe Ci::Build do expect(action).not_to have_received(:perform!) end end + + describe '#debug_mode?' do + subject { build.debug_mode? } + + context 'when feature is disabled' do + before do + stub_feature_flags(restrict_access_to_build_debug_mode: false) + end + + it { is_expected.to eq false } + + context 'when in variables' do + before do + create(:ci_instance_variable, key: 'CI_DEBUG_TRACE', value: 'true') + end + + it { is_expected.to eq false } + end + end + + context 'when CI_DEBUG_TRACE=true is in variables' do + context 'when in instance variables' do + before do + create(:ci_instance_variable, key: 'CI_DEBUG_TRACE', value: 'true') + end + + it { is_expected.to eq true } + end + + context 'when in group variables' do + before do + create(:ci_group_variable, key: 'CI_DEBUG_TRACE', value: 'true', group: project.group) + end + + it { is_expected.to eq true } + end + + context 'when in pipeline variables' do + before do + create(:ci_pipeline_variable, key: 'CI_DEBUG_TRACE', value: 'true', pipeline: pipeline) + end + + it { is_expected.to eq true } + end + + context 'when in project variables' do + before do + create(:ci_variable, key: 'CI_DEBUG_TRACE', value: 'true', project: project) + end + + it { is_expected.to eq true } + end + + context 'when in job variables' do + before do + create(:ci_job_variable, key: 'CI_DEBUG_TRACE', value: 'true', job: build) + end + + it { is_expected.to eq true } + end + + context 'when in yaml variables' do + before do + build.update!(yaml_variables: [{ key: :CI_DEBUG_TRACE, value: 'true' }]) + end + + it { is_expected.to eq true } + end + end + + context 'when CI_DEBUG_TRACE is not in variables' do + it { is_expected.to eq false } + end + end end diff --git a/spec/models/ci/build_trace_chunk_spec.rb b/spec/models/ci/build_trace_chunk_spec.rb index dce7b1d30ca..75ed5939724 100644 --- a/spec/models/ci/build_trace_chunk_spec.rb +++ b/spec/models/ci/build_trace_chunk_spec.rb @@ -91,7 +91,7 @@ RSpec.describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do end describe 'CHUNK_SIZE' do - it 'Chunk size can not be changed without special care' do + it 'chunk size can not be changed without special care' do expect(described_class::CHUNK_SIZE).to eq(128.kilobytes) end end diff --git a/spec/models/ci/build_trace_chunks/fog_spec.rb b/spec/models/ci/build_trace_chunks/fog_spec.rb index 20ca0c8b710..bc96e2584cf 100644 --- a/spec/models/ci/build_trace_chunks/fog_spec.rb +++ b/spec/models/ci/build_trace_chunks/fog_spec.rb @@ -74,6 +74,52 @@ RSpec.describe Ci::BuildTraceChunks::Fog do expect(data_store.data(model)).to eq new_data end + + context 'when S3 server side encryption is enabled' do + before do + config = Gitlab.config.artifacts.object_store.to_h + config[:storage_options] = { server_side_encryption: 'AES256' } + allow(data_store).to receive(:object_store_raw_config).and_return(config) + end + + it 'creates a file with attributes' do + expect_next_instance_of(Fog::AWS::Storage::Files) do |files| + expect(files).to receive(:create).with( + hash_including( + key: anything, + body: new_data, + 'x-amz-server-side-encryption' => 'AES256') + ).and_call_original + end + + expect(data_store.data(model)).to be_nil + + data_store.set_data(model, new_data) + + expect(data_store.data(model)).to eq new_data + end + + context 'when ci_live_trace_use_fog_attributes flag is disabled' do + before do + stub_feature_flags(ci_live_trace_use_fog_attributes: false) + end + + it 'does not pass along Fog attributes' do + expect_next_instance_of(Fog::AWS::Storage::Files) do |files| + expect(files).to receive(:create).with( + key: anything, + body: new_data + ).and_call_original + end + + expect(data_store.data(model)).to be_nil + + data_store.set_data(model, new_data) + + expect(data_store.data(model)).to eq new_data + end + end + end end end diff --git a/spec/models/ci/job_artifact_spec.rb b/spec/models/ci/job_artifact_spec.rb index 26851c93ac3..ef21ca8f100 100644 --- a/spec/models/ci/job_artifact_spec.rb +++ b/spec/models/ci/job_artifact_spec.rb @@ -96,6 +96,22 @@ RSpec.describe Ci::JobArtifact do end end + describe '.codequality_reports' do + subject { described_class.codequality_reports } + + context 'when there is a codequality report' do + let!(:artifact) { create(:ci_job_artifact, :codequality) } + + it { is_expected.to eq([artifact]) } + end + + context 'when there are no codequality reports' do + let!(:artifact) { create(:ci_job_artifact, :archive) } + + it { is_expected.to be_empty } + end + end + describe '.terraform_reports' do context 'when there is a terraform report' do it 'return the job artifact' do diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 1ca370dc950..f5e824bb066 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -222,6 +222,26 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do end end + describe '.for_branch' do + subject { described_class.for_branch(branch) } + + let(:branch) { 'master' } + let!(:pipeline) { create(:ci_pipeline, ref: 'master') } + + it 'returns the pipeline' do + is_expected.to contain_exactly(pipeline) + end + + context 'with tag pipeline' do + let(:branch) { 'v1.0' } + let!(:pipeline) { create(:ci_pipeline, ref: 'v1.0', tag: true) } + + it 'returns nothing' do + is_expected.to be_empty + end + end + end + describe '.ci_sources' do subject { described_class.ci_sources } @@ -242,6 +262,27 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do end end + describe '.ci_branch_sources' do + subject { described_class.ci_branch_sources } + + let_it_be(:push_pipeline) { create(:ci_pipeline, source: :push) } + let_it_be(:web_pipeline) { create(:ci_pipeline, source: :web) } + let_it_be(:api_pipeline) { create(:ci_pipeline, source: :api) } + let_it_be(:webide_pipeline) { create(:ci_pipeline, source: :webide) } + let_it_be(:child_pipeline) { create(:ci_pipeline, source: :parent_pipeline) } + let_it_be(:merge_request_pipeline) { create(:ci_pipeline, :detached_merge_request_pipeline) } + + it 'contains pipelines having CI only sources' do + expect(subject).to contain_exactly(push_pipeline, web_pipeline, api_pipeline) + end + + it 'filters on expected sources' do + expect(::Enums::Ci::Pipeline.ci_branch_sources.keys).to contain_exactly( + *%i[unknown push web trigger schedule api external pipeline chat + external_pull_request_event]) + end + end + describe '.outside_pipeline_family' do subject(:outside_pipeline_family) { described_class.outside_pipeline_family(upstream_pipeline) } @@ -269,7 +310,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do let!(:older_other_pipeline) { create(:ci_pipeline, project: project) } let!(:upstream_pipeline) { create(:ci_pipeline, project: project) } - let!(:child_pipeline) { create(:ci_pipeline, project: project) } + let!(:child_pipeline) { create(:ci_pipeline, child_of: upstream_pipeline) } let!(:other_pipeline) { create(:ci_pipeline, project: project) } @@ -498,6 +539,16 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do end end + context 'when pipeline has a codequality report' do + subject { described_class.with_reports(Ci::JobArtifact.codequality_reports) } + + let(:pipeline_with_report) { create(:ci_pipeline, :with_codequality_reports) } + + it 'selects the pipeline' do + is_expected.to eq([pipeline_with_report]) + end + end + context 'when pipeline has a terraform report' do it 'selects the pipeline' do pipeline_with_report = create(:ci_pipeline, :with_terraform_reports) @@ -744,11 +795,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do ] end - context 'when pipeline is merge request' do - let(:pipeline) do - create(:ci_pipeline, merge_request: merge_request) - end - + context 'when merge request is present' do let(:merge_request) do create(:merge_request, source_project: project, @@ -764,64 +811,142 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do let(:milestone) { create(:milestone, project: project) } let(:labels) { create_list(:label, 2) } - it 'exposes merge request pipeline variables' do - expect(subject.to_hash) - .to include( - 'CI_MERGE_REQUEST_ID' => merge_request.id.to_s, - 'CI_MERGE_REQUEST_IID' => merge_request.iid.to_s, - 'CI_MERGE_REQUEST_REF_PATH' => merge_request.ref_path.to_s, - 'CI_MERGE_REQUEST_PROJECT_ID' => merge_request.project.id.to_s, - 'CI_MERGE_REQUEST_PROJECT_PATH' => merge_request.project.full_path, - 'CI_MERGE_REQUEST_PROJECT_URL' => merge_request.project.web_url, - 'CI_MERGE_REQUEST_TARGET_BRANCH_NAME' => merge_request.target_branch.to_s, - 'CI_MERGE_REQUEST_TARGET_BRANCH_SHA' => pipeline.target_sha.to_s, - 'CI_MERGE_REQUEST_SOURCE_PROJECT_ID' => merge_request.source_project.id.to_s, - 'CI_MERGE_REQUEST_SOURCE_PROJECT_PATH' => merge_request.source_project.full_path, - 'CI_MERGE_REQUEST_SOURCE_PROJECT_URL' => merge_request.source_project.web_url, - 'CI_MERGE_REQUEST_SOURCE_BRANCH_NAME' => merge_request.source_branch.to_s, - 'CI_MERGE_REQUEST_SOURCE_BRANCH_SHA' => pipeline.source_sha.to_s, - 'CI_MERGE_REQUEST_TITLE' => merge_request.title, - 'CI_MERGE_REQUEST_ASSIGNEES' => merge_request.assignee_username_list, - 'CI_MERGE_REQUEST_MILESTONE' => milestone.title, - 'CI_MERGE_REQUEST_LABELS' => labels.map(&:title).sort.join(','), - 'CI_MERGE_REQUEST_EVENT_TYPE' => pipeline.merge_request_event_type.to_s) - end - - context 'when source project does not exist' do + context 'when pipeline for merge request is created' do + let(:pipeline) do + create(:ci_pipeline, :detached_merge_request_pipeline, + ci_ref_presence: false, + user: user, + merge_request: merge_request) + end + before do - merge_request.update_column(:source_project_id, nil) + project.add_developer(user) end - it 'does not expose source project related variables' do - expect(subject.to_hash.keys).not_to include( - %w[CI_MERGE_REQUEST_SOURCE_PROJECT_ID - CI_MERGE_REQUEST_SOURCE_PROJECT_PATH - CI_MERGE_REQUEST_SOURCE_PROJECT_URL - CI_MERGE_REQUEST_SOURCE_BRANCH_NAME]) + it 'exposes merge request pipeline variables' do + expect(subject.to_hash) + .to include( + 'CI_MERGE_REQUEST_ID' => merge_request.id.to_s, + 'CI_MERGE_REQUEST_IID' => merge_request.iid.to_s, + 'CI_MERGE_REQUEST_REF_PATH' => merge_request.ref_path.to_s, + 'CI_MERGE_REQUEST_PROJECT_ID' => merge_request.project.id.to_s, + 'CI_MERGE_REQUEST_PROJECT_PATH' => merge_request.project.full_path, + 'CI_MERGE_REQUEST_PROJECT_URL' => merge_request.project.web_url, + 'CI_MERGE_REQUEST_TARGET_BRANCH_NAME' => merge_request.target_branch.to_s, + 'CI_MERGE_REQUEST_TARGET_BRANCH_SHA' => '', + 'CI_MERGE_REQUEST_SOURCE_PROJECT_ID' => merge_request.source_project.id.to_s, + 'CI_MERGE_REQUEST_SOURCE_PROJECT_PATH' => merge_request.source_project.full_path, + 'CI_MERGE_REQUEST_SOURCE_PROJECT_URL' => merge_request.source_project.web_url, + 'CI_MERGE_REQUEST_SOURCE_BRANCH_NAME' => merge_request.source_branch.to_s, + 'CI_MERGE_REQUEST_SOURCE_BRANCH_SHA' => '', + 'CI_MERGE_REQUEST_TITLE' => merge_request.title, + 'CI_MERGE_REQUEST_ASSIGNEES' => merge_request.assignee_username_list, + 'CI_MERGE_REQUEST_MILESTONE' => milestone.title, + 'CI_MERGE_REQUEST_LABELS' => labels.map(&:title).sort.join(','), + 'CI_MERGE_REQUEST_EVENT_TYPE' => 'detached', + 'CI_OPEN_MERGE_REQUESTS' => merge_request.to_reference(full: true)) + end + + it 'exposes diff variables' do + expect(subject.to_hash) + .to include( + 'CI_MERGE_REQUEST_DIFF_ID' => merge_request.merge_request_diff.id.to_s, + 'CI_MERGE_REQUEST_DIFF_BASE_SHA' => merge_request.merge_request_diff.base_commit_sha) + end + + context 'without assignee' do + let(:assignees) { [] } + + it 'does not expose assignee variable' do + expect(subject.to_hash.keys).not_to include('CI_MERGE_REQUEST_ASSIGNEES') + end end - end - context 'without assignee' do - let(:assignees) { [] } + context 'without milestone' do + let(:milestone) { nil } - it 'does not expose assignee variable' do - expect(subject.to_hash.keys).not_to include('CI_MERGE_REQUEST_ASSIGNEES') + it 'does not expose milestone variable' do + expect(subject.to_hash.keys).not_to include('CI_MERGE_REQUEST_MILESTONE') + end + end + + context 'without labels' do + let(:labels) { [] } + + it 'does not expose labels variable' do + expect(subject.to_hash.keys).not_to include('CI_MERGE_REQUEST_LABELS') + end end end - context 'without milestone' do - let(:milestone) { nil } + context 'when pipeline on branch is created' do + let(:pipeline) do + create(:ci_pipeline, project: project, user: user, ref: 'feature') + end + + context 'when a merge request is created' do + before do + merge_request + end - it 'does not expose milestone variable' do - expect(subject.to_hash.keys).not_to include('CI_MERGE_REQUEST_MILESTONE') + context 'when user has access to project' do + before do + project.add_developer(user) + end + + it 'merge request references are returned matching the pipeline' do + expect(subject.to_hash).to include( + 'CI_OPEN_MERGE_REQUESTS' => merge_request.to_reference(full: true)) + end + end + + context 'when user does not have access to project' do + it 'CI_OPEN_MERGE_REQUESTS is not returned' do + expect(subject.to_hash).not_to have_key('CI_OPEN_MERGE_REQUESTS') + end + end + end + + context 'when no a merge request is created' do + it 'CI_OPEN_MERGE_REQUESTS is not returned' do + expect(subject.to_hash).not_to have_key('CI_OPEN_MERGE_REQUESTS') + end end end - context 'without labels' do - let(:labels) { [] } + context 'with merged results' do + let(:pipeline) do + create(:ci_pipeline, :merged_result_pipeline, merge_request: merge_request) + end + + it 'exposes merge request pipeline variables' do + expect(subject.to_hash) + .to include( + 'CI_MERGE_REQUEST_ID' => merge_request.id.to_s, + 'CI_MERGE_REQUEST_IID' => merge_request.iid.to_s, + 'CI_MERGE_REQUEST_REF_PATH' => merge_request.ref_path.to_s, + 'CI_MERGE_REQUEST_PROJECT_ID' => merge_request.project.id.to_s, + 'CI_MERGE_REQUEST_PROJECT_PATH' => merge_request.project.full_path, + 'CI_MERGE_REQUEST_PROJECT_URL' => merge_request.project.web_url, + 'CI_MERGE_REQUEST_TARGET_BRANCH_NAME' => merge_request.target_branch.to_s, + 'CI_MERGE_REQUEST_TARGET_BRANCH_SHA' => merge_request.target_branch_sha, + 'CI_MERGE_REQUEST_SOURCE_PROJECT_ID' => merge_request.source_project.id.to_s, + 'CI_MERGE_REQUEST_SOURCE_PROJECT_PATH' => merge_request.source_project.full_path, + 'CI_MERGE_REQUEST_SOURCE_PROJECT_URL' => merge_request.source_project.web_url, + 'CI_MERGE_REQUEST_SOURCE_BRANCH_NAME' => merge_request.source_branch.to_s, + 'CI_MERGE_REQUEST_SOURCE_BRANCH_SHA' => merge_request.source_branch_sha, + 'CI_MERGE_REQUEST_TITLE' => merge_request.title, + 'CI_MERGE_REQUEST_ASSIGNEES' => merge_request.assignee_username_list, + 'CI_MERGE_REQUEST_MILESTONE' => milestone.title, + 'CI_MERGE_REQUEST_LABELS' => labels.map(&:title).sort.join(','), + 'CI_MERGE_REQUEST_EVENT_TYPE' => 'merged_result') + end - it 'does not expose labels variable' do - expect(subject.to_hash.keys).not_to include('CI_MERGE_REQUEST_LABELS') + it 'exposes diff variables' do + expect(subject.to_hash) + .to include( + 'CI_MERGE_REQUEST_DIFF_ID' => merge_request.merge_request_diff.id.to_s, + 'CI_MERGE_REQUEST_DIFF_BASE_SHA' => merge_request.merge_request_diff.base_commit_sha) end end end @@ -1126,6 +1251,40 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do end end + describe 'synching status to Jira' do + let(:worker) { ::JiraConnect::SyncBuildsWorker } + + %i[prepare! run! skip! drop! succeed! cancel! block! delay!].each do |event| + context "when we call pipeline.#{event}" do + it 'triggers a Jira synch worker' do + expect(worker).to receive(:perform_async).with(pipeline.id, Integer) + + pipeline.send(event) + end + + context 'the feature is disabled' do + it 'does not trigger a worker' do + stub_feature_flags(jira_sync_builds: false) + + expect(worker).not_to receive(:perform_async) + + pipeline.send(event) + end + end + + context 'the feature is enabled for this project' do + it 'does trigger a worker' do + stub_feature_flags(jira_sync_builds: pipeline.project) + + expect(worker).to receive(:perform_async) + + pipeline.send(event) + end + end + end + end + end + describe '#duration', :sidekiq_inline do context 'when multiple builds are finished' do before do @@ -2539,6 +2698,14 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do it 'receives a pending event once' do expect(WebMock).to have_requested_pipeline_hook('pending').once end + + it 'builds hook data once' do + create(:pipelines_email_service, project: project) + + expect(Gitlab::DataBuilder::Pipeline).to receive(:build).once.and_call_original + + pipeline.execute_hooks + end end context 'when build is run' do @@ -2600,6 +2767,12 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do it 'did not execute pipeline_hook after touched' do expect(WebMock).not_to have_requested(:post, hook.url) end + + it 'does not build hook data' do + expect(Gitlab::DataBuilder::Pipeline).not_to receive(:build) + + pipeline.execute_hooks + end end def create_build(name, stage_idx) @@ -2734,6 +2907,93 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do end end + describe '#related_merge_requests' do + let(:project) { create(:project, :repository) } + let(:merge_request) { create(:merge_request, source_project: project, source_branch: 'feature', target_branch: 'master') } + let(:other_merge_request) { create(:merge_request, source_project: project, source_branch: 'feature', target_branch: 'stable') } + let(:branch_pipeline) { create(:ci_pipeline, project: project, ref: 'feature') } + let(:merge_pipeline) { create(:ci_pipeline, :detached_merge_request_pipeline, merge_request: merge_request) } + + context 'for a branch pipeline' do + subject { branch_pipeline.related_merge_requests } + + it 'when no merge request is created' do + is_expected.to be_empty + end + + it 'when another merge requests are created' do + merge_request + other_merge_request + + is_expected.to contain_exactly(merge_request, other_merge_request) + end + end + + context 'for a merge pipeline' do + subject { merge_pipeline.related_merge_requests } + + it 'when only merge pipeline is created' do + merge_pipeline + + is_expected.to contain_exactly(merge_request) + end + + it 'when a merge request is created' do + merge_pipeline + other_merge_request + + is_expected.to contain_exactly(merge_request, other_merge_request) + end + end + end + + describe '#open_merge_requests_refs' do + let(:project) { create(:project) } + let(:user) { create(:user) } + let!(:pipeline) { create(:ci_pipeline, user: user, project: project, ref: 'feature') } + let!(:merge_request) { create(:merge_request, source_project: project, source_branch: 'feature', target_branch: 'master') } + + subject { pipeline.open_merge_requests_refs } + + context 'when user is a developer' do + before do + project.add_developer(user) + end + + it 'returns open merge requests' do + is_expected.to eq([merge_request.to_reference(full: true)]) + end + + it 'does not return closed merge requests' do + merge_request.close! + + is_expected.to be_empty + end + + context 'limits amount of returned merge requests' do + let!(:other_merge_requests) do + Array.new(4) do |idx| + create(:merge_request, source_project: project, source_branch: 'feature', target_branch: "master-#{idx}") + end + end + + let(:other_merge_requests_refs) do + other_merge_requests.map { |mr| mr.to_reference(full: true) } + end + + it 'returns only last 4 in a reverse order' do + is_expected.to eq(other_merge_requests_refs.reverse) + end + end + end + + context 'when user does not have permissions' do + it 'does not return any merge requests' do + is_expected.to be_empty + end + end + end + describe '#same_family_pipeline_ids' do subject { pipeline.same_family_pipeline_ids.map(&:id) } @@ -2744,13 +3004,9 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do end context 'when pipeline is child' do - let(:parent) { create(:ci_pipeline, project: pipeline.project) } - let(:sibling) { create(:ci_pipeline, project: pipeline.project) } - - before do - create_source_pipeline(parent, pipeline) - create_source_pipeline(parent, sibling) - end + let(:parent) { create(:ci_pipeline, project: project) } + let!(:pipeline) { create(:ci_pipeline, child_of: parent) } + let!(:sibling) { create(:ci_pipeline, child_of: parent) } it 'returns parent sibling and self ids' do expect(subject).to contain_exactly(parent.id, pipeline.id, sibling.id) @@ -2758,11 +3014,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do end context 'when pipeline is parent' do - let(:child) { create(:ci_pipeline, project: pipeline.project) } - - before do - create_source_pipeline(pipeline, child) - end + let!(:child) { create(:ci_pipeline, child_of: pipeline) } it 'returns self and child ids' do expect(subject).to contain_exactly(pipeline.id, child.id) @@ -2770,17 +3022,11 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do end context 'when pipeline is a child of a child pipeline' do - let(:ancestor) { create(:ci_pipeline, project: pipeline.project) } - let(:parent) { create(:ci_pipeline, project: pipeline.project) } - let(:cousin_parent) { create(:ci_pipeline, project: pipeline.project) } - let(:cousin) { create(:ci_pipeline, project: pipeline.project) } - - before do - create_source_pipeline(ancestor, parent) - create_source_pipeline(ancestor, cousin_parent) - create_source_pipeline(parent, pipeline) - create_source_pipeline(cousin_parent, cousin) - end + let(:ancestor) { create(:ci_pipeline, project: project) } + let!(:parent) { create(:ci_pipeline, child_of: ancestor) } + let!(:pipeline) { create(:ci_pipeline, child_of: parent) } + let!(:cousin_parent) { create(:ci_pipeline, child_of: ancestor) } + let!(:cousin) { create(:ci_pipeline, child_of: cousin_parent) } it 'returns all family ids' do expect(subject).to contain_exactly( @@ -2790,11 +3036,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do end context 'when pipeline is a triggered pipeline' do - let(:upstream) { create(:ci_pipeline, project: create(:project)) } - - before do - create_source_pipeline(upstream, pipeline) - end + let!(:upstream) { create(:ci_pipeline, project: create(:project), upstream_of: pipeline)} it 'returns self id' do expect(subject).to contain_exactly(pipeline.id) @@ -2802,6 +3044,46 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do end end + describe '#root_ancestor' do + subject { pipeline.root_ancestor } + + let_it_be(:pipeline) { create(:ci_pipeline, project: project) } + + context 'when pipeline is child of child pipeline' do + let!(:root_ancestor) { create(:ci_pipeline, project: project) } + let!(:parent_pipeline) { create(:ci_pipeline, child_of: root_ancestor) } + let!(:pipeline) { create(:ci_pipeline, child_of: parent_pipeline) } + + it 'returns the root ancestor' do + expect(subject).to eq(root_ancestor) + end + end + + context 'when pipeline is root ancestor' do + let!(:child_pipeline) { create(:ci_pipeline, child_of: pipeline) } + + it 'returns itself' do + expect(subject).to eq(pipeline) + end + end + + context 'when pipeline is standalone' do + it 'returns itself' do + expect(subject).to eq(pipeline) + end + end + + context 'when pipeline is multi-project downstream pipeline' do + let!(:upstream_pipeline) do + create(:ci_pipeline, project: create(:project), upstream_of: pipeline) + end + + it 'ignores cross project ancestors' do + expect(subject).to eq(pipeline) + end + end + end + describe '#stuck?' do before do create(:ci_build, :pending, pipeline: pipeline) @@ -2838,7 +3120,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do stub_feature_flags(ci_store_pipeline_messages: false) end - it ' does not add pipeline error message' do + it 'does not add pipeline error message' do pipeline.add_error_message('The error message') expect(pipeline.messages).to be_empty @@ -3343,6 +3625,16 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do ]) end + it 'does not execute N+1 queries' do + single_build_pipeline = create(:ci_empty_pipeline, status: :created, project: project) + single_rspec = create(:ci_build, :success, name: 'rspec', pipeline: single_build_pipeline, project: project) + create(:ci_job_artifact, :cobertura, job: single_rspec, project: project) + + control = ActiveRecord::QueryRecorder.new { single_build_pipeline.coverage_reports } + + expect { subject }.not_to exceed_query_limit(control) + end + context 'when builds are retried' do let!(:build_rspec) { create(:ci_build, :retried, :success, name: 'rspec', pipeline: pipeline, project: project) } let!(:build_golang) { create(:ci_build, :retried, :success, name: 'golang', pipeline: pipeline, project: project) } @@ -3360,6 +3652,39 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do end end + describe '#codequality_reports' do + subject(:codequality_reports) { pipeline.codequality_reports } + + context 'when pipeline has multiple builds with codequality reports' do + let(:build_rspec) { create(:ci_build, :success, name: 'rspec', pipeline: pipeline, project: project) } + let(:build_golang) { create(:ci_build, :success, name: 'golang', pipeline: pipeline, project: project) } + + before do + create(:ci_job_artifact, :codequality, job: build_rspec, project: project) + create(:ci_job_artifact, :codequality_without_errors, job: build_golang, project: project) + end + + it 'returns codequality report with collected data' do + expect(codequality_reports.degradations_count).to eq(3) + end + + context 'when builds are retried' do + let(:build_rspec) { create(:ci_build, :retried, :success, name: 'rspec', pipeline: pipeline, project: project) } + let(:build_golang) { create(:ci_build, :retried, :success, name: 'golang', pipeline: pipeline, project: project) } + + it 'returns a codequality reports without degradations' do + expect(codequality_reports.degradations).to be_empty + end + end + end + + context 'when pipeline does not have any builds with codequality reports' do + it 'returns codequality reports without degradations' do + expect(codequality_reports.degradations).to be_empty + end + end + end + describe '#total_size' do let!(:build_job1) { create(:ci_build, pipeline: pipeline, stage_idx: 0) } let!(:build_job2) { create(:ci_build, pipeline: pipeline, stage_idx: 0) } @@ -3509,18 +3834,9 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do describe '#parent_pipeline' do let_it_be(:project) { create(:project) } - let(:pipeline) { create(:ci_pipeline, project: project) } - context 'when pipeline is triggered by a pipeline from the same project' do - let(:upstream_pipeline) { create(:ci_pipeline, project: pipeline.project) } - - before do - create(:ci_sources_pipeline, - source_pipeline: upstream_pipeline, - source_project: project, - pipeline: pipeline, - project: project) - end + let_it_be(:upstream_pipeline) { create(:ci_pipeline, project: project) } + let_it_be(:pipeline) { create(:ci_pipeline, child_of: upstream_pipeline) } it 'returns the parent pipeline' do expect(pipeline.parent_pipeline).to eq(upstream_pipeline) @@ -3532,15 +3848,8 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do end context 'when pipeline is triggered by a pipeline from another project' do - let(:upstream_pipeline) { create(:ci_pipeline) } - - before do - create(:ci_sources_pipeline, - source_pipeline: upstream_pipeline, - source_project: upstream_pipeline.project, - pipeline: pipeline, - project: project) - end + let(:pipeline) { create(:ci_pipeline, project: project) } + let!(:upstream_pipeline) { create(:ci_pipeline, project: create(:project), upstream_of: pipeline) } it 'returns nil' do expect(pipeline.parent_pipeline).to be_nil @@ -3552,6 +3861,8 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do end context 'when pipeline is not triggered by a pipeline' do + let_it_be(:pipeline) { create(:ci_pipeline) } + it 'returns nil' do expect(pipeline.parent_pipeline).to be_nil end @@ -3851,4 +4162,70 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do bridge end end + + describe 'test failure history processing' do + it 'performs the service asynchronously when the pipeline is completed' do + service = double + + expect(Ci::TestFailureHistoryService).to receive(:new).with(pipeline).and_return(service) + expect(service).to receive_message_chain(:async, :perform_if_needed) + + pipeline.succeed! + end + end + + describe '#latest_test_report_builds' do + it 'returns pipeline builds with test report artifacts' do + test_build = create(:ci_build, :test_reports, pipeline: pipeline, project: project) + create(:ci_build, :artifacts, pipeline: pipeline, project: project) + + expect(pipeline.latest_test_report_builds).to contain_exactly(test_build) + end + + it 'preloads project on each build to avoid N+1 queries' do + create(:ci_build, :test_reports, pipeline: pipeline, project: project) + + control_count = ActiveRecord::QueryRecorder.new do + pipeline.latest_test_report_builds.map(&:project).map(&:full_path) + end + + multi_build_pipeline = create(:ci_empty_pipeline, status: :created, project: project) + create(:ci_build, :test_reports, pipeline: multi_build_pipeline, project: project) + create(:ci_build, :test_reports, pipeline: multi_build_pipeline, project: project) + + expect { multi_build_pipeline.latest_test_report_builds.map(&:project).map(&:full_path) } + .not_to exceed_query_limit(control_count) + end + end + + describe '#builds_with_failed_tests' do + it 'returns pipeline builds with test report artifacts' do + failed_build = create(:ci_build, :failed, :test_reports, pipeline: pipeline, project: project) + create(:ci_build, :success, :test_reports, pipeline: pipeline, project: project) + + expect(pipeline.builds_with_failed_tests).to contain_exactly(failed_build) + end + + it 'supports limiting the number of builds to fetch' do + create(:ci_build, :failed, :test_reports, pipeline: pipeline, project: project) + create(:ci_build, :failed, :test_reports, pipeline: pipeline, project: project) + + expect(pipeline.builds_with_failed_tests(limit: 1).count).to eq(1) + end + + it 'preloads project on each build to avoid N+1 queries' do + create(:ci_build, :failed, :test_reports, pipeline: pipeline, project: project) + + control_count = ActiveRecord::QueryRecorder.new do + pipeline.builds_with_failed_tests.map(&:project).map(&:full_path) + end + + multi_build_pipeline = create(:ci_empty_pipeline, status: :created, project: project) + create(:ci_build, :failed, :test_reports, pipeline: multi_build_pipeline, project: project) + create(:ci_build, :failed, :test_reports, pipeline: multi_build_pipeline, project: project) + + expect { multi_build_pipeline.builds_with_failed_tests.map(&:project).map(&:full_path) } + .not_to exceed_query_limit(control_count) + end + end end |