diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-06-16 18:25:58 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-06-16 18:25:58 +0000 |
commit | a5f4bba440d7f9ea47046a0a561d49adf0a1e6d4 (patch) | |
tree | fb69158581673816a8cd895f9d352dcb3c678b1e /spec/models/ci | |
parent | d16b2e8639e99961de6ddc93909f3bb5c1445ba1 (diff) | |
download | gitlab-ce-a5f4bba440d7f9ea47046a0a561d49adf0a1e6d4.tar.gz |
Add latest changes from gitlab-org/gitlab@14-0-stable-eev14.0.0-rc42
Diffstat (limited to 'spec/models/ci')
-rw-r--r-- | spec/models/ci/build_dependencies_spec.rb | 9 | ||||
-rw-r--r-- | spec/models/ci/build_spec.rb | 383 | ||||
-rw-r--r-- | spec/models/ci/build_trace_chunk_spec.rb | 116 | ||||
-rw-r--r-- | spec/models/ci/build_trace_chunks/database_spec.rb | 6 | ||||
-rw-r--r-- | spec/models/ci/build_trace_chunks/redis_spec.rb | 6 | ||||
-rw-r--r-- | spec/models/ci/job_artifact_spec.rb | 26 | ||||
-rw-r--r-- | spec/models/ci/job_token/project_scope_link_spec.rb | 68 | ||||
-rw-r--r-- | spec/models/ci/job_token/scope_spec.rb | 65 | ||||
-rw-r--r-- | spec/models/ci/pending_build_spec.rb | 33 | ||||
-rw-r--r-- | spec/models/ci/pipeline_schedule_spec.rb | 77 | ||||
-rw-r--r-- | spec/models/ci/pipeline_spec.rb | 126 | ||||
-rw-r--r-- | spec/models/ci/runner_spec.rb | 177 | ||||
-rw-r--r-- | spec/models/ci/running_build_spec.rb | 55 |
13 files changed, 971 insertions, 176 deletions
diff --git a/spec/models/ci/build_dependencies_spec.rb b/spec/models/ci/build_dependencies_spec.rb index d00d88ae397..331ba9953ca 100644 --- a/spec/models/ci/build_dependencies_spec.rb +++ b/spec/models/ci/build_dependencies_spec.rb @@ -187,15 +187,6 @@ RSpec.describe Ci::BuildDependencies do it { expect(cross_pipeline_deps).to contain_exactly(upstream_job) } it { is_expected.to be_valid } end - - context 'when feature flag `ci_cross_pipeline_artifacts_download` is disabled' do - before do - stub_feature_flags(ci_cross_pipeline_artifacts_download: false) - end - - it { expect(cross_pipeline_deps).to be_empty } - it { is_expected.to be_valid } - end end context 'when same job names exist in other pipelines in the hierarchy' do 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 diff --git a/spec/models/ci/build_trace_chunk_spec.rb b/spec/models/ci/build_trace_chunk_spec.rb index 12bc5d9aa3c..a16453f3d01 100644 --- a/spec/models/ci/build_trace_chunk_spec.rb +++ b/spec/models/ci/build_trace_chunk_spec.rb @@ -2,13 +2,13 @@ require 'spec_helper' -RSpec.describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do +RSpec.describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state, :clean_gitlab_redis_trace_chunks do include ExclusiveLeaseHelpers let_it_be(:build) { create(:ci_build, :running) } let(:chunk_index) { 0 } - let(:data_store) { :redis } + let(:data_store) { :redis_trace_chunks } let(:raw_data) { nil } let(:build_trace_chunk) do @@ -18,10 +18,17 @@ RSpec.describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do it_behaves_like 'having unique enum values' before do - stub_feature_flags(ci_enable_live_trace: true, gitlab_ci_trace_read_consistency: true) + stub_feature_flags(ci_enable_live_trace: true) stub_artifacts_object_storage end + def redis_instance + { + redis: Gitlab::Redis::SharedState, + redis_trace_chunks: Gitlab::Redis::TraceChunks + }[data_store] + end + describe 'chunk creation' do let(:metrics) { spy('metrics') } @@ -85,7 +92,7 @@ RSpec.describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do end def external_data_counter - Gitlab::Redis::SharedState.with do |redis| + redis_instance.with do |redis| redis.scan_each(match: "gitlab:ci:trace:*:chunks:*").to_a.size end end @@ -101,24 +108,16 @@ RSpec.describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do subject { described_class.all_stores } it 'returns a correctly ordered array' do - is_expected.to eq(%i[redis database fog]) - end - - it 'returns redis store as the lowest precedence' do - expect(subject.first).to eq(:redis) - end - - it 'returns fog store as the highest precedence' do - expect(subject.last).to eq(:fog) + is_expected.to eq(%i[redis database fog redis_trace_chunks]) end end describe '#data' do subject { build_trace_chunk.data } - context 'when data_store is redis' do - let(:data_store) { :redis } + where(:data_store) { %i[redis redis_trace_chunks] } + with_them do before do build_trace_chunk.send(:unsafe_set_data!, +'Sample data in redis') end @@ -148,6 +147,22 @@ RSpec.describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do end end + describe '#data_store' do + subject { described_class.new.data_store } + + context 'default value' do + it { expect(subject).to eq('redis_trace_chunks') } + + context 'when dedicated_redis_trace_chunks is disabled' do + before do + stub_feature_flags(dedicated_redis_trace_chunks: false) + end + + it { expect(subject).to eq('redis') } + end + end + end + describe '#get_store_class' do using RSpec::Parameterized::TableSyntax @@ -155,6 +170,7 @@ RSpec.describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do :redis | Ci::BuildTraceChunks::Redis :database | Ci::BuildTraceChunks::Database :fog | Ci::BuildTraceChunks::Fog + :redis_trace_chunks | Ci::BuildTraceChunks::RedisTraceChunks end with_them do @@ -302,9 +318,9 @@ RSpec.describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do end end - context 'when data_store is redis' do - let(:data_store) { :redis } + where(:data_store) { %i[redis redis_trace_chunks] } + with_them do context 'when there are no data' do let(:data) { +'' } @@ -441,8 +457,9 @@ RSpec.describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do end end - context 'when data_store is redis' do - let(:data_store) { :redis } + where(:data_store) { %i[redis redis_trace_chunks] } + + with_them do let(:data) { +'Sample data in redis' } before do @@ -475,9 +492,9 @@ RSpec.describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do describe '#size' do subject { build_trace_chunk.size } - context 'when data_store is redis' do - let(:data_store) { :redis } + where(:data_store) { %i[redis redis_trace_chunks] } + with_them do context 'when data exists' do let(:data) { +'Sample data in redis' } @@ -537,9 +554,14 @@ RSpec.describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do subject { build_trace_chunk.persist_data! } - context 'when data_store is redis' do - let(:data_store) { :redis } + where(:data_store, :redis_class) do + [ + [:redis, Ci::BuildTraceChunks::Redis], + [:redis_trace_chunks, Ci::BuildTraceChunks::RedisTraceChunks] + ] + end + with_them do context 'when data exists' do before do build_trace_chunk.send(:unsafe_set_data!, data) @@ -549,15 +571,15 @@ RSpec.describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do let(:data) { +'a' * described_class::CHUNK_SIZE } it 'persists the data' do - expect(build_trace_chunk.redis?).to be_truthy - expect(Ci::BuildTraceChunks::Redis.new.data(build_trace_chunk)).to eq(data) + expect(build_trace_chunk.data_store).to eq(data_store.to_s) + expect(redis_class.new.data(build_trace_chunk)).to eq(data) expect(Ci::BuildTraceChunks::Database.new.data(build_trace_chunk)).to be_nil expect(Ci::BuildTraceChunks::Fog.new.data(build_trace_chunk)).to be_nil subject expect(build_trace_chunk.fog?).to be_truthy - expect(Ci::BuildTraceChunks::Redis.new.data(build_trace_chunk)).to be_nil + expect(redis_class.new.data(build_trace_chunk)).to be_nil expect(Ci::BuildTraceChunks::Database.new.data(build_trace_chunk)).to be_nil expect(Ci::BuildTraceChunks::Fog.new.data(build_trace_chunk)).to eq(data) end @@ -575,8 +597,8 @@ RSpec.describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do it 'does not persist the data and the orignal data is intact' do expect { subject }.to raise_error(described_class::FailedToPersistDataError) - expect(build_trace_chunk.redis?).to be_truthy - expect(Ci::BuildTraceChunks::Redis.new.data(build_trace_chunk)).to eq(data) + expect(build_trace_chunk.data_store).to eq(data_store.to_s) + expect(redis_class.new.data(build_trace_chunk)).to eq(data) expect(Ci::BuildTraceChunks::Database.new.data(build_trace_chunk)).to be_nil expect(Ci::BuildTraceChunks::Fog.new.data(build_trace_chunk)).to be_nil end @@ -810,7 +832,7 @@ RSpec.describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do shared_examples_for 'deletes all build_trace_chunk and data in redis' do it 'deletes all build_trace_chunk and data in redis', :sidekiq_might_not_need_inline do - Gitlab::Redis::SharedState.with do |redis| + redis_instance.with do |redis| expect(redis.scan_each(match: "gitlab:ci:trace:*:chunks:*").to_a.size).to eq(3) end @@ -820,7 +842,7 @@ RSpec.describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do expect(described_class.count).to eq(0) - Gitlab::Redis::SharedState.with do |redis| + redis_instance.with do |redis| expect(redis.scan_each(match: "gitlab:ci:trace:*:chunks:*").to_a.size).to eq(0) end end @@ -902,4 +924,38 @@ RSpec.describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do end end end + + describe '#live?' do + subject { build_trace_chunk.live? } + + where(:data_store, :value) do + [ + [:redis, true], + [:redis_trace_chunks, true], + [:database, false], + [:fog, false] + ] + end + + with_them do + it { is_expected.to eq(value) } + end + end + + describe '#flushed?' do + subject { build_trace_chunk.flushed? } + + where(:data_store, :value) do + [ + [:redis, false], + [:redis_trace_chunks, false], + [:database, true], + [:fog, true] + ] + end + + with_them do + it { is_expected.to eq(value) } + end + end end diff --git a/spec/models/ci/build_trace_chunks/database_spec.rb b/spec/models/ci/build_trace_chunks/database_spec.rb index 313328ac037..d99aede853c 100644 --- a/spec/models/ci/build_trace_chunks/database_spec.rb +++ b/spec/models/ci/build_trace_chunks/database_spec.rb @@ -5,12 +5,6 @@ require 'spec_helper' RSpec.describe Ci::BuildTraceChunks::Database do let(:data_store) { described_class.new } - describe '#available?' do - subject { data_store.available? } - - it { is_expected.to be_truthy } - end - describe '#data' do subject { data_store.data(model) } diff --git a/spec/models/ci/build_trace_chunks/redis_spec.rb b/spec/models/ci/build_trace_chunks/redis_spec.rb index cb0b6baadeb..c004887d609 100644 --- a/spec/models/ci/build_trace_chunks/redis_spec.rb +++ b/spec/models/ci/build_trace_chunks/redis_spec.rb @@ -5,12 +5,6 @@ require 'spec_helper' RSpec.describe Ci::BuildTraceChunks::Redis, :clean_gitlab_redis_shared_state do let(:data_store) { described_class.new } - describe '#available?' do - subject { data_store.available? } - - it { is_expected.to be_truthy } - end - describe '#data' do subject { data_store.data(model) } diff --git a/spec/models/ci/job_artifact_spec.rb b/spec/models/ci/job_artifact_spec.rb index 3c4769764d5..582639b105e 100644 --- a/spec/models/ci/job_artifact_spec.rb +++ b/spec/models/ci/job_artifact_spec.rb @@ -328,35 +328,9 @@ RSpec.describe Ci::JobArtifact do end end - describe 'validates if file format is supported' do - subject { artifact } - - let(:artifact) { build(:ci_job_artifact, file_type: :license_management, file_format: :raw) } - - context 'when license_management is supported' do - before do - stub_feature_flags(drop_license_management_artifact: false) - end - - it { is_expected.to be_valid } - end - - context 'when license_management is not supported' do - before do - stub_feature_flags(drop_license_management_artifact: true) - end - - it { is_expected.not_to be_valid } - end - end - describe 'validates file format' do subject { artifact } - before do - stub_feature_flags(drop_license_management_artifact: false) - end - described_class::TYPE_AND_FORMAT_PAIRS.except(:trace).each do |file_type, file_format| context "when #{file_type} type with #{file_format} format" do let(:artifact) { build(:ci_job_artifact, file_type: file_type, file_format: file_format) } diff --git a/spec/models/ci/job_token/project_scope_link_spec.rb b/spec/models/ci/job_token/project_scope_link_spec.rb new file mode 100644 index 00000000000..d18495b9312 --- /dev/null +++ b/spec/models/ci/job_token/project_scope_link_spec.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Ci::JobToken::ProjectScopeLink do + it { is_expected.to belong_to(:source_project) } + it { is_expected.to belong_to(:target_project) } + it { is_expected.to belong_to(:added_by) } + + let_it_be(:project) { create(:project) } + + describe 'unique index' do + let!(:link) { create(:ci_job_token_project_scope_link) } + + it 'raises an error' do + expect do + create(:ci_job_token_project_scope_link, + source_project: link.source_project, + target_project: link.target_project) + end.to raise_error(ActiveRecord::RecordNotUnique) + end + end + + describe 'validations' do + it 'must have a source project', :aggregate_failures do + link = build(:ci_job_token_project_scope_link, source_project: nil) + + expect(link).not_to be_valid + expect(link.errors[:source_project]).to contain_exactly("can't be blank") + end + + it 'must have a target project', :aggregate_failures do + link = build(:ci_job_token_project_scope_link, target_project: nil) + + expect(link).not_to be_valid + expect(link.errors[:target_project]).to contain_exactly("can't be blank") + end + + it 'must have a target project different than source project', :aggregate_failures do + link = build(:ci_job_token_project_scope_link, target_project: project, source_project: project) + + expect(link).not_to be_valid + expect(link.errors[:target_project]).to contain_exactly("can't be the same as the source project") + end + end + + describe '.from_project' do + subject { described_class.from_project(project) } + + let!(:source_link) { create(:ci_job_token_project_scope_link, source_project: project) } + let!(:target_link) { create(:ci_job_token_project_scope_link, target_project: project) } + + it 'returns only the links having the given source project' do + expect(subject).to contain_exactly(source_link) + end + end + + describe '.to_project' do + subject { described_class.to_project(project) } + + let!(:source_link) { create(:ci_job_token_project_scope_link, source_project: project) } + let!(:target_link) { create(:ci_job_token_project_scope_link, target_project: project) } + + it 'returns only the links having the given target project' do + expect(subject).to contain_exactly(target_link) + end + end +end diff --git a/spec/models/ci/job_token/scope_spec.rb b/spec/models/ci/job_token/scope_spec.rb new file mode 100644 index 00000000000..c731a2634f5 --- /dev/null +++ b/spec/models/ci/job_token/scope_spec.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Ci::JobToken::Scope do + let_it_be(:project) { create(:project) } + + let(:scope) { described_class.new(project) } + + describe '#all_projects' do + subject(:all_projects) { scope.all_projects } + + context 'when no projects are added to the scope' do + it 'returns the project defining the scope' do + expect(all_projects).to contain_exactly(project) + end + end + + context 'when other projects are added to the scope' do + let_it_be(:scoped_project) { create(:project) } + let_it_be(:unscoped_project) { create(:project) } + + let!(:link_in_scope) { create(:ci_job_token_project_scope_link, source_project: project, target_project: scoped_project) } + let!(:link_out_of_scope) { create(:ci_job_token_project_scope_link, target_project: unscoped_project) } + + it 'returns all projects that can be accessed from a given scope' do + expect(subject).to contain_exactly(project, scoped_project) + end + end + end + + describe 'includes?' do + subject { scope.includes?(target_project) } + + context 'when param is the project defining the scope' do + let(:target_project) { project } + + it { is_expected.to be_truthy } + end + + context 'when param is a project in scope' do + let(:target_link) { create(:ci_job_token_project_scope_link, source_project: project) } + let(:target_project) { target_link.target_project } + + it { is_expected.to be_truthy } + end + + context 'when param is a project in another scope' do + let(:scope_link) { create(:ci_job_token_project_scope_link) } + let(:target_project) { scope_link.target_project } + + it { is_expected.to be_falsey } + + context 'when project scope setting is disabled' do + before do + project.ci_job_token_scope_enabled = false + end + + it 'considers any project to be part of the scope' do + expect(subject).to be_truthy + end + end + end + end +end diff --git a/spec/models/ci/pending_build_spec.rb b/spec/models/ci/pending_build_spec.rb new file mode 100644 index 00000000000..c1d4f4b0a5e --- /dev/null +++ b/spec/models/ci/pending_build_spec.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Ci::PendingBuild do + let_it_be(:project) { create(:project) } + let_it_be(:pipeline) { create(:ci_pipeline, project: project) } + + let(:build) { create(:ci_build, :created, pipeline: pipeline) } + + describe '.upsert_from_build!' do + context 'another pending entry does not exist' do + it 'creates a new pending entry' do + result = described_class.upsert_from_build!(build) + + expect(result.rows.dig(0, 0)).to eq build.id + expect(build.reload.queuing_entry).to be_present + end + end + + context 'when another queuing entry exists for given build' do + before do + described_class.create!(build: build, project: project, protected: false) + end + + it 'returns a build id as a result' do + result = described_class.upsert_from_build!(build) + + expect(result.rows.dig(0, 0)).to eq build.id + end + end + end +end diff --git a/spec/models/ci/pipeline_schedule_spec.rb b/spec/models/ci/pipeline_schedule_spec.rb index d5560edbbfd..cf73460bf1e 100644 --- a/spec/models/ci/pipeline_schedule_spec.rb +++ b/spec/models/ci/pipeline_schedule_spec.rb @@ -3,6 +3,8 @@ require 'spec_helper' RSpec.describe Ci::PipelineSchedule do + let_it_be(:project) { create_default(:project) } + subject { build(:ci_pipeline_schedule) } it { is_expected.to belong_to(:project) } @@ -18,7 +20,7 @@ RSpec.describe Ci::PipelineSchedule do it { is_expected.to respond_to(:next_run_at) } it_behaves_like 'includes Limitable concern' do - subject { build(:ci_pipeline_schedule) } + subject { build(:ci_pipeline_schedule, project: project) } end describe 'validations' do @@ -103,26 +105,49 @@ RSpec.describe Ci::PipelineSchedule do end describe '#set_next_run_at' do - let(:pipeline_schedule) { create(:ci_pipeline_schedule, :nightly) } - let(:ideal_next_run_at) { pipeline_schedule.send(:ideal_next_run_from, Time.zone.now) } - let(:cron_worker_next_run_at) { pipeline_schedule.send(:cron_worker_next_run_from, Time.zone.now) } + using RSpec::Parameterized::TableSyntax + + where(:worker_cron, :schedule_cron, :plan_limit, :ff_enabled, :now, :result) do + '0 1 2 3 *' | '0 1 * * *' | nil | true | Time.zone.local(2021, 3, 2, 1, 0) | Time.zone.local(2022, 3, 2, 1, 0) + '0 1 2 3 *' | '0 1 * * *' | (1.day.in_minutes / 1.hour.in_minutes).to_i | true | Time.zone.local(2021, 3, 2, 1, 0) | Time.zone.local(2022, 3, 2, 1, 0) + '0 1 2 3 *' | '0 1 * * *' | (1.day.in_minutes / 1.hour.in_minutes).to_i | false | Time.zone.local(2021, 3, 2, 1, 0) | Time.zone.local(2022, 3, 2, 1, 0) + '*/5 * * * *' | '*/1 * * * *' | nil | true | Time.zone.local(2021, 5, 27, 11, 0) | Time.zone.local(2021, 5, 27, 11, 5) + '*/5 * * * *' | '*/1 * * * *' | (1.day.in_minutes / 1.hour.in_minutes).to_i | true | Time.zone.local(2021, 5, 27, 11, 0) | Time.zone.local(2021, 5, 27, 12, 0) + '*/5 * * * *' | '*/1 * * * *' | (1.day.in_minutes / 1.hour.in_minutes).to_i | false | Time.zone.local(2021, 5, 27, 11, 0) | Time.zone.local(2021, 5, 27, 11, 5) + '*/5 * * * *' | '*/1 * * * *' | (1.day.in_minutes / 10).to_i | true | Time.zone.local(2021, 5, 27, 11, 0) | Time.zone.local(2021, 5, 27, 11, 10) + '*/5 * * * *' | '*/1 * * * *' | 200 | true | Time.zone.local(2021, 5, 27, 11, 0) | Time.zone.local(2021, 5, 27, 11, 10) + '*/5 * * * *' | '*/1 * * * *' | 200 | false | Time.zone.local(2021, 5, 27, 11, 0) | Time.zone.local(2021, 5, 27, 11, 5) + '*/5 * * * *' | '0 * * * *' | nil | true | Time.zone.local(2021, 5, 27, 11, 0) | Time.zone.local(2021, 5, 27, 12, 5) + '*/5 * * * *' | '0 * * * *' | (1.day.in_minutes / 10).to_i | true | Time.zone.local(2021, 5, 27, 11, 0) | Time.zone.local(2021, 5, 27, 12, 0) + '*/5 * * * *' | '0 * * * *' | (1.day.in_minutes / 10).to_i | false | Time.zone.local(2021, 5, 27, 11, 0) | Time.zone.local(2021, 5, 27, 12, 5) + '*/5 * * * *' | '0 * * * *' | (1.day.in_minutes / 1.hour.in_minutes).to_i | true | Time.zone.local(2021, 5, 27, 11, 0) | Time.zone.local(2021, 5, 27, 12, 0) + '*/5 * * * *' | '0 * * * *' | (1.day.in_minutes / 2.hours.in_minutes).to_i | true | Time.zone.local(2021, 5, 27, 11, 0) | Time.zone.local(2021, 5, 27, 12, 5) + '*/5 * * * *' | '0 1 * * *' | (1.day.in_minutes / 1.hour.in_minutes).to_i | true | Time.zone.local(2021, 5, 27, 1, 0) | Time.zone.local(2021, 5, 28, 1, 0) + '*/5 * * * *' | '0 1 * * *' | (1.day.in_minutes / 1.hour.in_minutes).to_i | true | Time.zone.local(2021, 5, 27, 1, 0) | Time.zone.local(2021, 5, 28, 1, 0) + '*/5 * * * *' | '0 1 1 * *' | (1.day.in_minutes / 1.hour.in_minutes).to_i | true | Time.zone.local(2021, 5, 1, 1, 0) | Time.zone.local(2021, 6, 1, 1, 0) + end + + with_them do + let(:pipeline_schedule) { create(:ci_pipeline_schedule, cron: schedule_cron) } - context 'when PipelineScheduleWorker runs at a specific interval' do before do allow(Settings).to receive(:cron_jobs) do - { - 'pipeline_schedule_worker' => { - 'cron' => '0 1 2 3 *' - } - } + { 'pipeline_schedule_worker' => { 'cron' => worker_cron } } end + + create(:plan_limits, :default_plan, ci_daily_pipeline_schedule_triggers: plan_limit) if plan_limit + stub_feature_flags(ci_daily_limit_for_pipeline_schedules: false) unless ff_enabled + + # Setting this here to override initial save with the current time + pipeline_schedule.next_run_at = now end - it "updates next_run_at to the sidekiq worker's execution time" do - expect(pipeline_schedule.next_run_at.min).to eq(0) - expect(pipeline_schedule.next_run_at.hour).to eq(1) - expect(pipeline_schedule.next_run_at.day).to eq(2) - expect(pipeline_schedule.next_run_at.month).to eq(3) + it 'updates next_run_at' do + travel_to(now) do + pipeline_schedule.set_next_run_at + + expect(pipeline_schedule.next_run_at).to eq(result) + end end end @@ -176,4 +201,26 @@ RSpec.describe Ci::PipelineSchedule do it { is_expected.to contain_exactly(*pipeline_schedule_variables.map(&:to_runner_variable)) } end + + describe '#daily_limit' do + let(:pipeline_schedule) { build(:ci_pipeline_schedule) } + + subject(:daily_limit) { pipeline_schedule.daily_limit } + + context 'when there is no limit' do + before do + create(:plan_limits, :default_plan, ci_daily_pipeline_schedule_triggers: 0) + end + + it { is_expected.to be_nil } + end + + context 'when there is a limit' do + before do + create(:plan_limits, :default_plan, ci_daily_pipeline_schedule_triggers: 144) + end + + it { is_expected.to eq(144) } + end + end end diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index b9457055a18..72af40e31e0 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -744,6 +744,42 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do end end + describe '#update_builds_coverage' do + let_it_be(:pipeline) { create(:ci_empty_pipeline) } + + context 'builds with coverage_regex defined' do + let!(:build_1) { create(:ci_build, :success, :trace_with_coverage, trace_coverage: 60.0, pipeline: pipeline) } + let!(:build_2) { create(:ci_build, :success, :trace_with_coverage, trace_coverage: 80.0, pipeline: pipeline) } + + it 'updates the coverage value of each build from the trace' do + pipeline.update_builds_coverage + + expect(build_1.reload.coverage).to eq(60.0) + expect(build_2.reload.coverage).to eq(80.0) + end + end + + context 'builds without coverage_regex defined' do + let!(:build) { create(:ci_build, :success, :trace_with_coverage, coverage_regex: nil, trace_coverage: 60.0, pipeline: pipeline) } + + it 'does not update the coverage value of each build from the trace' do + pipeline.update_builds_coverage + + expect(build.reload.coverage).to eq(nil) + end + end + + context 'builds with coverage values already present' do + let!(:build) { create(:ci_build, :success, :trace_with_coverage, trace_coverage: 60.0, coverage: 10.0, pipeline: pipeline) } + + it 'does not update the coverage value of each build from the trace' do + pipeline.update_builds_coverage + + expect(build.reload.coverage).to eq(10.0) + end + end + end + describe '#retryable?' do subject { pipeline.retryable? } @@ -2726,7 +2762,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do pipeline2.cancel_running end - extra_update_queries = 3 # transition ... => :canceled + extra_update_queries = 4 # transition ... => :canceled, queue pop extra_generic_commit_status_validation_queries = 2 # name_uniqueness_across_types expect(control2.count).to eq(control1.count + extra_update_queries + extra_generic_commit_status_validation_queries) @@ -3162,6 +3198,81 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do end end + describe '#environments_in_self_and_descendants' do + subject { pipeline.environments_in_self_and_descendants } + + context 'when pipeline is not child nor parent' do + let_it_be(:pipeline) { create(:ci_pipeline, :created) } + let_it_be(:build) { create(:ci_build, :with_deployment, :deploy_to_production, pipeline: pipeline) } + + it 'returns just the pipeline environment' do + expect(subject).to contain_exactly(build.deployment.environment) + end + end + + context 'when pipeline is in extended family' do + let_it_be(:parent) { create(:ci_pipeline) } + let_it_be(:parent_build) { create(:ci_build, :with_deployment, environment: 'staging', pipeline: parent) } + + let_it_be(:pipeline) { create(:ci_pipeline, child_of: parent) } + let_it_be(:build) { create(:ci_build, :with_deployment, :deploy_to_production, pipeline: pipeline) } + + let_it_be(:child) { create(:ci_pipeline, child_of: pipeline) } + let_it_be(:child_build) { create(:ci_build, :with_deployment, environment: 'canary', pipeline: child) } + + let_it_be(:grandchild) { create(:ci_pipeline, child_of: child) } + let_it_be(:grandchild_build) { create(:ci_build, :with_deployment, environment: 'test', pipeline: grandchild) } + + let_it_be(:sibling) { create(:ci_pipeline, child_of: parent) } + let_it_be(:sibling_build) { create(:ci_build, :with_deployment, environment: 'review', pipeline: sibling) } + + it 'returns its own environment and from all descendants' do + expected_environments = [ + build.deployment.environment, + child_build.deployment.environment, + grandchild_build.deployment.environment + ] + expect(subject).to match_array(expected_environments) + end + + it 'does not return parent environment' do + expect(subject).not_to include(parent_build.deployment.environment) + end + + it 'does not return sibling environment' do + expect(subject).not_to include(sibling_build.deployment.environment) + end + end + + context 'when each pipeline has multiple environments' do + let_it_be(:pipeline) { create(:ci_pipeline, :created) } + let_it_be(:build1) { create(:ci_build, :with_deployment, :deploy_to_production, pipeline: pipeline) } + let_it_be(:build2) { create(:ci_build, :with_deployment, environment: 'staging', pipeline: pipeline) } + + let_it_be(:child) { create(:ci_pipeline, child_of: pipeline) } + let_it_be(:child_build1) { create(:ci_build, :with_deployment, environment: 'canary', pipeline: child) } + let_it_be(:child_build2) { create(:ci_build, :with_deployment, environment: 'test', pipeline: child) } + + it 'returns all related environments' do + expected_environments = [ + build1.deployment.environment, + build2.deployment.environment, + child_build1.deployment.environment, + child_build2.deployment.environment + ] + expect(subject).to match_array(expected_environments) + end + end + + context 'when pipeline has no environment' do + let_it_be(:pipeline) { create(:ci_pipeline, :created) } + + it 'returns empty' do + expect(subject).to be_empty + end + end + end + describe '#root_ancestor' do subject { pipeline.root_ancestor } @@ -4512,4 +4623,17 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do .not_to exceed_query_limit(control_count) end end + + describe '#build_matchers' do + let_it_be(:pipeline) { create(:ci_pipeline) } + let_it_be(:builds) { create_list(:ci_build, 2, pipeline: pipeline, project: pipeline.project) } + + subject(:matchers) { pipeline.build_matchers } + + it 'returns build matchers' do + expect(matchers.size).to eq(1) + expect(matchers).to all be_a(Gitlab::Ci::Matching::BuildMatcher) + expect(matchers.first.build_ids).to match_array(builds.map(&:id)) + end + end end diff --git a/spec/models/ci/runner_spec.rb b/spec/models/ci/runner_spec.rb index ffe0b0d0b19..61f80bd43b1 100644 --- a/spec/models/ci/runner_spec.rb +++ b/spec/models/ci/runner_spec.rb @@ -75,6 +75,22 @@ RSpec.describe Ci::Runner do expect { create(:group, runners: [project_runner]) } .to raise_error(ActiveRecord::RecordInvalid) end + + context 'when runner has config' do + it 'is valid' do + runner = build(:ci_runner, config: { gpus: "all" }) + + expect(runner).to be_valid + end + end + + context 'when runner has an invalid config' do + it 'is invalid' do + runner = build(:ci_runner, config: { test: 1 }) + + expect(runner).not_to be_valid + end + end end context 'cost factors validations' do @@ -257,6 +273,20 @@ RSpec.describe Ci::Runner do end end + describe '.recent' do + subject { described_class.recent } + + before do + @runner1 = create(:ci_runner, :instance, contacted_at: nil, created_at: 2.months.ago) + @runner2 = create(:ci_runner, :instance, contacted_at: nil, created_at: 3.months.ago) + @runner3 = create(:ci_runner, :instance, contacted_at: 1.month.ago, created_at: 2.months.ago) + @runner4 = create(:ci_runner, :instance, contacted_at: 1.month.ago, created_at: 3.months.ago) + @runner5 = create(:ci_runner, :instance, contacted_at: 3.months.ago, created_at: 5.months.ago) + end + + it { is_expected.to eq([@runner1, @runner3, @runner4])} + end + describe '.online' do subject { described_class.online } @@ -349,6 +379,22 @@ RSpec.describe Ci::Runner do it { is_expected.to eq([@runner1])} end + describe '#tick_runner_queue' do + it 'sticks the runner to the primary and calls the original method' do + runner = create(:ci_runner) + + allow(Gitlab::Database::LoadBalancing).to receive(:enable?) + .and_return(true) + + expect(Gitlab::Database::LoadBalancing::Sticking).to receive(:stick) + .with(:runner, runner.id) + + expect(Gitlab::Workhorse).to receive(:set_key_and_notify) + + runner.tick_runner_queue + end + end + describe '#can_pick?' do using RSpec::Parameterized::TableSyntax @@ -653,7 +699,7 @@ RSpec.describe Ci::Runner do describe '#heartbeat' do let(:runner) { create(:ci_runner, :project) } - subject { runner.heartbeat(architecture: '18-bit') } + subject { runner.heartbeat(architecture: '18-bit', config: { gpus: "all" }) } context 'when database was updated recently' do before do @@ -701,6 +747,7 @@ RSpec.describe Ci::Runner do def does_db_update expect { subject }.to change { runner.reload.read_attribute(:contacted_at) } .and change { runner.reload.read_attribute(:architecture) } + .and change { runner.reload.read_attribute(:config) } end end @@ -826,12 +873,12 @@ RSpec.describe Ci::Runner do expect(described_class.search(runner.token)).to eq([runner]) end - it 'returns runners with a partially matching token' do - expect(described_class.search(runner.token[0..2])).to eq([runner]) + it 'does not return runners with a partially matching token' do + expect(described_class.search(runner.token[0..2])).to be_empty end - it 'returns runners with a matching token regardless of the casing' do - expect(described_class.search(runner.token.upcase)).to eq([runner]) + it 'does not return runners with a matching token with different casing' do + expect(described_class.search(runner.token.upcase)).to be_empty end it 'returns runners with a matching description' do @@ -919,29 +966,13 @@ RSpec.describe Ci::Runner do end end - context 'build picking improvement enabled' do - before do - stub_feature_flags(ci_reduce_queries_when_ticking_runner_queue: true) - end - + context 'build picking improvement' do it 'does not check if the build is assignable to a runner' do expect(runner).not_to receive(:can_pick?) runner.pick_build!(build) end end - - context 'build picking improvement disabled' do - before do - stub_feature_flags(ci_reduce_queries_when_ticking_runner_queue: false) - end - - it 'checks if the build is assignable to a runner' do - expect(runner).to receive(:can_pick?).and_call_original - - runner.pick_build!(build) - end - end end describe 'project runner without projects is destroyable' do @@ -975,6 +1006,108 @@ RSpec.describe Ci::Runner do end end + describe '.runner_matchers' do + subject(:matchers) { described_class.all.runner_matchers } + + context 'deduplicates on runner_type' do + before do + create_list(:ci_runner, 2, :instance) + create_list(:ci_runner, 2, :project) + end + + it 'creates two matchers' do + expect(matchers.size).to eq(2) + + expect(matchers.map(&:runner_type)).to match_array(%w[instance_type project_type]) + end + end + + context 'deduplicates on public_projects_minutes_cost_factor' do + before do + create_list(:ci_runner, 2, public_projects_minutes_cost_factor: 5) + create_list(:ci_runner, 2, public_projects_minutes_cost_factor: 10) + end + + it 'creates two matchers' do + expect(matchers.size).to eq(2) + + expect(matchers.map(&:public_projects_minutes_cost_factor)).to match_array([5, 10]) + end + end + + context 'deduplicates on private_projects_minutes_cost_factor' do + before do + create_list(:ci_runner, 2, private_projects_minutes_cost_factor: 5) + create_list(:ci_runner, 2, private_projects_minutes_cost_factor: 10) + end + + it 'creates two matchers' do + expect(matchers.size).to eq(2) + + expect(matchers.map(&:private_projects_minutes_cost_factor)).to match_array([5, 10]) + end + end + + context 'deduplicates on run_untagged' do + before do + create_list(:ci_runner, 2, run_untagged: true, tag_list: ['a']) + create_list(:ci_runner, 2, run_untagged: false, tag_list: ['a']) + end + + it 'creates two matchers' do + expect(matchers.size).to eq(2) + + expect(matchers.map(&:run_untagged)).to match_array([true, false]) + end + end + + context 'deduplicates on access_level' do + before do + create_list(:ci_runner, 2, access_level: :ref_protected) + create_list(:ci_runner, 2, access_level: :not_protected) + end + + it 'creates two matchers' do + expect(matchers.size).to eq(2) + + expect(matchers.map(&:access_level)).to match_array(%w[ref_protected not_protected]) + end + end + + context 'deduplicates on tag_list' do + before do + create_list(:ci_runner, 2, tag_list: %w[tag1 tag2]) + create_list(:ci_runner, 2, tag_list: %w[tag3 tag4]) + end + + it 'creates two matchers' do + expect(matchers.size).to eq(2) + + expect(matchers.map(&:tag_list)).to match_array([%w[tag1 tag2], %w[tag3 tag4]]) + end + end + end + + describe '#runner_matcher' do + let(:runner) do + build_stubbed(:ci_runner, :instance_type, tag_list: %w[tag1 tag2]) + end + + subject(:matcher) { runner.runner_matcher } + + it { expect(matcher.runner_type).to eq(runner.runner_type) } + + it { expect(matcher.public_projects_minutes_cost_factor).to eq(runner.public_projects_minutes_cost_factor) } + + it { expect(matcher.private_projects_minutes_cost_factor).to eq(runner.private_projects_minutes_cost_factor) } + + it { expect(matcher.run_untagged).to eq(runner.run_untagged) } + + it { expect(matcher.access_level).to eq(runner.access_level) } + + it { expect(matcher.tag_list).to match_array(runner.tag_list) } + end + describe '#uncached_contacted_at' do let(:contacted_at_stored) { 1.hour.ago.change(usec: 0) } let(:runner) { create(:ci_runner, contacted_at: contacted_at_stored) } diff --git a/spec/models/ci/running_build_spec.rb b/spec/models/ci/running_build_spec.rb new file mode 100644 index 00000000000..589e5a86f4d --- /dev/null +++ b/spec/models/ci/running_build_spec.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Ci::RunningBuild do + let_it_be(:project) { create(:project) } + let_it_be(:pipeline) { create(:ci_pipeline, project: project) } + + let(:runner) { create(:ci_runner, :instance_type) } + let(:build) { create(:ci_build, :running, runner: runner, pipeline: pipeline) } + + describe '.upsert_shared_runner_build!' do + context 'another pending entry does not exist' do + it 'creates a new pending entry' do + result = described_class.upsert_shared_runner_build!(build) + + expect(result.rows.dig(0, 0)).to eq build.id + expect(build.reload.runtime_metadata).to be_present + end + end + + context 'when another queuing entry exists for given build' do + before do + described_class.create!(build: build, + project: project, + runner: runner, + runner_type: runner.runner_type) + end + + it 'returns a build id as a result' do + result = described_class.upsert_shared_runner_build!(build) + + expect(result.rows.dig(0, 0)).to eq build.id + end + end + + context 'when build has been picked by a specific runner' do + let(:runner) { create(:ci_runner, :project) } + + it 'raises an error' do + expect { described_class.upsert_shared_runner_build!(build) } + .to raise_error(ArgumentError, 'build has not been picked by a shared runner') + end + end + + context 'when build has not been picked by a runner yet' do + let(:build) { create(:ci_build, pipeline: pipeline) } + + it 'raises an error' do + expect { described_class.upsert_shared_runner_build!(build) } + .to raise_error(ArgumentError, 'build has not been picked by a shared runner') + end + end + end +end |