Add Spec for ProcessPipelineService
This commit is contained in:
parent
f3348951a8
commit
80a92650fa
|
@ -80,7 +80,7 @@ module Ci
|
||||||
|
|
||||||
state_machine :status, initial: :created do
|
state_machine :status, initial: :created do
|
||||||
event :enqueue do
|
event :enqueue do
|
||||||
transition [:created, :skipped] => :pending
|
transition [:created, :skipped, :scheduled] => :pending
|
||||||
transition [:success, :failed, :canceled] => :running
|
transition [:success, :failed, :canceled] => :running
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -2,13 +2,38 @@
|
||||||
|
|
||||||
module Ci
|
module Ci
|
||||||
class ProcessBuildService < BaseService
|
class ProcessBuildService < BaseService
|
||||||
def execute(build)
|
def execute(build, current_status)
|
||||||
if build.schedulable?
|
if valid_statuses_for_when(build.when).include?(current_status)
|
||||||
build.schedule!
|
if build.schedulable?
|
||||||
elsif build.action?
|
build.schedule!
|
||||||
build.actionize
|
elsif build.action?
|
||||||
|
build.actionize
|
||||||
|
else
|
||||||
|
build.enqueue
|
||||||
|
end
|
||||||
|
true
|
||||||
else
|
else
|
||||||
build.enqueue
|
build.skip
|
||||||
|
false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def valid_statuses_for_when(value)
|
||||||
|
case value
|
||||||
|
when 'on_success'
|
||||||
|
%w[success skipped]
|
||||||
|
when 'on_failure'
|
||||||
|
%w[failed]
|
||||||
|
when 'always'
|
||||||
|
%w[success failed skipped]
|
||||||
|
when 'manual'
|
||||||
|
%w[success skipped]
|
||||||
|
when 'delayed'
|
||||||
|
%w[success skipped]
|
||||||
|
else
|
||||||
|
[]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -29,39 +29,13 @@ module Ci
|
||||||
if HasStatus::COMPLETED_STATUSES.include?(current_status)
|
if HasStatus::COMPLETED_STATUSES.include?(current_status)
|
||||||
created_builds_in_stage(index).select do |build|
|
created_builds_in_stage(index).select do |build|
|
||||||
Gitlab::OptimisticLocking.retry_lock(build) do |subject|
|
Gitlab::OptimisticLocking.retry_lock(build) do |subject|
|
||||||
process_build(subject, current_status)
|
Ci::ProcessBuildService.new(project, @user)
|
||||||
|
.execute(build, current_status)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def process_build(build, current_status)
|
|
||||||
if valid_statuses_for_when(build.when).include?(current_status)
|
|
||||||
Ci::ProcessBuildService.new(project, @user).execute(build)
|
|
||||||
true
|
|
||||||
else
|
|
||||||
build.skip
|
|
||||||
false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def valid_statuses_for_when(value)
|
|
||||||
case value
|
|
||||||
when 'on_success'
|
|
||||||
%w[success skipped]
|
|
||||||
when 'on_failure'
|
|
||||||
%w[failed]
|
|
||||||
when 'always'
|
|
||||||
%w[success failed skipped]
|
|
||||||
when 'manual'
|
|
||||||
%w[success skipped]
|
|
||||||
when 'delayed'
|
|
||||||
%w[success skipped]
|
|
||||||
else
|
|
||||||
[]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# rubocop: disable CodeReuse/ActiveRecord
|
# rubocop: disable CodeReuse/ActiveRecord
|
||||||
def status_for_prior_stages(index)
|
def status_for_prior_stages(index)
|
||||||
pipeline.builds.where('stage_idx < ?', index).latest.status || 'success'
|
pipeline.builds.where('stage_idx < ?', index).latest.status || 'success'
|
||||||
|
|
|
@ -242,6 +242,187 @@ describe Ci::ProcessPipelineService, '#execute' do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when delayed jobs are defined' do
|
||||||
|
context 'when the scene is timed incremental rollout' do
|
||||||
|
before do
|
||||||
|
create_build('build', stage_idx: 0)
|
||||||
|
create_build('rollout10%', **delayed_options, stage_idx: 1)
|
||||||
|
create_build('rollout100%', **delayed_options, stage_idx: 2)
|
||||||
|
create_build('cleanup', stage_idx: 3)
|
||||||
|
|
||||||
|
allow(Ci::BuildScheduleWorker).to receive(:perform_at)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when builds are successful' do
|
||||||
|
it 'properly processes the pipeline' do
|
||||||
|
expect(process_pipeline).to be_truthy
|
||||||
|
expect(builds_names_and_statuses).to eq({ 'build': 'pending' })
|
||||||
|
|
||||||
|
succeed_pending
|
||||||
|
|
||||||
|
expect(builds_names_and_statuses).to eq({ 'build': 'success', 'rollout10%': 'scheduled' })
|
||||||
|
|
||||||
|
enqueue_scheduled('rollout10%')
|
||||||
|
succeed_pending
|
||||||
|
|
||||||
|
expect(builds_names_and_statuses).to eq({ 'build': 'success', 'rollout10%': 'success', 'rollout100%': 'scheduled' })
|
||||||
|
|
||||||
|
enqueue_scheduled('rollout100%')
|
||||||
|
succeed_pending
|
||||||
|
|
||||||
|
expect(builds_names_and_statuses).to eq({ 'build': 'success', 'rollout10%': 'success', 'rollout100%': 'success', 'cleanup': 'pending' })
|
||||||
|
|
||||||
|
succeed_pending
|
||||||
|
|
||||||
|
expect(builds_names_and_statuses).to eq({ 'build': 'success', 'rollout10%': 'success', 'rollout100%': 'success', 'cleanup': 'success' })
|
||||||
|
expect(pipeline.reload.status).to eq 'success'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when build job fails' do
|
||||||
|
it 'properly processes the pipeline' do
|
||||||
|
expect(process_pipeline).to be_truthy
|
||||||
|
expect(builds_names_and_statuses).to eq({ 'build': 'pending' })
|
||||||
|
|
||||||
|
fail_running_or_pending
|
||||||
|
|
||||||
|
expect(builds_names_and_statuses).to eq({ 'build': 'failed' })
|
||||||
|
expect(pipeline.reload.status).to eq 'failed'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when rollout 10% is unscheduled' do
|
||||||
|
it 'properly processes the pipeline' do
|
||||||
|
expect(process_pipeline).to be_truthy
|
||||||
|
expect(builds_names_and_statuses).to eq({ 'build': 'pending' })
|
||||||
|
|
||||||
|
succeed_pending
|
||||||
|
|
||||||
|
expect(builds_names_and_statuses).to eq({ 'build': 'success', 'rollout10%': 'scheduled' })
|
||||||
|
|
||||||
|
unschedule
|
||||||
|
|
||||||
|
expect(builds_names_and_statuses).to eq({ 'build': 'success', 'rollout10%': 'manual' })
|
||||||
|
expect(pipeline.reload.status).to eq 'manual'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when user plays rollout 10%' do
|
||||||
|
it 'schedules rollout100%' do
|
||||||
|
process_pipeline
|
||||||
|
succeed_pending
|
||||||
|
unschedule
|
||||||
|
play_manual_action('rollout10%')
|
||||||
|
succeed_pending
|
||||||
|
|
||||||
|
expect(builds_names_and_statuses).to eq({ 'build': 'success', 'rollout10%': 'success', 'rollout100%': 'scheduled' })
|
||||||
|
expect(pipeline.reload.status).to eq 'scheduled'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when rollout 10% fails' do
|
||||||
|
it 'properly processes the pipeline' do
|
||||||
|
expect(process_pipeline).to be_truthy
|
||||||
|
expect(builds_names_and_statuses).to eq({ 'build': 'pending' })
|
||||||
|
|
||||||
|
succeed_pending
|
||||||
|
|
||||||
|
expect(builds_names_and_statuses).to eq({ 'build': 'success', 'rollout10%': 'scheduled' })
|
||||||
|
|
||||||
|
enqueue_scheduled('rollout10%')
|
||||||
|
fail_running_or_pending
|
||||||
|
|
||||||
|
expect(builds_names_and_statuses).to eq({ 'build': 'success', 'rollout10%': 'failed' })
|
||||||
|
expect(pipeline.reload.status).to eq 'failed'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when user retries rollout 10%' do
|
||||||
|
it 'does not schedule rollout10% again' do
|
||||||
|
process_pipeline
|
||||||
|
succeed_pending
|
||||||
|
enqueue_scheduled('rollout10%')
|
||||||
|
fail_running_or_pending
|
||||||
|
retry_build('rollout10%')
|
||||||
|
|
||||||
|
expect(builds_names_and_statuses).to eq({ 'build': 'success', 'rollout10%': 'pending' })
|
||||||
|
expect(pipeline.reload.status).to eq 'running'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when rollout 10% is played immidiately' do
|
||||||
|
it 'properly processes the pipeline' do
|
||||||
|
expect(process_pipeline).to be_truthy
|
||||||
|
expect(builds_names_and_statuses).to eq({ 'build': 'pending' })
|
||||||
|
|
||||||
|
succeed_pending
|
||||||
|
|
||||||
|
expect(builds_names_and_statuses).to eq({ 'build': 'success', 'rollout10%': 'scheduled' })
|
||||||
|
|
||||||
|
play_manual_action('rollout10%')
|
||||||
|
|
||||||
|
expect(builds_names_and_statuses).to eq({ 'build': 'success', 'rollout10%': 'pending' })
|
||||||
|
expect(pipeline.reload.status).to eq 'running'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when only one scheduled job exists in a pipeline' do
|
||||||
|
before do
|
||||||
|
create_build('delayed', **delayed_options, stage_idx: 0)
|
||||||
|
|
||||||
|
allow(Ci::BuildScheduleWorker).to receive(:perform_at)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'properly processes the pipeline' do
|
||||||
|
expect(process_pipeline).to be_truthy
|
||||||
|
expect(builds_names_and_statuses).to eq({ 'delayed': 'scheduled' })
|
||||||
|
|
||||||
|
expect(pipeline.reload.status).to eq 'scheduled'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when there are two delayed jobs in a stage' do
|
||||||
|
before do
|
||||||
|
create_build('delayed1', **delayed_options, stage_idx: 0)
|
||||||
|
create_build('delayed2', **delayed_options, stage_idx: 0)
|
||||||
|
create_build('job', stage_idx: 1)
|
||||||
|
|
||||||
|
allow(Ci::BuildScheduleWorker).to receive(:perform_at)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'blocks the stage until all scheduled jobs finished' do
|
||||||
|
expect(process_pipeline).to be_truthy
|
||||||
|
expect(builds_names_and_statuses).to eq({ 'delayed1': 'scheduled', 'delayed2': 'scheduled' })
|
||||||
|
|
||||||
|
enqueue_scheduled('delayed1')
|
||||||
|
|
||||||
|
expect(builds_names_and_statuses).to eq({ 'delayed1': 'pending', 'delayed2': 'scheduled' })
|
||||||
|
expect(pipeline.reload.status).to eq 'scheduled'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when a delayed job is allowed to fail' do
|
||||||
|
before do
|
||||||
|
create_build('delayed', **delayed_options, allow_failure: true, stage_idx: 0)
|
||||||
|
create_build('job', stage_idx: 1)
|
||||||
|
|
||||||
|
allow(Ci::BuildScheduleWorker).to receive(:perform_at)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'blocks the stage and continues after it failed' do
|
||||||
|
expect(process_pipeline).to be_truthy
|
||||||
|
expect(builds_names_and_statuses).to eq({ 'delayed': 'scheduled' })
|
||||||
|
|
||||||
|
enqueue_scheduled('delayed')
|
||||||
|
fail_running_or_pending
|
||||||
|
|
||||||
|
expect(builds_names_and_statuses).to eq({ 'delayed': 'failed', 'job': 'pending' })
|
||||||
|
expect(pipeline.reload.status).to eq 'pending'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context 'when there are manual action in earlier stages' do
|
context 'when there are manual action in earlier stages' do
|
||||||
context 'when first stage has only optional manual actions' do
|
context 'when first stage has only optional manual actions' do
|
||||||
before do
|
before do
|
||||||
|
@ -536,6 +717,10 @@ describe Ci::ProcessPipelineService, '#execute' do
|
||||||
builds.pluck(:name)
|
builds.pluck(:name)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def builds_names_and_statuses
|
||||||
|
builds.inject({}) { |h, b| h[b.name.to_sym] = b.status; h }
|
||||||
|
end
|
||||||
|
|
||||||
def all_builds_names
|
def all_builds_names
|
||||||
all_builds.pluck(:name)
|
all_builds.pluck(:name)
|
||||||
end
|
end
|
||||||
|
@ -549,7 +734,7 @@ describe Ci::ProcessPipelineService, '#execute' do
|
||||||
end
|
end
|
||||||
|
|
||||||
def succeed_pending
|
def succeed_pending
|
||||||
builds.pending.update_all(status: 'success')
|
builds.pending.map(&:success)
|
||||||
end
|
end
|
||||||
|
|
||||||
def succeed_running_or_pending
|
def succeed_running_or_pending
|
||||||
|
@ -568,6 +753,14 @@ describe Ci::ProcessPipelineService, '#execute' do
|
||||||
builds.find_by(name: name).play(user)
|
builds.find_by(name: name).play(user)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def enqueue_scheduled(name)
|
||||||
|
builds.scheduled.find_by(name: name).enqueue
|
||||||
|
end
|
||||||
|
|
||||||
|
def retry_build(name)
|
||||||
|
Ci::Build.retry(builds.find_by(name: name), user)
|
||||||
|
end
|
||||||
|
|
||||||
def manual_actions
|
def manual_actions
|
||||||
pipeline.manual_actions(true)
|
pipeline.manual_actions(true)
|
||||||
end
|
end
|
||||||
|
@ -575,4 +768,12 @@ describe Ci::ProcessPipelineService, '#execute' do
|
||||||
def create_build(name, **opts)
|
def create_build(name, **opts)
|
||||||
create(:ci_build, :created, pipeline: pipeline, name: name, **opts)
|
create(:ci_build, :created, pipeline: pipeline, name: name, **opts)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def delayed_options
|
||||||
|
{ when: 'delayed', options: { start_in: '1 minute' } }
|
||||||
|
end
|
||||||
|
|
||||||
|
def unschedule
|
||||||
|
pipeline.builds.scheduled.map(&:unschedule)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in New Issue