diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-12-20 14:22:11 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-12-20 14:22:11 +0000 |
commit | 0c872e02b2c822e3397515ec324051ff540f0cd5 (patch) | |
tree | ce2fb6ce7030e4dad0f4118d21ab6453e5938cdd /spec/lib/gitlab/ci | |
parent | f7e05a6853b12f02911494c4b3fe53d9540d74fc (diff) | |
download | gitlab-ce-0c872e02b2c822e3397515ec324051ff540f0cd5.tar.gz |
Add latest changes from gitlab-org/gitlab@15-7-stable-eev15.7.0-rc42
Diffstat (limited to 'spec/lib/gitlab/ci')
53 files changed, 2188 insertions, 1238 deletions
diff --git a/spec/lib/gitlab/ci/build/cache_spec.rb b/spec/lib/gitlab/ci/build/cache_spec.rb index 7477aedb994..a8fa14b4b4c 100644 --- a/spec/lib/gitlab/ci/build/cache_spec.rb +++ b/spec/lib/gitlab/ci/build/cache_spec.rb @@ -14,8 +14,8 @@ RSpec.describe Gitlab::Ci::Build::Cache do cache = described_class.new(cache_config, pipeline) - expect(Gitlab::Ci::Pipeline::Seed::Build::Cache).to have_received(:new).with(pipeline, { key: 'key-a' }) - expect(Gitlab::Ci::Pipeline::Seed::Build::Cache).to have_received(:new).with(pipeline, { key: 'key-b' }) + expect(Gitlab::Ci::Pipeline::Seed::Build::Cache).to have_received(:new).with(pipeline, { key: 'key-a' }, 0) + expect(Gitlab::Ci::Pipeline::Seed::Build::Cache).to have_received(:new).with(pipeline, { key: 'key-b' }, 1) expect(cache.instance_variable_get(:@cache)).to eq([cache_seed_a, cache_seed_b]) end end @@ -29,7 +29,7 @@ RSpec.describe Gitlab::Ci::Build::Cache do cache = described_class.new(cache_config, pipeline) - expect(Gitlab::Ci::Pipeline::Seed::Build::Cache).to have_received(:new).with(pipeline, cache_config) + expect(Gitlab::Ci::Pipeline::Seed::Build::Cache).to have_received(:new).with(pipeline, cache_config, 0) expect(cache.instance_variable_get(:@cache)).to eq([cache_seed]) end end diff --git a/spec/lib/gitlab/ci/build/context/build_spec.rb b/spec/lib/gitlab/ci/build/context/build_spec.rb index 7f862a3b80a..74739a67be0 100644 --- a/spec/lib/gitlab/ci/build/context/build_spec.rb +++ b/spec/lib/gitlab/ci/build/context/build_spec.rb @@ -2,11 +2,11 @@ require 'spec_helper' -RSpec.describe Gitlab::Ci::Build::Context::Build do +RSpec.describe Gitlab::Ci::Build::Context::Build, feature_category: :pipeline_authoring do let(:pipeline) { create(:ci_pipeline) } let(:seed_attributes) { { 'name' => 'some-job' } } - let(:context) { described_class.new(pipeline, seed_attributes) } + subject(:context) { described_class.new(pipeline, seed_attributes) } shared_examples 'variables collection' do it { is_expected.to include('CI_COMMIT_REF_NAME' => 'master') } @@ -22,6 +22,12 @@ RSpec.describe Gitlab::Ci::Build::Context::Build do it { is_expected.to include('CI_BUILD_REF_NAME' => 'master') } it { is_expected.to include('CI_PROJECT_PATH' => pipeline.project.full_path) } end + + context 'when environment:name is provided' do + let(:seed_attributes) { { 'name' => 'some-job', 'environment' => 'test' } } + + it { is_expected.to include('CI_ENVIRONMENT_NAME' => 'test') } + end end describe '#variables' do diff --git a/spec/lib/gitlab/ci/build/hook_spec.rb b/spec/lib/gitlab/ci/build/hook_spec.rb new file mode 100644 index 00000000000..6ed40a44c97 --- /dev/null +++ b/spec/lib/gitlab/ci/build/hook_spec.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Build::Hook, feature_category: :pipeline_authoring do + let_it_be(:build1) do + FactoryBot.build(:ci_build, + options: { hooks: { pre_get_sources_script: ["echo 'hello pre_get_sources_script'"] } }) + end + + describe '.from_hooks' do + subject(:from_hooks) { described_class.from_hooks(build1) } + + it 'initializes and returns hooks' do + expect(from_hooks.size).to eq(1) + expect(from_hooks[0].name).to eq('pre_get_sources_script') + expect(from_hooks[0].script).to eq(["echo 'hello pre_get_sources_script'"]) + end + end +end diff --git a/spec/lib/gitlab/ci/config/entry/artifacts_spec.rb b/spec/lib/gitlab/ci/config/entry/artifacts_spec.rb index 7476fc6c25f..6264e0c8e33 100644 --- a/spec/lib/gitlab/ci/config/entry/artifacts_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/artifacts_spec.rb @@ -142,6 +142,26 @@ RSpec.describe Gitlab::Ci::Config::Entry::Artifacts do end end + context 'when the `when` keyword is not a string' do + context 'when it is an array' do + let(:config) { { paths: %w[results.txt], when: ['always'] } } + + it 'returns error' do + expect(entry).not_to be_valid + expect(entry.errors).to include 'artifacts when should be a string' + end + end + + context 'when it is a boolean' do + let(:config) { { paths: %w[results.txt], when: true } } + + it 'returns error' do + expect(entry).not_to be_valid + expect(entry.errors).to include 'artifacts when should be a string' + end + end + end + describe 'excluded artifacts' do context 'when configuration is valid' do let(:config) { { untracked: true, exclude: ['some/directory/'] } } diff --git a/spec/lib/gitlab/ci/config/entry/bridge_spec.rb b/spec/lib/gitlab/ci/config/entry/bridge_spec.rb index 8da46561b73..736c184a289 100644 --- a/spec/lib/gitlab/ci/config/entry/bridge_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/bridge_spec.rb @@ -13,7 +13,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Bridge do # that we know that we don't want to inherit # as they do not have sense in context of Bridge let(:ignored_inheritable_columns) do - %i[before_script after_script image services cache interruptible timeout + %i[before_script after_script hooks image services cache interruptible timeout retry tags artifacts] end end diff --git a/spec/lib/gitlab/ci/config/entry/cache_spec.rb b/spec/lib/gitlab/ci/config/entry/cache_spec.rb index 247f4b63910..414cbb169b9 100644 --- a/spec/lib/gitlab/ci/config/entry/cache_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/cache_spec.rb @@ -163,22 +163,6 @@ RSpec.describe Gitlab::Ci::Config::Entry::Cache do end end - context 'when policy is unknown' do - let(:config) { { policy: 'unknown' } } - - it 'reports error' do - is_expected.to include('cache policy should be pull-push, push, or pull') - end - end - - context 'when `when` is unknown' do - let(:config) { { when: 'unknown' } } - - it 'reports error' do - is_expected.to include('cache when should be on_success, on_failure or always') - end - end - context 'when descendants are invalid' do context 'with invalid keys' do let(:config) { { key: 1 } } @@ -228,6 +212,62 @@ RSpec.describe Gitlab::Ci::Config::Entry::Cache do is_expected.to include 'cache config contains unknown keys: invalid' end end + + context 'when the `when` keyword is not a valid string' do + context 'when `when` is unknown' do + let(:config) { { when: 'unknown' } } + + it 'returns error' do + is_expected.to include('cache when should be one of: on_success, on_failure, always') + end + end + + context 'when it is an array' do + let(:config) { { when: ['always'] } } + + it 'returns error' do + expect(entry).not_to be_valid + is_expected.to include('cache when should be a string') + end + end + + context 'when it is a boolean' do + let(:config) { { when: true } } + + it 'returns error' do + expect(entry).not_to be_valid + is_expected.to include('cache when should be a string') + end + end + end + + context 'when the `policy` keyword is not a valid string' do + context 'when `policy` is unknown' do + let(:config) { { policy: 'unknown' } } + + it 'returns error' do + is_expected.to include('cache policy should be one of: pull-push, push, pull') + end + end + + context 'when it is an array' do + let(:config) { { policy: ['pull-push'] } } + + it 'returns error' do + expect(entry).not_to be_valid + is_expected.to include('cache policy should be a string') + end + end + + context 'when it is a boolean' do + let(:config) { { policy: true } } + + it 'returns error' do + expect(entry).not_to be_valid + is_expected.to include('cache policy should be a string') + end + end + end end end end diff --git a/spec/lib/gitlab/ci/config/entry/default_spec.rb b/spec/lib/gitlab/ci/config/entry/default_spec.rb index 5613b0f09d1..46e96843ee3 100644 --- a/spec/lib/gitlab/ci/config/entry/default_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/default_spec.rb @@ -26,9 +26,8 @@ RSpec.describe Gitlab::Ci::Config::Entry::Default do context 'when filtering all the entry/node names' do it 'contains the expected node names' do expect(described_class.nodes.keys) - .to match_array(%i[before_script image services - after_script cache interruptible - timeout retry tags artifacts]) + .to match_array(%i[before_script after_script hooks cache image services + interruptible timeout retry tags artifacts]) end end end diff --git a/spec/lib/gitlab/ci/config/entry/hooks_spec.rb b/spec/lib/gitlab/ci/config/entry/hooks_spec.rb new file mode 100644 index 00000000000..7a5ff244e18 --- /dev/null +++ b/spec/lib/gitlab/ci/config/entry/hooks_spec.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +RSpec.describe Gitlab::Ci::Config::Entry::Hooks do + subject(:entry) { described_class.new(config) } + + before do + entry.compose! + end + + describe 'validations' do + context 'when passing a valid hook' do + let(:config) { { pre_get_sources_script: ['ls'] } } + + it { is_expected.to be_valid } + end + + context 'when passing an invalid hook' do + let(:config) { { x_get_something: ['ls'] } } + + it { is_expected.not_to be_valid } + end + + context 'when entry config is not a hash' do + let(:config) { 'ls' } + + it { is_expected.not_to be_valid } + end + end + + describe '#value' do + let(:config) { { pre_get_sources_script: ['ls'] } } + + it 'returns a hash' do + expect(entry.value).to eq(config) + end + end +end diff --git a/spec/lib/gitlab/ci/config/entry/id_token_spec.rb b/spec/lib/gitlab/ci/config/entry/id_token_spec.rb new file mode 100644 index 00000000000..12585d662ec --- /dev/null +++ b/spec/lib/gitlab/ci/config/entry/id_token_spec.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Config::Entry::IdToken do + context 'when given `aud` as a string' do + it 'is valid' do + config = { aud: 'https://gitlab.com' } + id_token = described_class.new(config) + + id_token.compose! + + expect(id_token).to be_valid + expect(id_token.value).to eq(aud: 'https://gitlab.com') + end + end + + context 'when given `aud` as an array' do + it 'is valid and concatenates the values' do + config = { aud: ['https://gitlab.com', 'https://aws.com'] } + id_token = described_class.new(config) + + id_token.compose! + + expect(id_token).to be_valid + expect(id_token.value).to eq(aud: ['https://gitlab.com', 'https://aws.com']) + end + end + + context 'when not given an `aud`' do + it 'is invalid' do + config = {} + id_token = described_class.new(config) + + id_token.compose! + + expect(id_token).not_to be_valid + expect(id_token.errors).to match_array([ + 'id token config missing required keys: aud', + 'id token aud should be an array of strings or a string' + ]) + end + end + + context 'when given an unknown keyword' do + it 'is invalid' do + config = { aud: 'https://gitlab.com', unknown: 'test' } + id_token = described_class.new(config) + + id_token.compose! + + expect(id_token).not_to be_valid + expect(id_token.errors).to match_array([ + 'id token config contains unknown keys: unknown' + ]) + end + end +end diff --git a/spec/lib/gitlab/ci/config/entry/job_spec.rb b/spec/lib/gitlab/ci/config/entry/job_spec.rb index acf60a6cdda..becb46ac2e7 100644 --- a/spec/lib/gitlab/ci/config/entry/job_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/job_spec.rb @@ -27,7 +27,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Job do subject { described_class.nodes.keys } let(:result) do - %i[before_script script stage after_script cache + %i[before_script script after_script hooks stage cache image services only except rules needs variables artifacts environment coverage retry interruptible timeout release tags inherit parallel] @@ -716,7 +716,9 @@ RSpec.describe Gitlab::Ci::Config::Entry::Job do let(:config) do { before_script: %w[ls pwd], script: 'rspec', - after_script: %w[cleanup] } + after_script: %w[cleanup], + id_tokens: { TEST_ID_TOKEN: { aud: 'https://gitlab.com' } }, + hooks: { pre_get_sources_script: 'echo hello' } } end it 'returns correct value' do @@ -727,10 +729,33 @@ RSpec.describe Gitlab::Ci::Config::Entry::Job do stage: 'test', ignore: false, after_script: %w[cleanup], + hooks: { pre_get_sources_script: ['echo hello'] }, only: { refs: %w[branches tags] }, job_variables: {}, root_variables_inheritance: true, - scheduling_type: :stage) + scheduling_type: :stage, + id_tokens: { TEST_ID_TOKEN: { aud: 'https://gitlab.com' } }) + end + + context 'when the FF ci_hooks_pre_get_sources_script is disabled' do + before do + stub_feature_flags(ci_hooks_pre_get_sources_script: false) + end + + it 'returns correct value' do + expect(entry.value) + .to eq(name: :rspec, + before_script: %w[ls pwd], + script: %w[rspec], + stage: 'test', + ignore: false, + after_script: %w[cleanup], + only: { refs: %w[branches tags] }, + job_variables: {}, + root_variables_inheritance: true, + scheduling_type: :stage, + id_tokens: { TEST_ID_TOKEN: { aud: 'https://gitlab.com' } }) + end end end end diff --git a/spec/lib/gitlab/ci/config/entry/root_spec.rb b/spec/lib/gitlab/ci/config/entry/root_spec.rb index 085293d7368..c40589104cd 100644 --- a/spec/lib/gitlab/ci/config/entry/root_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/root_spec.rb @@ -5,7 +5,8 @@ require 'spec_helper' RSpec.describe Gitlab::Ci::Config::Entry::Root do let(:user) {} let(:project) {} - let(:root) { described_class.new(hash, user: user, project: project) } + let(:logger) { Gitlab::Ci::Pipeline::Logger.new(project: project) } + let(:root) { described_class.new(hash, user: user, project: project, logger: logger) } describe '.nodes' do it 'returns a hash' do @@ -37,7 +38,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Root do variables: { VAR: 'root', VAR2: { value: 'val 2', description: 'this is var 2' }, - VAR3: { value: %w[val3 val3b], description: 'this is var 3' } + VAR3: { value: 'val3', options: %w[val3 val4 val5], description: 'this is var 3 and some options' } }, after_script: ['make clean'], stages: %w(build pages release), @@ -228,6 +229,14 @@ RSpec.describe Gitlab::Ci::Config::Entry::Root do ) end end + + it 'tracks log entries' do + expect(logger.observations_hash).to match( + a_hash_including( + 'config_root_compose_jobs_factory_duration_s' => a_kind_of(Numeric) + ) + ) + end end end @@ -317,6 +326,42 @@ RSpec.describe Gitlab::Ci::Config::Entry::Root do end end + context 'when variables have `options` data' do + before do + root.compose! + end + + context 'and the value is in the `options` array' do + let(:hash) do + { + variables: { 'VAR' => { value: 'val1', options: %w[val1 val2] } }, + rspec: { script: 'bin/rspec' } + } + end + + it 'returns correct value' do + expect(root.variables_entry.value_with_data).to eq( + 'VAR' => { value: 'val1' } + ) + + expect(root.variables_value).to eq('VAR' => 'val1') + end + end + + context 'and the value is not in the `options` array' do + let(:hash) do + { + variables: { 'VAR' => { value: 'val', options: %w[val1 val2] } }, + rspec: { script: 'bin/rspec' } + } + end + + it 'returns an error' do + expect(root.errors).to contain_exactly('variables:var config value must be present in options') + end + end + end + context 'when variables have "expand" data' do let(:hash) do { diff --git a/spec/lib/gitlab/ci/config/entry/trigger_spec.rb b/spec/lib/gitlab/ci/config/entry/trigger_spec.rb index d0116c961d7..f47923af45a 100644 --- a/spec/lib/gitlab/ci/config/entry/trigger_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/trigger_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::Ci::Config::Entry::Trigger do +RSpec.describe Gitlab::Ci::Config::Entry::Trigger, feature_category: :pipeline_authoring do subject { described_class.new(config) } context 'when trigger config is a non-empty string' do @@ -35,6 +35,48 @@ RSpec.describe Gitlab::Ci::Config::Entry::Trigger do end context 'when trigger is a hash - cross-project' do + context 'when project is a string' do + context 'when project is a non-empty string' do + let(:config) { { project: 'some/project' } } + + it 'is valid' do + expect(subject).to be_valid + end + end + + context 'when project is an empty string' do + let(:config) { { project: '' } } + + it 'returns error' do + expect(subject).not_to be_valid + expect(subject.errors.first) + .to match /project can't be blank/ + end + end + end + + context 'when project is not a string' do + context 'when project is an array' do + let(:config) { { project: ['some/project'] } } + + it 'returns error' do + expect(subject).not_to be_valid + expect(subject.errors.first) + .to match /should be a string/ + end + end + + context 'when project is a boolean' do + let(:config) { { project: true } } + + it 'returns error' do + expect(subject).not_to be_valid + expect(subject.errors.first) + .to match /should be a string/ + end + end + end + context 'when branch is provided' do let(:config) { { project: 'some/project', branch: 'feature' } } diff --git a/spec/lib/gitlab/ci/config/entry/variable_spec.rb b/spec/lib/gitlab/ci/config/entry/variable_spec.rb index d7023072312..97b06c8b1a5 100644 --- a/spec/lib/gitlab/ci/config/entry/variable_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/variable_spec.rb @@ -306,48 +306,48 @@ RSpec.describe Gitlab::Ci::Config::Entry::Variable do end end end - end - describe 'ComplexArrayVariable' do - context 'when allow_array_value metadata is false' do - let(:config) { { value: %w[value value2], description: 'description' } } - let(:metadata) { { allow_array_value: false } } + context 'when config is a hash with options' do + context 'when there is no metadata' do + let(:config) { { value: 'value', options: %w[value value2] } } + let(:metadata) { {} } - describe '#valid?' do - it { is_expected.not_to be_valid } - end + describe '#valid?' do + it { is_expected.not_to be_valid } + end - describe '#errors' do - subject(:errors) { entry.errors } + describe '#errors' do + subject(:errors) { entry.errors } - it { is_expected.to include 'var1 config value must be an alphanumeric string' } + it { is_expected.to include 'var1 config must be a string' } + end end - end - context 'when allow_array_value metadata is true' do - let(:config) { { value: %w[value value2], description: 'description' } } - let(:metadata) { { allowed_value_data: %i[value description], allow_array_value: true } } + context 'when options are allowed' do + let(:config) { { value: 'value', options: %w[value value2] } } + let(:metadata) { { allowed_value_data: %i[value options] } } - describe '#valid?' do - it { is_expected.to be_valid } - end + describe '#valid?' do + it { is_expected.to be_valid } + end - describe '#value' do - subject(:value) { entry.value } + describe '#value' do + subject(:value) { entry.value } - it { is_expected.to eq('value') } - end + it { is_expected.to eq('value') } + end - describe '#value_with_data' do - subject(:value_with_data) { entry.value_with_data } + describe '#value_with_data' do + subject(:value_with_data) { entry.value_with_data } - it { is_expected.to eq(value: 'value') } - end + it { is_expected.to eq(value: 'value') } + end - describe '#value_with_prefill_data' do - subject(:value_with_prefill_data) { entry.value_with_prefill_data } + describe '#value_with_prefill_data' do + subject(:value_with_prefill_data) { entry.value_with_prefill_data } - it { is_expected.to eq(value: 'value', description: 'description', value_options: %w[value value2]) } + it { is_expected.to eq(value: 'value', options: %w[value value2]) } + end end end end diff --git a/spec/lib/gitlab/ci/config/entry/variables_spec.rb b/spec/lib/gitlab/ci/config/entry/variables_spec.rb index 609e4422d5c..e7dbc78729d 100644 --- a/spec/lib/gitlab/ci/config/entry/variables_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/variables_spec.rb @@ -116,8 +116,8 @@ RSpec.describe Gitlab::Ci::Config::Entry::Variables do it_behaves_like 'invalid config', /variable_1 config must be a string/ end - context 'when metadata has allow_array_value and allowed_value_data' do - let(:metadata) { { allowed_value_data: %i[value description], allow_array_value: true } } + context 'when metadata has the allowed_value_data key' do + let(:metadata) { { allowed_value_data: %i[value description options] } } let(:result) do { 'VARIABLE_1' => 'value' } @@ -143,17 +143,15 @@ RSpec.describe Gitlab::Ci::Config::Entry::Variables do end end - context 'when entry config value has key-value pair and value is an array' do + context 'when entry config value has options' do let(:config) do - { 'VARIABLE_1' => { value: %w[value1 value2], description: 'variable 1' } } + { 'VARIABLE_1' => { + value: 'value1', options: %w[value1 value2], description: 'variable 1' + } } end - context 'when there is no allowed_value_data metadata' do - it_behaves_like 'invalid config', /variable_1 config value must be an alphanumeric string/ - end - - context 'when metadata has allow_array_value and allowed_value_data' do - let(:metadata) { { allowed_value_data: %i[value description], allow_array_value: true } } + context 'when metadata has allowed_value_data' do + let(:metadata) { { allowed_value_data: %i[value description options] } } let(:result) do { 'VARIABLE_1' => 'value1' } @@ -172,7 +170,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Variables do describe '#value_with_prefill_data' do it 'returns variable with prefill data' do expect(entry.value_with_prefill_data).to eq( - 'VARIABLE_1' => { value: 'value1', value_options: %w[value1 value2], description: 'variable 1' } + 'VARIABLE_1' => { value: 'value1', options: %w[value1 value2], description: 'variable 1' } ) end end @@ -234,14 +232,6 @@ RSpec.describe Gitlab::Ci::Config::Entry::Variables do it_behaves_like 'invalid config', /variable_1 config uses invalid data keys: hello/ end - context 'when entry config value has hash with nil description' do - let(:config) do - { 'VARIABLE_1' => { value: 'value 1', description: nil } } - end - - it_behaves_like 'invalid config', /variable_1 config description must be an alphanumeric string/ - end - context 'when entry config value has hash without description' do let(:config) do { 'VARIABLE_1' => { value: 'value 1' } } diff --git a/spec/lib/gitlab/ci/config/external/file/remote_spec.rb b/spec/lib/gitlab/ci/config/external/file/remote_spec.rb index c22afb32756..8d93cdcf378 100644 --- a/spec/lib/gitlab/ci/config/external/file/remote_spec.rb +++ b/spec/lib/gitlab/ci/config/external/file/remote_spec.rb @@ -188,6 +188,19 @@ RSpec.describe Gitlab::Ci::Config::External::File::Remote do 'is blocked: Requests to localhost are not allowed!' end end + + context 'when connection refused error has been raised' do + let(:location) { 'http://127.0.0.1/some/path/to/config.yaml' } + let(:exception) { Errno::ECONNREFUSED.new } + + before do + stub_full_request(location).to_raise(exception) + end + + it 'returns details about connection failure' do + expect(subject).to eq "Remote file could not be fetched because Connection refused!" + end + end end describe '#expand_context' do diff --git a/spec/lib/gitlab/ci/config/external/mapper/base_spec.rb b/spec/lib/gitlab/ci/config/external/mapper/base_spec.rb new file mode 100644 index 00000000000..0fdcc5e8ff7 --- /dev/null +++ b/spec/lib/gitlab/ci/config/external/mapper/base_spec.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Config::External::Mapper::Base, feature_category: :pipeline_authoring do + let(:test_class) do + Class.new(described_class) do + def self.name + 'TestClass' + end + end + end + + let(:context) { Gitlab::Ci::Config::External::Context.new } + let(:mapper) { test_class.new(context) } + + describe '#process' do + subject(:process) { mapper.process } + + context 'when the method is not implemented' do + it 'raises NotImplementedError' do + expect { process }.to raise_error(NotImplementedError) + end + end + + context 'when the method is implemented' do + before do + test_class.class_eval do + def process_without_instrumentation + 'test' + end + end + end + + it 'calls the method' do + expect(process).to eq('test') + end + end + end +end diff --git a/spec/lib/gitlab/ci/config/external/mapper/filter_spec.rb b/spec/lib/gitlab/ci/config/external/mapper/filter_spec.rb new file mode 100644 index 00000000000..df2a2f0fd01 --- /dev/null +++ b/spec/lib/gitlab/ci/config/external/mapper/filter_spec.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Config::External::Mapper::Filter, feature_category: :pipeline_authoring do + let_it_be(:variables) do + Gitlab::Ci::Variables::Collection.new.tap do |variables| + variables.append(key: 'VARIABLE1', value: 'hello') + end + end + + let_it_be(:context) do + Gitlab::Ci::Config::External::Context.new(variables: variables) + end + + subject(:filter) { described_class.new(context) } + + describe '#process' do + let(:locations) do + [{ local: 'config/.gitlab-ci.yml', rules: [{ if: '$VARIABLE1' }] }, + { remote: 'https://example.com/.gitlab-ci.yml', rules: [{ if: '$VARIABLE2' }] }] + end + + subject(:process) { filter.process(locations) } + + it 'filters locations according to rules' do + is_expected.to eq( + [{ local: 'config/.gitlab-ci.yml', rules: [{ if: '$VARIABLE1' }] }] + ) + end + end +end diff --git a/spec/lib/gitlab/ci/config/external/mapper/location_expander_spec.rb b/spec/lib/gitlab/ci/config/external/mapper/location_expander_spec.rb new file mode 100644 index 00000000000..b14b6b0ca29 --- /dev/null +++ b/spec/lib/gitlab/ci/config/external/mapper/location_expander_spec.rb @@ -0,0 +1,72 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Config::External::Mapper::LocationExpander, feature_category: :pipeline_authoring do + include RepoHelpers + + let_it_be(:project) { create(:project, :repository) } + let_it_be(:user) { project.owner } + + let(:sha) { project.commit.sha } + + let(:context) do + Gitlab::Ci::Config::External::Context.new(project: project, user: user, sha: sha) + end + + subject(:location_expander) { described_class.new(context) } + + describe '#process' do + subject(:process) { location_expander.process(locations) } + + context 'when there are project files' do + let(:locations) do + [{ project: 'gitlab-org/gitlab-1', file: ['builds.yml', 'tests.yml'] }, + { project: 'gitlab-org/gitlab-2', file: 'deploy.yml' }] + end + + it 'returns expanded locations' do + is_expected.to eq( + [{ project: 'gitlab-org/gitlab-1', file: 'builds.yml' }, + { project: 'gitlab-org/gitlab-1', file: 'tests.yml' }, + { project: 'gitlab-org/gitlab-2', file: 'deploy.yml' }] + ) + end + end + + context 'when there are local files' do + let(:locations) do + [{ local: 'builds/*.yml' }, + { local: 'tests.yml' }] + end + + let(:project_files) do + { 'builds/1.yml' => 'a', 'builds/2.yml' => 'b', 'tests.yml' => 'c' } + end + + around do |example| + create_and_delete_files(project, project_files) do + example.run + end + end + + it 'returns expanded locations' do + is_expected.to eq( + [{ local: 'builds/1.yml' }, + { local: 'builds/2.yml' }, + { local: 'tests.yml' }] + ) + end + end + + context 'when there are other files' do + let(:locations) do + [{ remote: 'https://gitlab.com/gitlab-org/gitlab-ce/raw/master/.gitlab-ci.yml' }] + end + + it 'returns the same location' do + is_expected.to eq(locations) + end + end + end +end diff --git a/spec/lib/gitlab/ci/config/external/mapper/matcher_spec.rb b/spec/lib/gitlab/ci/config/external/mapper/matcher_spec.rb new file mode 100644 index 00000000000..5f321a696c9 --- /dev/null +++ b/spec/lib/gitlab/ci/config/external/mapper/matcher_spec.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Config::External::Mapper::Matcher, feature_category: :pipeline_authoring do + let_it_be(:variables) do + Gitlab::Ci::Variables::Collection.new.tap do |variables| + variables.append(key: 'A_MASKED_VAR', value: 'this-is-secret', masked: true) + end + end + + let_it_be(:context) do + Gitlab::Ci::Config::External::Context.new(variables: variables) + end + + subject(:matcher) { described_class.new(context) } + + describe '#process' do + 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 + + subject(:process) { matcher.process(locations) } + + 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 + + context 'when a location is not valid' do + let(:locations) { [{ invalid: 'file.yml' }] } + + it 'raises an error' do + expect { process }.to raise_error( + Gitlab::Ci::Config::External::Mapper::AmbigiousSpecificationError, + '`{"invalid":"file.yml"}` does not have a valid subkey for include. ' \ + 'Valid subkeys are: `local`, `project`, `remote`, `template`, `artifact`' + ) + end + + context 'when the invalid location includes a masked variable' do + let(:locations) { [{ invalid: 'this-is-secret.yml' }] } + + it 'raises an error with a masked sentence' do + expect { process }.to raise_error( + Gitlab::Ci::Config::External::Mapper::AmbigiousSpecificationError, + '`{"invalid":"xxxxxxxxxxxxxx.yml"}` does not have a valid subkey for include. ' \ + 'Valid subkeys are: `local`, `project`, `remote`, `template`, `artifact`' + ) + end + end + end + + context 'when a location is ambiguous' do + let(:locations) { [{ local: 'file.yml', remote: 'https://example.com/.gitlab-ci.yml' }] } + + it 'raises an error' do + expect { process }.to raise_error( + Gitlab::Ci::Config::External::Mapper::AmbigiousSpecificationError, + "Each include must use only one of: `local`, `project`, `remote`, `template`, `artifact`" + ) + end + end + end +end diff --git a/spec/lib/gitlab/ci/config/external/mapper/normalizer_spec.rb b/spec/lib/gitlab/ci/config/external/mapper/normalizer_spec.rb new file mode 100644 index 00000000000..709c234253b --- /dev/null +++ b/spec/lib/gitlab/ci/config/external/mapper/normalizer_spec.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Config::External::Mapper::Normalizer, feature_category: :pipeline_authoring do + let_it_be(:variables) do + Gitlab::Ci::Variables::Collection.new.tap do |variables| + variables.append(key: 'VARIABLE1', value: 'config') + variables.append(key: 'VARIABLE2', value: 'https://example.com') + end + end + + let_it_be(:context) do + Gitlab::Ci::Config::External::Context.new(variables: variables) + end + + subject(:normalizer) { described_class.new(context) } + + describe '#process' do + let(:locations) do + ['https://example.com/.gitlab-ci.yml', + 'config/.gitlab-ci.yml', + { local: 'config/.gitlab-ci.yml' }, + { remote: 'https://example.com/.gitlab-ci.yml' }, + { template: 'Template.gitlab-ci.yml' }, + '$VARIABLE1/.gitlab-ci.yml', + '$VARIABLE2/.gitlab-ci.yml'] + end + + subject(:process) { normalizer.process(locations) } + + it 'converts locations to canonical form' do + is_expected.to eq( + [{ remote: 'https://example.com/.gitlab-ci.yml' }, + { local: 'config/.gitlab-ci.yml' }, + { local: 'config/.gitlab-ci.yml' }, + { remote: 'https://example.com/.gitlab-ci.yml' }, + { template: 'Template.gitlab-ci.yml' }, + { local: 'config/.gitlab-ci.yml' }, + { remote: 'https://example.com/.gitlab-ci.yml' }] + ) + end + end +end diff --git a/spec/lib/gitlab/ci/config/external/mapper/variables_expander_spec.rb b/spec/lib/gitlab/ci/config/external/mapper/variables_expander_spec.rb new file mode 100644 index 00000000000..f7454dcd4be --- /dev/null +++ b/spec/lib/gitlab/ci/config/external/mapper/variables_expander_spec.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Config::External::Mapper::VariablesExpander, feature_category: :pipeline_authoring do + let_it_be(:variables) do + Gitlab::Ci::Variables::Collection.new.tap do |variables| + variables.append(key: 'VARIABLE1', value: 'hello') + end + end + + let_it_be(:context) do + Gitlab::Ci::Config::External::Context.new(variables: variables) + end + + subject(:variables_expander) { described_class.new(context) } + + describe '#process' do + subject(:process) { variables_expander.process(locations) } + + context 'when locations are strings' do + let(:locations) { ['$VARIABLE1.gitlab-ci.yml'] } + + it 'expands variables' do + is_expected.to eq(['hello.gitlab-ci.yml']) + end + end + + context 'when locations are hashes' do + let(:locations) { [{ local: '$VARIABLE1.gitlab-ci.yml' }] } + + it 'expands variables' do + is_expected.to eq([{ local: 'hello.gitlab-ci.yml' }]) + end + end + + context 'when locations are arrays' do + let(:locations) { [{ local: ['$VARIABLE1.gitlab-ci.yml'] }] } + + it 'expands variables' do + is_expected.to eq([{ local: ['hello.gitlab-ci.yml'] }]) + end + end + end +end diff --git a/spec/lib/gitlab/ci/config/external/mapper/verifier_spec.rb b/spec/lib/gitlab/ci/config/external/mapper/verifier_spec.rb new file mode 100644 index 00000000000..7c7252c6b0e --- /dev/null +++ b/spec/lib/gitlab/ci/config/external/mapper/verifier_spec.rb @@ -0,0 +1,137 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Config::External::Mapper::Verifier, feature_category: :pipeline_authoring do + include RepoHelpers + include StubRequests + + let_it_be(:project) { create(:project, :repository) } + let_it_be(:user) { project.owner } + + let(:context) do + Gitlab::Ci::Config::External::Context.new(project: project, user: user, sha: project.commit.id) + end + + let(:remote_url) { 'https://gitlab.com/gitlab-org/gitlab-foss/blob/1234/.gitlab-ci-1.yml' } + + let(:project_files) do + { + 'myfolder/file1.yml' => <<~YAML, + my_build: + script: echo Hello World + YAML + 'myfolder/file2.yml' => <<~YAML, + my_test: + script: echo Hello World + YAML + 'nested_configs.yml' => <<~YAML + include: + - local: myfolder/file1.yml + - local: myfolder/file2.yml + - remote: #{remote_url} + YAML + } + end + + around(:all) do |example| + create_and_delete_files(project, project_files) do + example.run + end + end + + before do + stub_full_request(remote_url).to_return( + body: <<~YAML + remote_test: + script: echo Hello World + YAML + ) + end + + subject(:verifier) { described_class.new(context) } + + describe '#process' do + subject(:process) { verifier.process(files) } + + context 'when files are local' 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 + + it 'returns an array of file objects' do + expect(process.map(&:location)).to contain_exactly('myfolder/file1.yml', 'myfolder/file2.yml') + end + + it 'adds files to the expandset' do + expect { process }.to change { context.expandset.count }.by(2) + end + end + + context 'when a file includes other files' do + let(:files) do + [ + Gitlab::Ci::Config::External::File::Local.new({ local: 'nested_configs.yml' }, context) + ] + end + + it 'returns an array of file objects with combined hash' do + expect(process.map(&:to_hash)).to contain_exactly( + { my_build: { script: 'echo Hello World' }, + my_test: { script: 'echo Hello World' }, + remote_test: { script: 'echo Hello World' } } + ) + end + end + + context 'when there is an invalid file' do + let(:files) do + [ + Gitlab::Ci::Config::External::File::Local.new({ local: 'myfolder/invalid.yml' }, context) + ] + end + + it 'adds an error to the file' do + expect(process.first.errors).to include("Local file `myfolder/invalid.yml` does not exist!") + end + end + + context 'when max_includes is exceeded' do + context 'when files are nested' do + let(:files) do + [ + Gitlab::Ci::Config::External::File::Local.new({ local: 'nested_configs.yml' }, context) + ] + end + + before do + allow(context).to receive(:max_includes).and_return(1) + end + + it 'raises Processor::IncludeError' do + expect { process }.to raise_error(Gitlab::Ci::Config::External::Processor::IncludeError) + end + end + + context 'when files are not nested' 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 + + before do + allow(context).to receive(:max_includes).and_return(1) + end + + it 'raises Mapper::TooManyIncludesError' do + expect { process }.to raise_error(Gitlab::Ci::Config::External::Mapper::TooManyIncludesError) + end + end + end + end +end diff --git a/spec/lib/gitlab/ci/config/external/mapper_spec.rb b/spec/lib/gitlab/ci/config/external/mapper_spec.rb index d905568f01e..b7e58d4dfa1 100644 --- a/spec/lib/gitlab/ci/config/external/mapper_spec.rb +++ b/spec/lib/gitlab/ci/config/external/mapper_spec.rb @@ -2,8 +2,10 @@ require 'spec_helper' -RSpec.describe Gitlab::Ci::Config::External::Mapper do +# This will be removed with FF ci_refactoring_external_mapper and moved to below. +RSpec.shared_context 'gitlab_ci_config_external_mapper' do include StubRequests + include RepoHelpers let_it_be(:project) { create(:project, :repository) } let_it_be(:user) { project.owner } @@ -12,13 +14,13 @@ RSpec.describe Gitlab::Ci::Config::External::Mapper do let(:remote_url) { 'https://gitlab.com/gitlab-org/gitlab-foss/blob/1234/.gitlab-ci-1.yml' } let(:template_file) { 'Auto-DevOps.gitlab-ci.yml' } let(:variables) { project.predefined_variables } - let(:context_params) { { project: project, sha: '123456', user: user, variables: variables } } + let(:context_params) { { project: project, sha: project.commit.sha, user: user, variables: variables } } let(:context) { Gitlab::Ci::Config::External::Context.new(**context_params) } let(:file_content) do - <<~HEREDOC + <<~YAML image: 'image:1.0' - HEREDOC + YAML end subject(:mapper) { described_class.new(values, context) } @@ -38,7 +40,7 @@ RSpec.describe Gitlab::Ci::Config::External::Mapper do it 'propagates the pipeline logger' do process - fetch_content_log_count = mapper + fetch_content_log_count = context .logger .observations_hash .dig(key, 'count') @@ -231,7 +233,7 @@ RSpec.describe Gitlab::Ci::Config::External::Mapper do it 'has expanset with one' do process - expect(mapper.expandset.size).to eq(1) + expect(context.expandset.size).to eq(1) end end @@ -379,17 +381,28 @@ RSpec.describe Gitlab::Ci::Config::External::Mapper do end context 'when local file path has wildcard' do - let(:project) { create(:project, :repository) } + let_it_be(:project) { create(:project, :repository) } let(:values) do { include: 'myfolder/*.yml' } end - before do - allow_next_instance_of(Repository) do |repository| - allow(repository).to receive(:search_files_by_wildcard_path).with('myfolder/*.yml', '123456') do - ['myfolder/file1.yml', 'myfolder/file2.yml'] - end + let(:project_files) do + { + 'myfolder/file1.yml' => <<~YAML, + my_build: + script: echo Hello World + YAML + 'myfolder/file2.yml' => <<~YAML + my_test: + script: echo Hello World + YAML + } + end + + around do |example| + create_and_delete_files(project, project_files) do + example.run end end @@ -445,8 +458,20 @@ RSpec.describe Gitlab::Ci::Config::External::Mapper do it 'has expanset with two' do process - expect(mapper.expandset.size).to eq(2) + expect(context.expandset.size).to eq(2) end end end end + +RSpec.describe Gitlab::Ci::Config::External::Mapper, feature_category: :pipeline_authoring do + it_behaves_like 'gitlab_ci_config_external_mapper' + + context 'when the FF ci_refactoring_external_mapper is disabled' do + before do + stub_feature_flags(ci_refactoring_external_mapper: false) + end + + it_behaves_like 'gitlab_ci_config_external_mapper' + 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 b1dff6f9723..c9efaf2e1af 100644 --- a/spec/lib/gitlab/ci/config/external/processor_spec.rb +++ b/spec/lib/gitlab/ci/config/external/processor_spec.rb @@ -2,17 +2,31 @@ require 'spec_helper' -RSpec.describe Gitlab::Ci::Config::External::Processor do +RSpec.describe Gitlab::Ci::Config::External::Processor, feature_category: :pipeline_authoring do include StubRequests + include RepoHelpers - let_it_be(:project) { create(:project, :repository) } - let_it_be_with_reload(:another_project) { create(:project, :repository) } let_it_be(:user) { create(:user) } - let(:sha) { '12345' } + let_it_be_with_reload(:project) { create(:project, :repository) } + let_it_be_with_reload(:another_project) { create(:project, :repository) } + + let(:project_files) { {} } + let(:other_project_files) { {} } + + let(:sha) { project.commit.sha } let(:context_params) { { project: project, sha: sha, user: user } } let(:context) { Gitlab::Ci::Config::External::Context.new(**context_params) } - let(:processor) { described_class.new(values, context) } + + subject(:processor) { described_class.new(values, context) } + + around do |example| + create_and_delete_files(project, project_files) do + create_and_delete_files(another_project, other_project_files) do + example.run + end + end + end before do project.add_developer(user) @@ -63,7 +77,7 @@ RSpec.describe Gitlab::Ci::Config::External::Processor do let(:remote_file) { 'https://gitlab.com/gitlab-org/gitlab-foss/blob/1234/.gitlab-ci-1.yml' } let(:values) { { include: remote_file, image: 'image:1.0' } } let(:external_file_content) do - <<-HEREDOC + <<-YAML before_script: - apt-get update -qq && apt-get install -y -qq sqlite3 libsqlite3-dev nodejs - ruby -v @@ -77,7 +91,7 @@ RSpec.describe Gitlab::Ci::Config::External::Processor do rubocop: script: - bundle exec rubocop - HEREDOC + YAML end before do @@ -98,7 +112,7 @@ RSpec.describe Gitlab::Ci::Config::External::Processor do let(:remote_file) { 'https://gitlab.com/gitlab-org/gitlab-foss/blob/1234/.gitlab-ci-1.yml' } let(:values) { { include: remote_file, image: 'image:1.0' } } let(:external_file_content) do - <<-HEREDOC + <<-YAML include: - local: another-file.yml rules: @@ -107,7 +121,7 @@ RSpec.describe Gitlab::Ci::Config::External::Processor do rspec: script: - bundle exec rspec - HEREDOC + YAML end before do @@ -127,19 +141,16 @@ RSpec.describe Gitlab::Ci::Config::External::Processor do context 'with a valid local external file is defined' do let(:values) { { include: '/lib/gitlab/ci/templates/template.yml', image: 'image:1.0' } } let(:local_file_content) do - <<-HEREDOC + <<-YAML before_script: - apt-get update -qq && apt-get install -y -qq sqlite3 libsqlite3-dev nodejs - ruby -v - which ruby - bundle install --jobs $(nproc) "${FLAGS[@]}" - HEREDOC + YAML end - before do - allow_any_instance_of(Gitlab::Ci::Config::External::File::Local) - .to receive(:fetch_local_content).and_return(local_file_content) - end + let(:project_files) { { '/lib/gitlab/ci/templates/template.yml' => local_file_content } } it 'appends the file to the values' do output = processor.perform @@ -153,6 +164,11 @@ RSpec.describe Gitlab::Ci::Config::External::Processor do context 'with multiple external files are defined' do let(:remote_file) { 'https://gitlab.com/gitlab-org/gitlab-foss/blob/1234/.gitlab-ci-1.yml' } + + let(:local_file_content) do + File.read(Rails.root.join('spec/fixtures/gitlab/ci/external_files/.gitlab-ci-template-1.yml')) + end + let(:external_files) do [ '/spec/fixtures/gitlab/ci/external_files/.gitlab-ci-template-1.yml', @@ -168,20 +184,21 @@ RSpec.describe Gitlab::Ci::Config::External::Processor do end let(:remote_file_content) do - <<-HEREDOC + <<-YAML stages: - build - review - cleanup - HEREDOC + YAML end - before do - local_file_content = File.read(Rails.root.join('spec/fixtures/gitlab/ci/external_files/.gitlab-ci-template-1.yml')) - - allow_any_instance_of(Gitlab::Ci::Config::External::File::Local) - .to receive(:fetch_local_content).and_return(local_file_content) + let(:project_files) do + { + '/spec/fixtures/gitlab/ci/external_files/.gitlab-ci-template-1.yml' => local_file_content + } + end + before do stub_full_request(remote_file).to_return(body: remote_file_content) end @@ -199,10 +216,7 @@ RSpec.describe Gitlab::Ci::Config::External::Processor do let(:local_file_content) { 'invalid content file ////' } - before do - allow_any_instance_of(Gitlab::Ci::Config::External::File::Local) - .to receive(:fetch_local_content).and_return(local_file_content) - end + let(:project_files) { { '/lib/gitlab/ci/templates/template.yml' => local_file_content } } it 'raises an error' do expect { processor.perform }.to raise_error( @@ -222,9 +236,9 @@ RSpec.describe Gitlab::Ci::Config::External::Processor do end let(:remote_file_content) do - <<~HEREDOC + <<~YAML image: php:5-fpm-alpine - HEREDOC + YAML end it 'takes precedence' do @@ -244,31 +258,32 @@ RSpec.describe Gitlab::Ci::Config::External::Processor do } end - before do - allow(project.repository).to receive(:blob_data_at).with('12345', '/local/file.yml') do - <<~HEREDOC - include: - - template: Ruby.gitlab-ci.yml - - remote: http://my.domain.com/config.yml - - project: #{another_project.full_path} - file: /templates/my-workflow.yml - HEREDOC - end - - allow_any_instance_of(Repository).to receive(:blob_data_at).with(another_project.commit.id, '/templates/my-workflow.yml') do - <<~HEREDOC - include: - - local: /templates/my-build.yml - HEREDOC - end + let(:project_files) do + { + '/local/file.yml' => <<~YAML + include: + - template: Ruby.gitlab-ci.yml + - remote: http://my.domain.com/config.yml + - project: #{another_project.full_path} + file: /templates/my-workflow.yml + YAML + } + end - allow_any_instance_of(Repository).to receive(:blob_data_at).with(another_project.commit.id, '/templates/my-build.yml') do - <<~HEREDOC - my_build: - script: echo Hello World - HEREDOC - end + let(:other_project_files) do + { + '/templates/my-workflow.yml' => <<~YAML, + include: + - local: /templates/my-build.yml + YAML + '/templates/my-build.yml' => <<~YAML + my_build: + script: echo Hello World + YAML + } + end + before do stub_full_request('http://my.domain.com/config.yml') .to_return(body: 'remote_build: { script: echo Hello World }') end @@ -299,32 +314,32 @@ RSpec.describe Gitlab::Ci::Config::External::Processor do expect(context.includes).to contain_exactly( { type: :local, location: '/local/file.yml', - blob: "http://localhost/#{project.full_path}/-/blob/12345/local/file.yml", - raw: "http://localhost/#{project.full_path}/-/raw/12345/local/file.yml", + blob: "http://localhost/#{project.full_path}/-/blob/#{sha}/local/file.yml", + raw: "http://localhost/#{project.full_path}/-/raw/#{sha}/local/file.yml", extra: {}, context_project: project.full_path, - context_sha: '12345' }, + context_sha: sha }, { type: :template, location: 'Ruby.gitlab-ci.yml', blob: nil, raw: 'https://gitlab.com/gitlab-org/gitlab/-/raw/master/lib/gitlab/ci/templates/Ruby.gitlab-ci.yml', extra: {}, context_project: project.full_path, - context_sha: '12345' }, + context_sha: sha }, { type: :remote, location: 'http://my.domain.com/config.yml', blob: nil, raw: "http://my.domain.com/config.yml", extra: {}, context_project: project.full_path, - context_sha: '12345' }, + context_sha: sha }, { type: :file, location: '/templates/my-workflow.yml', blob: "http://localhost/#{another_project.full_path}/-/blob/#{another_project.commit.sha}/templates/my-workflow.yml", raw: "http://localhost/#{another_project.full_path}/-/raw/#{another_project.commit.sha}/templates/my-workflow.yml", extra: { project: another_project.full_path, ref: 'HEAD' }, context_project: project.full_path, - context_sha: '12345' }, + context_sha: sha }, { type: :local, location: '/templates/my-build.yml', blob: "http://localhost/#{another_project.full_path}/-/blob/#{another_project.commit.sha}/templates/my-build.yml", @@ -393,17 +408,17 @@ RSpec.describe Gitlab::Ci::Config::External::Processor do } end + let(:other_project_files) do + { + '/templates/my-build.yml' => <<~YAML + my_build: + script: echo Hello World + YAML + } + end + before do another_project.add_developer(user) - - allow_next_instance_of(Repository) do |repository| - allow(repository).to receive(:blob_data_at).with(another_project.commit.id, '/templates/my-build.yml') do - <<~HEREDOC - my_build: - script: echo Hello World - HEREDOC - end - end end it 'appends the file to the values' do @@ -423,24 +438,21 @@ RSpec.describe Gitlab::Ci::Config::External::Processor do } end + let(:other_project_files) do + { + '/templates/my-build.yml' => <<~YAML, + my_build: + script: echo Hello World + YAML + '/templates/my-test.yml' => <<~YAML + my_test: + script: echo Hello World + YAML + } + end + before do another_project.add_developer(user) - - allow_next_instance_of(Repository) do |repository| - allow(repository).to receive(:blob_data_at).with(another_project.commit.id, '/templates/my-build.yml') do - <<~HEREDOC - my_build: - script: echo Hello World - HEREDOC - end - - allow(repository).to receive(:blob_data_at).with(another_project.commit.id, '/templates/my-test.yml') do - <<~HEREDOC - my_test: - script: echo Hello World - HEREDOC - end - end end it 'appends the file to the values' do @@ -458,45 +470,34 @@ RSpec.describe Gitlab::Ci::Config::External::Processor do raw: "http://localhost/#{another_project.full_path}/-/raw/#{another_project.commit.sha}/templates/my-build.yml", extra: { project: another_project.full_path, ref: 'HEAD' }, context_project: project.full_path, - context_sha: '12345' }, + context_sha: sha }, { type: :file, blob: "http://localhost/#{another_project.full_path}/-/blob/#{another_project.commit.sha}/templates/my-test.yml", raw: "http://localhost/#{another_project.full_path}/-/raw/#{another_project.commit.sha}/templates/my-test.yml", location: '/templates/my-test.yml', extra: { project: another_project.full_path, ref: 'HEAD' }, context_project: project.full_path, - context_sha: '12345' } + context_sha: sha } ) end end context 'when local file path has wildcard' do - let(:project) { create(:project, :repository) } - let(:values) do { include: 'myfolder/*.yml', image: 'image:1.0' } end - before do - allow_next_instance_of(Repository) do |repository| - allow(repository).to receive(:search_files_by_wildcard_path).with('myfolder/*.yml', sha) do - ['myfolder/file1.yml', 'myfolder/file2.yml'] - end - - allow(repository).to receive(:blob_data_at).with(sha, 'myfolder/file1.yml') do - <<~HEREDOC - my_build: - script: echo Hello World - HEREDOC - end - - allow(repository).to receive(:blob_data_at).with(sha, 'myfolder/file2.yml') do - <<~HEREDOC - my_test: - script: echo Hello World - HEREDOC - end - end + let(:project_files) do + { + 'myfolder/file1.yml' => <<~YAML, + my_build: + script: echo Hello World + YAML + 'myfolder/file2.yml' => <<~YAML + my_test: + script: echo Hello World + YAML + } end it 'fetches the matched files' do @@ -510,18 +511,18 @@ RSpec.describe Gitlab::Ci::Config::External::Processor do expect(context.includes).to contain_exactly( { type: :local, location: 'myfolder/file1.yml', - blob: "http://localhost/#{project.full_path}/-/blob/12345/myfolder/file1.yml", - raw: "http://localhost/#{project.full_path}/-/raw/12345/myfolder/file1.yml", + blob: "http://localhost/#{project.full_path}/-/blob/#{sha}/myfolder/file1.yml", + raw: "http://localhost/#{project.full_path}/-/raw/#{sha}/myfolder/file1.yml", extra: {}, context_project: project.full_path, - context_sha: '12345' }, + context_sha: sha }, { type: :local, - blob: "http://localhost/#{project.full_path}/-/blob/12345/myfolder/file2.yml", - raw: "http://localhost/#{project.full_path}/-/raw/12345/myfolder/file2.yml", + blob: "http://localhost/#{project.full_path}/-/blob/#{sha}/myfolder/file2.yml", + raw: "http://localhost/#{project.full_path}/-/raw/#{sha}/myfolder/file2.yml", location: 'myfolder/file2.yml', extra: {}, context_project: project.full_path, - context_sha: '12345' } + context_sha: sha } ) end end diff --git a/spec/lib/gitlab/ci/config_spec.rb b/spec/lib/gitlab/ci/config_spec.rb index c4a6641ff6b..b48a89059bf 100644 --- a/spec/lib/gitlab/ci/config_spec.rb +++ b/spec/lib/gitlab/ci/config_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::Ci::Config do +RSpec.describe Gitlab::Ci::Config, feature_category: :pipeline_authoring do include StubRequests let_it_be(:user) { create(:user) } @@ -305,7 +305,7 @@ RSpec.describe Gitlab::Ci::Config do it 'raises error' do expect { config }.to raise_error( described_class::ConfigError, - /\!reference \["job-2", "before_script"\] is part of a circular chain/ + /!reference \["job-2", "before_script"\] is part of a circular chain/ ) end end @@ -503,7 +503,7 @@ RSpec.describe Gitlab::Ci::Config do expect { config }.to raise_error( described_class::ConfigError, - 'Resolving config took longer than expected' + 'Request timed out when fetching configuration files.' ) end end diff --git a/spec/lib/gitlab/ci/cron_parser_spec.rb b/spec/lib/gitlab/ci/cron_parser_spec.rb index 33474865a93..4b750cf3bcf 100644 --- a/spec/lib/gitlab/ci/cron_parser_spec.rb +++ b/spec/lib/gitlab/ci/cron_parser_spec.rb @@ -358,4 +358,22 @@ RSpec.describe Gitlab::Ci::CronParser do end end end + + describe '#match?' do + let(:run_date) { Time.zone.local(2021, 3, 2, 1, 0) } + + subject(:matched) { described_class.new(cron, Gitlab::Ci::CronParser::VALID_SYNTAX_SAMPLE_TIME_ZONE).match?(run_date) } + + context 'when cron matches up' do + let(:cron) { '0 1 2 3 *' } + + it { is_expected.to eq(true) } + end + + context 'when cron does not match' do + let(:cron) { '5 4 3 2 1' } + + it { is_expected.to eq(false) } + end + end end diff --git a/spec/lib/gitlab/ci/environment_matcher_spec.rb b/spec/lib/gitlab/ci/environment_matcher_spec.rb new file mode 100644 index 00000000000..172ada1b764 --- /dev/null +++ b/spec/lib/gitlab/ci/environment_matcher_spec.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::EnvironmentMatcher, feature_category: :continuous_integration do + describe '#match?' do + context 'when given pattern is a normal string' do + subject { described_class.new('production') } + + it 'returns true on an exact match' do + expect(subject.match?('production')).to eq true + end + + it 'returns false if not an exact match' do + expect(subject.match?('productiom')).to eq false + end + end + + context 'when given pattern has a wildcard' do + it 'returns true on wildcard matches', :aggregate_failures do + expect(described_class.new('review/*').match?('review/123')).to eq true + expect(described_class.new('review/*/*').match?('review/123/456')).to eq true + expect(described_class.new('*-this-is-a-pattern-*').match?('abc123-this-is-a-pattern-abc123')).to eq true + end + + it 'returns false when not a wildcard match', :aggregate_failures do + expect(described_class.new('review/*').match?('review123')).to eq false + expect(described_class.new('review/*/*').match?('review/123')).to eq false + expect(described_class.new('*-this-is-a-pattern-*').match?('abc123-this-is-a-pattern')).to eq false + end + end + + context 'when given pattern is nil' do + subject { described_class.new(nil) } + + it 'always returns false' do + expect(subject.match?('production')).to eq false + expect(subject.match?('review/123')).to eq false + end + end + + context 'when given pattern is an empty string' do + subject { described_class.new('') } + + it 'always returns false' do + expect(subject.match?('production')).to eq false + expect(subject.match?('review/123')).to eq false + end + end + end +end diff --git a/spec/lib/gitlab/ci/lint_spec.rb b/spec/lib/gitlab/ci/lint_spec.rb index cf07e952f26..b836ca395fa 100644 --- a/spec/lib/gitlab/ci/lint_spec.rb +++ b/spec/lib/gitlab/ci/lint_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::Ci::Lint do +RSpec.describe Gitlab::Ci::Lint, feature_category: :pipeline_authoring do let_it_be(:project) { create(:project, :repository) } let_it_be(:user) { create(:user) } @@ -337,35 +337,28 @@ RSpec.describe Gitlab::Ci::Lint do end end - context 'pipeline logger' do - let(:counters) do - { - 'count' => a_kind_of(Numeric), - 'avg' => a_kind_of(Numeric), - 'sum' => a_kind_of(Numeric), - 'max' => a_kind_of(Numeric), - 'min' => a_kind_of(Numeric) - } - end - - let(:loggable_data) do + describe 'pipeline logger' do + let(:expected_data) do { 'class' => 'Gitlab::Ci::Pipeline::Logger', - 'config_build_context_duration_s' => counters, - 'config_build_variables_duration_s' => counters, - 'config_compose_duration_s' => counters, - 'config_expand_duration_s' => counters, - 'config_external_process_duration_s' => counters, - 'config_stages_inject_duration_s' => counters, - 'config_tags_resolve_duration_s' => counters, - 'config_yaml_extend_duration_s' => counters, - 'config_yaml_load_duration_s' => counters, + 'config_build_context_duration_s' => a_kind_of(Numeric), + 'config_build_variables_duration_s' => a_kind_of(Numeric), + 'config_root_duration_s' => a_kind_of(Numeric), + 'config_root_compose_duration_s' => a_kind_of(Numeric), + 'config_root_compose_jobs_factory_duration_s' => a_kind_of(Numeric), + 'config_root_compose_jobs_create_duration_s' => a_kind_of(Numeric), + 'config_expand_duration_s' => a_kind_of(Numeric), + 'config_external_process_duration_s' => a_kind_of(Numeric), + 'config_stages_inject_duration_s' => a_kind_of(Numeric), + 'config_tags_resolve_duration_s' => a_kind_of(Numeric), + 'config_yaml_extend_duration_s' => a_kind_of(Numeric), + 'config_yaml_load_duration_s' => a_kind_of(Numeric), 'pipeline_creation_caller' => 'Gitlab::Ci::Lint', 'pipeline_creation_service_duration_s' => a_kind_of(Numeric), 'pipeline_persisted' => false, 'pipeline_source' => 'unknown', 'project_id' => project&.id, - 'yaml_process_duration_s' => counters + 'yaml_process_duration_s' => a_kind_of(Numeric) } end @@ -403,7 +396,7 @@ RSpec.describe Gitlab::Ci::Lint do end it 'creates a log entry' do - expect(Gitlab::AppJsonLogger).to receive(:info).with(loggable_data) + expect(Gitlab::AppJsonLogger).to receive(:info).with(a_hash_including(expected_data)) validate end @@ -424,11 +417,11 @@ RSpec.describe Gitlab::Ci::Lint do let(:project) { nil } let(:project_nil_loggable_data) do - loggable_data.except('project_id') + expected_data.except('project_id') end it 'creates a log entry without project_id' do - expect(Gitlab::AppJsonLogger).to receive(:info).with(project_nil_loggable_data) + expect(Gitlab::AppJsonLogger).to receive(:info).with(a_hash_including(project_nil_loggable_data)) validate end diff --git a/spec/lib/gitlab/ci/parsers/sbom/cyclonedx_properties_spec.rb b/spec/lib/gitlab/ci/parsers/sbom/cyclonedx_properties_spec.rb index f09b85aa2c7..dacbe07c8b3 100644 --- a/spec/lib/gitlab/ci/parsers/sbom/cyclonedx_properties_spec.rb +++ b/spec/lib/gitlab/ci/parsers/sbom/cyclonedx_properties_spec.rb @@ -2,7 +2,7 @@ require 'fast_spec_helper' -RSpec.describe Gitlab::Ci::Parsers::Sbom::CyclonedxProperties do +RSpec.describe Gitlab::Ci::Parsers::Sbom::CyclonedxProperties, feature_category: :dependency_management do subject(:parse_source_from_properties) { described_class.parse_source(properties) } context 'when properties are nil' do diff --git a/spec/lib/gitlab/ci/parsers/sbom/cyclonedx_spec.rb b/spec/lib/gitlab/ci/parsers/sbom/cyclonedx_spec.rb index 0b094880f69..d06537ac330 100644 --- a/spec/lib/gitlab/ci/parsers/sbom/cyclonedx_spec.rb +++ b/spec/lib/gitlab/ci/parsers/sbom/cyclonedx_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::Ci::Parsers::Sbom::Cyclonedx do +RSpec.describe Gitlab::Ci::Parsers::Sbom::Cyclonedx, feature_category: :dependency_management do let(:report) { instance_double('Gitlab::Ci::Reports::Sbom::Report') } let(:report_data) { base_report_data } let(:raw_report_data) { report_data.to_json } diff --git a/spec/lib/gitlab/ci/parsers/sbom/source/dependency_scanning_spec.rb b/spec/lib/gitlab/ci/parsers/sbom/source/dependency_scanning_spec.rb index e12fa380209..bc97eb2d950 100644 --- a/spec/lib/gitlab/ci/parsers/sbom/source/dependency_scanning_spec.rb +++ b/spec/lib/gitlab/ci/parsers/sbom/source/dependency_scanning_spec.rb @@ -2,7 +2,7 @@ require 'fast_spec_helper' -RSpec.describe Gitlab::Ci::Parsers::Sbom::Source::DependencyScanning do +RSpec.describe Gitlab::Ci::Parsers::Sbom::Source::DependencyScanning, feature_category: :dependency_management do subject { described_class.source(property_data) } context 'when all property data is present' do diff --git a/spec/lib/gitlab/ci/parsers/sbom/validators/cyclonedx_schema_validator_spec.rb b/spec/lib/gitlab/ci/parsers/sbom/validators/cyclonedx_schema_validator_spec.rb index f58a463f047..712dc00ec7a 100644 --- a/spec/lib/gitlab/ci/parsers/sbom/validators/cyclonedx_schema_validator_spec.rb +++ b/spec/lib/gitlab/ci/parsers/sbom/validators/cyclonedx_schema_validator_spec.rb @@ -2,7 +2,8 @@ require "spec_helper" -RSpec.describe Gitlab::Ci::Parsers::Sbom::Validators::CyclonedxSchemaValidator do +RSpec.describe Gitlab::Ci::Parsers::Sbom::Validators::CyclonedxSchemaValidator, + feature_category: :dependency_management do # Reports should be valid or invalid according to the specification at # https://cyclonedx.org/docs/1.4/json/ diff --git a/spec/lib/gitlab/ci/parsers/security/validators/schema_validator_spec.rb b/spec/lib/gitlab/ci/parsers/security/validators/schema_validator_spec.rb index e730afc72b5..c94ed1f8d6d 100644 --- a/spec/lib/gitlab/ci/parsers/security/validators/schema_validator_spec.rb +++ b/spec/lib/gitlab/ci/parsers/security/validators/schema_validator_spec.rb @@ -95,7 +95,7 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do context 'when all files under schema path are explicitly listed' do # We only care about the part that comes before report-format.json # https://rubular.com/r/N8Juz7r8hYDYgD - filename_regex = /(?<report_type>[-\w]*)\-report-format.json/ + filename_regex = /(?<report_type>[-\w]*)-report-format.json/ versions = Dir.glob(File.join(schema_path, "*", File::SEPARATOR)).map { |path| path.split("/").last } diff --git a/spec/lib/gitlab/ci/pipeline/chain/assign_partition_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/assign_partition_spec.rb index 15df5b2f68c..74a68f28f3e 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/assign_partition_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/assign_partition_spec.rb @@ -10,13 +10,15 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::AssignPartition do Gitlab::Ci::Pipeline::Chain::Command.new(project: project, current_user: user) end - let(:pipeline) { build(:ci_pipeline, project: project) } + let(:pipeline) { build(:ci_pipeline, project: project, partition_id: nil) } let(:step) { described_class.new(pipeline, command) } let(:current_partition_id) { 123 } describe '#perform!' do + include Ci::PartitioningHelpers + before do - allow(Ci::Pipeline).to receive(:current_partition_value) { current_partition_id } + stub_current_partition_id(current_partition_id) end subject { step.perform! } diff --git a/spec/lib/gitlab/ci/pipeline/chain/build/associations_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/build/associations_spec.rb index 32c92724f62..b2128f77960 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/build/associations_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/build/associations_spec.rb @@ -2,11 +2,12 @@ require 'spec_helper' -RSpec.describe Gitlab::Ci::Pipeline::Chain::Build::Associations do +RSpec.describe Gitlab::Ci::Pipeline::Chain::Build::Associations, feature_category: :continuous_integration do let_it_be_with_reload(:project) { create(:project, :repository) } let_it_be(:user) { create(:user, developer_projects: [project]) } - let(:pipeline) { Ci::Pipeline.new } + # Assigning partition_id here to validate it is being propagated correctly + let(:pipeline) { Ci::Pipeline.new(partition_id: ci_testing_partition_id) } let(:bridge) { nil } let(:variables_attributes) do diff --git a/spec/lib/gitlab/ci/pipeline/chain/cancel_pending_pipelines_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/cancel_pending_pipelines_spec.rb index fc3de2a14cd..16deeb6916f 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/cancel_pending_pipelines_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/cancel_pending_pipelines_spec.rb @@ -173,21 +173,6 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::CancelPendingPipelines do expect(build_statuses(prev_pipeline)).to contain_exactly('running', 'success', 'created') expect(build_statuses(parent_pipeline)).to contain_exactly('running', 'running') end - - context 'when feature flag ci_skip_auto_cancelation_on_child_pipelines is disabled' do - before do - stub_feature_flags(ci_skip_auto_cancelation_on_child_pipelines: false) - end - - it 'does not cancel the parent pipeline' do - expect(build_statuses(parent_pipeline)).to contain_exactly('running', 'running') - - perform - - expect(build_statuses(prev_pipeline)).to contain_exactly('success', 'canceled', 'canceled') - expect(build_statuses(parent_pipeline)).to contain_exactly('running', 'running') - end - end end context 'when the previous pipeline source is webide' do diff --git a/spec/lib/gitlab/ci/pipeline/chain/command_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/command_spec.rb index 9126c6dab21..68158503628 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/command_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/command_spec.rb @@ -374,21 +374,57 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Command do end end + describe '#observe_creation_duration' do + let(:histogram) { instance_double(Prometheus::Client::Histogram) } + let(:duration) { 1.hour } + let(:command) { described_class.new(project: project) } + + subject(:observe_creation_duration) do + command.observe_creation_duration(duration) + end + + it 'records the duration as histogram' do + expect(::Gitlab::Ci::Pipeline::Metrics).to receive(:pipeline_creation_duration_histogram) + .and_return(histogram) + expect(histogram).to receive(:observe) + .with({ gitlab: 'false' }, duration.seconds) + + observe_creation_duration + end + + context 'when project is gitlab-org/gitlab' do + before do + allow(project).to receive(:full_path).and_return('gitlab-org/gitlab') + end + + it 'tracks the duration with the expected label' do + expect(::Gitlab::Ci::Pipeline::Metrics).to receive(:pipeline_creation_duration_histogram) + .and_return(histogram) + expect(histogram).to receive(:observe) + .with({ gitlab: 'true' }, duration.seconds) + + observe_creation_duration + end + end + end + describe '#observe_step_duration' do + let(:histogram) { instance_double(Prometheus::Client::Histogram) } + let(:duration) { 1.hour } + let(:command) { described_class.new } + + subject(:observe_step_duration) do + command.observe_step_duration(Gitlab::Ci::Pipeline::Chain::Build, duration) + end + context 'when ci_pipeline_creation_step_duration_tracking is enabled' do it 'adds the duration to the step duration histogram' do - histogram = instance_double(Prometheus::Client::Histogram) - duration = 1.hour - expect(::Gitlab::Ci::Pipeline::Metrics).to receive(:pipeline_creation_step_duration_histogram) .and_return(histogram) expect(histogram).to receive(:observe) .with({ step: 'Gitlab::Ci::Pipeline::Chain::Build' }, duration.seconds) - described_class.new.observe_step_duration( - Gitlab::Ci::Pipeline::Chain::Build, - duration - ) + observe_step_duration end end @@ -398,14 +434,9 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Command do end it 'does nothing' do - duration = 1.hour - expect(::Gitlab::Ci::Pipeline::Metrics).not_to receive(:pipeline_creation_step_duration_histogram) - described_class.new.observe_step_duration( - Gitlab::Ci::Pipeline::Chain::Build, - duration - ) + observe_step_duration end end end diff --git a/spec/lib/gitlab/ci/pipeline/chain/ensure_environments_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/ensure_environments_spec.rb index 7fb5b0b4200..39520149032 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/ensure_environments_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/ensure_environments_spec.rb @@ -36,9 +36,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::EnsureEnvironments, :aggregate_failu end context 'and the pipeline is for a merge request' do - let(:command) do - Gitlab::Ci::Pipeline::Chain::Command.new(project: project, current_user: user, merge_request: merge_request) - end + let(:pipeline) { build(:ci_pipeline, project: project, stages: [stage], merge_request: merge_request) } it 'associates the environment with the merge request' do expect { subject }.to change { Environment.count }.by(1) @@ -62,9 +60,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::EnsureEnvironments, :aggregate_failu end context 'and the pipeline is for a merge request' do - let(:command) do - Gitlab::Ci::Pipeline::Chain::Command.new(project: project, current_user: user, merge_request: merge_request) - end + let(:pipeline) { build(:ci_pipeline, project: project, stages: [stage], merge_request: merge_request) } it 'does not associate the environment with the merge request' do expect { subject }.not_to change { Environment.count } diff --git a/spec/lib/gitlab/ci/pipeline/chain/validate/abilities_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/validate/abilities_spec.rb index 7aaeee32f49..9373888aada 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/validate/abilities_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/validate/abilities_spec.rb @@ -2,8 +2,8 @@ require 'spec_helper' -RSpec.describe Gitlab::Ci::Pipeline::Chain::Validate::Abilities do - let_it_be(:project, reload: true) { create(:project, :repository) } +RSpec.describe Gitlab::Ci::Pipeline::Chain::Validate::Abilities, feature_category: :pipeline_execution do + let(:project) { create(:project, :test_repo) } let_it_be(:user) { create(:user) } let(:pipeline) do diff --git a/spec/lib/gitlab/ci/pipeline/logger_spec.rb b/spec/lib/gitlab/ci/pipeline/logger_spec.rb index 3af0ebe7484..1c285889d1b 100644 --- a/spec/lib/gitlab/ci/pipeline/logger_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/logger_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe ::Gitlab::Ci::Pipeline::Logger do +RSpec.describe ::Gitlab::Ci::Pipeline::Logger, feature_category: :continuous_integration do let_it_be(:project) { build_stubbed(:project) } let_it_be(:pipeline) { build_stubbed(:ci_pipeline, project: project) } @@ -22,61 +22,54 @@ RSpec.describe ::Gitlab::Ci::Pipeline::Logger do end it 'records durations of instrumented operations' do - loggable_data = { + logger.instrument(:expensive_operation) { 123 } + + expected_data = { 'expensive_operation_duration_s' => { 'count' => 1, - 'sum' => a_kind_of(Numeric), - 'avg' => a_kind_of(Numeric), 'max' => a_kind_of(Numeric), - 'min' => a_kind_of(Numeric) + 'sum' => a_kind_of(Numeric) } } - - logger.instrument(:expensive_operation) { 123 } - expect(logger.observations_hash).to match(a_hash_including(loggable_data)) + expect(logger.observations_hash).to match(a_hash_including(expected_data)) end it 'raises an error when block is not provided' do expect { logger.instrument(:expensive_operation) } .to raise_error(ArgumentError, 'block not given') end + + context 'when once: true' do + it 'logs only one observation' do + logger.instrument(:expensive_operation, once: true) { 123 } + logger.instrument(:expensive_operation, once: true) { 123 } + + expected_data = { + 'expensive_operation_duration_s' => a_kind_of(Numeric) + } + expect(logger.observations_hash).to match(a_hash_including(expected_data)) + end + end end - describe '#instrument_with_sql', :request_store do - subject(:instrument_with_sql) do - logger.instrument_with_sql(:expensive_operation, &operation) + describe '#instrument_once_with_sql', :request_store do + subject(:instrument_once_with_sql) do + logger.instrument_once_with_sql(:expensive_operation, &operation) end - def loggable_data(count:, db_count: nil) + def expected_data(count:, db_count: nil) database_name = Ci::ApplicationRecord.connection.pool.db_config.name - keys = %W[ - expensive_operation_duration_s - expensive_operation_db_count - expensive_operation_db_primary_count - expensive_operation_db_primary_duration_s - expensive_operation_db_#{database_name}_count - expensive_operation_db_#{database_name}_duration_s - ] - - data = keys.each.with_object({}) do |key, accumulator| - accumulator[key] = { - 'count' => count, - 'avg' => a_kind_of(Numeric), - 'sum' => a_kind_of(Numeric), - 'max' => a_kind_of(Numeric), - 'min' => a_kind_of(Numeric) - } - end - - if db_count - data['expensive_operation_db_count']['max'] = db_count - data['expensive_operation_db_count']['min'] = db_count - data['expensive_operation_db_count']['avg'] = db_count - data['expensive_operation_db_count']['sum'] = count * db_count - end + total_db_count = count * db_count if db_count - data + { + "expensive_operation_duration_s" => a_kind_of(Numeric), + "expensive_operation_db_count" => total_db_count || a_kind_of(Numeric), + "expensive_operation_db_primary_count" => a_kind_of(Numeric), + "expensive_operation_db_primary_duration_s" => a_kind_of(Numeric), + "expensive_operation_db_#{database_name}_count" => a_kind_of(Numeric), + "expensive_operation_db_#{database_name}_duration_s" => a_kind_of(Numeric) + } end context 'with a single query' do @@ -85,10 +78,10 @@ RSpec.describe ::Gitlab::Ci::Pipeline::Logger do it { is_expected.to eq(operation.call) } it 'includes SQL metrics' do - instrument_with_sql + instrument_once_with_sql expect(logger.observations_hash) - .to match(a_hash_including(loggable_data(count: 1, db_count: 1))) + .to match(a_hash_including(expected_data(count: 1, db_count: 1))) end end @@ -98,21 +91,10 @@ RSpec.describe ::Gitlab::Ci::Pipeline::Logger do it { is_expected.to eq(operation.call) } it 'includes SQL metrics' do - instrument_with_sql - - expect(logger.observations_hash) - .to match(a_hash_including(loggable_data(count: 1, db_count: 2))) - end - end - - context 'with multiple observations' do - let(:operation) { -> { Ci::Build.count + Ci::Bridge.count } } - - it 'includes SQL metrics' do - 2.times { logger.instrument_with_sql(:expensive_operation, &operation) } + instrument_once_with_sql expect(logger.observations_hash) - .to match(a_hash_including(loggable_data(count: 2, db_count: 2))) + .to match(a_hash_including(expected_data(count: 1, db_count: 2))) end end @@ -122,7 +104,7 @@ RSpec.describe ::Gitlab::Ci::Pipeline::Logger do it { is_expected.to eq(operation.call) } it 'does not include SQL metrics' do - instrument_with_sql + instrument_once_with_sql expect(logger.observations_hash.keys) .to match_array(['expensive_operation_duration_s']) @@ -132,14 +114,40 @@ RSpec.describe ::Gitlab::Ci::Pipeline::Logger do describe '#observe' do it 'records durations of observed operations' do - loggable_data = { + expect(logger.observe(:pipeline_creation_duration_s, 30)).to be_truthy + + expected_data = { 'pipeline_creation_duration_s' => { - 'avg' => 30, 'sum' => 30, 'count' => 1, 'max' => 30, 'min' => 30 + 'sum' => 30, 'count' => 1, 'max' => 30 } } + expect(logger.observations_hash).to match(a_hash_including(expected_data)) + end - expect(logger.observe(:pipeline_creation_duration_s, 30)).to be_truthy - expect(logger.observations_hash).to match(a_hash_including(loggable_data)) + context 'when once: true' do + it 'records the latest observation' do + expect(logger.observe(:pipeline_creation_duration_s, 20, once: true)).to be_truthy + expect(logger.observe(:pipeline_creation_duration_s, 30, once: true)).to be_truthy + + expected_data = { + 'pipeline_creation_duration_s' => 30 + } + expect(logger.observations_hash).to match(a_hash_including(expected_data)) + end + + it 'logs data as expected' do + expect(logger.observe(:pipeline_creation_duration_s, 30, once: true)).to be_truthy + expect(logger.observe(:pipeline_operation_x_duration_s, 20)).to be_truthy + expect(logger.observe(:pipeline_operation_x_duration_s, 20)).to be_truthy + + expected_data = { + 'pipeline_creation_duration_s' => 30, + 'pipeline_operation_x_duration_s' => { + 'sum' => 40, 'count' => 2, 'max' => 20 + } + } + expect(logger.observations_hash).to match(a_hash_including(expected_data)) + end end end @@ -158,8 +166,11 @@ RSpec.describe ::Gitlab::Ci::Pipeline::Logger do context 'when the feature flag is enabled' do let(:flag) { true } - let(:loggable_data) do + let(:expected_data) do { + 'correlation_id' => a_kind_of(String), + 'meta.project' => project.full_path, + 'meta.root_namespace' => project.root_namespace.full_path, 'class' => described_class.name.to_s, 'pipeline_id' => pipeline.id, 'pipeline_persisted' => true, @@ -168,10 +179,10 @@ RSpec.describe ::Gitlab::Ci::Pipeline::Logger do 'pipeline_creation_caller' => 'source', 'pipeline_source' => pipeline.source, 'pipeline_save_duration_s' => { - 'avg' => 60, 'sum' => 60, 'count' => 1, 'max' => 60, 'min' => 60 + 'sum' => 60, 'count' => 1, 'max' => 60 }, 'pipeline_creation_duration_s' => { - 'avg' => 20, 'sum' => 40, 'count' => 2, 'max' => 30, 'min' => 10 + 'sum' => 40, 'count' => 2, 'max' => 30 } } end @@ -179,7 +190,7 @@ RSpec.describe ::Gitlab::Ci::Pipeline::Logger do it 'logs to application.json' do expect(Gitlab::AppJsonLogger) .to receive(:info) - .with(a_hash_including(loggable_data)) + .with(a_hash_including(expected_data)) .and_call_original expect(commit).to be_truthy @@ -200,28 +211,43 @@ RSpec.describe ::Gitlab::Ci::Pipeline::Logger do expect(Gitlab::AppJsonLogger) .to receive(:info) - .with(a_hash_including(loggable_data)) + .with(a_hash_including(expected_data)) .and_call_original expect(commit).to be_truthy end + + context 'with unexistent observations in condition' do + it 'does not commit the log' do + logger.log_when do |observations| + value = observations['non_existent_value'] + next false unless value + + value > 0 + end + + expect(Gitlab::AppJsonLogger).not_to receive(:info) + + expect(commit).to be_falsey + end + end end context 'when project is not passed and pipeline is not persisted' do let(:project) {} let(:pipeline) { build(:ci_pipeline) } - let(:loggable_data) do + let(:expected_data) do { 'class' => described_class.name.to_s, 'pipeline_persisted' => false, 'pipeline_creation_service_duration_s' => a_kind_of(Numeric), 'pipeline_creation_caller' => 'source', 'pipeline_save_duration_s' => { - 'avg' => 60, 'sum' => 60, 'count' => 1, 'max' => 60, 'min' => 60 + 'sum' => 60, 'count' => 1, 'max' => 60 }, 'pipeline_creation_duration_s' => { - 'avg' => 20, 'sum' => 40, 'count' => 2, 'max' => 30, 'min' => 10 + 'sum' => 40, 'count' => 2, 'max' => 30 } } end @@ -229,7 +255,7 @@ RSpec.describe ::Gitlab::Ci::Pipeline::Logger do it 'logs to application.json' do expect(Gitlab::AppJsonLogger) .to receive(:info) - .with(a_hash_including(loggable_data)) + .with(a_hash_including(expected_data)) .and_call_original expect(commit).to be_truthy diff --git a/spec/lib/gitlab/ci/pipeline/seed/build/cache_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/build/cache_spec.rb index 910c12389c3..fb8020bf43e 100644 --- a/spec/lib/gitlab/ci/pipeline/seed/build/cache_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/seed/build/cache_spec.rb @@ -6,8 +6,9 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build::Cache do let_it_be(:project) { create(:project, :repository) } let_it_be(:head_sha) { project.repository.head_commit.id } let_it_be(:pipeline) { create(:ci_pipeline, project: project, sha: head_sha) } + let(:index) { 1 } - let(:processor) { described_class.new(pipeline, config) } + let(:processor) { described_class.new(pipeline, config, index) } describe '#attributes' do subject { processor.attributes } @@ -40,10 +41,12 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build::Cache do { key: { files: files } } end - it 'uses default key' do - expected = { key: 'default' } + context 'without a prefix' do + it 'uses default key with an index as a prefix' do + expected = { key: '1-default' } - is_expected.to include(expected) + is_expected.to include(expected) + end end end @@ -57,13 +60,15 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build::Cache do } end - it 'builds a string key' do - expected = { - key: '703ecc8fef1635427a1f86a8a1a308831c122392', - paths: ['vendor/ruby'] - } + context 'without a prefix' do + it 'builds a string key with an index as a prefix' do + expected = { + key: '1-703ecc8fef1635427a1f86a8a1a308831c122392', + paths: ['vendor/ruby'] + } - is_expected.to include(expected) + is_expected.to include(expected) + end end end @@ -107,10 +112,12 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build::Cache do } end - it 'builds a string key' do - expected = { key: '74bf43fb1090f161bdd4e265802775dbda2f03d1' } + context 'without a prefix' do + it 'builds a string key with an index as a prefix' do + expected = { key: '1-74bf43fb1090f161bdd4e265802775dbda2f03d1' } - is_expected.to include(expected) + is_expected.to include(expected) + end end end diff --git a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb index 75f6a773c2d..1f7f800e238 100644 --- a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do +RSpec.describe Gitlab::Ci::Pipeline::Seed::Build, feature_category: :pipeline_authoring do let_it_be_with_reload(:project) { create(:project, :repository) } let_it_be(:head_sha) { project.repository.head_commit.id } @@ -11,861 +11,954 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do let(:seed_context) { Gitlab::Ci::Pipeline::Seed::Context.new(pipeline, root_variables: root_variables) } let(:attributes) { { name: 'rspec', ref: 'master', scheduling_type: :stage, when: 'on_success' } } let(:previous_stages) { [] } - let(:current_stage) { double(seeds_names: [attributes[:name]]) } + let(:current_stage) { instance_double(Gitlab::Ci::Pipeline::Seed::Stage, seeds_names: [attributes[:name]]) } + let(:current_ci_stage) { build(:ci_stage, pipeline: pipeline) } - let(:seed_build) { described_class.new(seed_context, attributes, previous_stages + [current_stage]) } + let(:seed_build) { described_class.new(seed_context, attributes, previous_stages + [current_stage], current_ci_stage) } - describe '#attributes' do - subject { seed_build.attributes } + shared_examples 'build seed' do + describe '#attributes' do + subject { seed_build.attributes } - it { is_expected.to be_a(Hash) } - it { is_expected.to include(:name, :project, :ref) } + it { is_expected.to be_a(Hash) } + it { is_expected.to include(:name, :project, :ref) } - context 'with job:when' do - let(:attributes) { { name: 'rspec', ref: 'master', when: 'on_failure' } } + context 'with job:when' do + let(:attributes) { { name: 'rspec', ref: 'master', when: 'on_failure' } } - it { is_expected.to include(when: 'on_failure') } - end - - context 'with job:when:delayed' do - let(:attributes) { { name: 'rspec', ref: 'master', when: 'delayed', start_in: '3 hours' } } - - it { is_expected.to include(when: 'delayed', start_in: '3 hours') } - end - - context 'with job:rules:[when:]' do - context 'is matched' do - let(:attributes) { { name: 'rspec', ref: 'master', rules: [{ if: '$VAR == null', when: 'always' }] } } - - it { is_expected.to include(when: 'always') } + it { is_expected.to include(when: 'on_failure') } end - context 'is not matched' do - let(:attributes) { { name: 'rspec', ref: 'master', rules: [{ if: '$VAR != null', when: 'always' }] } } - - it { is_expected.to include(when: 'never') } - end - end - - context 'with job:rules:[when:delayed]' do - context 'is matched' do - let(:attributes) { { name: 'rspec', ref: 'master', rules: [{ if: '$VAR == null', when: 'delayed', start_in: '3 hours' }] } } + context 'with job:when:delayed' do + let(:attributes) { { name: 'rspec', ref: 'master', when: 'delayed', options: { start_in: '3 hours' } } } it { is_expected.to include(when: 'delayed', options: { start_in: '3 hours' }) } end - context 'is not matched' do - let(:attributes) { { name: 'rspec', ref: 'master', rules: [{ if: '$VAR != null', when: 'delayed', start_in: '3 hours' }] } } - - it { is_expected.to include(when: 'never') } - end - end - - context 'with job: rules but no explicit when:' do - let(:base_attributes) { { name: 'rspec', ref: 'master' } } - - context 'with a manual job' do - context 'with a matched rule' do - let(:attributes) { base_attributes.merge(when: 'manual', rules: [{ if: '$VAR == null' }]) } + context 'with job:rules:[when:]' do + context 'is matched' do + let(:attributes) { { name: 'rspec', ref: 'master', rules: [{ if: '$VAR == null', when: 'always' }] } } - it { is_expected.to include(when: 'manual') } + it { is_expected.to include(when: 'always') } end context 'is not matched' do - let(:attributes) { base_attributes.merge(when: 'manual', rules: [{ if: '$VAR != null' }]) } + let(:attributes) { { name: 'rspec', ref: 'master', rules: [{ if: '$VAR != null', when: 'always' }] } } it { is_expected.to include(when: 'never') } end end - context 'with an automatic job' do + context 'with job:rules:[when:delayed]' do context 'is matched' do - let(:attributes) { base_attributes.merge(when: 'on_success', rules: [{ if: '$VAR == null' }]) } + let(:attributes) { { name: 'rspec', ref: 'master', rules: [{ if: '$VAR == null', when: 'delayed', start_in: '3 hours' }] } } - it { is_expected.to include(when: 'on_success') } + it { is_expected.to include(when: 'delayed', options: { start_in: '3 hours' }) } end context 'is not matched' do - let(:attributes) { base_attributes.merge(when: 'on_success', rules: [{ if: '$VAR != null' }]) } + let(:attributes) { { name: 'rspec', ref: 'master', rules: [{ if: '$VAR != null', when: 'delayed', start_in: '3 hours' }] } } it { is_expected.to include(when: 'never') } end end - end - context 'with job:rules:[variables:]' do - let(:attributes) do - { name: 'rspec', - ref: 'master', - job_variables: [{ key: 'VAR1', value: 'var 1' }, - { key: 'VAR2', value: 'var 2' }], - rules: [{ if: '$VAR == null', variables: { VAR1: 'new var 1', VAR3: 'var 3' } }] } - end + context 'with job: rules but no explicit when:' do + let(:base_attributes) { { name: 'rspec', ref: 'master' } } - it do - is_expected.to include(yaml_variables: [{ key: 'VAR1', value: 'new var 1' }, - { key: 'VAR3', value: 'var 3' }, - { key: 'VAR2', value: 'var 2' }]) - end - end + context 'with a manual job' do + context 'with a matched rule' do + let(:attributes) { base_attributes.merge(when: 'manual', rules: [{ if: '$VAR == null' }]) } - context 'with job:tags' do - let(:attributes) do - { - name: 'rspec', - ref: 'master', - job_variables: [{ key: 'VARIABLE', value: 'value' }], - tag_list: ['static-tag', '$VARIABLE', '$NO_VARIABLE'] - } - end + it { is_expected.to include(when: 'manual') } + end - it { is_expected.to include(tag_list: ['static-tag', 'value', '$NO_VARIABLE']) } - it { is_expected.to include(yaml_variables: [{ key: 'VARIABLE', value: 'value' }]) } - end + context 'is not matched' do + let(:attributes) { base_attributes.merge(when: 'manual', rules: [{ if: '$VAR != null' }]) } - context 'with cache:key' do - let(:attributes) do - { - name: 'rspec', - ref: 'master', - cache: [{ - key: 'a-value' - }] - } - end + it { is_expected.to include(when: 'never') } + end + end + + context 'with an automatic job' do + context 'is matched' do + let(:attributes) { base_attributes.merge(when: 'on_success', rules: [{ if: '$VAR == null' }]) } + + it { is_expected.to include(when: 'on_success') } + end - it { is_expected.to include(options: { cache: [a_hash_including(key: 'a-value')] }) } + context 'is not matched' do + let(:attributes) { base_attributes.merge(when: 'on_success', rules: [{ if: '$VAR != null' }]) } + + it { is_expected.to include(when: 'never') } + end + end + end - context 'with cache:key:files' do + context 'with job:rules:[variables:]' do let(:attributes) do - { - name: 'rspec', + { name: 'rspec', ref: 'master', - cache: [{ - key: { - files: ['VERSION'] - } - }] - } + job_variables: [{ key: 'VAR1', value: 'var 1' }, + { key: 'VAR2', value: 'var 2' }], + rules: [{ if: '$VAR == null', variables: { VAR1: 'new var 1', VAR3: 'var 3' } }] } end - it 'includes cache options' do - cache_options = { - options: { - cache: [a_hash_including(key: 'f155568ad0933d8358f66b846133614f76dd0ca4')] - } - } + it do + is_expected.to include(yaml_variables: [{ key: 'VAR1', value: 'new var 1' }, + { key: 'VAR3', value: 'var 3' }, + { key: 'VAR2', value: 'var 2' }]) + end - is_expected.to include(cache_options) + it 'expects the same results on to_resource' do + expect(seed_build.to_resource.yaml_variables).to include({ key: 'VAR1', value: 'new var 1' }, + { key: 'VAR3', value: 'var 3' }, + { key: 'VAR2', value: 'var 2' }) end end - context 'with cache:key:prefix' do + context 'with job:tags' do let(:attributes) do { name: 'rspec', ref: 'master', - cache: [{ - key: { - prefix: 'something' - } - }] + job_variables: [{ key: 'VARIABLE', value: 'value' }], + tag_list: ['static-tag', '$VARIABLE', '$NO_VARIABLE'] } end - it { is_expected.to include(options: { cache: [a_hash_including( key: 'something-default' )] }) } + it { is_expected.to include(tag_list: ['static-tag', 'value', '$NO_VARIABLE']) } + it { is_expected.to include(yaml_variables: [{ key: 'VARIABLE', value: 'value' }]) } end - context 'with cache:key:files and prefix' do + context 'with cache:key' do let(:attributes) do { name: 'rspec', ref: 'master', cache: [{ - key: { - files: ['VERSION'], - prefix: 'something' - } + key: 'a-value' }] } end - it 'includes cache options' do - cache_options = { - options: { - cache: [a_hash_including(key: 'something-f155568ad0933d8358f66b846133614f76dd0ca4')] + it { is_expected.to include(options: { cache: [a_hash_including(key: 'a-value')] }) } + + context 'with cache:key:files' do + let(:attributes) do + { + name: 'rspec', + ref: 'master', + cache: [{ + key: { + files: ['VERSION'] + } + }] } - } + end - is_expected.to include(cache_options) - end - end - end + it 'includes cache options' do + cache_options = { + options: { + cache: [a_hash_including(key: '0-f155568ad0933d8358f66b846133614f76dd0ca4')] + } + } - context 'with empty cache' do - let(:attributes) do - { - name: 'rspec', - ref: 'master', - cache: {} - } - end + is_expected.to include(cache_options) + end + end - it { is_expected.to include({}) } - end + context 'with cache:key:prefix' do + let(:attributes) do + { + name: 'rspec', + ref: 'master', + cache: [{ + key: { + prefix: 'something' + } + }] + } + end - context 'with allow_failure' do - let(:options) do - { allow_failure_criteria: { exit_codes: [42] } } - end + it { is_expected.to include(options: { cache: [a_hash_including( key: 'something-default' )] }) } + end - let(:rules) do - [{ if: '$VAR == null', when: 'always' }] - end + context 'with cache:key:files and prefix' do + let(:attributes) do + { + name: 'rspec', + ref: 'master', + cache: [{ + key: { + files: ['VERSION'], + prefix: 'something' + } + }] + } + end - let(:attributes) do - { - name: 'rspec', - ref: 'master', - options: options, - rules: rules - } - end + it 'includes cache options' do + cache_options = { + options: { + cache: [a_hash_including(key: 'something-f155568ad0933d8358f66b846133614f76dd0ca4')] + } + } - context 'when rules does not override allow_failure' do - it { is_expected.to match a_hash_including(options: options) } + is_expected.to include(cache_options) + end + end end - context 'when rules set allow_failure to true' do - let(:rules) do - [{ if: '$VAR == null', when: 'always', allow_failure: true }] + context 'with empty cache' do + let(:attributes) do + { + name: 'rspec', + ref: 'master', + cache: {} + } end - it { is_expected.to match a_hash_including(options: { allow_failure_criteria: nil }) } + it { is_expected.to include({}) } end - context 'when rules set allow_failure to false' do - let(:rules) do - [{ if: '$VAR == null', when: 'always', allow_failure: false }] + context 'with allow_failure' do + let(:options) do + { allow_failure_criteria: { exit_codes: [42] } } end - it { is_expected.to match a_hash_including(options: { allow_failure_criteria: nil }) } - end - end - - context 'with workflow:rules:[variables:]' do - let(:attributes) do - { name: 'rspec', - ref: 'master', - yaml_variables: [{ key: 'VAR2', value: 'var 2' }, - { key: 'VAR3', value: 'var 3' }], - job_variables: [{ key: 'VAR2', value: 'var 2' }, - { key: 'VAR3', value: 'var 3' }], - root_variables_inheritance: root_variables_inheritance } - end + let(:rules) do + [{ if: '$VAR == null', when: 'always' }] + end - context 'when the pipeline has variables' do - let(:root_variables) do - [{ key: 'VAR1', value: 'var overridden pipeline 1' }, - { key: 'VAR2', value: 'var pipeline 2' }, - { key: 'VAR3', value: 'var pipeline 3' }, - { key: 'VAR4', value: 'new var pipeline 4' }] + let(:attributes) do + { + name: 'rspec', + ref: 'master', + options: options, + rules: rules + } end - context 'when root_variables_inheritance is true' do - let(:root_variables_inheritance) { true } + context 'when rules does not override allow_failure' do + it { is_expected.to match a_hash_including(options: options) } + end - it 'returns calculated yaml variables' do - expect(subject[:yaml_variables]).to match_array( - [{ key: 'VAR1', value: 'var overridden pipeline 1' }, - { key: 'VAR2', value: 'var 2' }, - { key: 'VAR3', value: 'var 3' }, - { key: 'VAR4', value: 'new var pipeline 4' }] - ) + context 'when rules set allow_failure to true' do + let(:rules) do + [{ if: '$VAR == null', when: 'always', allow_failure: true }] end - end - context 'when root_variables_inheritance is false' do - let(:root_variables_inheritance) { false } + it { is_expected.to match a_hash_including(options: { allow_failure_criteria: nil }) } - it 'returns job variables' do - expect(subject[:yaml_variables]).to match_array( - [{ key: 'VAR2', value: 'var 2' }, - { key: 'VAR3', value: 'var 3' }] - ) - end - end + context 'when options contain other static values' do + let(:options) do + { image: 'busybox', allow_failure_criteria: { exit_codes: [42] } } + end - context 'when root_variables_inheritance is an array' do - let(:root_variables_inheritance) { %w(VAR1 VAR2 VAR3) } + it { is_expected.to match a_hash_including(options: { image: 'busybox', allow_failure_criteria: nil }) } - it 'returns calculated yaml variables' do - expect(subject[:yaml_variables]).to match_array( - [{ key: 'VAR1', value: 'var overridden pipeline 1' }, - { key: 'VAR2', value: 'var 2' }, - { key: 'VAR3', value: 'var 3' }] - ) + it 'deep merges options when exporting to_resource' do + expect(seed_build.to_resource.options).to match a_hash_including( + image: 'busybox', allow_failure_criteria: nil + ) + end end end - end - context 'when the pipeline has not a variable' do - let(:root_variables_inheritance) { true } + context 'when rules set allow_failure to false' do + let(:rules) do + [{ if: '$VAR == null', when: 'always', allow_failure: false }] + end - it 'returns seed yaml variables' do - expect(subject[:yaml_variables]).to match_array( - [{ key: 'VAR2', value: 'var 2' }, - { key: 'VAR3', value: 'var 3' }]) + it { is_expected.to match a_hash_including(options: { allow_failure_criteria: nil }) } end end - end - context 'when the job rule depends on variables' do - let(:attributes) do - { name: 'rspec', - ref: 'master', - yaml_variables: [{ key: 'VAR1', value: 'var 1' }], - job_variables: [{ key: 'VAR1', value: 'var 1' }], - root_variables_inheritance: root_variables_inheritance, - rules: rules } - end + context 'with workflow:rules:[variables:]' do + let(:attributes) do + { name: 'rspec', + ref: 'master', + yaml_variables: [{ key: 'VAR2', value: 'var 2' }, + { key: 'VAR3', value: 'var 3' }], + job_variables: [{ key: 'VAR2', value: 'var 2' }, + { key: 'VAR3', value: 'var 3' }], + root_variables_inheritance: root_variables_inheritance } + end + + context 'when the pipeline has variables' do + let(:root_variables) do + [{ key: 'VAR1', value: 'var overridden pipeline 1' }, + { key: 'VAR2', value: 'var pipeline 2' }, + { key: 'VAR3', value: 'var pipeline 3' }, + { key: 'VAR4', value: 'new var pipeline 4' }] + end - let(:root_variables_inheritance) { true } + context 'when root_variables_inheritance is true' do + let(:root_variables_inheritance) { true } - context 'when the rules use job variables' do - let(:rules) do - [{ if: '$VAR1 == "var 1"', variables: { VAR1: 'overridden var 1', VAR2: 'new var 2' } }] + it 'returns calculated yaml variables' do + expect(subject[:yaml_variables]).to match_array( + [{ key: 'VAR1', value: 'var overridden pipeline 1' }, + { key: 'VAR2', value: 'var 2' }, + { key: 'VAR3', value: 'var 3' }, + { key: 'VAR4', value: 'new var pipeline 4' }] + ) + end + end + + context 'when root_variables_inheritance is false' do + let(:root_variables_inheritance) { false } + + it 'returns job variables' do + expect(subject[:yaml_variables]).to match_array( + [{ key: 'VAR2', value: 'var 2' }, + { key: 'VAR3', value: 'var 3' }] + ) + end + end + + context 'when root_variables_inheritance is an array' do + let(:root_variables_inheritance) { %w(VAR1 VAR2 VAR3) } + + it 'returns calculated yaml variables' do + expect(subject[:yaml_variables]).to match_array( + [{ key: 'VAR1', value: 'var overridden pipeline 1' }, + { key: 'VAR2', value: 'var 2' }, + { key: 'VAR3', value: 'var 3' }] + ) + end + end end - it 'recalculates the variables' do - expect(subject[:yaml_variables]).to contain_exactly({ key: 'VAR1', value: 'overridden var 1' }, - { key: 'VAR2', value: 'new var 2' }) + context 'when the pipeline has not a variable' do + let(:root_variables_inheritance) { true } + + it 'returns seed yaml variables' do + expect(subject[:yaml_variables]).to match_array( + [{ key: 'VAR2', value: 'var 2' }, + { key: 'VAR3', value: 'var 3' }]) + end end end - context 'when the rules use root variables' do - let(:root_variables) do - [{ key: 'VAR2', value: 'var pipeline 2' }] + context 'when the job rule depends on variables' do + let(:attributes) do + { name: 'rspec', + ref: 'master', + yaml_variables: [{ key: 'VAR1', value: 'var 1' }], + job_variables: [{ key: 'VAR1', value: 'var 1' }], + root_variables_inheritance: root_variables_inheritance, + rules: rules } end - let(:rules) do - [{ if: '$VAR2 == "var pipeline 2"', variables: { VAR1: 'overridden var 1', VAR2: 'overridden var 2' } }] - end + let(:root_variables_inheritance) { true } + + context 'when the rules use job variables' do + let(:rules) do + [{ if: '$VAR1 == "var 1"', variables: { VAR1: 'overridden var 1', VAR2: 'new var 2' } }] + end - it 'recalculates the variables' do - expect(subject[:yaml_variables]).to contain_exactly({ key: 'VAR1', value: 'overridden var 1' }, - { key: 'VAR2', value: 'overridden var 2' }) + it 'recalculates the variables' do + expect(subject[:yaml_variables]).to contain_exactly({ key: 'VAR1', value: 'overridden var 1' }, + { key: 'VAR2', value: 'new var 2' }) + end end - context 'when the root_variables_inheritance is false' do - let(:root_variables_inheritance) { false } + context 'when the rules use root variables' do + let(:root_variables) do + [{ key: 'VAR2', value: 'var pipeline 2' }] + end - it 'does not recalculate the variables' do - expect(subject[:yaml_variables]).to contain_exactly({ key: 'VAR1', value: 'var 1' }) + let(:rules) do + [{ if: '$VAR2 == "var pipeline 2"', variables: { VAR1: 'overridden var 1', VAR2: 'overridden var 2' } }] + end + + it 'recalculates the variables' do + expect(subject[:yaml_variables]).to contain_exactly({ key: 'VAR1', value: 'overridden var 1' }, + { key: 'VAR2', value: 'overridden var 2' }) end - end - end - end - end - describe '#bridge?' do - subject { seed_build.bridge? } + context 'when the root_variables_inheritance is false' do + let(:root_variables_inheritance) { false } - context 'when job is a downstream bridge' do - let(:attributes) do - { name: 'rspec', ref: 'master', options: { trigger: 'my/project' } } + it 'does not recalculate the variables' do + expect(subject[:yaml_variables]).to contain_exactly({ key: 'VAR1', value: 'var 1' }) + end + end + end end + end - it { is_expected.to be_truthy } + describe '#bridge?' do + subject { seed_build.bridge? } - context 'when trigger definition is empty' do + context 'when job is a downstream bridge' do let(:attributes) do - { name: 'rspec', ref: 'master', options: { trigger: '' } } + { name: 'rspec', ref: 'master', options: { trigger: 'my/project' } } end - it { is_expected.to be_falsey } - end - end + it { is_expected.to be_truthy } - context 'when job is an upstream bridge' do - let(:attributes) do - { name: 'rspec', ref: 'master', options: { bridge_needs: { pipeline: 'my/project' } } } - end + context 'when trigger definition is empty' do + let(:attributes) do + { name: 'rspec', ref: 'master', options: { trigger: '' } } + end - it { is_expected.to be_truthy } + it { is_expected.to be_falsey } + end + end - context 'when upstream definition is empty' do + context 'when job is an upstream bridge' do let(:attributes) do - { name: 'rspec', ref: 'master', options: { bridge_needs: { pipeline: '' } } } + { name: 'rspec', ref: 'master', options: { bridge_needs: { pipeline: 'my/project' } } } end - it { is_expected.to be_falsey } - end - end + it { is_expected.to be_truthy } - context 'when job is not a bridge' do - it { is_expected.to be_falsey } - end - end + context 'when upstream definition is empty' do + let(:attributes) do + { name: 'rspec', ref: 'master', options: { bridge_needs: { pipeline: '' } } } + end - describe '#to_resource' do - subject { seed_build.to_resource } + it { is_expected.to be_falsey } + end + end - it 'memoizes a resource object' do - expect(subject.object_id).to eq seed_build.to_resource.object_id + context 'when job is not a bridge' do + it { is_expected.to be_falsey } + end end - it 'can not be persisted without explicit assignment' do - pipeline.save! + describe '#to_resource' do + subject { seed_build.to_resource } - expect(subject).not_to be_persisted - end - end + it 'memoizes a resource object' do + expect(subject.object_id).to eq seed_build.to_resource.object_id + end - describe 'applying job inclusion policies' do - subject { seed_build } + it 'can not be persisted without explicit assignment' do + pipeline.save! - context 'when no branch policy is specified' do - let(:attributes) do - { name: 'rspec' } + expect(subject).not_to be_persisted end - - it { is_expected.to be_included } end - context 'when branch policy does not match' do - context 'when using only' do - let(:attributes) do - { name: 'rspec', only: { refs: ['deploy'] } } - end - - it { is_expected.not_to be_included } - end + describe 'applying job inclusion policies' do + subject { seed_build } - context 'when using except' do + context 'when no branch policy is specified' do let(:attributes) do - { name: 'rspec', except: { refs: ['deploy'] } } + { name: 'rspec' } end it { is_expected.to be_included } end - context 'with both only and except policies' do - let(:attributes) do - { - name: 'rspec', - only: { refs: %w[deploy] }, - except: { refs: %w[deploy] } - } + context 'when branch policy does not match' do + context 'when using only' do + let(:attributes) do + { name: 'rspec', only: { refs: ['deploy'] } } + end + + it { is_expected.not_to be_included } end - it { is_expected.not_to be_included } - end - end + context 'when using except' do + let(:attributes) do + { name: 'rspec', except: { refs: ['deploy'] } } + end - context 'when branch regexp policy does not match' do - context 'when using only' do - let(:attributes) do - { name: 'rspec', only: { refs: %w[/^deploy$/] } } + it { is_expected.to be_included } end - it { is_expected.not_to be_included } - end + context 'with both only and except policies' do + let(:attributes) do + { + name: 'rspec', + only: { refs: %w[deploy] }, + except: { refs: %w[deploy] } + } + end - context 'when using except' do - let(:attributes) do - { name: 'rspec', except: { refs: %w[/^deploy$/] } } + it { is_expected.not_to be_included } end - - it { is_expected.to be_included } end - context 'with both only and except policies' do - let(:attributes) do - { - name: 'rspec', - only: { refs: %w[/^deploy$/] }, - except: { refs: %w[/^deploy$/] } - } + context 'when branch regexp policy does not match' do + context 'when using only' do + let(:attributes) do + { name: 'rspec', only: { refs: %w[/^deploy$/] } } + end + + it { is_expected.not_to be_included } end - it { is_expected.not_to be_included } - end - end + context 'when using except' do + let(:attributes) do + { name: 'rspec', except: { refs: %w[/^deploy$/] } } + end - context 'when branch policy matches' do - context 'when using only' do - let(:attributes) do - { name: 'rspec', only: { refs: %w[deploy master] } } + it { is_expected.to be_included } end - it { is_expected.to be_included } - end + context 'with both only and except policies' do + let(:attributes) do + { + name: 'rspec', + only: { refs: %w[/^deploy$/] }, + except: { refs: %w[/^deploy$/] } + } + end - context 'when using except' do - let(:attributes) do - { name: 'rspec', except: { refs: %w[deploy master] } } + it { is_expected.not_to be_included } end - - it { is_expected.not_to be_included } end - context 'when using both only and except policies' do - let(:attributes) do - { - name: 'rspec', - only: { refs: %w[deploy master] }, - except: { refs: %w[deploy master] } - } + context 'when branch policy matches' do + context 'when using only' do + let(:attributes) do + { name: 'rspec', only: { refs: %w[deploy master] } } + end + + it { is_expected.to be_included } end - it { is_expected.not_to be_included } - end - end + context 'when using except' do + let(:attributes) do + { name: 'rspec', except: { refs: %w[deploy master] } } + end - context 'when keyword policy matches' do - context 'when using only' do - let(:attributes) do - { name: 'rspec', only: { refs: %w[branches] } } + it { is_expected.not_to be_included } end - it { is_expected.to be_included } - end + context 'when using both only and except policies' do + let(:attributes) do + { + name: 'rspec', + only: { refs: %w[deploy master] }, + except: { refs: %w[deploy master] } + } + end - context 'when using except' do - let(:attributes) do - { name: 'rspec', except: { refs: %w[branches] } } + it { is_expected.not_to be_included } end - - it { is_expected.not_to be_included } end - context 'when using both only and except policies' do - let(:attributes) do - { - name: 'rspec', - only: { refs: %w[branches] }, - except: { refs: %w[branches] } - } + context 'when keyword policy matches' do + context 'when using only' do + let(:attributes) do + { name: 'rspec', only: { refs: %w[branches] } } + end + + it { is_expected.to be_included } end - it { is_expected.not_to be_included } - end - end + context 'when using except' do + let(:attributes) do + { name: 'rspec', except: { refs: %w[branches] } } + end - context 'when keyword policy does not match' do - context 'when using only' do - let(:attributes) do - { name: 'rspec', only: { refs: %w[tags] } } + it { is_expected.not_to be_included } end - it { is_expected.not_to be_included } - end + context 'when using both only and except policies' do + let(:attributes) do + { + name: 'rspec', + only: { refs: %w[branches] }, + except: { refs: %w[branches] } + } + end - context 'when using except' do - let(:attributes) do - { name: 'rspec', except: { refs: %w[tags] } } + it { is_expected.not_to be_included } end - - it { is_expected.to be_included } end - context 'when using both only and except policies' do - let(:attributes) do - { - name: 'rspec', - only: { refs: %w[tags] }, - except: { refs: %w[tags] } - } + context 'when keyword policy does not match' do + context 'when using only' do + let(:attributes) do + { name: 'rspec', only: { refs: %w[tags] } } + end + + it { is_expected.not_to be_included } end - it { is_expected.not_to be_included } - end - end + context 'when using except' do + let(:attributes) do + { name: 'rspec', except: { refs: %w[tags] } } + end - context 'with source-keyword policy' do - using RSpec::Parameterized + it { is_expected.to be_included } + end - let(:pipeline) do - build(:ci_empty_pipeline, ref: 'deploy', tag: false, source: source, project: project) - end + context 'when using both only and except policies' do + let(:attributes) do + { + name: 'rspec', + only: { refs: %w[tags] }, + except: { refs: %w[tags] } + } + end - context 'matches' do - where(:keyword, :source) do - [ - %w[pushes push], - %w[web web], - %w[triggers trigger], - %w[schedules schedule], - %w[api api], - %w[external external] - ] + it { is_expected.not_to be_included } end + end - with_them do - context 'using an only policy' do - let(:attributes) do - { name: 'rspec', only: { refs: [keyword] } } - end + context 'with source-keyword policy' do + using RSpec::Parameterized - it { is_expected.to be_included } + let(:pipeline) do + build(:ci_empty_pipeline, ref: 'deploy', tag: false, source: source, project: project) + end + + context 'matches' do + where(:keyword, :source) do + [ + %w[pushes push], + %w[web web], + %w[triggers trigger], + %w[schedules schedule], + %w[api api], + %w[external external] + ] end - context 'using an except policy' do - let(:attributes) do - { name: 'rspec', except: { refs: [keyword] } } + with_them do + context 'using an only policy' do + let(:attributes) do + { name: 'rspec', only: { refs: [keyword] } } + end + + it { is_expected.to be_included } end - it { is_expected.not_to be_included } - end + context 'using an except policy' do + let(:attributes) do + { name: 'rspec', except: { refs: [keyword] } } + end - context 'using both only and except policies' do - let(:attributes) do - { - name: 'rspec', - only: { refs: [keyword] }, - except: { refs: [keyword] } - } + it { is_expected.not_to be_included } end - it { is_expected.not_to be_included } + context 'using both only and except policies' do + let(:attributes) do + { + name: 'rspec', + only: { refs: [keyword] }, + except: { refs: [keyword] } + } + end + + it { is_expected.not_to be_included } + end end end - end - context 'non-matches' do - where(:keyword, :source) do - %w[web trigger schedule api external].map { |source| ['pushes', source] } + - %w[push trigger schedule api external].map { |source| ['web', source] } + - %w[push web schedule api external].map { |source| ['triggers', source] } + - %w[push web trigger api external].map { |source| ['schedules', source] } + - %w[push web trigger schedule external].map { |source| ['api', source] } + - %w[push web trigger schedule api].map { |source| ['external', source] } - end + context 'non-matches' do + where(:keyword, :source) do + %w[web trigger schedule api external].map { |source| ['pushes', source] } + + %w[push trigger schedule api external].map { |source| ['web', source] } + + %w[push web schedule api external].map { |source| ['triggers', source] } + + %w[push web trigger api external].map { |source| ['schedules', source] } + + %w[push web trigger schedule external].map { |source| ['api', source] } + + %w[push web trigger schedule api].map { |source| ['external', source] } + end - with_them do - context 'using an only policy' do - let(:attributes) do - { name: 'rspec', only: { refs: [keyword] } } + with_them do + context 'using an only policy' do + let(:attributes) do + { name: 'rspec', only: { refs: [keyword] } } + end + + it { is_expected.not_to be_included } end - it { is_expected.not_to be_included } - end + context 'using an except policy' do + let(:attributes) do + { name: 'rspec', except: { refs: [keyword] } } + end - context 'using an except policy' do - let(:attributes) do - { name: 'rspec', except: { refs: [keyword] } } + it { is_expected.to be_included } end - it { is_expected.to be_included } - end + context 'using both only and except policies' do + let(:attributes) do + { + name: 'rspec', + only: { refs: [keyword] }, + except: { refs: [keyword] } + } + end - context 'using both only and except policies' do - let(:attributes) do - { - name: 'rspec', - only: { refs: [keyword] }, - except: { refs: [keyword] } - } + it { is_expected.not_to be_included } end - - it { is_expected.not_to be_included } end end end - end - context 'when repository path matches' do - context 'when using only' do - let(:attributes) do - { name: 'rspec', only: { refs: ["branches@#{pipeline.project_full_path}"] } } + context 'when repository path matches' do + context 'when using only' do + let(:attributes) do + { name: 'rspec', only: { refs: ["branches@#{pipeline.project_full_path}"] } } + end + + it { is_expected.to be_included } end - it { is_expected.to be_included } - end + context 'when using except' do + let(:attributes) do + { name: 'rspec', except: { refs: ["branches@#{pipeline.project_full_path}"] } } + end - context 'when using except' do - let(:attributes) do - { name: 'rspec', except: { refs: ["branches@#{pipeline.project_full_path}"] } } + it { is_expected.not_to be_included } end - it { is_expected.not_to be_included } - end + context 'when using both only and except policies' do + let(:attributes) do + { + name: 'rspec', + only: { refs: ["branches@#{pipeline.project_full_path}"] }, + except: { refs: ["branches@#{pipeline.project_full_path}"] } + } + end - context 'when using both only and except policies' do - let(:attributes) do - { - name: 'rspec', - only: { refs: ["branches@#{pipeline.project_full_path}"] }, - except: { refs: ["branches@#{pipeline.project_full_path}"] } - } + it { is_expected.not_to be_included } end - it { is_expected.not_to be_included } - end - - context 'when using both only and except policies' do - let(:attributes) do - { - name: 'rspec', - only: { - refs: ["branches@#{pipeline.project_full_path}"] - }, - except: { - refs: ["branches@#{pipeline.project_full_path}"] + context 'when using both only and except policies' do + let(:attributes) do + { + name: 'rspec', + only: { + refs: ["branches@#{pipeline.project_full_path}"] + }, + except: { + refs: ["branches@#{pipeline.project_full_path}"] + } } - } - end + end - it { is_expected.not_to be_included } + it { is_expected.not_to be_included } + end end - end - context 'when repository path does not match' do - context 'when using only' do - let(:attributes) do - { name: 'rspec', only: { refs: %w[branches@fork] } } + context 'when repository path does not match' do + context 'when using only' do + let(:attributes) do + { name: 'rspec', only: { refs: %w[branches@fork] } } + end + + it { is_expected.not_to be_included } end - it { is_expected.not_to be_included } - end + context 'when using except' do + let(:attributes) do + { name: 'rspec', except: { refs: %w[branches@fork] } } + end - context 'when using except' do - let(:attributes) do - { name: 'rspec', except: { refs: %w[branches@fork] } } + it { is_expected.to be_included } end - it { is_expected.to be_included } - end + context 'when using both only and except policies' do + let(:attributes) do + { + name: 'rspec', + only: { refs: %w[branches@fork] }, + except: { refs: %w[branches@fork] } + } + end - context 'when using both only and except policies' do - let(:attributes) do - { - name: 'rspec', - only: { refs: %w[branches@fork] }, - except: { refs: %w[branches@fork] } - } + it { is_expected.not_to be_included } end - - it { is_expected.not_to be_included } end - end - context 'using rules:' do - using RSpec::Parameterized + context 'using rules:' do + using RSpec::Parameterized - let(:attributes) { { name: 'rspec', rules: rule_set, when: 'on_success' } } + let(:attributes) { { name: 'rspec', rules: rule_set, when: 'on_success' } } - context 'with a matching if: rule' do - context 'with an explicit `when: never`' do - where(:rule_set) do - [ - [[{ if: '$VARIABLE == null', when: 'never' }]], - [[{ if: '$VARIABLE == null', when: 'never' }, { if: '$VARIABLE == null', when: 'always' }]], - [[{ if: '$VARIABLE != "the wrong value"', when: 'never' }, { if: '$VARIABLE == null', when: 'always' }]] - ] - end + context 'with a matching if: rule' do + context 'with an explicit `when: never`' do + where(:rule_set) do + [ + [[{ if: '$VARIABLE == null', when: 'never' }]], + [[{ if: '$VARIABLE == null', when: 'never' }, { if: '$VARIABLE == null', when: 'always' }]], + [[{ if: '$VARIABLE != "the wrong value"', when: 'never' }, { if: '$VARIABLE == null', when: 'always' }]] + ] + end - with_them do - it { is_expected.not_to be_included } + with_them do + it { is_expected.not_to be_included } - it 'still correctly populates when:' do - expect(seed_build.attributes).to include(when: 'never') + it 'still correctly populates when:' do + expect(seed_build.attributes).to include(when: 'never') + end end end - end - context 'with an explicit `when: always`' do - where(:rule_set) do - [ - [[{ if: '$VARIABLE == null', when: 'always' }]], - [[{ if: '$VARIABLE == null', when: 'always' }, { if: '$VARIABLE == null', when: 'never' }]], - [[{ if: '$VARIABLE != "the wrong value"', when: 'always' }, { if: '$VARIABLE == null', when: 'never' }]] - ] + context 'with an explicit `when: always`' do + where(:rule_set) do + [ + [[{ if: '$VARIABLE == null', when: 'always' }]], + [[{ if: '$VARIABLE == null', when: 'always' }, { if: '$VARIABLE == null', when: 'never' }]], + [[{ if: '$VARIABLE != "the wrong value"', when: 'always' }, { if: '$VARIABLE == null', when: 'never' }]] + ] + end + + with_them do + it { is_expected.to be_included } + + it 'correctly populates when:' do + expect(seed_build.attributes).to include(when: 'always') + end + end end - with_them do - it { is_expected.to be_included } + context 'with an explicit `when: on_failure`' do + where(:rule_set) do + [ + [[{ if: '$CI_JOB_NAME == "rspec" && $VAR == null', when: 'on_failure' }]], + [[{ if: '$VARIABLE != null', when: 'delayed', start_in: '1 day' }, { if: '$CI_JOB_NAME == "rspec"', when: 'on_failure' }]], + [[{ if: '$VARIABLE == "the wrong value"', when: 'delayed', start_in: '1 day' }, { if: '$CI_BUILD_NAME == "rspec"', when: 'on_failure' }]] + ] + end - it 'correctly populates when:' do - expect(seed_build.attributes).to include(when: 'always') + with_them do + it { is_expected.to be_included } + + it 'correctly populates when:' do + expect(seed_build.attributes).to include(when: 'on_failure') + end end end - end - context 'with an explicit `when: on_failure`' do - where(:rule_set) do - [ - [[{ if: '$CI_JOB_NAME == "rspec" && $VAR == null', when: 'on_failure' }]], - [[{ if: '$VARIABLE != null', when: 'delayed', start_in: '1 day' }, { if: '$CI_JOB_NAME == "rspec"', when: 'on_failure' }]], - [[{ if: '$VARIABLE == "the wrong value"', when: 'delayed', start_in: '1 day' }, { if: '$CI_BUILD_NAME == "rspec"', when: 'on_failure' }]] - ] + context 'with an explicit `when: delayed`' do + where(:rule_set) do + [ + [[{ if: '$VARIABLE == null', when: 'delayed', start_in: '1 day' }]], + [[{ if: '$VARIABLE == null', when: 'delayed', start_in: '1 day' }, { if: '$VARIABLE == null', when: 'never' }]], + [[{ if: '$VARIABLE != "the wrong value"', when: 'delayed', start_in: '1 day' }, { if: '$VARIABLE == null', when: 'never' }]] + ] + end + + with_them do + it { is_expected.to be_included } + + it 'correctly populates when:' do + expect(seed_build.attributes).to include(when: 'delayed', options: { start_in: '1 day' }) + end + end end - with_them do - it { is_expected.to be_included } + context 'without an explicit when: value' do + where(:rule_set) do + [ + [[{ if: '$VARIABLE == null' }]], + [[{ if: '$VARIABLE == null' }, { if: '$VARIABLE == null' }]], + [[{ if: '$VARIABLE != "the wrong value"' }, { if: '$VARIABLE == null' }]] + ] + end - it 'correctly populates when:' do - expect(seed_build.attributes).to include(when: 'on_failure') + with_them do + it { is_expected.to be_included } + + it 'correctly populates when:' do + expect(seed_build.attributes).to include(when: 'on_success') + end end end end - context 'with an explicit `when: delayed`' do - where(:rule_set) do - [ - [[{ if: '$VARIABLE == null', when: 'delayed', start_in: '1 day' }]], - [[{ if: '$VARIABLE == null', when: 'delayed', start_in: '1 day' }, { if: '$VARIABLE == null', when: 'never' }]], - [[{ if: '$VARIABLE != "the wrong value"', when: 'delayed', start_in: '1 day' }, { if: '$VARIABLE == null', when: 'never' }]] - ] + context 'with a matching changes: rule' do + let(:pipeline) do + build(:ci_pipeline, project: project).tap do |pipeline| + stub_pipeline_modified_paths(pipeline, %w[app/models/ci/pipeline.rb spec/models/ci/pipeline_spec.rb .gitlab-ci.yml]) + end end - with_them do - it { is_expected.to be_included } + context 'with an explicit `when: never`' do + where(:rule_set) do + [ + [[{ changes: { paths: %w[*/**/*.rb] }, when: 'never' }, { changes: { paths: %w[*/**/*.rb] }, when: 'always' }]], + [[{ changes: { paths: %w[app/models/ci/pipeline.rb] }, when: 'never' }, { changes: { paths: %w[app/models/ci/pipeline.rb] }, when: 'always' }]], + [[{ changes: { paths: %w[spec/**/*.rb] }, when: 'never' }, { changes: { paths: %w[spec/**/*.rb] }, when: 'always' }]], + [[{ changes: { paths: %w[*.yml] }, when: 'never' }, { changes: { paths: %w[*.yml] }, when: 'always' }]], + [[{ changes: { paths: %w[.*.yml] }, when: 'never' }, { changes: { paths: %w[.*.yml] }, when: 'always' }]], + [[{ changes: { paths: %w[**/*] }, when: 'never' }, { changes: { paths: %w[**/*] }, when: 'always' }]], + [[{ changes: { paths: %w[*/**/*.rb *.yml] }, when: 'never' }, { changes: { paths: %w[*/**/*.rb *.yml] }, when: 'always' }]], + [[{ changes: { paths: %w[.*.yml **/*] }, when: 'never' }, { changes: { paths: %w[.*.yml **/*] }, when: 'always' }]] + ] + end - it 'correctly populates when:' do - expect(seed_build.attributes).to include(when: 'delayed', options: { start_in: '1 day' }) + with_them do + it { is_expected.not_to be_included } + + it 'correctly populates when:' do + expect(seed_build.attributes).to include(when: 'never') + end end end - end - context 'without an explicit when: value' do - where(:rule_set) do - [ - [[{ if: '$VARIABLE == null' }]], - [[{ if: '$VARIABLE == null' }, { if: '$VARIABLE == null' }]], - [[{ if: '$VARIABLE != "the wrong value"' }, { if: '$VARIABLE == null' }]] - ] - end + context 'with an explicit `when: always`' do + where(:rule_set) do + [ + [[{ changes: { paths: %w[*/**/*.rb] }, when: 'always' }, { changes: { paths: %w[*/**/*.rb] }, when: 'never' }]], + [[{ changes: { paths: %w[app/models/ci/pipeline.rb] }, when: 'always' }, { changes: { paths: %w[app/models/ci/pipeline.rb] }, when: 'never' }]], + [[{ changes: { paths: %w[spec/**/*.rb] }, when: 'always' }, { changes: { paths: %w[spec/**/*.rb] }, when: 'never' }]], + [[{ changes: { paths: %w[*.yml] }, when: 'always' }, { changes: { paths: %w[*.yml] }, when: 'never' }]], + [[{ changes: { paths: %w[.*.yml] }, when: 'always' }, { changes: { paths: %w[.*.yml] }, when: 'never' }]], + [[{ changes: { paths: %w[**/*] }, when: 'always' }, { changes: { paths: %w[**/*] }, when: 'never' }]], + [[{ changes: { paths: %w[*/**/*.rb *.yml] }, when: 'always' }, { changes: { paths: %w[*/**/*.rb *.yml] }, when: 'never' }]], + [[{ changes: { paths: %w[.*.yml **/*] }, when: 'always' }, { changes: { paths: %w[.*.yml **/*] }, when: 'never' }]] + ] + end - with_them do - it { is_expected.to be_included } + with_them do + it { is_expected.to be_included } - it 'correctly populates when:' do - expect(seed_build.attributes).to include(when: 'on_success') + it 'correctly populates when:' do + expect(seed_build.attributes).to include(when: 'always') + end end end - end - end - context 'with a matching changes: rule' do - let(:pipeline) do - build(:ci_pipeline, project: project).tap do |pipeline| - stub_pipeline_modified_paths(pipeline, %w[app/models/ci/pipeline.rb spec/models/ci/pipeline_spec.rb .gitlab-ci.yml]) + context 'without an explicit when: value' do + where(:rule_set) do + [ + [[{ changes: { paths: %w[*/**/*.rb] } }]], + [[{ changes: { paths: %w[app/models/ci/pipeline.rb] } }]], + [[{ changes: { paths: %w[spec/**/*.rb] } }]], + [[{ changes: { paths: %w[*.yml] } }]], + [[{ changes: { paths: %w[.*.yml] } }]], + [[{ changes: { paths: %w[**/*] } }]], + [[{ changes: { paths: %w[*/**/*.rb *.yml] } }]], + [[{ changes: { paths: %w[.*.yml **/*] } }]] + ] + end + + with_them do + it { is_expected.to be_included } + + it 'correctly populates when:' do + expect(seed_build.attributes).to include(when: 'on_success') + end + end end end - context 'with an explicit `when: never`' do + context 'with no matching rule' do where(:rule_set) do [ - [[{ changes: { paths: %w[*/**/*.rb] }, when: 'never' }, { changes: { paths: %w[*/**/*.rb] }, when: 'always' }]], - [[{ changes: { paths: %w[app/models/ci/pipeline.rb] }, when: 'never' }, { changes: { paths: %w[app/models/ci/pipeline.rb] }, when: 'always' }]], - [[{ changes: { paths: %w[spec/**/*.rb] }, when: 'never' }, { changes: { paths: %w[spec/**/*.rb] }, when: 'always' }]], - [[{ changes: { paths: %w[*.yml] }, when: 'never' }, { changes: { paths: %w[*.yml] }, when: 'always' }]], - [[{ changes: { paths: %w[.*.yml] }, when: 'never' }, { changes: { paths: %w[.*.yml] }, when: 'always' }]], - [[{ changes: { paths: %w[**/*] }, when: 'never' }, { changes: { paths: %w[**/*] }, when: 'always' }]], - [[{ changes: { paths: %w[*/**/*.rb *.yml] }, when: 'never' }, { changes: { paths: %w[*/**/*.rb *.yml] }, when: 'always' }]], - [[{ changes: { paths: %w[.*.yml **/*] }, when: 'never' }, { changes: { paths: %w[.*.yml **/*] }, when: 'always' }]] + [[{ if: '$VARIABLE != null', when: 'never' }]], + [[{ if: '$VARIABLE != null', when: 'never' }, { if: '$VARIABLE != null', when: 'always' }]], + [[{ if: '$VARIABLE == "the wrong value"', when: 'never' }, { if: '$VARIABLE != null', when: 'always' }]], + [[{ if: '$VARIABLE != null', when: 'always' }]], + [[{ if: '$VARIABLE != null', when: 'always' }, { if: '$VARIABLE != null', when: 'never' }]], + [[{ if: '$VARIABLE == "the wrong value"', when: 'always' }, { if: '$VARIABLE != null', when: 'never' }]], + [[{ if: '$VARIABLE != null' }]], + [[{ if: '$VARIABLE != null' }, { if: '$VARIABLE != null' }]], + [[{ if: '$VARIABLE == "the wrong value"' }, { if: '$VARIABLE != null' }]] ] end @@ -878,257 +971,249 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do end end - context 'with an explicit `when: always`' do - where(:rule_set) do - [ - [[{ changes: { paths: %w[*/**/*.rb] }, when: 'always' }, { changes: { paths: %w[*/**/*.rb] }, when: 'never' }]], - [[{ changes: { paths: %w[app/models/ci/pipeline.rb] }, when: 'always' }, { changes: { paths: %w[app/models/ci/pipeline.rb] }, when: 'never' }]], - [[{ changes: { paths: %w[spec/**/*.rb] }, when: 'always' }, { changes: { paths: %w[spec/**/*.rb] }, when: 'never' }]], - [[{ changes: { paths: %w[*.yml] }, when: 'always' }, { changes: { paths: %w[*.yml] }, when: 'never' }]], - [[{ changes: { paths: %w[.*.yml] }, when: 'always' }, { changes: { paths: %w[.*.yml] }, when: 'never' }]], - [[{ changes: { paths: %w[**/*] }, when: 'always' }, { changes: { paths: %w[**/*] }, when: 'never' }]], - [[{ changes: { paths: %w[*/**/*.rb *.yml] }, when: 'always' }, { changes: { paths: %w[*/**/*.rb *.yml] }, when: 'never' }]], - [[{ changes: { paths: %w[.*.yml **/*] }, when: 'always' }, { changes: { paths: %w[.*.yml **/*] }, when: 'never' }]] - ] + context 'with a rule using CI_ENVIRONMENT_NAME variable' do + let(:rule_set) do + [{ if: '$CI_ENVIRONMENT_NAME == "test"' }] end - with_them do + context 'when environment:name satisfies the rule' do + let(:attributes) { { name: 'rspec', rules: rule_set, environment: 'test', when: 'on_success' } } + it { is_expected.to be_included } it 'correctly populates when:' do - expect(seed_build.attributes).to include(when: 'always') + expect(seed_build.attributes).to include(when: 'on_success') end end - end - context 'without an explicit when: value' do - where(:rule_set) do - [ - [[{ changes: { paths: %w[*/**/*.rb] } }]], - [[{ changes: { paths: %w[app/models/ci/pipeline.rb] } }]], - [[{ changes: { paths: %w[spec/**/*.rb] } }]], - [[{ changes: { paths: %w[*.yml] } }]], - [[{ changes: { paths: %w[.*.yml] } }]], - [[{ changes: { paths: %w[**/*] } }]], - [[{ changes: { paths: %w[*/**/*.rb *.yml] } }]], - [[{ changes: { paths: %w[.*.yml **/*] } }]] - ] + context 'when environment:name does not satisfy rule' do + let(:attributes) { { name: 'rspec', rules: rule_set, environment: 'dev', when: 'on_success' } } + + it { is_expected.not_to be_included } + + it 'correctly populates when:' do + expect(seed_build.attributes).to include(when: 'never') + end end - with_them do - it { is_expected.to be_included } + context 'when environment:name is not set' do + it { is_expected.not_to be_included } it 'correctly populates when:' do - expect(seed_build.attributes).to include(when: 'on_success') + expect(seed_build.attributes).to include(when: 'never') end end end - end - context 'with no matching rule' do - where(:rule_set) do - [ - [[{ if: '$VARIABLE != null', when: 'never' }]], - [[{ if: '$VARIABLE != null', when: 'never' }, { if: '$VARIABLE != null', when: 'always' }]], - [[{ if: '$VARIABLE == "the wrong value"', when: 'never' }, { if: '$VARIABLE != null', when: 'always' }]], - [[{ if: '$VARIABLE != null', when: 'always' }]], - [[{ if: '$VARIABLE != null', when: 'always' }, { if: '$VARIABLE != null', when: 'never' }]], - [[{ if: '$VARIABLE == "the wrong value"', when: 'always' }, { if: '$VARIABLE != null', when: 'never' }]], - [[{ if: '$VARIABLE != null' }]], - [[{ if: '$VARIABLE != null' }, { if: '$VARIABLE != null' }]], - [[{ if: '$VARIABLE == "the wrong value"' }, { if: '$VARIABLE != null' }]] - ] + context 'with no rules' do + let(:rule_set) { [] } + + it { is_expected.not_to be_included } + + it 'correctly populates when:' do + expect(seed_build.attributes).to include(when: 'never') + end end - with_them do + context 'with invalid rules raising error' do + let(:rule_set) do + [ + { changes: { paths: ['README.md'], compare_to: 'invalid-ref' }, when: 'never' } + ] + end + it { is_expected.not_to be_included } it 'correctly populates when:' do expect(seed_build.attributes).to include(when: 'never') end + + it 'returns an error' do + expect(seed_build.errors).to contain_exactly( + 'Failed to parse rule for rspec: rules:changes:compare_to is not a valid ref' + ) + end end end + end - context 'with no rules' do - let(:rule_set) { [] } + describe 'applying needs: dependency' do + subject { seed_build } - it { is_expected.not_to be_included } + let(:needs_count) { 1 } - it 'correctly populates when:' do - expect(seed_build.attributes).to include(when: 'never') - end + let(:needs_attributes) do + Array.new(needs_count, name: 'build') end - context 'with invalid rules raising error' do - let(:rule_set) do - [ - { changes: { paths: ['README.md'], compare_to: 'invalid-ref' }, when: 'never' } - ] - end + let(:attributes) do + { + name: 'rspec', + needs_attributes: needs_attributes + } + end - it { is_expected.not_to be_included } + context 'when build job is not present in prior stages' do + it "is included" do + is_expected.to be_included + end - it 'correctly populates when:' do - expect(seed_build.attributes).to include(when: 'never') + it "returns an error" do + expect(subject.errors).to contain_exactly( + "'rspec' job needs 'build' job, but 'build' is not in any previous stage") end - it 'returns an error' do - expect(seed_build.errors).to contain_exactly( - 'Failed to parse rule for rspec: rules:changes:compare_to is not a valid ref' - ) + context 'when the needed job is optional' do + let(:needs_attributes) { [{ name: 'build', optional: true }] } + + it "does not return an error" do + expect(subject.errors).to be_empty + end end end - end - end - describe 'applying needs: dependency' do - subject { seed_build } + context 'when build job is part of prior stages' do + let(:stage_attributes) do + { + name: 'build', + index: 0, + builds: [{ name: 'build' }] + } + end - let(:needs_count) { 1 } + let(:stage_seed) do + Gitlab::Ci::Pipeline::Seed::Stage.new(seed_context, stage_attributes, []) + end - let(:needs_attributes) do - Array.new(needs_count, name: 'build') - end + let(:previous_stages) { [stage_seed] } - let(:attributes) do - { - name: 'rspec', - needs_attributes: needs_attributes - } - end + it "is included" do + is_expected.to be_included + end - context 'when build job is not present in prior stages' do - it "is included" do - is_expected.to be_included + it "does not have errors" do + expect(subject.errors).to be_empty + end end - it "returns an error" do - expect(subject.errors).to contain_exactly( - "'rspec' job needs 'build' job, but 'build' is not in any previous stage") - end + context 'when build job is part of the same stage' do + let(:current_stage) { double(seeds_names: [attributes[:name], 'build']) } - context 'when the needed job is optional' do - let(:needs_attributes) { [{ name: 'build', optional: true }] } + it 'is included' do + is_expected.to be_included + end - it "does not return an error" do + it 'does not have errors' do expect(subject.errors).to be_empty end end - end - - context 'when build job is part of prior stages' do - let(:stage_attributes) do - { - name: 'build', - index: 0, - builds: [{ name: 'build' }] - } - end - - let(:stage_seed) do - Gitlab::Ci::Pipeline::Seed::Stage.new(seed_context, stage_attributes, []) - end - let(:previous_stages) { [stage_seed] } + context 'when using 101 needs' do + let(:needs_count) { 101 } - it "is included" do - is_expected.to be_included - end + it "returns an error" do + expect(subject.errors).to contain_exactly( + "rspec: one job can only need 50 others, but you have listed 101. See needs keyword documentation for more details") + end - it "does not have errors" do - expect(subject.errors).to be_empty - end - end + context 'when ci_needs_size_limit is set to 100' do + before do + project.actual_limits.update!(ci_needs_size_limit: 100) + end - context 'when build job is part of the same stage' do - let(:current_stage) { double(seeds_names: [attributes[:name], 'build']) } + it "returns an error" do + expect(subject.errors).to contain_exactly( + "rspec: one job can only need 100 others, but you have listed 101. See needs keyword documentation for more details") + end + end - it 'is included' do - is_expected.to be_included - end + context 'when ci_needs_size_limit is set to 0' do + before do + project.actual_limits.update!(ci_needs_size_limit: 0) + end - it 'does not have errors' do - expect(subject.errors).to be_empty + it "returns an error" do + expect(subject.errors).to contain_exactly( + "rspec: one job can only need 0 others, but you have listed 101. See needs keyword documentation for more details") + end + end end end - context 'when using 101 needs' do - let(:needs_count) { 101 } + describe 'applying pipeline variables' do + subject { seed_build } - it "returns an error" do - expect(subject.errors).to contain_exactly( - "rspec: one job can only need 50 others, but you have listed 101. See needs keyword documentation for more details") + let(:pipeline_variables) { [] } + let(:pipeline) do + build(:ci_empty_pipeline, project: project, sha: head_sha, variables: pipeline_variables) end - context 'when ci_needs_size_limit is set to 100' do - before do - project.actual_limits.update!(ci_needs_size_limit: 100) + context 'containing variable references' do + let(:pipeline_variables) do + [ + build(:ci_pipeline_variable, key: 'A', value: '$B'), + build(:ci_pipeline_variable, key: 'B', value: '$C') + ] end - it "returns an error" do - expect(subject.errors).to contain_exactly( - "rspec: one job can only need 100 others, but you have listed 101. See needs keyword documentation for more details") + it "does not have errors" do + expect(subject.errors).to be_empty end end - context 'when ci_needs_size_limit is set to 0' do - before do - project.actual_limits.update!(ci_needs_size_limit: 0) + context 'containing cyclic reference' do + let(:pipeline_variables) do + [ + build(:ci_pipeline_variable, key: 'A', value: '$B'), + build(:ci_pipeline_variable, key: 'B', value: '$C'), + build(:ci_pipeline_variable, key: 'C', value: '$A') + ] end it "returns an error" do expect(subject.errors).to contain_exactly( - "rspec: one job can only need 0 others, but you have listed 101. See needs keyword documentation for more details") + 'rspec: circular variable reference detected: ["A", "B", "C"]') + end + + context 'with job:rules:[if:]' do + let(:attributes) { { name: 'rspec', ref: 'master', rules: [{ if: '$C != null', when: 'always' }] } } + + it "included? does not raise" do + expect { subject.included? }.not_to raise_error + end + + it "included? returns true" do + expect(subject.included?).to eq(true) + end end end end end - describe 'applying pipeline variables' do - subject { seed_build } - - let(:pipeline_variables) { [] } - let(:pipeline) do - build(:ci_empty_pipeline, project: project, sha: head_sha, variables: pipeline_variables) + describe 'feature flag ci_reuse_build_in_seed_context' do + let(:attributes) do + { name: 'rspec', rules: [{ if: '$VARIABLE == null' }], when: 'on_success' } end - context 'containing variable references' do - let(:pipeline_variables) do - [ - build(:ci_pipeline_variable, key: 'A', value: '$B'), - build(:ci_pipeline_variable, key: 'B', value: '$C') - ] - end + context 'when enabled' do + it_behaves_like 'build seed' - it "does not have errors" do - expect(subject.errors).to be_empty + it 'initializes the build once' do + expect(Ci::Build).to receive(:new).once.and_call_original + seed_build.to_resource end end - context 'containing cyclic reference' do - let(:pipeline_variables) do - [ - build(:ci_pipeline_variable, key: 'A', value: '$B'), - build(:ci_pipeline_variable, key: 'B', value: '$C'), - build(:ci_pipeline_variable, key: 'C', value: '$A') - ] - end - - it "returns an error" do - expect(subject.errors).to contain_exactly( - 'rspec: circular variable reference detected: ["A", "B", "C"]') + context 'when disabled' do + before do + stub_feature_flags(ci_reuse_build_in_seed_context: false) end - context 'with job:rules:[if:]' do - let(:attributes) { { name: 'rspec', ref: 'master', rules: [{ if: '$C != null', when: 'always' }] } } - - it "included? does not raise" do - expect { subject.included? }.not_to raise_error - end + it_behaves_like 'build seed' - it "included? returns true" do - expect(subject.included?).to eq(true) - end + it 'initializes the build twice' do + expect(Ci::Build).to receive(:new).twice.and_call_original + seed_build.to_resource end end end diff --git a/spec/lib/gitlab/ci/pipeline/seed/stage_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/stage_spec.rb index a632b5dedcf..288ac3f3854 100644 --- a/spec/lib/gitlab/ci/pipeline/seed/stage_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/seed/stage_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::Ci::Pipeline::Seed::Stage do +RSpec.describe Gitlab::Ci::Pipeline::Seed::Stage, feature_category: :pipeline_authoring do let(:project) { create(:project, :repository) } let(:pipeline) { create(:ci_empty_pipeline, project: project) } let(:previous_stages) { [] } diff --git a/spec/lib/gitlab/ci/reports/sbom/component_spec.rb b/spec/lib/gitlab/ci/reports/sbom/component_spec.rb index cdaf9354104..5dbcc1991d4 100644 --- a/spec/lib/gitlab/ci/reports/sbom/component_spec.rb +++ b/spec/lib/gitlab/ci/reports/sbom/component_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::Ci::Reports::Sbom::Component do +RSpec.describe Gitlab::Ci::Reports::Sbom::Component, feature_category: :dependency_management do let(:component_type) { 'library' } let(:name) { 'component-name' } let(:purl_type) { 'npm' } diff --git a/spec/lib/gitlab/ci/reports/sbom/report_spec.rb b/spec/lib/gitlab/ci/reports/sbom/report_spec.rb index f9a83378f46..5d281f6ed76 100644 --- a/spec/lib/gitlab/ci/reports/sbom/report_spec.rb +++ b/spec/lib/gitlab/ci/reports/sbom/report_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::Ci::Reports::Sbom::Report do +RSpec.describe Gitlab::Ci::Reports::Sbom::Report, feature_category: :dependency_management do subject(:report) { described_class.new } describe '#valid?' do diff --git a/spec/lib/gitlab/ci/reports/sbom/reports_spec.rb b/spec/lib/gitlab/ci/reports/sbom/reports_spec.rb index 75ea91251eb..4fb766d7d38 100644 --- a/spec/lib/gitlab/ci/reports/sbom/reports_spec.rb +++ b/spec/lib/gitlab/ci/reports/sbom/reports_spec.rb @@ -2,7 +2,7 @@ require 'fast_spec_helper' -RSpec.describe Gitlab::Ci::Reports::Sbom::Reports do +RSpec.describe Gitlab::Ci::Reports::Sbom::Reports, feature_category: :dependency_management do subject(:reports_list) { described_class.new } describe '#add_report' do diff --git a/spec/lib/gitlab/ci/reports/sbom/source_spec.rb b/spec/lib/gitlab/ci/reports/sbom/source_spec.rb index 343c0d8c15c..63b8e5fdf01 100644 --- a/spec/lib/gitlab/ci/reports/sbom/source_spec.rb +++ b/spec/lib/gitlab/ci/reports/sbom/source_spec.rb @@ -2,7 +2,7 @@ require 'fast_spec_helper' -RSpec.describe Gitlab::Ci::Reports::Sbom::Source do +RSpec.describe Gitlab::Ci::Reports::Sbom::Source, feature_category: :dependency_management do let(:attributes) do { type: :dependency_scanning, diff --git a/spec/lib/gitlab/ci/reports/security/reports_spec.rb b/spec/lib/gitlab/ci/reports/security/reports_spec.rb index 33f3317c655..cb6a91655ed 100644 --- a/spec/lib/gitlab/ci/reports/security/reports_spec.rb +++ b/spec/lib/gitlab/ci/reports/security/reports_spec.rb @@ -52,105 +52,4 @@ RSpec.describe Gitlab::Ci::Reports::Security::Reports do it { is_expected.to match_array(expected_findings) } end - - describe "#violates_default_policy_against?" do - let(:high_severity_dast) { build(:ci_reports_security_finding, severity: 'high', report_type: 'dast') } - let(:vulnerabilities_allowed) { 0 } - let(:severity_levels) { %w(critical high) } - let(:vulnerability_states) { %w(newly_detected) } - - subject { security_reports.violates_default_policy_against?(target_reports, vulnerabilities_allowed, severity_levels, vulnerability_states) } - - before do - security_reports.get_report('sast', artifact).add_finding(high_severity_dast) - end - - context 'when the target_reports is `nil`' do - let(:target_reports) { nil } - - context 'with severity levels matching the existing vulnerabilities' do - it { is_expected.to be(true) } - end - - context "without any severity levels matching the existing vulnerabilities" do - let(:severity_levels) { %w(critical) } - - it { is_expected.to be(false) } - end - end - - context 'when the target_reports is not `nil`' do - let(:target_reports) { described_class.new(pipeline) } - - context "when a report has a new unsafe vulnerability" do - context 'with severity levels matching the existing vulnerabilities' do - it { is_expected.to be(true) } - end - - it { is_expected.to be(true) } - - context 'with vulnerabilities_allowed higher than the number of new vulnerabilities' do - let(:vulnerabilities_allowed) { 10000 } - - it { is_expected.to be(false) } - end - - context "without any severity levels matching the existing vulnerabilities" do - let(:severity_levels) { %w(critical) } - - it { is_expected.to be(false) } - end - end - - context "when none of the reports have a new unsafe vulnerability" do - before do - target_reports.get_report('sast', artifact).add_finding(high_severity_dast) - end - - it { is_expected.to be(false) } - end - - context 'with related report_types' do - let(:report_types) { %w(dast sast) } - - subject { security_reports.violates_default_policy_against?(target_reports, vulnerabilities_allowed, severity_levels, vulnerability_states, report_types) } - - it { is_expected.to be(true) } - end - - context 'with unrelated report_types' do - let(:report_types) { %w(dependency_scanning sast) } - - subject { security_reports.violates_default_policy_against?(target_reports, vulnerabilities_allowed, severity_levels, vulnerability_states, report_types) } - - it { is_expected.to be(false) } - end - - context 'when target_reports is not nil and reports is empty' do - let(:without_reports) { described_class.new(pipeline) } - - subject { without_reports.violates_default_policy_against?(target_reports, vulnerabilities_allowed, severity_levels, vulnerability_states) } - - before do - target_reports.get_report('sast', artifact).add_finding(high_severity_dast) - end - - context 'when require_approval_on_scan_removal feature is enabled' do - before do - stub_feature_flags(require_approval_on_scan_removal: true) - end - - it { is_expected.to be(true) } - end - - context 'when require_approval_on_scan_removal feature is disabled' do - before do - stub_feature_flags(require_approval_on_scan_removal: false) - end - - it { is_expected.to be(false) } - end - end - end - end end diff --git a/spec/lib/gitlab/ci/runner_instructions_spec.rb b/spec/lib/gitlab/ci/runner_instructions_spec.rb index f872c631a50..56f69720b87 100644 --- a/spec/lib/gitlab/ci/runner_instructions_spec.rb +++ b/spec/lib/gitlab/ci/runner_instructions_spec.rb @@ -69,6 +69,7 @@ RSpec.describe Gitlab::Ci::RunnerInstructions do 'windows' | 'amd64' 'windows' | '386' 'osx' | 'amd64' + 'osx' | 'arm64' end with_them do diff --git a/spec/lib/gitlab/ci/trace/archive_spec.rb b/spec/lib/gitlab/ci/trace/archive_spec.rb index f91cb03883a..582c4ad343f 100644 --- a/spec/lib/gitlab/ci/trace/archive_spec.rb +++ b/spec/lib/gitlab/ci/trace/archive_spec.rb @@ -75,15 +75,6 @@ RSpec.describe Gitlab::Ci::Trace::Archive do include_context 'with FIPS' end - context 'with background_upload enabled' do - before do - stub_artifacts_object_storage(background_upload: true) - end - - it_behaves_like 'skips validations' - include_context 'with FIPS' - end - context 'with direct_upload enabled' do before do stub_artifacts_object_storage(direct_upload: true) diff --git a/spec/lib/gitlab/ci/variables/builder_spec.rb b/spec/lib/gitlab/ci/variables/builder_spec.rb index 52ba85d2df1..5aa752ee429 100644 --- a/spec/lib/gitlab/ci/variables/builder_spec.rb +++ b/spec/lib/gitlab/ci/variables/builder_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::Ci::Variables::Builder, :clean_gitlab_redis_cache do +RSpec.describe Gitlab::Ci::Variables::Builder, :clean_gitlab_redis_cache, feature_category: :pipeline_authoring do include Ci::TemplateHelpers let_it_be(:group) { create(:group) } let_it_be(:project) { create(:project, :repository, namespace: group) } @@ -13,7 +13,8 @@ RSpec.describe Gitlab::Ci::Variables::Builder, :clean_gitlab_redis_cache do name: 'rspec:test 1', pipeline: pipeline, user: user, - yaml_variables: [{ key: 'YAML_VARIABLE', value: 'value' }] + yaml_variables: [{ key: 'YAML_VARIABLE', value: 'value' }], + environment: 'test' ) end @@ -32,6 +33,8 @@ RSpec.describe Gitlab::Ci::Variables::Builder, :clean_gitlab_redis_cache do value: job.stage_name }, { key: 'CI_NODE_TOTAL', value: '1' }, + { key: 'CI_ENVIRONMENT_NAME', + value: 'test' }, { key: 'CI_BUILD_NAME', value: 'rspec:test 1' }, { key: 'CI_BUILD_STAGE', @@ -76,6 +79,8 @@ RSpec.describe Gitlab::Ci::Variables::Builder, :clean_gitlab_redis_cache do value: project.full_path_slug }, { key: 'CI_PROJECT_NAMESPACE', value: project.namespace.full_path }, + { key: 'CI_PROJECT_NAMESPACE_ID', + value: project.namespace.id.to_s }, { key: 'CI_PROJECT_ROOT_NAMESPACE', value: project.namespace.root_ancestor.path }, { key: 'CI_PROJECT_URL', @@ -276,11 +281,17 @@ RSpec.describe Gitlab::Ci::Variables::Builder, :clean_gitlab_redis_cache do subject { builder.kubernetes_variables(environment: nil, job: job) } before do - allow(Ci::GenerateKubeconfigService).to receive(:new).with(job.pipeline, token: job.token).and_return(service) + allow(Ci::GenerateKubeconfigService).to receive(:new).with(job.pipeline, token: job.token, environment: anything).and_return(service) end it { is_expected.to include(key: 'KUBECONFIG', value: 'example-kubeconfig', public: false, file: true) } + it 'calls the GenerateKubeconfigService with the correct arguments' do + expect(Ci::GenerateKubeconfigService).to receive(:new).with(job.pipeline, token: job.token, environment: nil) + + subject + end + context 'generated config is invalid' do let(:template_valid) { false } @@ -297,6 +308,16 @@ RSpec.describe Gitlab::Ci::Variables::Builder, :clean_gitlab_redis_cache do expect(subject['KUBECONFIG'].value).to eq('example-kubeconfig') expect(subject['OTHER'].value).to eq('some value') end + + context 'when environment is not nil' do + subject { builder.kubernetes_variables(environment: 'production', job: job) } + + it 'passes the environment when generating the KUBECONFIG' do + expect(Ci::GenerateKubeconfigService).to receive(:new).with(job.pipeline, token: job.token, environment: 'production') + + subject + end + end end describe '#deployment_variables' do diff --git a/spec/lib/gitlab/ci/yaml_processor/result_spec.rb b/spec/lib/gitlab/ci/yaml_processor/result_spec.rb index 7f203168706..5c9f156e054 100644 --- a/spec/lib/gitlab/ci/yaml_processor/result_spec.rb +++ b/spec/lib/gitlab/ci/yaml_processor/result_spec.rb @@ -12,6 +12,20 @@ module Gitlab let(:ci_config) { Gitlab::Ci::Config.new(config_content, user: user) } let(:result) { described_class.new(ci_config: ci_config, warnings: ci_config&.warnings) } + describe '#builds' do + context 'when a job has ID tokens' do + let(:config_content) do + YAML.dump( + test: { stage: 'test', script: 'echo', id_tokens: { TEST_ID_TOKEN: { aud: 'https://gitlab.com' } } } + ) + end + + it 'includes `id_tokens`' do + expect(result.builds.first[:id_tokens]).to eq({ TEST_ID_TOKEN: { aud: 'https://gitlab.com' } }) + end + end + end + describe '#config_metadata' do subject(:config_metadata) { result.config_metadata } diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb index 5de813f7739..ae98d2e0cad 100644 --- a/spec/lib/gitlab/ci/yaml_processor_spec.rb +++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb @@ -870,6 +870,69 @@ module Gitlab end end end + + describe "hooks" do + context 'when it is a simple script' do + let(:config) do + { + test: { script: ["script"], + hooks: { pre_get_sources_script: ["echo 1", "echo 2", "pwd"] } } + } + end + + it "returns hooks in options" do + expect(subject[:options][:hooks]).to eq( + { pre_get_sources_script: ["echo 1", "echo 2", "pwd"] } + ) + end + end + + context 'when it is nested arrays of strings' do + let(:config) do + { + test: { script: ["script"], + hooks: { pre_get_sources_script: [[["global script"], "echo 1"], "echo 2", ["ls"], "pwd"] } } + } + end + + it "returns hooks in options" do + expect(subject[:options][:hooks]).to eq( + { pre_get_sources_script: ["global script", "echo 1", "echo 2", "ls", "pwd"] } + ) + end + end + + context 'when receiving from the default' do + let(:config) do + { + default: { hooks: { pre_get_sources_script: ["echo 1", "echo 2", "pwd"] } }, + test: { script: ["script"] } + } + end + + it "inherits hooks" do + expect(subject[:options][:hooks]).to eq( + { pre_get_sources_script: ["echo 1", "echo 2", "pwd"] } + ) + end + end + + context 'when overriding the default' do + let(:config) do + { + default: { hooks: { pre_get_sources_script: ["echo 1", "echo 2", "pwd"] } }, + test: { script: ["script"], + hooks: { pre_get_sources_script: ["echo 3", "echo 4", "pwd"] } } + } + end + + it "overrides hooks" do + expect(subject[:options][:hooks]).to eq( + { pre_get_sources_script: ["echo 3", "echo 4", "pwd"] } + ) + end + end + end end describe "Image and service handling" do @@ -2883,7 +2946,7 @@ module Gitlab context 'returns errors if job artifacts:when is not an a predefined value' do let(:config) { YAML.dump({ stages: %w(build test), rspec: { script: "test", artifacts: { when: 1 } } }) } - it_behaves_like 'returns errors', 'jobs:rspec:artifacts when should be on_success, on_failure or always' + it_behaves_like 'returns errors', 'jobs:rspec:artifacts when should be one of: on_success, on_failure, always' end context 'returns errors if job artifacts:expire_in is not an a string' do |