diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-08-18 13:50:51 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-08-18 13:50:51 +0300 |
commit | db384e6b19af03b4c3c82a5760d83a3fd79f7982 (patch) | |
tree | 34beaef37df5f47ccbcf5729d7583aae093cffa0 /spec/lib/gitlab/ci | |
parent | 54fd7b1bad233e3944434da91d257fa7f63c3996 (diff) |
Add latest changes from gitlab-org/gitlab@16-3-stable-eev16.3.0-rc42
Diffstat (limited to 'spec/lib/gitlab/ci')
55 files changed, 1714 insertions, 668 deletions
diff --git a/spec/lib/gitlab/ci/artifacts/decompressed_artifact_size_validator_spec.rb b/spec/lib/gitlab/ci/artifacts/decompressed_artifact_size_validator_spec.rb index ef39a431d63..47d91e2478e 100644 --- a/spec/lib/gitlab/ci/artifacts/decompressed_artifact_size_validator_spec.rb +++ b/spec/lib/gitlab/ci/artifacts/decompressed_artifact_size_validator_spec.rb @@ -12,7 +12,7 @@ RSpec.describe Gitlab::Ci::Artifacts::DecompressedArtifactSizeValidator, feature let(:gzip_valid?) { true } let(:validator) { instance_double(::Gitlab::Ci::DecompressedGzipSizeValidator, valid?: gzip_valid?) } - before(:all) do + before_all do Zlib::GzipWriter.open(file_path) do |gz| gz.write('Hello World!') end diff --git a/spec/lib/gitlab/ci/components/instance_path_spec.rb b/spec/lib/gitlab/ci/components/instance_path_spec.rb index 511036efd37..f4bc706f9b4 100644 --- a/spec/lib/gitlab/ci/components/instance_path_spec.rb +++ b/spec/lib/gitlab/ci/components/instance_path_spec.rb @@ -106,7 +106,7 @@ RSpec.describe Gitlab::Ci::Components::InstancePath, feature_category: :pipeline create(:release, project: existing_project, sha: 'sha-1', released_at: Time.zone.now) end - before(:all) do + before_all do # Previous release create(:release, project: existing_project, sha: 'sha-2', released_at: Time.zone.now - 1.day) end diff --git a/spec/lib/gitlab/ci/config/entry/include/rules/rule_spec.rb b/spec/lib/gitlab/ci/config/entry/include/rules/rule_spec.rb index 10c1d92e209..dd15b049b9b 100644 --- a/spec/lib/gitlab/ci/config/entry/include/rules/rule_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/include/rules/rule_spec.rb @@ -1,117 +1,132 @@ # frozen_string_literal: true -require 'fast_spec_helper' +require 'spec_helper' # Change this to fast spec helper when FF `ci_refactor_external_rules` is removed require_dependency 'active_model' RSpec.describe Gitlab::Ci::Config::Entry::Include::Rules::Rule, feature_category: :pipeline_composition do let(:factory) do - Gitlab::Config::Entry::Factory.new(described_class) - .value(config) + Gitlab::Config::Entry::Factory.new(described_class).value(config) end subject(:entry) { factory.create! } - describe '.new' do - shared_examples 'an invalid config' do |error_message| - it { is_expected.not_to be_valid } + before do + entry.compose! + end + + shared_examples 'a valid config' do + it { is_expected.to be_valid } + + it 'returns the expected value' do + expect(entry.value).to eq(config.compact) + end - it 'has errors' do - expect(entry.errors).to include(error_message) + context 'when FF `ci_refactor_external_rules` is disabled' do + before do + stub_feature_flags(ci_refactor_external_rules: false) end + + it 'returns the expected value' do + expect(entry.value).to eq(config) + end + end + end + + shared_examples 'an invalid config' do |error_message| + it { is_expected.not_to be_valid } + + it 'has errors' do + expect(entry.errors).to include(error_message) end + end - context 'when specifying an if: clause' do - let(:config) { { if: '$THIS || $THAT' } } + context 'when specifying an if: clause' do + let(:config) { { if: '$THIS || $THAT' } } - it { is_expected.to be_valid } + it_behaves_like 'a valid config' - context 'with when:' do - let(:config) { { if: '$THIS || $THAT', when: 'never' } } + context 'with when:' do + let(:config) { { if: '$THIS || $THAT', when: 'never' } } - it { is_expected.to be_valid } - end + it_behaves_like 'a valid config' end - context 'when specifying an exists: clause' do - let(:config) { { exists: './this.md' } } + context 'with when: <invalid string>' do + let(:config) { { if: '$THIS || $THAT', when: 'on_success' } } - it { is_expected.to be_valid } + it_behaves_like 'an invalid config', /when unknown value: on_success/ end - context 'using a list of multiple expressions' do - let(:config) { { if: ['$MY_VAR == "this"', '$YOUR_VAR == "that"'] } } + context 'with when: null' do + let(:config) { { if: '$THIS || $THAT', when: nil } } - it_behaves_like 'an invalid config', /invalid expression syntax/ + it_behaves_like 'a valid config' end - context 'when specifying an invalid if: clause expression' do - let(:config) { { if: ['$MY_VAR =='] } } + context 'when if: clause is invalid' do + let(:config) { { if: '$MY_VAR ==' } } it_behaves_like 'an invalid config', /invalid expression syntax/ end - context 'when specifying an if: clause expression with an invalid token' do - let(:config) { { if: ['$MY_VAR == 123'] } } + context 'when if: clause has an integer operand' do + let(:config) { { if: '$MY_VAR == 123' } } it_behaves_like 'an invalid config', /invalid expression syntax/ end - context 'when using invalid regex in an if: clause' do - let(:config) { { if: ['$MY_VAR =~ /some ( thing/'] } } + context 'when if: clause has invalid regex' do + let(:config) { { if: '$MY_VAR =~ /some ( thing/' } } it_behaves_like 'an invalid config', /invalid expression syntax/ end - context 'when using an if: clause with lookahead regex character "?"' do + context 'when if: clause has lookahead regex character "?"' do let(:config) { { if: '$CI_COMMIT_REF =~ /^(?!master).+/' } } it_behaves_like 'an invalid config', /invalid expression syntax/ end - context 'when specifying unknown policy' do - let(:config) { { invalid: :something } } + context 'when if: clause has array of expressions' do + let(:config) { { if: ['$MY_VAR == "this"', '$YOUR_VAR == "that"'] } } - it_behaves_like 'an invalid config', /unknown keys: invalid/ + it_behaves_like 'an invalid config', /invalid expression syntax/ end + end + + context 'when specifying an exists: clause' do + let(:config) { { exists: './this.md' } } - context 'when clause is empty' do - let(:config) { {} } + it_behaves_like 'a valid config' - it_behaves_like 'an invalid config', /can't be blank/ + context 'when array' do + let(:config) { { exists: ['./this.md', './that.md'] } } + + it_behaves_like 'a valid config' end - context 'when policy strategy does not match' do - let(:config) { 'string strategy' } + context 'when null' do + let(:config) { { exists: nil } } - it_behaves_like 'an invalid config', /should be a hash/ + it_behaves_like 'a valid config' end end - describe '#value' do - subject(:value) { entry.value } - - context 'when specifying an if: clause' do - let(:config) { { if: '$THIS || $THAT' } } + context 'when specifying an unknown keyword' do + let(:config) { { invalid: :something } } - it 'returns the config' do - expect(subject).to eq(if: '$THIS || $THAT') - end + it_behaves_like 'an invalid config', /unknown keys: invalid/ + end - context 'with when:' do - let(:config) { { if: '$THIS || $THAT', when: 'never' } } + context 'when config is blank' do + let(:config) { {} } - it 'returns the config' do - expect(subject).to eq(if: '$THIS || $THAT', when: 'never') - end - end - end + it_behaves_like 'an invalid config', /can't be blank/ + end - context 'when specifying an exists: clause' do - let(:config) { { exists: './test.md' } } + context 'when config type is invalid' do + let(:config) { 'invalid' } - it 'returns the config' do - expect(subject).to eq(exists: './test.md') - end - end + it_behaves_like 'an invalid config', /should be a hash/ end end diff --git a/spec/lib/gitlab/ci/config/entry/include/rules_spec.rb b/spec/lib/gitlab/ci/config/entry/include/rules_spec.rb index d5988dbbb58..05db81abfc1 100644 --- a/spec/lib/gitlab/ci/config/entry/include/rules_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/include/rules_spec.rb @@ -1,9 +1,9 @@ # frozen_string_literal: true -require 'fast_spec_helper' +require 'spec_helper' # Change this to fast spec helper when FF `ci_refactor_external_rules` is removed require_dependency 'active_model' -RSpec.describe Gitlab::Ci::Config::Entry::Include::Rules do +RSpec.describe Gitlab::Ci::Config::Entry::Include::Rules, feature_category: :pipeline_composition do let(:factory) do Gitlab::Config::Entry::Factory.new(described_class) .value(config) @@ -77,23 +77,68 @@ RSpec.describe Gitlab::Ci::Config::Entry::Include::Rules do describe '#value' do subject(:value) { entry.value } - context 'with an "if"' do - let(:config) do - [{ if: '$THIS == "that"' }] + let(:config) do + [ + { if: '$THIS == "that"' }, + { if: '$SKIP', when: 'never' } + ] + end + + it { is_expected.to eq([]) } + + context 'when composed' do + before do + entry.compose! end - it { is_expected.to eq(config) } + it 'returns the composed entries value' do + expect(entry).to be_valid + is_expected.to eq( + [ + { if: '$THIS == "that"' }, + { if: '$SKIP', when: 'never' } + ] + ) + end + + context 'when invalid' do + let(:config) do + [ + { if: '$THIS == "that"' }, + { if: '$SKIP', invalid: 'invalid' } + ] + end + + it 'returns the invalid config' do + expect(entry).not_to be_valid + is_expected.to eq(config) + end + end end - context 'with a list of two rules' do - let(:config) do - [ - { if: '$THIS == "that"' }, - { if: '$SKIP' } - ] + context 'when FF `ci_refactor_external_rules` is disabled' do + before do + stub_feature_flags(ci_refactor_external_rules: false) + end + + context 'with an "if"' do + let(:config) do + [{ if: '$THIS == "that"' }] + end + + it { is_expected.to eq(config) } end - it { is_expected.to eq(config) } + context 'with a list of two rules' do + let(:config) do + [ + { if: '$THIS == "that"' }, + { if: '$SKIP' } + ] + end + + it { is_expected.to eq(config) } + end end end end diff --git a/spec/lib/gitlab/ci/config/entry/need_spec.rb b/spec/lib/gitlab/ci/config/entry/need_spec.rb index ab2e8d4db78..eba9411560e 100644 --- a/spec/lib/gitlab/ci/config/entry/need_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/need_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe ::Gitlab::Ci::Config::Entry::Need do +RSpec.describe ::Gitlab::Ci::Config::Entry::Need, feature_category: :pipeline_composition do subject(:need) { described_class.new(config) } shared_examples 'job type' do @@ -219,6 +219,81 @@ RSpec.describe ::Gitlab::Ci::Config::Entry::Need do it_behaves_like 'job type' end + + context 'when parallel:matrix has a value' do + before do + need.compose! + end + + context 'and it is a string value' do + let(:config) do + { job: 'job_name', parallel: { matrix: [{ platform: 'p1', stack: 's1' }] } } + end + + describe '#valid?' do + it { is_expected.to be_valid } + end + + describe '#value' do + it 'returns job needs configuration' do + expect(need.value).to eq( + name: 'job_name', + artifacts: true, + optional: false, + parallel: { matrix: [{ "platform" => ['p1'], "stack" => ['s1'] }] } + ) + end + end + + it_behaves_like 'job type' + end + + context 'and it is an array value' do + let(:config) do + { job: 'job_name', parallel: { matrix: [{ platform: %w[p1 p2], stack: %w[s1 s2] }] } } + end + + describe '#valid?' do + it { is_expected.to be_valid } + end + + describe '#value' do + it 'returns job needs configuration' do + expect(need.value).to eq( + name: 'job_name', + artifacts: true, + optional: false, + parallel: { matrix: [{ 'platform' => %w[p1 p2], 'stack' => %w[s1 s2] }] } + ) + end + end + + it_behaves_like 'job type' + end + + context 'and it is a both an array and string value' do + let(:config) do + { job: 'job_name', parallel: { matrix: [{ platform: %w[p1 p2], stack: 's1' }] } } + end + + describe '#valid?' do + it { is_expected.to be_valid } + end + + describe '#value' do + it 'returns job needs configuration' do + expect(need.value).to eq( + name: 'job_name', + artifacts: true, + optional: false, + parallel: { matrix: [{ 'platform' => %w[p1 p2], 'stack' => ['s1'] }] } + ) + end + end + + it_behaves_like 'job type' + end + end end context 'with cross pipeline artifacts needs' do diff --git a/spec/lib/gitlab/ci/config/entry/needs_spec.rb b/spec/lib/gitlab/ci/config/entry/needs_spec.rb index 489fbac68b2..d1a8a74ac06 100644 --- a/spec/lib/gitlab/ci/config/entry/needs_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/needs_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe ::Gitlab::Ci::Config::Entry::Needs do +RSpec.describe ::Gitlab::Ci::Config::Entry::Needs, feature_category: :pipeline_composition do subject(:needs) { described_class.new(config) } before do @@ -67,6 +67,141 @@ RSpec.describe ::Gitlab::Ci::Config::Entry::Needs do end end + context 'when needs value is a hash' do + context 'with a job value' do + let(:config) do + { job: 'job_name' } + end + + describe '#valid?' do + it { is_expected.to be_valid } + end + end + + context 'with a parallel value that is a numeric value' do + let(:config) do + { job: 'job_name', parallel: 2 } + end + + describe '#valid?' do + it { is_expected.not_to be_valid } + end + + describe '#errors' do + it 'returns errors about number values being invalid for needs:parallel' do + expect(needs.errors).to match_array(["needs config cannot use \"parallel: <number>\"."]) + end + end + end + end + + context 'when needs:parallel value is incorrect' do + context 'with a keyword that is not "matrix"' do + let(:config) do + [ + { job: 'job_name', parallel: { not_matrix: [{ one: 'aaa', two: 'bbb' }] } } + ] + end + + describe '#valid?' do + it { is_expected.not_to be_valid } + end + + describe '#errors' do + it 'returns errors about incorrect matrix keyword' do + expect(needs.errors).to match_array([ + 'need:parallel config contains unknown keys: not_matrix', + 'need:parallel config missing required keys: matrix' + ]) + end + end + end + + context 'with a number value' do + let(:config) { [{ job: 'job_name', parallel: 2 }] } + + describe '#valid?' do + it { is_expected.not_to be_valid } + end + + describe '#errors' do + it 'returns errors about number values being invalid for needs:parallel' do + expect(needs.errors).to match_array(["needs config cannot use \"parallel: <number>\"."]) + end + end + end + end + + context 'when needs:parallel:matrix value is empty' do + let(:config) { [{ job: 'job_name', parallel: { matrix: {} } }] } + + describe '#valid?' do + it { is_expected.not_to be_valid } + end + + describe '#errors' do + it 'returns error about incorrect type' do + expect(needs.errors).to contain_exactly( + 'need:parallel:matrix config should be an array of hashes') + end + end + end + + context 'when needs:parallel:matrix value is incorrect' do + let(:config) { [{ job: 'job_name', parallel: { matrix: 'aaa' } }] } + + describe '#valid?' do + it { is_expected.not_to be_valid } + end + + describe '#errors' do + it 'returns error about incorrect type' do + expect(needs.errors).to contain_exactly( + 'need:parallel:matrix config should be an array of hashes') + end + end + end + + context 'when needs:parallel:matrix value is correct' do + context 'with a simple config' do + let(:config) do + [ + { job: 'job_name', parallel: { matrix: [{ A: 'a1', B: 'b1' }] } } + ] + end + + describe '#valid?' do + it { is_expected.to be_valid } + end + end + + context 'with a complex config' do + let(:config) do + [ + { + job: 'job_name1', + artifacts: true, + parallel: { matrix: [{ A: %w[a1 a2], B: %w[b1 b2 b3], C: %w[c1 c2] }] } + }, + { + job: 'job_name2', + parallel: { + matrix: [ + { A: %w[a1 a2], D: %w[d1 d2] }, + { E: %w[e1 e2], F: ['f1'] }, + { C: %w[c1 c2 c3], G: %w[g1 g2], H: ['h1'] } + ] + } + } + ] + end + + describe '#valid?' do + it { is_expected.to be_valid } + end + end + end + context 'with too many cross pipeline dependencies' do let(:limit) { described_class::NEEDS_CROSS_PIPELINE_DEPENDENCIES_LIMIT } diff --git a/spec/lib/gitlab/ci/config/entry/reports_spec.rb b/spec/lib/gitlab/ci/config/entry/reports_spec.rb index 73bf2d422b7..d610c3ce2f6 100644 --- a/spec/lib/gitlab/ci/config/entry/reports_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/reports_spec.rb @@ -48,6 +48,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Reports, feature_category: :pipeline_c :terraform | 'tfplan.json' :accessibility | 'gl-accessibility.json' :cyclonedx | 'gl-sbom.cdx.zip' + :annotations | 'gl-annotations.json' end with_them do diff --git a/spec/lib/gitlab/ci/config/external/context_spec.rb b/spec/lib/gitlab/ci/config/external/context_spec.rb index d917924f257..d8bd578be94 100644 --- a/spec/lib/gitlab/ci/config/external/context_spec.rb +++ b/spec/lib/gitlab/ci/config/external/context_spec.rb @@ -57,6 +57,24 @@ RSpec.describe Gitlab::Ci::Config::External::Context, feature_category: :pipelin end end end + + describe 'max_total_yaml_size_bytes' do + context 'when application setting `max_total_yaml_size_bytes` is requsted and was never updated by the admin' do + it 'returns the default value `max_total_yaml_size_bytes`' do + expect(subject.max_total_yaml_size_bytes).to eq(157286400) + end + end + + context 'when `max_total_yaml_size_bytes` was adjusted by the admin' do + before do + stub_application_setting(ci_max_total_yaml_size_bytes: 200000000) + end + + it 'returns the updated value of application setting `max_total_yaml_size_bytes`' do + expect(subject.max_total_yaml_size_bytes).to eq(200000000) + end + end + end end describe '#set_deadline' do diff --git a/spec/lib/gitlab/ci/config/external/file/base_spec.rb b/spec/lib/gitlab/ci/config/external/file/base_spec.rb index d6dd75f4b10..1415dbeb532 100644 --- a/spec/lib/gitlab/ci/config/external/file/base_spec.rb +++ b/spec/lib/gitlab/ci/config/external/file/base_spec.rb @@ -254,7 +254,12 @@ RSpec.describe Gitlab::Ci::Config::External::File::Base, feature_category: :pipe describe '#load_and_validate_expanded_hash!' do let(:location) { 'some/file/config.yml' } let(:logger) { instance_double(::Gitlab::Ci::Pipeline::Logger, :instrument) } - let(:context_params) { { sha: 'HEAD', variables: variables, project: project, logger: logger } } + let(:context_params) { { sha: 'HEAD', variables: variables, project: project, logger: logger, user: user } } + let(:user) { instance_double(User, id: 'test-user-id') } + + before do + allow(logger).to receive(:instrument).and_yield + end it 'includes instrumentation for loading and expanding the content' do expect(logger).to receive(:instrument).once.ordered.with(:config_file_fetch_content_hash).and_yield @@ -262,5 +267,26 @@ RSpec.describe Gitlab::Ci::Config::External::File::Base, feature_category: :pipe file.load_and_validate_expanded_hash! end + + context 'when the content is interpolated' do + let(:content) { "spec:\n inputs:\n website:\n---\nkey: value" } + + subject(:file) { test_class.new({ inputs: { website: 'test' }, location: location, content: content }, ctx) } + + it 'increments the ci_interpolation_users usage counter' do + expect(::Gitlab::UsageDataCounters::HLLRedisCounter).to receive(:track_event) + .with('ci_interpolation_users', values: 'test-user-id') + + file.load_and_validate_expanded_hash! + end + end + + context 'when the content is not interpolated' do + it 'does not increment the ci_interpolation_users usage counter' do + expect(::Gitlab::UsageDataCounters::HLLRedisCounter).not_to receive(:track_event) + + file.load_and_validate_expanded_hash! + end + end end end diff --git a/spec/lib/gitlab/ci/config/external/file/component_spec.rb b/spec/lib/gitlab/ci/config/external/file/component_spec.rb index 7e3406413d0..487690296b5 100644 --- a/spec/lib/gitlab/ci/config/external/file/component_spec.rb +++ b/spec/lib/gitlab/ci/config/external/file/component_spec.rb @@ -41,14 +41,6 @@ RSpec.describe Gitlab::Ci::Config::External::File::Component, feature_category: let(:params) { { component: 'some-value' } } it { is_expected.to be_truthy } - - context 'when feature flag ci_include_components is disabled' do - before do - stub_feature_flags(ci_include_components: false) - end - - it { is_expected.to be_falsey } - end end context 'when component is not specified' do diff --git a/spec/lib/gitlab/ci/config/external/mapper/matcher_spec.rb b/spec/lib/gitlab/ci/config/external/mapper/matcher_spec.rb index 719c75dca80..cea65faccd7 100644 --- a/spec/lib/gitlab/ci/config/external/mapper/matcher_spec.rb +++ b/spec/lib/gitlab/ci/config/external/mapper/matcher_spec.rb @@ -18,54 +18,26 @@ RSpec.describe Gitlab::Ci::Config::External::Mapper::Matcher, feature_category: describe '#process' do subject(:process) { matcher.process(locations) } - context 'with ci_include_components FF disabled' do - before do - stub_feature_flags(ci_include_components: false) - end - - let(:locations) do - [ - { local: 'file.yml' }, - { file: 'file.yml', project: 'namespace/project' }, - { remote: 'https://example.com/.gitlab-ci.yml' }, - { template: 'file.yml' }, - { artifact: 'generated.yml', job: 'test' } - ] - end - - it 'returns an array of file objects' do - is_expected.to contain_exactly( - an_instance_of(Gitlab::Ci::Config::External::File::Local), - an_instance_of(Gitlab::Ci::Config::External::File::Project), - an_instance_of(Gitlab::Ci::Config::External::File::Remote), - an_instance_of(Gitlab::Ci::Config::External::File::Template), - an_instance_of(Gitlab::Ci::Config::External::File::Artifact) - ) - end + let(:locations) do + [ + { local: 'file.yml' }, + { file: 'file.yml', project: 'namespace/project' }, + { component: 'gitlab.com/org/component@1.0' }, + { remote: 'https://example.com/.gitlab-ci.yml' }, + { template: 'file.yml' }, + { artifact: 'generated.yml', job: 'test' } + ] end - context 'with ci_include_components FF enabled' do - let(:locations) do - [ - { local: 'file.yml' }, - { file: 'file.yml', project: 'namespace/project' }, - { component: 'gitlab.com/org/component@1.0' }, - { remote: 'https://example.com/.gitlab-ci.yml' }, - { template: 'file.yml' }, - { artifact: 'generated.yml', job: 'test' } - ] - end - - it 'returns an array of file objects' do - is_expected.to contain_exactly( - an_instance_of(Gitlab::Ci::Config::External::File::Local), - an_instance_of(Gitlab::Ci::Config::External::File::Project), - an_instance_of(Gitlab::Ci::Config::External::File::Component), - an_instance_of(Gitlab::Ci::Config::External::File::Remote), - an_instance_of(Gitlab::Ci::Config::External::File::Template), - an_instance_of(Gitlab::Ci::Config::External::File::Artifact) - ) - end + it 'returns an array of file objects' do + is_expected.to contain_exactly( + an_instance_of(Gitlab::Ci::Config::External::File::Local), + an_instance_of(Gitlab::Ci::Config::External::File::Project), + an_instance_of(Gitlab::Ci::Config::External::File::Component), + an_instance_of(Gitlab::Ci::Config::External::File::Remote), + an_instance_of(Gitlab::Ci::Config::External::File::Template), + an_instance_of(Gitlab::Ci::Config::External::File::Artifact) + ) end context 'when a location is not valid' do diff --git a/spec/lib/gitlab/ci/config/external/mapper/verifier_spec.rb b/spec/lib/gitlab/ci/config/external/mapper/verifier_spec.rb index e7dd5bd5079..69b0524be9e 100644 --- a/spec/lib/gitlab/ci/config/external/mapper/verifier_spec.rb +++ b/spec/lib/gitlab/ci/config/external/mapper/verifier_spec.rb @@ -364,5 +364,77 @@ RSpec.describe Gitlab::Ci::Config::External::Mapper::Verifier, feature_category: end end end + + describe '#verify_max_total_pipeline_size' do + let(:files) do + [ + Gitlab::Ci::Config::External::File::Local.new({ local: 'myfolder/file1.yml' }, context), + Gitlab::Ci::Config::External::File::Local.new({ local: 'myfolder/file2.yml' }, context) + ] + end + + let(:project_files) do + { + 'myfolder/file1.yml' => <<~YAML, + build: + script: echo Hello World + YAML + 'myfolder/file2.yml' => <<~YAML + include: + - local: myfolder/file1.yml + build: + script: echo Hello from the other file + YAML + } + end + + context 'when pipeline tree size is within the limit' do + before do + stub_application_setting(ci_max_total_yaml_size_bytes: 10000) + end + + it 'passes the verification' do + expect(process.all?(&:valid?)).to be_truthy + end + end + + context 'when pipeline tree size is larger then the limit' do + before do + stub_application_setting(ci_max_total_yaml_size_bytes: 50) + end + + let(:expected_error_class) { Gitlab::Ci::Config::External::Mapper::TooMuchDataInPipelineTreeError } + + it 'raises a limit error' do + expect { process }.to raise_error(expected_error_class) + end + end + + context 'when introduce_ci_max_total_yaml_size_bytes is disabled' do + before do + stub_feature_flags(introduce_ci_max_total_yaml_size_bytes: false) + end + + context 'when pipeline tree size is within the limit' do + before do + stub_application_setting(ci_max_total_yaml_size_bytes: 10000) + end + + it 'passes the verification' do + expect(process.all?(&:valid?)).to be_truthy + end + end + + context 'when pipeline tree size is larger then the limit' do + before do + stub_application_setting(ci_max_total_yaml_size_bytes: 100) + end + + it 'passes the verification' do + expect(process.all?(&:valid?)).to be_truthy + end + end + end + end end end diff --git a/spec/lib/gitlab/ci/config/external/processor_spec.rb b/spec/lib/gitlab/ci/config/external/processor_spec.rb index 935b6989dd7..19113ce6a4e 100644 --- a/spec/lib/gitlab/ci/config/external/processor_spec.rb +++ b/spec/lib/gitlab/ci/config/external/processor_spec.rb @@ -425,17 +425,6 @@ RSpec.describe Gitlab::Ci::Config::External::Processor, feature_category: :pipel output = processor.perform expect(output.keys).to match_array([:image, :component_x_job]) end - - context 'when feature flag ci_include_components is disabled' do - before do - stub_feature_flags(ci_include_components: false) - end - - it 'returns an error' do - expect { processor.perform } - .to raise_error(described_class::IncludeError, /does not have a valid subkey for include./) - end - end end context 'when a valid project file is defined' do @@ -572,7 +561,17 @@ RSpec.describe Gitlab::Ci::Config::External::Processor, feature_category: :pipel end it 'raises IncludeError' do - expect { subject }.to raise_error(described_class::IncludeError, /invalid include rule/) + expect { subject }.to raise_error(described_class::IncludeError, /contains unknown keys: changes/) + end + + context 'when FF `ci_refactor_external_rules` is disabled' do + before do + stub_feature_flags(ci_refactor_external_rules: false) + end + + it 'raises IncludeError' do + expect { subject }.to raise_error(described_class::IncludeError, /invalid include rule/) + end end end end diff --git a/spec/lib/gitlab/ci/config/external/rules_spec.rb b/spec/lib/gitlab/ci/config/external/rules_spec.rb index 25b7998ef5e..8674af7ab65 100644 --- a/spec/lib/gitlab/ci/config/external/rules_spec.rb +++ b/spec/lib/gitlab/ci/config/external/rules_spec.rb @@ -76,8 +76,7 @@ RSpec.describe Gitlab::Ci::Config::External::Rules, feature_category: :pipeline_ let(:rule_hashes) { [{ if: '$MY_VAR == "hello"', when: 'on_success' }] } it 'raises an error' do - expect { result }.to raise_error(described_class::InvalidIncludeRulesError, - 'invalid include rule: {:if=>"$MY_VAR == \"hello\"", :when=>"on_success"}') + expect { result }.to raise_error(described_class::InvalidIncludeRulesError, /when unknown value: on_success/) end end @@ -105,8 +104,7 @@ RSpec.describe Gitlab::Ci::Config::External::Rules, feature_category: :pipeline_ let(:rule_hashes) { [{ exists: 'Dockerfile', when: 'on_success' }] } it 'raises an error' do - expect { result }.to raise_error(described_class::InvalidIncludeRulesError, - 'invalid include rule: {:exists=>"Dockerfile", :when=>"on_success"}') + expect { result }.to raise_error(described_class::InvalidIncludeRulesError, /when unknown value: on_success/) end end @@ -121,8 +119,94 @@ RSpec.describe Gitlab::Ci::Config::External::Rules, feature_category: :pipeline_ let(:rule_hashes) { [{ changes: ['$MY_VAR'] }] } it 'raises an error' do - expect { result }.to raise_error(described_class::InvalidIncludeRulesError, - 'invalid include rule: {:changes=>["$MY_VAR"]}') + expect { result }.to raise_error(described_class::InvalidIncludeRulesError, /contains unknown keys: changes/) + end + end + + context 'when FF `ci_refactor_external_rules` is disabled' do + before do + stub_feature_flags(ci_refactor_external_rules: false) + end + + context 'when there is no rule' do + let(:rule_hashes) {} + + it { is_expected.to eq(true) } + end + + it_behaves_like 'when there is a rule with if' + + context 'when there is a rule with exists' do + let(:rule_hashes) { [{ exists: 'Dockerfile' }] } + + it_behaves_like 'when there is a rule with exists' + end + + context 'when there is a rule with if and when' do + context 'with when: never' do + let(:rule_hashes) { [{ if: '$MY_VAR == "hello"', when: 'never' }] } + + it_behaves_like 'when there is a rule with if', false, false + end + + context 'with when: always' do + let(:rule_hashes) { [{ if: '$MY_VAR == "hello"', when: 'always' }] } + + it_behaves_like 'when there is a rule with if' + end + + context 'with when: <invalid string>' do + let(:rule_hashes) { [{ if: '$MY_VAR == "hello"', when: 'on_success' }] } + + it 'raises an error' do + expect { result }.to raise_error(described_class::InvalidIncludeRulesError, + 'invalid include rule: {:if=>"$MY_VAR == \"hello\"", :when=>"on_success"}') + end + end + + context 'with when: null' do + let(:rule_hashes) { [{ if: '$MY_VAR == "hello"', when: nil }] } + + it_behaves_like 'when there is a rule with if' + end + end + + context 'when there is a rule with exists and when' do + context 'with when: never' do + let(:rule_hashes) { [{ exists: 'Dockerfile', when: 'never' }] } + + it_behaves_like 'when there is a rule with exists', false, false + end + + context 'with when: always' do + let(:rule_hashes) { [{ exists: 'Dockerfile', when: 'always' }] } + + it_behaves_like 'when there is a rule with exists' + end + + context 'with when: <invalid string>' do + let(:rule_hashes) { [{ exists: 'Dockerfile', when: 'on_success' }] } + + it 'raises an error' do + expect { result }.to raise_error(described_class::InvalidIncludeRulesError, + 'invalid include rule: {:exists=>"Dockerfile", :when=>"on_success"}') + end + end + + context 'with when: null' do + let(:rule_hashes) { [{ exists: 'Dockerfile', when: nil }] } + + it_behaves_like 'when there is a rule with exists' + end + end + + context 'when there is a rule with changes' do + let(:rule_hashes) { [{ changes: ['$MY_VAR'] }] } + + it 'raises an error' do + expect { result }.to raise_error(described_class::InvalidIncludeRulesError, + 'invalid include rule: {:changes=>["$MY_VAR"]}') + end end end end diff --git a/spec/lib/gitlab/ci/config/header/input_spec.rb b/spec/lib/gitlab/ci/config/header/input_spec.rb index 73b5b8f9497..b5155dff6e8 100644 --- a/spec/lib/gitlab/ci/config/header/input_spec.rb +++ b/spec/lib/gitlab/ci/config/header/input_spec.rb @@ -46,12 +46,29 @@ RSpec.describe Gitlab::Ci::Config::Header::Input, feature_category: :pipeline_co it_behaves_like 'a valid input' end - context 'when is a required required input' do + context 'when is a required input' do let(:input_hash) { nil } it_behaves_like 'a valid input' end + context 'when given a valid type' do + where(:input_type) { ::Gitlab::Ci::Config::Interpolation::Inputs.input_types } + + with_them do + let(:input_hash) { { type: input_type } } + + it_behaves_like 'a valid input' + end + end + + context 'when given an invalid type' do + let(:input_hash) { { type: 'datetime' } } + let(:expected_errors) { ['foo input type unknown value: datetime'] } + + it_behaves_like 'an invalid input' + end + context 'when contains unknown keywords' do let(:input_hash) { { test: 123 } } let(:expected_errors) { ['foo config contains unknown keys: test'] } diff --git a/spec/lib/gitlab/ci/interpolation/access_spec.rb b/spec/lib/gitlab/ci/config/interpolation/access_spec.rb index f327377b7e3..ee414c209f7 100644 --- a/spec/lib/gitlab/ci/interpolation/access_spec.rb +++ b/spec/lib/gitlab/ci/config/interpolation/access_spec.rb @@ -2,7 +2,7 @@ require 'fast_spec_helper' -RSpec.describe Gitlab::Ci::Interpolation::Access, feature_category: :pipeline_composition do +RSpec.describe Gitlab::Ci::Config::Interpolation::Access, feature_category: :pipeline_composition do subject { described_class.new(access, ctx) } let(:access) do @@ -46,4 +46,13 @@ RSpec.describe Gitlab::Ci::Interpolation::Access, feature_category: :pipeline_co .to eq 'invalid interpolation access pattern' end end + + context 'when a non-existent key is accessed' do + let(:access) { 'inputs.nonexistent' } + + it 'returns an error' do + expect(subject).not_to be_valid + expect(subject.errors.first).to eq('unknown interpolation key: `nonexistent`') + end + end end diff --git a/spec/lib/gitlab/ci/config/interpolation/block_spec.rb b/spec/lib/gitlab/ci/config/interpolation/block_spec.rb new file mode 100644 index 00000000000..bfaa4eb3e05 --- /dev/null +++ b/spec/lib/gitlab/ci/config/interpolation/block_spec.rb @@ -0,0 +1,112 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' + +RSpec.describe Gitlab::Ci::Config::Interpolation::Block, feature_category: :pipeline_composition do + subject { described_class.new(block, data, ctx) } + + let(:data) do + 'inputs.data' + end + + let(:block) do + "$[[ #{data} ]]" + end + + let(:ctx) do + { inputs: { data: 'abcdef' }, env: { 'ENV' => 'dev' } } + end + + it 'knows its content' do + expect(subject.content).to eq 'inputs.data' + end + + it 'properly evaluates the access pattern' do + expect(subject.value).to eq 'abcdef' + end + + describe '.match' do + it 'matches each block in a string' do + expect { |b| described_class.match('$[[ access1 ]] $[[ access2 ]]', &b) } + .to yield_successive_args(['$[[ access1 ]]', 'access1'], ['$[[ access2 ]]', 'access2']) + end + + it 'matches an empty block' do + expect { |b| described_class.match('$[[]]', &b) } + .to yield_with_args('$[[]]', '') + end + + context 'when functions are specified in the block' do + it 'matches each block in a string' do + expect { |b| described_class.match('$[[ access1 | func1 ]] $[[ access2 | func1 | func2(0,1) ]]', &b) } + .to yield_successive_args(['$[[ access1 | func1 ]]', 'access1 | func1'], + ['$[[ access2 | func1 | func2(0,1) ]]', 'access2 | func1 | func2(0,1)']) + end + end + end + + describe 'when functions are specified in the block' do + let(:function_string1) { 'truncate(1,5)' } + let(:data) { "inputs.data | #{function_string1}" } + let(:access_value) { 'abcdef' } + + it 'returns the modified value' do + expect(subject).to be_valid + expect(subject.value).to eq('bcdef') + end + + context 'when there is an access error' do + let(:data) { "inputs.undefined | #{function_string1}" } + + it 'returns the access error' do + expect(subject).not_to be_valid + expect(subject.errors.first).to eq('unknown interpolation key: `undefined`') + end + end + + context 'when there is a function error' do + let(:data) { 'inputs.data | undefined' } + + it 'returns the function error' do + expect(subject).not_to be_valid + expect(subject.errors.first).to match(/no function matching `undefined`/) + end + end + + context 'when multiple functions are specified' do + let(:function_string2) { 'truncate(2,2)' } + let(:data) { "inputs.data | #{function_string1} | #{function_string2}" } + + it 'executes each function in the specified order' do + expect(subject.value).to eq('de') + end + + context 'when the data has inconsistent spacing' do + let(:data) { "inputs.data|#{function_string1} | #{function_string2} " } + + it 'executes each function in the specified order' do + expect(subject.value).to eq('de') + end + end + + context 'when a stack of functions errors in the middle' do + let(:function_string2) { 'truncate(2)' } + + it 'does not modify the value' do + expect(subject).not_to be_valid + expect(subject.errors.first).to match(/no function matching `truncate\(2\)`/) + expect(subject.instance_variable_get(:@value)).to be_nil + end + end + + context 'when too many functions are specified' do + it 'returns error' do + stub_const('Gitlab::Ci::Config::Interpolation::Block::MAX_FUNCTIONS', 1) + + expect(subject).not_to be_valid + expect(subject.errors.first).to eq('too many functions in interpolation block') + end + end + end + end +end diff --git a/spec/lib/gitlab/ci/config/interpolation/config_spec.rb b/spec/lib/gitlab/ci/config/interpolation/config_spec.rb new file mode 100644 index 00000000000..1731e954906 --- /dev/null +++ b/spec/lib/gitlab/ci/config/interpolation/config_spec.rb @@ -0,0 +1,86 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' + +RSpec.describe Gitlab::Ci::Config::Interpolation::Config, feature_category: :pipeline_composition do + subject { described_class.new(YAML.safe_load(config)) } + + let(:config) do + <<~CFG + test: + spec: + env: $[[ inputs.env ]] + + $[[ inputs.key ]]: + name: $[[ inputs.key ]] + script: my-value + CFG + end + + describe '.fabricate' do + subject { described_class.fabricate(config) } + + context 'when given an Interpolation::Config' do + let(:config) { described_class.new(YAML.safe_load('yaml:')) } + + it 'returns the given config' do + is_expected.to be(config) + end + end + + context 'when given an unknown object' do + let(:config) { [] } + + it 'raises an ArgumentError' do + expect { subject }.to raise_error(ArgumentError, 'unknown interpolation config') + end + end + end + + describe '#replace!' do + it 'replaces each of the nodes with a block return value' do + result = subject.replace! { |node| "abc#{node}cde" } + + expect(result).to eq({ + 'abctestcde' => { 'abcspeccde' => { 'abcenvcde' => 'abc$[[ inputs.env ]]cde' } }, + 'abc$[[ inputs.key ]]cde' => { + 'abcnamecde' => 'abc$[[ inputs.key ]]cde', + 'abcscriptcde' => 'abcmy-valuecde' + } + }) + expect(subject.to_h).to eq({ + '$[[ inputs.key ]]' => { 'name' => '$[[ inputs.key ]]', 'script' => 'my-value' }, + 'test' => { 'spec' => { 'env' => '$[[ inputs.env ]]' } } + }) + end + + context 'when config size is exceeded' do + before do + stub_const("#{described_class}::MAX_NODES", 7) + end + + it 'returns a config size error' do + replaced = 0 + + subject.replace! { replaced += 1 } + + expect(replaced).to eq 4 + expect(subject.errors.size).to eq 1 + expect(subject.errors.first).to eq 'config too large' + end + end + + context 'when node size is exceeded' do + before do + stub_const("#{described_class}::MAX_NODE_SIZE", 1) + end + + it 'returns a config size error' do + subject.replace! { |node| "abc#{node}cde" } + + expect(subject.errors.size).to eq 1 + expect(subject.errors.first).to eq 'config node too large' + end + end + end +end diff --git a/spec/lib/gitlab/ci/interpolation/context_spec.rb b/spec/lib/gitlab/ci/config/interpolation/context_spec.rb index 2b126f4a8b3..c90866c986a 100644 --- a/spec/lib/gitlab/ci/interpolation/context_spec.rb +++ b/spec/lib/gitlab/ci/config/interpolation/context_spec.rb @@ -2,13 +2,27 @@ require 'fast_spec_helper' -RSpec.describe Gitlab::Ci::Interpolation::Context, feature_category: :pipeline_composition do +RSpec.describe Gitlab::Ci::Config::Interpolation::Context, feature_category: :pipeline_composition do subject { described_class.new(ctx) } let(:ctx) do { inputs: { key: 'abc' } } end + describe '.fabricate' do + context 'when given an unexpected object' do + it 'raises an ArgumentError' do + expect { described_class.fabricate([]) }.to raise_error(ArgumentError, 'unknown interpolation context') + end + end + end + + describe '#to_h' do + it 'returns the context hash' do + expect(subject.to_h).to eq(ctx) + end + end + describe '#depth' do it 'returns a max depth of the hash' do expect(subject.depth).to eq 2 diff --git a/spec/lib/gitlab/ci/config/interpolation/functions/base_spec.rb b/spec/lib/gitlab/ci/config/interpolation/functions/base_spec.rb new file mode 100644 index 00000000000..c193e88dbe2 --- /dev/null +++ b/spec/lib/gitlab/ci/config/interpolation/functions/base_spec.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' + +RSpec.describe Gitlab::Ci::Config::Interpolation::Functions::Base, feature_category: :pipeline_composition do + let(:custom_function_klass) do + Class.new(described_class) do + def self.function_expression_pattern + /.*/ + end + + def self.name + 'test_function' + end + end + end + + it 'defines an expected interface for child classes' do + expect { described_class.function_expression_pattern }.to raise_error(NotImplementedError) + expect { described_class.name }.to raise_error(NotImplementedError) + expect { custom_function_klass.new('test').execute('input') }.to raise_error(NotImplementedError) + end +end diff --git a/spec/lib/gitlab/ci/config/interpolation/functions/truncate_spec.rb b/spec/lib/gitlab/ci/config/interpolation/functions/truncate_spec.rb new file mode 100644 index 00000000000..c521eff9811 --- /dev/null +++ b/spec/lib/gitlab/ci/config/interpolation/functions/truncate_spec.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Config::Interpolation::Functions::Truncate, feature_category: :pipeline_composition do + it 'matches exactly the truncate function with 2 numeric arguments' do + expect(described_class.matches?('truncate(1,2)')).to be_truthy + expect(described_class.matches?('truncate( 11 , 222 )')).to be_truthy + expect(described_class.matches?('truncate( string , 222 )')).to be_falsey + expect(described_class.matches?('truncate(222)')).to be_falsey + expect(described_class.matches?('unknown(1,2)')).to be_falsey + end + + it 'truncates the given input' do + function = described_class.new('truncate(1,2)') + + output = function.execute('test') + + expect(function).to be_valid + expect(output).to eq('es') + end + + context 'when given a non-string input' do + it 'returns an error' do + function = described_class.new('truncate(1,2)') + + function.execute(100) + + expect(function).not_to be_valid + expect(function.errors).to contain_exactly( + 'error in `truncate` function: invalid input type: truncate can only be used with string inputs' + ) + end + end +end diff --git a/spec/lib/gitlab/ci/config/interpolation/functions_stack_spec.rb b/spec/lib/gitlab/ci/config/interpolation/functions_stack_spec.rb new file mode 100644 index 00000000000..881f092c440 --- /dev/null +++ b/spec/lib/gitlab/ci/config/interpolation/functions_stack_spec.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Config::Interpolation::FunctionsStack, feature_category: :pipeline_composition do + let(:functions) { ['truncate(0,4)', 'truncate(1,2)'] } + let(:input_value) { 'test_input_value' } + + subject { described_class.new(functions).evaluate(input_value) } + + it 'modifies the given input value according to the function expressions' do + expect(subject).to be_success + expect(subject.value).to eq('es') + end + + context 'when applying a function fails' do + let(:input_value) { 666 } + + it 'returns the error given by the failure' do + expect(subject).not_to be_success + expect(subject.errors).to contain_exactly( + 'error in `truncate` function: invalid input type: truncate can only be used with string inputs' + ) + end + end + + context 'when function expressions do not match any function' do + let(:functions) { ['truncate(0)', 'unknown'] } + + it 'returns an error' do + expect(subject).not_to be_success + expect(subject.errors).to contain_exactly( + 'no function matching `truncate(0)`: check that the function name, arguments, and types are correct', + 'no function matching `unknown`: check that the function name, arguments, and types are correct' + ) + end + end +end diff --git a/spec/lib/gitlab/ci/config/interpolation/inputs/base_input_spec.rb b/spec/lib/gitlab/ci/config/interpolation/inputs/base_input_spec.rb new file mode 100644 index 00000000000..30036ee68ed --- /dev/null +++ b/spec/lib/gitlab/ci/config/interpolation/inputs/base_input_spec.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Config::Interpolation::Inputs::BaseInput, feature_category: :pipeline_composition do + describe '.matches?' do + it 'is not implemented' do + expect { described_class.matches?(double) }.to raise_error(NotImplementedError) + end + end + + describe '.type_name' do + it 'is not implemented' do + expect { described_class.type_name }.to raise_error(NotImplementedError) + end + end + + describe '#valid_value?' do + it 'is not implemented' do + expect do + described_class.new( + name: 'website', spec: { website: nil }, value: { website: 'example.com' } + ).valid_value?('test') + end.to raise_error(NotImplementedError) + end + end +end diff --git a/spec/lib/gitlab/ci/config/interpolation/inputs_spec.rb b/spec/lib/gitlab/ci/config/interpolation/inputs_spec.rb new file mode 100644 index 00000000000..ea06f181fa4 --- /dev/null +++ b/spec/lib/gitlab/ci/config/interpolation/inputs_spec.rb @@ -0,0 +1,137 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Config::Interpolation::Inputs, feature_category: :pipeline_composition do + let(:inputs) { described_class.new(specs, args) } + let(:specs) { { foo: { default: 'bar' } } } + let(:args) { {} } + + context 'when inputs are valid' do + where(:specs, :args, :merged) do + [ + [ + { foo: { default: 'bar' } }, {}, + { foo: 'bar' } + ], + [ + { foo: { default: 'bar' } }, { foo: 'test' }, + { foo: 'test' } + ], + [ + { foo: nil }, { foo: 'bar' }, + { foo: 'bar' } + ], + [ + { foo: { type: 'string' } }, { foo: 'bar' }, + { foo: 'bar' } + ], + [ + { foo: { type: 'string', default: 'bar' } }, { foo: 'test' }, + { foo: 'test' } + ], + [ + { foo: { type: 'string', default: 'bar' } }, {}, + { foo: 'bar' } + ], + [ + { foo: { default: 'bar' }, baz: nil }, { baz: 'test' }, + { foo: 'bar', baz: 'test' } + ], + [ + { number_input: { type: 'number' } }, + { number_input: 8 }, + { number_input: 8 } + ], + [ + { default_number_input: { default: 9, type: 'number' } }, + {}, + { default_number_input: 9 } + ], + [ + { true_input: { type: 'boolean' }, false_input: { type: 'boolean' } }, + { true_input: true, false_input: false }, + { true_input: true, false_input: false } + ], + [ + { default_boolean_input: { default: true, type: 'boolean' } }, + {}, + { default_boolean_input: true } + ] + ] + end + + with_them do + it 'contains the merged inputs' do + expect(inputs).to be_valid + expect(inputs.to_hash).to eq(merged) + end + end + end + + context 'when inputs are invalid' do + where(:specs, :args, :errors) do + [ + [ + { foo: nil }, { foo: 'bar', test: 'bar' }, + ['unknown input arguments: test'] + ], + [ + { foo: nil }, { test: 'bar', gitlab: '1' }, + ['unknown input arguments: test, gitlab', '`foo` input: required value has not been provided'] + ], + [ + { foo: 123 }, {}, + ['unknown input specification for `foo` (valid types: boolean, number, string)'] + ], + [ + { a: nil, foo: 123 }, { a: '123' }, + ['unknown input specification for `foo` (valid types: boolean, number, string)'] + ], + [ + { foo: nil }, {}, + ['`foo` input: required value has not been provided'] + ], + [ + { foo: { default: 123 } }, { foo: 'test' }, + ['`foo` input: default value is not a string'] + ], + [ + { foo: { default: 'test' } }, { foo: 123 }, + ['`foo` input: provided value is not a string'] + ], + [ + { foo: nil }, { foo: 123 }, + ['`foo` input: provided value is not a string'] + ], + [ + { number_input: { type: 'number' } }, + { number_input: 'NaN' }, + ['`number_input` input: provided value is not a number'] + ], + [ + { default_number_input: { default: 'NaN', type: 'number' } }, + {}, + ['`default_number_input` input: default value is not a number'] + ], + [ + { boolean_input: { type: 'boolean' } }, + { boolean_input: 'string' }, + ['`boolean_input` input: provided value is not a boolean'] + ], + [ + { default_boolean_input: { default: 'string', type: 'boolean' } }, + {}, + ['`default_boolean_input` input: default value is not a boolean'] + ] + ] + end + + with_them do + it 'contains the merged inputs', :aggregate_failures do + expect(inputs).not_to be_valid + expect(inputs.errors).to contain_exactly(*errors) + end + end + end +end diff --git a/spec/lib/gitlab/ci/config/yaml/interpolator_spec.rb b/spec/lib/gitlab/ci/config/interpolation/interpolator_spec.rb index 888756a3eb1..7bb09d35064 100644 --- a/spec/lib/gitlab/ci/config/yaml/interpolator_spec.rb +++ b/spec/lib/gitlab/ci/config/interpolation/interpolator_spec.rb @@ -2,13 +2,12 @@ require 'spec_helper' -RSpec.describe Gitlab::Ci::Config::Yaml::Interpolator, feature_category: :pipeline_composition do +RSpec.describe Gitlab::Ci::Config::Interpolation::Interpolator, feature_category: :pipeline_composition do let_it_be(:project) { create(:project) } - let(:current_user) { build(:user, id: 1234) } let(:result) { ::Gitlab::Ci::Config::Yaml::Result.new(config: [header, content]) } - subject { described_class.new(result, arguments, current_user: current_user) } + subject { described_class.new(result, arguments) } context 'when input data is valid' do let(:header) do @@ -26,16 +25,10 @@ RSpec.describe Gitlab::Ci::Config::Yaml::Interpolator, feature_category: :pipeli it 'correctly interpolates the config' do subject.interpolate! + expect(subject).to be_interpolated expect(subject).to be_valid expect(subject.to_hash).to eq({ test: 'deploy gitlab.com' }) end - - it 'tracks the event' do - expect(::Gitlab::UsageDataCounters::HLLRedisCounter).to receive(:track_event) - .with('ci_interpolation_users', { values: 1234 }) - - subject.interpolate! - end end context 'when config has a syntax error' do @@ -54,6 +47,20 @@ RSpec.describe Gitlab::Ci::Config::Yaml::Interpolator, feature_category: :pipeli end end + context 'when spec header is missing but inputs are specified' do + let(:header) { nil } + let(:content) { { test: 'echo' } } + let(:arguments) { { foo: 'bar' } } + + it 'surfaces an error about invalid inputs' do + subject.interpolate! + + expect(subject).not_to be_valid + expect(subject.error_message).to eq subject.errors.first + expect(subject.errors).to include('unknown input arguments') + end + end + context 'when spec header is invalid' do let(:header) do { spec: { arguments: { website: nil } } } @@ -76,47 +83,47 @@ RSpec.describe Gitlab::Ci::Config::Yaml::Interpolator, feature_category: :pipeli end end - context 'when interpolation block is invalid' do + context 'when provided interpolation argument is invalid' do let(:header) do { spec: { inputs: { website: nil } } } end let(:content) do - { test: 'deploy $[[ inputs.abc ]]' } + { test: 'deploy $[[ inputs.website ]]' } end let(:arguments) do - { website: 'gitlab.com' } + { website: ['gitlab.com'] } end - it 'correctly interpolates the config' do + it 'returns an error' do subject.interpolate! expect(subject).not_to be_valid - expect(subject.errors).to include 'unknown interpolation key: `abc`' - expect(subject.error_message).to eq 'interpolation interrupted by errors, unknown interpolation key: `abc`' + expect(subject.error_message).to eq subject.errors.first + expect(subject.errors).to include '`website` input: provided value is not a string' end end - context 'when provided interpolation argument is invalid' do + context 'when interpolation block is invalid' do let(:header) do { spec: { inputs: { website: nil } } } end let(:content) do - { test: 'deploy $[[ inputs.website ]]' } + { test: 'deploy $[[ inputs.abc ]]' } end let(:arguments) do - { website: ['gitlab.com'] } + { website: 'gitlab.com' } end - it 'correctly interpolates the config' do + it 'returns an error' do subject.interpolate! expect(subject).not_to be_valid - expect(subject.error_message).to eq subject.errors.first - expect(subject.errors).to include 'unsupported value in input argument `website`' + expect(subject.errors).to include 'unknown interpolation key: `abc`' + expect(subject.error_message).to eq 'interpolation interrupted by errors, unknown interpolation key: `abc`' end end @@ -133,11 +140,12 @@ RSpec.describe Gitlab::Ci::Config::Yaml::Interpolator, feature_category: :pipeli { website: 'gitlab.com' } end - it 'correctly interpolates the config' do + it 'returns an error' do subject.interpolate! expect(subject).not_to be_valid - expect(subject.error_message).to eq 'interpolation interrupted by errors, unknown interpolation key: `something`' + expect(subject.error_message) + .to eq 'interpolation interrupted by errors, unknown interpolation key: `something`' end end diff --git a/spec/lib/gitlab/ci/interpolation/template_spec.rb b/spec/lib/gitlab/ci/config/interpolation/template_spec.rb index a3ef1bb4445..c7d88822558 100644 --- a/spec/lib/gitlab/ci/interpolation/template_spec.rb +++ b/spec/lib/gitlab/ci/config/interpolation/template_spec.rb @@ -2,7 +2,7 @@ require 'fast_spec_helper' -RSpec.describe Gitlab::Ci::Interpolation::Template, feature_category: :pipeline_composition do +RSpec.describe Gitlab::Ci::Config::Interpolation::Template, feature_category: :pipeline_composition do subject { described_class.new(YAML.safe_load(config), ctx) } let(:config) do @@ -67,7 +67,7 @@ RSpec.describe Gitlab::Ci::Interpolation::Template, feature_category: :pipeline_ context 'when template contains symbols that need interpolation' do subject do - described_class.new({ '$[[ inputs.key ]]'.to_sym => 'cde' }, ctx) + described_class.new({ '$[[ inputs.key ]]': 'cde' }, ctx) end it 'performs a valid interpolation' do @@ -78,7 +78,7 @@ RSpec.describe Gitlab::Ci::Interpolation::Template, feature_category: :pipeline_ context 'when template is too large' do before do - stub_const('Gitlab::Ci::Interpolation::Config::MAX_NODES', 1) + stub_const('Gitlab::Ci::Config::Interpolation::Config::MAX_NODES', 1) end it 'returns an error' do diff --git a/spec/lib/gitlab/ci/config/normalizer_spec.rb b/spec/lib/gitlab/ci/config/normalizer_spec.rb index 96ca5d98a6e..cc549b38dc3 100644 --- a/spec/lib/gitlab/ci/config/normalizer_spec.rb +++ b/spec/lib/gitlab/ci/config/normalizer_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'fast_spec_helper' +require 'spec_helper' RSpec.describe Gitlab::Ci::Config::Normalizer do let(:job_name) { :rspec } @@ -103,6 +103,34 @@ RSpec.describe Gitlab::Ci::Config::Normalizer do end end + shared_examples 'needs:parallel:matrix' do + let(:expanded_needs_parallel_job_attributes) do + expanded_needs_parallel_job_names.map do |job_name| + { name: job_name } + end + end + + context 'when job has needs:parallel:matrix on parallelized jobs' do + let(:config) do + { + job_name => job_config, + other_job: { + script: 'echo 1', + needs: { + job: [ + { name: job_name.to_s, parallel: needs_parallel_config } + ] + } + } + } + end + + it 'parallelizes and only keeps needs specified by needs:parallel:matrix' do + expect(subject.dig(:other_job, :needs, :job)).to eq(expanded_needs_parallel_job_attributes) + end + end + end + context 'with parallel config as integer' do let(:variables_config) { {} } let(:parallel_config) { 5 } @@ -167,7 +195,7 @@ RSpec.describe Gitlab::Ci::Config::Normalizer do it_behaves_like 'parallel needs' end - context 'with parallel matrix config' do + context 'with a simple parallel matrix config' do let(:variables_config) do { USER_VARIABLE: 'user value' @@ -192,6 +220,19 @@ RSpec.describe Gitlab::Ci::Config::Normalizer do ] end + let(:needs_parallel_config) do + { + matrix: [ + { + VAR_1: ['A'], + VAR_2: ['C'] + } + ] + } + end + + let(:expanded_needs_parallel_job_names) { ['rspec: [A, C]'] } + it 'does not have original job' do is_expected.not_to include(job_name) end @@ -228,6 +269,66 @@ RSpec.describe Gitlab::Ci::Config::Normalizer do it_behaves_like 'parallel dependencies' it_behaves_like 'parallel needs' + it_behaves_like 'needs:parallel:matrix' + end + + context 'with a complex parallel matrix config' do + let(:variables_config) { {} } + let(:parallel_config) do + { + matrix: [ + { + PLATFORM: ['centos'], + STACK: %w[ruby python java], + DB: %w[postgresql mysql] + }, + { + PLATFORM: ['ubuntu'], + PROVIDER: %w[aws gcp] + } + ] + } + end + + let(:needs_parallel_config) do + { + matrix: [ + { + PLATFORM: ['centos'], + STACK: %w[ruby python], + DB: ['postgresql'] + }, + { + PLATFORM: ['ubuntu'], + PROVIDER: ['aws'] + } + ] + } + end + + let(:expanded_needs_parallel_job_names) do + [ + 'rspec: [centos, ruby, postgresql]', + 'rspec: [centos, python, postgresql]', + 'rspec: [ubuntu, aws]' + ] + end + + let(:expanded_job_names) do + [ + 'rspec: [centos, ruby, postgresql]', + 'rspec: [centos, ruby, mysql]', + 'rspec: [centos, python, postgresql]', + 'rspec: [centos, python, mysql]', + 'rspec: [centos, java, postgresql]', + 'rspec: [centos, java, mysql]', + 'rspec: [ubuntu, aws]', + 'rspec: [ubuntu, gcp]' + ] + end + + it_behaves_like 'parallel needs' + it_behaves_like 'needs:parallel:matrix' end context 'when parallel config does not matches a factory' do diff --git a/spec/lib/gitlab/ci/config/yaml/loader_spec.rb b/spec/lib/gitlab/ci/config/yaml/loader_spec.rb index 4e6151677e6..57a9a47d699 100644 --- a/spec/lib/gitlab/ci/config/yaml/loader_spec.rb +++ b/spec/lib/gitlab/ci/config/yaml/loader_spec.rb @@ -21,12 +21,13 @@ RSpec.describe ::Gitlab::Ci::Config::Yaml::Loader, feature_category: :pipeline_c YAML end - subject(:result) { described_class.new(yaml, inputs: inputs, current_user: project.creator).load } + subject(:result) { described_class.new(yaml, inputs: inputs).load } it 'loads and interpolates CI config YAML' do expected_config = { test_job: { script: ['echo "hello test"'] } } expect(result).to be_valid + expect(result).to be_interpolated expect(result.content).to eq(expected_config) end diff --git a/spec/lib/gitlab/ci/config/yaml/result_spec.rb b/spec/lib/gitlab/ci/config/yaml/result_spec.rb index d17e0609ef6..a66c630dfc9 100644 --- a/spec/lib/gitlab/ci/config/yaml/result_spec.rb +++ b/spec/lib/gitlab/ci/config/yaml/result_spec.rb @@ -51,4 +51,14 @@ RSpec.describe Gitlab::Ci::Config::Yaml::Result, feature_category: :pipeline_com expect(result).not_to be_valid expect(result.error).to be_a ArgumentError end + + describe '#interpolated?' do + it 'defaults to false' do + expect(described_class.new).not_to be_interpolated + end + + it 'returns the value passed to the initializer' do + expect(described_class.new(interpolated: true)).to be_interpolated + end + end end diff --git a/spec/lib/gitlab/ci/config/yaml_spec.rb b/spec/lib/gitlab/ci/config/yaml_spec.rb index 27d93d555f1..e30ddbb8033 100644 --- a/spec/lib/gitlab/ci/config/yaml_spec.rb +++ b/spec/lib/gitlab/ci/config/yaml_spec.rb @@ -36,17 +36,5 @@ RSpec.describe Gitlab::Ci::Config::Yaml, feature_category: :pipeline_composition .to raise_error ::Gitlab::Config::Loader::FormatError, /mapping values are not allowed in this context/ end end - - context 'when given a user' do - let(:user) { instance_double(User) } - - subject(:config) { described_class.load!(yaml, current_user: user) } - - it 'passes it to Loader' do - expect(::Gitlab::Ci::Config::Yaml::Loader).to receive(:new).with(yaml, current_user: user).and_call_original - - config - end - end end end diff --git a/spec/lib/gitlab/ci/decompressed_gzip_size_validator_spec.rb b/spec/lib/gitlab/ci/decompressed_gzip_size_validator_spec.rb index dad5bd2548b..f1b10648f51 100644 --- a/spec/lib/gitlab/ci/decompressed_gzip_size_validator_spec.rb +++ b/spec/lib/gitlab/ci/decompressed_gzip_size_validator_spec.rb @@ -5,7 +5,7 @@ require 'spec_helper' RSpec.describe Gitlab::Ci::DecompressedGzipSizeValidator, feature_category: :importers do let_it_be(:filepath) { File.join(Dir.tmpdir, 'decompressed_gzip_size_validator_spec.gz') } - before(:all) do + before_all do create_compressed_file end diff --git a/spec/lib/gitlab/ci/input/arguments/base_spec.rb b/spec/lib/gitlab/ci/input/arguments/base_spec.rb deleted file mode 100644 index ed8e99b7257..00000000000 --- a/spec/lib/gitlab/ci/input/arguments/base_spec.rb +++ /dev/null @@ -1,19 +0,0 @@ -# frozen_string_literal: true - -require 'fast_spec_helper' - -RSpec.describe Gitlab::Ci::Input::Arguments::Base, feature_category: :pipeline_composition do - subject do - Class.new(described_class) do - def validate!; end - def to_value; end - end - end - - it 'fabricates an invalid input argument if unknown value is provided' do - argument = subject.new(:something, { spec: 123 }, [:a, :b]) - - expect(argument).not_to be_valid - expect(argument.errors.first).to eq 'unsupported value in input argument `something`' - end -end diff --git a/spec/lib/gitlab/ci/input/arguments/default_spec.rb b/spec/lib/gitlab/ci/input/arguments/default_spec.rb deleted file mode 100644 index bc0cee6ac4e..00000000000 --- a/spec/lib/gitlab/ci/input/arguments/default_spec.rb +++ /dev/null @@ -1,53 +0,0 @@ -# frozen_string_literal: true - -require 'fast_spec_helper' - -RSpec.describe Gitlab::Ci::Input::Arguments::Default, feature_category: :pipeline_composition do - it 'returns a user-provided value if it is present' do - argument = described_class.new(:website, { default: 'https://gitlab.com' }, 'https://example.gitlab.com') - - expect(argument).to be_valid - expect(argument.to_value).to eq 'https://example.gitlab.com' - expect(argument.to_hash).to eq({ website: 'https://example.gitlab.com' }) - end - - it 'returns an empty value if user-provider input is empty' do - argument = described_class.new(:website, { default: 'https://gitlab.com' }, '') - - expect(argument).to be_valid - expect(argument.to_value).to eq '' - expect(argument.to_hash).to eq({ website: '' }) - end - - it 'returns a default value if user-provider one is unknown' do - argument = described_class.new(:website, { default: 'https://gitlab.com' }, nil) - - expect(argument).to be_valid - expect(argument.to_value).to eq 'https://gitlab.com' - expect(argument.to_hash).to eq({ website: 'https://gitlab.com' }) - end - - it 'returns an error if the default argument has not been recognized' do - argument = described_class.new(:website, { default: ['gitlab.com'] }, 'abc') - - expect(argument).not_to be_valid - end - - it 'returns an error if the argument has not been fabricated correctly' do - argument = described_class.new(:website, { required: 'https://gitlab.com' }, 'https://example.gitlab.com') - - expect(argument).not_to be_valid - end - - describe '.matches?' do - it 'matches specs with default configuration' do - expect(described_class.matches?({ default: 'abc' })).to be true - end - - it 'does not match specs different configuration keyword' do - expect(described_class.matches?({ options: %w[a b] })).to be false - expect(described_class.matches?('a b c')).to be false - expect(described_class.matches?(%w[default a])).to be false - end - end -end diff --git a/spec/lib/gitlab/ci/input/arguments/options_spec.rb b/spec/lib/gitlab/ci/input/arguments/options_spec.rb deleted file mode 100644 index 17e3469b294..00000000000 --- a/spec/lib/gitlab/ci/input/arguments/options_spec.rb +++ /dev/null @@ -1,54 +0,0 @@ -# frozen_string_literal: true - -require 'fast_spec_helper' - -RSpec.describe Gitlab::Ci::Input::Arguments::Options, feature_category: :pipeline_composition do - it 'returns a user-provided value if it is an allowed one' do - argument = described_class.new(:run, { options: %w[opt1 opt2] }, 'opt1') - - expect(argument).to be_valid - expect(argument.to_value).to eq 'opt1' - expect(argument.to_hash).to eq({ run: 'opt1' }) - end - - it 'returns an error if user-provided value is not allowlisted' do - argument = described_class.new(:run, { options: %w[opt1 opt2] }, 'opt3') - - expect(argument).not_to be_valid - expect(argument.errors.first).to eq '`run` input: argument value opt3 not allowlisted' - end - - it 'returns an error if specification is not correct' do - argument = described_class.new(:website, { options: nil }, 'opt1') - - expect(argument).not_to be_valid - expect(argument.errors.first).to eq '`website` input: argument specification invalid' - end - - it 'returns an error if specification is using a hash' do - argument = described_class.new(:website, { options: { a: 1 } }, 'opt1') - - expect(argument).not_to be_valid - expect(argument.errors.first).to eq '`website` input: argument specification invalid' - end - - it 'returns an empty value if it is allowlisted' do - argument = described_class.new(:run, { options: ['opt1', ''] }, '') - - expect(argument).to be_valid - expect(argument.to_value).to be_empty - expect(argument.to_hash).to eq({ run: '' }) - end - - describe '.matches?' do - it 'matches specs with options configuration' do - expect(described_class.matches?({ options: %w[a b] })).to be true - end - - it 'does not match specs different configuration keyword' do - expect(described_class.matches?({ default: 'abc' })).to be false - expect(described_class.matches?(['options'])).to be false - expect(described_class.matches?('options')).to be false - end - end -end diff --git a/spec/lib/gitlab/ci/input/arguments/required_spec.rb b/spec/lib/gitlab/ci/input/arguments/required_spec.rb deleted file mode 100644 index 847272998c2..00000000000 --- a/spec/lib/gitlab/ci/input/arguments/required_spec.rb +++ /dev/null @@ -1,45 +0,0 @@ -# frozen_string_literal: true - -require 'fast_spec_helper' - -RSpec.describe Gitlab::Ci::Input::Arguments::Required, feature_category: :pipeline_composition do - it 'returns a user-provided value if it is present' do - argument = described_class.new(:website, nil, 'https://example.gitlab.com') - - expect(argument).to be_valid - expect(argument.to_value).to eq 'https://example.gitlab.com' - expect(argument.to_hash).to eq({ website: 'https://example.gitlab.com' }) - end - - it 'returns an empty value if user-provider value is empty' do - argument = described_class.new(:website, nil, '') - - expect(argument).to be_valid - expect(argument.to_hash).to eq(website: '') - end - - it 'returns an error if user-provided value is unspecified' do - argument = described_class.new(:website, nil, nil) - - expect(argument).not_to be_valid - expect(argument.errors.first).to eq '`website` input: required value has not been provided' - end - - describe '.matches?' do - it 'matches specs without configuration' do - expect(described_class.matches?(nil)).to be true - end - - it 'matches specs with empty configuration' do - expect(described_class.matches?('')).to be true - end - - it 'matches specs with an empty hash configuration' do - expect(described_class.matches?({})).to be true - end - - it 'does not match specs with configuration' do - expect(described_class.matches?({ options: %w[a b] })).to be false - end - end -end diff --git a/spec/lib/gitlab/ci/input/arguments/unknown_spec.rb b/spec/lib/gitlab/ci/input/arguments/unknown_spec.rb deleted file mode 100644 index 1270423ac72..00000000000 --- a/spec/lib/gitlab/ci/input/arguments/unknown_spec.rb +++ /dev/null @@ -1,18 +0,0 @@ -# frozen_string_literal: true - -require 'fast_spec_helper' - -RSpec.describe Gitlab::Ci::Input::Arguments::Unknown, feature_category: :pipeline_composition do - it 'raises an error when someone tries to evaluate the value' do - argument = described_class.new(:website, nil, 'https://example.gitlab.com') - - expect(argument).not_to be_valid - expect { argument.to_value }.to raise_error ArgumentError - end - - describe '.matches?' do - it 'always matches' do - expect(described_class.matches?('abc')).to be true - end - end -end diff --git a/spec/lib/gitlab/ci/input/inputs_spec.rb b/spec/lib/gitlab/ci/input/inputs_spec.rb deleted file mode 100644 index 5d2d5192299..00000000000 --- a/spec/lib/gitlab/ci/input/inputs_spec.rb +++ /dev/null @@ -1,126 +0,0 @@ -# frozen_string_literal: true - -require 'fast_spec_helper' - -RSpec.describe Gitlab::Ci::Input::Inputs, feature_category: :pipeline_composition do - describe '#valid?' do - let(:spec) { { website: nil } } - - it 'describes user-provided inputs' do - inputs = described_class.new(spec, { website: 'http://example.gitlab.com' }) - - expect(inputs).to be_valid - end - end - - context 'when proper specification has been provided' do - let(:spec) do - { - website: nil, - env: { default: 'development' }, - run: { options: %w[tests spec e2e] } - } - end - - let(:args) { { website: 'https://gitlab.com', run: 'tests' } } - - it 'fabricates desired input arguments' do - inputs = described_class.new(spec, args) - - expect(inputs).to be_valid - expect(inputs.count).to eq 3 - expect(inputs.to_hash).to eq(args.merge(env: 'development')) - end - end - - context 'when inputs and args are empty' do - it 'is a valid use-case' do - inputs = described_class.new({}, {}) - - expect(inputs).to be_valid - expect(inputs.to_hash).to be_empty - end - end - - context 'when there are arguments recoincilation errors present' do - context 'when required argument is missing' do - let(:spec) { { website: nil } } - - it 'returns an error' do - inputs = described_class.new(spec, {}) - - expect(inputs).not_to be_valid - expect(inputs.errors.first).to eq '`website` input: required value has not been provided' - end - end - - context 'when argument is not present but configured as allowlist' do - let(:spec) do - { run: { options: %w[opt1 opt2] } } - end - - it 'returns an error' do - inputs = described_class.new(spec, {}) - - expect(inputs).not_to be_valid - expect(inputs.errors.first).to eq '`run` input: argument not provided' - end - end - end - - context 'when unknown specification argument has been used' do - let(:spec) do - { - website: nil, - env: { default: 'development' }, - run: { options: %w[tests spec e2e] }, - test: { unknown: 'something' } - } - end - - let(:args) { { website: 'https://gitlab.com', run: 'tests' } } - - it 'fabricates an unknown argument entry and returns an error' do - inputs = described_class.new(spec, args) - - expect(inputs).not_to be_valid - expect(inputs.count).to eq 4 - expect(inputs.errors.first).to eq '`test` input: unrecognized input argument specification: `unknown`' - end - end - - context 'when unknown arguments are being passed by a user' do - let(:spec) do - { env: { default: 'development' } } - end - - let(:args) { { website: 'https://gitlab.com', run: 'tests' } } - - it 'returns an error with a list of unknown arguments' do - inputs = described_class.new(spec, args) - - expect(inputs).not_to be_valid - expect(inputs.errors.first).to eq 'unknown input arguments: [:website, :run]' - end - end - - context 'when composite specification is being used' do - let(:spec) do - { - env: { - default: 'dev', - options: %w[test dev prod] - } - } - end - - let(:args) { { env: 'dev' } } - - it 'returns an error describing an unknown specification' do - inputs = described_class.new(spec, args) - - expect(inputs).not_to be_valid - expect(inputs.errors.first).to eq '`env` input: unrecognized input argument definition' - end - end -end diff --git a/spec/lib/gitlab/ci/interpolation/block_spec.rb b/spec/lib/gitlab/ci/interpolation/block_spec.rb deleted file mode 100644 index 4a8709df3dc..00000000000 --- a/spec/lib/gitlab/ci/interpolation/block_spec.rb +++ /dev/null @@ -1,39 +0,0 @@ -# frozen_string_literal: true - -require 'fast_spec_helper' - -RSpec.describe Gitlab::Ci::Interpolation::Block, feature_category: :pipeline_composition do - subject { described_class.new(block, data, ctx) } - - let(:data) do - 'inputs.data' - end - - let(:block) do - "$[[ #{data} ]]" - end - - let(:ctx) do - { inputs: { data: 'abc' }, env: { 'ENV' => 'dev' } } - end - - it 'knows its content' do - expect(subject.content).to eq 'inputs.data' - end - - it 'properly evaluates the access pattern' do - expect(subject.value).to eq 'abc' - end - - describe '.match' do - it 'matches each block in a string' do - expect { |b| described_class.match('$[[ access1 ]] $[[ access2 ]]', &b) } - .to yield_successive_args(['$[[ access1 ]]', 'access1'], ['$[[ access2 ]]', 'access2']) - end - - it 'matches an empty block' do - expect { |b| described_class.match('$[[]]', &b) } - .to yield_with_args('$[[]]', '') - end - end -end diff --git a/spec/lib/gitlab/ci/interpolation/config_spec.rb b/spec/lib/gitlab/ci/interpolation/config_spec.rb deleted file mode 100644 index e745269d8c0..00000000000 --- a/spec/lib/gitlab/ci/interpolation/config_spec.rb +++ /dev/null @@ -1,49 +0,0 @@ -# frozen_string_literal: true - -require 'fast_spec_helper' - -RSpec.describe Gitlab::Ci::Interpolation::Config, feature_category: :pipeline_composition do - subject { described_class.new(YAML.safe_load(config)) } - - let(:config) do - <<~CFG - test: - spec: - env: $[[ inputs.env ]] - - $[[ inputs.key ]]: - name: $[[ inputs.key ]] - script: my-value - CFG - end - - describe '#replace!' do - it 'replaces each od the nodes with a block return value' do - result = subject.replace! { |node| "abc#{node}cde" } - - expect(result).to eq({ - 'abctestcde' => { 'abcspeccde' => { 'abcenvcde' => 'abc$[[ inputs.env ]]cde' } }, - 'abc$[[ inputs.key ]]cde' => { - 'abcnamecde' => 'abc$[[ inputs.key ]]cde', - 'abcscriptcde' => 'abcmy-valuecde' - } - }) - end - end - - context 'when config size is exceeded' do - before do - stub_const("#{described_class}::MAX_NODES", 7) - end - - it 'returns a config size error' do - replaced = 0 - - subject.replace! { replaced += 1 } - - expect(replaced).to eq 4 - expect(subject.errors.size).to eq 1 - expect(subject.errors.first).to eq 'config too large' - end - end -end diff --git a/spec/lib/gitlab/ci/jwt_v2/claim_mapper/repository_spec.rb b/spec/lib/gitlab/ci/jwt_v2/claim_mapper/repository_spec.rb new file mode 100644 index 00000000000..0dd0d2fcf0d --- /dev/null +++ b/spec/lib/gitlab/ci/jwt_v2/claim_mapper/repository_spec.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::JwtV2::ClaimMapper::Repository, feature_category: :continuous_integration do + let_it_be(:sha) { '35fa264414ee3ed7d0b8a6f5da40751c8600a772' } + let_it_be(:pipeline) { build_stubbed(:ci_pipeline, ref: 'test-branch-for-claim-mapper', sha: sha) } + + let(:url) { 'gitlab.com/gitlab-org/gitlab//.gitlab-ci.yml' } + let(:project_config) { instance_double(Gitlab::Ci::ProjectConfig, url: url) } + + subject(:mapper) { described_class.new(project_config, pipeline) } + + describe '#to_h' do + it 'returns expected claims' do + expect(mapper.to_h).to eq({ + ci_config_ref_uri: 'gitlab.com/gitlab-org/gitlab//.gitlab-ci.yml@refs/heads/test-branch-for-claim-mapper', + ci_config_sha: sha + }) + end + + context 'when ref is a tag' do + let_it_be(:tag) { 'test-tag-for-claim-mapper' } + let_it_be(:pipeline) { build_stubbed(:ci_pipeline, tag: tag, ref: tag, sha: sha) } + + it 'returns expected claims' do + expect(mapper.to_h).to eq({ + ci_config_ref_uri: 'gitlab.com/gitlab-org/gitlab//.gitlab-ci.yml@refs/tags/test-tag-for-claim-mapper', + ci_config_sha: sha + }) + end + end + end +end diff --git a/spec/lib/gitlab/ci/jwt_v2/claim_mapper_spec.rb b/spec/lib/gitlab/ci/jwt_v2/claim_mapper_spec.rb new file mode 100644 index 00000000000..b7a73c938a3 --- /dev/null +++ b/spec/lib/gitlab/ci/jwt_v2/claim_mapper_spec.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::JwtV2::ClaimMapper, feature_category: :continuous_integration do + let_it_be(:pipeline) { build_stubbed(:ci_pipeline) } + + let(:source) { :unknown_source } + let(:url) { 'gitlab.com/gitlab-org/gitlab//.gitlab-ci.yml' } + let(:project_config) { instance_double(Gitlab::Ci::ProjectConfig, url: url, source: source) } + + subject(:mapper) { described_class.new(project_config, pipeline) } + + describe '#to_h' do + it 'returns an empty hash when source is not implemented' do + expect(mapper.to_h).to eq({}) + end + + context 'when mapper for source is implemented' do + where(:source) { described_class::MAPPER_FOR_CONFIG_SOURCE.keys } + let(:result) do + { + ci_config_ref_uri: 'ci_config_ref_uri', + ci_config_sha: 'ci_config_sha' + } + end + + with_them do + it 'uses mapper' do + mapper_class = described_class::MAPPER_FOR_CONFIG_SOURCE[source] + expect_next_instance_of(mapper_class, project_config, pipeline) do |instance| + expect(instance).to receive(:to_h).and_return(result) + end + + expect(mapper.to_h).to eq(result) + end + end + end + end +end diff --git a/spec/lib/gitlab/ci/jwt_v2_spec.rb b/spec/lib/gitlab/ci/jwt_v2_spec.rb index 575f174f737..d45d8cacb88 100644 --- a/spec/lib/gitlab/ci/jwt_v2_spec.rb +++ b/spec/lib/gitlab/ci/jwt_v2_spec.rb @@ -129,75 +129,39 @@ RSpec.describe Gitlab::Ci::JwtV2, feature_category: :continuous_integration do end end - describe 'ci_config_ref_uri' do - it 'joins project_config.url and pipeline.source_ref_path with @' do - expect(payload[:ci_config_ref_uri]).to eq('gitlab.com/gitlab-org/gitlab//.gitlab-ci.yml' \ - '@refs/heads/auto-deploy-2020-03-19') - end - - context 'when project config is nil' do - before do - allow(Gitlab::Ci::ProjectConfig).to receive(:new).and_return(nil) - end - - it 'is nil' do - expect(payload[:ci_config_ref_uri]).to be_nil - end - end - - context 'when ProjectConfig#url raises an error' do - before do - allow(project_config).to receive(:url).and_raise(RuntimeError) - end + describe 'claims delegated to mapper' do + let(:ci_config_ref_uri) { 'ci_config_ref_uri' } + let(:ci_config_sha) { 'ci_config_sha' } - it 'raises the same error' do - expect { payload }.to raise_error(RuntimeError) + it 'delegates claims to Gitlab::Ci::JwtV2::ClaimMapper' do + expect_next_instance_of(Gitlab::Ci::JwtV2::ClaimMapper, project_config, pipeline) do |mapper| + expect(mapper).to receive(:to_h).and_return({ + ci_config_ref_uri: ci_config_ref_uri, + ci_config_sha: ci_config_sha + }) end - context 'in production' do - before do - stub_rails_env('production') - end - - it 'is nil' do - expect(payload[:ci_config_ref_uri]).to be_nil - end - end - end - - context 'when config source is not repository' do - before do - allow(project_config).to receive(:source).and_return(:auto_devops_source) - end - - it 'is nil' do - expect(payload[:ci_config_ref_uri]).to be_nil - end + expect(payload[:ci_config_ref_uri]).to eq(ci_config_ref_uri) + expect(payload[:ci_config_sha]).to eq(ci_config_sha) end end - describe 'ci_config_sha' do - it 'is the SHA of the pipeline' do - expect(payload[:ci_config_sha]).to eq(pipeline.sha) - end + describe 'project_visibility' do + using RSpec::Parameterized::TableSyntax - context 'when project config is nil' do - before do - allow(Gitlab::Ci::ProjectConfig).to receive(:new).and_return(nil) - end - - it 'is nil' do - expect(payload[:ci_config_sha]).to be_nil - end + where(:visibility_level, :visibility_level_string) do + Project::PUBLIC | 'public' + Project::INTERNAL | 'internal' + Project::PRIVATE | 'private' end - context 'when config source is not repository' do + with_them do before do - allow(project_config).to receive(:source).and_return(:auto_devops_source) + project.visibility_level = visibility_level end - it 'is nil' do - expect(payload[:ci_config_sha]).to be_nil + it 'is a string representation of the project visibility_level' do + expect(payload[:project_visibility]).to eq(visibility_level_string) end end end diff --git a/spec/lib/gitlab/ci/pipeline/chain/config/content_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/config/content_spec.rb index 9c268d9039e..66e4b987ac1 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/config/content_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/config/content_spec.rb @@ -42,9 +42,9 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Config::Content, feature_category: : before do expect(project.repository) - .to receive(:gitlab_ci_yml_for) + .to receive(:blob_at) .with(pipeline.sha, ci_config_path) - .and_return('the-content') + .and_return(instance_double(Blob, empty?: false)) end it 'builds root config including the local custom file' do @@ -132,9 +132,9 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Config::Content, feature_category: : before do expect(project.repository) - .to receive(:gitlab_ci_yml_for) + .to receive(:blob_at) .with(pipeline.sha, '.gitlab-ci.yml') - .and_return('the-content') + .and_return(instance_double(Blob, empty?: false)) end it 'builds root config including the canonical CI config file' do diff --git a/spec/lib/gitlab/ci/project_config/repository_spec.rb b/spec/lib/gitlab/ci/project_config/repository_spec.rb index e8a997a7e43..bd95eefe821 100644 --- a/spec/lib/gitlab/ci/project_config/repository_spec.rb +++ b/spec/lib/gitlab/ci/project_config/repository_spec.rb @@ -32,7 +32,7 @@ RSpec.describe Gitlab::Ci::ProjectConfig::Repository, feature_category: :continu context 'when Gitaly raises error' do before do - allow(project.repository).to receive(:gitlab_ci_yml_for).and_raise(GRPC::Internal) + allow(project.repository).to receive(:blob_at).and_raise(GRPC::Internal) end it { is_expected.to be_nil } diff --git a/spec/lib/gitlab/ci/project_config_spec.rb b/spec/lib/gitlab/ci/project_config_spec.rb index 13ef0939ddd..6a4af3c61bf 100644 --- a/spec/lib/gitlab/ci/project_config_spec.rb +++ b/spec/lib/gitlab/ci/project_config_spec.rb @@ -45,9 +45,9 @@ RSpec.describe Gitlab::Ci::ProjectConfig, feature_category: :pipeline_compositio before do allow(project.repository) - .to receive(:gitlab_ci_yml_for) + .to receive(:blob_at) .with(sha, ci_config_path) - .and_return('the-content') + .and_return(instance_double(Blob, empty?: false)) end it 'returns root config including the local custom file' do @@ -122,9 +122,9 @@ RSpec.describe Gitlab::Ci::ProjectConfig, feature_category: :pipeline_compositio before do allow(project.repository) - .to receive(:gitlab_ci_yml_for) + .to receive(:blob_at) .with(sha, '.gitlab-ci.yml') - .and_return('the-content') + .and_return(instance_double(Blob, empty?: false)) end it 'returns root config including the canonical CI config file' do diff --git a/spec/lib/gitlab/ci/queue/metrics_spec.rb b/spec/lib/gitlab/ci/queue/metrics_spec.rb new file mode 100644 index 00000000000..2fb4226ba5a --- /dev/null +++ b/spec/lib/gitlab/ci/queue/metrics_spec.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Queue::Metrics, feature_category: :continuous_integration do + let(:metrics) { described_class.new(build(:ci_runner)) } + + describe '#observe_queue_depth' do + subject { metrics.observe_queue_depth(:found, 1) } + + it { is_expected.not_to be_nil } + + context 'with feature flag gitlab_ci_builds_queueing_metrics disabled' do + before do + stub_feature_flags(gitlab_ci_builds_queuing_metrics: false) + end + + it { is_expected.to be_nil } + end + end + + describe '#observe_queue_size' do + subject { metrics.observe_queue_size(-> { 0 }, :some_runner_type) } + + it { is_expected.not_to be_nil } + + context 'with feature flag gitlab_ci_builds_queueing_metrics disabled' do + before do + stub_feature_flags(gitlab_ci_builds_queuing_metrics: false) + end + + it { is_expected.to be_nil } + end + end + + describe '#observe_queue_time' do + subject { metrics.observe_queue_time(:process, :some_runner_type) { 1 } } + + specify do + expect(described_class).to receive(:queue_iteration_duration_seconds).and_call_original + + subject + end + + context 'with feature flag gitlab_ci_builds_queueing_metrics disabled' do + before do + stub_feature_flags(gitlab_ci_builds_queuing_metrics: false) + end + + specify do + expect(described_class).not_to receive(:queue_iteration_duration_seconds) + + subject + end + end + + describe '.observe_active_runners' do + subject { described_class.observe_active_runners(-> { 0 }) } + + it { is_expected.not_to be_nil } + + context 'with feature flag gitlab_ci_builds_queueing_metrics disabled' do + before do + stub_feature_flags(gitlab_ci_builds_queuing_metrics: false) + end + + it { is_expected.to be_nil } + end + end + end +end diff --git a/spec/lib/gitlab/ci/reports/sbom/component_spec.rb b/spec/lib/gitlab/ci/reports/sbom/component_spec.rb index 5dbcc1991d4..d62d25aeefe 100644 --- a/spec/lib/gitlab/ci/reports/sbom/component_spec.rb +++ b/spec/lib/gitlab/ci/reports/sbom/component_spec.rb @@ -27,6 +27,154 @@ RSpec.describe Gitlab::Ci::Reports::Sbom::Component, feature_category: :dependen ) end + describe '#name' do + subject { component.name } + + it { is_expected.to eq(name) } + + context 'with namespace' do + let(:purl) do + 'pkg:maven/org.NameSpace/Name@v0.0.1' + end + + it { is_expected.to eq('org.NameSpace/Name') } + + context 'when needing normalization' do + let(:purl) do + 'pkg:pypi/org.NameSpace/Name@v0.0.1' + end + + it { is_expected.to eq('org.namespace/name') } + end + end + end + + describe '#<=>' do + where do + { + 'equal' => { + a_name: 'component-a', + b_name: 'component-a', + a_type: 'library', + b_type: 'library', + a_purl: 'pkg:npm/component-a@1.0.0', + b_purl: 'pkg:npm/component-a@1.0.0', + a_version: '1.0.0', + b_version: '1.0.0', + expected: 0 + }, + 'name lesser' => { + a_name: 'component-a', + b_name: 'component-b', + a_type: 'library', + b_type: 'library', + a_purl: 'pkg:npm/component-a@1.0.0', + b_purl: 'pkg:npm/component-b@1.0.0', + a_version: '1.0.0', + b_version: '1.0.0', + expected: -1 + }, + 'name greater' => { + a_name: 'component-b', + b_name: 'component-a', + a_type: 'library', + b_type: 'library', + a_purl: 'pkg:npm/component-b@1.0.0', + b_purl: 'pkg:npm/component-a@1.0.0', + a_version: '1.0.0', + b_version: '1.0.0', + expected: 1 + }, + 'purl type lesser' => { + a_name: 'component-a', + b_name: 'component-a', + a_type: 'library', + b_type: 'library', + a_purl: 'pkg:composer/component-a@1.0.0', + b_purl: 'pkg:npm/component-a@1.0.0', + a_version: '1.0.0', + b_version: '1.0.0', + expected: -1 + }, + 'purl type greater' => { + a_name: 'component-a', + b_name: 'component-a', + a_type: 'library', + b_type: 'library', + a_purl: 'pkg:npm/component-a@1.0.0', + b_purl: 'pkg:composer/component-a@1.0.0', + a_version: '1.0.0', + b_version: '1.0.0', + expected: 1 + }, + 'purl type nulls first' => { + a_name: 'component-a', + b_name: 'component-a', + a_type: 'library', + b_type: 'library', + a_purl: nil, + b_purl: 'pkg:npm/component-a@1.0.0', + a_version: '1.0.0', + b_version: '1.0.0', + expected: -1 + }, + 'version lesser' => { + a_name: 'component-a', + b_name: 'component-a', + a_type: 'library', + b_type: 'library', + a_purl: 'pkg:npm/component-a@1.0.0', + b_purl: 'pkg:npm/component-a@1.0.0', + a_version: '1.0.0', + b_version: '2.0.0', + expected: -1 + }, + 'version greater' => { + a_name: 'component-a', + b_name: 'component-a', + a_type: 'library', + b_type: 'library', + a_purl: 'pkg:npm/component-a@1.0.0', + b_purl: 'pkg:npm/component-a@1.0.0', + a_version: '2.0.0', + b_version: '1.0.0', + expected: 1 + }, + 'version nulls first' => { + a_name: 'component-a', + b_name: 'component-a', + a_type: 'library', + b_type: 'library', + a_purl: 'pkg:npm/component-a@1.0.0', + b_purl: 'pkg:npm/component-a@1.0.0', + a_version: nil, + b_version: '1.0.0', + expected: -1 + } + } + end + + with_them do + specify do + a = described_class.new( + name: a_name, + type: a_type, + purl: a_purl, + version: a_version + ) + + b = described_class.new( + name: b_name, + type: b_type, + purl: b_purl, + version: b_version + ) + + expect(a <=> b).to eq(expected) + end + end + end + describe '#ingestible?' do subject { component.ingestible? } diff --git a/spec/lib/gitlab/ci/status/stage/factory_spec.rb b/spec/lib/gitlab/ci/status/stage/factory_spec.rb index 702341a7ea7..34e430202c9 100644 --- a/spec/lib/gitlab/ci/status/stage/factory_spec.rb +++ b/spec/lib/gitlab/ci/status/stage/factory_spec.rb @@ -62,7 +62,7 @@ RSpec.describe Gitlab::Ci::Status::Stage::Factory, feature_category: :continuous end context 'when stage has manual builds' do - Ci::HasStatus::BLOCKED_STATUS.each do |core_status| + (Ci::HasStatus::BLOCKED_STATUS + ['skipped']).each do |core_status| context "when status is #{core_status}" do let(:stage) { create(:ci_stage, pipeline: pipeline, status: core_status) } diff --git a/spec/lib/gitlab/ci/status/stage/play_manual_spec.rb b/spec/lib/gitlab/ci/status/stage/play_manual_spec.rb index e23645c106b..fc52b7bf9d4 100644 --- a/spec/lib/gitlab/ci/status/stage/play_manual_spec.rb +++ b/spec/lib/gitlab/ci/status/stage/play_manual_spec.rb @@ -48,7 +48,7 @@ RSpec.describe Gitlab::Ci::Status::Stage::PlayManual, feature_category: :continu context 'when stage is skipped' do let(:stage) { create(:ci_stage, status: :skipped) } - it { is_expected.to be_falsy } + it { is_expected.to be_truthy } end context 'when stage is manual' do diff --git a/spec/lib/gitlab/ci/tags/bulk_insert_spec.rb b/spec/lib/gitlab/ci/tags/bulk_insert_spec.rb index b72a818c16c..460ecbb05d0 100644 --- a/spec/lib/gitlab/ci/tags/bulk_insert_spec.rb +++ b/spec/lib/gitlab/ci/tags/bulk_insert_spec.rb @@ -13,7 +13,7 @@ RSpec.describe Gitlab::Ci::Tags::BulkInsert do subject(:service) { described_class.new(statuses) } describe 'gem version' do - let(:acceptable_version) { '9.0.0' } + let(:acceptable_version) { '9.0.1' } let(:error_message) do <<~MESSAGE diff --git a/spec/lib/gitlab/ci/variables/builder/pipeline_spec.rb b/spec/lib/gitlab/ci/variables/builder/pipeline_spec.rb index e5324560944..0880c556523 100644 --- a/spec/lib/gitlab/ci/variables/builder/pipeline_spec.rb +++ b/spec/lib/gitlab/ci/variables/builder/pipeline_spec.rb @@ -18,6 +18,7 @@ RSpec.describe Gitlab::Ci::Variables::Builder::Pipeline, feature_category: :secr CI_PIPELINE_IID CI_PIPELINE_SOURCE CI_PIPELINE_CREATED_AT + CI_PIPELINE_NAME CI_COMMIT_SHA CI_COMMIT_SHORT_SHA CI_COMMIT_BEFORE_SHA @@ -43,6 +44,7 @@ RSpec.describe Gitlab::Ci::Variables::Builder::Pipeline, feature_category: :secr CI_PIPELINE_IID CI_PIPELINE_SOURCE CI_PIPELINE_CREATED_AT + CI_PIPELINE_NAME CI_COMMIT_SHA CI_COMMIT_SHORT_SHA CI_COMMIT_BEFORE_SHA diff --git a/spec/lib/gitlab/ci/variables/builder_spec.rb b/spec/lib/gitlab/ci/variables/builder_spec.rb index 28c9bdc4c4b..3411426fcdb 100644 --- a/spec/lib/gitlab/ci/variables/builder_spec.rb +++ b/spec/lib/gitlab/ci/variables/builder_spec.rb @@ -111,6 +111,8 @@ RSpec.describe Gitlab::Ci::Variables::Builder, :clean_gitlab_redis_cache, featur value: pipeline.source }, { key: 'CI_PIPELINE_CREATED_AT', value: pipeline.created_at.iso8601 }, + { key: 'CI_PIPELINE_NAME', + value: pipeline.name }, { key: 'CI_COMMIT_SHA', value: job.sha }, { key: 'CI_COMMIT_SHORT_SHA', diff --git a/spec/lib/gitlab/ci/variables/downstream/expandable_variable_generator_spec.rb b/spec/lib/gitlab/ci/variables/downstream/expandable_variable_generator_spec.rb index 5b33527e06c..95d0f089f6d 100644 --- a/spec/lib/gitlab/ci/variables/downstream/expandable_variable_generator_spec.rb +++ b/spec/lib/gitlab/ci/variables/downstream/expandable_variable_generator_spec.rb @@ -7,13 +7,19 @@ RSpec.describe Gitlab::Ci::Variables::Downstream::ExpandableVariableGenerator, f Gitlab::Ci::Variables::Collection.fabricate( [ { key: 'REF1', value: 'ref 1' }, - { key: 'REF2', value: 'ref 2' } + { key: 'REF2', value: 'ref 2' }, + { key: 'NESTED_REF1', value: 'nested $REF1' } ] ) end + let(:expand_file_refs) { false } + let(:context) do - Gitlab::Ci::Variables::Downstream::Generator::Context.new(all_bridge_variables: all_bridge_variables) + Gitlab::Ci::Variables::Downstream::Generator::Context.new( + all_bridge_variables: all_bridge_variables, + expand_file_refs: expand_file_refs + ) end subject(:generator) { described_class.new(context) } @@ -34,5 +40,54 @@ RSpec.describe Gitlab::Ci::Variables::Downstream::ExpandableVariableGenerator, f expect(generator.for(var)).to match_array([{ key: 'VAR1', value: 'ref 1 ref 2 ' }]) end end + + context 'when given a variable with nested interpolation' do + it 'returns an array containing the expanded variables' do + var = Gitlab::Ci::Variables::Collection::Item.fabricate({ key: 'VAR1', value: '$REF1 $REF2 $NESTED_REF1' }) + + expect(generator.for(var)).to match_array([{ key: 'VAR1', value: 'ref 1 ref 2 nested $REF1' }]) + end + end + + context 'when given a variable with expansion on a file variable' do + let(:all_bridge_variables) do + Gitlab::Ci::Variables::Collection.fabricate( + [ + { key: 'REF1', value: 'ref 1' }, + { key: 'FILE_REF2', value: 'ref 2', file: true }, + { key: 'NESTED_REF3', value: 'ref 3 $REF1 and $FILE_REF2', file: true } + ] + ) + end + + context 'when expand_file_refs is false' do + let(:expand_file_refs) { false } + + it 'returns an array containing the unexpanded variable and the file variable dependency' do + var = { key: 'VAR1', value: '$REF1 $FILE_REF2 $FILE_REF3 $NESTED_REF3' } + var = Gitlab::Ci::Variables::Collection::Item.fabricate(var) + + expected = [ + { key: 'VAR1', value: 'ref 1 $FILE_REF2 $NESTED_REF3' }, + { key: 'FILE_REF2', value: 'ref 2', variable_type: :file }, + { key: 'NESTED_REF3', value: 'ref 3 $REF1 and $FILE_REF2', variable_type: :file } + ] + + expect(generator.for(var)).to match_array(expected) + end + end + + context 'when expand_file_refs is true' do + let(:expand_file_refs) { true } + + it 'returns an array containing the expanded variables' do + var = { key: 'VAR1', value: '$REF1 $FILE_REF2 $FILE_REF3 $NESTED_REF3' } + var = Gitlab::Ci::Variables::Collection::Item.fabricate(var) + + expected = { key: 'VAR1', value: 'ref 1 ref 2 ref 3 $REF1 and $FILE_REF2' } + expect(generator.for(var)).to contain_exactly(expected) + end + end + end end end diff --git a/spec/lib/gitlab/ci/variables/downstream/generator_spec.rb b/spec/lib/gitlab/ci/variables/downstream/generator_spec.rb index 61e8b9a8c4a..cd68b0cdf2b 100644 --- a/spec/lib/gitlab/ci/variables/downstream/generator_spec.rb +++ b/spec/lib/gitlab/ci/variables/downstream/generator_spec.rb @@ -45,6 +45,7 @@ RSpec.describe Gitlab::Ci::Variables::Downstream::Generator, feature_category: : variables: bridge_variables, forward_yaml_variables?: true, forward_pipeline_variables?: true, + expand_file_refs?: false, yaml_variables: yaml_variables, pipeline_variables: pipeline_variables, pipeline_schedule_variables: pipeline_schedule_variables @@ -81,5 +82,61 @@ RSpec.describe Gitlab::Ci::Variables::Downstream::Generator, feature_category: : expect(generator.calculate).to be_empty end + + context 'with file variable interpolation' do + let(:bridge_variables) do + Gitlab::Ci::Variables::Collection.fabricate( + [ + { key: 'REF1', value: 'ref 1' }, + { key: 'FILE_REF3', value: 'ref 3', file: true } + ] + ) + end + + let(:yaml_variables) do + [{ key: 'INTERPOLATION_VAR', value: 'interpolate $REF1 $REF2 $FILE_REF3 $FILE_REF4' }] + end + + let(:pipeline_variables) do + [{ key: 'PIPELINE_INTERPOLATION_VAR', value: 'interpolate $REF1 $REF2 $FILE_REF3 $FILE_REF4' }] + end + + let(:pipeline_schedule_variables) do + [{ key: 'PIPELINE_SCHEDULE_INTERPOLATION_VAR', value: 'interpolate $REF1 $REF2 $FILE_REF3 $FILE_REF4' }] + end + + context 'when expand_file_refs is true' do + before do + allow(bridge).to receive(:expand_file_refs?).and_return(true) + end + + it 'expands file variables' do + expected = [ + { key: 'INTERPOLATION_VAR', value: 'interpolate ref 1 ref 3 ' }, + { key: 'PIPELINE_INTERPOLATION_VAR', value: 'interpolate ref 1 ref 3 ' }, + { key: 'PIPELINE_SCHEDULE_INTERPOLATION_VAR', value: 'interpolate ref 1 ref 3 ' } + ] + + expect(generator.calculate).to contain_exactly(*expected) + end + end + + context 'when expand_file_refs is false' do + before do + allow(bridge).to receive(:expand_file_refs?).and_return(false) + end + + it 'does not expand file variables and adds file variables' do + expected = [ + { key: 'INTERPOLATION_VAR', value: 'interpolate ref 1 $FILE_REF3 ' }, + { key: 'PIPELINE_INTERPOLATION_VAR', value: 'interpolate ref 1 $FILE_REF3 ' }, + { key: 'PIPELINE_SCHEDULE_INTERPOLATION_VAR', value: 'interpolate ref 1 $FILE_REF3 ' }, + { key: 'FILE_REF3', value: 'ref 3', variable_type: :file } + ] + + expect(generator.calculate).to contain_exactly(*expected) + end + end + end end end diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb index c4e27d0e420..f8f1d71e773 100644 --- a/spec/lib/gitlab/ci/yaml_processor_spec.rb +++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb @@ -2675,6 +2675,42 @@ module Gitlab it_behaves_like 'returns errors', 'jobs:test1 dependencies should be an array of strings' end + + context 'needs with parallel:matrix' do + let(:config) do + { + build1: { + stage: 'build', + script: 'build', + parallel: { matrix: [{ 'PROVIDER': ['aws'], 'STACK': %w[monitoring app1 app2] }] } + }, + test1: { + stage: 'test', + script: 'test', + needs: [{ job: 'build1', parallel: { matrix: [{ 'PROVIDER': ['aws'], 'STACK': ['app1'] }] } }] + } + } + end + + it "does create jobs with valid specification" do + expect(subject.builds.size).to eq(4) + expect(subject.builds[3]).to eq( + stage: "test", + stage_idx: 2, + name: "test1", + only: { refs: %w[branches tags] }, + options: { script: ["test"] }, + needs_attributes: [ + { name: "build1: [aws, app1]", artifacts: true, optional: false } + ], + when: "on_success", + allow_failure: false, + job_variables: [], + root_variables_inheritance: true, + scheduling_type: :dag + ) + end + end end context 'with when/rules' do |