diff options
Diffstat (limited to 'spec/models/ci/pipeline_spec.rb')
-rw-r--r-- | spec/models/ci/pipeline_spec.rb | 473 |
1 files changed, 412 insertions, 61 deletions
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index b4e80fa7588..228a1e8f7a2 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -2,12 +2,14 @@ require 'spec_helper' -RSpec.describe Ci::Pipeline, :mailer do +RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do include ProjectForksHelper include StubRequests + include Ci::SourcePipelineHelpers - let(:user) { create(:user) } - let_it_be(:project) { create(:project) } + let_it_be(:user) { create(:user) } + let_it_be(:namespace) { create_default(:namespace) } + let_it_be(:project) { create_default(:project, :repository) } let(:pipeline) do create(:ci_empty_pipeline, status: :created, project: project) @@ -220,6 +222,26 @@ RSpec.describe Ci::Pipeline, :mailer do end end + describe '.ci_sources' do + subject { described_class.ci_sources } + + let!(:push_pipeline) { create(:ci_pipeline, source: :push) } + let!(:web_pipeline) { create(:ci_pipeline, source: :web) } + let!(:api_pipeline) { create(:ci_pipeline, source: :api) } + let!(:webide_pipeline) { create(:ci_pipeline, source: :webide) } + let!(:child_pipeline) { create(:ci_pipeline, source: :parent_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_sources.keys).to contain_exactly( + *%i[unknown push web trigger schedule api external pipeline chat + merge_request_event external_pull_request_event]) + end + end + describe '.outside_pipeline_family' do subject(:outside_pipeline_family) { described_class.outside_pipeline_family(upstream_pipeline) } @@ -714,6 +736,7 @@ RSpec.describe Ci::Pipeline, :mailer do CI_COMMIT_TITLE CI_COMMIT_DESCRIPTION CI_COMMIT_REF_PROTECTED + CI_COMMIT_TIMESTAMP CI_BUILD_REF CI_BUILD_BEFORE_SHA CI_BUILD_REF_NAME @@ -879,7 +902,7 @@ RSpec.describe Ci::Pipeline, :mailer do context 'when there is auto_canceled_by' do before do - pipeline.update(auto_canceled_by: create(:ci_empty_pipeline)) + pipeline.update!(auto_canceled_by: create(:ci_empty_pipeline)) end it 'is auto canceled' do @@ -1237,14 +1260,42 @@ RSpec.describe Ci::Pipeline, :mailer do end describe 'pipeline caching' do - before do - pipeline.config_source = 'repository_source' + context 'if pipeline is cacheable' do + before do + pipeline.source = 'push' + end + + it 'performs ExpirePipelinesCacheWorker' do + expect(ExpirePipelineCacheWorker).to receive(:perform_async).with(pipeline.id) + + pipeline.cancel + end end - it 'performs ExpirePipelinesCacheWorker' do - expect(ExpirePipelineCacheWorker).to receive(:perform_async).with(pipeline.id) + context 'if pipeline is not cacheable' do + before do + pipeline.source = 'webide' + end - pipeline.cancel + it 'deos not perform ExpirePipelinesCacheWorker' do + expect(ExpirePipelineCacheWorker).not_to receive(:perform_async) + + pipeline.cancel + end + end + end + + describe '#dangling?' do + it 'returns true if pipeline comes from any dangling sources' do + pipeline.source = Enums::Ci::Pipeline.dangling_sources.each_key.first + + expect(pipeline).to be_dangling + end + + it 'returns true if pipeline comes from any CI sources' do + pipeline.source = Enums::Ci::Pipeline.ci_sources.each_key.first + + expect(pipeline).not_to be_dangling end end @@ -1309,34 +1360,126 @@ RSpec.describe Ci::Pipeline, :mailer do end end - context 'when pipeline is bridge triggered' do - before do - pipeline.source_bridge = create(:ci_bridge) - end + def auto_devops_pipelines_completed_total(status) + Gitlab::Metrics.counter(:auto_devops_pipelines_completed_total, 'Number of completed auto devops pipelines').get(status: status) + end + end + + describe 'bridge triggered pipeline' do + shared_examples 'upstream downstream pipeline' do + let!(:source_pipeline) { create(:ci_sources_pipeline, pipeline: downstream_pipeline, source_job: bridge) } + let!(:job) { downstream_pipeline.builds.first } context 'when source bridge is dependent on pipeline status' do - before do - allow(pipeline.source_bridge).to receive(:dependent?).and_return(true) - end + let!(:bridge) { create(:ci_bridge, :strategy_depend, pipeline: upstream_pipeline) } it 'schedules the pipeline bridge worker' do - expect(::Ci::PipelineBridgeStatusWorker).to receive(:perform_async) + expect(::Ci::PipelineBridgeStatusWorker).to receive(:perform_async).with(downstream_pipeline.id) + + downstream_pipeline.succeed! + end + + context 'when the downstream pipeline first fails then retries and succeeds' do + it 'makes the upstream pipeline successful' do + Sidekiq::Testing.inline! { job.drop! } + + expect(downstream_pipeline.reload).to be_failed + expect(upstream_pipeline.reload).to be_failed + + Sidekiq::Testing.inline! do + new_job = Ci::Build.retry(job, project.users.first) + + expect(downstream_pipeline.reload).to be_running + expect(upstream_pipeline.reload).to be_running + + new_job.success! + end + + expect(downstream_pipeline.reload).to be_success + expect(upstream_pipeline.reload).to be_success + end + end + + context 'when the downstream pipeline first succeeds then retries and fails' do + it 'makes the upstream pipeline failed' do + Sidekiq::Testing.inline! { job.success! } + + expect(downstream_pipeline.reload).to be_success + expect(upstream_pipeline.reload).to be_success + + Sidekiq::Testing.inline! do + new_job = Ci::Build.retry(job, project.users.first) + + expect(downstream_pipeline.reload).to be_running + expect(upstream_pipeline.reload).to be_running + + new_job.drop! + end + + expect(downstream_pipeline.reload).to be_failed + expect(upstream_pipeline.reload).to be_failed + end + end + + context 'when the upstream pipeline has another dependent upstream pipeline' do + let!(:upstream_of_upstream_pipeline) { create(:ci_pipeline) } + + before do + upstream_bridge = create(:ci_bridge, :strategy_depend, pipeline: upstream_of_upstream_pipeline) + create(:ci_sources_pipeline, pipeline: upstream_pipeline, + source_job: upstream_bridge) + end + + context 'when the downstream pipeline first fails then retries and succeeds' do + it 'makes upstream pipelines successful' do + Sidekiq::Testing.inline! { job.drop! } + + expect(downstream_pipeline.reload).to be_failed + expect(upstream_pipeline.reload).to be_failed + expect(upstream_of_upstream_pipeline.reload).to be_failed - pipeline.succeed! + Sidekiq::Testing.inline! do + new_job = Ci::Build.retry(job, project.users.first) + + expect(downstream_pipeline.reload).to be_running + expect(upstream_pipeline.reload).to be_running + expect(upstream_of_upstream_pipeline.reload).to be_running + + new_job.success! + end + + expect(downstream_pipeline.reload).to be_success + expect(upstream_pipeline.reload).to be_success + expect(upstream_of_upstream_pipeline.reload).to be_success + end + end end end context 'when source bridge is not dependent on pipeline status' do + let!(:bridge) { create(:ci_bridge, pipeline: upstream_pipeline) } + it 'does not schedule the pipeline bridge worker' do expect(::Ci::PipelineBridgeStatusWorker).not_to receive(:perform_async) - pipeline.succeed! + downstream_pipeline.succeed! end end end - def auto_devops_pipelines_completed_total(status) - Gitlab::Metrics.counter(:auto_devops_pipelines_completed_total, 'Number of completed auto devops pipelines').get(status: status) + context 'multi-project pipelines' do + let!(:downstream_project) { create(:project, :repository) } + let!(:upstream_pipeline) { create(:ci_pipeline, project: project) } + let!(:downstream_pipeline) { create(:ci_pipeline, :with_job, project: downstream_project) } + + it_behaves_like 'upstream downstream pipeline' + end + + context 'parent-child pipelines' do + let!(:upstream_pipeline) { create(:ci_pipeline, project: project) } + let!(:downstream_pipeline) { create(:ci_pipeline, :with_job, project: project) } + + it_behaves_like 'upstream downstream pipeline' end end @@ -1436,8 +1579,6 @@ RSpec.describe Ci::Pipeline, :mailer do context 'when repository exists' do using RSpec::Parameterized::TableSyntax - let(:project) { create(:project, :repository) } - where(:tag, :ref, :result) do false | 'master' | true false | 'non-existent-branch' | false @@ -1457,6 +1598,7 @@ RSpec.describe Ci::Pipeline, :mailer do end context 'when repository does not exist' do + let(:project) { create(:project) } let(:pipeline) do create(:ci_empty_pipeline, project: project, ref: 'master') end @@ -1468,8 +1610,6 @@ RSpec.describe Ci::Pipeline, :mailer do end context 'with non-empty project' do - let(:project) { create(:project, :repository) } - let(:pipeline) do create(:ci_pipeline, project: project, @@ -1524,7 +1664,7 @@ RSpec.describe Ci::Pipeline, :mailer do it 'looks up a commit for a tag' do expect(project.repository.branch_names).not_to include 'v1.0.0' - pipeline.update(sha: project.commit('v1.0.0').sha, ref: 'v1.0.0', tag: true) + pipeline.update!(sha: project.commit('v1.0.0').sha, ref: 'v1.0.0', tag: true) expect(pipeline).to be_tag expect(pipeline).to be_latest @@ -1533,7 +1673,7 @@ RSpec.describe Ci::Pipeline, :mailer do context 'with not latest sha' do before do - pipeline.update(sha: project.commit("#{project.default_branch}~1").sha) + pipeline.update!(sha: project.commit("#{project.default_branch}~1").sha) end it 'returns false' do @@ -1561,7 +1701,7 @@ RSpec.describe Ci::Pipeline, :mailer do let!(:manual2) { create(:ci_build, :manual, pipeline: pipeline, name: 'deploy') } before do - manual.update(retried: true) + manual.update!(retried: true) end it 'returns latest one' do @@ -1596,10 +1736,8 @@ RSpec.describe Ci::Pipeline, :mailer do describe '#modified_paths' do context 'when old and new revisions are set' do - let(:project) { create(:project, :repository) } - before do - pipeline.update(before_sha: '1234abcd', sha: '2345bcde') + pipeline.update!(before_sha: '1234abcd', sha: '2345bcde') end it 'fetches stats for changes between commits' do @@ -1866,8 +2004,6 @@ RSpec.describe Ci::Pipeline, :mailer do end describe '.latest_pipeline_per_commit' do - let(:project) { create(:project) } - let!(:commit_123_ref_master) do create( :ci_empty_pipeline, @@ -1962,15 +2098,14 @@ RSpec.describe Ci::Pipeline, :mailer do end describe '.last_finished_for_ref_id' do - let(:project) { create(:project, :repository) } let(:branch) { project.default_branch } let(:ref) { project.ci_refs.take } - let(:config_source) { Ci::PipelineEnums.config_sources[:parameter_source] } + let(:dangling_source) { Enums::Ci::Pipeline.sources[:ondemand_dast_scan] } let!(:pipeline1) { create(:ci_pipeline, :success, project: project, ref: branch) } let!(:pipeline2) { create(:ci_pipeline, :success, project: project, ref: branch) } let!(:pipeline3) { create(:ci_pipeline, :failed, project: project, ref: branch) } let!(:pipeline4) { create(:ci_pipeline, :success, project: project, ref: branch) } - let!(:pipeline5) { create(:ci_pipeline, :success, project: project, ref: branch, config_source: config_source) } + let!(:pipeline5) { create(:ci_pipeline, :success, project: project, ref: branch, source: dangling_source) } it 'returns the expected pipeline' do result = described_class.last_finished_for_ref_id(ref.id) @@ -2452,7 +2587,6 @@ RSpec.describe Ci::Pipeline, :mailer do end describe "#merge_requests_as_head_pipeline" do - let(:project) { create(:project) } let(:pipeline) { create(:ci_empty_pipeline, status: 'created', project: project, ref: 'master', sha: 'a288a022a53a5a944fae87bcec6efc87b7061808') } it "returns merge requests whose `diff_head_sha` matches the pipeline's SHA" do @@ -2575,11 +2709,11 @@ RSpec.describe Ci::Pipeline, :mailer do end describe '#same_family_pipeline_ids' do - subject(:same_family_pipeline_ids) { pipeline.same_family_pipeline_ids } + subject { pipeline.same_family_pipeline_ids.map(&:id) } context 'when pipeline is not child nor parent' do it 'returns just the pipeline id' do - expect(same_family_pipeline_ids).to contain_exactly(pipeline.id) + expect(subject).to contain_exactly(pipeline.id) end end @@ -2588,21 +2722,12 @@ RSpec.describe Ci::Pipeline, :mailer do let(:sibling) { create(:ci_pipeline, project: pipeline.project) } before do - create(:ci_sources_pipeline, - source_job: create(:ci_build, pipeline: parent), - source_project: parent.project, - pipeline: pipeline, - project: pipeline.project) - - create(:ci_sources_pipeline, - source_job: create(:ci_build, pipeline: parent), - source_project: parent.project, - pipeline: sibling, - project: sibling.project) + create_source_pipeline(parent, pipeline) + create_source_pipeline(parent, sibling) end it 'returns parent sibling and self ids' do - expect(same_family_pipeline_ids).to contain_exactly(parent.id, pipeline.id, sibling.id) + expect(subject).to contain_exactly(parent.id, pipeline.id, sibling.id) end end @@ -2610,15 +2735,43 @@ RSpec.describe Ci::Pipeline, :mailer do let(:child) { create(:ci_pipeline, project: pipeline.project) } before do - create(:ci_sources_pipeline, - source_job: create(:ci_build, pipeline: pipeline), - source_project: pipeline.project, - pipeline: child, - project: child.project) + create_source_pipeline(pipeline, child) end it 'returns self and child ids' do - expect(same_family_pipeline_ids).to contain_exactly(pipeline.id, child.id) + expect(subject).to contain_exactly(pipeline.id, child.id) + end + 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 + + it 'returns all family ids' do + expect(subject).to contain_exactly( + ancestor.id, parent.id, cousin_parent.id, cousin.id, pipeline.id + ) + end + 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 + + it 'returns self id' do + expect(subject).to contain_exactly(pipeline.id) end end end @@ -2685,7 +2838,8 @@ RSpec.describe Ci::Pipeline, :mailer do end describe 'notifications when pipeline success or failed' do - let(:project) { create(:project, :repository) } + let(:namespace) { create(:namespace) } + let(:project) { create(:project, :repository, namespace: namespace) } let(:pipeline) do create(:ci_pipeline, @@ -2698,7 +2852,7 @@ RSpec.describe Ci::Pipeline, :mailer do project.add_developer(pipeline.user) pipeline.user.global_notification_setting - .update(level: 'custom', failed_pipeline: true, success_pipeline: true) + .update!(level: 'custom', failed_pipeline: true, success_pipeline: true) perform_enqueued_jobs do pipeline.enqueue @@ -2948,6 +3102,54 @@ RSpec.describe Ci::Pipeline, :mailer do end end + describe '#has_coverage_reports?' do + subject { pipeline.has_coverage_reports? } + + context 'when pipeline has a code coverage artifact' do + let(:pipeline) { create(:ci_pipeline, :with_coverage_report_artifact, :running, project: project) } + + it { expect(subject).to be_truthy } + end + + context 'when pipeline does not have a code coverage artifact' do + let(:pipeline) { create(:ci_pipeline, :success, project: project) } + + it { expect(subject).to be_falsey } + end + end + + describe '#can_generate_coverage_reports?' do + subject { pipeline.can_generate_coverage_reports? } + + context 'when pipeline has builds with coverage reports' do + before do + create(:ci_build, :coverage_reports, pipeline: pipeline, project: project) + end + + context 'when pipeline status is running' do + let(:pipeline) { create(:ci_pipeline, :running, project: project) } + + it { expect(subject).to be_falsey } + end + + context 'when pipeline status is success' do + let(:pipeline) { create(:ci_pipeline, :success, project: project) } + + it { expect(subject).to be_truthy } + end + end + + context 'when pipeline does not have builds with coverage reports' do + before do + create(:ci_build, :artifacts, pipeline: pipeline, project: project) + end + + let(:pipeline) { create(:ci_pipeline, :success, project: project) } + + it { expect(subject).to be_falsey } + end + end + describe '#test_report_summary' do subject { pipeline.test_report_summary } @@ -3228,7 +3430,8 @@ RSpec.describe Ci::Pipeline, :mailer do end describe '#parent_pipeline' do - let(:project) { create(:project) } + 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 @@ -3283,7 +3486,7 @@ RSpec.describe Ci::Pipeline, :mailer do end describe '#child_pipelines' do - let(:project) { create(:project) } + let_it_be(:project) { create(:project) } let(:pipeline) { create(:ci_pipeline, project: project) } context 'when pipeline triggered other pipelines on same project' do @@ -3413,4 +3616,152 @@ RSpec.describe Ci::Pipeline, :mailer do it { is_expected.to eq(Gitlab::Git::TAG_REF_PREFIX + pipeline.source_ref.to_s) } end end + + describe "#builds_with_coverage" do + it 'returns builds with coverage only' do + rspec = create(:ci_build, name: 'rspec', coverage: 97.1, pipeline: pipeline) + jest = create(:ci_build, name: 'jest', coverage: 94.1, pipeline: pipeline) + karma = create(:ci_build, name: 'karma', coverage: nil, pipeline: pipeline) + + builds = pipeline.builds_with_coverage + + expect(builds).to include(rspec, jest) + expect(builds).not_to include(karma) + end + end + + describe '#base_and_ancestors' do + let(:same_project) { false } + + subject { pipeline.base_and_ancestors(same_project: same_project) } + + context 'when pipeline is not child nor parent' do + it 'returns just the pipeline itself' do + expect(subject).to contain_exactly(pipeline) + end + 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 + + it 'returns parent and self' do + expect(subject).to contain_exactly(parent, pipeline) + end + end + + context 'when pipeline is parent' do + let(:child) { create(:ci_pipeline, project: pipeline.project) } + + before do + create_source_pipeline(pipeline, child) + end + + it 'returns self' do + expect(subject).to contain_exactly(pipeline) + end + 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) } + + before do + create_source_pipeline(ancestor, parent) + create_source_pipeline(parent, pipeline) + end + + it 'returns self, parent and ancestor' do + expect(subject).to contain_exactly(ancestor, parent, pipeline) + end + 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 + + context 'same_project: false' do + it 'returns upstream and self' do + expect(subject).to contain_exactly(pipeline, upstream) + end + end + + context 'same_project: true' do + let(:same_project) { true } + + it 'returns self' do + expect(subject).to contain_exactly(pipeline) + end + end + end + end + + describe 'reset_ancestor_bridges!' do + context 'when the pipeline is a child pipeline and the bridge is depended' do + let!(:parent_pipeline) { create(:ci_pipeline, project: project) } + let!(:bridge) { create_bridge(parent_pipeline, pipeline, true) } + + it 'marks source bridge as pending' do + pipeline.reset_ancestor_bridges! + + expect(bridge.reload).to be_pending + end + + context 'when the parent pipeline has a dependent upstream pipeline' do + let!(:upstream_bridge) do + create_bridge(create(:ci_pipeline, project: create(:project)), parent_pipeline, true) + end + + it 'marks all source bridges as pending' do + pipeline.reset_ancestor_bridges! + + expect(bridge.reload).to be_pending + expect(upstream_bridge.reload).to be_pending + end + end + end + + context 'when the pipeline is a child pipeline and the bridge is not depended' do + let!(:parent_pipeline) { create(:ci_pipeline, project: project) } + let!(:bridge) { create_bridge(parent_pipeline, pipeline, false) } + + it 'does not touch source bridge' do + pipeline.reset_ancestor_bridges! + + expect(bridge.reload).to be_success + end + + context 'when the parent pipeline has a dependent upstream pipeline' do + let!(:upstream_bridge) do + create_bridge(create(:ci_pipeline, project: create(:project)), parent_pipeline, true) + end + + it 'does not touch any source bridge' do + pipeline.reset_ancestor_bridges! + + expect(bridge.reload).to be_success + expect(upstream_bridge.reload).to be_success + end + end + end + + private + + def create_bridge(upstream, downstream, depend = false) + options = depend ? { trigger: { strategy: 'depend' } } : {} + + bridge = create(:ci_bridge, pipeline: upstream, status: 'success', options: options) + create(:ci_sources_pipeline, pipeline: downstream, source_job: bridge) + + bridge + end + end end |