summaryrefslogtreecommitdiff
path: root/spec/models/ci/build_spec.rb
diff options
context:
space:
mode:
Diffstat (limited to 'spec/models/ci/build_spec.rb')
-rw-r--r--spec/models/ci/build_spec.rb383
1 files changed, 322 insertions, 61 deletions
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index 66d2f5f4ee9..62dec522161 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -111,10 +111,6 @@ RSpec.describe Ci::Build do
describe '.with_downloadable_artifacts' do
subject { described_class.with_downloadable_artifacts }
- before do
- stub_feature_flags(drop_license_management_artifact: false)
- end
-
context 'when job does not have a downloadable artifact' do
let!(:job) { create(:ci_build) }
@@ -320,11 +316,23 @@ RSpec.describe Ci::Build do
end
end
+ describe '#stick_build_if_status_changed' do
+ it 'sticks the build if the status changed' do
+ job = create(:ci_build, :pending)
+
+ allow(Gitlab::Database::LoadBalancing).to receive(:enable?)
+ .and_return(true)
+
+ expect(Gitlab::Database::LoadBalancing::Sticking).to receive(:stick)
+ .with(:build, job.id)
+
+ job.update!(status: :running)
+ end
+ end
+
describe '#enqueue' do
let(:build) { create(:ci_build, :created) }
- subject { build.enqueue }
-
before do
allow(build).to receive(:any_unmet_prerequisites?).and_return(has_prerequisites)
allow(Ci::PrepareBuildService).to receive(:perform_async)
@@ -334,28 +342,74 @@ RSpec.describe Ci::Build do
let(:has_prerequisites) { true }
it 'transitions to preparing' do
- subject
+ build.enqueue
expect(build).to be_preparing
end
+
+ it 'does not push build to the queue' do
+ build.enqueue
+
+ expect(build.queuing_entry).not_to be_present
+ end
end
context 'build has no prerequisites' do
let(:has_prerequisites) { false }
it 'transitions to pending' do
- subject
+ build.enqueue
expect(build).to be_pending
end
+
+ it 'pushes build to a queue' do
+ build.enqueue
+
+ expect(build.queuing_entry).to be_present
+ end
+
+ context 'when build status transition fails' do
+ before do
+ ::Ci::Build.find(build.id).update_column(:lock_version, 100)
+ end
+
+ it 'does not push build to a queue' do
+ expect { build.enqueue! }
+ .to raise_error(ActiveRecord::StaleObjectError)
+
+ expect(build.queuing_entry).not_to be_present
+ end
+ end
+
+ context 'when there is a queuing entry already present' do
+ before do
+ ::Ci::PendingBuild.create!(build: build, project: build.project)
+ end
+
+ it 'does not raise an error' do
+ expect { build.enqueue! }.not_to raise_error
+ expect(build.reload.queuing_entry).to be_present
+ end
+ end
+
+ context 'when both failure scenario happen at the same time' do
+ before do
+ ::Ci::Build.find(build.id).update_column(:lock_version, 100)
+ ::Ci::PendingBuild.create!(build: build, project: build.project)
+ end
+
+ it 'raises stale object error exception' do
+ expect { build.enqueue! }
+ .to raise_error(ActiveRecord::StaleObjectError)
+ end
+ end
end
end
describe '#enqueue_preparing' do
let(:build) { create(:ci_build, :preparing) }
- subject { build.enqueue_preparing }
-
before do
allow(build).to receive(:any_unmet_prerequisites?).and_return(has_unmet_prerequisites)
end
@@ -364,9 +418,10 @@ RSpec.describe Ci::Build do
let(:has_unmet_prerequisites) { false }
it 'transitions to pending' do
- subject
+ build.enqueue_preparing
expect(build).to be_pending
+ expect(build.queuing_entry).to be_present
end
end
@@ -374,9 +429,10 @@ RSpec.describe Ci::Build do
let(:has_unmet_prerequisites) { true }
it 'remains in preparing' do
- subject
+ build.enqueue_preparing
expect(build).to be_preparing
+ expect(build.queuing_entry).not_to be_present
end
end
end
@@ -405,6 +461,64 @@ RSpec.describe Ci::Build do
end
end
+ describe '#run' do
+ context 'when build has been just created' do
+ let(:build) { create(:ci_build, :created) }
+
+ it 'creates queuing entry and then removes it' do
+ build.enqueue!
+ expect(build.queuing_entry).to be_present
+
+ build.run!
+ expect(build.reload.queuing_entry).not_to be_present
+ end
+ end
+
+ context 'when build status transition fails' do
+ let(:build) { create(:ci_build, :pending) }
+
+ before do
+ ::Ci::PendingBuild.create!(build: build, project: build.project)
+ ::Ci::Build.find(build.id).update_column(:lock_version, 100)
+ end
+
+ it 'does not remove build from a queue' do
+ expect { build.run! }
+ .to raise_error(ActiveRecord::StaleObjectError)
+
+ expect(build.queuing_entry).to be_present
+ end
+ end
+
+ context 'when build has been picked by a shared runner' do
+ let(:build) { create(:ci_build, :pending) }
+
+ it 'creates runtime metadata entry' do
+ build.runner = create(:ci_runner, :instance_type)
+
+ build.run!
+
+ expect(build.reload.runtime_metadata).to be_present
+ end
+ end
+ end
+
+ describe '#drop' do
+ context 'when has a runtime tracking entry' do
+ let(:build) { create(:ci_build, :pending) }
+
+ it 'removes runtime tracking entry' do
+ build.runner = create(:ci_runner, :instance_type)
+
+ build.run!
+ expect(build.reload.runtime_metadata).to be_present
+
+ build.drop!
+ expect(build.reload.runtime_metadata).not_to be_present
+ end
+ end
+ end
+
describe '#schedulable?' do
subject { build.schedulable? }
@@ -586,28 +700,10 @@ RSpec.describe Ci::Build do
end
end
- context 'with runners_cached_states feature flag enabled' do
- before do
- stub_feature_flags(runners_cached_states: true)
- end
-
- it 'caches the result in Redis' do
- expect(Rails.cache).to receive(:fetch).with(['has-online-runners', build.id], expires_in: 1.minute)
-
- build.any_runners_online?
- end
- end
-
- context 'with runners_cached_states feature flag disabled' do
- before do
- stub_feature_flags(runners_cached_states: false)
- end
-
- it 'does not cache' do
- expect(Rails.cache).not_to receive(:fetch).with(['has-online-runners', build.id], expires_in: 1.minute)
+ it 'caches the result in Redis' do
+ expect(Rails.cache).to receive(:fetch).with(['has-online-runners', build.id], expires_in: 1.minute)
- build.any_runners_online?
- end
+ build.any_runners_online?
end
end
@@ -624,28 +720,10 @@ RSpec.describe Ci::Build do
it { is_expected.to be_truthy }
end
- context 'with runners_cached_states feature flag enabled' do
- before do
- stub_feature_flags(runners_cached_states: true)
- end
-
- it 'caches the result in Redis' do
- expect(Rails.cache).to receive(:fetch).with(['has-available-runners', build.project.id], expires_in: 1.minute)
-
- build.any_runners_available?
- end
- end
-
- context 'with runners_cached_states feature flag disabled' do
- before do
- stub_feature_flags(runners_cached_states: false)
- end
-
- it 'does not cache' do
- expect(Rails.cache).not_to receive(:fetch).with(['has-available-runners', build.project.id], expires_in: 1.minute)
+ it 'caches the result in Redis' do
+ expect(Rails.cache).to receive(:fetch).with(['has-available-runners', build.project.id], expires_in: 1.minute)
- build.any_runners_available?
- end
+ build.any_runners_available?
end
end
@@ -1650,8 +1728,6 @@ RSpec.describe Ci::Build do
subject { build.erase_erasable_artifacts! }
before do
- stub_feature_flags(drop_license_management_artifact: false)
-
Ci::JobArtifact.file_types.keys.each do |file_type|
create(:ci_job_artifact, job: build, file_type: file_type, file_format: Ci::JobArtifact::TYPE_AND_FORMAT_PAIRS[file_type.to_sym])
end
@@ -1840,6 +1916,26 @@ RSpec.describe Ci::Build do
it { is_expected.not_to be_retryable }
end
+
+ context 'when a canceled build has been retried already' do
+ before do
+ project.add_developer(user)
+ build.cancel!
+ described_class.retry(build, user)
+ end
+
+ context 'when prevent_retry_of_retried_jobs feature flag is enabled' do
+ it { is_expected.not_to be_retryable }
+ end
+
+ context 'when prevent_retry_of_retried_jobs feature flag is disabled' do
+ before do
+ stub_feature_flags(prevent_retry_of_retried_jobs: false)
+ end
+
+ it { is_expected.to be_retryable }
+ end
+ end
end
end
@@ -2525,7 +2621,6 @@ RSpec.describe Ci::Build do
{ key: 'CI_PROJECT_VISIBILITY', value: 'private', public: true, masked: false },
{ key: 'CI_PROJECT_REPOSITORY_LANGUAGES', value: project.repository_languages.map(&:name).join(',').downcase, public: true, masked: false },
{ key: 'CI_DEFAULT_BRANCH', value: project.default_branch, public: true, masked: false },
- { key: 'CI_PROJECT_CONFIG_PATH', value: project.ci_config_path_or_default, public: true, masked: false },
{ key: 'CI_CONFIG_PATH', value: project.ci_config_path_or_default, public: true, masked: false },
{ key: 'CI_PAGES_DOMAIN', value: Gitlab.config.pages.host, public: true, masked: false },
{ key: 'CI_PAGES_URL', value: project.pages_url, public: true, masked: false },
@@ -2566,6 +2661,17 @@ RSpec.describe Ci::Build do
it { is_expected.to be_instance_of(Gitlab::Ci::Variables::Collection) }
it { expect(subject.to_runner_variables).to eq(predefined_variables) }
+ it 'excludes variables that require an environment or user' do
+ environment_based_variables_collection = subject.filter do |variable|
+ %w[
+ YAML_VARIABLE CI_ENVIRONMENT_NAME CI_ENVIRONMENT_SLUG
+ CI_ENVIRONMENT_ACTION CI_ENVIRONMENT_URL
+ ].include?(variable[:key])
+ end
+
+ expect(environment_based_variables_collection).to be_empty
+ end
+
context 'when ci_job_jwt feature flag is disabled' do
before do
stub_feature_flags(ci_job_jwt: false)
@@ -2635,7 +2741,7 @@ RSpec.describe Ci::Build do
let(:expected_variables) do
predefined_variables.map { |variable| variable.fetch(:key) } +
%w[YAML_VARIABLE CI_ENVIRONMENT_NAME CI_ENVIRONMENT_SLUG
- CI_ENVIRONMENT_URL]
+ CI_ENVIRONMENT_TIER CI_ENVIRONMENT_ACTION CI_ENVIRONMENT_URL]
end
before do
@@ -2653,6 +2759,50 @@ RSpec.describe Ci::Build do
expect(received_variables).to eq expected_variables
end
+
+ describe 'CI_ENVIRONMENT_ACTION' do
+ let(:enviroment_action_variable) { subject.find { |variable| variable[:key] == 'CI_ENVIRONMENT_ACTION' } }
+
+ shared_examples 'defaults value' do
+ it 'value matches start' do
+ expect(enviroment_action_variable[:value]).to eq('start')
+ end
+ end
+
+ it_behaves_like 'defaults value'
+
+ context 'when options is set' do
+ before do
+ build.update!(options: options)
+ end
+
+ context 'when options is empty' do
+ let(:options) { {} }
+
+ it_behaves_like 'defaults value'
+ end
+
+ context 'when options is nil' do
+ let(:options) { nil }
+
+ it_behaves_like 'defaults value'
+ end
+
+ context 'when options environment is specified' do
+ let(:options) { { environment: {} } }
+
+ it_behaves_like 'defaults value'
+ end
+
+ context 'when options environment action specified' do
+ let(:options) { { environment: { action: 'stop' } } }
+
+ it 'matches the specified action' do
+ expect(enviroment_action_variable[:value]).to eq('stop')
+ end
+ end
+ end
+ end
end
end
end
@@ -2691,7 +2841,8 @@ RSpec.describe Ci::Build do
let(:environment_variables) do
[
{ key: 'CI_ENVIRONMENT_NAME', value: 'production', public: true, masked: false },
- { key: 'CI_ENVIRONMENT_SLUG', value: 'prod-slug', public: true, masked: false }
+ { key: 'CI_ENVIRONMENT_SLUG', value: 'prod-slug', public: true, masked: false },
+ { key: 'CI_ENVIRONMENT_TIER', value: 'production', public: true, masked: false }
]
end
@@ -2700,6 +2851,7 @@ RSpec.describe Ci::Build do
project: build.project,
name: 'production',
slug: 'prod-slug',
+ tier: 'production',
external_url: '')
end
@@ -4693,7 +4845,7 @@ RSpec.describe Ci::Build do
context 'with project services' do
before do
- create(:service, active: true, job_events: true, project: project)
+ create(:integration, active: true, job_events: true, project: project)
end
it 'executes services' do
@@ -4707,7 +4859,7 @@ RSpec.describe Ci::Build do
context 'without relevant project services' do
before do
- create(:service, active: true, job_events: false, project: project)
+ create(:integration, active: true, job_events: false, project: project)
end
it 'does not execute services' do
@@ -4987,4 +5139,113 @@ RSpec.describe Ci::Build do
it { is_expected.to be_truthy }
end
end
+
+ describe '.build_matchers' do
+ let_it_be(:pipeline) { create(:ci_pipeline, :protected) }
+
+ subject(:matchers) { pipeline.builds.build_matchers(pipeline.project) }
+
+ context 'when the pipeline is empty' do
+ it 'does not throw errors' do
+ is_expected.to eq([])
+ end
+ end
+
+ context 'when the pipeline has builds' do
+ let_it_be(:build_without_tags) do
+ create(:ci_build, pipeline: pipeline)
+ end
+
+ let_it_be(:build_with_tags) do
+ create(:ci_build, pipeline: pipeline, tag_list: %w[tag1 tag2])
+ end
+
+ let_it_be(:other_build_with_tags) do
+ create(:ci_build, pipeline: pipeline, tag_list: %w[tag2 tag1])
+ end
+
+ it { expect(matchers.size).to eq(2) }
+
+ it 'groups build ids' do
+ expect(matchers.map(&:build_ids)).to match_array([
+ [build_without_tags.id],
+ match_array([build_with_tags.id, other_build_with_tags.id])
+ ])
+ end
+
+ it { expect(matchers.map(&:tag_list)).to match_array([[], %w[tag1 tag2]]) }
+
+ it { expect(matchers.map(&:protected?)).to all be_falsey }
+
+ context 'when the builds are protected' do
+ before do
+ pipeline.builds.update_all(protected: true)
+ end
+
+ it { expect(matchers).to all be_protected }
+ end
+ end
+ end
+
+ describe '#build_matcher' do
+ let_it_be(:build) do
+ build_stubbed(:ci_build, tag_list: %w[tag1 tag2])
+ end
+
+ subject(:matcher) { build.build_matcher }
+
+ it { expect(matcher.build_ids).to eq([build.id]) }
+
+ it { expect(matcher.tag_list).to match_array(%w[tag1 tag2]) }
+
+ it { expect(matcher.protected?).to eq(build.protected?) }
+
+ it { expect(matcher.project).to eq(build.project) }
+ end
+
+ describe '#shared_runner_build?' do
+ context 'when build does not have a runner assigned' do
+ it 'is not a shared runner build' do
+ expect(build.runner).to be_nil
+
+ expect(build).not_to be_shared_runner_build
+ end
+ end
+
+ context 'when build has a project runner assigned' do
+ before do
+ build.runner = create(:ci_runner, :project)
+ end
+
+ it 'is not a shared runner build' do
+ expect(build).not_to be_shared_runner_build
+ end
+ end
+
+ context 'when build has an instance runner assigned' do
+ before do
+ build.runner = create(:ci_runner, :instance_type)
+ end
+
+ it 'is a shared runner build' do
+ expect(build).to be_shared_runner_build
+ end
+ end
+ end
+
+ describe '.without_coverage' do
+ let!(:build_with_coverage) { create(:ci_build, pipeline: pipeline, coverage: 100.0) }
+
+ it 'returns builds without coverage values' do
+ expect(described_class.without_coverage).to eq([build])
+ end
+ end
+
+ describe '.with_coverage_regex' do
+ let!(:build_with_coverage_regex) { create(:ci_build, pipeline: pipeline, coverage_regex: '\d') }
+
+ it 'returns builds with coverage regex values' do
+ expect(described_class.with_coverage_regex).to eq([build_with_coverage_regex])
+ end
+ end
end