diff options
Diffstat (limited to 'spec/models/ci')
-rw-r--r-- | spec/models/ci/build_spec.rb | 353 | ||||
-rw-r--r-- | spec/models/ci/daily_build_group_report_result_spec.rb (renamed from spec/models/ci/daily_report_result_spec.rb) | 25 | ||||
-rw-r--r-- | spec/models/ci/freeze_period_spec.rb | 50 | ||||
-rw-r--r-- | spec/models/ci/freeze_period_status_spec.rb | 62 | ||||
-rw-r--r-- | spec/models/ci/instance_variable_spec.rb | 93 | ||||
-rw-r--r-- | spec/models/ci/job_artifact_spec.rb | 120 | ||||
-rw-r--r-- | spec/models/ci/persistent_ref_spec.rb | 12 | ||||
-rw-r--r-- | spec/models/ci/pipeline_schedule_spec.rb | 8 | ||||
-rw-r--r-- | spec/models/ci/pipeline_spec.rb | 145 | ||||
-rw-r--r-- | spec/models/ci/processable_spec.rb | 159 | ||||
-rw-r--r-- | spec/models/ci/runner_spec.rb | 8 | ||||
-rw-r--r-- | spec/models/ci/stage_spec.rb | 26 |
12 files changed, 790 insertions, 271 deletions
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index a4f3fa518c6..6605866d9c0 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -106,10 +106,14 @@ describe Ci::Build do end end - describe '.with_artifacts_archive' do - subject { described_class.with_artifacts_archive } + describe '.with_downloadable_artifacts' do + subject { described_class.with_downloadable_artifacts } - context 'when job does not have an archive' do + before do + stub_feature_flags(drop_license_management_artifact: false) + end + + context 'when job does not have a downloadable artifact' do let!(:job) { create(:ci_build) } it 'does not return the job' do @@ -117,15 +121,23 @@ describe Ci::Build do end end - context 'when job has a job artifact archive' do - let!(:job) { create(:ci_build, :artifacts) } + ::Ci::JobArtifact::DOWNLOADABLE_TYPES.each do |type| + context "when job has a #{type} artifact" do + it 'returns the job' do + job = create(:ci_build) + create( + :ci_job_artifact, + file_format: ::Ci::JobArtifact::TYPE_AND_FORMAT_PAIRS[type.to_sym], + file_type: type, + job: job + ) - it 'returns the job' do - is_expected.to include(job) + is_expected.to include(job) + end end end - context 'when job has a job artifact trace' do + context 'when job has a non-downloadable artifact' do let!(:job) { create(:ci_build, :trace_artifact) } it 'does not return the job' do @@ -1419,6 +1431,8 @@ describe Ci::Build do subject { build.erase_erasable_artifacts! } before do + stub_feature_flags(drop_license_management_artifact: false) + Ci::JobArtifact.file_types.keys.each do |file_type| create(:ci_job_artifact, job: build, file_type: file_type, file_format: Ci::JobArtifact::TYPE_AND_FORMAT_PAIRS[file_type.to_sym]) end @@ -2367,12 +2381,14 @@ describe Ci::Build do let(:pipeline_pre_var) { { key: 'pipeline', value: 'value', public: true, masked: false } } let(:build_yaml_var) { { key: 'yaml', value: 'value', public: true, masked: false } } let(:job_jwt_var) { { key: 'CI_JOB_JWT', value: 'ci.job.jwt', public: false, masked: true } } + let(:job_dependency_var) { { key: 'job_dependency', value: 'value', public: true, masked: false } } before do allow(build).to receive(:predefined_variables) { [build_pre_var] } allow(build).to receive(:yaml_variables) { [build_yaml_var] } allow(build).to receive(:persisted_variables) { [] } allow(build).to receive(:job_jwt_variables) { [job_jwt_var] } + allow(build).to receive(:dependency_variables) { [job_dependency_var] } allow_any_instance_of(Project) .to receive(:predefined_variables) { [project_pre_var] } @@ -2390,6 +2406,7 @@ describe Ci::Build do project_pre_var, pipeline_pre_var, build_yaml_var, + job_dependency_var, { key: 'secret', value: 'value', public: false, masked: false }]) end end @@ -2884,6 +2901,19 @@ describe Ci::Build do it { is_expected.to include(deployment_variable) } end + context 'when build has a freeze period' do + let(:freeze_variable) { { key: 'CI_DEPLOY_FREEZE', value: 'true', masked: false, public: true } } + + before do + expect_next_instance_of(Ci::FreezePeriodStatus) do |freeze_period| + expect(freeze_period).to receive(:execute) + .and_return(true) + end + end + + it { is_expected.to include(freeze_variable) } + end + context 'when project has default CI config path' do let(:ci_config_path) { { key: 'CI_CONFIG_PATH', value: '.gitlab-ci.yml', public: true, masked: false } } @@ -2987,6 +3017,15 @@ describe Ci::Build do end end end + + context 'when build has dependency which has dotenv variable' do + let!(:prepare) { create(:ci_build, pipeline: pipeline, stage_idx: 0) } + let!(:build) { create(:ci_build, pipeline: pipeline, stage_idx: 1, options: { dependencies: [prepare.name] }) } + + let!(:job_variable) { create(:ci_job_variable, :dotenv_source, job: prepare) } + + it { is_expected.to include(key: job_variable.key, value: job_variable.value, public: false, masked: false) } + end end describe '#scoped_variables' do @@ -3049,71 +3088,36 @@ describe Ci::Build do end end end - end - describe '#secret_group_variables' do - subject { build.secret_group_variables } - - let!(:variable) { create(:ci_group_variable, protected: true, group: group) } + context 'with dependency variables' do + let!(:prepare) { create(:ci_build, name: 'prepare', pipeline: pipeline, stage_idx: 0) } + let!(:build) { create(:ci_build, pipeline: pipeline, stage_idx: 1, options: { dependencies: ['prepare'] }) } - context 'when ref is branch' do - let(:build) { create(:ci_build, ref: 'master', tag: false, project: project) } + let!(:job_variable) { create(:ci_job_variable, :dotenv_source, job: prepare) } - context 'when ref is protected' do + context 'FF ci_dependency_variables is enabled' do before do - create(:protected_branch, :developers_can_merge, name: 'master', project: project) + stub_feature_flags(ci_dependency_variables: true) end - it { is_expected.to include(variable) } - end - - context 'when ref is not protected' do - it { is_expected.not_to include(variable) } - end - end - - context 'when ref is tag' do - let(:build) { create(:ci_build, ref: 'v1.1.0', tag: true, project: project) } - - context 'when ref is protected' do - before do - create(:protected_tag, project: project, name: 'v*') + it 'inherits dependent variables' do + expect(build.scoped_variables.to_hash).to include(job_variable.key => job_variable.value) end - - it { is_expected.to include(variable) } - end - - context 'when ref is not protected' do - it { is_expected.not_to include(variable) } end - end - context 'when ref is merge request' do - let(:merge_request) { create(:merge_request, :with_detached_merge_request_pipeline) } - let(:pipeline) { merge_request.pipelines_for_merge_request.first } - let(:build) { create(:ci_build, ref: merge_request.source_branch, tag: false, pipeline: pipeline, project: project) } - - context 'when ref is protected' do + context 'FF ci_dependency_variables is disabled' do before do - create(:protected_branch, :developers_can_merge, name: merge_request.source_branch, project: project) + stub_feature_flags(ci_dependency_variables: false) end - it 'does not return protected variables as it is not supported for merge request pipelines' do - is_expected.not_to include(variable) + it 'does not inherit dependent variables' do + expect(build.scoped_variables.to_hash).not_to include(job_variable.key => job_variable.value) end end - - context 'when ref is not protected' do - it { is_expected.not_to include(variable) } - end end end - describe '#secret_project_variables' do - subject { build.secret_project_variables } - - let!(:variable) { create(:ci_variable, protected: true, project: project) } - + shared_examples "secret CI variables" do context 'when ref is branch' do let(:build) { create(:ci_build, ref: 'master', tag: false, project: project) } @@ -3167,6 +3171,30 @@ describe Ci::Build do end end + describe '#secret_instance_variables' do + subject { build.secret_instance_variables } + + let_it_be(:variable) { create(:ci_instance_variable, protected: true) } + + include_examples "secret CI variables" + end + + describe '#secret_group_variables' do + subject { build.secret_group_variables } + + let_it_be(:variable) { create(:ci_group_variable, protected: true, group: group) } + + include_examples "secret CI variables" + end + + describe '#secret_project_variables' do + subject { build.secret_project_variables } + + let_it_be(:variable) { create(:ci_variable, protected: true, project: project) } + + include_examples "secret CI variables" + end + describe '#deployment_variables' do let(:build) { create(:ci_build, environment: environment) } let(:environment) { 'production' } @@ -3217,6 +3245,29 @@ describe Ci::Build do expect(build.scoped_variables_hash).not_to include('MY_VAR': 'myvar') end end + + context 'when overriding CI instance variables' do + before do + create(:ci_instance_variable, key: 'MY_VAR', value: 'my value 1') + group.variables.create!(key: 'MY_VAR', value: 'my value 2') + end + + it 'returns a regular hash created using valid ordering' do + expect(build.scoped_variables_hash).to include('MY_VAR': 'my value 2') + expect(build.scoped_variables_hash).not_to include('MY_VAR': 'my value 1') + end + end + + context 'when CI instance variables are disabled' do + before do + create(:ci_instance_variable, key: 'MY_VAR', value: 'my value 1') + stub_feature_flags(ci_instance_level_variables: false) + end + + it 'does not include instance level variables' do + expect(build.scoped_variables_hash).not_to include('MY_VAR': 'my value 1') + end + end end describe '#any_unmet_prerequisites?' do @@ -3293,6 +3344,41 @@ describe Ci::Build do end end + describe '#dependency_variables' do + subject { build.dependency_variables } + + context 'when using dependencies' do + let!(:prepare1) { create(:ci_build, name: 'prepare1', pipeline: pipeline, stage_idx: 0) } + let!(:prepare2) { create(:ci_build, name: 'prepare2', pipeline: pipeline, stage_idx: 0) } + let!(:build) { create(:ci_build, pipeline: pipeline, stage_idx: 1, options: { dependencies: ['prepare1'] }) } + + let!(:job_variable_1) { create(:ci_job_variable, :dotenv_source, job: prepare1) } + let!(:job_variable_2) { create(:ci_job_variable, job: prepare1) } + let!(:job_variable_3) { create(:ci_job_variable, :dotenv_source, job: prepare2) } + + it 'inherits only dependent variables' do + expect(subject.to_hash).to eq(job_variable_1.key => job_variable_1.value) + end + end + + context 'when using needs' do + let!(:prepare1) { create(:ci_build, name: 'prepare1', pipeline: pipeline, stage_idx: 0) } + let!(:prepare2) { create(:ci_build, name: 'prepare2', pipeline: pipeline, stage_idx: 0) } + let!(:prepare3) { create(:ci_build, name: 'prepare3', pipeline: pipeline, stage_idx: 0) } + let!(:build) { create(:ci_build, pipeline: pipeline, stage_idx: 1, scheduling_type: 'dag') } + let!(:build_needs_prepare1) { create(:ci_build_need, build: build, name: 'prepare1', artifacts: true) } + let!(:build_needs_prepare2) { create(:ci_build_need, build: build, name: 'prepare2', artifacts: false) } + + let!(:job_variable_1) { create(:ci_job_variable, :dotenv_source, job: prepare1) } + let!(:job_variable_2) { create(:ci_job_variable, :dotenv_source, job: prepare2) } + let!(:job_variable_3) { create(:ci_job_variable, :dotenv_source, job: prepare3) } + + it 'inherits only needs with artifacts variables' do + expect(subject.to_hash).to eq(job_variable_1.key => job_variable_1.value) + end + end + end + describe 'state transition: any => [:preparing]' do let(:build) { create(:ci_build, :created) } @@ -3822,8 +3908,68 @@ describe Ci::Build do create(:ci_job_artifact, :junit_with_corrupted_data, job: build, project: build.project) end - it 'raises an error' do - expect { subject }.to raise_error(Gitlab::Ci::Parsers::Test::Junit::JunitParserError) + it 'returns no test data and includes a suite_error message' do + expect { subject }.not_to raise_error + + expect(test_reports.get_suite(build.name).total_count).to eq(0) + expect(test_reports.get_suite(build.name).success_count).to eq(0) + expect(test_reports.get_suite(build.name).failed_count).to eq(0) + expect(test_reports.get_suite(build.name).suite_error).to eq('JUnit XML parsing failed: 1:1: FATAL: Document is empty') + end + end + end + end + + describe '#collect_accessibility_reports!' do + subject { build.collect_accessibility_reports!(accessibility_report) } + + let(:accessibility_report) { Gitlab::Ci::Reports::AccessibilityReports.new } + + it { expect(accessibility_report.urls).to eq({}) } + + context 'when build has an accessibility report' do + context 'when there is an accessibility report with errors' do + before do + create(:ci_job_artifact, :accessibility, job: build, project: build.project) + end + + it 'parses blobs and add the results to the accessibility report' do + expect { subject }.not_to raise_error + + expect(accessibility_report.urls.keys).to match_array(['https://about.gitlab.com/']) + expect(accessibility_report.errors_count).to eq(10) + expect(accessibility_report.scans_count).to eq(1) + expect(accessibility_report.passes_count).to eq(0) + end + end + + context 'when there is an accessibility report without errors' do + before do + create(:ci_job_artifact, :accessibility_without_errors, job: build, project: build.project) + end + + it 'parses blobs and add the results to the accessibility report' do + expect { subject }.not_to raise_error + + expect(accessibility_report.urls.keys).to match_array(['https://pa11y.org/']) + expect(accessibility_report.errors_count).to eq(0) + expect(accessibility_report.scans_count).to eq(1) + expect(accessibility_report.passes_count).to eq(1) + end + end + + context 'when there is an accessibility report with an invalid url' do + before do + create(:ci_job_artifact, :accessibility_with_invalid_url, job: build, project: build.project) + end + + it 'parses blobs and add the results to the accessibility report' do + expect { subject }.not_to raise_error + + expect(accessibility_report.urls).to be_empty + expect(accessibility_report.errors_count).to eq(0) + expect(accessibility_report.scans_count).to eq(0) + expect(accessibility_report.passes_count).to eq(0) end end end @@ -3876,6 +4022,48 @@ describe Ci::Build do end end + describe '#collect_terraform_reports!' do + let(:terraform_reports) { Gitlab::Ci::Reports::TerraformReports.new } + + it 'returns an empty hash' do + expect(build.collect_terraform_reports!(terraform_reports).plans).to eq({}) + end + + context 'when build has a terraform report' do + context 'when there is a valid tfplan.json' do + before do + create(:ci_job_artifact, :terraform, job: build, project: build.project) + end + + it 'parses blobs and add the results to the terraform report' do + expect { build.collect_terraform_reports!(terraform_reports) }.not_to raise_error + + expect(terraform_reports.plans).to match( + a_hash_including( + 'tfplan.json' => a_hash_including( + 'create' => 0, + 'update' => 1, + 'delete' => 0 + ) + ) + ) + end + end + + context 'when there is an invalid tfplan.json' do + before do + create(:ci_job_artifact, :terraform_with_corrupted_data, job: build, project: build.project) + end + + it 'raises an error' do + expect { build.collect_terraform_reports!(terraform_reports) }.to raise_error( + Gitlab::Ci::Parsers::Terraform::Tfplan::TfplanParserError + ) + end + end + end + end + describe '#report_artifacts' do subject { build.report_artifacts } @@ -3986,6 +4174,28 @@ describe Ci::Build do it { is_expected.to include(:upload_multiple_artifacts) } end + + context 'when artifacts exclude is defined and the is feature enabled' do + let(:options) do + { artifacts: { exclude: %w[something] } } + end + + context 'when a feature flag is enabled' do + before do + stub_feature_flags(ci_artifacts_exclude: true) + end + + it { is_expected.to include(:artifacts_exclude) } + end + + context 'when a feature flag is disabled' do + before do + stub_feature_flags(ci_artifacts_exclude: false) + end + + it { is_expected.not_to include(:artifacts_exclude) } + end + end end describe '#supported_runner?' do @@ -4312,4 +4522,31 @@ describe Ci::Build do it { is_expected.to be_nil } end end + + describe '#degradation_threshold' do + subject { build.degradation_threshold } + + context 'when threshold variable is defined' do + before do + build.yaml_variables = [ + { key: 'SOME_VAR_1', value: 'SOME_VAL_1' }, + { key: 'DEGRADATION_THRESHOLD', value: '5' }, + { key: 'SOME_VAR_2', value: 'SOME_VAL_2' } + ] + end + + it { is_expected.to eq(5) } + end + + context 'when threshold variable is not defined' do + before do + build.yaml_variables = [ + { key: 'SOME_VAR_1', value: 'SOME_VAL_1' }, + { key: 'SOME_VAR_2', value: 'SOME_VAL_2' } + ] + end + + it { is_expected.to be_nil } + end + end end diff --git a/spec/models/ci/daily_report_result_spec.rb b/spec/models/ci/daily_build_group_report_result_spec.rb index 61aa58c6692..d4c305c649a 100644 --- a/spec/models/ci/daily_report_result_spec.rb +++ b/spec/models/ci/daily_build_group_report_result_spec.rb @@ -2,14 +2,14 @@ require 'spec_helper' -describe Ci::DailyReportResult do +describe Ci::DailyBuildGroupReportResult do describe '.upsert_reports' do let!(:rspec_coverage) do create( - :ci_daily_report_result, - title: 'rspec', + :ci_daily_build_group_report_result, + group_name: 'rspec', date: '2020-03-09', - value: 71.2 + data: { coverage: 71.2 } ) end let!(:new_pipeline) { create(:ci_pipeline) } @@ -19,20 +19,18 @@ describe Ci::DailyReportResult do { project_id: rspec_coverage.project_id, ref_path: rspec_coverage.ref_path, - param_type: described_class.param_types[rspec_coverage.param_type], last_pipeline_id: new_pipeline.id, date: rspec_coverage.date, - title: 'rspec', - value: 81.0 + group_name: 'rspec', + data: { 'coverage' => 81.0 } }, { project_id: rspec_coverage.project_id, ref_path: rspec_coverage.ref_path, - param_type: described_class.param_types[rspec_coverage.param_type], last_pipeline_id: new_pipeline.id, date: rspec_coverage.date, - title: 'karma', - value: 87.0 + group_name: 'karma', + data: { 'coverage' => 87.0 } } ]) @@ -40,16 +38,15 @@ describe Ci::DailyReportResult do expect(rspec_coverage).to have_attributes( last_pipeline_id: new_pipeline.id, - value: 81.0 + data: { 'coverage' => 81.0 } ) - expect(described_class.find_by_title('karma')).to have_attributes( + expect(described_class.find_by_group_name('karma')).to have_attributes( project_id: rspec_coverage.project_id, ref_path: rspec_coverage.ref_path, - param_type: rspec_coverage.param_type, last_pipeline_id: new_pipeline.id, date: rspec_coverage.date, - value: 87.0 + data: { 'coverage' => 87.0 } ) end diff --git a/spec/models/ci/freeze_period_spec.rb b/spec/models/ci/freeze_period_spec.rb new file mode 100644 index 00000000000..f7f840c6696 --- /dev/null +++ b/spec/models/ci/freeze_period_spec.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Ci::FreezePeriod, type: :model do + subject { build(:ci_freeze_period) } + + let(:invalid_cron) { '0 0 0 * *' } + + it { is_expected.to belong_to(:project) } + + it { is_expected.to respond_to(:freeze_start) } + it { is_expected.to respond_to(:freeze_end) } + it { is_expected.to respond_to(:cron_timezone) } + + describe 'cron validations' do + it 'allows valid cron patterns' do + freeze_period = build(:ci_freeze_period) + + expect(freeze_period).to be_valid + end + + it 'does not allow invalid cron patterns on freeze_start' do + freeze_period = build(:ci_freeze_period, freeze_start: invalid_cron) + + expect(freeze_period).not_to be_valid + end + + it 'does not allow invalid cron patterns on freeze_end' do + freeze_period = build(:ci_freeze_period, freeze_end: invalid_cron) + + expect(freeze_period).not_to be_valid + end + + it 'does not allow an invalid timezone' do + freeze_period = build(:ci_freeze_period, cron_timezone: 'invalid') + + expect(freeze_period).not_to be_valid + end + + context 'when cron contains trailing whitespaces' do + it 'strips the attribute' do + freeze_period = build(:ci_freeze_period, freeze_start: ' 0 0 * * * ') + + expect(freeze_period).to be_valid + expect(freeze_period.freeze_start).to eq('0 0 * * *') + end + end + end +end diff --git a/spec/models/ci/freeze_period_status_spec.rb b/spec/models/ci/freeze_period_status_spec.rb new file mode 100644 index 00000000000..b700ec8c45f --- /dev/null +++ b/spec/models/ci/freeze_period_status_spec.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true +require 'spec_helper' + +describe Ci::FreezePeriodStatus do + let(:project) { create :project } + # '0 23 * * 5' == "At 23:00 on Friday."", '0 7 * * 1' == "At 07:00 on Monday."" + let(:friday_2300) { '0 23 * * 5' } + let(:monday_0700) { '0 7 * * 1' } + + subject { described_class.new(project: project).execute } + + shared_examples 'within freeze period' do |time| + it 'is frozen' do + Timecop.freeze(time) do + expect(subject).to be_truthy + end + end + end + + shared_examples 'outside freeze period' do |time| + it 'is not frozen' do + Timecop.freeze(time) do + expect(subject).to be_falsy + end + end + end + + describe 'single freeze period' do + let!(:freeze_period) { create(:ci_freeze_period, project: project, freeze_start: friday_2300, freeze_end: monday_0700) } + + it_behaves_like 'outside freeze period', Time.utc(2020, 4, 10, 22, 59) + + it_behaves_like 'within freeze period', Time.utc(2020, 4, 10, 23, 1) + + it_behaves_like 'within freeze period', Time.utc(2020, 4, 13, 6, 59) + + it_behaves_like 'outside freeze period', Time.utc(2020, 4, 13, 7, 1) + end + + describe 'multiple freeze periods' do + # '30 23 * * 5' == "At 23:30 on Friday."", '0 8 * * 1' == "At 08:00 on Monday."" + let(:friday_2330) { '30 23 * * 5' } + let(:monday_0800) { '0 8 * * 1' } + + let!(:freeze_period_1) { create(:ci_freeze_period, project: project, freeze_start: friday_2300, freeze_end: monday_0700) } + let!(:freeze_period_2) { create(:ci_freeze_period, project: project, freeze_start: friday_2330, freeze_end: monday_0800) } + + it_behaves_like 'outside freeze period', Time.utc(2020, 4, 10, 22, 59) + + it_behaves_like 'within freeze period', Time.utc(2020, 4, 10, 23, 29) + + it_behaves_like 'within freeze period', Time.utc(2020, 4, 11, 10, 0) + + it_behaves_like 'within freeze period', Time.utc(2020, 4, 10, 23, 1) + + it_behaves_like 'within freeze period', Time.utc(2020, 4, 13, 6, 59) + + it_behaves_like 'within freeze period', Time.utc(2020, 4, 13, 7, 59) + + it_behaves_like 'outside freeze period', Time.utc(2020, 4, 13, 8, 1) + end +end diff --git a/spec/models/ci/instance_variable_spec.rb b/spec/models/ci/instance_variable_spec.rb new file mode 100644 index 00000000000..ff8676e1424 --- /dev/null +++ b/spec/models/ci/instance_variable_spec.rb @@ -0,0 +1,93 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Ci::InstanceVariable do + subject { build(:ci_instance_variable) } + + it_behaves_like "CI variable" + + it { is_expected.to include_module(Ci::Maskable) } + it { is_expected.to validate_uniqueness_of(:key).with_message(/\(\w+\) has already been taken/) } + + describe '.unprotected' do + subject { described_class.unprotected } + + context 'when variable is protected' do + before do + create(:ci_instance_variable, :protected) + end + + it 'returns nothing' do + is_expected.to be_empty + end + end + + context 'when variable is not protected' do + let(:variable) { create(:ci_instance_variable, protected: false) } + + it 'returns the variable' do + is_expected.to contain_exactly(variable) + end + end + end + + describe '.all_cached', :use_clean_rails_memory_store_caching do + let_it_be(:unprotected_variable) { create(:ci_instance_variable, protected: false) } + let_it_be(:protected_variable) { create(:ci_instance_variable, protected: true) } + + 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 + + 2.times do + expect(described_class.all_cached).to contain_exactly(protected_variable, unprotected_variable) + end + end + + it 'removes scopes' do + expect(described_class.unprotected.all_cached).to contain_exactly(protected_variable, unprotected_variable) + end + + it 'resets the cache when records are deleted' do + expect(described_class.all_cached).to contain_exactly(protected_variable, unprotected_variable) + + protected_variable.destroy + + expect(described_class.all_cached).to contain_exactly(unprotected_variable) + end + + it 'resets the cache when records are inserted' do + expect(described_class.all_cached).to contain_exactly(protected_variable, unprotected_variable) + + variable = create(:ci_instance_variable, protected: true) + + 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 + let_it_be(:unprotected_variable) { create(:ci_instance_variable, protected: false) } + let_it_be(:protected_variable) { create(:ci_instance_variable, protected: true) } + + 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 + + 2.times do + expect(described_class.unprotected_cached).to contain_exactly(unprotected_variable) + end + end + end +end diff --git a/spec/models/ci/job_artifact_spec.rb b/spec/models/ci/job_artifact_spec.rb index 6f6ff3704b4..4cdc74d7a41 100644 --- a/spec/models/ci/job_artifact_spec.rb +++ b/spec/models/ci/job_artifact_spec.rb @@ -19,24 +19,8 @@ describe Ci::JobArtifact do it_behaves_like 'having unique enum values' - context 'with update_project_statistics_after_commit enabled' do - before do - stub_feature_flags(update_project_statistics_after_commit: true) - end - - it_behaves_like 'UpdateProjectStatistics' do - subject { build(:ci_job_artifact, :archive, size: 107464) } - end - end - - context 'with update_project_statistics_after_commit disabled' do - before do - stub_feature_flags(update_project_statistics_after_commit: false) - end - - it_behaves_like 'UpdateProjectStatistics' do - subject { build(:ci_job_artifact, :archive, size: 107464) } - end + it_behaves_like 'UpdateProjectStatistics' do + subject { build(:ci_job_artifact, :archive, size: 107464) } end describe '.with_reports' do @@ -70,6 +54,22 @@ describe Ci::JobArtifact do end end + describe '.accessibility_reports' do + subject { described_class.accessibility_reports } + + context 'when there is an accessibility report' do + let(:artifact) { create(:ci_job_artifact, :accessibility) } + + it { is_expected.to eq([artifact]) } + end + + context 'when there are no accessibility report' do + let(:artifact) { create(:ci_job_artifact, :archive) } + + it { is_expected.to be_empty } + end + end + describe '.coverage_reports' do subject { described_class.coverage_reports } @@ -86,6 +86,22 @@ describe Ci::JobArtifact do end end + describe '.terraform_reports' do + context 'when there is a terraform report' do + it 'return the job artifact' do + artifact = create(:ci_job_artifact, :terraform) + + expect(described_class.terraform_reports).to eq([artifact]) + end + end + + context 'when there are no terraform reports' do + it 'return the an empty array' do + expect(described_class.terraform_reports).to eq([]) + end + end + end + describe '.erasable' do subject { described_class.erasable } @@ -128,15 +144,26 @@ describe Ci::JobArtifact do end describe '.for_sha' do + let(:first_pipeline) { create(:ci_pipeline) } + let(:second_pipeline) { create(:ci_pipeline, project: first_pipeline.project, sha: Digest::SHA1.hexdigest(SecureRandom.hex)) } + let!(:first_artifact) { create(:ci_job_artifact, job: create(:ci_build, pipeline: first_pipeline)) } + let!(:second_artifact) { create(:ci_job_artifact, job: create(:ci_build, pipeline: second_pipeline)) } + it 'returns job artifacts for a given pipeline sha' do - project = create(:project) - first_pipeline = create(:ci_pipeline, project: project) - second_pipeline = create(:ci_pipeline, project: project, sha: Digest::SHA1.hexdigest(SecureRandom.hex)) - first_artifact = create(:ci_job_artifact, job: create(:ci_build, pipeline: first_pipeline)) - second_artifact = create(:ci_job_artifact, job: create(:ci_build, pipeline: second_pipeline)) + expect(described_class.for_sha(first_pipeline.sha, first_pipeline.project.id)).to eq([first_artifact]) + expect(described_class.for_sha(second_pipeline.sha, first_pipeline.project.id)).to eq([second_artifact]) + end + end - expect(described_class.for_sha(first_pipeline.sha, project.id)).to eq([first_artifact]) - expect(described_class.for_sha(second_pipeline.sha, project.id)).to eq([second_artifact]) + describe '.for_ref' do + let(:first_pipeline) { create(:ci_pipeline, ref: 'first_ref') } + let(:second_pipeline) { create(:ci_pipeline, ref: 'second_ref', project: first_pipeline.project) } + let!(:first_artifact) { create(:ci_job_artifact, job: create(:ci_build, pipeline: first_pipeline)) } + let!(:second_artifact) { create(:ci_job_artifact, job: create(:ci_build, pipeline: second_pipeline)) } + + it 'returns job artifacts for a given pipeline ref' do + expect(described_class.for_ref(first_pipeline.ref, first_pipeline.project.id)).to eq([first_artifact]) + expect(described_class.for_ref(second_pipeline.ref, first_pipeline.project.id)).to eq([second_artifact]) end end @@ -153,9 +180,9 @@ describe Ci::JobArtifact do end describe 'callbacks' do - subject { create(:ci_job_artifact, :archive) } - describe '#schedule_background_upload' do + subject { create(:ci_job_artifact, :archive) } + context 'when object storage is disabled' do before do stub_artifacts_object_storage(enabled: false) @@ -212,9 +239,35 @@ describe Ci::JobArtifact do end end + describe 'validates if file format is supported' do + subject { artifact } + + let(:artifact) { build(:ci_job_artifact, file_type: :license_management, file_format: :raw) } + + context 'when license_management is supported' do + before do + stub_feature_flags(drop_license_management_artifact: false) + end + + it { is_expected.to be_valid } + end + + context 'when license_management is not supported' do + before do + stub_feature_flags(drop_license_management_artifact: true) + end + + it { is_expected.not_to be_valid } + end + end + describe 'validates file format' do subject { artifact } + before do + stub_feature_flags(drop_license_management_artifact: false) + end + described_class::TYPE_AND_FORMAT_PAIRS.except(:trace).each do |file_type, file_format| context "when #{file_type} type with #{file_format} format" do let(:artifact) { build(:ci_job_artifact, file_type: file_type, file_format: file_format) } @@ -351,19 +404,6 @@ describe Ci::JobArtifact do describe 'file is being stored' do subject { create(:ci_job_artifact, :archive) } - context 'when object has nil store' do - before do - subject.update_column(:file_store, nil) - subject.reload - end - - it 'is stored locally' do - expect(subject.file_store).to be(nil) - expect(subject.file).to be_file_storage - expect(subject.file.object_store).to eq(ObjectStorage::Store::LOCAL) - end - end - context 'when existing object has local store' do it 'is stored locally' do expect(subject.file_store).to be(ObjectStorage::Store::LOCAL) diff --git a/spec/models/ci/persistent_ref_spec.rb b/spec/models/ci/persistent_ref_spec.rb index 4cece0664cf..89dd9b05331 100644 --- a/spec/models/ci/persistent_ref_spec.rb +++ b/spec/models/ci/persistent_ref_spec.rb @@ -45,18 +45,6 @@ describe Ci::PersistentRef do expect(pipeline.persistent_ref).to be_exist end - context 'when depend_on_persistent_pipeline_ref feature flag is disabled' do - before do - stub_feature_flags(depend_on_persistent_pipeline_ref: false) - end - - it 'does not create a persistent ref' do - expect(project.repository).not_to receive(:create_ref) - - subject - end - end - context 'when sha does not exist in the repository' do let(:sha) { 'not-exist' } diff --git a/spec/models/ci/pipeline_schedule_spec.rb b/spec/models/ci/pipeline_schedule_spec.rb index 4ed4b7e38d8..9a10c7629b2 100644 --- a/spec/models/ci/pipeline_schedule_spec.rb +++ b/spec/models/ci/pipeline_schedule_spec.rb @@ -17,14 +17,18 @@ describe Ci::PipelineSchedule do it { is_expected.to respond_to(:description) } it { is_expected.to respond_to(:next_run_at) } + it_behaves_like 'includes Limitable concern' do + subject { build(:ci_pipeline_schedule) } + end + describe 'validations' do - it 'does not allow invalid cron patters' do + it 'does not allow invalid cron patterns' do pipeline_schedule = build(:ci_pipeline_schedule, cron: '0 0 0 * *') expect(pipeline_schedule).not_to be_valid end - it 'does not allow invalid cron patters' do + it 'does not allow invalid cron patterns' do pipeline_schedule = build(:ci_pipeline_schedule, cron_timezone: 'invalid') expect(pipeline_schedule).not_to be_valid diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 90412136c1d..4f53b6b4418 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -53,6 +53,29 @@ describe Ci::Pipeline, :mailer do end end + describe '#set_status' do + where(:from_status, :to_status) do + from_status_names = described_class.state_machines[:status].states.map(&:name) + to_status_names = from_status_names - [:created] # we never want to transition into created + + from_status_names.product(to_status_names) + end + + with_them do + it do + pipeline.status = from_status.to_s + + if from_status != to_status + expect(pipeline.set_status(to_status.to_s)) + .to eq(true) + else + expect(pipeline.set_status(to_status.to_s)) + .to eq(false), "loopback transitions are not allowed" + end + end + end + end + describe '.processables' do before do create(:ci_build, name: 'build', pipeline: pipeline) @@ -364,6 +387,26 @@ describe Ci::Pipeline, :mailer do end end + context 'when pipeline has an accessibility report' do + subject { described_class.with_reports(Ci::JobArtifact.accessibility_reports) } + + let(:pipeline_with_report) { create(:ci_pipeline, :with_accessibility_reports) } + + it 'selects the pipeline' do + is_expected.to eq([pipeline_with_report]) + end + end + + context 'when pipeline has a terraform report' do + it 'selects the pipeline' do + pipeline_with_report = create(:ci_pipeline, :with_terraform_reports) + + expect(described_class.with_reports(Ci::JobArtifact.terraform_reports)).to eq( + [pipeline_with_report] + ) + end + end + context 'when pipeline does not have metrics reports' do subject { described_class.with_reports(Ci::JobArtifact.test_reports) } @@ -699,6 +742,28 @@ describe Ci::Pipeline, :mailer do ) end end + + describe 'variable CI_KUBERNETES_ACTIVE' do + context 'when pipeline.has_kubernetes_active? is true' do + before do + allow(pipeline).to receive(:has_kubernetes_active?).and_return(true) + end + + it "is included with value 'true'" do + expect(subject.to_hash).to include('CI_KUBERNETES_ACTIVE' => 'true') + end + end + + context 'when pipeline.has_kubernetes_active? is false' do + before do + allow(pipeline).to receive(:has_kubernetes_active?).and_return(false) + end + + it 'is not included' do + expect(subject.to_hash).not_to have_key('CI_KUBERNETES_ACTIVE') + end + end + end end describe '#protected_ref?' do @@ -944,7 +1009,10 @@ describe Ci::Pipeline, :mailer do context 'when using legacy stages' do before do - stub_feature_flags(ci_pipeline_persisted_stages: false) + stub_feature_flags( + ci_pipeline_persisted_stages: false, + ci_atomic_processing: false + ) end it 'returns legacy stages in valid order' do @@ -952,9 +1020,40 @@ describe Ci::Pipeline, :mailer do end end + context 'when using atomic processing' do + before do + stub_feature_flags( + ci_atomic_processing: true + ) + end + + context 'when pipelines is not complete' do + it 'returns stages in valid order' do + expect(subject).to all(be_a Ci::Stage) + expect(subject.map(&:name)) + .to eq %w[sanity build test deploy cleanup] + end + end + + context 'when pipeline is complete' do + before do + pipeline.succeed! + end + + it 'returns stages in valid order' do + expect(subject).to all(be_a Ci::Stage) + expect(subject.map(&:name)) + .to eq %w[sanity build test deploy cleanup] + end + end + end + context 'when using persisted stages' do before do - stub_feature_flags(ci_pipeline_persisted_stages: true) + stub_feature_flags( + ci_pipeline_persisted_stages: true, + ci_atomic_processing: false + ) end context 'when pipelines is not complete' do @@ -1119,8 +1218,8 @@ describe Ci::Pipeline, :mailer do context "from #{status}" do let(:from_status) { status } - it 'schedules pipeline success worker' do - expect(Ci::DailyReportResultsWorker).to receive(:perform_in).with(10.minutes, pipeline.id) + it 'schedules daily build group report results worker' do + expect(Ci::DailyBuildGroupReportResultsWorker).to receive(:perform_in).with(10.minutes, pipeline.id) pipeline.succeed end @@ -2307,7 +2406,7 @@ describe Ci::Pipeline, :mailer do def have_requested_pipeline_hook(status) have_requested(:post, stubbed_hostname(hook.url)).with do |req| - json_body = JSON.parse(req.body) + json_body = Gitlab::Json.parse(req.body) json_body['object_attributes']['status'] == status && json_body['builds'].length == 2 end @@ -2755,6 +2854,42 @@ describe Ci::Pipeline, :mailer do end end + describe '#accessibility_reports' do + subject { pipeline.accessibility_reports } + + context 'when pipeline has multiple builds with accessibility reports' do + let(:build_rspec) { create(:ci_build, :success, name: 'rspec', pipeline: pipeline, project: project) } + let(:build_golang) { create(:ci_build, :success, name: 'golang', pipeline: pipeline, project: project) } + + before do + create(:ci_job_artifact, :accessibility, job: build_rspec, project: project) + create(:ci_job_artifact, :accessibility_without_errors, job: build_golang, project: project) + end + + it 'returns accessibility report with collected data' do + expect(subject.urls.keys).to match_array([ + "https://pa11y.org/", + "https://about.gitlab.com/" + ]) + end + + context 'when builds are retried' do + let(:build_rspec) { create(:ci_build, :retried, :success, name: 'rspec', pipeline: pipeline, project: project) } + let(:build_golang) { create(:ci_build, :retried, :success, name: 'golang', pipeline: pipeline, project: project) } + + it 'returns empty urls for accessibility reports' do + expect(subject.urls).to be_empty + end + end + end + + context 'when pipeline does not have any builds with accessibility reports' do + it 'returns empty urls for accessibility reports' do + expect(subject.urls).to be_empty + end + end + end + describe '#coverage_reports' do subject { pipeline.coverage_reports } diff --git a/spec/models/ci/processable_spec.rb b/spec/models/ci/processable_spec.rb index 4490371bde5..e67f740279b 100644 --- a/spec/models/ci/processable_spec.rb +++ b/spec/models/ci/processable_spec.rb @@ -6,16 +6,12 @@ describe Ci::Processable do let_it_be(:project) { create(:project) } let_it_be(:pipeline) { create(:ci_pipeline, project: project) } - let_it_be(:detached_merge_request_pipeline) do - create(:ci_pipeline, :detached_merge_request_pipeline, :with_job, project: project) - end - - let_it_be(:legacy_detached_merge_request_pipeline) do - create(:ci_pipeline, :legacy_detached_merge_request_pipeline, :with_job, project: project) - end + describe 'delegations' do + subject { Ci::Processable.new } - let_it_be(:merged_result_pipeline) do - create(:ci_pipeline, :merged_result_pipeline, :with_job, project: project) + it { is_expected.to delegate_method(:merge_request?).to(:pipeline) } + it { is_expected.to delegate_method(:merge_request_ref?).to(:pipeline) } + it { is_expected.to delegate_method(:legacy_detached_merge_request_pipeline?).to(:pipeline) } end describe '#aggregated_needs_names' do @@ -52,69 +48,28 @@ describe Ci::Processable do end describe 'validate presence of scheduling_type' do - context 'on create' do - let(:processable) do - build( - :ci_build, :created, project: project, pipeline: pipeline, - importing: importing, scheduling_type: nil - ) - end - - context 'when importing' do - let(:importing) { true } - - context 'when validate_scheduling_type_of_processables is true' do - before do - stub_feature_flags(validate_scheduling_type_of_processables: true) - end + using RSpec::Parameterized::TableSyntax - it 'does not validate' do - expect(processable).to be_valid - end - end - - context 'when validate_scheduling_type_of_processables is false' do - before do - stub_feature_flags(validate_scheduling_type_of_processables: false) - end - - it 'does not validate' do - expect(processable).to be_valid - end - end - end + subject { build(:ci_build, project: project, pipeline: pipeline, importing: importing) } - context 'when not importing' do - let(:importing) { false } - - context 'when validate_scheduling_type_of_processables is true' do - before do - stub_feature_flags(validate_scheduling_type_of_processables: true) - end - - it 'validates' do - expect(processable).not_to be_valid - end - end - - context 'when validate_scheduling_type_of_processables is false' do - before do - stub_feature_flags(validate_scheduling_type_of_processables: false) - end + where(:importing, :should_validate) do + false | true + true | false + end - it 'does not validate' do - expect(processable).to be_valid + with_them do + context 'on create' do + it 'validates presence' do + if should_validate + is_expected.to validate_presence_of(:scheduling_type).on(:create) + else + is_expected.not_to validate_presence_of(:scheduling_type).on(:create) end end end - end - - context 'on update' do - let(:processable) { create(:ci_build, :created, project: project, pipeline: pipeline) } - it 'does not validate' do - processable.scheduling_type = nil - expect(processable).to be_valid + context 'on update' do + it { is_expected.not_to validate_presence_of(:scheduling_type).on(:update) } end end end @@ -147,6 +102,8 @@ describe Ci::Processable do describe '#needs_attributes' do let(:build) { create(:ci_build, :created, project: project, pipeline: pipeline) } + subject { build.needs_attributes } + context 'with needs' do before do create(:ci_build_need, build: build, name: 'test1') @@ -154,7 +111,7 @@ describe Ci::Processable do end it 'returns all needs attributes' do - expect(build.needs_attributes).to contain_exactly( + is_expected.to contain_exactly( { 'artifacts' => true, 'name' => 'test1' }, { 'artifacts' => true, 'name' => 'test2' } ) @@ -162,75 +119,7 @@ describe Ci::Processable do end context 'without needs' do - it 'returns all needs attributes' do - expect(build.needs_attributes).to be_empty - end - end - end - - describe '#merge_request?' do - subject { pipeline.processables.first.merge_request? } - - context 'in a detached merge request pipeline' do - let(:pipeline) { detached_merge_request_pipeline } - - it { is_expected.to eq(pipeline.merge_request?) } - end - - context 'in a legacy detached merge_request_pipeline' do - let(:pipeline) { legacy_detached_merge_request_pipeline } - - it { is_expected.to eq(pipeline.merge_request?) } - end - - context 'in a pipeline for merged results' do - let(:pipeline) { merged_result_pipeline } - - it { is_expected.to eq(pipeline.merge_request?) } - end - end - - describe '#merge_request_ref?' do - subject { pipeline.processables.first.merge_request_ref? } - - context 'in a detached merge request pipeline' do - let(:pipeline) { detached_merge_request_pipeline } - - it { is_expected.to eq(pipeline.merge_request_ref?) } - end - - context 'in a legacy detached merge_request_pipeline' do - let(:pipeline) { legacy_detached_merge_request_pipeline } - - it { is_expected.to eq(pipeline.merge_request_ref?) } - end - - context 'in a pipeline for merged results' do - let(:pipeline) { merged_result_pipeline } - - it { is_expected.to eq(pipeline.merge_request_ref?) } - end - end - - describe '#legacy_detached_merge_request_pipeline?' do - subject { pipeline.processables.first.legacy_detached_merge_request_pipeline? } - - context 'in a detached merge request pipeline' do - let(:pipeline) { detached_merge_request_pipeline } - - it { is_expected.to eq(pipeline.legacy_detached_merge_request_pipeline?) } - end - - context 'in a legacy detached merge_request_pipeline' do - let(:pipeline) { legacy_detached_merge_request_pipeline } - - it { is_expected.to eq(pipeline.legacy_detached_merge_request_pipeline?) } - end - - context 'in a pipeline for merged results' do - let(:pipeline) { merged_result_pipeline } - - it { is_expected.to eq(pipeline.legacy_detached_merge_request_pipeline?) } + it { is_expected.to be_empty } end end end diff --git a/spec/models/ci/runner_spec.rb b/spec/models/ci/runner_spec.rb index 2dedff7f15b..8b6a4fa6ade 100644 --- a/spec/models/ci/runner_spec.rb +++ b/spec/models/ci/runner_spec.rb @@ -270,7 +270,7 @@ describe Ci::Runner do it { is_expected.to eq([@runner2])} end - describe '#online?' do + describe '#online?', :clean_gitlab_redis_cache do let(:runner) { create(:ci_runner, :instance) } subject { runner.online? } @@ -332,7 +332,7 @@ describe Ci::Runner do end def stub_redis_runner_contacted_at(value) - Gitlab::Redis::SharedState.with do |redis| + Gitlab::Redis::Cache.with do |redis| cache_key = runner.send(:cache_attribute_key) expect(redis).to receive(:get).with(cache_key) .and_return({ contacted_at: value }.to_json).at_least(:once) @@ -640,7 +640,7 @@ describe Ci::Runner do end def expect_redis_update - Gitlab::Redis::SharedState.with do |redis| + Gitlab::Redis::Cache.with do |redis| redis_key = runner.send(:cache_attribute_key) expect(redis).to receive(:set).with(redis_key, anything, any_args) end @@ -664,7 +664,7 @@ describe Ci::Runner do end it 'cleans up the queue' do - Gitlab::Redis::SharedState.with do |redis| + Gitlab::Redis::Cache.with do |redis| expect(redis.get(queue_key)).to be_nil end end diff --git a/spec/models/ci/stage_spec.rb b/spec/models/ci/stage_spec.rb index 3aeaa27abce..a1549532559 100644 --- a/spec/models/ci/stage_spec.rb +++ b/spec/models/ci/stage_spec.rb @@ -3,7 +3,8 @@ require 'spec_helper' describe Ci::Stage, :models do - let(:stage) { create(:ci_stage_entity) } + let_it_be(:pipeline) { create(:ci_empty_pipeline) } + let(:stage) { create(:ci_stage_entity, pipeline: pipeline, project: pipeline.project) } it_behaves_like 'having unique enum values' @@ -55,6 +56,29 @@ describe Ci::Stage, :models do end end + describe '#set_status' do + where(:from_status, :to_status) do + from_status_names = described_class.state_machines[:status].states.map(&:name) + to_status_names = from_status_names - [:created] # we never want to transition into created + + from_status_names.product(to_status_names) + end + + with_them do + it do + stage.status = from_status.to_s + + if from_status != to_status + expect(stage.set_status(to_status.to_s)) + .to eq(true) + else + expect(stage.set_status(to_status.to_s)) + .to eq(false), "loopback transitions are not allowed" + end + end + end + end + describe '#update_status' do context 'when stage objects needs to be updated' do before do |