Newer
Older
Kamil Trzciński
committed
describe Ci::Pipeline, models: true do
include EmailHelpers
let(:project) { FactoryGirl.create :empty_project }
let(:pipeline) { FactoryGirl.create :ci_empty_pipeline, status: 'created', project: project }
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 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 }
context 'commit.sha can not start with 00000000' do
before do
pipeline.sha = '0' * 40
pipeline.valid_commit_sha
it('commit errors should not be empty') { expect(pipeline.errors).not_to be_empty }
subject { pipeline.short_sha }
it 'has 8 items' do
expect(subject.size).to eq(8)
end
it { expect(pipeline.sha).to start_with(subject) }
subject { pipeline.retried }
@build1 = FactoryGirl.create :ci_build, pipeline: pipeline, name: 'deploy'
@build2 = FactoryGirl.create :ci_build, pipeline: pipeline, name: 'deploy'
is_expected.to contain_exactly(@build1)
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
subject { pipeline.retryable? }
context 'no failed builds' do
before do
create_build('rspec', 'success')
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')
def create_build(name, status)
create(:ci_build, name: name, status: status, pipeline: pipeline)
end
end
describe '#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')
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
it 'returns a valid number of stages' do
expect(pipeline.stages_count).to eq(3)
end
it 'returns a valid names of stages' do
expect(pipeline.stages_name).to eq(['build', 'test', 'deploy'])
end
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
context 'stages with statuses' do
let(:statuses) do
subject.map do |stage|
[stage.name, stage.status]
end
end
it 'returns list of stages with statuses' do
expect(statuses).to eq([['build', 'failed'],
['test', 'success'],
['deploy', 'running']
])
end
context 'when build 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([['build', 'success'],
['test', 'success'],
['deploy', 'running']
])
end
end
describe '#stage' do
subject { pipeline.stage('test') }
context 'with status in stage' do
let!(:status) { create(:commit_status, pipeline: pipeline, stage: 'test') }
it 'return stage object' do
is_expected.to be_a(Ci::Stage)
end
end
context 'without status in stage' do
let!(:status) { create(:commit_status, pipeline: pipeline, stage: 'build') }
it 'return stage object' do
is_expected.to be_nil
end
end
end
describe 'state machine' do
Kamil Trzciński
committed
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!
travel_to(current + 40) do
travel_to(current + 70) do
end
it 'matches sum of builds duration' do
expect(pipeline.duration).to eq(40)
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
describe '#finished_at' do
it 'updates on transitioning to success' do
build.success
expect(pipeline.reload.finished_at).not_to be_nil
it 'does not update on transitioning to running' do
build.run
expect(pipeline.reload.finished_at).to be_nil
describe 'merge request metrics' do
let(:project) { FactoryGirl.create :project }
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
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)
subject { pipeline.branch? }
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) }
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'
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'
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
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')
describe '#status' do
let!(:build) { create(:ci_build, :created, pipeline: pipeline, name: 'test') }
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
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
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
it { is_expected.to eq('canceled') }
end
context 'on failure and build retry' do
before do
build.drop
Ci::Build.retry(build)
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
describe '#detailed_status' do
let(:user) { create(:user) }
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 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
%i[created running pending].each do |status0|
context "when there is a build #{status0}" do
create(:ci_build, status0, pipeline: pipeline)
it 'is cancelable' do
expect(pipeline.cancelable?).to be_truthy
end
context "when there is an external job #{status0}" do
create(:generic_commit_status, status0, pipeline: pipeline)
it 'is cancelable' do
expect(pipeline.cancelable?).to be_truthy
end
%i[success failed canceled].each do |status1|
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
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
%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
describe '#cancel_running' do
Kamil Trzciński
committed
let(:latest_status) { pipeline.statuses.pluck(:status) }
context 'when there is a running external job and created build' do
before do
Kamil Trzciński
committed
create(:ci_build, :running, pipeline: pipeline)
create(:generic_commit_status, :running, pipeline: pipeline)
pipeline.cancel_running
end
it 'cancels both jobs' do
Kamil Trzciński
committed
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
expect(latest_status).to contain_exactly('canceled', 'canceled')
end
end
context 'when builds 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
end
describe '#retry_failed' do
let(:latest_status) { pipeline.statuses.latest.pluck(:status) }
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(create(:user))
Kamil Trzciński
committed
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(create(:user))
Kamil Trzciński
committed
end
it 'retries both builds' do
expect(latest_status).to contain_exactly('pending', 'pending')
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(create(:user))
Kamil Trzciński
committed
end
it 'retries both builds' do
expect(latest_status).to contain_exactly('pending', 'pending')
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
expect(WebMock).to have_requested_pipeline_hook('pending').once
context 'when build is run' do
before do
expect(WebMock).to have_requested_pipeline_hook('running').once
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
expect(WebMock).to have_requested_pipeline_hook('success').once
context 'when stage one failed' do
before do
build_a.drop
end
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
context 'with pipeline hooks disabled' do
let(:enabled) { false }
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)
describe "#merge_requests" do
let(:project) { FactoryGirl.create :project }
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 'notifications when pipeline success or failed' do
let(:project) { create(:project) }
let(:pipeline) do
create(:ci_pipeline,
project: project,
sha: project.commit('master').sha,
user: create(:user))
end
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
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)
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'