diff options
Diffstat (limited to 'spec/models/ci')
-rw-r--r-- | spec/models/ci/bridge_spec.rb | 14 | ||||
-rw-r--r-- | spec/models/ci/build_dependencies_spec.rb | 4 | ||||
-rw-r--r-- | spec/models/ci/build_metadata_spec.rb | 13 | ||||
-rw-r--r-- | spec/models/ci/build_runner_session_spec.rb | 2 | ||||
-rw-r--r-- | spec/models/ci/build_spec.rb | 249 | ||||
-rw-r--r-- | spec/models/ci/daily_build_group_report_result_spec.rb | 2 | ||||
-rw-r--r-- | spec/models/ci/job_artifact_spec.rb | 14 | ||||
-rw-r--r-- | spec/models/ci/pipeline_schedule_spec.rb | 2 | ||||
-rw-r--r-- | spec/models/ci/pipeline_spec.rb | 311 | ||||
-rw-r--r-- | spec/models/ci/processable_spec.rb | 2 | ||||
-rw-r--r-- | spec/models/ci/runner_spec.rb | 65 | ||||
-rw-r--r-- | spec/models/ci/runner_version_spec.rb | 9 | ||||
-rw-r--r-- | spec/models/ci/secure_file_spec.rb | 1 |
13 files changed, 586 insertions, 102 deletions
diff --git a/spec/models/ci/bridge_spec.rb b/spec/models/ci/bridge_spec.rb index cb29cce554f..40c2d62c465 100644 --- a/spec/models/ci/bridge_spec.rb +++ b/spec/models/ci/bridge_spec.rb @@ -25,6 +25,8 @@ RSpec.describe Ci::Bridge do expect(bridge).to have_many(:sourced_pipelines) end + it_behaves_like 'has ID tokens', :ci_bridge + it 'has one downstream pipeline' do expect(bridge).to have_one(:sourced_pipeline) expect(bridge).to have_one(:downstream_pipeline) @@ -401,6 +403,18 @@ RSpec.describe Ci::Bridge do end end + describe '#downstream_project_path' do + context 'when trigger is defined' do + context 'when using variable expansion' do + let(:options) { { trigger: { project: 'my/$BRIDGE/project' } } } + + it 'correctly expands variables' do + expect(bridge.downstream_project_path).to eq('my/cross/project') + end + end + end + end + describe '#target_ref' do context 'when trigger is defined' do it 'returns a ref name' do diff --git a/spec/models/ci/build_dependencies_spec.rb b/spec/models/ci/build_dependencies_spec.rb index 91048cae064..737348765d9 100644 --- a/spec/models/ci/build_dependencies_spec.rb +++ b/spec/models/ci/build_dependencies_spec.rb @@ -76,8 +76,8 @@ RSpec.describe Ci::BuildDependencies do end describe 'jobs from specified dependencies' do - let(:dependencies) { } - let(:needs) { } + let(:dependencies) {} + let(:needs) {} let!(:job) do scheduling_type = needs.present? ? :dag : :stage diff --git a/spec/models/ci/build_metadata_spec.rb b/spec/models/ci/build_metadata_spec.rb index 5e30f9160cd..e904463a5ca 100644 --- a/spec/models/ci/build_metadata_spec.rb +++ b/spec/models/ci/build_metadata_spec.rb @@ -105,6 +105,13 @@ RSpec.describe Ci::BuildMetadata do } } } + metadata.id_tokens = { + TEST_JWT_TOKEN: { + id_token: { + aud: 'https://gitlab.test' + } + } + } expect(metadata).to be_valid end @@ -113,10 +120,14 @@ RSpec.describe Ci::BuildMetadata do context 'when data is invalid' do it 'returns errors' do metadata.secrets = { DATABASE_PASSWORD: { vault: {} } } + metadata.id_tokens = { TEST_JWT_TOKEN: { id_token: { aud: nil } } } aggregate_failures do expect(metadata).to be_invalid - expect(metadata.errors.full_messages).to eq(["Secrets must be a valid json schema"]) + expect(metadata.errors.full_messages).to contain_exactly( + 'Secrets must be a valid json schema', + 'Id tokens must be a valid json schema' + ) end end end diff --git a/spec/models/ci/build_runner_session_spec.rb b/spec/models/ci/build_runner_session_spec.rb index 601c6ad26f9..ed5ed456d7b 100644 --- a/spec/models/ci/build_runner_session_spec.rb +++ b/spec/models/ci/build_runner_session_spec.rb @@ -65,7 +65,7 @@ RSpec.describe Ci::BuildRunnerSession, model: true do end describe '#service_specification' do - let(:service) { 'foo'} + let(:service) { 'foo' } let(:port) { 80 } let(:path) { 'path' } let(:subprotocols) { nil } diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index e0166ba64a4..b865688d370 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -3,6 +3,9 @@ require 'spec_helper' RSpec.describe Ci::Build do + include Ci::TemplateHelpers + include AfterNextHelpers + let_it_be(:user) { create(:user) } let_it_be(:group, reload: true) { create(:group) } let_it_be(:project, reload: true) { create(:project, :repository, group: group) } @@ -59,11 +62,36 @@ RSpec.describe Ci::Build do describe 'callbacks' do context 'when running after_create callback' do - it 'triggers asynchronous build hooks worker' do - expect(BuildHooksWorker).to receive(:perform_async) + it 'executes hooks' do + expect_next(described_class).to receive(:execute_hooks) create(:ci_build) end + + context 'when the execute_build_hooks_inline flag is disabled' do + before do + stub_feature_flags(execute_build_hooks_inline: false) + end + + it 'uses the old job hooks worker' do + expect(::BuildHooksWorker).to receive(:perform_async).with(Ci::Build) + + create(:ci_build) + end + end + + context 'when the execute_build_hooks_inline flag is enabled for a project' do + before do + stub_feature_flags(execute_build_hooks_inline: project) + end + + it 'executes hooks inline' do + expect(::BuildHooksWorker).not_to receive(:perform_async) + expect_next(described_class).to receive(:execute_hooks) + + create(:ci_build, project: project) + end + end end end @@ -81,6 +109,8 @@ RSpec.describe Ci::Build do end end + it_behaves_like 'has ID tokens', :ci_build + describe '.manual_actions' do let!(:manual_but_created) { create(:ci_build, :manual, status: :created, pipeline: pipeline) } let!(:manual_but_succeeded) { create(:ci_build, :manual, status: :success, pipeline: pipeline) } @@ -1289,7 +1319,7 @@ RSpec.describe Ci::Build do let(:subject) { build.hide_secrets(data) } context 'hide runners token' do - let(:data) { "new #{project.runners_token} data"} + let(:data) { "new #{project.runners_token} data" } it { is_expected.to match(/^new x+ data$/) } @@ -1303,7 +1333,7 @@ RSpec.describe Ci::Build do end context 'hide build token' do - let(:data) { "new #{build.token} data"} + let(:data) { "new #{build.token} data" } it { is_expected.to match(/^new x+ data$/) } @@ -1335,6 +1365,43 @@ RSpec.describe Ci::Build do end end + describe 'state transition metrics' do + using RSpec::Parameterized::TableSyntax + + subject { build.send(event) } + + where(:ff_enabled, :state, :report_count, :trait) do + true | :success! | 1 | :sast + true | :cancel! | 1 | :sast + true | :drop! | 2 | :multiple_report_artifacts + true | :success! | 0 | :allowed_to_fail + true | :skip! | 0 | :pending + false | :success! | 0 | :sast + end + + with_them do + let(:build) { create(:ci_build, trait, project: project, pipeline: pipeline) } + let(:event) { state } + + context "when transitioning to #{params[:state]}" do + before do + allow(Gitlab).to receive(:com?).and_return(true) + stub_feature_flags(report_artifact_build_completed_metrics_on_build_completion: ff_enabled) + end + + it 'increments build_completed_report_type metric' do + expect( + ::Gitlab::Ci::Artifacts::Metrics + ).to receive( + :build_completed_report_type_counter + ).exactly(report_count).times.and_call_original + + subject + end + end + end + end + describe 'state transition as a deployable' do subject { build.send(event) } @@ -1518,8 +1585,8 @@ RSpec.describe Ci::Build do end end - describe '#environment_deployment_tier' do - subject { build.environment_deployment_tier } + describe '#environment_tier_from_options' do + subject { build.environment_tier_from_options } let(:build) { described_class.new(options: options) } let(:options) { { environment: { deployment_tier: 'production' } } } @@ -1533,6 +1600,30 @@ RSpec.describe Ci::Build do end end + describe '#environment_tier' do + subject { build.environment_tier } + + let(:options) { { environment: { deployment_tier: 'production' } } } + let!(:environment) { create(:environment, name: 'production', tier: 'development', project: project) } + let(:build) { described_class.new(options: options, environment: 'production', project: project) } + + it { is_expected.to eq('production') } + + context 'when options does not include deployment_tier' do + let(:options) { { environment: { name: 'production' } } } + + it 'uses tier from environment' do + is_expected.to eq('development') + end + + context 'when persisted environment is absent' do + let(:environment) { nil } + + it { is_expected.to be_nil } + end + end + end + describe 'environment' do describe '#has_environment?' do subject { build.has_environment? } @@ -1601,20 +1692,18 @@ RSpec.describe Ci::Build do end it 'returns an expanded environment name with a list of variables' do - expect(build).to receive(:simple_variables).once.and_call_original - is_expected.to eq('review/host') end context 'when build metadata has already persisted the expanded environment name' do before do - build.metadata.expanded_environment_name = 'review/host' + build.metadata.expanded_environment_name = 'review/foo' end it 'returns a persisted expanded environment name without a list of variables' do expect(build).not_to receive(:simple_variables) - is_expected.to eq('review/host') + is_expected.to eq('review/foo') end end end @@ -1642,14 +1731,6 @@ RSpec.describe Ci::Build do end it { is_expected.to eq('review/master') } - - context 'when the FF ci_expand_environment_name_and_url is disabled' do - before do - stub_feature_flags(ci_expand_environment_name_and_url: false) - end - - it { is_expected.to eq('review/${CI_COMMIT_REF_NAME}') } - end end end @@ -1693,7 +1774,7 @@ RSpec.describe Ci::Build do end context 'with a dynamic value' do - let(:namespace) { 'deploy-$CI_COMMIT_REF_NAME'} + let(:namespace) { 'deploy-$CI_COMMIT_REF_NAME' } it { is_expected.to eq 'deploy-master' } end @@ -1806,6 +1887,21 @@ RSpec.describe Ci::Build do end context 'build is erasable' do + context 'logging erase' do + let!(:build) { create(:ci_build, :test_reports, :trace_artifact, :success, :artifacts) } + + it 'logs erased artifacts' do + expect(Gitlab::Ci::Artifacts::Logger) + .to receive(:log_deleted) + .with( + match_array(build.job_artifacts.to_a), + 'Ci::Build#erase' + ) + + build.erase + end + end + context 'when project is undergoing stats refresh' do let!(:build) { create(:ci_build, :test_reports, :trace_artifact, :success, :artifacts) } @@ -1908,7 +2004,14 @@ RSpec.describe Ci::Build do end end - it "erases erasable artifacts" do + it "erases erasable artifacts and logs them" do + expect(Gitlab::Ci::Artifacts::Logger) + .to receive(:log_deleted) + .with( + match_array(build.job_artifacts.erasable.to_a), + 'Ci::Build#erase_erasable_artifacts!' + ) + subject expect(build.job_artifacts.erasable).to be_empty @@ -2627,7 +2730,7 @@ RSpec.describe Ci::Build do build.update_columns(token_encrypted: nil) end - it { is_expected.to be_nil} + it { is_expected.to be_nil } end end @@ -2812,6 +2915,7 @@ RSpec.describe Ci::Build do public: true, masked: false }, { key: 'CI_API_V4_URL', value: 'http://localhost/api/v4', public: true, masked: false }, + { key: 'CI_TEMPLATE_REGISTRY_HOST', value: template_registry_host, public: true, masked: false }, { key: 'CI_PIPELINE_IID', value: pipeline.iid.to_s, public: true, masked: false }, { key: 'CI_PIPELINE_SOURCE', value: pipeline.source, public: true, masked: false }, { key: 'CI_PIPELINE_CREATED_AT', value: pipeline.created_at.iso8601, public: true, masked: false }, @@ -2929,7 +3033,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_TIER CI_ENVIRONMENT_ACTION CI_ENVIRONMENT_URL] + CI_ENVIRONMENT_ACTION CI_ENVIRONMENT_TIER CI_ENVIRONMENT_URL] end before do @@ -3096,6 +3200,16 @@ RSpec.describe Ci::Build do end end + context 'when environment_tier is updated in options' do + before do + build.update!(options: { environment: { name: 'production', deployment_tier: 'development' } }) + end + + it 'uses tier from options' do + is_expected.to include({ key: 'CI_ENVIRONMENT_TIER', value: 'development', public: true, masked: false }) + end + end + context 'when project has an environment specific variable' do let(:environment_specific_variable) do { key: 'MY_STAGING_ONLY_VARIABLE', value: 'environment_specific_variable', public: false, masked: false } @@ -3508,8 +3622,8 @@ RSpec.describe Ci::Build do context 'when gitlab-deploy-token does not exist for project' do it 'does not include deploy token variables' do - expect(subject.find { |v| v[:key] == 'CI_DEPLOY_USER'}).to be_nil - expect(subject.find { |v| v[:key] == 'CI_DEPLOY_PASSWORD'}).to be_nil + expect(subject.find { |v| v[:key] == 'CI_DEPLOY_USER' }).to be_nil + expect(subject.find { |v| v[:key] == 'CI_DEPLOY_PASSWORD' }).to be_nil end context 'when gitlab-deploy-token exists for group' do @@ -3527,8 +3641,8 @@ RSpec.describe Ci::Build do end it 'does not include deploy token variables' do - expect(subject.find { |v| v[:key] == 'CI_DEPLOY_USER'}).to be_nil - expect(subject.find { |v| v[:key] == 'CI_DEPLOY_PASSWORD'}).to be_nil + expect(subject.find { |v| v[:key] == 'CI_DEPLOY_USER' }).to be_nil + expect(subject.find { |v| v[:key] == 'CI_DEPLOY_PASSWORD' }).to be_nil end end end @@ -3559,10 +3673,10 @@ RSpec.describe Ci::Build do context 'when harbor_integration does not exist' do it 'does not include harbor variables' do - expect(subject.find { |v| v[:key] == 'HARBOR_URL'}).to be_nil - expect(subject.find { |v| v[:key] == 'HARBOR_PROJECT_NAME'}).to be_nil - expect(subject.find { |v| v[:key] == 'HARBOR_USERNAME'}).to be_nil - expect(subject.find { |v| v[:key] == 'HARBOR_PASSWORD'}).to be_nil + expect(subject.find { |v| v[:key] == 'HARBOR_URL' }).to be_nil + expect(subject.find { |v| v[:key] == 'HARBOR_PROJECT_NAME' }).to be_nil + expect(subject.find { |v| v[:key] == 'HARBOR_USERNAME' }).to be_nil + expect(subject.find { |v| v[:key] == 'HARBOR_PASSWORD' }).to be_nil end end end @@ -3807,8 +3921,20 @@ RSpec.describe Ci::Build do build.enqueue end - it 'queues BuildHooksWorker' do - expect(BuildHooksWorker).to receive(:perform_async).with(build) + context 'when the execute_build_hooks_inline flag is disabled' do + before do + stub_feature_flags(execute_build_hooks_inline: false) + end + + it 'queues BuildHooksWorker' do + expect(BuildHooksWorker).to receive(:perform_async).with(build) + + build.enqueue + end + end + + it 'executes hooks' do + expect(build).to receive(:execute_hooks) build.enqueue end @@ -4526,7 +4652,7 @@ RSpec.describe Ci::Build do end describe '#each_report' do - let(:report_types) { Ci::JobArtifact::COVERAGE_REPORT_FILE_TYPES } + let(:report_types) { Ci::JobArtifact.file_types_for_report(:coverage) } let!(:codequality) { create(:ci_job_artifact, :codequality, job: build) } let!(:coverage) { create(:ci_job_artifact, :coverage_gocov_xml, job: build) } @@ -4559,6 +4685,7 @@ RSpec.describe Ci::Build do end before do + allow(build).to receive(:execute_hooks) stub_artifacts_object_storage end @@ -5499,7 +5626,7 @@ RSpec.describe Ci::Build do build.cancel_gracefully? end - let_it_be(:build) { create(:ci_build, pipeline: pipeline) } + let(:build) { create(:ci_build, pipeline: pipeline) } it 'cannot cancel gracefully' do expect(subject).to be false @@ -5520,4 +5647,58 @@ RSpec.describe Ci::Build do let!(:model) { create(:ci_build, user: create(:user)) } let!(:parent) { model.user } end + + describe '#clone' do + let_it_be(:user) { FactoryBot.build(:user) } + + context 'when given new job variables' do + context 'when the cloned build has an action' do + it 'applies the new job variables' do + build = create(:ci_build, :actionable) + create(:ci_job_variable, job: build, key: 'TEST_KEY', value: 'old value') + create(:ci_job_variable, job: build, key: 'OLD_KEY', value: 'i will not live for long') + + new_build = build.clone(current_user: user, new_job_variables_attributes: [ + { key: 'TEST_KEY', value: 'new value' }, + { key: 'NEW_KEY', value: 'exciting new value' } + ]) + new_build.save! + + expect(new_build.job_variables.count).to be(2) + expect(new_build.job_variables.pluck(:key)).to contain_exactly('TEST_KEY', 'NEW_KEY') + expect(new_build.job_variables.map(&:value)).to contain_exactly('new value', 'exciting new value') + end + end + + context 'when the cloned build does not have an action' do + it 'applies the old job variables' do + build = create(:ci_build) + create(:ci_job_variable, job: build, key: 'TEST_KEY', value: 'old value') + + new_build = build.clone(current_user: user, new_job_variables_attributes: [ + { key: 'TEST_KEY', value: 'new value' } + ]) + new_build.save! + + expect(new_build.job_variables.count).to be(1) + expect(new_build.job_variables.pluck(:key)).to contain_exactly('TEST_KEY') + expect(new_build.job_variables.map(&:value)).to contain_exactly('old value') + end + end + end + + context 'when not given new job variables' do + it 'applies the old job variables' do + build = create(:ci_build) + create(:ci_job_variable, job: build, key: 'TEST_KEY', value: 'old value') + + new_build = build.clone(current_user: user) + new_build.save! + + expect(new_build.job_variables.count).to be(1) + expect(new_build.job_variables.pluck(:key)).to contain_exactly('TEST_KEY') + expect(new_build.job_variables.map(&:value)).to contain_exactly('old value') + end + end + end end diff --git a/spec/models/ci/daily_build_group_report_result_spec.rb b/spec/models/ci/daily_build_group_report_result_spec.rb index 43ba4c32477..d0141a1469e 100644 --- a/spec/models/ci/daily_build_group_report_result_spec.rb +++ b/spec/models/ci/daily_build_group_report_result_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' RSpec.describe Ci::DailyBuildGroupReportResult do - let(:daily_build_group_report_result) { build(:ci_daily_build_group_report_result)} + let(:daily_build_group_report_result) { build(:ci_daily_build_group_report_result) } describe 'associations' do it { is_expected.to belong_to(:last_pipeline) } diff --git a/spec/models/ci/job_artifact_spec.rb b/spec/models/ci/job_artifact_spec.rb index b9cac6c3f99..b996bf84529 100644 --- a/spec/models/ci/job_artifact_spec.rb +++ b/spec/models/ci/job_artifact_spec.rb @@ -128,6 +128,18 @@ RSpec.describe Ci::JobArtifact do end end + describe '.file_types_for_report' do + it 'returns the report file types for the report type' do + expect(described_class.file_types_for_report(:test)).to match_array(%w[junit]) + end + + context 'when given an unrecognized report type' do + it 'raises error' do + expect { described_class.file_types_for_report(:blah) }.to raise_error(KeyError, /blah/) + end + end + end + describe '.associated_file_types_for' do using RSpec::Parameterized::TableSyntax @@ -193,7 +205,7 @@ RSpec.describe Ci::JobArtifact do it { is_expected.to be_truthy } context 'when the job does have archived trace' do - let!(:artifact) { } + let!(:artifact) {} it { is_expected.to be_falsy } end diff --git a/spec/models/ci/pipeline_schedule_spec.rb b/spec/models/ci/pipeline_schedule_spec.rb index 3c295fb345b..b28b61e2b39 100644 --- a/spec/models/ci/pipeline_schedule_spec.rb +++ b/spec/models/ci/pipeline_schedule_spec.rb @@ -73,7 +73,7 @@ RSpec.describe Ci::PipelineSchedule do end context 'when there are no runnable schedules' do - let!(:pipeline_schedule) { } + let!(:pipeline_schedule) {} it 'returns an empty array' do is_expected.to be_empty diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 6a71b2cfbed..0c28c99c113 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -844,6 +844,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do it 'has 8 items' do expect(subject.size).to eq(8) end + it { expect(pipeline.sha).to start_with(subject) } end @@ -2162,6 +2163,60 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do end end + describe '#modified_paths_since' do + let(:project) do + create(:project, :custom_repo, + files: { 'file1.txt' => 'file 1' }) + end + + let(:user) { project.owner } + let(:main_branch) { project.default_branch } + let(:new_branch) { 'feature_x' } + let(:pipeline) { build(:ci_pipeline, project: project, sha: new_branch) } + + subject(:modified_paths_since) { pipeline.modified_paths_since(main_branch) } + + before do + project.repository.add_branch(user, new_branch, main_branch) + end + + context 'when no change in the new branch' do + it 'returns an empty array' do + expect(modified_paths_since).to be_empty + end + end + + context 'when adding a new file' do + before do + project.repository.create_file(user, 'file2.txt', 'file 2', message: 'Create file2.txt', branch_name: new_branch) + end + + it 'returns the new file path' do + expect(modified_paths_since).to eq(['file2.txt']) + end + + context 'and when updating an existing file' do + before do + project.repository.update_file(user, 'file1.txt', 'file 1 updated', message: 'Update file1.txt', branch_name: new_branch) + end + + it 'returns the new and updated file paths' do + expect(modified_paths_since).to eq(['file1.txt', 'file2.txt']) + end + end + end + + context 'when updating an existing file' do + before do + project.repository.update_file(user, 'file1.txt', 'file 1 updated', message: 'Update file1.txt', branch_name: new_branch) + end + + it 'returns the updated file path' do + expect(modified_paths_since).to eq(['file1.txt']) + end + end + end + describe '#all_worktree_paths' do let(:files) { { 'main.go' => '', 'mocks/mocks.go' => '' } } let(:project) { create(:project, :custom_repo, files: files) } @@ -2866,7 +2921,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do end describe '#cancel_running' do - subject(:latest_status) { pipeline.statuses.pluck(:status) } + let(:latest_status) { pipeline.statuses.pluck(:status) } let_it_be(:pipeline) { create(:ci_empty_pipeline, :created) } @@ -2909,6 +2964,32 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do end end + context 'with bridge jobs' do + before do + create(:ci_bridge, :created, pipeline: pipeline) + + pipeline.cancel_running + end + + it 'bridges are canceled' do + expect(pipeline.bridges.first.status).to eq 'canceled' + end + end + + context 'when pipeline is not cancelable' do + before do + create(:ci_build, :canceled, stage_idx: 0, pipeline: pipeline) + + pipeline.cancel_running + end + + it 'does not send cancel signal to cancel self' do + expect(pipeline).not_to receive(:cancel_self_only) + + pipeline.cancel_running + end + end + context 'preloading relations' do let(:pipeline1) { create(:ci_empty_pipeline, :created) } let(:pipeline2) { create(:ci_empty_pipeline, :created) } @@ -2940,37 +3021,211 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do end end - context 'when the first try cannot get an exclusive lock' do - let(:retries) { 1 } + shared_examples 'retries' do + context 'when the first try cannot get an exclusive lock' do + let(:retries) { 1 } - subject(:cancel_running) { pipeline.cancel_running(retries: retries) } + subject { pipeline.cancel_running(retries: retries) } - before do - build = create(:ci_build, :running, pipeline: pipeline) + before do + create(:ci_build, :running, pipeline: pipeline) - allow(pipeline.cancelable_statuses).to receive(:find_in_batches).and_yield([build]) + stub_first_cancel_call_fails + end + + it 'retries again and cancels the build' do + subject + + expect(latest_status).to contain_exactly('canceled') + end + context 'when the retries parameter is 0' do + let(:retries) { 0 } + + it 'raises error' do + expect { subject }.to raise_error(ActiveRecord::StaleObjectError) + end + end + end + + def stub_first_cancel_call_fails call_count = 0 - allow(build).to receive(:cancel).and_wrap_original do |original, *args| - call_count >= retries ? raise(ActiveRecord::StaleObjectError) : original.call(*args) - call_count += 1 + allow_next_found_instance_of(Ci::Build) do |build| + allow(build).to receive(:cancel).and_wrap_original do |original, *args| # rubocop:disable RSpec/AnyInstanceOf + call_count >= retries ? raise(ActiveRecord::StaleObjectError) : original.call(*args) + + call_count += 1 + end end end + end + + it_behaves_like 'retries' - it 'retries again and cancels the build' do - cancel_running + context 'when auto canceled' do + let!(:canceled_by) { create(:ci_empty_pipeline) } - expect(latest_status).to contain_exactly('canceled') + before do + create(:ci_build, :running, pipeline: pipeline) + + pipeline.cancel_running(auto_canceled_by_pipeline_id: canceled_by.id) + end + + it 'sets auto cancel' do + jobs_canceled_by = pipeline.statuses.map { |s| s.auto_canceled_by.id } + + expect(jobs_canceled_by).to contain_exactly(canceled_by.id) + expect(pipeline.auto_canceled_by.id).to eq(canceled_by.id) end + end - context 'when the retries parameter is 0' do - let(:retries) { 0 } + context 'when there are child pipelines', :sidekiq_inline do + let_it_be(:child_pipeline) { create(:ci_empty_pipeline, :created, child_of: pipeline) } - it 'raises error' do - expect do + before do + project.clear_memoization(:cascade_cancel_pipelines_enabled) + + pipeline.reload + end + + context 'when cascade_to_children is true' do + let(:cascade_to_children) { true } + let(:canceled_by) { nil } + let(:execute_async) { true } + + let(:params) do + { + cascade_to_children: cascade_to_children, + execute_async: execute_async + }.tap do |p| + p.merge!(auto_canceled_by_pipeline_id: canceled_by.id) if canceled_by + end + end + + subject(:cancel_running) { pipeline.cancel_running(**params) } + + context 'when cancelable child pipeline builds' do + before do + create(:ci_build, :created, pipeline: child_pipeline) + create(:ci_build, :running, pipeline: child_pipeline) + end + + it 'cancels child builds' do cancel_running - end.to raise_error(ActiveRecord::StaleObjectError) + + latest_status_for_child = child_pipeline.statuses.pluck(:status) + expect(latest_status_for_child).to eq %w(canceled canceled) + expect(latest_status).to eq %w(canceled) + end + + it 'cancels bridges' do + create(:ci_bridge, :created, pipeline: pipeline) + create(:ci_bridge, :created, pipeline: child_pipeline) + + cancel_running + + expect(pipeline.bridges.reload.first.status).to eq 'canceled' + expect(child_pipeline.bridges.reload.first.status).to eq 'canceled' + end + + context 'with nested child pipelines' do + let!(:nested_child_pipeline) { create(:ci_empty_pipeline, :created, child_of: child_pipeline) } + let!(:nested_child_pipeline_build) { create(:ci_build, :created, pipeline: nested_child_pipeline) } + + it 'cancels them' do + cancel_running + + expect(nested_child_pipeline.reload.status).to eq 'canceled' + expect(nested_child_pipeline_build.reload.status).to eq 'canceled' + end + end + + context 'when auto canceled' do + let(:canceled_by) { create(:ci_empty_pipeline) } + + it 'sets auto cancel' do + cancel_running + + pipeline.reload + + jobs_canceled_by_ids = pipeline.statuses.map(&:auto_canceled_by_id) + child_pipelines_canceled_by_ids = pipeline.child_pipelines.map(&:auto_canceled_by_id) + child_pipelines_jobs_canceled_by_ids = pipeline.child_pipelines.map(&:statuses).flatten.map(&:auto_canceled_by_id) + + expect(jobs_canceled_by_ids).to contain_exactly(canceled_by.id) + expect(pipeline.auto_canceled_by_id).to eq(canceled_by.id) + expect(child_pipelines_canceled_by_ids).to contain_exactly(canceled_by.id) + expect(child_pipelines_jobs_canceled_by_ids).to contain_exactly(canceled_by.id, canceled_by.id) + end + end + + context 'when execute_async is false' do + let(:execute_async) { false } + + it 'runs sync' do + expect(::Ci::CancelPipelineWorker).not_to receive(:perform_async) + + cancel_running + end + + it 'cancels children' do + cancel_running + + latest_status_for_child = child_pipeline.statuses.pluck(:status) + expect(latest_status_for_child).to eq %w(canceled canceled) + expect(latest_status).to eq %w(canceled) + end + + context 'with nested child pipelines' do + let!(:nested_child_pipeline) { create(:ci_empty_pipeline, :created, child_of: child_pipeline) } + let!(:nested_child_pipeline_build) { create(:ci_build, :created, pipeline: nested_child_pipeline) } + + it 'cancels them' do + cancel_running + + expect(nested_child_pipeline.reload.status).to eq 'canceled' + expect(nested_child_pipeline_build.reload.status).to eq 'canceled' + end + end + end + end + + it 'does not cancel uncancelable child pipeline builds' do + create(:ci_build, :failed, pipeline: child_pipeline) + + cancel_running + + latest_status_for_child = child_pipeline.statuses.pluck(:status) + expect(latest_status_for_child).to eq %w(failed) + expect(latest_status).to eq %w(canceled) + end + end + + context 'when cascade_to_children is false' do + let(:cascade_to_children) { false } + + subject(:cancel_running) { pipeline.cancel_running(cascade_to_children: cascade_to_children) } + + it 'does not cancel cancelable child pipeline builds' do + create(:ci_build, :created, pipeline: child_pipeline) + create(:ci_build, :running, pipeline: child_pipeline) + + cancel_running + + latest_status_for_child = child_pipeline.statuses.order_id_desc.pluck(:status) + expect(latest_status_for_child).to eq %w(running created) + expect(latest_status).to eq %w(canceled) + end + + it 'does not cancel uncancelable child pipeline builds' do + create(:ci_build, :failed, pipeline: child_pipeline) + + cancel_running + + latest_status_for_child = child_pipeline.statuses.pluck(:status) + expect(latest_status_for_child).to eq %w(failed) + expect(latest_status).to eq %w(canceled) end end end @@ -3352,7 +3607,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do end context 'when pipeline is a triggered pipeline' do - let!(:upstream) { create(:ci_pipeline, project: create(:project), upstream_of: pipeline)} + let!(:upstream) { create(:ci_pipeline, project: create(:project), upstream_of: pipeline) } it 'returns self id' do expect(subject).to contain_exactly(pipeline.id) @@ -4335,24 +4590,6 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do end end end - - describe '#find_stage_by_name' do - subject { pipeline.find_stage_by_name!(stage_name) } - - context 'when stage exists' do - it { is_expected.to eq(stage) } - end - - context 'when stage does not exist' do - let(:stage_name) { 'build' } - - it 'raises an ActiveRecord exception' do - expect do - subject - end.to raise_exception(ActiveRecord::RecordNotFound) - end - end - end end describe '#full_error_messages' do diff --git a/spec/models/ci/processable_spec.rb b/spec/models/ci/processable_spec.rb index 789ae3a2ccc..127a1417d9e 100644 --- a/spec/models/ci/processable_spec.rb +++ b/spec/models/ci/processable_spec.rb @@ -72,7 +72,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_api_fuzzing terraform_state_versions].freeze + job_artifacts_api_fuzzing terraform_state_versions job_artifacts_cyclonedx].freeze end let(:ignore_accessors) do diff --git a/spec/models/ci/runner_spec.rb b/spec/models/ci/runner_spec.rb index 2fbfbbaf830..ae8748f8ae3 100644 --- a/spec/models/ci/runner_spec.rb +++ b/spec/models/ci/runner_spec.rb @@ -489,7 +489,7 @@ RSpec.describe Ci::Runner do let!(:runner3) { create(:ci_runner, :instance, contacted_at: 1.month.ago, created_at: 2.months.ago) } let!(:runner4) { create(:ci_runner, :instance, contacted_at: 1.month.ago, created_at: 3.months.ago) } - it { is_expected.to eq([runner1, runner3, runner4])} + it { is_expected.to eq([runner1, runner3, runner4]) } end describe '.active' do @@ -552,6 +552,10 @@ RSpec.describe Ci::Runner do allow_any_instance_of(described_class).to receive(:cached_attribute).and_call_original allow_any_instance_of(described_class).to receive(:cached_attribute) .with(:platform).and_return("darwin") + allow_any_instance_of(described_class).to receive(:cached_attribute) + .with(:version).and_return("14.0.0") + + allow(Ci::Runners::ProcessRunnerVersionUpdateWorker).to receive(:perform_async).once end context 'table tests' do @@ -623,6 +627,10 @@ RSpec.describe Ci::Runner do allow_any_instance_of(described_class).to receive(:cached_attribute).and_call_original allow_any_instance_of(described_class).to receive(:cached_attribute) .with(:platform).and_return("darwin") + allow_any_instance_of(described_class).to receive(:cached_attribute) + .with(:version).and_return("14.0.0") + + allow(Ci::Runners::ProcessRunnerVersionUpdateWorker).to receive(:perform_async).once end context 'no cache value' do @@ -693,19 +701,6 @@ 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) - - expect(described_class.sticking).to receive(:stick) - .with(:runner, runner.id) - - expect(Gitlab::Workhorse).to receive(:set_key_and_notify) - - runner.tick_runner_queue - end - end - describe '#matches_build?' do using RSpec::Parameterized::TableSyntax @@ -866,7 +861,7 @@ RSpec.describe Ci::Runner do describe '#status' do let(:runner) { build(:ci_runner, :instance, created_at: 4.months.ago) } - let(:legacy_mode) { } + let(:legacy_mode) {} subject { runner.status(legacy_mode) } @@ -989,6 +984,16 @@ RSpec.describe Ci::Runner do it 'returns a new last_update value' do expect(runner.tick_runner_queue).not_to be_empty end + + it 'sticks the runner to the primary and calls the original method' do + runner = create(:ci_runner) + + expect(described_class.sticking).to receive(:stick).with(:runner, runner.id) + + expect(Gitlab::Workhorse).to receive(:set_key_and_notify) + + runner.tick_runner_queue + end end describe '#ensure_runner_queue_value' do @@ -1055,14 +1060,19 @@ RSpec.describe Ci::Runner do it 'updates cache' do expect_redis_update + expect(Ci::Runners::ProcessRunnerVersionUpdateWorker).not_to receive(:perform_async) heartbeat + + expect(runner.runner_version).to be_nil end end context 'when database was not updated recently' do before do runner.contacted_at = 2.hours.ago + + allow(Ci::Runners::ProcessRunnerVersionUpdateWorker).to receive(:perform_async) end context 'with invalid runner' do @@ -1075,12 +1085,25 @@ RSpec.describe Ci::Runner do expect_redis_update does_db_update + + expect(Ci::Runners::ProcessRunnerVersionUpdateWorker).to have_received(:perform_async).once + end + end + + context 'with unchanged runner version' do + let(:runner) { create(:ci_runner, version: version) } + + it 'does not schedule ci_runner_versions update' do + heartbeat + + expect(Ci::Runners::ProcessRunnerVersionUpdateWorker).not_to have_received(:perform_async) end end it 'updates redis cache and database' do expect_redis_update does_db_update + expect(Ci::Runners::ProcessRunnerVersionUpdateWorker).to have_received(:perform_async).once end %w(custom shell docker docker-windows docker-ssh ssh parallels virtualbox docker+machine docker-ssh+machine kubernetes some-unknown-type).each do |executor| @@ -1795,11 +1818,21 @@ RSpec.describe Ci::Runner do end context ':recommended' do - let(:upgrade_status) { :recommended} + let(:upgrade_status) { :recommended } it 'returns runners whose version is assigned :recommended' do is_expected.to contain_exactly(runner_14_1_0) end end + + describe 'composed with other scopes' do + subject { described_class.active(false).with_upgrade_status(:available) } + + let(:inactive_runner_14_0_0) { create(:ci_runner, version: '14.0.0', active: false) } + + it 'returns runner matching the composed scope' do + is_expected.to contain_exactly(inactive_runner_14_0_0) + end + end end end diff --git a/spec/models/ci/runner_version_spec.rb b/spec/models/ci/runner_version_spec.rb index d3395942a39..7a4b2e8f21e 100644 --- a/spec/models/ci/runner_version_spec.rb +++ b/spec/models/ci/runner_version_spec.rb @@ -27,16 +27,11 @@ RSpec.describe Ci::RunnerVersion do create(:ci_runner_version, version: 'abc456', status: :available) end - let_it_be(:runner_version_unknown) do - create(:ci_runner_version, version: 'abc567', status: :unknown) - end - - it 'contains any runner version that is not already recommended' do + 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, - runner_version_unknown + runner_version_available ]) end end diff --git a/spec/models/ci/secure_file_spec.rb b/spec/models/ci/secure_file_spec.rb index a3f1c7b7ef7..e47efff5dfd 100644 --- a/spec/models/ci/secure_file_spec.rb +++ b/spec/models/ci/secure_file_spec.rb @@ -26,6 +26,7 @@ RSpec.describe Ci::SecureFile do it { is_expected.to validate_presence_of(:file_store) } it { is_expected.to validate_presence_of(:name) } it { is_expected.to validate_presence_of(:project_id) } + context 'unique filename' do let_it_be(:project1) { create(:project) } |