summaryrefslogtreecommitdiff
path: root/spec/models/ci
diff options
context:
space:
mode:
Diffstat (limited to 'spec/models/ci')
-rw-r--r--spec/models/ci/bridge_spec.rb5
-rw-r--r--spec/models/ci/build_report_result_spec.rb74
-rw-r--r--spec/models/ci/build_runner_session_spec.rb60
-rw-r--r--spec/models/ci/build_spec.rb99
-rw-r--r--spec/models/ci/build_trace_chunk_spec.rb2
-rw-r--r--spec/models/ci/daily_build_group_report_result_spec.rb24
-rw-r--r--spec/models/ci/instance_variable_spec.rb33
-rw-r--r--spec/models/ci/job_artifact_spec.rb79
-rw-r--r--spec/models/ci/pipeline_schedule_spec.rb2
-rw-r--r--spec/models/ci/pipeline_spec.rb184
-rw-r--r--spec/models/ci/ref_spec.rb153
-rw-r--r--spec/models/ci/runner_spec.rb10
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