diff options
Diffstat (limited to 'spec/models/ci')
-rw-r--r-- | spec/models/ci/bridge_spec.rb | 5 | ||||
-rw-r--r-- | spec/models/ci/build_report_result_spec.rb | 74 | ||||
-rw-r--r-- | spec/models/ci/build_runner_session_spec.rb | 60 | ||||
-rw-r--r-- | spec/models/ci/build_spec.rb | 99 | ||||
-rw-r--r-- | spec/models/ci/build_trace_chunk_spec.rb | 2 | ||||
-rw-r--r-- | spec/models/ci/daily_build_group_report_result_spec.rb | 24 | ||||
-rw-r--r-- | spec/models/ci/instance_variable_spec.rb | 33 | ||||
-rw-r--r-- | spec/models/ci/job_artifact_spec.rb | 79 | ||||
-rw-r--r-- | spec/models/ci/pipeline_schedule_spec.rb | 2 | ||||
-rw-r--r-- | spec/models/ci/pipeline_spec.rb | 184 | ||||
-rw-r--r-- | spec/models/ci/ref_spec.rb | 153 | ||||
-rw-r--r-- | spec/models/ci/runner_spec.rb | 10 |
12 files changed, 631 insertions, 94 deletions
diff --git a/spec/models/ci/bridge_spec.rb b/spec/models/ci/bridge_spec.rb index 34f89d9cdae..385261e0ee9 100644 --- a/spec/models/ci/bridge_spec.rb +++ b/spec/models/ci/bridge_spec.rb @@ -21,6 +21,11 @@ describe Ci::Bridge do expect(bridge).to have_many(:sourced_pipelines) end + it 'has one downstream pipeline' do + expect(bridge).to have_one(:sourced_pipeline) + expect(bridge).to have_one(:downstream_pipeline) + end + describe '#tags' do it 'only has a bridge tag' do expect(bridge.tags).to eq [:bridge] diff --git a/spec/models/ci/build_report_result_spec.rb b/spec/models/ci/build_report_result_spec.rb new file mode 100644 index 00000000000..078b0d100a1 --- /dev/null +++ b/spec/models/ci/build_report_result_spec.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Ci::BuildReportResult do + let(:build_report_result) { build(:ci_build_report_result, :with_junit_success) } + + describe 'associations' do + it { is_expected.to belong_to(:build) } + it { is_expected.to belong_to(:project) } + end + + describe 'validations' do + it { is_expected.to validate_presence_of(:project) } + it { is_expected.to validate_presence_of(:build) } + + context 'when attributes are valid' do + it 'returns no errors' do + expect(build_report_result).to be_valid + end + end + + context 'when data is invalid' do + it 'returns errors' do + build_report_result.data = { invalid: 'data' } + + expect(build_report_result).to be_invalid + expect(build_report_result.errors.full_messages).to eq(["Data must be a valid json schema"]) + end + end + end + + describe '#tests_name' do + it 'returns the suite name' do + expect(build_report_result.tests_name).to eq("rspec") + end + end + + describe '#tests_duration' do + it 'returns the suite duration' do + expect(build_report_result.tests_duration).to eq(0.42) + end + end + + describe '#tests_success' do + it 'returns the success count' do + expect(build_report_result.tests_success).to eq(2) + end + end + + describe '#tests_failed' do + it 'returns the failed count' do + expect(build_report_result.tests_failed).to eq(0) + end + end + + describe '#tests_errored' do + it 'returns the errored count' do + expect(build_report_result.tests_errored).to eq(0) + end + end + + describe '#tests_skipped' do + it 'returns the skipped count' do + expect(build_report_result.tests_skipped).to eq(0) + end + end + + describe '#tests_total' do + it 'returns the total count' do + expect(build_report_result.tests_total).to eq(2) + end + end +end diff --git a/spec/models/ci/build_runner_session_spec.rb b/spec/models/ci/build_runner_session_spec.rb index cdf56f24cd7..3e520407884 100644 --- a/spec/models/ci/build_runner_session_spec.rb +++ b/spec/models/ci/build_runner_session_spec.rb @@ -63,4 +63,64 @@ describe Ci::BuildRunnerSession, model: true do end end end + + describe '#service_specification' do + let(:service) { 'foo'} + let(:port) { 80 } + let(:path) { 'path' } + let(:subprotocols) { nil } + let(:specification) { subject.service_specification(service: service, port: port, path: path, subprotocols: subprotocols) } + + it 'returns service proxy url' do + expect(specification[:url]).to eq "https://localhost/proxy/#{service}/#{port}/#{path}" + end + + it 'returns default service proxy websocket subprotocol' do + expect(specification[:subprotocols]).to eq %w[terminal.gitlab.com] + end + + it 'returns empty hash if no url' do + subject.url = '' + + expect(specification).to be_empty + end + + context 'when port is not present' do + let(:port) { nil } + + it 'uses the default port name' do + expect(specification[:url]).to eq "https://localhost/proxy/#{service}/default_port/#{path}" + end + end + + context 'when the service is not present' do + let(:service) { '' } + + it 'uses the service name "build" as default' do + expect(specification[:url]).to eq "https://localhost/proxy/build/#{port}/#{path}" + end + end + + context 'when url is present' do + it 'returns ca_pem nil if empty certificate' do + subject.certificate = '' + + expect(specification[:ca_pem]).to be_nil + end + + it 'adds Authorization header if authorization is present' do + subject.authorization = 'foobar' + + expect(specification[:headers]).to include(Authorization: ['foobar']) + end + end + + context 'when subprotocol is present' do + let(:subprotocols) { 'foobar' } + + it 'returns the new subprotocol' do + expect(specification[:subprotocols]).to eq [subprotocols] + end + end + end end diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index 6605866d9c0..6fdd8463329 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -24,6 +24,7 @@ describe Ci::Build do it { is_expected.to have_many(:needs) } it { is_expected.to have_many(:sourced_pipelines) } it { is_expected.to have_many(:job_variables) } + it { is_expected.to have_many(:report_results) } it { is_expected.to have_one(:deployment) } it { is_expected.to have_one(:runner_session) } @@ -626,7 +627,7 @@ describe Ci::Build do context 'is expired' do before do - build.update(artifacts_expire_at: Time.now - 7.days) + build.update(artifacts_expire_at: Time.current - 7.days) end it { is_expected.to be_truthy } @@ -634,7 +635,7 @@ describe Ci::Build do context 'is not expired' do before do - build.update(artifacts_expire_at: Time.now + 7.days) + build.update(artifacts_expire_at: Time.current + 7.days) end it { is_expected.to be_falsey } @@ -661,13 +662,13 @@ describe Ci::Build do it { is_expected.to be_nil } context 'when artifacts_expire_at is specified' do - let(:expire_at) { Time.now + 7.days } + let(:expire_at) { Time.current + 7.days } before do build.artifacts_expire_at = expire_at end - it { is_expected.to be_within(5).of(expire_at - Time.now) } + it { is_expected.to be_within(5).of(expire_at - Time.current) } end end @@ -874,6 +875,22 @@ describe Ci::Build do end end + describe '#has_test_reports?' do + subject { build.has_test_reports? } + + context 'when build has a test report' do + let(:build) { create(:ci_build, :test_reports) } + + it { is_expected.to be_truthy } + end + + context 'when build does not have a test report' do + let(:build) { create(:ci_build) } + + it { is_expected.to be_falsey } + end + end + describe '#has_old_trace?' do subject { build.has_old_trace? } @@ -1795,7 +1812,7 @@ describe Ci::Build do end describe '#keep_artifacts!' do - let(:build) { create(:ci_build, artifacts_expire_at: Time.now + 7.days) } + let(:build) { create(:ci_build, artifacts_expire_at: Time.current + 7.days) } subject { build.keep_artifacts! } @@ -2285,7 +2302,7 @@ describe Ci::Build do let(:predefined_variables) do [ { key: 'CI_PIPELINE_ID', value: pipeline.id.to_s, public: true, masked: false }, - { key: 'CI_PIPELINE_URL', value: project.web_url + "/pipelines/#{pipeline.id}", public: true, masked: false }, + { key: 'CI_PIPELINE_URL', value: project.web_url + "/-/pipelines/#{pipeline.id}", public: true, masked: false }, { key: 'CI_JOB_ID', value: build.id.to_s, public: true, masked: false }, { key: 'CI_JOB_URL', value: project.web_url + "/-/jobs/#{build.id}", public: true, masked: false }, { key: 'CI_JOB_TOKEN', value: 'my-token', public: false, masked: true }, @@ -2390,13 +2407,13 @@ describe Ci::Build do allow(build).to receive(:job_jwt_variables) { [job_jwt_var] } allow(build).to receive(:dependency_variables) { [job_dependency_var] } - allow_any_instance_of(Project) + allow(build.project) .to receive(:predefined_variables) { [project_pre_var] } project.variables.create!(key: 'secret', value: 'value') - allow_any_instance_of(Ci::Pipeline) - .to receive(:predefined_variables) { [pipeline_pre_var] } + allow(build.pipeline) + .to receive(:predefined_variables).and_return([pipeline_pre_var]) end it 'returns variables in order depending on resource hierarchy' do @@ -2512,6 +2529,17 @@ describe Ci::Build do end end end + + context 'with the :modified_path_ci_variables feature flag disabled' do + before do + stub_feature_flags(modified_path_ci_variables: false) + end + + it 'does not set CI_MERGE_REQUEST_CHANGED_PAGES_* variables' do + expect(subject.find { |var| var[:key] == 'CI_MERGE_REQUEST_CHANGED_PAGE_PATHS' }).to be_nil + expect(subject.find { |var| var[:key] == 'CI_MERGE_REQUEST_CHANGED_PAGE_URLS' }).to be_nil + end + end end context 'when build has user' do @@ -3095,24 +3123,8 @@ describe Ci::Build do let!(:job_variable) { create(:ci_job_variable, :dotenv_source, job: prepare) } - context 'FF ci_dependency_variables is enabled' do - before do - stub_feature_flags(ci_dependency_variables: true) - end - - it 'inherits dependent variables' do - expect(build.scoped_variables.to_hash).to include(job_variable.key => job_variable.value) - end - end - - context 'FF ci_dependency_variables is disabled' do - before do - stub_feature_flags(ci_dependency_variables: false) - end - - it 'does not inherit dependent variables' do - expect(build.scoped_variables.to_hash).not_to include(job_variable.key => job_variable.value) - end + it 'inherits dependent variables' do + expect(build.scoped_variables.to_hash).to include(job_variable.key => job_variable.value) end end end @@ -3601,7 +3613,7 @@ describe Ci::Build do .to receive(:execute) .with(subject) .and_raise(Gitlab::Access::AccessDeniedError) - allow(Rails.logger).to receive(:error) + allow(Gitlab::AppLogger).to receive(:error) end it 'handles raised exception' do @@ -3611,7 +3623,7 @@ describe Ci::Build do it 'logs the error' do subject.drop! - expect(Rails.logger) + expect(Gitlab::AppLogger) .to have_received(:error) .with(a_string_matching("Unable to auto-retry job #{subject.id}")) end @@ -4040,10 +4052,11 @@ describe Ci::Build do expect(terraform_reports.plans).to match( a_hash_including( - 'tfplan.json' => a_hash_including( + build.id.to_s => a_hash_including( 'create' => 0, 'update' => 1, - 'delete' => 0 + 'delete' => 0, + 'job_name' => build.options.dig(:artifacts, :name).to_s ) ) ) @@ -4203,7 +4216,7 @@ describe Ci::Build do subject { build.supported_runner?(runner_features) } - context 'when feature is required by build' do + context 'when `upload_multiple_artifacts` feature is required by build' do before do expect(build).to receive(:runner_required_feature_names) do [:upload_multiple_artifacts] @@ -4227,7 +4240,7 @@ describe Ci::Build do end end - context 'when refspecs feature is required by build' do + context 'when `refspecs` feature is required by build' do before do allow(build).to receive(:merge_request_ref?) { true } end @@ -4244,6 +4257,26 @@ describe Ci::Build do it { is_expected.to be_falsey } end end + + context 'when `release_steps` feature is required by build' do + before do + expect(build).to receive(:runner_required_feature_names) do + [:release_steps] + end + end + + context 'when runner provides given feature' do + let(:runner_features) { { release_steps: true } } + + it { is_expected.to be_truthy } + end + + context 'when runner does not provide given feature' do + let(:runner_features) { {} } + + it { is_expected.to be_falsey } + end + end end describe '#deployment_status' do diff --git a/spec/models/ci/build_trace_chunk_spec.rb b/spec/models/ci/build_trace_chunk_spec.rb index f08f05a09bf..85873847fca 100644 --- a/spec/models/ci/build_trace_chunk_spec.rb +++ b/spec/models/ci/build_trace_chunk_spec.rb @@ -33,7 +33,7 @@ describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do expect(subjects.count).to be > 0 expect { subjects.first.destroy }.to raise_error('`destroy` and `destroy_all` are forbidden. Please use `fast_destroy_all`') - expect { subjects.destroy_all }.to raise_error('`destroy` and `destroy_all` are forbidden. Please use `fast_destroy_all`') # rubocop: disable DestroyAll + expect { subjects.destroy_all }.to raise_error('`destroy` and `destroy_all` are forbidden. Please use `fast_destroy_all`') # rubocop: disable Cop/DestroyAll expect(subjects.count).to be > 0 expect(external_data_counter).to be > 0 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 d4c305c649a..f2ce1b5775f 100644 --- a/spec/models/ci/daily_build_group_report_result_spec.rb +++ b/spec/models/ci/daily_build_group_report_result_spec.rb @@ -3,6 +3,30 @@ require 'spec_helper' describe Ci::DailyBuildGroupReportResult do + let(:daily_build_group_report_result) { build(:ci_daily_build_group_report_result)} + + describe 'associations' do + it { is_expected.to belong_to(:last_pipeline) } + it { is_expected.to belong_to(:project) } + end + + describe 'validations' do + context 'when attributes are valid' do + it 'returns no errors' do + expect(daily_build_group_report_result).to be_valid + end + end + + context 'when data is invalid' do + it 'returns errors' do + daily_build_group_report_result.data = { invalid: 'data' } + + expect(daily_build_group_report_result).to be_invalid + expect(daily_build_group_report_result.errors.full_messages).to eq(["Data must be a valid json schema"]) + end + end + end + describe '.upsert_reports' do let!(:rspec_coverage) do create( diff --git a/spec/models/ci/instance_variable_spec.rb b/spec/models/ci/instance_variable_spec.rb index ff8676e1424..4d69b7ac2f8 100644 --- a/spec/models/ci/instance_variable_spec.rb +++ b/spec/models/ci/instance_variable_spec.rb @@ -9,6 +9,26 @@ describe Ci::InstanceVariable do it { is_expected.to include_module(Ci::Maskable) } it { is_expected.to validate_uniqueness_of(:key).with_message(/\(\w+\) has already been taken/) } + it { is_expected.to validate_length_of(:encrypted_value).is_at_most(1024).with_message(/Variables over 700 characters risk exceeding the limit/) } + + it_behaves_like 'includes Limitable concern' do + subject { build(:ci_instance_variable) } + end + + context 'with instance level variable feature flag disabled' do + let(:plan_limits) { create(:plan_limits, :default_plan) } + + before do + stub_feature_flags(ci_instance_level_variables_limit: false) + plan_limits.update(described_class.limit_name => 1) + create(:ci_instance_variable) + end + + it 'can create new models exceeding the plan limits', :aggregate_failures do + expect { subject.save }.to change { described_class.count } + expect(subject.errors[:base]).to be_empty + end + end describe '.unprotected' do subject { described_class.unprotected } @@ -39,7 +59,7 @@ describe Ci::InstanceVariable do it { expect(described_class.all_cached).to contain_exactly(protected_variable, unprotected_variable) } it 'memoizes the result' do - expect(described_class).to receive(:store_cache).with(:ci_instance_variable_data).once.and_call_original + expect(described_class).to receive(:unscoped).once.and_call_original 2.times do expect(described_class.all_cached).to contain_exactly(protected_variable, unprotected_variable) @@ -65,15 +85,6 @@ describe Ci::InstanceVariable do expect(described_class.all_cached).to contain_exactly(protected_variable, unprotected_variable, variable) end - - it 'resets the cache when the shared key is missing' do - expect(Rails.cache).to receive(:read).with(:ci_instance_variable_changed_at).twice.and_return(nil) - expect(described_class).to receive(:store_cache).with(:ci_instance_variable_data).thrice.and_call_original - - 3.times do - expect(described_class.all_cached).to contain_exactly(protected_variable, unprotected_variable) - end - end end describe '.unprotected_cached', :use_clean_rails_memory_store_caching do @@ -83,7 +94,7 @@ describe Ci::InstanceVariable do it { expect(described_class.unprotected_cached).to contain_exactly(unprotected_variable) } it 'memoizes the result' do - expect(described_class).to receive(:store_cache).with(:ci_instance_variable_data).once.and_call_original + expect(described_class).to receive(:unscoped).once.and_call_original 2.times do expect(described_class.unprotected_cached).to contain_exactly(unprotected_variable) diff --git a/spec/models/ci/job_artifact_spec.rb b/spec/models/ci/job_artifact_spec.rb index 4cdc74d7a41..17e00533ac3 100644 --- a/spec/models/ci/job_artifact_spec.rb +++ b/spec/models/ci/job_artifact_spec.rb @@ -23,6 +23,14 @@ describe Ci::JobArtifact do subject { build(:ci_job_artifact, :archive, size: 107464) } end + describe '.not_expired' do + it 'returns artifacts that have not expired' do + _expired_artifact = create(:ci_job_artifact, :expired) + + expect(described_class.not_expired).to contain_exactly(artifact) + end + end + describe '.with_reports' do let!(:artifact) { create(:ci_job_artifact, :archive) } @@ -118,6 +126,17 @@ describe Ci::JobArtifact do end end + describe '.downloadable' do + subject { described_class.downloadable } + + it 'filters for downloadable artifacts' do + downloadable_artifact = create(:ci_job_artifact, :codequality) + _not_downloadable_artifact = create(:ci_job_artifact, :trace) + + expect(subject).to contain_exactly(downloadable_artifact) + end + end + describe '.archived_trace_exists_for?' do subject { described_class.archived_trace_exists_for?(job_id) } @@ -357,19 +376,75 @@ describe Ci::JobArtifact do end end + describe 'expired?' do + subject { artifact.expired? } + + context 'when expire_at is nil' do + let(:artifact) { build(:ci_job_artifact, expire_at: nil) } + + it 'returns false' do + is_expected.to be_falsy + end + end + + context 'when expire_at is in the past' do + let(:artifact) { build(:ci_job_artifact, expire_at: Date.yesterday) } + + it 'returns true' do + is_expected.to be_truthy + end + end + + context 'when expire_at is in the future' do + let(:artifact) { build(:ci_job_artifact, expire_at: Date.tomorrow) } + + it 'returns false' do + is_expected.to be_falsey + end + end + end + + describe '#expiring?' do + subject { artifact.expiring? } + + context 'when expire_at is nil' do + let(:artifact) { build(:ci_job_artifact, expire_at: nil) } + + it 'returns false' do + is_expected.to be_falsy + end + end + + context 'when expire_at is in the past' do + let(:artifact) { build(:ci_job_artifact, expire_at: Date.yesterday) } + + it 'returns false' do + is_expected.to be_falsy + end + end + + context 'when expire_at is in the future' do + let(:artifact) { build(:ci_job_artifact, expire_at: Date.tomorrow) } + + it 'returns true' do + is_expected.to be_truthy + end + end + end + describe '#expire_in' do subject { artifact.expire_in } it { is_expected.to be_nil } context 'when expire_at is specified' do - let(:expire_at) { Time.now + 7.days } + let(:expire_at) { Time.current + 7.days } before do artifact.expire_at = expire_at end - it { is_expected.to be_within(5).of(expire_at - Time.now) } + it { is_expected.to be_within(5).of(expire_at - Time.current) } end end diff --git a/spec/models/ci/pipeline_schedule_spec.rb b/spec/models/ci/pipeline_schedule_spec.rb index 9a10c7629b2..4ba70552f01 100644 --- a/spec/models/ci/pipeline_schedule_spec.rb +++ b/spec/models/ci/pipeline_schedule_spec.rb @@ -118,7 +118,7 @@ describe Ci::PipelineSchedule do let(:pipeline_schedule) { create(:ci_pipeline_schedule, :every_minute) } it "updates next_run_at to the sidekiq worker's execution time" do - Timecop.freeze(Time.parse("2019-06-01 12:18:00+0000")) do + Timecop.freeze(Time.zone.parse("2019-06-01 12:18:00+0000")) do expect(pipeline_schedule.next_run_at).to eq(cron_worker_next_run_at) end end diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 4f53b6b4418..782a4206c36 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -26,6 +26,8 @@ describe Ci::Pipeline, :mailer do it { is_expected.to have_many(:trigger_requests) } it { is_expected.to have_many(:variables) } it { is_expected.to have_many(:builds) } + it { is_expected.to have_many(:bridges) } + it { is_expected.to have_many(:job_artifacts).through(:builds) } it { is_expected.to have_many(:auto_canceled_pipelines) } it { is_expected.to have_many(:auto_canceled_jobs) } it { is_expected.to have_many(:sourced_pipelines) } @@ -51,6 +53,27 @@ describe Ci::Pipeline, :mailer do expect(Project.reflect_on_association(:all_pipelines).has_inverse?).to eq(:project) expect(Project.reflect_on_association(:ci_pipelines).has_inverse?).to eq(:project) end + + describe '#latest_builds' do + it 'has a one to many relationship with its latest builds' do + _old_build = create(:ci_build, :retried, pipeline: pipeline) + latest_build = create(:ci_build, :expired, pipeline: pipeline) + + expect(pipeline.latest_builds).to contain_exactly(latest_build) + end + end + + describe '#downloadable_artifacts' do + let(:build) { create(:ci_build, pipeline: pipeline) } + + it 'returns downloadable artifacts that have not expired' do + downloadable_artifact = create(:ci_job_artifact, :codequality, job: build) + _expired_artifact = create(:ci_job_artifact, :junit, :expired, job: build) + _undownloadable_artifact = create(:ci_job_artifact, :trace, job: build) + + expect(pipeline.downloadable_artifacts).to contain_exactly(downloadable_artifact) + end + end end describe '#set_status' do @@ -99,6 +122,17 @@ describe Ci::Pipeline, :mailer do end end + describe '.for_iid' do + subject { described_class.for_iid(iid) } + + let(:iid) { '1234' } + let!(:pipeline) { create(:ci_pipeline, iid: '1234') } + + it 'returns the pipeline' do + is_expected.to contain_exactly(pipeline) + end + end + describe '.for_sha' do subject { described_class.for_sha(sha) } @@ -1007,19 +1041,6 @@ describe Ci::Pipeline, :mailer do subject { pipeline.ordered_stages } - context 'when using legacy stages' do - before do - stub_feature_flags( - ci_pipeline_persisted_stages: false, - ci_atomic_processing: false - ) - end - - it 'returns legacy stages in valid order' do - expect(subject.map(&:name)).to eq %w[build test] - end - end - context 'when using atomic processing' do before do stub_feature_flags( @@ -1051,7 +1072,6 @@ describe Ci::Pipeline, :mailer do context 'when using persisted stages' do before do stub_feature_flags( - ci_pipeline_persisted_stages: true, ci_atomic_processing: false ) end @@ -1079,7 +1099,7 @@ describe Ci::Pipeline, :mailer do end describe 'state machine' do - let(:current) { Time.now.change(usec: 0) } + let(:current) { Time.current.change(usec: 0) } let(:build) { create_build('build1', queued_at: 0) } let(:build_b) { create_build('build2', queued_at: 0) } let(:build_c) { create_build('build3', queued_at: 0) } @@ -2633,38 +2653,34 @@ describe Ci::Pipeline, :mailer do end end - shared_examples 'enqueues the notification worker' do - it 'enqueues PipelineUpdateCiRefStatusWorker' do - expect(PipelineUpdateCiRefStatusWorker).to receive(:perform_async).with(pipeline.id) - expect(PipelineNotificationWorker).not_to receive(:perform_async).with(pipeline.id) + context 'with success pipeline' do + it_behaves_like 'sending a notification' do + before do + perform_enqueued_jobs do + pipeline.succeed + end + end + end + + it 'enqueues PipelineNotificationWorker' do + expect(PipelineNotificationWorker) + .to receive(:perform_async).with(pipeline.id, ref_status: :success) pipeline.succeed end - context 'when ci_pipeline_fixed_notifications is disabled' do + context 'when pipeline is not the latest' do before do - stub_feature_flags(ci_pipeline_fixed_notifications: false) + create(:ci_pipeline, :success, project: project, ci_ref: pipeline.ci_ref) end - it 'enqueues PipelineNotificationWorker' do - expect(PipelineUpdateCiRefStatusWorker).not_to receive(:perform_async).with(pipeline.id) - expect(PipelineNotificationWorker).to receive(:perform_async).with(pipeline.id) + it 'does not pass ref_status' do + expect(PipelineNotificationWorker) + .to receive(:perform_async).with(pipeline.id, ref_status: nil) - pipeline.succeed - end - end - end - - context 'with success pipeline' do - it_behaves_like 'sending a notification' do - before do - perform_enqueued_jobs do - pipeline.succeed - end + pipeline.succeed! end end - - it_behaves_like 'enqueues the notification worker' end context 'with failed pipeline' do @@ -2679,7 +2695,12 @@ describe Ci::Pipeline, :mailer do end end - it_behaves_like 'enqueues the notification worker' + it 'enqueues PipelineNotificationWorker' do + expect(PipelineNotificationWorker) + .to receive(:perform_async).with(pipeline.id, ref_status: :failed) + + pipeline.drop + end end context 'with skipped pipeline' do @@ -2703,6 +2724,69 @@ describe Ci::Pipeline, :mailer do end end + describe 'updates ci_ref when pipeline finished' do + context 'when ci_ref exists' do + let!(:pipeline) { create(:ci_pipeline, :running) } + + it 'updates the ci_ref' do + expect(pipeline.ci_ref) + .to receive(:update_status_by!).with(pipeline).and_call_original + + pipeline.succeed! + end + end + + context 'when ci_ref does not exist' do + let!(:pipeline) { create(:ci_pipeline, :running, ci_ref_presence: false) } + + it 'does not raise an exception' do + expect { pipeline.succeed! }.not_to raise_error + end + end + end + + describe '#ensure_ci_ref!' do + subject { pipeline.ensure_ci_ref! } + + shared_examples_for 'protected by feature flag' do + context 'when feature flag is disabled' do + before do + stub_feature_flags(ci_pipeline_fixed_notifications: false) + end + + it 'does not do anything' do + expect(Ci::Ref).not_to receive(:ensure_for) + + subject + end + end + end + + context 'when ci_ref does not exist yet' do + let!(:pipeline) { create(:ci_pipeline, ci_ref_presence: false) } + + it_behaves_like 'protected by feature flag' + + it 'creates a new ci_ref and assigns it' do + expect { subject }.to change { Ci::Ref.count }.by(1) + + expect(pipeline.ci_ref).to be_present + end + end + + context 'when ci_ref already exists' do + let!(:pipeline) { create(:ci_pipeline) } + + it_behaves_like 'protected by feature flag' + + it 'fetches a new ci_ref and assigns it' do + expect { subject }.not_to change { Ci::Ref.count } + + expect(pipeline.ci_ref).to be_present + end + end + end + describe '#find_job_with_archive_artifacts' do let!(:old_job) { create(:ci_build, name: 'rspec', retried: true, pipeline: pipeline) } let!(:job_without_artifacts) { create(:ci_build, name: 'rspec', pipeline: pipeline) } @@ -2741,6 +2825,30 @@ describe Ci::Pipeline, :mailer do end end + describe '#latest_report_builds' do + it 'returns build with test artifacts' do + test_build = create(:ci_build, :test_reports, pipeline: pipeline, project: project) + coverage_build = create(:ci_build, :coverage_reports, pipeline: pipeline, project: project) + create(:ci_build, :artifacts, pipeline: pipeline, project: project) + + expect(pipeline.latest_report_builds).to contain_exactly(test_build, coverage_build) + end + + it 'filters builds by scope' do + test_build = create(:ci_build, :test_reports, pipeline: pipeline, project: project) + create(:ci_build, :coverage_reports, pipeline: pipeline, project: project) + + expect(pipeline.latest_report_builds(Ci::JobArtifact.test_reports)).to contain_exactly(test_build) + end + + it 'only returns not retried builds' do + test_build = create(:ci_build, :test_reports, pipeline: pipeline, project: project) + create(:ci_build, :test_reports, :retried, pipeline: pipeline, project: project) + + expect(pipeline.latest_report_builds).to contain_exactly(test_build) + end + end + describe '#has_reports?' do subject { pipeline.has_reports?(Ci::JobArtifact.test_reports) } diff --git a/spec/models/ci/ref_spec.rb b/spec/models/ci/ref_spec.rb index aa3b8cdbc3e..3d75cb63141 100644 --- a/spec/models/ci/ref_spec.rb +++ b/spec/models/ci/ref_spec.rb @@ -4,8 +4,155 @@ require 'spec_helper' describe Ci::Ref do it { is_expected.to belong_to(:project) } - it { is_expected.to belong_to(:last_updated_by_pipeline) } - it { is_expected.to validate_inclusion_of(:status).in_array(%w[success failed fixed]) } - it { is_expected.to validate_presence_of(:last_updated_by_pipeline) } + describe '.ensure_for' do + let_it_be(:project) { create(:project, :repository) } + + subject { described_class.ensure_for(pipeline) } + + shared_examples_for 'ensures ci_ref' do + context 'when ci_ref already exists' do + let(:options) { {} } + + it 'returns an existing ci_ref' do + expect { subject }.not_to change { described_class.count } + + expect(subject).to eq(Ci::Ref.find_by(project_id: project.id, ref_path: expected_ref_path)) + end + end + + context 'when ci_ref does not exist yet' do + let(:options) { { ci_ref_presence: false } } + + it 'creates a new ci_ref' do + expect { subject }.to change { described_class.count }.by(1) + + expect(subject).to eq(Ci::Ref.find_by(project_id: project.id, ref_path: expected_ref_path)) + end + end + end + + context 'when pipeline is a branch pipeline' do + let!(:pipeline) { create(:ci_pipeline, ref: 'master', project: project, **options) } + let(:expected_ref_path) { 'refs/heads/master' } + + it_behaves_like 'ensures ci_ref' + end + + context 'when pipeline is a tag pipeline' do + let!(:pipeline) { create(:ci_pipeline, ref: 'v1.1.0', tag: true, project: project, **options) } + let(:expected_ref_path) { 'refs/tags/v1.1.0' } + + it_behaves_like 'ensures ci_ref' + end + + context 'when pipeline is a detached merge request pipeline' do + let(:merge_request) do + create(:merge_request, target_project: project, target_branch: 'master', + source_project: project, source_branch: 'feature') + end + + let!(:pipeline) do + create(:ci_pipeline, :detached_merge_request_pipeline, merge_request: merge_request, project: project, **options) + end + + let(:expected_ref_path) { 'refs/heads/feature' } + + it_behaves_like 'ensures ci_ref' + end + end + + describe '#update_status_by!' do + subject { ci_ref.update_status_by!(pipeline) } + + let!(:ci_ref) { create(:ci_ref) } + + shared_examples_for 'no-op' do + it 'does nothing and returns nil' do + expect { subject }.not_to change { ci_ref.status_name } + + is_expected.to be_nil + end + end + + context 'when pipeline status is success or failed' do + using RSpec::Parameterized::TableSyntax + + where(:pipeline_status, :current_ref_status, :expected_ref_status) do + :success | :unknown | :success + :success | :success | :success + :success | :failed | :fixed + :success | :fixed | :success + :success | :broken | :fixed + :success | :still_failing | :fixed + :failed | :unknown | :failed + :failed | :success | :broken + :failed | :failed | :still_failing + :failed | :fixed | :broken + :failed | :broken | :still_failing + :failed | :still_failing | :still_failing + end + + with_them do + let(:ci_ref) { create(:ci_ref, status: described_class.state_machines[:status].states[current_ref_status].value) } + let(:pipeline) { create(:ci_pipeline, status: pipeline_status, ci_ref: ci_ref) } + + it 'transitions the status via state machine' do + expect(subject).to eq(expected_ref_status) + expect(ci_ref.status_name).to eq(expected_ref_status) + end + end + end + + context 'when pipeline status is success' do + let(:pipeline) { create(:ci_pipeline, :success, ci_ref: ci_ref) } + + it 'updates the status' do + expect { subject }.to change { ci_ref.status_name }.from(:unknown).to(:success) + + is_expected.to eq(:success) + end + end + + context 'when pipeline status is canceled' do + let(:pipeline) { create(:ci_pipeline, status: :canceled, ci_ref: ci_ref) } + + it { is_expected.to eq(:unknown) } + end + + context 'when pipeline status is skipped' do + let(:pipeline) { create(:ci_pipeline, status: :skipped, ci_ref: ci_ref) } + + it_behaves_like 'no-op' + end + + context 'when pipeline status is not complete' do + let(:pipeline) { create(:ci_pipeline, :running, ci_ref: ci_ref) } + + it_behaves_like 'no-op' + end + + context 'when feature flag is disabled' do + let(:pipeline) { create(:ci_pipeline, :success, ci_ref: ci_ref) } + + before do + stub_feature_flags(ci_pipeline_fixed_notifications: false) + end + + it_behaves_like 'no-op' + end + + context 'when pipeline is not the latest pipeline' do + let!(:pipeline) { create(:ci_pipeline, :success, ci_ref: ci_ref) } + let!(:latest_pipeline) { create(:ci_pipeline, :success, ci_ref: ci_ref) } + + it_behaves_like 'no-op' + end + + context 'when pipeline does not belong to the ci_ref' do + let(:pipeline) { create(:ci_pipeline, :success, ci_ref: create(:ci_ref)) } + + it_behaves_like 'no-op' + end + end end diff --git a/spec/models/ci/runner_spec.rb b/spec/models/ci/runner_spec.rb index 8b6a4fa6ade..296240b1602 100644 --- a/spec/models/ci/runner_spec.rb +++ b/spec/models/ci/runner_spec.rb @@ -263,7 +263,7 @@ describe Ci::Runner do subject { described_class.online } before do - @runner1 = create(:ci_runner, :instance, contacted_at: 1.hour.ago) + @runner1 = create(:ci_runner, :instance, contacted_at: 2.hours.ago) @runner2 = create(:ci_runner, :instance, contacted_at: 1.second.ago) end @@ -344,7 +344,7 @@ describe Ci::Runner do subject { described_class.offline } before do - @runner1 = create(:ci_runner, :instance, contacted_at: 1.hour.ago) + @runner1 = create(:ci_runner, :instance, contacted_at: 2.hours.ago) @runner2 = create(:ci_runner, :instance, contacted_at: 1.second.ago) end @@ -598,14 +598,14 @@ describe Ci::Runner do end end - describe '#update_cached_info' do + describe '#heartbeat' do let(:runner) { create(:ci_runner, :project) } - subject { runner.update_cached_info(architecture: '18-bit') } + subject { runner.heartbeat(architecture: '18-bit') } context 'when database was updated recently' do before do - runner.contacted_at = Time.now + runner.contacted_at = Time.current end it 'updates cache' do |