summaryrefslogtreecommitdiff
path: root/spec/models/ci
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-05-19 15:44:42 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2021-05-19 15:44:42 +0000
commit4555e1b21c365ed8303ffb7a3325d773c9b8bf31 (patch)
tree5423a1c7516cffe36384133ade12572cf709398d /spec/models/ci
parente570267f2f6b326480d284e0164a6464ba4081bc (diff)
downloadgitlab-ce-4555e1b21c365ed8303ffb7a3325d773c9b8bf31.tar.gz
Add latest changes from gitlab-org/gitlab@13-12-stable-eev13.12.0-rc42
Diffstat (limited to 'spec/models/ci')
-rw-r--r--spec/models/ci/build_dependencies_spec.rb22
-rw-r--r--spec/models/ci/build_spec.rb82
-rw-r--r--spec/models/ci/commit_with_pipeline_spec.rb40
-rw-r--r--spec/models/ci/job_artifact_spec.rb28
-rw-r--r--spec/models/ci/pipeline_artifact_spec.rb24
-rw-r--r--spec/models/ci/pipeline_schedule_spec.rb30
-rw-r--r--spec/models/ci/pipeline_spec.rb123
-rw-r--r--spec/models/ci/runner_namespace_spec.rb9
-rw-r--r--spec/models/ci/runner_project_spec.rb9
-rw-r--r--spec/models/ci/stage_spec.rb12
10 files changed, 260 insertions, 119 deletions
diff --git a/spec/models/ci/build_dependencies_spec.rb b/spec/models/ci/build_dependencies_spec.rb
index e343ec0e698..d00d88ae397 100644
--- a/spec/models/ci/build_dependencies_spec.rb
+++ b/spec/models/ci/build_dependencies_spec.rb
@@ -18,12 +18,8 @@ RSpec.describe Ci::BuildDependencies do
let!(:rubocop_test) { create(:ci_build, pipeline: pipeline, name: 'rubocop', stage_idx: 1, stage: 'test') }
let!(:staging) { create(:ci_build, pipeline: pipeline, name: 'staging', stage_idx: 2, stage: 'deploy') }
- before do
- stub_feature_flags(ci_validate_build_dependencies_override: false)
- end
-
- describe '#local' do
- subject { described_class.new(job).local }
+ context 'for local dependencies' do
+ subject { described_class.new(job).all }
describe 'jobs from previous stages' do
context 'when job is in the first stage' do
@@ -52,7 +48,7 @@ RSpec.describe Ci::BuildDependencies do
project.add_developer(user)
end
- let(:retried_job) { Ci::Build.retry(rspec_test, user) }
+ let!(:retried_job) { Ci::Build.retry(rspec_test, user) }
it 'contains the retried job instead of the original one' do
is_expected.to contain_exactly(build, retried_job, rubocop_test)
@@ -150,7 +146,7 @@ RSpec.describe Ci::BuildDependencies do
end
end
- describe '#cross_pipeline' do
+ context 'for cross_pipeline dependencies' do
let!(:job) do
create(:ci_build,
pipeline: pipeline,
@@ -160,7 +156,7 @@ RSpec.describe Ci::BuildDependencies do
subject { described_class.new(job) }
- let(:cross_pipeline_deps) { subject.cross_pipeline }
+ let(:cross_pipeline_deps) { subject.all }
context 'when dependency specifications are valid' do
context 'when pipeline exists in the hierarchy' do
@@ -378,14 +374,6 @@ RSpec.describe Ci::BuildDependencies do
end
it { is_expected.to eq(false) }
-
- context 'when ci_validate_build_dependencies_override feature flag is enabled' do
- before do
- stub_feature_flags(ci_validate_build_dependencies_override: job.project)
- end
-
- it { is_expected.to eq(true) }
- end
end
end
end
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index 339dffa507f..66d2f5f4ee9 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -1132,7 +1132,7 @@ RSpec.describe Ci::Build do
it "executes UPDATE query" do
recorded = ActiveRecord::QueryRecorder.new { subject }
- expect(recorded.log.select { |l| l.match?(/UPDATE.*ci_builds/) }.count).to eq(1)
+ expect(recorded.log.count { |l| l.match?(/UPDATE.*ci_builds/) }).to eq(1)
end
end
@@ -1140,7 +1140,7 @@ RSpec.describe Ci::Build do
it 'does not execute UPDATE query' do
recorded = ActiveRecord::QueryRecorder.new { subject }
- expect(recorded.log.select { |l| l.match?(/UPDATE.*ci_builds/) }.count).to eq(0)
+ expect(recorded.log.count { |l| l.match?(/UPDATE.*ci_builds/) }).to eq(0)
end
end
end
@@ -1205,7 +1205,7 @@ RSpec.describe Ci::Build do
before do
allow(Deployments::LinkMergeRequestWorker).to receive(:perform_async)
- allow(Deployments::ExecuteHooksWorker).to receive(:perform_async)
+ allow(Deployments::HooksWorker).to receive(:perform_async)
end
it 'has deployments record with created status' do
@@ -1241,7 +1241,7 @@ RSpec.describe Ci::Build do
before do
allow(Deployments::UpdateEnvironmentWorker).to receive(:perform_async)
- allow(Deployments::ExecuteHooksWorker).to receive(:perform_async)
+ allow(Deployments::HooksWorker).to receive(:perform_async)
end
it_behaves_like 'avoid deadlock'
@@ -3631,46 +3631,29 @@ RSpec.describe Ci::Build do
end
let!(:job) { create(:ci_build, :pending, pipeline: pipeline, stage_idx: 1, options: options) }
+ let!(:pre_stage_job) { create(:ci_build, :success, pipeline: pipeline, name: 'test', stage_idx: 0) }
- context 'when validates for dependencies is enabled' do
- before do
- stub_feature_flags(ci_validate_build_dependencies_override: false)
- end
-
- let!(:pre_stage_job) { create(:ci_build, :success, pipeline: pipeline, name: 'test', stage_idx: 0) }
-
- context 'when "dependencies" keyword is not defined' do
- let(:options) { {} }
-
- it { expect(job).to have_valid_build_dependencies }
- end
-
- context 'when "dependencies" keyword is empty' do
- let(:options) { { dependencies: [] } }
+ context 'when "dependencies" keyword is not defined' do
+ let(:options) { {} }
- it { expect(job).to have_valid_build_dependencies }
- end
+ it { expect(job).to have_valid_build_dependencies }
+ end
- context 'when "dependencies" keyword is specified' do
- let(:options) { { dependencies: ['test'] } }
+ context 'when "dependencies" keyword is empty' do
+ let(:options) { { dependencies: [] } }
- it_behaves_like 'validation is active'
- end
+ it { expect(job).to have_valid_build_dependencies }
end
- context 'when validates for dependencies is disabled' do
+ context 'when "dependencies" keyword is specified' do
let(:options) { { dependencies: ['test'] } }
- before do
- stub_feature_flags(ci_validate_build_dependencies_override: true)
- end
-
- it_behaves_like 'validation is not active'
+ it_behaves_like 'validation is active'
end
end
describe 'state transition when build fails' do
- let(:service) { ::MergeRequests::AddTodoWhenBuildFailsService.new(project, user) }
+ let(:service) { ::MergeRequests::AddTodoWhenBuildFailsService.new(project: project, current_user: user) }
before do
allow(::MergeRequests::AddTodoWhenBuildFailsService).to receive(:new).and_return(service)
@@ -4679,25 +4662,30 @@ RSpec.describe Ci::Build do
end
describe '#execute_hooks' do
+ before do
+ build.clear_memoization(:build_data)
+ end
+
context 'with project hooks' do
+ let(:build_data) { double(:BuildData, dup: double(:DupedData)) }
+
before do
create(:project_hook, project: project, job_events: true)
end
- it 'execute hooks' do
- expect_any_instance_of(ProjectHook).to receive(:async_execute)
+ it 'calls project.execute_hooks(build_data, :job_hooks)' do
+ expect(::Gitlab::DataBuilder::Build)
+ .to receive(:build).with(build).and_return(build_data)
+ expect(build.project)
+ .to receive(:execute_hooks).with(build_data.dup, :job_hooks)
build.execute_hooks
end
end
- context 'without relevant project hooks' do
- before do
- create(:project_hook, project: project, job_events: false)
- end
-
- it 'does not execute a hook' do
- expect_any_instance_of(ProjectHook).not_to receive(:async_execute)
+ context 'without project hooks' do
+ it 'does not call project.execute_hooks' do
+ expect(build.project).not_to receive(:execute_hooks)
build.execute_hooks
end
@@ -4708,8 +4696,10 @@ RSpec.describe Ci::Build do
create(:service, active: true, job_events: true, project: project)
end
- it 'execute services' do
- expect_any_instance_of(Service).to receive(:async_execute)
+ it 'executes services' do
+ allow_next_found_instance_of(Integration) do |integration|
+ expect(integration).to receive(:async_execute)
+ end
build.execute_hooks
end
@@ -4720,8 +4710,10 @@ RSpec.describe Ci::Build do
create(:service, active: true, job_events: false, project: project)
end
- it 'execute services' do
- expect_any_instance_of(Service).not_to receive(:async_execute)
+ it 'does not execute services' do
+ allow_next_found_instance_of(Integration) do |integration|
+ expect(integration).not_to receive(:async_execute)
+ end
build.execute_hooks
end
diff --git a/spec/models/ci/commit_with_pipeline_spec.rb b/spec/models/ci/commit_with_pipeline_spec.rb
index 4dd288bde62..320143535e2 100644
--- a/spec/models/ci/commit_with_pipeline_spec.rb
+++ b/spec/models/ci/commit_with_pipeline_spec.rb
@@ -26,15 +26,47 @@ RSpec.describe Ci::CommitWithPipeline do
end
end
+ describe '#lazy_latest_pipeline' do
+ let(:commit_1) do
+ described_class.new(Commit.new(RepoHelpers.sample_commit, project))
+ end
+
+ let(:commit_2) do
+ described_class.new(Commit.new(RepoHelpers.another_sample_commit, project))
+ end
+
+ let!(:commits) { [commit_1, commit_2] }
+
+ it 'executes only 1 SQL query' do
+ recorder = ActiveRecord::QueryRecorder.new do
+ # Running this first ensures we don't run one query for every
+ # commit.
+ commits.each(&:lazy_latest_pipeline)
+
+ # This forces the execution of the SQL queries necessary to load the
+ # data.
+ commits.each { |c| c.latest_pipeline.try(:id) }
+ end
+
+ expect(recorder.count).to eq(1)
+ end
+ end
+
describe '#latest_pipeline' do
let(:pipeline) { double }
shared_examples_for 'fetching latest pipeline' do |ref|
it 'returns the latest pipeline for the project' do
- expect(commit)
- .to receive(:latest_pipeline_for_project)
- .with(ref, project)
- .and_return(pipeline)
+ if ref
+ expect(commit)
+ .to receive(:latest_pipeline_for_project)
+ .with(ref, project)
+ .and_return(pipeline)
+ else
+ expect(commit)
+ .to receive(:lazy_latest_pipeline)
+ .and_return(pipeline)
+ end
expect(result).to eq(pipeline)
end
diff --git a/spec/models/ci/job_artifact_spec.rb b/spec/models/ci/job_artifact_spec.rb
index cdb123573f1..3c4769764d5 100644
--- a/spec/models/ci/job_artifact_spec.rb
+++ b/spec/models/ci/job_artifact_spec.rb
@@ -602,6 +602,34 @@ RSpec.describe Ci::JobArtifact do
end
end
+ context 'FastDestroyAll' do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
+ let_it_be(:job) { create(:ci_build, pipeline: pipeline, project: project) }
+
+ let!(:job_artifact) { create(:ci_job_artifact, :archive, job: job) }
+ let(:subjects) { pipeline.job_artifacts }
+
+ describe '.use_fast_destroy' do
+ it 'performs cascading delete with fast_destroy_all' do
+ expect(Ci::DeletedObject.count).to eq(0)
+ expect(subjects.count).to be > 0
+
+ expect { pipeline.destroy! }.not_to raise_error
+
+ expect(subjects.count).to eq(0)
+ expect(Ci::DeletedObject.count).to be > 0
+ end
+
+ it 'updates project statistics' do
+ expect(ProjectStatistics).to receive(:increment_statistic).once
+ .with(project, :build_artifacts_size, -job_artifact.file.size)
+
+ pipeline.destroy!
+ end
+ end
+ end
+
def file_type_limit_failure_message(type, limit_name)
<<~MSG
The artifact type `#{type}` is missing its counterpart plan limit which is expected to be named `#{limit_name}`.
diff --git a/spec/models/ci/pipeline_artifact_spec.rb b/spec/models/ci/pipeline_artifact_spec.rb
index 3fe09f05cab..f65483d2290 100644
--- a/spec/models/ci/pipeline_artifact_spec.rb
+++ b/spec/models/ci/pipeline_artifact_spec.rb
@@ -50,6 +50,30 @@ RSpec.describe Ci::PipelineArtifact, type: :model do
end
end
+ describe 'scopes' do
+ describe '.unlocked' do
+ subject(:pipeline_artifacts) { described_class.unlocked }
+
+ context 'when pipeline is locked' do
+ it 'returns an empty collection' do
+ expect(pipeline_artifacts).to be_empty
+ end
+ end
+
+ context 'when pipeline is unlocked' do
+ before do
+ create(:ci_pipeline_artifact, :with_coverage_report)
+ end
+
+ it 'returns unlocked artifacts' do
+ codequality_report = create(:ci_pipeline_artifact, :with_codequality_mr_diff_report, :unlocked)
+
+ expect(pipeline_artifacts).to eq([codequality_report])
+ end
+ end
+ end
+ end
+
describe 'file is being stored' do
subject { create(:ci_pipeline_artifact, :with_coverage_report) }
diff --git a/spec/models/ci/pipeline_schedule_spec.rb b/spec/models/ci/pipeline_schedule_spec.rb
index 3e5fbbfe823..d5560edbbfd 100644
--- a/spec/models/ci/pipeline_schedule_spec.rb
+++ b/spec/models/ci/pipeline_schedule_spec.rb
@@ -126,16 +126,6 @@ RSpec.describe Ci::PipelineSchedule do
end
end
- context 'when pipeline schedule runs every minute' do
- let(:pipeline_schedule) { create(:ci_pipeline_schedule, :every_minute) }
-
- it "updates next_run_at to the sidekiq worker's execution time" do
- travel_to(Time.zone.parse("2019-06-01 12:18:00+0000")) do
- expect(pipeline_schedule.next_run_at).to eq(cron_worker_next_run_at)
- end
- end
- end
-
context 'when there are two different pipeline schedules in different time zones' do
let(:pipeline_schedule_1) { create(:ci_pipeline_schedule, :weekly, cron_timezone: 'Eastern Time (US & Canada)') }
let(:pipeline_schedule_2) { create(:ci_pipeline_schedule, :weekly, cron_timezone: 'UTC') }
@@ -144,24 +134,6 @@ RSpec.describe Ci::PipelineSchedule do
expect(pipeline_schedule_1.next_run_at).not_to eq(pipeline_schedule_2.next_run_at)
end
end
-
- context 'when there are two different pipeline schedules in the same time zones' do
- let(:pipeline_schedule_1) { create(:ci_pipeline_schedule, :weekly, cron_timezone: 'UTC') }
- let(:pipeline_schedule_2) { create(:ci_pipeline_schedule, :weekly, cron_timezone: 'UTC') }
-
- it 'sets the sames next_run_at' do
- expect(pipeline_schedule_1.next_run_at).to eq(pipeline_schedule_2.next_run_at)
- end
- end
-
- context 'when updates cron of exsisted pipeline schedule' do
- let(:new_cron) { '0 0 1 1 *' }
-
- it 'updates next_run_at automatically' do
- expect { pipeline_schedule.update!(cron: new_cron) }
- .to change { pipeline_schedule.next_run_at }
- end
- end
end
describe '#schedule_next_run!' do
@@ -178,7 +150,7 @@ RSpec.describe Ci::PipelineSchedule do
context 'when record is invalid' do
before do
- allow(pipeline_schedule).to receive(:save!) { raise ActiveRecord::RecordInvalid.new(pipeline_schedule) }
+ allow(pipeline_schedule).to receive(:save!) { raise ActiveRecord::RecordInvalid, pipeline_schedule }
end
it 'nullifies the next run at' do
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index b7f5811e945..b9457055a18 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -68,14 +68,23 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
end
describe '#downloadable_artifacts' do
- let(:build) { create(:ci_build, pipeline: pipeline) }
+ let_it_be(:build) { create(:ci_build, pipeline: pipeline) }
+ let_it_be(:downloadable_artifact) { create(:ci_job_artifact, :codequality, job: build) }
+ let_it_be(:expired_artifact) { create(:ci_job_artifact, :junit, :expired, job: build) }
+ let_it_be(:undownloadable_artifact) { create(:ci_job_artifact, :trace, job: build) }
+
+ context 'when artifacts are locked' do
+ it 'returns downloadable artifacts including locked artifacts' do
+ expect(pipeline.downloadable_artifacts).to contain_exactly(downloadable_artifact, expired_artifact)
+ end
+ end
- it 'returns downloadable artifacts that have not expired' do
- downloadable_artifact = create(:ci_job_artifact, :codequality, job: build)
- _expired_artifact = create(:ci_job_artifact, :junit, :expired, job: build)
- _undownloadable_artifact = create(:ci_job_artifact, :trace, job: build)
+ context 'when artifacts are unlocked' do
+ it 'returns only downloadable artifacts not expired' do
+ expired_artifact.job.pipeline.unlocked!
- expect(pipeline.downloadable_artifacts).to contain_exactly(downloadable_artifact)
+ expect(pipeline.reload.downloadable_artifacts).to contain_exactly(downloadable_artifact)
+ end
end
end
end
@@ -1939,6 +1948,30 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
expect(pipeline.modified_paths).to match(merge_request.modified_paths)
end
end
+
+ context 'when source is an external pull request' do
+ let(:pipeline) do
+ create(:ci_pipeline, source: :external_pull_request_event, external_pull_request: external_pull_request)
+ end
+
+ let(:external_pull_request) do
+ create(:external_pull_request, project: project, target_sha: '281d3a7', source_sha: '498214d')
+ end
+
+ it 'returns external pull request modified paths' do
+ expect(pipeline.modified_paths).to match(external_pull_request.modified_paths)
+ end
+
+ context 'when the FF ci_modified_paths_of_external_prs is disabled' do
+ before do
+ stub_feature_flags(ci_modified_paths_of_external_prs: false)
+ end
+
+ it 'returns nil' do
+ expect(pipeline.modified_paths).to be_nil
+ end
+ end
+ end
end
describe '#all_worktree_paths' do
@@ -3201,18 +3234,6 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
expect(pipeline.messages.map(&:content)).to contain_exactly('The error message')
end
-
- context 'when feature flag ci_store_pipeline_messages is disabled' do
- before do
- stub_feature_flags(ci_store_pipeline_messages: false)
- end
-
- it 'does not add pipeline error message' do
- pipeline.add_error_message('The error message')
-
- expect(pipeline.messages).to be_empty
- end
- end
end
describe '#has_yaml_errors?' do
@@ -4303,26 +4324,80 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
end
end
- describe 'reset_ancestor_bridges!' do
- let_it_be(:pipeline) { create(:ci_pipeline, :created) }
+ describe '#reset_source_bridge!' do
+ let(:pipeline) { create(:ci_pipeline, :created, project: project) }
+
+ subject(:reset_bridge) { pipeline.reset_source_bridge!(project.owner) }
+
+ # This whole block will be removed by https://gitlab.com/gitlab-org/gitlab/-/issues/329194
+ # It contains some duplicate checks.
+ context 'when the FF ci_reset_bridge_with_subsequent_jobs is disabled' do
+ before do
+ stub_feature_flags(ci_reset_bridge_with_subsequent_jobs: false)
+ end
+
+ context 'when the pipeline is a child pipeline and the bridge is depended' do
+ let!(:parent_pipeline) { create(:ci_pipeline) }
+ let!(:bridge) { create_bridge(parent_pipeline, pipeline, true) }
+
+ it 'marks source bridge as pending' do
+ reset_bridge
+
+ expect(bridge.reload).to be_pending
+ end
+
+ context 'when the parent pipeline has subsequent jobs after the bridge' do
+ let!(:after_bridge_job) { create(:ci_build, :skipped, pipeline: parent_pipeline, stage_idx: bridge.stage_idx + 1) }
+
+ it 'does not touch subsequent jobs of the bridge' do
+ reset_bridge
+
+ expect(after_bridge_job.reload).to be_skipped
+ end
+ 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
+ reset_bridge
+
+ expect(bridge.reload).to be_pending
+ expect(upstream_bridge.reload).to be_pending
+ end
+ end
+ end
+ end
context 'when the pipeline is a child pipeline and the bridge is depended' do
let!(:parent_pipeline) { create(:ci_pipeline) }
let!(:bridge) { create_bridge(parent_pipeline, pipeline, true) }
it 'marks source bridge as pending' do
- pipeline.reset_ancestor_bridges!
+ reset_bridge
expect(bridge.reload).to be_pending
end
+ context 'when the parent pipeline has subsequent jobs after the bridge' do
+ let!(:after_bridge_job) { create(:ci_build, :skipped, pipeline: parent_pipeline, stage_idx: bridge.stage_idx + 1) }
+
+ it 'marks subsequent jobs of the bridge as processable' do
+ reset_bridge
+
+ expect(after_bridge_job.reload).to be_created
+ end
+ 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!
+ reset_bridge
expect(bridge.reload).to be_pending
expect(upstream_bridge.reload).to be_pending
@@ -4335,7 +4410,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
let!(:bridge) { create_bridge(parent_pipeline, pipeline, false) }
it 'does not touch source bridge' do
- pipeline.reset_ancestor_bridges!
+ reset_bridge
expect(bridge.reload).to be_success
end
@@ -4346,7 +4421,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
end
it 'does not touch any source bridge' do
- pipeline.reset_ancestor_bridges!
+ reset_bridge
expect(bridge.reload).to be_success
expect(upstream_bridge.reload).to be_success
diff --git a/spec/models/ci/runner_namespace_spec.rb b/spec/models/ci/runner_namespace_spec.rb
new file mode 100644
index 00000000000..41d805adb9f
--- /dev/null
+++ b/spec/models/ci/runner_namespace_spec.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ci::RunnerNamespace do
+ it_behaves_like 'includes Limitable concern' do
+ subject { build(:ci_runner_namespace, group: create(:group, :nested), runner: create(:ci_runner, :group)) }
+ end
+end
diff --git a/spec/models/ci/runner_project_spec.rb b/spec/models/ci/runner_project_spec.rb
new file mode 100644
index 00000000000..13369dba2cf
--- /dev/null
+++ b/spec/models/ci/runner_project_spec.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ci::RunnerProject do
+ it_behaves_like 'includes Limitable concern' do
+ subject { build(:ci_runner_project, project: create(:project), runner: create(:ci_runner, :project)) }
+ end
+end
diff --git a/spec/models/ci/stage_spec.rb b/spec/models/ci/stage_spec.rb
index e46d9189c86..5e0fcb4882f 100644
--- a/spec/models/ci/stage_spec.rb
+++ b/spec/models/ci/stage_spec.rb
@@ -286,6 +286,18 @@ RSpec.describe Ci::Stage, :models do
end
end
+ context 'when stage has statuses with nil idx' do
+ before do
+ create(:ci_build, :running, stage_id: stage.id, stage_idx: nil)
+ create(:ci_build, :running, stage_id: stage.id, stage_idx: 10)
+ create(:ci_build, :running, stage_id: stage.id, stage_idx: nil)
+ end
+
+ it 'sets index to a non-empty value' do
+ expect { stage.update_legacy_status }.to change { stage.reload.position }.from(nil).to(10)
+ end
+ end
+
context 'when stage does not have statuses' do
it 'fallbacks to zero' do
expect(stage.reload.position).to be_nil