require 'spec_helper' describe Ci::Pipeline, models: true do include EmailHelpers let(:user) { create(:user) } let(:project) { create(:empty_project) } let(:pipeline) do create(:ci_empty_pipeline, status: :created, project: project) end it { is_expected.to belong_to(:project) } it { is_expected.to belong_to(:user) } it { is_expected.to have_many(:statuses) } it { is_expected.to have_many(:trigger_requests) } it { is_expected.to have_many(:builds) } it { is_expected.to validate_presence_of :sha } it { is_expected.to validate_presence_of :status } it { is_expected.to respond_to :git_author_name } it { is_expected.to respond_to :git_author_email } it { is_expected.to respond_to :short_sha } describe '#block' do it 'changes pipeline status to manual' do expect(pipeline.block).to be true expect(pipeline.reload).to be_manual expect(pipeline.reload).to be_blocked end end describe '#valid_commit_sha' do context 'commit.sha can not start with 00000000' do before do pipeline.sha = '0' * 40 pipeline.valid_commit_sha end it('commit errors should not be empty') { expect(pipeline.errors).not_to be_empty } end end describe '#short_sha' do subject { pipeline.short_sha } it 'has 8 items' do expect(subject.size).to eq(8) end it { expect(pipeline.sha).to start_with(subject) } end describe '#retried' do subject { pipeline.retried } before do @build1 = FactoryGirl.create :ci_build, pipeline: pipeline, name: 'deploy' @build2 = FactoryGirl.create :ci_build, pipeline: pipeline, name: 'deploy' end it 'returns old builds' do is_expected.to contain_exactly(@build1) end end describe "coverage" do let(:project) { FactoryGirl.create :empty_project, build_coverage_regex: "/.*/" } let(:pipeline) { FactoryGirl.create :ci_empty_pipeline, project: project } it "calculates average when there are two builds with coverage" do FactoryGirl.create :ci_build, name: "rspec", coverage: 30, pipeline: pipeline FactoryGirl.create :ci_build, name: "rubocop", coverage: 40, pipeline: pipeline expect(pipeline.coverage).to eq("35.00") end it "calculates average when there are two builds with coverage and one with nil" do FactoryGirl.create :ci_build, name: "rspec", coverage: 30, pipeline: pipeline FactoryGirl.create :ci_build, name: "rubocop", coverage: 40, pipeline: pipeline FactoryGirl.create :ci_build, pipeline: pipeline expect(pipeline.coverage).to eq("35.00") end it "calculates average when there are two builds with coverage and one is retried" do FactoryGirl.create :ci_build, name: "rspec", coverage: 30, pipeline: pipeline FactoryGirl.create :ci_build, name: "rubocop", coverage: 30, pipeline: pipeline FactoryGirl.create :ci_build, name: "rubocop", coverage: 40, pipeline: pipeline expect(pipeline.coverage).to eq("35.00") end it "calculates average when there is one build without coverage" do FactoryGirl.create :ci_build, pipeline: pipeline expect(pipeline.coverage).to be_nil end end describe '#retryable?' do subject { pipeline.retryable? } context 'no failed builds' do before do create_build('rspec', 'success') end it 'is not retryable' do is_expected.to be_falsey end context 'one canceled job' do before do create_build('rubocop', 'canceled') end it 'is retryable' do is_expected.to be_truthy end end end context 'with failed builds' do before do create_build('rspec', 'running') create_build('rubocop', 'failed') end it 'is retryable' do is_expected.to be_truthy end end def create_build(name, status) create(:ci_build, name: name, status: status, pipeline: pipeline) end end describe 'pipeline stages' do before do create(:commit_status, pipeline: pipeline, stage: 'build', name: 'linux', stage_idx: 0, status: 'success') create(:commit_status, pipeline: pipeline, stage: 'build', name: 'mac', stage_idx: 0, status: 'failed') create(:commit_status, pipeline: pipeline, stage: 'deploy', name: 'staging', stage_idx: 2, status: 'running') create(:commit_status, pipeline: pipeline, stage: 'test', name: 'rspec', stage_idx: 1, status: 'success') end describe '#stages' do subject { pipeline.stages } context 'stages list' do it 'returns ordered list of stages' do expect(subject.map(&:name)).to eq(%w[build test deploy]) end end context 'stages with statuses' do let(:statuses) do subject.map { |stage| [stage.name, stage.status] } end it 'returns list of stages with correct statuses' do expect(statuses).to eq([%w(build failed), %w(test success), %w(deploy running)]) end context 'when commit status is retried' do before do create(:commit_status, pipeline: pipeline, stage: 'build', name: 'mac', stage_idx: 0, status: 'success') end it 'ignores the previous state' do expect(statuses).to eq([%w(build success), %w(test success), %w(deploy running)]) end end end context 'when there is a stage with warnings' do before do create(:commit_status, pipeline: pipeline, stage: 'deploy', name: 'prod:2', stage_idx: 2, status: 'failed', allow_failure: true) end it 'populates stage with correct number of warnings' do deploy_stage = pipeline.stages.third expect(deploy_stage).not_to receive(:statuses) expect(deploy_stage).to have_warnings end end end describe '#stages_count' do it 'returns a valid number of stages' do expect(pipeline.stages_count).to eq(3) end end describe '#stages_name' do it 'returns a valid names of stages' do expect(pipeline.stages_name).to eq(%w(build test deploy)) end end end describe '#stage' do subject { pipeline.stage('test') } context 'with status in stage' do before do create(:commit_status, pipeline: pipeline, stage: 'test') end it { expect(subject).to be_a Ci::Stage } it { expect(subject.name).to eq 'test' } it { expect(subject.statuses).not_to be_empty } end context 'without status in stage' do before do create(:commit_status, pipeline: pipeline, stage: 'build') end it 'return stage object' do is_expected.to be_nil end end end describe 'state machine' do let(:current) { Time.now.change(usec: 0) } let(:build) { create_build('build1', 0) } let(:build_b) { create_build('build2', 0) } let(:build_c) { create_build('build3', 0) } describe '#duration' do before do travel_to(current + 30) do build.run! build.success! build_b.run! build_c.run! end travel_to(current + 40) do build_b.drop! end travel_to(current + 70) do build_c.success! end end it 'matches sum of builds duration' do pipeline.reload expect(pipeline.duration).to eq(40) end end describe '#started_at' do it 'updates on transitioning to running' do build.run expect(pipeline.reload.started_at).not_to be_nil end it 'does not update on transitioning to success' do build.success expect(pipeline.reload.started_at).to be_nil end end describe '#finished_at' do it 'updates on transitioning to success' do build.success expect(pipeline.reload.finished_at).not_to be_nil end it 'does not update on transitioning to running' do build.run expect(pipeline.reload.finished_at).to be_nil end end describe 'merge request metrics' do let(:project) { create(:project, :repository) } let(:pipeline) { FactoryGirl.create(:ci_empty_pipeline, status: 'created', project: project, ref: 'master', sha: project.repository.commit('master').id) } let!(:merge_request) { create(:merge_request, source_project: project, source_branch: pipeline.ref) } before do expect(PipelineMetricsWorker).to receive(:perform_async).with(pipeline.id) end context 'when transitioning to running' do it 'schedules metrics workers' do pipeline.run end end context 'when transitioning to success' do it 'schedules metrics workers' do pipeline.succeed end end end def create_build(name, queued_at = current, started_from = 0) create(:ci_build, name: name, pipeline: pipeline, queued_at: queued_at, started_at: queued_at + started_from) end end describe '#branch?' do subject { pipeline.branch? } context 'is not a tag' do before do pipeline.tag = false end it 'return true when tag is set to false' do is_expected.to be_truthy end end context 'is not a tag' do before do pipeline.tag = true end it 'return false when tag is set to true' do is_expected.to be_falsey end end end context 'with non-empty project' do let(:project) { create(:project, :repository) } let(:pipeline) do create(:ci_pipeline, project: project, ref: project.default_branch, sha: project.commit.sha) end describe '#latest?' do context 'with latest sha' do it 'returns true' do expect(pipeline).to be_latest end end context 'with not latest sha' do before do pipeline.update( sha: project.commit("#{project.default_branch}~1").sha) end it 'returns false' do expect(pipeline).not_to be_latest end end end end describe '#manual_actions' do subject { pipeline.manual_actions } it 'when none defined' do is_expected.to be_empty end context 'when action defined' do let!(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'deploy') } it 'returns one action' do is_expected.to contain_exactly(manual) end context 'there are multiple of the same name' do let!(:manual2) { create(:ci_build, :manual, pipeline: pipeline, name: 'deploy') } it 'returns latest one' do is_expected.to contain_exactly(manual2) end end end end describe '#has_warnings?' do subject { pipeline.has_warnings? } context 'build which is allowed to fail fails' do before do create :ci_build, :success, pipeline: pipeline, name: 'rspec' create :ci_build, :allowed_to_fail, :failed, pipeline: pipeline, name: 'rubocop' end it 'returns true' do is_expected.to be_truthy end end context 'build which is allowed to fail succeeds' do before do create :ci_build, :success, pipeline: pipeline, name: 'rspec' create :ci_build, :allowed_to_fail, :success, pipeline: pipeline, name: 'rubocop' end it 'returns false' do is_expected.to be_falsey end end context 'build is retried and succeeds' do before do create :ci_build, :success, pipeline: pipeline, name: 'rubocop' create :ci_build, :failed, pipeline: pipeline, name: 'rspec' create :ci_build, :success, pipeline: pipeline, name: 'rspec' end it 'returns false' do is_expected.to be_falsey end end end shared_context 'with some outdated pipelines' do before do create_pipeline(:canceled, 'ref', 'A') create_pipeline(:success, 'ref', 'A') create_pipeline(:failed, 'ref', 'B') create_pipeline(:skipped, 'feature', 'C') end def create_pipeline(status, ref, sha) create(:ci_empty_pipeline, status: status, ref: ref, sha: sha) end end describe '.latest' do include_context 'with some outdated pipelines' context 'when no ref is specified' do let(:pipelines) { described_class.latest.all } it 'returns the latest pipeline for the same ref and different sha' do expect(pipelines.map(&:sha)).to contain_exactly('A', 'B', 'C') expect(pipelines.map(&:status)). to contain_exactly('success', 'failed', 'skipped') end end context 'when ref is specified' do let(:pipelines) { described_class.latest('ref').all } it 'returns the latest pipeline for ref and different sha' do expect(pipelines.map(&:sha)).to contain_exactly('A', 'B') expect(pipelines.map(&:status)). to contain_exactly('success', 'failed') end end end describe '.latest_status' do include_context 'with some outdated pipelines' context 'when no ref is specified' do let(:latest_status) { described_class.latest_status } it 'returns the latest status for the same ref and different sha' do expect(latest_status).to eq(described_class.latest.status) expect(latest_status).to eq('failed') end end context 'when ref is specified' do let(:latest_status) { described_class.latest_status('ref') } it 'returns the latest status for ref and different sha' do expect(latest_status).to eq(described_class.latest_status('ref')) expect(latest_status).to eq('failed') end end end describe '.latest_successful_for' do include_context 'with some outdated pipelines' let!(:latest_successful_pipeline) do create_pipeline(:success, 'ref', 'D') end it 'returns the latest successful pipeline' do expect(described_class.latest_successful_for('ref')). to eq(latest_successful_pipeline) end end describe '.latest_successful_for_refs' do include_context 'with some outdated pipelines' let!(:latest_successful_pipeline1) { create_pipeline(:success, 'ref1', 'D') } let!(:latest_successful_pipeline2) { create_pipeline(:success, 'ref2', 'D') } it 'returns the latest successful pipeline for both refs' do refs = %w(ref1 ref2 ref3) expect(described_class.latest_successful_for_refs(refs)).to eq({ 'ref1' => latest_successful_pipeline1, 'ref2' => latest_successful_pipeline2 }) end end describe '#status' do let(:build) do create(:ci_build, :created, pipeline: pipeline, name: 'test') end subject { pipeline.reload.status } context 'on queuing' do before do build.enqueue end it { is_expected.to eq('pending') } end context 'on run' do before do build.enqueue build.run end it { is_expected.to eq('running') } end context 'on drop' do before do build.drop end it { is_expected.to eq('failed') } end context 'on success' do before do build.success end it { is_expected.to eq('success') } end context 'on cancel' do before do build.cancel end context 'when build is pending' do let(:build) do create(:ci_build, :pending, pipeline: pipeline) end it { is_expected.to eq('canceled') } end end context 'on failure and build retry' do before do build.drop project.add_developer(user) Ci::Build.retry(build, user) end # We are changing a state: created > failed > running # Instead of: created > failed > pending # Since the pipeline already run, so it should not be pending anymore it { is_expected.to eq('running') } end end describe '#detailed_status' do subject { pipeline.detailed_status(user) } context 'when pipeline is created' do let(:pipeline) { create(:ci_pipeline, status: :created) } it 'returns detailed status for created pipeline' do expect(subject.text).to eq 'created' end end context 'when pipeline is pending' do let(:pipeline) { create(:ci_pipeline, status: :pending) } it 'returns detailed status for pending pipeline' do expect(subject.text).to eq 'pending' end end context 'when pipeline is running' do let(:pipeline) { create(:ci_pipeline, status: :running) } it 'returns detailed status for running pipeline' do expect(subject.text).to eq 'running' end end context 'when pipeline is successful' do let(:pipeline) { create(:ci_pipeline, status: :success) } it 'returns detailed status for successful pipeline' do expect(subject.text).to eq 'passed' end end context 'when pipeline is failed' do let(:pipeline) { create(:ci_pipeline, status: :failed) } it 'returns detailed status for failed pipeline' do expect(subject.text).to eq 'failed' end end context 'when pipeline is canceled' do let(:pipeline) { create(:ci_pipeline, status: :canceled) } it 'returns detailed status for canceled pipeline' do expect(subject.text).to eq 'canceled' end end context 'when pipeline is skipped' do let(:pipeline) { create(:ci_pipeline, status: :skipped) } it 'returns detailed status for skipped pipeline' do expect(subject.text).to eq 'skipped' end end context 'when pipeline is blocked' do let(:pipeline) { create(:ci_pipeline, status: :manual) } it 'returns detailed status for blocked pipeline' do expect(subject.text).to eq 'blocked' end end context 'when pipeline is successful but with warnings' do let(:pipeline) { create(:ci_pipeline, status: :success) } before do create(:ci_build, :allowed_to_fail, :failed, pipeline: pipeline) end it 'retruns detailed status for successful pipeline with warnings' do expect(subject.label).to eq 'passed with warnings' end end end describe '#cancelable?' do %i[created running pending].each do |status0| context "when there is a build #{status0}" do before do create(:ci_build, status0, pipeline: pipeline) end it 'is cancelable' do expect(pipeline.cancelable?).to be_truthy end end context "when there is an external job #{status0}" do before do create(:generic_commit_status, status0, pipeline: pipeline) end it 'is cancelable' do expect(pipeline.cancelable?).to be_truthy end end %i[success failed canceled].each do |status1| context "when there are generic_commit_status jobs for #{status0} and #{status1}" do before do create(:generic_commit_status, status0, pipeline: pipeline) create(:generic_commit_status, status1, pipeline: pipeline) end it 'is cancelable' do expect(pipeline.cancelable?).to be_truthy end end context "when there are generic_commit_status and ci_build jobs for #{status0} and #{status1}" do before do create(:generic_commit_status, status0, pipeline: pipeline) create(:ci_build, status1, pipeline: pipeline) end it 'is cancelable' do expect(pipeline.cancelable?).to be_truthy end end context "when there are ci_build jobs for #{status0} and #{status1}" do before do create(:ci_build, status0, pipeline: pipeline) create(:ci_build, status1, pipeline: pipeline) end it 'is cancelable' do expect(pipeline.cancelable?).to be_truthy end end end end %i[success failed canceled].each do |status| context "when there is a build #{status}" do before do create(:ci_build, status, pipeline: pipeline) end it 'is not cancelable' do expect(pipeline.cancelable?).to be_falsey end end context "when there is an external job #{status}" do before do create(:generic_commit_status, status, pipeline: pipeline) end it 'is not cancelable' do expect(pipeline.cancelable?).to be_falsey end end end end describe '#cancel_running' do let(:latest_status) { pipeline.statuses.pluck(:status) } context 'when there is a running external job and a regular job' do before do create(:ci_build, :running, pipeline: pipeline) create(:generic_commit_status, :running, pipeline: pipeline) pipeline.cancel_running end it 'cancels both jobs' do expect(latest_status).to contain_exactly('canceled', 'canceled') end end context 'when jobs are in different stages' do before do create(:ci_build, :running, stage_idx: 0, pipeline: pipeline) create(:ci_build, :running, stage_idx: 1, pipeline: pipeline) pipeline.cancel_running end it 'cancels both jobs' do expect(latest_status).to contain_exactly('canceled', 'canceled') end end context 'when there are created builds present in the pipeline' do before do create(:ci_build, :running, stage_idx: 0, pipeline: pipeline) create(:ci_build, :created, stage_idx: 1, pipeline: pipeline) pipeline.cancel_running end it 'cancels created builds' do expect(latest_status).to eq %w(canceled canceled) end end end describe '#retry_failed' do let(:latest_status) { pipeline.statuses.latest.pluck(:status) } before do project.add_developer(user) end context 'when there is a failed build and failed external status' do before do create(:ci_build, :failed, name: 'build', pipeline: pipeline) create(:generic_commit_status, :failed, name: 'jenkins', pipeline: pipeline) pipeline.retry_failed(user) end it 'retries only build' do expect(latest_status).to contain_exactly('pending', 'failed') end end context 'when builds are in different stages' do before do create(:ci_build, :failed, name: 'build', stage_idx: 0, pipeline: pipeline) create(:ci_build, :failed, name: 'jenkins', stage_idx: 1, pipeline: pipeline) pipeline.retry_failed(user) end it 'retries both builds' do expect(latest_status).to contain_exactly('pending', 'created') end end context 'when there are canceled and failed' do before do create(:ci_build, :failed, name: 'build', stage_idx: 0, pipeline: pipeline) create(:ci_build, :canceled, name: 'jenkins', stage_idx: 1, pipeline: pipeline) pipeline.retry_failed(user) end it 'retries both builds' do expect(latest_status).to contain_exactly('pending', 'created') end end end describe '#execute_hooks' do let!(:build_a) { create_build('a', 0) } let!(:build_b) { create_build('b', 1) } let!(:hook) do create(:project_hook, project: project, pipeline_events: enabled) end before do ProjectWebHookWorker.drain end context 'with pipeline hooks enabled' do let(:enabled) { true } before do WebMock.stub_request(:post, hook.url) end context 'with multiple builds' do context 'when build is queued' do before do build_a.enqueue build_b.enqueue end it 'receives a pending event once' do expect(WebMock).to have_requested_pipeline_hook('pending').once end end context 'when build is run' do before do build_a.enqueue build_a.run build_b.enqueue build_b.run end it 'receives a running event once' do expect(WebMock).to have_requested_pipeline_hook('running').once end end context 'when all builds succeed' do before do build_a.success # We have to reload build_b as this is in next stage and it gets triggered by PipelineProcessWorker build_b.reload.success end it 'receives a success event once' do expect(WebMock).to have_requested_pipeline_hook('success').once end end context 'when stage one failed' do before do build_a.drop end it 'receives a failed event once' do expect(WebMock).to have_requested_pipeline_hook('failed').once end end def have_requested_pipeline_hook(status) have_requested(:post, hook.url).with do |req| json_body = JSON.parse(req.body) json_body['object_attributes']['status'] == status && json_body['builds'].length == 2 end end end end context 'with pipeline hooks disabled' do let(:enabled) { false } before do build_a.enqueue build_b.enqueue end it 'did not execute pipeline_hook after touched' do expect(WebMock).not_to have_requested(:post, hook.url) end end def create_build(name, stage_idx) create(:ci_build, :created, pipeline: pipeline, name: name, stage_idx: stage_idx) end end describe "#merge_requests" do let(:project) { create(:project, :repository) } let(:pipeline) { FactoryGirl.create(:ci_empty_pipeline, status: 'created', project: project, ref: 'master', sha: project.repository.commit('master').id) } it "returns merge requests whose `diff_head_sha` matches the pipeline's SHA" do merge_request = create(:merge_request, source_project: project, source_branch: pipeline.ref) expect(pipeline.merge_requests).to eq([merge_request]) end it "doesn't return merge requests whose source branch doesn't match the pipeline's ref" do create(:merge_request, source_project: project, source_branch: 'feature', target_branch: 'master') expect(pipeline.merge_requests).to be_empty end it "doesn't return merge requests whose `diff_head_sha` doesn't match the pipeline's SHA" do create(:merge_request, source_project: project, source_branch: pipeline.ref) allow_any_instance_of(MergeRequest).to receive(:diff_head_sha) { '97de212e80737a608d939f648d959671fb0a0142b' } expect(pipeline.merge_requests).to be_empty end end describe '#stuck?' do before do create(:ci_build, :pending, pipeline: pipeline) end context 'when pipeline is stuck' do it 'is stuck' do expect(pipeline).to be_stuck end end context 'when pipeline is not stuck' do before { create(:ci_runner, :shared, :online) } it 'is not stuck' do expect(pipeline).not_to be_stuck end end end describe '#has_yaml_errors?' do context 'when pipeline has errors' do let(:pipeline) do create(:ci_pipeline, config: { rspec: nil }) end it 'contains yaml errors' do expect(pipeline).to have_yaml_errors end end context 'when pipeline does not have errors' do let(:pipeline) do create(:ci_pipeline, config: { rspec: { script: 'rake test' } }) end it 'does not containyaml errors' do expect(pipeline).not_to have_yaml_errors end end end describe '#update_status' do let(:pipeline) { create(:ci_pipeline, sha: '123456') } it 'updates the cached status' do fake_status = double # after updating the status, the status is set to `skipped` for this pipeline's builds expect(Ci::PipelineStatus).to receive(:new).with(pipeline.project, sha: '123456', status: 'skipped').and_return(fake_status) expect(fake_status).to receive(:store_in_cache_if_needed) pipeline.update_status end end describe 'notifications when pipeline success or failed' do let(:project) { create(:project, :repository) } let(:pipeline) do create(:ci_pipeline, project: project, sha: project.commit('master').sha, user: create(:user)) end before do reset_delivered_emails! project.team << [pipeline.user, Gitlab::Access::DEVELOPER] perform_enqueued_jobs do pipeline.enqueue pipeline.run end end shared_examples 'sending a notification' do it 'sends an email' do should_only_email(pipeline.user, kind: :bcc) end end shared_examples 'not sending any notification' do it 'does not send any email' do should_not_email_anyone end end context 'with success pipeline' do before do perform_enqueued_jobs do pipeline.succeed end end it_behaves_like 'sending a notification' end context 'with failed pipeline' do before do perform_enqueued_jobs do create(:ci_build, :failed, pipeline: pipeline) create(:generic_commit_status, :failed, pipeline: pipeline) pipeline.drop end end it_behaves_like 'sending a notification' end context 'with skipped pipeline' do before do perform_enqueued_jobs do pipeline.skip end end it_behaves_like 'not sending any notification' end context 'with cancelled pipeline' do before do perform_enqueued_jobs do pipeline.cancel end end it_behaves_like 'not sending any notification' end end end