diff options
Diffstat (limited to 'spec/models/ci')
28 files changed, 997 insertions, 321 deletions
diff --git a/spec/models/ci/bridge_spec.rb b/spec/models/ci/bridge_spec.rb index df24c92149d..169b00b9c74 100644 --- a/spec/models/ci/bridge_spec.rb +++ b/spec/models/ci/bridge_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Ci::Bridge do +RSpec.describe Ci::Bridge, feature_category: :continuous_integration do let_it_be(:project) { create(:project) } let_it_be(:target_project) { create(:project, name: 'project', namespace: create(:namespace, name: 'my')) } let_it_be(:pipeline) { create(:ci_pipeline, project: project) } @@ -34,6 +34,24 @@ RSpec.describe Ci::Bridge do expect(bridge).to have_one(:downstream_pipeline) end + describe '#sourced_pipelines' do + subject { bridge.sourced_pipelines } + + it 'raises error' do + expect { subject }.to raise_error RuntimeError, 'Ci::Bridge does not have sourced_pipelines association' + end + + context 'when ci_bridge_remove_sourced_pipelines is disabled' do + before do + stub_feature_flags(ci_bridge_remove_sourced_pipelines: false) + end + + it 'returns the sourced_pipelines association' do + expect(bridge.sourced_pipelines).to eq([]) + end + end + end + describe '#retryable?' do let(:bridge) { create(:ci_bridge, :success) } diff --git a/spec/models/ci/build_metadata_spec.rb b/spec/models/ci/build_metadata_spec.rb index e728ce0f474..8bf3af44be6 100644 --- a/spec/models/ci/build_metadata_spec.rb +++ b/spec/models/ci/build_metadata_spec.rb @@ -6,7 +6,6 @@ RSpec.describe Ci::BuildMetadata do let_it_be(:user) { create(:user) } let_it_be(:group) { create(:group) } let_it_be(:project) { create(:project, :repository, group: group, build_timeout: 2000) } - let_it_be(:pipeline) do create(:ci_pipeline, project: project, sha: project.commit.id, @@ -14,7 +13,9 @@ RSpec.describe Ci::BuildMetadata do status: 'success') end - let(:job) { create(:ci_build, pipeline: pipeline) } + let_it_be_with_reload(:runner) { create(:ci_runner) } + + let(:job) { create(:ci_build, pipeline: pipeline, runner: runner) } let(:metadata) { job.metadata } it_behaves_like 'having unique enum values' @@ -32,63 +33,110 @@ RSpec.describe Ci::BuildMetadata do end end - context 'when project timeout is set' do - context 'when runner is assigned to the job' do + context 'when job, project and runner timeouts are set' do + context 'when job timeout is lower then runner timeout' do before do - job.update!(runner: runner) + runner.update!(maximum_timeout: 4000) + job.update!(options: { job_timeout: 3000 }) end - context 'when runner timeout is not set' do - let(:runner) { create(:ci_runner, maximum_timeout: nil) } + it_behaves_like 'sets timeout', 'job_timeout_source', 3000 + end - it_behaves_like 'sets timeout', 'project_timeout_source', 2000 + context 'when runner timeout is lower then job timeout' do + before do + runner.update!(maximum_timeout: 2000) + job.update!(options: { job_timeout: 3000 }) end - context 'when runner timeout is lower than project timeout' do - let(:runner) { create(:ci_runner, maximum_timeout: 1900) } + it_behaves_like 'sets timeout', 'runner_timeout_source', 2000 + end + end - it_behaves_like 'sets timeout', 'runner_timeout_source', 1900 + context 'when job, project timeout values are set and runner is assigned' do + context 'when runner has no timeout set' do + before do + runner.update!(maximum_timeout: nil) + job.update!(options: { job_timeout: 3000 }) end - context 'when runner timeout is higher than project timeout' do - let(:runner) { create(:ci_runner, maximum_timeout: 2100) } + it_behaves_like 'sets timeout', 'job_timeout_source', 3000 + end + end - it_behaves_like 'sets timeout', 'project_timeout_source', 2000 + context 'when only job and project timeouts are defined' do + context 'when job timeout is lower then project timeout' do + before do + job.update!(options: { job_timeout: 1000 }) end - end - context 'when job timeout is set' do - context 'when job timeout is higher than project timeout' do - let(:job) { create(:ci_build, pipeline: pipeline, options: { job_timeout: 3000 }) } + it_behaves_like 'sets timeout', 'job_timeout_source', 1000 + end - it_behaves_like 'sets timeout', 'job_timeout_source', 3000 + context 'when project timeout is lower then job timeout' do + before do + job.update!(options: { job_timeout: 3000 }) end - context 'when job timeout is lower than project timeout' do - let(:job) { create(:ci_build, pipeline: pipeline, options: { job_timeout: 1000 }) } + it_behaves_like 'sets timeout', 'job_timeout_source', 3000 + end + end + + context 'when only project and runner timeouts are defined' do + before do + runner.update!(maximum_timeout: 1900) + end + + context 'when runner timeout is lower then project timeout' do + it_behaves_like 'sets timeout', 'runner_timeout_source', 1900 + end - it_behaves_like 'sets timeout', 'job_timeout_source', 1000 + context 'when project timeout is lower then runner timeout' do + before do + runner.update!(maximum_timeout: 2100) end + + it_behaves_like 'sets timeout', 'project_timeout_source', 2000 end + end - context 'when both runner and job timeouts are set' do + context 'when only job and runner timeouts are defined' do + context 'when runner timeout is lower them job timeout' do before do - job.update!(runner: runner) + job.update!(options: { job_timeout: 2000 }) + runner.update!(maximum_timeout: 1900) end - context 'when job timeout is higher than runner timeout' do - let(:job) { create(:ci_build, pipeline: pipeline, options: { job_timeout: 3000 }) } - let(:runner) { create(:ci_runner, maximum_timeout: 2100) } + it_behaves_like 'sets timeout', 'runner_timeout_source', 1900 + end - it_behaves_like 'sets timeout', 'runner_timeout_source', 2100 + context 'when job timeout is lower them runner timeout' do + before do + job.update!(options: { job_timeout: 1000 }) + runner.update!(maximum_timeout: 1900) end - context 'when job timeout is lower than runner timeout' do - let(:job) { create(:ci_build, pipeline: pipeline, options: { job_timeout: 1900 }) } - let(:runner) { create(:ci_runner, maximum_timeout: 2100) } + it_behaves_like 'sets timeout', 'job_timeout_source', 1000 + end + end + + context 'when only job timeout is defined and runner is assigned, but has no timeout set' do + before do + job.update!(options: { job_timeout: 1000 }) + runner.update!(maximum_timeout: nil) + end + + it_behaves_like 'sets timeout', 'job_timeout_source', 1000 + end - it_behaves_like 'sets timeout', 'job_timeout_source', 1900 + context 'when only one timeout value is defined' do + context 'when only project timeout value is defined' do + before do + job.update!(options: { job_timeout: nil }) + runner.update!(maximum_timeout: nil) end + + it_behaves_like 'sets timeout', 'project_timeout_source', 2000 end end end @@ -107,9 +155,7 @@ RSpec.describe Ci::BuildMetadata do } metadata.id_tokens = { TEST_JWT_TOKEN: { - id_token: { - aud: 'https://gitlab.test' - } + aud: 'https://gitlab.test' } } @@ -152,6 +198,29 @@ RSpec.describe Ci::BuildMetadata do end end + describe '#enable_debug_trace!' do + subject { metadata.enable_debug_trace! } + + context 'when debug_trace_enabled is false' do + it 'sets debug_trace_enabled to true' do + subject + + expect(metadata.debug_trace_enabled).to eq(true) + end + end + + context 'when debug_trace_enabled is true' do + before do + metadata.update!(debug_trace_enabled: true) + end + + it 'does not set debug_trace_enabled to true', :aggregate_failures do + expect(described_class).not_to receive(:save!) + expect(metadata.debug_trace_enabled).to eq(true) + end + end + end + describe 'partitioning' do context 'with job' do let(:status) { build(:commit_status, partition_id: 123) } @@ -183,28 +252,6 @@ RSpec.describe Ci::BuildMetadata do end end - describe 'routing table switch' do - context 'with ff disabled' do - before do - stub_feature_flags(ci_partitioning_use_ci_builds_metadata_routing_table: false) - end - - it 'uses the legacy table' do - expect(described_class.table_name).to eq('ci_builds_metadata') - end - end - - context 'with ff enabled' do - before do - stub_feature_flags(ci_partitioning_use_ci_builds_metadata_routing_table: true) - end - - it 'uses the routing table' do - expect(described_class.table_name).to eq('p_ci_builds_metadata') - end - end - end - context 'jsonb fields serialization' do it 'changing other fields does not change config_options' do expect { metadata.id = metadata.id }.not_to change(metadata, :changes) diff --git a/spec/models/ci/build_need_spec.rb b/spec/models/ci/build_need_spec.rb index c2cf9027055..aa1c57d1788 100644 --- a/spec/models/ci/build_need_spec.rb +++ b/spec/models/ci/build_need_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Ci::BuildNeed, model: true do +RSpec.describe Ci::BuildNeed, model: true, feature_category: :continuous_integration do let(:build_need) { build(:ci_build_need) } it { is_expected.to belong_to(:build).class_name('Ci::Processable') } @@ -35,4 +35,62 @@ RSpec.describe Ci::BuildNeed, model: true do end end end + + describe 'partitioning' do + context 'with build' do + let(:build) { FactoryBot.build(:ci_build, partition_id: ci_testing_partition_id) } + let(:build_need) { FactoryBot.build(:ci_build_need, build: build) } + + it 'sets partition_id to the current partition value' do + expect { build_need.valid? }.to change { build_need.partition_id }.to(ci_testing_partition_id) + end + + context 'when it is already set' do + let(:build_need) { FactoryBot.build(:ci_build_need, partition_id: 125) } + + it 'does not change the partition_id value' do + expect { build_need.valid? }.not_to change { build_need.partition_id } + end + end + end + + context 'without build' do + let(:build_need) { FactoryBot.build(:ci_build_need, build: nil) } + + it { is_expected.to validate_presence_of(:partition_id) } + + it 'does not change the partition_id value' do + expect { build_need.valid? }.not_to change { build_need.partition_id } + end + end + + context 'when using bulk_insert' do + include Ci::PartitioningHelpers + + let(:new_pipeline) { create(:ci_pipeline) } + let(:ci_build) { build(:ci_build, pipeline: new_pipeline) } + + before do + stub_current_partition_id + end + + it 'creates build needs successfully', :aggregate_failures do + ci_build.needs_attributes = [ + { name: "build", artifacts: true }, + { name: "build2", artifacts: true }, + { name: "build3", artifacts: true } + ] + + expect(described_class).to receive(:bulk_insert!).and_call_original + + BulkInsertableAssociations.with_bulk_insert do + ci_build.save! + end + + expect(described_class.count).to eq(3) + expect(described_class.first.partition_id).to eq(ci_testing_partition_id) + expect(described_class.second.partition_id).to eq(ci_testing_partition_id) + end + end + end end diff --git a/spec/models/ci/build_pending_state_spec.rb b/spec/models/ci/build_pending_state_spec.rb index a546d2aff65..756180621ec 100644 --- a/spec/models/ci/build_pending_state_spec.rb +++ b/spec/models/ci/build_pending_state_spec.rb @@ -24,4 +24,33 @@ RSpec.describe Ci::BuildPendingState do end end end + + describe 'partitioning' do + context 'with build' do + let(:build) { FactoryBot.build(:ci_build, partition_id: ci_testing_partition_id) } + let(:build_pending_state) { FactoryBot.build(:ci_build_pending_state, build: build) } + + it 'sets partition_id to the current partition value' do + expect { build_pending_state.valid? }.to change { build_pending_state.partition_id }.to(ci_testing_partition_id) + end + + context 'when it is already set' do + let(:build_pending_state) { FactoryBot.build(:ci_build_pending_state, partition_id: 125) } + + it 'does not change the partition_id value' do + expect { build_pending_state.valid? }.not_to change { build_pending_state.partition_id } + end + end + end + + context 'without build' do + let(:build_pending_state) { FactoryBot.build(:ci_build_pending_state, build: nil) } + + it { is_expected.to validate_presence_of(:partition_id) } + + it 'does not change the partition_id value' do + expect { build_pending_state.valid? }.not_to change { build_pending_state.partition_id } + end + end + end end diff --git a/spec/models/ci/build_report_result_spec.rb b/spec/models/ci/build_report_result_spec.rb index 09ea19cf077..90b23d3e824 100644 --- a/spec/models/ci/build_report_result_spec.rb +++ b/spec/models/ci/build_report_result_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' RSpec.describe Ci::BuildReportResult do - let(:build_report_result) { build(:ci_build_report_result, :with_junit_success) } + let_it_be_with_reload(:build_report_result) { create(:ci_build_report_result, :with_junit_success) } it_behaves_like 'cleanup by a loose foreign key' do let!(:parent) { create(:project) } @@ -70,4 +70,34 @@ RSpec.describe Ci::BuildReportResult do expect(build_report_result.tests_skipped).to eq(0) end end + + describe 'partitioning' do + let(:build_report_result) { FactoryBot.build(:ci_build_report_result, build: build) } + + context 'with build' do + let(:build) { FactoryBot.build(:ci_build, partition_id: ci_testing_partition_id) } + + it 'copies the partition_id from build' do + expect { build_report_result.valid? }.to change { build_report_result.partition_id }.to(ci_testing_partition_id) + end + + context 'when it is already set' do + let(:build_report_result) { FactoryBot.build(:ci_build_report_result, partition_id: 125) } + + it 'does not change the partition_id value' do + expect { build_report_result.valid? }.not_to change { build_report_result.partition_id } + end + end + end + + context 'without build' do + subject(:build_report_result) { FactoryBot.build(:ci_build_report_result, build: nil, partition_id: 125) } + + it { is_expected.to validate_presence_of(:partition_id) } + + it 'does not change the partition_id value' do + expect { build_report_result.valid? }.not_to change { build_report_result.partition_id } + end + end + end end diff --git a/spec/models/ci/build_runner_session_spec.rb b/spec/models/ci/build_runner_session_spec.rb index 8dfe854511c..5e1a489ed8b 100644 --- a/spec/models/ci/build_runner_session_spec.rb +++ b/spec/models/ci/build_runner_session_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Ci::BuildRunnerSession, model: true do +RSpec.describe Ci::BuildRunnerSession, model: true, feature_category: :continuous_integration do let!(:build) { create(:ci_build, :with_runner_session) } let(:url) { 'https://new.example.com' } @@ -174,4 +174,20 @@ RSpec.describe Ci::BuildRunnerSession, model: true do end end end + + describe 'partitioning' do + include Ci::PartitioningHelpers + + let(:new_pipeline) { create(:ci_pipeline) } + let(:new_build) { create(:ci_build, pipeline: new_pipeline) } + let(:build_runner_session) { create(:ci_build_runner_session, build: new_build) } + + before do + stub_current_partition_id + end + + it 'assigns the same partition id as the one that build has' do + expect(build_runner_session.partition_id).to eq(ci_testing_partition_id) + end + end end diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index 813b4b3faa6..c978e33bf54 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Ci::Build do +RSpec.describe Ci::Build, feature_category: :continuous_integration do include Ci::TemplateHelpers include AfterNextHelpers @@ -1754,8 +1754,8 @@ RSpec.describe Ci::Build do end end - describe '#starts_environment?' do - subject { build.starts_environment? } + describe '#deployment_job?' do + subject { build.deployment_job? } context 'when environment is defined' do before do @@ -2528,20 +2528,24 @@ RSpec.describe Ci::Build do end describe '#ref_slug' do - { - 'master' => 'master', - '1-foo' => '1-foo', - 'fix/1-foo' => 'fix-1-foo', - 'fix-1-foo' => 'fix-1-foo', - 'a' * 63 => 'a' * 63, - 'a' * 64 => 'a' * 63, - 'FOO' => 'foo', - '-' + 'a' * 61 + '-' => 'a' * 61, - '-' + 'a' * 62 + '-' => 'a' * 62, - '-' + 'a' * 63 + '-' => 'a' * 62, - 'a' * 62 + ' ' => 'a' * 62 - }.each do |ref, slug| - it "transforms #{ref} to #{slug}" do + using RSpec::Parameterized::TableSyntax + + where(:ref, :slug) do + 'master' | 'master' + '1-foo' | '1-foo' + 'fix/1-foo' | 'fix-1-foo' + 'fix-1-foo' | 'fix-1-foo' + 'a' * 63 | 'a' * 63 + 'a' * 64 | 'a' * 63 + 'FOO' | 'foo' + '-' + 'a' * 61 + '-' | 'a' * 61 + '-' + 'a' * 62 + '-' | 'a' * 62 + '-' + 'a' * 63 + '-' | 'a' * 62 + 'a' * 62 + ' ' | 'a' * 62 + end + + with_them do + it "transforms ref to slug" do build.ref = ref expect(build.ref_slug).to eq(slug) @@ -2737,6 +2741,7 @@ RSpec.describe Ci::Build do { key: 'CI_PROJECT_PATH', value: project.full_path, public: true, masked: false }, { key: 'CI_PROJECT_PATH_SLUG', value: project.full_path_slug, public: true, masked: false }, { key: 'CI_PROJECT_NAMESPACE', value: project.namespace.full_path, public: true, masked: false }, + { key: 'CI_PROJECT_NAMESPACE_ID', value: project.namespace.id.to_s, public: true, masked: false }, { key: 'CI_PROJECT_ROOT_NAMESPACE', value: project.namespace.root_ancestor.path, public: true, masked: false }, { key: 'CI_PROJECT_URL', value: project.web_url, public: true, masked: false }, { key: 'CI_PROJECT_VISIBILITY', value: 'private', public: true, masked: false }, @@ -2883,6 +2888,9 @@ RSpec.describe Ci::Build do value: 'var', public: true }] build.environment = 'staging' + + # CI_ENVIRONMENT_NAME is set in predefined_variables when job environment is provided + predefined_variables.insert(20, { key: 'CI_ENVIRONMENT_NAME', value: 'staging', public: true, masked: false }) end it 'matches explicit variables ordering' do @@ -3539,8 +3547,8 @@ RSpec.describe Ci::Build do rsa_key = OpenSSL::PKey::RSA.generate(3072).to_s stub_application_setting(ci_jwt_signing_key: rsa_key) build.metadata.update!(id_tokens: { - 'ID_TOKEN_1' => { id_token: { aud: 'developers' } }, - 'ID_TOKEN_2' => { id_token: { aud: 'maintainers' } } + 'ID_TOKEN_1' => { aud: 'developers' }, + 'ID_TOKEN_2' => { aud: 'maintainers' } }) end @@ -3817,22 +3825,6 @@ RSpec.describe Ci::Build do it 'assigns the token' do expect { build.enqueue }.to change(build, :token).from(nil).to(an_instance_of(String)) end - - context 'with ci_assign_job_token_on_scheduling disabled' do - before do - stub_feature_flags(ci_assign_job_token_on_scheduling: false) - end - - it 'assigns the token on creation' do - expect(build.token).to be_present - end - - it 'does not change the token when enqueuing' do - expect { build.enqueue }.not_to change(build, :token) - - expect(build).to be_pending - end - end end describe 'state transition: pending: :running' do @@ -5442,7 +5434,7 @@ RSpec.describe Ci::Build do it 'delegates to Ci::BuildTraceMetadata' do expect(Ci::BuildTraceMetadata) .to receive(:find_or_upsert_for!) - .with(build.id) + .with(build.id, build.partition_id) build.ensure_trace_metadata! end @@ -5617,4 +5609,72 @@ RSpec.describe Ci::Build do end end end + + describe '#runtime_hooks' do + let(:build1) do + FactoryBot.build(:ci_build, + options: { hooks: { pre_get_sources_script: ["echo 'hello pre_get_sources_script'"] } }) + end + + subject(:runtime_hooks) { build1.runtime_hooks } + + it 'returns an array of hook objects' do + expect(runtime_hooks.size).to eq(1) + expect(runtime_hooks[0].name).to eq('pre_get_sources_script') + expect(runtime_hooks[0].script).to eq(["echo 'hello pre_get_sources_script'"]) + end + end + + describe 'partitioning', :ci_partitionable do + include Ci::PartitioningHelpers + + let(:new_pipeline) { create(:ci_pipeline) } + let(:ci_build) { FactoryBot.build(:ci_build, pipeline: new_pipeline) } + + before do + stub_current_partition_id + end + + it 'assigns partition_id to job variables successfully', :aggregate_failures do + ci_build.job_variables_attributes = [ + { key: 'TEST_KEY', value: 'new value' }, + { key: 'NEW_KEY', value: 'exciting new value' } + ] + + ci_build.save! + + expect(ci_build.job_variables.count).to eq(2) + expect(ci_build.job_variables.first.partition_id).to eq(ci_testing_partition_id) + expect(ci_build.job_variables.second.partition_id).to eq(ci_testing_partition_id) + end + end + + describe 'assigning token', :ci_partitionable do + include Ci::PartitioningHelpers + + let(:new_pipeline) { create(:ci_pipeline) } + let(:ci_build) { create(:ci_build, pipeline: new_pipeline) } + + before do + stub_current_partition_id + end + + it 'includes partition_id as a token prefix' do + prefix = ci_build.token.split('_').first.to_i(16) + + expect(prefix).to eq(ci_testing_partition_id) + end + + context 'when ci_build_partition_id_token_prefix is disabled' do + before do + stub_feature_flags(ci_build_partition_id_token_prefix: false) + end + + it 'does not include partition_id as a token prefix' do + prefix = ci_build.token.split('_').first.to_i(16) + + expect(prefix).not_to eq(ci_testing_partition_id) + end + end + end end diff --git a/spec/models/ci/build_trace_chunk_spec.rb b/spec/models/ci/build_trace_chunk_spec.rb index 3328ed62f15..ac0a18a176d 100644 --- a/spec/models/ci/build_trace_chunk_spec.rb +++ b/spec/models/ci/build_trace_chunk_spec.rb @@ -15,13 +15,13 @@ RSpec.describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state, :clean_git described_class.new(build: build, chunk_index: chunk_index, data_store: data_store, raw_data: raw_data) end - it_behaves_like 'having unique enum values' - before do stub_feature_flags(ci_enable_live_trace: true) stub_artifacts_object_storage end + it_behaves_like 'having unique enum values' + def redis_instance { redis: Gitlab::Redis::SharedState, @@ -954,4 +954,33 @@ RSpec.describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state, :clean_git it { is_expected.to eq(value) } end end + + describe 'partitioning' do + context 'with build' do + let(:build) { FactoryBot.build(:ci_build, partition_id: ci_testing_partition_id) } + let(:build_trace_chunk) { FactoryBot.build(:ci_build_trace_chunk, build: build) } + + it 'sets partition_id to the current partition value' do + expect { build_trace_chunk.valid? }.to change { build_trace_chunk.partition_id }.to(ci_testing_partition_id) + end + + context 'when it is already set' do + let(:build_trace_chunk) { FactoryBot.build(:ci_build_trace_chunk, partition_id: 125) } + + it 'does not change the partition_id value' do + expect { build_trace_chunk.valid? }.not_to change { build_trace_chunk.partition_id } + end + end + end + + context 'without build' do + let(:build_trace_chunk) { FactoryBot.build(:ci_build_trace_chunk, build: nil, partition_id: 125) } + + it { is_expected.to validate_presence_of(:partition_id) } + + it 'does not change the partition_id value' do + expect { build_trace_chunk.valid? }.not_to change { build_trace_chunk.partition_id } + end + end + end end diff --git a/spec/models/ci/build_trace_metadata_spec.rb b/spec/models/ci/build_trace_metadata_spec.rb index 120e4289da2..2ab300e4054 100644 --- a/spec/models/ci/build_trace_metadata_spec.rb +++ b/spec/models/ci/build_trace_metadata_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Ci::BuildTraceMetadata do +RSpec.describe Ci::BuildTraceMetadata, feature_category: :continuous_integration do it { is_expected.to belong_to(:build) } it { is_expected.to belong_to(:trace_artifact) } @@ -106,7 +106,7 @@ RSpec.describe Ci::BuildTraceMetadata do let_it_be(:build) { create(:ci_build) } subject(:execute) do - described_class.find_or_upsert_for!(build.id) + described_class.find_or_upsert_for!(build.id, build.partition_id) end it 'creates a new record' do @@ -158,4 +158,22 @@ RSpec.describe Ci::BuildTraceMetadata do it { is_expected.to eq(result) } end end + + describe 'partitioning' do + include Ci::PartitioningHelpers + + let_it_be(:pipeline) { create(:ci_pipeline) } + let_it_be(:build) { create(:ci_build, pipeline: pipeline) } + let(:new_pipeline) { create(:ci_pipeline) } + let(:new_build) { create(:ci_build, pipeline: new_pipeline) } + let(:metadata) { create(:ci_build_trace_metadata, build: new_build) } + + before do + stub_current_partition_id + end + + it 'assigns the same partition id as the one that build has' do + expect(metadata.partition_id).to eq(ci_testing_partition_id) + end + end end diff --git a/spec/models/ci/freeze_period_spec.rb b/spec/models/ci/freeze_period_spec.rb index b9bf1657e28..d8add736d6a 100644 --- a/spec/models/ci/freeze_period_spec.rb +++ b/spec/models/ci/freeze_period_spec.rb @@ -2,16 +2,22 @@ require 'spec_helper' -RSpec.describe Ci::FreezePeriod, type: :model do +RSpec.describe Ci::FreezePeriod, feature_category: :release_orchestration, type: :model do + let_it_be(:project) { create(:project) } + + # Freeze period factory is on a weekend, so we travel in time, in and around that. + let(:friday_2300_time) { Time.utc(2020, 4, 10, 23, 0) } + let(:saturday_1200_time) { Time.utc(2020, 4, 11, 12, 0) } + let(:monday_0700_time) { Time.utc(2020, 4, 13, 7, 0) } + let(:tuesday_0800_time) { Time.utc(2020, 4, 14, 8, 0) } + subject { build(:ci_freeze_period) } it_behaves_like 'cleanup by a loose foreign key' do let!(:parent) { create(:project) } - let!(:model) { create(:ci_freeze_period, project: parent) } + let!(:model) { create(:ci_freeze_period, project: parent) } end - let(:invalid_cron) { '0 0 0 * *' } - it { is_expected.to belong_to(:project) } it { is_expected.to respond_to(:freeze_start) } @@ -19,37 +25,142 @@ RSpec.describe Ci::FreezePeriod, type: :model do it { is_expected.to respond_to(:cron_timezone) } describe 'cron validations' do + let(:invalid_cron) { '0 0 0 * *' } + it 'allows valid cron patterns' do - freeze_period = build(:ci_freeze_period) + freeze_period = build_stubbed(:ci_freeze_period) expect(freeze_period).to be_valid end it 'does not allow invalid cron patterns on freeze_start' do - freeze_period = build(:ci_freeze_period, freeze_start: invalid_cron) + freeze_period = build_stubbed(:ci_freeze_period, freeze_start: invalid_cron) expect(freeze_period).not_to be_valid end it 'does not allow invalid cron patterns on freeze_end' do - freeze_period = build(:ci_freeze_period, freeze_end: invalid_cron) + freeze_period = build_stubbed(:ci_freeze_period, freeze_end: invalid_cron) expect(freeze_period).not_to be_valid end it 'does not allow an invalid timezone' do - freeze_period = build(:ci_freeze_period, cron_timezone: 'invalid') + freeze_period = build_stubbed(:ci_freeze_period, cron_timezone: 'invalid') expect(freeze_period).not_to be_valid end context 'when cron contains trailing whitespaces' do it 'strips the attribute' do - freeze_period = build(:ci_freeze_period, freeze_start: ' 0 0 * * * ') + freeze_period = build_stubbed(:ci_freeze_period, freeze_start: ' 0 0 * * * ') expect(freeze_period).to be_valid expect(freeze_period.freeze_start).to eq('0 0 * * *') end end end + + shared_examples 'within freeze period' do |time| + it 'is frozen' do + travel_to(time) do + expect(subject).to eq(Ci::FreezePeriod::STATUS_ACTIVE) + end + end + end + + shared_examples 'outside freeze period' do |time| + it 'is not frozen' do + travel_to(time) do + expect(subject).to eq(Ci::FreezePeriod::STATUS_INACTIVE) + end + end + end + + describe '#status' do + subject { freeze_period.status } + + describe 'single freeze period' do + let(:freeze_period) do + build_stubbed(:ci_freeze_period, project: project) + end + + it_behaves_like 'outside freeze period', Time.utc(2020, 4, 10, 22, 59) + it_behaves_like 'within freeze period', Time.utc(2020, 4, 10, 23, 1) + it_behaves_like 'within freeze period', Time.utc(2020, 4, 13, 6, 59) + it_behaves_like 'outside freeze period', Time.utc(2020, 4, 13, 7, 1) + end + + # See https://gitlab.com/gitlab-org/gitlab/-/issues/370472 + context 'when period overlaps with itself' do + let(:freeze_period) do + build_stubbed(:ci_freeze_period, project: project, freeze_start: '* * * 8 *', freeze_end: '* * * 10 *') + end + + it_behaves_like 'within freeze period', Time.utc(2020, 8, 11, 0, 0) + it_behaves_like 'outside freeze period', Time.utc(2020, 10, 11, 0, 0) + end + end + + shared_examples 'a freeze period method' do + let(:freeze_period) { build_stubbed(:ci_freeze_period, project: project) } + + it 'returns the correct value' do + travel_to(now) do + expect(freeze_period.send(method)).to eq(expected) + end + end + end + + describe '#active?' do + context 'when freeze period status is active' do + it_behaves_like 'a freeze period method' do + let(:now) { saturday_1200_time } + let(:method) { :active? } + let(:expected) { true } + end + end + + context 'when freeze period status is inactive' do + it_behaves_like 'a freeze period method' do + let(:now) { tuesday_0800_time } + let(:method) { :active? } + let(:expected) { false } + end + end + end + + describe '#time_start' do + it_behaves_like 'a freeze period method' do + let(:now) { monday_0700_time } + let(:method) { :time_start } + let(:expected) { friday_2300_time } + end + end + + describe '#next_time_start' do + let(:next_friday_2300_time) { Time.utc(2020, 4, 17, 23, 0) } + + it_behaves_like 'a freeze period method' do + let(:now) { monday_0700_time } + let(:method) { :next_time_start } + let(:expected) { next_friday_2300_time } + end + end + + describe '#time_end_from_now' do + it_behaves_like 'a freeze period method' do + let(:now) { saturday_1200_time } + let(:method) { :time_end_from_now } + let(:expected) { monday_0700_time } + end + end + + describe '#time_end_from_start' do + it_behaves_like 'a freeze period method' do + let(:now) { saturday_1200_time } + let(:method) { :time_end_from_start } + let(:expected) { monday_0700_time } + end + end end diff --git a/spec/models/ci/freeze_period_status_spec.rb b/spec/models/ci/freeze_period_status_spec.rb deleted file mode 100644 index ecbb7af64f7..00000000000 --- a/spec/models/ci/freeze_period_status_spec.rb +++ /dev/null @@ -1,71 +0,0 @@ -# frozen_string_literal: true -require 'spec_helper' - -RSpec.describe Ci::FreezePeriodStatus do - let(:project) { create :project } - # '0 23 * * 5' == "At 23:00 on Friday."", '0 7 * * 1' == "At 07:00 on Monday."" - let(:friday_2300) { '0 23 * * 5' } - let(:monday_0700) { '0 7 * * 1' } - - subject { described_class.new(project: project).execute } - - shared_examples 'within freeze period' do |time| - it 'is frozen' do - travel_to(time) do - expect(subject).to be_truthy - end - end - end - - shared_examples 'outside freeze period' do |time| - it 'is not frozen' do - travel_to(time) do - expect(subject).to be_falsy - end - end - end - - describe 'single freeze period' do - let!(:freeze_period) { create(:ci_freeze_period, project: project, freeze_start: friday_2300, freeze_end: monday_0700) } - - it_behaves_like 'outside freeze period', Time.utc(2020, 4, 10, 22, 59) - - it_behaves_like 'within freeze period', Time.utc(2020, 4, 10, 23, 1) - - it_behaves_like 'within freeze period', Time.utc(2020, 4, 13, 6, 59) - - it_behaves_like 'outside freeze period', Time.utc(2020, 4, 13, 7, 1) - end - - describe 'multiple freeze periods' do - # '30 23 * * 5' == "At 23:30 on Friday."", '0 8 * * 1' == "At 08:00 on Monday."" - let(:friday_2330) { '30 23 * * 5' } - let(:monday_0800) { '0 8 * * 1' } - - let!(:freeze_period_1) { create(:ci_freeze_period, project: project, freeze_start: friday_2300, freeze_end: monday_0700) } - let!(:freeze_period_2) { create(:ci_freeze_period, project: project, freeze_start: friday_2330, freeze_end: monday_0800) } - - it_behaves_like 'outside freeze period', Time.utc(2020, 4, 10, 22, 59) - - it_behaves_like 'within freeze period', Time.utc(2020, 4, 10, 23, 29) - - it_behaves_like 'within freeze period', Time.utc(2020, 4, 11, 10, 0) - - it_behaves_like 'within freeze period', Time.utc(2020, 4, 10, 23, 1) - - it_behaves_like 'within freeze period', Time.utc(2020, 4, 13, 6, 59) - - it_behaves_like 'within freeze period', Time.utc(2020, 4, 13, 7, 59) - - it_behaves_like 'outside freeze period', Time.utc(2020, 4, 13, 8, 1) - end - - # https://gitlab.com/gitlab-org/gitlab/-/issues/370472 - context 'when period overlaps with itself' do - let!(:freeze_period) { create(:ci_freeze_period, project: project, freeze_start: '* * * 8 *', freeze_end: '* * * 10 *') } - - it_behaves_like 'within freeze period', Time.utc(2020, 8, 11, 0, 0) - - it_behaves_like 'outside freeze period', Time.utc(2020, 10, 11, 0, 0) - end -end diff --git a/spec/models/ci/job_artifact_spec.rb b/spec/models/ci/job_artifact_spec.rb index 098f8bd4514..18aaab1d1f3 100644 --- a/spec/models/ci/job_artifact_spec.rb +++ b/spec/models/ci/job_artifact_spec.rb @@ -356,50 +356,6 @@ RSpec.describe Ci::JobArtifact do end end - describe 'callbacks' do - describe '#schedule_background_upload' do - subject { create(:ci_job_artifact, :archive) } - - context 'when object storage is disabled' do - before do - stub_artifacts_object_storage(enabled: false) - end - - it 'does not schedule the migration' do - expect(ObjectStorage::BackgroundMoveWorker).not_to receive(:perform_async) - - subject - end - end - - context 'when object storage is enabled' do - context 'when background upload is enabled' do - before do - stub_artifacts_object_storage(background_upload: true) - end - - it 'schedules the model for migration' do - expect(ObjectStorage::BackgroundMoveWorker).to receive(:perform_async).with('JobArtifactUploader', described_class.name, :file, kind_of(Numeric)) - - subject - end - end - - context 'when background upload is disabled' do - before do - stub_artifacts_object_storage(background_upload: false) - end - - it 'schedules the model for migration' do - expect(ObjectStorage::BackgroundMoveWorker).not_to receive(:perform_async) - - subject - end - end - end - end - end - context 'creating the artifact' do let(:project) { create(:project) } let(:artifact) { create(:ci_job_artifact, :archive, project: project) } diff --git a/spec/models/ci/job_token/allowlist_spec.rb b/spec/models/ci/job_token/allowlist_spec.rb new file mode 100644 index 00000000000..45083d64393 --- /dev/null +++ b/spec/models/ci/job_token/allowlist_spec.rb @@ -0,0 +1,81 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Ci::JobToken::Allowlist, feature_category: :continuous_integration do + using RSpec::Parameterized::TableSyntax + + let_it_be(:source_project) { create(:project) } + + let(:allowlist) { described_class.new(source_project, direction: direction) } + let(:direction) { :outbound } + + describe '#projects' do + subject(:projects) { allowlist.projects } + + context 'when no projects are added to the scope' do + [:inbound, :outbound].each do |d| + let(:direction) { d } + + it 'returns the project defining the scope' do + expect(projects).to contain_exactly(source_project) + end + end + end + + context 'when projects are added to the scope' do + include_context 'with scoped projects' + + where(:direction, :additional_project) do + :outbound | ref(:outbound_scoped_project) + :inbound | ref(:inbound_scoped_project) + end + + with_them do + it 'returns all projects that can be accessed from a given scope' do + expect(projects).to contain_exactly(source_project, additional_project) + end + end + end + end + + describe '#includes?' do + subject { allowlist.includes?(includes_project) } + + context 'without scoped projects' do + let(:unscoped_project) { build(:project) } + + where(:includes_project, :direction, :result) do + ref(:source_project) | :outbound | false + ref(:source_project) | :inbound | false + ref(:unscoped_project) | :outbound | false + ref(:unscoped_project) | :inbound | false + end + + with_them do + it { is_expected.to be result } + end + end + + context 'with scoped projects' do + include_context 'with scoped projects' + + where(:includes_project, :direction, :result) do + ref(:source_project) | :outbound | false + ref(:source_project) | :inbound | false + ref(:inbound_scoped_project) | :outbound | false + ref(:inbound_scoped_project) | :inbound | true + ref(:outbound_scoped_project) | :outbound | true + ref(:outbound_scoped_project) | :inbound | false + ref(:unscoped_project1) | :outbound | false + ref(:unscoped_project1) | :inbound | false + ref(:unscoped_project2) | :outbound | false + ref(:unscoped_project2) | :inbound | false + end + + with_them do + it { is_expected.to be result } + end + end + end +end diff --git a/spec/models/ci/job_token/project_scope_link_spec.rb b/spec/models/ci/job_token/project_scope_link_spec.rb index 92ed86b55b2..91491733c44 100644 --- a/spec/models/ci/job_token/project_scope_link_spec.rb +++ b/spec/models/ci/job_token/project_scope_link_spec.rb @@ -2,14 +2,14 @@ require 'spec_helper' -RSpec.describe Ci::JobToken::ProjectScopeLink do +RSpec.describe Ci::JobToken::ProjectScopeLink, feature_category: :continuous_integration do + let_it_be(:project) { create(:project) } + let_it_be(:group) { create(:group) } + 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(:group) { create(:group) } - let_it_be(:project) { create(:project) } - it_behaves_like 'cleanup by a loose foreign key' do let!(:parent) { create(:user) } let!(:model) { create(:ci_job_token_project_scope_link, added_by: parent) } @@ -50,8 +50,8 @@ RSpec.describe Ci::JobToken::ProjectScopeLink do end end - describe '.from_project' do - subject { described_class.from_project(project) } + describe '.with_source' do + subject { described_class.with_source(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) } @@ -61,8 +61,8 @@ RSpec.describe Ci::JobToken::ProjectScopeLink do end end - describe '.to_project' do - subject { described_class.to_project(project) } + describe '.with_target' do + subject { described_class.with_target(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) } diff --git a/spec/models/ci/job_token/scope_spec.rb b/spec/models/ci/job_token/scope_spec.rb index 1e3f6d044d2..37c56973506 100644 --- a/spec/models/ci/job_token/scope_spec.rb +++ b/spec/models/ci/job_token/scope_spec.rb @@ -2,58 +2,72 @@ require 'spec_helper' -RSpec.describe Ci::JobToken::Scope do - let_it_be(:project) { create(:project, ci_outbound_job_token_scope_enabled: true).tap(&:save!) } +RSpec.describe Ci::JobToken::Scope, feature_category: :continuous_integration do + let_it_be(:source_project) { create(:project, ci_outbound_job_token_scope_enabled: true) } - let(:scope) { described_class.new(project) } + let(:scope) { described_class.new(source_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) + expect(all_projects).to contain_exactly(source_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) } + context 'when projects are added to the scope' do + include_context 'with scoped projects' it 'returns all projects that can be accessed from a given scope' do - expect(subject).to contain_exactly(project, scoped_project) + expect(subject).to contain_exactly(source_project, outbound_scoped_project) end end end - describe '#includes?' do - subject { scope.includes?(target_project) } + describe '#allows?' do + subject { scope.allows?(includes_project) } - context 'when param is the project defining the scope' do - let(:target_project) { project } + context 'without scoped projects' do + context 'when self referential' do + let(:includes_project) { source_project } - it { is_expected.to be_truthy } + it { is_expected.to be_truthy } + end 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 } + context 'with scoped projects' do + include_context 'with scoped projects' - it { is_expected.to be_truthy } - end + context 'when project is in outbound scope' do + let(:includes_project) { outbound_scoped_project } - 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_truthy } + end + + context 'when project is in inbound scope' do + let(:includes_project) { inbound_scoped_project } + + it { is_expected.to be_falsey } + end - it { is_expected.to be_falsey } + context 'when project is linked to a different project' do + let(:includes_project) { unscoped_project1 } + + it { is_expected.to be_falsey } + end + + context 'when project is unlinked to a project' do + let(:includes_project) { unscoped_project2 } + + it { is_expected.to be_falsey } + end context 'when project scope setting is disabled' do + let(:includes_project) { unscoped_project1 } + before do - project.ci_outbound_job_token_scope_enabled = false + source_project.ci_outbound_job_token_scope_enabled = false end it 'considers any project to be part of the scope' do diff --git a/spec/models/ci/job_variable_spec.rb b/spec/models/ci/job_variable_spec.rb index 4aebd3283f0..0a65708160a 100644 --- a/spec/models/ci/job_variable_spec.rb +++ b/spec/models/ci/job_variable_spec.rb @@ -2,11 +2,63 @@ require 'spec_helper' -RSpec.describe Ci::JobVariable do - subject { build(:ci_job_variable) } - +RSpec.describe Ci::JobVariable, feature_category: :continuous_integration do it_behaves_like "CI variable" - it { is_expected.to belong_to(:job) } - it { is_expected.to validate_uniqueness_of(:key).scoped_to(:job_id) } + describe 'associations' do + let!(:job_variable) { create(:ci_job_variable) } + + it { is_expected.to belong_to(:job) } + it { is_expected.to validate_uniqueness_of(:key).scoped_to(:job_id) } + end + + describe 'partitioning' do + let(:job_variable) { build(:ci_job_variable, job: ci_build) } + + context 'with build' do + let(:ci_build) { build(:ci_build, partition_id: ci_testing_partition_id) } + + it 'copies the partition_id from build' do + expect { job_variable.valid? }.to change { job_variable.partition_id }.to(ci_testing_partition_id) + end + + context 'when it is already set' do + let(:job_variable) { build(:ci_job_variable, partition_id: 125) } + + it 'does not change the partition_id value' do + expect { job_variable.valid? }.not_to change { job_variable.partition_id } + end + end + end + + context 'without build' do + subject(:job_variable) { build(:ci_job_variable, job: nil, partition_id: 125) } + + it { is_expected.to validate_presence_of(:partition_id) } + + it 'does not change the partition_id value' do + expect { job_variable.valid? }.not_to change { job_variable.partition_id } + end + end + + context 'when using bulk_insert', :ci_partitionable do + include Ci::PartitioningHelpers + + let(:new_pipeline) { create(:ci_pipeline) } + let(:ci_build) { create(:ci_build, pipeline: new_pipeline) } + let(:job_variable_2) { build(:ci_job_variable, job: ci_build) } + + before do + stub_current_partition_id + end + + it 'creates job variables successfully', :aggregate_failures do + described_class.bulk_insert!([job_variable, job_variable_2]) + + expect(described_class.count).to eq(2) + expect(described_class.first.partition_id).to eq(ci_testing_partition_id) + expect(described_class.last.partition_id).to eq(ci_testing_partition_id) + end + end + end end diff --git a/spec/models/ci/pending_build_spec.rb b/spec/models/ci/pending_build_spec.rb index 4bb43233dbd..331522070df 100644 --- a/spec/models/ci/pending_build_spec.rb +++ b/spec/models/ci/pending_build_spec.rb @@ -196,6 +196,28 @@ RSpec.describe Ci::PendingBuild do end end + describe 'partitioning', :ci_partitionable do + include Ci::PartitioningHelpers + + before do + stub_current_partition_id + end + + let(:new_pipeline ) { create(:ci_pipeline, project: pipeline.project) } + let(:new_build) { create(:ci_build, pipeline: new_pipeline) } + + it 'assigns the same partition id as the one that build has', :aggregate_failures do + expect(new_build.partition_id).to eq ci_testing_partition_id + expect(new_build.partition_id).not_to eq pipeline.partition_id + + described_class.upsert_from_build!(build) + described_class.upsert_from_build!(new_build) + + expect(build.reload.queuing_entry.partition_id).to eq pipeline.partition_id + expect(new_build.reload.queuing_entry.partition_id).to eq ci_testing_partition_id + end + end + it_behaves_like 'cleanup by a loose foreign key' do let!(:parent) { create(:namespace) } let!(:model) { create(:ci_pending_build, namespace: parent) } diff --git a/spec/models/ci/pipeline_schedule_spec.rb b/spec/models/ci/pipeline_schedule_spec.rb index b28b61e2b39..9b70f7c2839 100644 --- a/spec/models/ci/pipeline_schedule_spec.rb +++ b/spec/models/ci/pipeline_schedule_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Ci::PipelineSchedule do +RSpec.describe Ci::PipelineSchedule, feature_category: :continuous_integration do let_it_be_with_reload(:project) { create_default(:project) } subject { build(:ci_pipeline_schedule) } @@ -41,6 +41,12 @@ RSpec.describe Ci::PipelineSchedule do expect(pipeline_schedule).not_to be_valid end + it 'does not allow empty variable key' do + pipeline_schedule = build(:ci_pipeline_schedule, variables_attributes: [{ secret_value: 'test_value' }]) + + expect(pipeline_schedule).not_to be_valid + end + context 'when active is false' do it 'does not allow nullified ref' do pipeline_schedule = build(:ci_pipeline_schedule, :inactive, ref: nil) @@ -110,48 +116,18 @@ RSpec.describe Ci::PipelineSchedule do end describe '#set_next_run_at' do - using RSpec::Parameterized::TableSyntax - - where(:worker_cron, :schedule_cron, :plan_limit, :now, :result) do - '0 1 2 3 *' | '0 1 * * *' | nil | 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 | Time.zone.local(2021, 3, 2, 1, 0) | Time.zone.local(2022, 3, 2, 1, 0) - '*/5 * * * *' | '*/1 * * * *' | nil | 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 | Time.zone.local(2021, 5, 27, 11, 0) | Time.zone.local(2021, 5, 27, 12, 0) - '*/5 * * * *' | '*/1 * * * *' | (1.day.in_minutes / 10).to_i | Time.zone.local(2021, 5, 27, 11, 0) | Time.zone.local(2021, 5, 27, 11, 10) - '*/5 * * * *' | '*/1 * * * *' | 200 | Time.zone.local(2021, 5, 27, 11, 0) | Time.zone.local(2021, 5, 27, 11, 10) - '*/5 * * * *' | '0 * * * *' | nil | 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 | Time.zone.local(2021, 5, 27, 11, 0) | Time.zone.local(2021, 5, 27, 12, 0) - '*/5 * * * *' | '0 * * * *' | (1.day.in_minutes / 1.hour.in_minutes).to_i | 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 | 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 | Time.zone.local(2021, 5, 27, 1, 0) | Time.zone.local(2021, 5, 28, 1, 0) - '*/5 * * * *' | '0 1 * * *' | (1.day.in_minutes / 10).to_i | Time.zone.local(2021, 5, 27, 1, 0) | Time.zone.local(2021, 5, 28, 1, 0) - '*/5 * * * *' | '0 1 * * *' | (1.day.in_minutes / 8).to_i | 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 | Time.zone.local(2021, 5, 1, 1, 0) | Time.zone.local(2021, 6, 1, 1, 0) - '*/9 * * * *' | '0 1 1 * *' | (1.day.in_minutes / 1.hour.in_minutes).to_i | Time.zone.local(2021, 5, 1, 1, 9) | Time.zone.local(2021, 6, 1, 1, 0) - '*/5 * * * *' | '59 14 * * *' | (1.day.in_minutes / 1.hour.in_minutes).to_i | Time.zone.local(2021, 5, 1, 15, 0) | Time.zone.local(2021, 5, 2, 15, 0) - '*/5 * * * *' | '45 21 1 2 *' | (1.day.in_minutes / 5).to_i | Time.zone.local(2021, 2, 1, 21, 45) | Time.zone.local(2022, 2, 1, 21, 45) - end + let(:now) { Time.zone.local(2021, 3, 2, 1, 0) } + let(:pipeline_schedule) { create(:ci_pipeline_schedule, cron: "0 1 * * *") } - with_them do - let(:pipeline_schedule) { create(:ci_pipeline_schedule, cron: schedule_cron) } + it 'calls fallback method next_run_at if there is no plan limit' do + allow(Settings).to receive(:cron_jobs).and_return({ 'pipeline_schedule_worker' => { 'cron' => "0 1 2 3 *" } }) - before do - allow(Settings).to receive(:cron_jobs) do - { 'pipeline_schedule_worker' => { 'cron' => worker_cron } } - end + travel_to(now) do + expect(pipeline_schedule).to receive(:calculate_next_run_at).and_call_original - create(:plan_limits, :default_plan, ci_daily_pipeline_schedule_triggers: plan_limit) if plan_limit + pipeline_schedule.set_next_run_at - # Setting this here to override initial save with the current time - pipeline_schedule.next_run_at = now - end - - 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 + expect(pipeline_schedule.next_run_at).to eq(Time.zone.local(2022, 3, 2, 1, 0)) end end @@ -288,6 +264,17 @@ RSpec.describe Ci::PipelineSchedule do end end + describe '#worker_cron' do + before do + allow(Settings).to receive(:cron_jobs) + .and_return({ pipeline_schedule_worker: { cron: "* 1 2 3 4" } }.with_indifferent_access) + end + + it "returns cron expression set in Settings" do + expect(subject.worker_cron_expression).to eq("* 1 2 3 4") + end + end + context 'loose foreign key on ci_pipeline_schedules.project_id' do it_behaves_like 'cleanup by a loose foreign key' do let!(:parent) { create(:project) } diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 2c945898e61..b72693d9994 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -219,6 +219,29 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do end end + describe '.for_name' do + subject { described_class.for_name(name) } + + let_it_be(:pipeline1) { create(:ci_pipeline, name: 'Build pipeline') } + let_it_be(:pipeline2) { create(:ci_pipeline, name: 'Chatops pipeline') } + + context 'when name exists' do + let(:name) { 'build Pipeline' } + + it 'performs case insensitive compare' do + is_expected.to contain_exactly(pipeline1) + end + end + + context 'when name does not exist' do + let(:name) { 'absent-name' } + + it 'returns empty' do + is_expected.to be_empty + end + end + end + describe '.created_after' do let_it_be(:old_pipeline) { create(:ci_pipeline, created_at: 1.week.ago) } let_it_be(:pipeline) { create(:ci_pipeline) } @@ -5287,6 +5310,20 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do end end end + + context 'when the current user is not the bridge user' do + let(:current_user) { create(:user) } + + before do + project.add_maintainer(current_user) + end + + it 'changes bridge user to current user' do + expect { reset_bridge } + .to change { bridge.reload.user } + .from(owner).to(current_user) + end + end end context 'when the user does not have permissions for the processable' do @@ -5305,6 +5342,15 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do .and not_change { bridge_dependant_dag_job.reload.status } end end + + context 'when the current user is not the bridge user' do + let(:current_user) { create(:user) } + + it 'does not change bridge user' do + expect { reset_bridge } + .to not_change { bridge.reload.user } + end + end end end diff --git a/spec/models/ci/processable_spec.rb b/spec/models/ci/processable_spec.rb index e62e5f84a6d..07fac4ee2f7 100644 --- a/spec/models/ci/processable_spec.rb +++ b/spec/models/ci/processable_spec.rb @@ -73,6 +73,7 @@ RSpec.describe Ci::Processable do job_artifacts_network_referee job_artifacts_dotenv job_artifacts_cobertura needs job_artifacts_accessibility job_artifacts_requirements job_artifacts_coverage_fuzzing + job_artifacts_requirements_v2 job_artifacts_api_fuzzing terraform_state_versions job_artifacts_cyclonedx].freeze end @@ -423,8 +424,8 @@ RSpec.describe Ci::Processable do it 'returns all needs attributes' do is_expected.to contain_exactly( - { 'artifacts' => true, 'name' => 'test1', 'optional' => false }, - { 'artifacts' => true, 'name' => 'test2', 'optional' => false } + { 'artifacts' => true, 'name' => 'test1', 'optional' => false, 'partition_id' => build.partition_id }, + { 'artifacts' => true, 'name' => 'test2', 'optional' => false, 'partition_id' => build.partition_id } ) end end diff --git a/spec/models/ci/resource_group_spec.rb b/spec/models/ci/resource_group_spec.rb index e8eccc233db..01acf5194f0 100644 --- a/spec/models/ci/resource_group_spec.rb +++ b/spec/models/ci/resource_group_spec.rb @@ -33,7 +33,13 @@ RSpec.describe Ci::ResourceGroup do end end - describe '#assign_resource_to' do + describe '#assign_resource_to', :ci_partitionable do + include Ci::PartitioningHelpers + + before do + stub_current_partition_id + end + subject { resource_group.assign_resource_to(build) } let(:build) { create(:ci_build) } @@ -41,10 +47,12 @@ RSpec.describe Ci::ResourceGroup do it 'retains resource for the processable' do expect(resource_group.resources.first.processable).to be_nil + expect(resource_group.resources.first.partition_id).to be_nil is_expected.to eq(true) expect(resource_group.resources.first.processable).to eq(build) + expect(resource_group.resources.first.partition_id).to eq(build.partition_id) end context 'when there are no free resources' do @@ -66,7 +74,13 @@ RSpec.describe Ci::ResourceGroup do end end - describe '#release_resource_from' do + describe '#release_resource_from', :ci_partitionable do + include Ci::PartitioningHelpers + + before do + stub_current_partition_id + end + subject { resource_group.release_resource_from(build) } let(:build) { create(:ci_build) } @@ -79,10 +93,12 @@ RSpec.describe Ci::ResourceGroup do it 'releases resource from the build' do expect(resource_group.resources.first.processable).to eq(build) + expect(resource_group.resources.first.partition_id).to eq(build.partition_id) is_expected.to eq(true) expect(resource_group.resources.first.processable).to be_nil + expect(resource_group.resources.first.partition_id).to be_nil end end diff --git a/spec/models/ci/runner_namespace_spec.rb b/spec/models/ci/runner_namespace_spec.rb index 2d1fe11147c..6cbb151d703 100644 --- a/spec/models/ci/runner_namespace_spec.rb +++ b/spec/models/ci/runner_namespace_spec.rb @@ -12,4 +12,27 @@ RSpec.describe Ci::RunnerNamespace do let!(:parent) { model.namespace } end + + describe '.for_runner' do + subject(:for_runner) { described_class.for_runner(runner_ids) } + + let_it_be(:group) { create(:group) } + let_it_be(:runners) { create_list(:ci_runner, 3, :group, groups: [group]) } + + context 'with runner ids' do + let(:runner_ids) { runners[1..2].map(&:id) } + + it 'returns requested runner namespaces' do + is_expected.to eq(runners[1..2].flat_map(&:runner_namespaces)) + end + end + + context 'with runners' do + let(:runner_ids) { runners.first } + + it 'returns requested runner namespaces' do + is_expected.to eq(runners.first.runner_namespaces) + end + end + end end diff --git a/spec/models/ci/runner_spec.rb b/spec/models/ci/runner_spec.rb index 13eb7086586..803b766c822 100644 --- a/spec/models/ci/runner_spec.rb +++ b/spec/models/ci/runner_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Ci::Runner do +RSpec.describe Ci::Runner, feature_category: :runner do include StubGitlabCalls it_behaves_like 'having unique enum values' @@ -701,6 +701,30 @@ RSpec.describe Ci::Runner do it { is_expected.to eq([runner1]) } end + describe '.with_running_builds' do + subject { described_class.with_running_builds } + + let_it_be(:runner1) { create(:ci_runner) } + + context 'with no builds running' do + it { is_expected.to be_empty } + end + + context 'with single build running on runner2' do + let(:runner2) { create(:ci_runner) } + let(:runner3) { create(:ci_runner) } + + before do + project = create(:project, :repository) + pipeline = create(:ci_pipeline, project: project) + create(:ci_build, :running, runner: runner2, pipeline: pipeline) + create(:ci_build, :running, runner: runner3, pipeline: pipeline) + end + + it { is_expected.to contain_exactly(runner2, runner3) } + end + end + describe '#matches_build?' do using RSpec::Parameterized::TableSyntax diff --git a/spec/models/ci/runner_version_spec.rb b/spec/models/ci/runner_version_spec.rb index 7a4b2e8f21e..552b271fe85 100644 --- a/spec/models/ci/runner_version_spec.rb +++ b/spec/models/ci/runner_version_spec.rb @@ -2,16 +2,16 @@ require 'spec_helper' -RSpec.describe Ci::RunnerVersion do - it_behaves_like 'having unique enum values' +RSpec.describe Ci::RunnerVersion, feature_category: :runner_fleet do + let_it_be(:runner_version_recommended) do + create(:ci_runner_version, version: 'abc234', status: :recommended) + end let_it_be(:runner_version_not_available) do create(:ci_runner_version, version: 'abc123', status: :not_available) end - let_it_be(:runner_version_recommended) do - create(:ci_runner_version, version: 'abc234', status: :recommended) - end + it_behaves_like 'having unique enum values' describe '.not_available' do subject { described_class.not_available } @@ -28,11 +28,9 @@ RSpec.describe Ci::RunnerVersion do end it 'contains any valid or unprocessed runner version that is not already recommended' do - is_expected.to match_array([ - runner_version_nil, - runner_version_not_available, - runner_version_available - ]) + is_expected.to match_array( + [runner_version_nil, runner_version_not_available, runner_version_available] + ) end end diff --git a/spec/models/ci/running_build_spec.rb b/spec/models/ci/running_build_spec.rb index d2f74494308..1a5ea044ba3 100644 --- a/spec/models/ci/running_build_spec.rb +++ b/spec/models/ci/running_build_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Ci::RunningBuild do +RSpec.describe Ci::RunningBuild, feature_category: :continuous_integration do let_it_be(:project) { create(:project) } let_it_be(:pipeline) { create(:ci_pipeline, project: project) } @@ -50,6 +50,28 @@ RSpec.describe Ci::RunningBuild do end end + describe 'partitioning', :ci_partitionable do + include Ci::PartitioningHelpers + + before do + stub_current_partition_id + end + + let(:new_pipeline ) { create(:ci_pipeline, project: pipeline.project) } + let(:new_build) { create(:ci_build, :running, pipeline: new_pipeline, runner: runner) } + + it 'assigns the same partition id as the one that build has', :aggregate_failures do + expect(new_build.partition_id).to eq ci_testing_partition_id + expect(new_build.partition_id).not_to eq pipeline.partition_id + + described_class.upsert_shared_runner_build!(build) + described_class.upsert_shared_runner_build!(new_build) + + expect(build.reload.runtime_metadata.partition_id).to eq pipeline.partition_id + expect(new_build.reload.runtime_metadata.partition_id).to eq ci_testing_partition_id + end + end + it_behaves_like 'cleanup by a loose foreign key' do let!(:parent) { create(:project) } let!(:model) { create(:ci_running_build, project: parent) } diff --git a/spec/models/ci/secure_file_spec.rb b/spec/models/ci/secure_file_spec.rb index 4413bd8e98b..87077fe2db1 100644 --- a/spec/models/ci/secure_file_spec.rb +++ b/spec/models/ci/secure_file_spec.rb @@ -138,17 +138,48 @@ RSpec.describe Ci::SecureFile do end describe '#update_metadata!' do - it 'assigns the expected metadata when a parsable file is supplied' do + it 'assigns the expected metadata when a parsable .cer file is supplied' do file = create(:ci_secure_file, name: 'file1.cer', file: CarrierWaveStringFile.new(fixture_file('ci_secure_files/sample.cer'))) file.update_metadata! + file.reload + expect(file.expires_at).to eq(DateTime.parse('2022-04-26 19:20:40')) expect(file.metadata['id']).to eq('33669367788748363528491290218354043267') expect(file.metadata['issuer']['CN']).to eq('Apple Worldwide Developer Relations Certification Authority') expect(file.metadata['subject']['OU']).to eq('N7SYAN8PX8') end + it 'assigns the expected metadata when a parsable .p12 file is supplied' do + file = create(:ci_secure_file, name: 'file1.p12', + file: CarrierWaveStringFile.new(fixture_file('ci_secure_files/sample.p12'))) + file.update_metadata! + + file.reload + + expect(file.expires_at).to eq(DateTime.parse('2022-09-21 14:56:00')) + expect(file.metadata['id']).to eq('75949910542696343243264405377658443914') + expect(file.metadata['issuer']['CN']).to eq('Apple Worldwide Developer Relations Certification Authority') + expect(file.metadata['subject']['OU']).to eq('N7SYAN8PX8') + end + + it 'assigns the expected metadata when a parsable .mobileprovision file is supplied' do + file = create(:ci_secure_file, name: 'file1.mobileprovision', + file: CarrierWaveStringFile.new( + fixture_file('ci_secure_files/sample.mobileprovision') + )) + file.update_metadata! + + file.reload + + expect(file.expires_at).to eq(DateTime.parse('2023-08-01 23:15:13')) + expect(file.metadata['id']).to eq('6b9fcce1-b9a9-4b37-b2ce-ec4da2044abf') + expect(file.metadata['platforms'].first).to eq('iOS') + expect(file.metadata['app_name']).to eq('iOS Demo') + expect(file.metadata['app_id']).to eq('match Development com.gitlab.ios-demo') + end + it 'logs an error when something goes wrong with the file parsing' do corrupt_file = create(:ci_secure_file, name: 'file1.cer', file: CarrierWaveStringFile.new('11111111')) message = 'Validation failed: Metadata must be a valid json schema - not enough data.' diff --git a/spec/models/ci/sources/pipeline_spec.rb b/spec/models/ci/sources/pipeline_spec.rb index fdc1c111c40..707872d0a15 100644 --- a/spec/models/ci/sources/pipeline_spec.rb +++ b/spec/models/ci/sources/pipeline_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Ci::Sources::Pipeline do +RSpec.describe Ci::Sources::Pipeline, feature_category: :continuous_integration do it { is_expected.to belong_to(:project) } it { is_expected.to belong_to(:pipeline) } @@ -31,4 +31,20 @@ RSpec.describe Ci::Sources::Pipeline do let!(:model) { create(:ci_sources_pipeline, project: parent) } end end + + describe 'partitioning', :ci_partitioning do + include Ci::PartitioningHelpers + + let(:new_pipeline) { create(:ci_pipeline) } + let(:source_pipeline) { create(:ci_sources_pipeline, pipeline: new_pipeline) } + + before do + stub_current_partition_id + end + + it 'assigns partition_id and source_partition_id from pipeline and source_job', :aggregate_failures do + expect(source_pipeline.partition_id).to eq(ci_testing_partition_id) + expect(source_pipeline.source_partition_id).to eq(ci_testing_partition_id) + end + end end diff --git a/spec/models/ci/unit_test_failure_spec.rb b/spec/models/ci/unit_test_failure_spec.rb index f9b8c66b603..7ffd103bc7d 100644 --- a/spec/models/ci/unit_test_failure_spec.rb +++ b/spec/models/ci/unit_test_failure_spec.rb @@ -70,4 +70,46 @@ RSpec.describe Ci::UnitTestFailure do end end end + + describe 'partitioning', :ci_partitionable do + let(:project) { FactoryBot.build(:project) } + let(:unit_test) { FactoryBot.build(:ci_unit_test, project: project) } + + context 'with build' do + let(:build) { FactoryBot.build(:ci_build, partition_id: ci_testing_partition_id) } + let(:unit_test_failure) do + FactoryBot.build(:ci_unit_test_failure, build: build, unit_test: unit_test, failed_at: 1.day.ago) + end + + it 'copies the partition_id from build' do + expect { unit_test_failure.valid? }.to change { unit_test_failure.partition_id }.to(ci_testing_partition_id) + end + + context 'when it is already set' do + let(:unit_test_failure) do + FactoryBot.build( + :ci_unit_test_failure, + build: build, + unit_test: unit_test, + failed_at: 1.day.ago, + partition_id: 125 + ) + end + + it 'does not change the partition_id value' do + expect { unit_test_failure.valid? }.not_to change { unit_test_failure.partition_id } + end + end + end + + context 'without build' do + subject(:unit_test_failure) { FactoryBot.build(:ci_unit_test_failure, build: nil, partition_id: 125) } + + it { is_expected.to validate_presence_of(:partition_id) } + + it 'does not change the partition_id value' do + expect { unit_test_failure.valid? }.not_to change { unit_test_failure.partition_id } + end + end + end end |