diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-03-16 21:18:33 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-03-16 21:18:33 +0300 |
commit | f64a639bcfa1fc2bc89ca7db268f594306edfd7c (patch) | |
tree | a2c3c2ebcc3b45e596949db485d6ed18ffaacfa1 /spec/lib/gitlab/ci | |
parent | bfbc3e0d6583ea1a91f627528bedc3d65ba4b10f (diff) |
Add latest changes from gitlab-org/gitlab@13-10-stable-eev13.10.0-rc40
Diffstat (limited to 'spec/lib/gitlab/ci')
33 files changed, 2508 insertions, 842 deletions
diff --git a/spec/lib/gitlab/ci/artifacts/metrics_spec.rb b/spec/lib/gitlab/ci/artifacts/metrics_spec.rb new file mode 100644 index 00000000000..3a2095498ec --- /dev/null +++ b/spec/lib/gitlab/ci/artifacts/metrics_spec.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Artifacts::Metrics, :prometheus do + let(:metrics) { described_class.new } + + describe '#increment_destroyed_artifacts' do + context 'when incrementing by more than one' do + let(:counter) { metrics.send(:destroyed_artifacts_counter) } + + it 'increments a single counter' do + subject.increment_destroyed_artifacts(10) + subject.increment_destroyed_artifacts(20) + subject.increment_destroyed_artifacts(30) + + expect(counter.get).to eq 60 + expect(counter.values.count).to eq 1 + end + end + end +end diff --git a/spec/lib/gitlab/ci/build/cache_spec.rb b/spec/lib/gitlab/ci/build/cache_spec.rb new file mode 100644 index 00000000000..9188045988b --- /dev/null +++ b/spec/lib/gitlab/ci/build/cache_spec.rb @@ -0,0 +1,105 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Build::Cache do + describe '.initialize' do + context 'when the multiple cache feature flag is disabled' do + before do + stub_feature_flags(multiple_cache_per_job: false) + end + + it 'instantiates a cache seed' do + cache_config = { key: 'key-a' } + pipeline = double(::Ci::Pipeline) + cache_seed = double(Gitlab::Ci::Pipeline::Seed::Build::Cache) + allow(Gitlab::Ci::Pipeline::Seed::Build::Cache).to receive(:new).and_return(cache_seed) + + cache = described_class.new(cache_config, pipeline) + + expect(Gitlab::Ci::Pipeline::Seed::Build::Cache).to have_received(:new).with(pipeline, cache_config) + expect(cache.instance_variable_get(:@cache)).to eq(cache_seed) + end + end + + context 'when the multiple cache feature flag is enabled' do + context 'when the cache is an array' do + it 'instantiates an array of cache seeds' do + cache_config = [{ key: 'key-a' }, { key: 'key-b' }] + pipeline = double(::Ci::Pipeline) + cache_seed_a = double(Gitlab::Ci::Pipeline::Seed::Build::Cache) + cache_seed_b = double(Gitlab::Ci::Pipeline::Seed::Build::Cache) + allow(Gitlab::Ci::Pipeline::Seed::Build::Cache).to receive(:new).and_return(cache_seed_a, cache_seed_b) + + 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(cache.instance_variable_get(:@cache)).to eq([cache_seed_a, cache_seed_b]) + end + end + + context 'when the cache is a hash' do + it 'instantiates a cache seed' do + cache_config = { key: 'key-a' } + pipeline = double(::Ci::Pipeline) + cache_seed = double(Gitlab::Ci::Pipeline::Seed::Build::Cache) + allow(Gitlab::Ci::Pipeline::Seed::Build::Cache).to receive(:new).and_return(cache_seed) + + cache = described_class.new(cache_config, pipeline) + + expect(Gitlab::Ci::Pipeline::Seed::Build::Cache).to have_received(:new).with(pipeline, cache_config) + expect(cache.instance_variable_get(:@cache)).to eq([cache_seed]) + end + end + end + end + + describe '#cache_attributes' do + context 'when the multiple cache feature flag is disabled' do + before do + stub_feature_flags(multiple_cache_per_job: false) + end + + it "returns the cache seed's build attributes" do + cache_config = { key: 'key-a' } + pipeline = double(::Ci::Pipeline) + cache = described_class.new(cache_config, pipeline) + + attributes = cache.cache_attributes + + expect(attributes).to eq({ + options: { cache: { key: 'key-a' } } + }) + end + end + + context 'when the multiple cache feature flag is enabled' do + context 'when there are no caches' do + it 'returns an empty hash' do + cache_config = [] + pipeline = double(::Ci::Pipeline) + cache = described_class.new(cache_config, pipeline) + + attributes = cache.cache_attributes + + expect(attributes).to eq({}) + end + end + + context 'when there are caches' do + it 'returns the structured attributes for the caches' do + cache_config = [{ key: 'key-a' }, { key: 'key-b' }] + pipeline = double(::Ci::Pipeline) + cache = described_class.new(cache_config, pipeline) + + attributes = cache.cache_attributes + + expect(attributes).to eq({ + options: { cache: cache_config } + }) + end + end + end + 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 61ca8e759b5..46447231424 100644 --- a/spec/lib/gitlab/ci/build/context/build_spec.rb +++ b/spec/lib/gitlab/ci/build/context/build_spec.rb @@ -9,7 +9,9 @@ RSpec.describe Gitlab::Ci::Build::Context::Build do let(:context) { described_class.new(pipeline, seed_attributes) } describe '#variables' do - subject { context.variables } + subject { context.variables.to_hash } + + it { expect(context.variables).to be_instance_of(Gitlab::Ci::Variables::Collection) } it { is_expected.to include('CI_COMMIT_REF_NAME' => 'master') } it { is_expected.to include('CI_PIPELINE_IID' => pipeline.iid.to_s) } diff --git a/spec/lib/gitlab/ci/build/context/global_spec.rb b/spec/lib/gitlab/ci/build/context/global_spec.rb index 7394708f9b6..61f2b90426d 100644 --- a/spec/lib/gitlab/ci/build/context/global_spec.rb +++ b/spec/lib/gitlab/ci/build/context/global_spec.rb @@ -9,7 +9,9 @@ RSpec.describe Gitlab::Ci::Build::Context::Global do let(:context) { described_class.new(pipeline, yaml_variables: yaml_variables) } describe '#variables' do - subject { context.variables } + subject { context.variables.to_hash } + + it { expect(context.variables).to be_instance_of(Gitlab::Ci::Variables::Collection) } it { is_expected.to include('CI_COMMIT_REF_NAME' => 'master') } it { is_expected.to include('CI_PIPELINE_IID' => pipeline.iid.to_s) } diff --git a/spec/lib/gitlab/ci/build/policy/variables_spec.rb b/spec/lib/gitlab/ci/build/policy/variables_spec.rb index f692aa6146e..6c8c968dc0c 100644 --- a/spec/lib/gitlab/ci/build/policy/variables_spec.rb +++ b/spec/lib/gitlab/ci/build/policy/variables_spec.rb @@ -16,7 +16,7 @@ RSpec.describe Gitlab::Ci::Build::Policy::Variables do let(:seed) do double('build seed', to_resource: ci_build, - variables: ci_build.scoped_variables_hash + variables: ci_build.scoped_variables ) end @@ -91,7 +91,7 @@ RSpec.describe Gitlab::Ci::Build::Policy::Variables do let(:seed) do double('bridge seed', to_resource: bridge, - variables: ci_build.scoped_variables_hash + variables: ci_build.scoped_variables ) end diff --git a/spec/lib/gitlab/ci/build/rules/rule_spec.rb b/spec/lib/gitlab/ci/build/rules/rule_spec.rb index 5694cd5d0a0..6f3c9278677 100644 --- a/spec/lib/gitlab/ci/build/rules/rule_spec.rb +++ b/spec/lib/gitlab/ci/build/rules/rule_spec.rb @@ -6,7 +6,7 @@ RSpec.describe Gitlab::Ci::Build::Rules::Rule do let(:seed) do double('build seed', to_resource: ci_build, - variables: ci_build.scoped_variables_hash + variables: ci_build.scoped_variables ) end diff --git a/spec/lib/gitlab/ci/build/rules_spec.rb b/spec/lib/gitlab/ci/build/rules_spec.rb index 0b50def05d4..1d5bdf30278 100644 --- a/spec/lib/gitlab/ci/build/rules_spec.rb +++ b/spec/lib/gitlab/ci/build/rules_spec.rb @@ -9,7 +9,7 @@ RSpec.describe Gitlab::Ci::Build::Rules do let(:seed) do double('build seed', to_resource: ci_build, - variables: ci_build.scoped_variables_hash + variables: ci_build.scoped_variables ) end diff --git a/spec/lib/gitlab/ci/charts_spec.rb b/spec/lib/gitlab/ci/charts_spec.rb index 46d7d4a58f0..3a82d058819 100644 --- a/spec/lib/gitlab/ci/charts_spec.rb +++ b/spec/lib/gitlab/ci/charts_spec.rb @@ -98,7 +98,12 @@ RSpec.describe Gitlab::Ci::Charts do subject { chart.total } before do - create(:ci_empty_pipeline, project: project, duration: 120) + # The created_at time used by the following execution + # can end up being after the creation of the 'today' time + # objects created above, and cause the queried counts to + # go to zero when the test executes close to midnight on the + # CI system, so we explicitly set it to a day earlier + create(:ci_empty_pipeline, project: project, duration: 120, created_at: today - 1.day) end it 'uses a utc time zone for range times' do diff --git a/spec/lib/gitlab/ci/config/entry/bridge_spec.rb b/spec/lib/gitlab/ci/config/entry/bridge_spec.rb index b3b7901074a..179578fe0a8 100644 --- a/spec/lib/gitlab/ci/config/entry/bridge_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/bridge_spec.rb @@ -244,6 +244,52 @@ RSpec.describe Gitlab::Ci::Config::Entry::Bridge do end end end + + context 'when bridge config contains parallel' do + let(:config) { { trigger: 'some/project', parallel: parallel_config } } + + context 'when parallel config is a number' do + let(:parallel_config) { 2 } + + describe '#valid?' do + it { is_expected.not_to be_valid } + end + + describe '#errors' do + it 'returns an error message' do + expect(subject.errors) + .to include(/cannot use "parallel: <number>"/) + end + end + end + + context 'when parallel config is a matrix' do + let(:parallel_config) do + { matrix: [{ PROVIDER: 'aws', STACK: %w[monitoring app1] }, + { PROVIDER: 'gcp', STACK: %w[data] }] } + end + + describe '#valid?' do + it { is_expected.to be_valid } + end + + describe '#value' do + it 'is returns a bridge job configuration' do + expect(subject.value).to eq( + name: :my_bridge, + trigger: { project: 'some/project' }, + ignore: false, + stage: 'test', + only: { refs: %w[branches tags] }, + parallel: { matrix: [{ 'PROVIDER' => ['aws'], 'STACK' => %w(monitoring app1) }, + { 'PROVIDER' => ['gcp'], 'STACK' => %w(data) }] }, + variables: {}, + scheduling_type: :stage + ) + end + end + end + end end describe '#manual_action?' do diff --git a/spec/lib/gitlab/ci/config/entry/cache_spec.rb b/spec/lib/gitlab/ci/config/entry/cache_spec.rb index 247f4b63910..064990667d5 100644 --- a/spec/lib/gitlab/ci/config/entry/cache_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/cache_spec.rb @@ -7,225 +7,285 @@ RSpec.describe Gitlab::Ci::Config::Entry::Cache do subject(:entry) { described_class.new(config) } - describe 'validations' do + context 'with multiple caches' do before do entry.compose! end - context 'when entry config value is correct' do - let(:policy) { nil } - let(:key) { 'some key' } - let(:when_config) { nil } - - let(:config) do - { - key: key, - untracked: true, - paths: ['some/path/'] - }.tap do |config| - config[:policy] = policy if policy - config[:when] = when_config if when_config + describe '#valid?' do + context 'when configuration is valid with a single cache' do + let(:config) { { key: 'key', paths: ["logs/"], untracked: true } } + + it 'is valid' do + expect(entry).to be_valid end end - describe '#value' do - shared_examples 'hash key value' do - it 'returns hash value' do - expect(entry.value).to eq(key: key, untracked: true, paths: ['some/path/'], policy: 'pull-push', when: 'on_success') - end + context 'when configuration is valid with multiple caches' do + let(:config) do + [ + { key: 'key', paths: ["logs/"], untracked: true }, + { key: 'key2', paths: ["logs/"], untracked: true }, + { key: 'key3', paths: ["logs/"], untracked: true } + ] end - it_behaves_like 'hash key value' + it 'is valid' do + expect(entry).to be_valid + end + end - context 'with files' do - let(:key) { { files: %w[a-file other-file] } } + context 'when configuration is not a Hash or Array' do + let(:config) { 'invalid' } - it_behaves_like 'hash key value' + it 'is invalid' do + expect(entry).not_to be_valid end + end - context 'with files and prefix' do - let(:key) { { files: %w[a-file other-file], prefix: 'prefix-value' } } + context 'when entry values contain more than four caches' do + let(:config) do + [ + { key: 'key', paths: ["logs/"], untracked: true }, + { key: 'key2', paths: ["logs/"], untracked: true }, + { key: 'key3', paths: ["logs/"], untracked: true }, + { key: 'key4', paths: ["logs/"], untracked: true }, + { key: 'key5', paths: ["logs/"], untracked: true } + ] + end - it_behaves_like 'hash key value' + it 'is invalid' do + expect(entry.errors).to eq(["caches config no more than 4 caches can be created"]) + expect(entry).not_to be_valid end + end + end + end + + context 'with a single cache' do + before do + stub_feature_flags(multiple_cache_per_job: false) + end + describe 'validations' do + before do + entry.compose! + end - context 'with prefix' do - let(:key) { { prefix: 'prefix-value' } } + context 'when entry config value is correct' do + let(:policy) { nil } + let(:key) { 'some key' } + let(:when_config) { nil } - it 'key is nil' do - expect(entry.value).to match(a_hash_including(key: nil)) + let(:config) do + { + key: key, + untracked: true, + paths: ['some/path/'] + }.tap do |config| + config[:policy] = policy if policy + config[:when] = when_config if when_config end end - context 'with `policy`' do - where(:policy, :result) do - 'pull-push' | 'pull-push' - 'push' | 'push' - 'pull' | 'pull' - 'unknown' | 'unknown' # invalid + describe '#value' do + shared_examples 'hash key value' do + it 'returns hash value' do + expect(entry.value).to eq(key: key, untracked: true, paths: ['some/path/'], policy: 'pull-push', when: 'on_success') + end end - with_them do - it { expect(entry.value).to include(policy: result) } + it_behaves_like 'hash key value' + + context 'with files' do + let(:key) { { files: %w[a-file other-file] } } + + it_behaves_like 'hash key value' end - end - context 'without `policy`' do - it 'assigns policy to default' do - expect(entry.value).to include(policy: 'pull-push') + context 'with files and prefix' do + let(:key) { { files: %w[a-file other-file], prefix: 'prefix-value' } } + + it_behaves_like 'hash key value' end - end - context 'with `when`' do - where(:when_config, :result) do - 'on_success' | 'on_success' - 'on_failure' | 'on_failure' - 'always' | 'always' - 'unknown' | 'unknown' # invalid + context 'with prefix' do + let(:key) { { prefix: 'prefix-value' } } + + it 'key is nil' do + expect(entry.value).to match(a_hash_including(key: nil)) + end end - with_them do - it { expect(entry.value).to include(when: result) } + context 'with `policy`' do + where(:policy, :result) do + 'pull-push' | 'pull-push' + 'push' | 'push' + 'pull' | 'pull' + 'unknown' | 'unknown' # invalid + end + + with_them do + it { expect(entry.value).to include(policy: result) } + end end - end - context 'without `when`' do - it 'assigns when to default' do - expect(entry.value).to include(when: 'on_success') + context 'without `policy`' do + it 'assigns policy to default' do + expect(entry.value).to include(policy: 'pull-push') + end end - end - end - describe '#valid?' do - it { is_expected.to be_valid } + context 'with `when`' do + where(:when_config, :result) do + 'on_success' | 'on_success' + 'on_failure' | 'on_failure' + 'always' | 'always' + 'unknown' | 'unknown' # invalid + end - context 'with files' do - let(:key) { { files: %w[a-file other-file] } } + with_them do + it { expect(entry.value).to include(when: result) } + end + end - it { is_expected.to be_valid } + context 'without `when`' do + it 'assigns when to default' do + expect(entry.value).to include(when: 'on_success') + end + end end - end - context 'with `policy`' do - where(:policy, :valid) do - 'pull-push' | true - 'push' | true - 'pull' | true - 'unknown' | false - end + describe '#valid?' do + it { is_expected.to be_valid } + + context 'with files' do + let(:key) { { files: %w[a-file other-file] } } - with_them do - it 'returns expected validity' do - expect(entry.valid?).to eq(valid) + it { is_expected.to be_valid } end end - end - context 'with `when`' do - where(:when_config, :valid) do - 'on_success' | true - 'on_failure' | true - 'always' | true - 'unknown' | false - end + context 'with `policy`' do + where(:policy, :valid) do + 'pull-push' | true + 'push' | true + 'pull' | true + 'unknown' | false + end - with_them do - it 'returns expected validity' do - expect(entry.valid?).to eq(valid) + with_them do + it 'returns expected validity' do + expect(entry.valid?).to eq(valid) + end end end - end - context 'with key missing' do - let(:config) do - { untracked: true, - paths: ['some/path/'] } + context 'with `when`' do + where(:when_config, :valid) do + 'on_success' | true + 'on_failure' | true + 'always' | true + 'unknown' | false + end + + with_them do + it 'returns expected validity' do + expect(entry.valid?).to eq(valid) + end + end end - describe '#value' do - it 'sets key with the default' do - expect(entry.value[:key]) - .to eq(Gitlab::Ci::Config::Entry::Key.default) + context 'with key missing' do + let(:config) do + { untracked: true, + paths: ['some/path/'] } + end + + describe '#value' do + it 'sets key with the default' do + expect(entry.value[:key]) + .to eq(Gitlab::Ci::Config::Entry::Key.default) + end end end end - end - context 'when entry value is not correct' do - describe '#errors' do - subject { entry.errors } + context 'when entry value is not correct' do + describe '#errors' do + subject { entry.errors } - context 'when is not a hash' do - let(:config) { 'ls' } + context 'when is not a hash' do + let(:config) { 'ls' } - it 'reports errors with config value' do - is_expected.to include 'cache config should be a hash' + it 'reports errors with config value' do + is_expected.to include 'cache config should be a hash' + end end - end - context 'when policy is unknown' do - let(:config) { { policy: 'unknown' } } + 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') + it 'reports error' do + is_expected.to include('cache policy should be pull-push, push, or pull') + end end - end - context 'when `when` is unknown' do - let(:config) { { when: 'unknown' } } + 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') + it 'reports error' do + is_expected.to include('cache when should be on_success, on_failure or always') + end end - end - context 'when descendants are invalid' do - context 'with invalid keys' do - let(:config) { { key: 1 } } + context 'when descendants are invalid' do + context 'with invalid keys' do + let(:config) { { key: 1 } } - it 'reports error with descendants' do - is_expected.to include 'key should be a hash, a string or a symbol' + it 'reports error with descendants' do + is_expected.to include 'key should be a hash, a string or a symbol' + end end - end - context 'with empty key' do - let(:config) { { key: {} } } + context 'with empty key' do + let(:config) { { key: {} } } - it 'reports error with descendants' do - is_expected.to include 'key config missing required keys: files' + it 'reports error with descendants' do + is_expected.to include 'key config missing required keys: files' + end end - end - context 'with invalid files' do - let(:config) { { key: { files: 'a-file' } } } + context 'with invalid files' do + let(:config) { { key: { files: 'a-file' } } } - it 'reports error with descendants' do - is_expected.to include 'key:files config should be an array of strings' + it 'reports error with descendants' do + is_expected.to include 'key:files config should be an array of strings' + end end - end - context 'with prefix without files' do - let(:config) { { key: { prefix: 'a-prefix' } } } + context 'with prefix without files' do + let(:config) { { key: { prefix: 'a-prefix' } } } - it 'reports error with descendants' do - is_expected.to include 'key config missing required keys: files' + it 'reports error with descendants' do + is_expected.to include 'key config missing required keys: files' + end end - end - context 'when there is an unknown key present' do - let(:config) { { key: { unknown: 'a-file' } } } + context 'when there is an unknown key present' do + let(:config) { { key: { unknown: 'a-file' } } } - it 'reports error with descendants' do - is_expected.to include 'key config contains unknown keys: unknown' + it 'reports error with descendants' do + is_expected.to include 'key config contains unknown keys: unknown' + end end end - end - context 'when there is an unknown key present' do - let(:config) { { invalid: true } } + context 'when there is an unknown key present' do + let(:config) { { invalid: true } } - it 'reports error with descendants' do - is_expected.to include 'cache config contains unknown keys: invalid' + it 'reports error with descendants' do + is_expected.to include 'cache config contains unknown keys: invalid' + end end end end diff --git a/spec/lib/gitlab/ci/config/entry/environment_spec.rb b/spec/lib/gitlab/ci/config/entry/environment_spec.rb index 0c18a7fb71e..dd8a79f0d84 100644 --- a/spec/lib/gitlab/ci/config/entry/environment_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/environment_spec.rb @@ -305,4 +305,37 @@ RSpec.describe Gitlab::Ci::Config::Entry::Environment do it { expect(entry).to be_valid } end end + + describe 'deployment_tier' do + let(:config) do + { name: 'customer-portal', deployment_tier: deployment_tier } + end + + context 'is a string' do + let(:deployment_tier) { 'production' } + + it { expect(entry).to be_valid } + end + + context 'is a hash' do + let(:deployment_tier) { Hash(tier: 'production') } + + it { expect(entry).not_to be_valid } + end + + context 'is nil' do + let(:deployment_tier) { nil } + + it { expect(entry).to be_valid } + end + + context 'is unknown value' do + let(:deployment_tier) { 'unknown' } + + it 'is invalid and adds an error' do + expect(entry).not_to be_valid + expect(entry.errors).to include("environment deployment tier must be one of #{::Environment.tiers.keys.join(', ')}") + end + 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 a3b5f32b9f9..a4167003987 100644 --- a/spec/lib/gitlab/ci/config/entry/job_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/job_spec.rb @@ -537,7 +537,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Job do it 'overrides default config' do expect(entry[:image].value).to eq(name: 'some_image') - expect(entry[:cache].value).to eq(key: 'test', policy: 'pull-push', when: 'on_success') + expect(entry[:cache].value).to eq([key: 'test', policy: 'pull-push', when: 'on_success']) end end @@ -552,7 +552,43 @@ RSpec.describe Gitlab::Ci::Config::Entry::Job do it 'uses config from default entry' do expect(entry[:image].value).to eq 'specified' - expect(entry[:cache].value).to eq(key: 'test', policy: 'pull-push', when: 'on_success') + expect(entry[:cache].value).to eq([key: 'test', policy: 'pull-push', when: 'on_success']) + end + end + + context 'with multiple_cache_per_job FF disabled' do + before do + stub_feature_flags(multiple_cache_per_job: false) + end + + context 'when job config overrides default config' do + before do + entry.compose!(deps) + end + + let(:config) do + { script: 'rspec', image: 'some_image', cache: { key: 'test' } } + end + + it 'overrides default config' do + expect(entry[:image].value).to eq(name: 'some_image') + expect(entry[:cache].value).to eq(key: 'test', policy: 'pull-push', when: 'on_success') + end + end + + context 'when job config does not override default config' do + before do + allow(default).to receive('[]').with(:image).and_return(specified) + + entry.compose!(deps) + end + + let(:config) { { script: 'ls', cache: { key: 'test' } } } + + it 'uses config from default entry' do + expect(entry[:image].value).to eq 'specified' + expect(entry[:cache].value).to eq(key: 'test', policy: 'pull-push', when: 'on_success') + end end end diff --git a/spec/lib/gitlab/ci/config/entry/need_spec.rb b/spec/lib/gitlab/ci/config/entry/need_spec.rb index 983e95fae42..a0a5dd52ad4 100644 --- a/spec/lib/gitlab/ci/config/entry/need_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/need_spec.rb @@ -23,7 +23,17 @@ RSpec.describe ::Gitlab::Ci::Config::Entry::Need do describe '#value' do it 'returns job needs configuration' do - expect(need.value).to eq(name: 'job_name', artifacts: true) + expect(need.value).to eq(name: 'job_name', artifacts: true, optional: false) + end + + context 'when the FF ci_needs_optional is disabled' do + before do + stub_feature_flags(ci_needs_optional: false) + end + + it 'returns job needs configuration without `optional`' do + expect(need.value).to eq(name: 'job_name', artifacts: true) + end end end @@ -58,7 +68,7 @@ RSpec.describe ::Gitlab::Ci::Config::Entry::Need do describe '#value' do it 'returns job needs configuration' do - expect(need.value).to eq(name: 'job_name', artifacts: true) + expect(need.value).to eq(name: 'job_name', artifacts: true, optional: false) end end @@ -74,7 +84,7 @@ RSpec.describe ::Gitlab::Ci::Config::Entry::Need do describe '#value' do it 'returns job needs configuration' do - expect(need.value).to eq(name: 'job_name', artifacts: false) + expect(need.value).to eq(name: 'job_name', artifacts: false, optional: false) end end @@ -90,7 +100,7 @@ RSpec.describe ::Gitlab::Ci::Config::Entry::Need do describe '#value' do it 'returns job needs configuration' do - expect(need.value).to eq(name: 'job_name', artifacts: true) + expect(need.value).to eq(name: 'job_name', artifacts: true, optional: false) end end @@ -106,11 +116,77 @@ RSpec.describe ::Gitlab::Ci::Config::Entry::Need do describe '#value' do it 'returns job needs configuration' do - expect(need.value).to eq(name: 'job_name', artifacts: true) + expect(need.value).to eq(name: 'job_name', artifacts: true, optional: false) + end + end + + it_behaves_like 'job type' + end + + context 'with job name and optional true' do + let(:config) { { job: 'job_name', optional: true } } + + it { is_expected.to be_valid } + + it_behaves_like 'job type' + + describe '#value' do + it 'returns job needs configuration' do + expect(need.value).to eq(name: 'job_name', artifacts: true, optional: true) + end + + context 'when the FF ci_needs_optional is disabled' do + before do + stub_feature_flags(ci_needs_optional: false) + end + + it 'returns job needs configuration without `optional`' do + expect(need.value).to eq(name: 'job_name', artifacts: true) + end end end + end + + context 'with job name and optional false' do + let(:config) { { job: 'job_name', optional: false } } + + it { is_expected.to be_valid } it_behaves_like 'job type' + + describe '#value' do + it 'returns job needs configuration' do + expect(need.value).to eq(name: 'job_name', artifacts: true, optional: false) + end + end + end + + context 'with job name and optional nil' do + let(:config) { { job: 'job_name', optional: nil } } + + it { is_expected.to be_valid } + + it_behaves_like 'job type' + + describe '#value' do + it 'returns job needs configuration' do + expect(need.value).to eq(name: 'job_name', artifacts: true, optional: false) + end + end + end + + context 'without optional key' do + let(:config) { { job: 'job_name' } } + + it { is_expected.to be_valid } + + it_behaves_like 'job type' + + describe '#value' do + it 'returns job needs configuration' do + expect(need.value).to eq(name: 'job_name', artifacts: true, optional: false) + end + end end context 'when job name is empty' do diff --git a/spec/lib/gitlab/ci/config/entry/needs_spec.rb b/spec/lib/gitlab/ci/config/entry/needs_spec.rb index f11f2a56f5f..489fbac68b2 100644 --- a/spec/lib/gitlab/ci/config/entry/needs_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/needs_spec.rb @@ -111,8 +111,8 @@ RSpec.describe ::Gitlab::Ci::Config::Entry::Needs do it 'returns key value' do expect(needs.value).to eq( job: [ - { name: 'first_job_name', artifacts: true }, - { name: 'second_job_name', artifacts: true } + { name: 'first_job_name', artifacts: true, optional: false }, + { name: 'second_job_name', artifacts: true, optional: false } ] ) end @@ -124,8 +124,8 @@ RSpec.describe ::Gitlab::Ci::Config::Entry::Needs do context 'with complex job entries composed' do let(:config) do [ - { job: 'first_job_name', artifacts: true }, - { job: 'second_job_name', artifacts: false } + { job: 'first_job_name', artifacts: true, optional: false }, + { job: 'second_job_name', artifacts: false, optional: false } ] end @@ -137,8 +137,8 @@ RSpec.describe ::Gitlab::Ci::Config::Entry::Needs do it 'returns key value' do expect(needs.value).to eq( job: [ - { name: 'first_job_name', artifacts: true }, - { name: 'second_job_name', artifacts: false } + { name: 'first_job_name', artifacts: true, optional: false }, + { name: 'second_job_name', artifacts: false, optional: false } ] ) end @@ -163,8 +163,8 @@ RSpec.describe ::Gitlab::Ci::Config::Entry::Needs do it 'returns key value' do expect(needs.value).to eq( job: [ - { name: 'first_job_name', artifacts: true }, - { name: 'second_job_name', artifacts: false } + { name: 'first_job_name', artifacts: true, optional: false }, + { name: 'second_job_name', artifacts: false, optional: false } ] ) end diff --git a/spec/lib/gitlab/ci/config/entry/product/parallel_spec.rb b/spec/lib/gitlab/ci/config/entry/product/parallel_spec.rb index bc09e20d748..937642f07e7 100644 --- a/spec/lib/gitlab/ci/config/entry/product/parallel_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/product/parallel_spec.rb @@ -4,21 +4,23 @@ require 'fast_spec_helper' require_dependency 'active_model' RSpec.describe ::Gitlab::Ci::Config::Entry::Product::Parallel do - subject(:parallel) { described_class.new(config) } + let(:metadata) { {} } - context 'with invalid config' do - shared_examples 'invalid config' do |error_message| - describe '#valid?' do - it { is_expected.not_to be_valid } - end + subject(:parallel) { described_class.new(config, **metadata) } - describe '#errors' do - it 'returns error about invalid type' do - expect(parallel.errors).to match(a_collection_including(error_message)) - end + shared_examples 'invalid config' do |error_message| + describe '#valid?' do + it { is_expected.not_to be_valid } + end + + describe '#errors' do + it 'returns error about invalid type' do + expect(parallel.errors).to match(a_collection_including(error_message)) end end + end + context 'with invalid config' do context 'when it is not a numeric value' do let(:config) { true } @@ -63,6 +65,12 @@ RSpec.describe ::Gitlab::Ci::Config::Entry::Product::Parallel do expect(parallel.value).to match(number: config) end end + + context 'when :numeric is not allowed' do + let(:metadata) { { allowed_strategies: [:matrix] } } + + it_behaves_like 'invalid config', /cannot use "parallel: <number>"/ + end end end @@ -89,6 +97,12 @@ RSpec.describe ::Gitlab::Ci::Config::Entry::Product::Parallel do ]) end end + + context 'when :matrix is not allowed' do + let(:metadata) { { allowed_strategies: [:numeric] } } + + it_behaves_like 'invalid config', /cannot use "parallel: matrix"/ + 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 54c7a5c3602..7b38c21788f 100644 --- a/spec/lib/gitlab/ci/config/entry/root_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/root_spec.rb @@ -126,49 +126,105 @@ RSpec.describe Gitlab::Ci::Config::Entry::Root do expect(root.jobs_value.keys).to eq([:rspec, :spinach, :release]) expect(root.jobs_value[:rspec]).to eq( { name: :rspec, - script: %w[rspec ls], - before_script: %w(ls pwd), - image: { name: 'ruby:2.7' }, - services: [{ name: 'postgres:9.1' }, { name: 'mysql:5.5' }], - stage: 'test', - cache: { key: 'k', untracked: true, paths: ['public/'], policy: 'pull-push', when: 'on_success' }, - variables: { 'VAR' => 'root', 'VAR2' => 'val 2' }, - ignore: false, - after_script: ['make clean'], - only: { refs: %w[branches tags] }, - scheduling_type: :stage } + script: %w[rspec ls], + before_script: %w(ls pwd), + image: { name: 'ruby:2.7' }, + services: [{ name: 'postgres:9.1' }, { name: 'mysql:5.5' }], + stage: 'test', + cache: [{ key: 'k', untracked: true, paths: ['public/'], policy: 'pull-push', when: 'on_success' }], + variables: { 'VAR' => 'root', 'VAR2' => 'val 2' }, + ignore: false, + after_script: ['make clean'], + only: { refs: %w[branches tags] }, + scheduling_type: :stage } ) expect(root.jobs_value[:spinach]).to eq( { name: :spinach, - before_script: [], - script: %w[spinach], - image: { name: 'ruby:2.7' }, - services: [{ name: 'postgres:9.1' }, { name: 'mysql:5.5' }], - stage: 'test', - cache: { key: 'k', untracked: true, paths: ['public/'], policy: 'pull-push', when: 'on_success' }, - variables: { 'VAR' => 'root', 'VAR2' => 'val 2' }, - ignore: false, - after_script: ['make clean'], - only: { refs: %w[branches tags] }, - scheduling_type: :stage } + before_script: [], + script: %w[spinach], + image: { name: 'ruby:2.7' }, + services: [{ name: 'postgres:9.1' }, { name: 'mysql:5.5' }], + stage: 'test', + cache: [{ key: 'k', untracked: true, paths: ['public/'], policy: 'pull-push', when: 'on_success' }], + variables: { 'VAR' => 'root', 'VAR2' => 'val 2' }, + ignore: false, + after_script: ['make clean'], + only: { refs: %w[branches tags] }, + scheduling_type: :stage } ) expect(root.jobs_value[:release]).to eq( { name: :release, - stage: 'release', - before_script: [], - script: ["make changelog | tee release_changelog.txt"], - release: { name: "Release $CI_TAG_NAME", tag_name: 'v0.06', description: "./release_changelog.txt" }, - image: { name: "ruby:2.7" }, - services: [{ name: "postgres:9.1" }, { name: "mysql:5.5" }], - cache: { key: "k", untracked: true, paths: ["public/"], policy: "pull-push", when: 'on_success' }, - only: { refs: %w(branches tags) }, - variables: { 'VAR' => 'job', 'VAR2' => 'val 2' }, - after_script: [], - ignore: false, - scheduling_type: :stage } + stage: 'release', + before_script: [], + script: ["make changelog | tee release_changelog.txt"], + release: { name: "Release $CI_TAG_NAME", tag_name: 'v0.06', description: "./release_changelog.txt" }, + image: { name: "ruby:2.7" }, + services: [{ name: "postgres:9.1" }, { name: "mysql:5.5" }], + cache: [{ key: "k", untracked: true, paths: ["public/"], policy: "pull-push", when: 'on_success' }], + only: { refs: %w(branches tags) }, + variables: { 'VAR' => 'job', 'VAR2' => 'val 2' }, + after_script: [], + ignore: false, + scheduling_type: :stage } ) end end + + context 'with multuple_cache_per_job FF disabled' do + before do + stub_feature_flags(multiple_cache_per_job: false) + root.compose! + end + + describe '#jobs_value' do + it 'returns jobs configuration' do + expect(root.jobs_value.keys).to eq([:rspec, :spinach, :release]) + expect(root.jobs_value[:rspec]).to eq( + { name: :rspec, + script: %w[rspec ls], + before_script: %w(ls pwd), + image: { name: 'ruby:2.7' }, + services: [{ name: 'postgres:9.1' }, { name: 'mysql:5.5' }], + stage: 'test', + cache: { key: 'k', untracked: true, paths: ['public/'], policy: 'pull-push', when: 'on_success' }, + variables: { 'VAR' => 'root', 'VAR2' => 'val 2' }, + ignore: false, + after_script: ['make clean'], + only: { refs: %w[branches tags] }, + scheduling_type: :stage } + ) + expect(root.jobs_value[:spinach]).to eq( + { name: :spinach, + before_script: [], + script: %w[spinach], + image: { name: 'ruby:2.7' }, + services: [{ name: 'postgres:9.1' }, { name: 'mysql:5.5' }], + stage: 'test', + cache: { key: 'k', untracked: true, paths: ['public/'], policy: 'pull-push', when: 'on_success' }, + variables: { 'VAR' => 'root', 'VAR2' => 'val 2' }, + ignore: false, + after_script: ['make clean'], + only: { refs: %w[branches tags] }, + scheduling_type: :stage } + ) + expect(root.jobs_value[:release]).to eq( + { name: :release, + stage: 'release', + before_script: [], + script: ["make changelog | tee release_changelog.txt"], + release: { name: "Release $CI_TAG_NAME", tag_name: 'v0.06', description: "./release_changelog.txt" }, + image: { name: "ruby:2.7" }, + services: [{ name: "postgres:9.1" }, { name: "mysql:5.5" }], + cache: { key: "k", untracked: true, paths: ["public/"], policy: "pull-push", when: 'on_success' }, + only: { refs: %w(branches tags) }, + variables: { 'VAR' => 'job', 'VAR2' => 'val 2' }, + after_script: [], + ignore: false, + scheduling_type: :stage } + ) + end + end + end end end @@ -187,6 +243,52 @@ RSpec.describe Gitlab::Ci::Config::Entry::Root do spinach: { before_script: [], variables: { VAR: 'job' }, script: 'spinach' } } end + context 'with multiple_cache_per_job FF disabled' do + context 'when composed' do + before do + stub_feature_flags(multiple_cache_per_job: false) + root.compose! + end + + describe '#errors' do + it 'has no errors' do + expect(root.errors).to be_empty + end + end + + describe '#jobs_value' do + it 'returns jobs configuration' do + expect(root.jobs_value).to eq( + rspec: { name: :rspec, + script: %w[rspec ls], + before_script: %w(ls pwd), + image: { name: 'ruby:2.7' }, + services: [{ name: 'postgres:9.1' }, { name: 'mysql:5.5' }], + stage: 'test', + cache: { key: 'k', untracked: true, paths: ['public/'], policy: 'pull-push', when: 'on_success' }, + variables: { 'VAR' => 'root' }, + ignore: false, + after_script: ['make clean'], + only: { refs: %w[branches tags] }, + scheduling_type: :stage }, + spinach: { name: :spinach, + before_script: [], + script: %w[spinach], + image: { name: 'ruby:2.7' }, + services: [{ name: 'postgres:9.1' }, { name: 'mysql:5.5' }], + stage: 'test', + cache: { key: 'k', untracked: true, paths: ['public/'], policy: 'pull-push', when: 'on_success' }, + variables: { 'VAR' => 'job' }, + ignore: false, + after_script: ['make clean'], + only: { refs: %w[branches tags] }, + scheduling_type: :stage } + ) + end + end + end + end + context 'when composed' do before do root.compose! @@ -202,29 +304,29 @@ RSpec.describe Gitlab::Ci::Config::Entry::Root do it 'returns jobs configuration' do expect(root.jobs_value).to eq( rspec: { name: :rspec, - script: %w[rspec ls], - before_script: %w(ls pwd), - image: { name: 'ruby:2.7' }, - services: [{ name: 'postgres:9.1' }, { name: 'mysql:5.5' }], - stage: 'test', - cache: { key: 'k', untracked: true, paths: ['public/'], policy: 'pull-push', when: 'on_success' }, - variables: { 'VAR' => 'root' }, - ignore: false, - after_script: ['make clean'], - only: { refs: %w[branches tags] }, - scheduling_type: :stage }, + script: %w[rspec ls], + before_script: %w(ls pwd), + image: { name: 'ruby:2.7' }, + services: [{ name: 'postgres:9.1' }, { name: 'mysql:5.5' }], + stage: 'test', + cache: [{ key: 'k', untracked: true, paths: ['public/'], policy: 'pull-push', when: 'on_success' }], + variables: { 'VAR' => 'root' }, + ignore: false, + after_script: ['make clean'], + only: { refs: %w[branches tags] }, + scheduling_type: :stage }, spinach: { name: :spinach, - before_script: [], - script: %w[spinach], - image: { name: 'ruby:2.7' }, - services: [{ name: 'postgres:9.1' }, { name: 'mysql:5.5' }], - stage: 'test', - cache: { key: 'k', untracked: true, paths: ['public/'], policy: 'pull-push', when: 'on_success' }, - variables: { 'VAR' => 'job' }, - ignore: false, - after_script: ['make clean'], - only: { refs: %w[branches tags] }, - scheduling_type: :stage } + before_script: [], + script: %w[spinach], + image: { name: 'ruby:2.7' }, + services: [{ name: 'postgres:9.1' }, { name: 'mysql:5.5' }], + stage: 'test', + cache: [{ key: 'k', untracked: true, paths: ['public/'], policy: 'pull-push', when: 'on_success' }], + variables: { 'VAR' => 'job' }, + ignore: false, + after_script: ['make clean'], + only: { refs: %w[branches tags] }, + scheduling_type: :stage } ) end end @@ -265,7 +367,20 @@ RSpec.describe Gitlab::Ci::Config::Entry::Root do describe '#cache_value' do it 'returns correct cache definition' do - expect(root.cache_value).to eq(key: 'a', policy: 'pull-push', when: 'on_success') + expect(root.cache_value).to eq([key: 'a', policy: 'pull-push', when: 'on_success']) + end + end + + context 'with multiple_cache_per_job FF disabled' do + before do + stub_feature_flags(multiple_cache_per_job: false) + root.compose! + end + + describe '#cache_value' do + it 'returns correct cache definition' do + expect(root.cache_value).to eq(key: 'a', policy: 'pull-push', when: 'on_success') + end end end end diff --git a/spec/lib/gitlab/ci/jwt_spec.rb b/spec/lib/gitlab/ci/jwt_spec.rb index 342ca6b8b75..480a4a05379 100644 --- a/spec/lib/gitlab/ci/jwt_spec.rb +++ b/spec/lib/gitlab/ci/jwt_spec.rb @@ -114,17 +114,6 @@ RSpec.describe Gitlab::Ci::Jwt do expect(payload[:environment]).to eq('production') expect(payload[:environment_protected]).to eq('false') end - - context ':ci_jwt_include_environment feature flag is disabled' do - before do - stub_feature_flags(ci_jwt_include_environment: false) - end - - it 'does not include environment attributes' do - expect(payload).not_to have_key(:environment) - expect(payload).not_to have_key(:environment_protected) - end - end end end diff --git a/spec/lib/gitlab/ci/pipeline/expression/statement_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/statement_spec.rb index cf3644c9ad5..ec7eebdc056 100644 --- a/spec/lib/gitlab/ci/pipeline/expression/statement_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/expression/statement_spec.rb @@ -3,17 +3,16 @@ require 'spec_helper' RSpec.describe Gitlab::Ci::Pipeline::Expression::Statement do - subject do - described_class.new(text, variables) + let(:variables) do + Gitlab::Ci::Variables::Collection.new + .append(key: 'PRESENT_VARIABLE', value: 'my variable') + .append(key: 'PATH_VARIABLE', value: 'a/path/variable/value') + .append(key: 'FULL_PATH_VARIABLE', value: '/a/full/path/variable/value') + .append(key: 'EMPTY_VARIABLE', value: '') end - let(:variables) do - { - 'PRESENT_VARIABLE' => 'my variable', - 'PATH_VARIABLE' => 'a/path/variable/value', - 'FULL_PATH_VARIABLE' => '/a/full/path/variable/value', - 'EMPTY_VARIABLE' => '' - } + subject do + described_class.new(text, variables) end describe '.new' do 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 570706bfaac..773cb61b946 100644 --- a/spec/lib/gitlab/ci/pipeline/seed/build/cache_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/seed/build/cache_spec.rb @@ -9,8 +9,255 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build::Cache do let(:processor) { described_class.new(pipeline, config) } - describe '#build_attributes' do - subject { processor.build_attributes } + context 'with multiple_cache_per_job ff disabled' do + before do + stub_feature_flags(multiple_cache_per_job: false) + end + + describe '#build_attributes' do + subject { processor.build_attributes } + + context 'with cache:key' do + let(:config) do + { + key: 'a-key', + paths: ['vendor/ruby'] + } + end + + it { is_expected.to include(options: { cache: config }) } + end + + context 'with cache:key as a symbol' do + let(:config) do + { + key: :a_key, + paths: ['vendor/ruby'] + } + end + + it { is_expected.to include(options: { cache: config.merge(key: "a_key") }) } + end + + context 'with cache:key:files' do + shared_examples 'default key' do + let(:config) do + { key: { files: files } } + end + + it 'uses default key' do + expected = { options: { cache: { key: 'default' } } } + + is_expected.to include(expected) + end + end + + shared_examples 'version and gemfile files' do + let(:config) do + { + key: { + files: files + }, + paths: ['vendor/ruby'] + } + end + + it 'builds a string key' do + expected = { + options: { + cache: { + key: '703ecc8fef1635427a1f86a8a1a308831c122392', + paths: ['vendor/ruby'] + } + } + } + + is_expected.to include(expected) + end + end + + context 'with existing files' do + let(:files) { ['VERSION', 'Gemfile.zip'] } + + it_behaves_like 'version and gemfile files' + end + + context 'with files starting with ./' do + let(:files) { ['Gemfile.zip', './VERSION'] } + + it_behaves_like 'version and gemfile files' + end + + context 'with files ending with /' do + let(:files) { ['Gemfile.zip/'] } + + it_behaves_like 'default key' + end + + context 'with new line in filenames' do + let(:files) { ["Gemfile.zip\nVERSION"] } + + it_behaves_like 'default key' + end + + context 'with missing files' do + let(:files) { ['project-gemfile.lock', ''] } + + it_behaves_like 'default key' + end + + context 'with directories' do + shared_examples 'foo/bar directory key' do + let(:config) do + { + key: { + files: files + } + } + end + + it 'builds a string key' do + expected = { + options: { + cache: { key: '74bf43fb1090f161bdd4e265802775dbda2f03d1' } + } + } + + is_expected.to include(expected) + end + end + + context 'with directory' do + let(:files) { ['foo/bar'] } + + it_behaves_like 'foo/bar directory key' + end + + context 'with directory ending in slash' do + let(:files) { ['foo/bar/'] } + + it_behaves_like 'foo/bar directory key' + end + + context 'with directories ending in slash star' do + let(:files) { ['foo/bar/*'] } + + it_behaves_like 'foo/bar directory key' + end + end + end + + context 'with cache:key:prefix' do + context 'without files' do + let(:config) do + { + key: { + prefix: 'a-prefix' + }, + paths: ['vendor/ruby'] + } + end + + it 'adds prefix to default key' do + expected = { + options: { + cache: { + key: 'a-prefix-default', + paths: ['vendor/ruby'] + } + } + } + + is_expected.to include(expected) + end + end + + context 'with existing files' do + let(:config) do + { + key: { + files: ['VERSION', 'Gemfile.zip'], + prefix: 'a-prefix' + }, + paths: ['vendor/ruby'] + } + end + + it 'adds prefix key' do + expected = { + options: { + cache: { + key: 'a-prefix-703ecc8fef1635427a1f86a8a1a308831c122392', + paths: ['vendor/ruby'] + } + } + } + + is_expected.to include(expected) + end + end + + context 'with missing files' do + let(:config) do + { + key: { + files: ['project-gemfile.lock', ''], + prefix: 'a-prefix' + }, + paths: ['vendor/ruby'] + } + end + + it 'adds prefix to default key' do + expected = { + options: { + cache: { + key: 'a-prefix-default', + paths: ['vendor/ruby'] + } + } + } + + is_expected.to include(expected) + end + end + end + + context 'with all cache option keys' do + let(:config) do + { + key: 'a-key', + paths: ['vendor/ruby'], + untracked: true, + policy: 'push', + when: 'on_success' + } + end + + it { is_expected.to include(options: { cache: config }) } + end + + context 'with unknown cache option keys' do + let(:config) do + { + key: 'a-key', + unknown_key: true + } + end + + it { expect { subject }.to raise_error(ArgumentError, /unknown_key/) } + end + + context 'with empty config' do + let(:config) { {} } + + it { is_expected.to include(options: {}) } + end + end + end + + describe '#attributes' do + subject { processor.attributes } context 'with cache:key' do let(:config) do @@ -20,7 +267,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build::Cache do } end - it { is_expected.to include(options: { cache: config }) } + it { is_expected.to include(config) } end context 'with cache:key as a symbol' do @@ -31,7 +278,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build::Cache do } end - it { is_expected.to include(options: { cache: config.merge(key: "a_key") }) } + it { is_expected.to include(config.merge(key: "a_key")) } end context 'with cache:key:files' do @@ -41,7 +288,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build::Cache do end it 'uses default key' do - expected = { options: { cache: { key: 'default' } } } + expected = { key: 'default' } is_expected.to include(expected) end @@ -59,13 +306,9 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build::Cache do it 'builds a string key' do expected = { - options: { - cache: { key: '703ecc8fef1635427a1f86a8a1a308831c122392', paths: ['vendor/ruby'] - } } - } is_expected.to include(expected) end @@ -112,11 +355,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build::Cache do end it 'builds a string key' do - expected = { - options: { - cache: { key: '74bf43fb1090f161bdd4e265802775dbda2f03d1' } - } - } + expected = { key: '74bf43fb1090f161bdd4e265802775dbda2f03d1' } is_expected.to include(expected) end @@ -155,13 +394,9 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build::Cache do it 'adds prefix to default key' do expected = { - options: { - cache: { key: 'a-prefix-default', paths: ['vendor/ruby'] } - } - } is_expected.to include(expected) end @@ -180,13 +415,9 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build::Cache do it 'adds prefix key' do expected = { - options: { - cache: { key: 'a-prefix-703ecc8fef1635427a1f86a8a1a308831c122392', paths: ['vendor/ruby'] } - } - } is_expected.to include(expected) end @@ -205,13 +436,9 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build::Cache do it 'adds prefix to default key' do expected = { - options: { - cache: { key: 'a-prefix-default', paths: ['vendor/ruby'] } - } - } is_expected.to include(expected) end @@ -229,7 +456,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build::Cache do } end - it { is_expected.to include(options: { cache: config }) } + it { is_expected.to include(config) } end context 'with unknown cache option keys' do @@ -242,11 +469,5 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build::Cache do it { expect { subject }.to raise_error(ArgumentError, /unknown_key/) } end - - context 'with empty config' do - let(:config) { {} } - - it { is_expected.to include(options: {}) } - 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 0efc7484699..7ec6949f852 100644 --- a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb @@ -85,99 +85,169 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do { key: 'VAR2', value: 'var 2', public: true }, { key: 'VAR3', value: 'var 3', public: true }]) end + end - context 'when FF ci_rules_variables is disabled' do - before do - stub_feature_flags(ci_rules_variables: false) - end + context 'with multiple_cache_per_job FF disabled' do + before do + stub_feature_flags(multiple_cache_per_job: false) + end - it do - is_expected.to include(yaml_variables: [{ key: 'VAR1', value: 'var 1', public: true }, - { key: 'VAR2', value: 'var 2', public: true }]) + context 'with cache:key' do + let(:attributes) do + { + name: 'rspec', + ref: 'master', + cache: { + key: 'a-value' + } + } end + + it { is_expected.to include(options: { cache: { key: 'a-value' } }) } end - end - context 'with cache:key' do - let(:attributes) do - { - name: 'rspec', - ref: 'master', - cache: { - key: 'a-value' + context 'with cache:key:files' do + let(:attributes) do + { + name: 'rspec', + ref: 'master', + cache: { + key: { + files: ['VERSION'] + } + } } - } - end + end - it { is_expected.to include(options: { cache: { key: 'a-value' } }) } - end + it 'includes cache options' do + cache_options = { + options: { + cache: { key: 'f155568ad0933d8358f66b846133614f76dd0ca4' } + } + } - context 'with cache:key:files' do - let(:attributes) do - { - name: 'rspec', - ref: 'master', - cache: { - key: { - files: ['VERSION'] + is_expected.to include(cache_options) + end + end + + context 'with cache:key:prefix' do + let(:attributes) do + { + name: 'rspec', + ref: 'master', + cache: { + key: { + prefix: 'something' + } } } - } + end + + it { is_expected.to include(options: { cache: { key: 'something-default' } }) } end - it 'includes cache options' do - cache_options = { - options: { + context 'with cache:key:files and prefix' do + let(:attributes) do + { + name: 'rspec', + ref: 'master', cache: { - key: 'f155568ad0933d8358f66b846133614f76dd0ca4' + key: { + files: ['VERSION'], + prefix: 'something' + } } } - } + end - is_expected.to include(cache_options) + it 'includes cache options' do + cache_options = { + options: { + cache: { key: 'something-f155568ad0933d8358f66b846133614f76dd0ca4' } + } + } + + is_expected.to include(cache_options) + end end end - context 'with cache:key:prefix' do + context 'with cache:key' do let(:attributes) do { name: 'rspec', ref: 'master', - cache: { - key: { - prefix: 'something' - } - } + cache: [{ + key: 'a-value' + }] } end - it { is_expected.to include(options: { cache: { key: 'something-default' } }) } - end + it { is_expected.to include(options: { cache: [a_hash_including(key: 'a-value')] }) } - context 'with cache:key:files and prefix' do - let(:attributes) do - { - name: 'rspec', - ref: 'master', - cache: { - key: { - files: ['VERSION'], - prefix: 'something' + context 'with cache:key:files' do + let(:attributes) do + { + name: 'rspec', + ref: 'master', + cache: [{ + key: { + files: ['VERSION'] + } + }] + } + end + + it 'includes cache options' do + cache_options = { + options: { + cache: [a_hash_including(key: 'f155568ad0933d8358f66b846133614f76dd0ca4')] } } - } + + is_expected.to include(cache_options) + end end - it 'includes cache options' do - cache_options = { - options: { - cache: { - key: 'something-f155568ad0933d8358f66b846133614f76dd0ca4' + context 'with cache:key:prefix' do + let(:attributes) do + { + name: 'rspec', + ref: 'master', + cache: [{ + key: { + prefix: 'something' + } + }] + } + end + + it { is_expected.to include(options: { cache: [a_hash_including( key: 'something-default' )] }) } + end + + context 'with cache:key:files and prefix' do + let(:attributes) do + { + name: 'rspec', + ref: 'master', + cache: [{ + key: { + files: ['VERSION'], + prefix: 'something' + } + }] + } + end + + it 'includes cache options' do + cache_options = { + options: { + cache: [a_hash_including(key: 'something-f155568ad0933d8358f66b846133614f76dd0ca4')] } } - } - is_expected.to include(cache_options) + is_expected.to include(cache_options) + end end end @@ -190,7 +260,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do } end - it { is_expected.to include(options: {}) } + it { is_expected.to include({}) } end context 'with allow_failure' do @@ -307,7 +377,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do it 'does not have environment' do expect(subject).not_to be_has_environment expect(subject.environment).to be_nil - expect(subject.metadata.expanded_environment_name).to be_nil + expect(subject.metadata).to be_nil expect(Environment.exists?(name: expected_environment_name)).to eq(false) end end @@ -979,6 +1049,25 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do expect(subject.errors).to contain_exactly( "'rspec' job needs 'build' job, but it was not added to the pipeline") end + + 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 + + context 'when the FF ci_needs_optional is disabled' do + before do + stub_feature_flags(ci_needs_optional: false) + end + + it "returns an error" do + expect(subject.errors).to contain_exactly( + "'rspec' job needs 'build' job, but it was not added to the pipeline") + end + end + end end context 'when build job is part of prior stages' do @@ -1036,4 +1125,75 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do 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) + 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 FF :variable_inside_variable is enabled' do + before do + stub_feature_flags(variable_inside_variable: [project]) + end + + it "does not have errors" do + expect(subject.errors).to be_empty + end + 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 + + context 'when FF :variable_inside_variable is disabled' do + before do + stub_feature_flags(variable_inside_variable: false) + end + + it "does not have errors" do + expect(subject.errors).to be_empty + end + end + + context 'when FF :variable_inside_variable is enabled' do + before do + stub_feature_flags(variable_inside_variable: [project]) + end + + it "returns an error" do + expect(subject.errors).to contain_exactly( + '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 end diff --git a/spec/lib/gitlab/ci/pipeline/seed/environment_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/environment_spec.rb index 664aaaedf7b..99196d393c6 100644 --- a/spec/lib/gitlab/ci/pipeline/seed/environment_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/seed/environment_spec.rb @@ -88,6 +88,55 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Environment do end end + context 'when job has deployment tier attribute' do + let(:attributes) do + { + environment: 'customer-portal', + options: { + environment: { + name: 'customer-portal', + deployment_tier: deployment_tier + } + } + } + end + + let(:deployment_tier) { 'production' } + + context 'when environment has not been created yet' do + it 'sets the specified deployment tier' do + is_expected.to be_production + end + + context 'when deployment tier is staging' do + let(:deployment_tier) { 'staging' } + + it 'sets the specified deployment tier' do + is_expected.to be_staging + end + end + + context 'when deployment tier is unknown' do + let(:deployment_tier) { 'unknown' } + + it 'raises an error' do + expect { subject }.to raise_error(ArgumentError, "'unknown' is not a valid tier") + end + end + end + + context 'when environment has already been created' do + before do + create(:environment, :staging, project: project, name: 'customer-portal') + end + + it 'does not overwrite the specified deployment tier' do + # This is to be updated when a deployment succeeded i.e. Deployments::UpdateEnvironmentService. + is_expected.to be_staging + end + end + end + context 'when job starts a review app' do let(:environment_name) { 'review/$CI_COMMIT_REF_NAME' } let(:expected_environment_name) { "review/#{job.ref}" } diff --git a/spec/lib/gitlab/ci/reports/codequality_reports_comparer_spec.rb b/spec/lib/gitlab/ci/reports/codequality_reports_comparer_spec.rb index 90188b56f5a..b322e55cb5a 100644 --- a/spec/lib/gitlab/ci/reports/codequality_reports_comparer_spec.rb +++ b/spec/lib/gitlab/ci/reports/codequality_reports_comparer_spec.rb @@ -27,6 +27,22 @@ RSpec.describe Gitlab::Ci::Reports::CodequalityReportsComparer do expect(report_status).to eq(described_class::STATUS_SUCCESS) end end + + context 'when head report does not exist' do + let(:head_report) { nil } + + it 'returns status not found' do + expect(report_status).to eq(described_class::STATUS_NOT_FOUND) + end + end + + context 'when base report does not exist' do + let(:base_report) { nil } + + it 'returns status success' do + expect(report_status).to eq(described_class::STATUS_NOT_FOUND) + end + end end describe '#errors_count' do @@ -93,6 +109,14 @@ RSpec.describe Gitlab::Ci::Reports::CodequalityReportsComparer do expect(resolved_count).to be_zero end end + + context 'when base report is nil' do + let(:base_report) { nil } + + it 'returns zero' do + expect(resolved_count).to be_zero + end + end end describe '#total_count' do @@ -140,6 +164,14 @@ RSpec.describe Gitlab::Ci::Reports::CodequalityReportsComparer do expect(total_count).to eq(2) end end + + context 'when base report is nil' do + let(:base_report) { nil } + + it 'returns zero' do + expect(total_count).to be_zero + end + end end describe '#existing_errors' do @@ -177,6 +209,14 @@ RSpec.describe Gitlab::Ci::Reports::CodequalityReportsComparer do expect(existing_errors).to be_empty end end + + context 'when base report is nil' do + let(:base_report) { nil } + + it 'returns an empty array' do + expect(existing_errors).to be_empty + end + end end describe '#new_errors' do @@ -213,6 +253,14 @@ RSpec.describe Gitlab::Ci::Reports::CodequalityReportsComparer do expect(new_errors).to eq([degradation_1]) end end + + context 'when base report is nil' do + let(:base_report) { nil } + + it 'returns an empty array' do + expect(new_errors).to be_empty + end + end end describe '#resolved_errors' do @@ -250,5 +298,13 @@ RSpec.describe Gitlab::Ci::Reports::CodequalityReportsComparer do expect(resolved_errors).to be_empty end end + + context 'when base report is nil' do + let(:base_report) { nil } + + it 'returns an empty array' do + expect(resolved_errors).to be_empty + end + end end end diff --git a/spec/lib/gitlab/ci/reports/reports_comparer_spec.rb b/spec/lib/gitlab/ci/reports/reports_comparer_spec.rb index 1e5e4766583..7ed9270e9a0 100644 --- a/spec/lib/gitlab/ci/reports/reports_comparer_spec.rb +++ b/spec/lib/gitlab/ci/reports/reports_comparer_spec.rb @@ -45,6 +45,22 @@ RSpec.describe Gitlab::Ci::Reports::ReportsComparer do expect(status).to eq('failed') end end + + context 'when base_report is nil' do + let(:base_report) { nil } + + it 'returns status not_found' do + expect(status).to eq('not_found') + end + end + + context 'when head_report is nil' do + let(:head_report) { nil } + + it 'returns status not_found' do + expect(status).to eq('not_found') + end + end end describe '#success?' do @@ -94,4 +110,22 @@ RSpec.describe Gitlab::Ci::Reports::ReportsComparer do expect { total_count }.to raise_error(NotImplementedError) end end + + describe '#not_found?' do + subject(:not_found) { comparer.not_found? } + + context 'when base report is nil' do + let(:base_report) { nil } + + it { is_expected.to be_truthy } + end + + context 'when base report exists' do + before do + allow(comparer).to receive(:success?).and_return(true) + end + + it { is_expected.to be_falsey } + end + end end diff --git a/spec/lib/gitlab/ci/reports/test_suite_summary_spec.rb b/spec/lib/gitlab/ci/reports/test_suite_summary_spec.rb index a98d3db4e82..9acea852832 100644 --- a/spec/lib/gitlab/ci/reports/test_suite_summary_spec.rb +++ b/spec/lib/gitlab/ci/reports/test_suite_summary_spec.rb @@ -87,12 +87,44 @@ RSpec.describe Gitlab::Ci::Reports::TestSuiteSummary do end end + describe '#suite_error' do + subject(:suite_error) { test_suite_summary.suite_error } + + context 'when there are no build report results with suite errors' do + it { is_expected.to be_nil } + end + + context 'when there are build report results with suite errors' do + let(:build_report_result_1) do + build( + :ci_build_report_result, + :with_junit_suite_error, + test_suite_name: 'karma', + test_suite_error: 'karma parsing error' + ) + end + + let(:build_report_result_2) do + build( + :ci_build_report_result, + :with_junit_suite_error, + test_suite_name: 'karma', + test_suite_error: 'another karma parsing error' + ) + end + + it 'includes the first suite error from the collection of build report results' do + expect(suite_error).to eq('karma parsing error') + end + end + end + describe '#to_h' do subject { test_suite_summary.to_h } context 'when test suite summary has several build report results' do it 'returns the total as a hash' do - expect(subject).to include(:time, :count, :success, :failed, :skipped, :error) + expect(subject).to include(:time, :count, :success, :failed, :skipped, :error, :suite_error) end end end diff --git a/spec/lib/gitlab/ci/status/composite_spec.rb b/spec/lib/gitlab/ci/status/composite_spec.rb index bcfb9f19792..543cfe874ca 100644 --- a/spec/lib/gitlab/ci/status/composite_spec.rb +++ b/spec/lib/gitlab/ci/status/composite_spec.rb @@ -69,6 +69,8 @@ RSpec.describe Gitlab::Ci::Status::Composite do %i(manual) | false | 'skipped' | false %i(skipped failed) | false | 'success' | true %i(skipped failed) | true | 'skipped' | true + %i(success manual) | true | 'skipped' | false + %i(success manual) | false | 'success' | false %i(created failed) | false | 'created' | true %i(preparing manual) | false | 'preparing' | false end @@ -80,6 +82,25 @@ RSpec.describe Gitlab::Ci::Status::Composite do it_behaves_like 'compares status and warnings' end + + context 'when FF ci_fix_pipeline_status_for_dag_needs_manual is disabled' do + before do + stub_feature_flags(ci_fix_pipeline_status_for_dag_needs_manual: false) + end + + where(:build_statuses, :dag, :result, :has_warnings) do + %i(success manual) | true | 'pending' | false + %i(success manual) | false | 'success' | false + end + + with_them do + let(:all_statuses) do + build_statuses.map { |status| @statuses_with_allow_failure[status] } + end + + it_behaves_like 'compares status and warnings' + end + end end end end diff --git a/spec/lib/gitlab/ci/status/factory_spec.rb b/spec/lib/gitlab/ci/status/factory_spec.rb index 641cb0183d3..94a6255f1e2 100644 --- a/spec/lib/gitlab/ci/status/factory_spec.rb +++ b/spec/lib/gitlab/ci/status/factory_spec.rb @@ -134,4 +134,14 @@ RSpec.describe Gitlab::Ci::Status::Factory do it_behaves_like 'compound decorator factory' end end + + context 'behaviour of FactoryBot traits that create associations' do + context 'creating a namespace with an associated aggregation_schedule record' do + it 'creates only one Namespace record and one Namespace::AggregationSchedule record' do + expect { create(:namespace, :with_aggregation_schedule) } + .to change { Namespace.count }.by(1) + .and change { Namespace::AggregationSchedule.count }.by(1) + end + end + end end diff --git a/spec/lib/gitlab/ci/templates/auto_devops_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/auto_devops_gitlab_ci_yaml_spec.rb index f9d6fe24e70..6dfcecb853a 100644 --- a/spec/lib/gitlab/ci/templates/auto_devops_gitlab_ci_yaml_spec.rb +++ b/spec/lib/gitlab/ci/templates/auto_devops_gitlab_ci_yaml_spec.rb @@ -3,252 +3,260 @@ require 'spec_helper' RSpec.describe 'Auto-DevOps.gitlab-ci.yml' do + using RSpec::Parameterized::TableSyntax + subject(:template) { Gitlab::Template::GitlabCiYmlTemplate.find('Auto-DevOps') } - describe 'the created pipeline' do - let(:default_branch) { 'master' } - let(:pipeline_branch) { default_branch } - let(:project) { create(:project, :auto_devops, :custom_repo, files: { 'README.md' => '' }) } - let(:user) { project.owner } - let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_branch ) } - let(:pipeline) { service.execute!(:push) } - let(:build_names) { pipeline.builds.pluck(:name) } - - before do - stub_ci_pipeline_yaml_file(template.content) - allow_any_instance_of(Ci::BuildScheduleWorker).to receive(:perform).and_return(true) - allow(project).to receive(:default_branch).and_return(default_branch) - end + where(:default_branch) do + %w[master main] + end - shared_examples 'no Kubernetes deployment job' do - it 'does not create any Kubernetes deployment-related builds' do - expect(build_names).not_to include('production') - expect(build_names).not_to include('production_manual') - expect(build_names).not_to include('staging') - expect(build_names).not_to include('canary') - expect(build_names).not_to include('review') - expect(build_names).not_to include(a_string_matching(/rollout \d+%/)) - end - end + with_them do + describe 'the created pipeline' do + let(:pipeline_branch) { default_branch } + let(:project) { create(:project, :auto_devops, :custom_repo, files: { 'README.md' => '' }) } + let(:user) { project.owner } + let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_branch ) } + let(:pipeline) { service.execute!(:push) } + let(:build_names) { pipeline.builds.pluck(:name) } - it 'creates a build and a test job' do - expect(build_names).to include('build', 'test') - end + before do + stub_application_setting(default_branch_name: default_branch) + stub_ci_pipeline_yaml_file(template.content) + allow_any_instance_of(Ci::BuildScheduleWorker).to receive(:perform).and_return(true) + end - context 'when the project is set for deployment to AWS' do - let(:platform_value) { 'ECS' } - let(:review_prod_build_names) { build_names.select {|n| n.include?('review') || n.include?('production')} } + shared_examples 'no Kubernetes deployment job' do + it 'does not create any Kubernetes deployment-related builds' do + expect(build_names).not_to include('production') + expect(build_names).not_to include('production_manual') + expect(build_names).not_to include('staging') + expect(build_names).not_to include('canary') + expect(build_names).not_to include('review') + expect(build_names).not_to include(a_string_matching(/rollout \d+%/)) + end + end - before do - create(:ci_variable, project: project, key: 'AUTO_DEVOPS_PLATFORM_TARGET', value: platform_value) + it 'creates a build and a test job' do + expect(build_names).to include('build', 'test') end - shared_examples 'no ECS job when AUTO_DEVOPS_PLATFORM_TARGET is not present' do |job_name| - context 'when AUTO_DEVOPS_PLATFORM_TARGET is nil' do - let(:platform_value) { nil } + context 'when the project is set for deployment to AWS' do + let(:platform_value) { 'ECS' } + let(:review_prod_build_names) { build_names.select {|n| n.include?('review') || n.include?('production')} } - it 'does not trigger the job' do - expect(build_names).not_to include(job_name) - end + before do + create(:ci_variable, project: project, key: 'AUTO_DEVOPS_PLATFORM_TARGET', value: platform_value) end - context 'when AUTO_DEVOPS_PLATFORM_TARGET is empty' do - let(:platform_value) { '' } + shared_examples 'no ECS job when AUTO_DEVOPS_PLATFORM_TARGET is not present' do |job_name| + context 'when AUTO_DEVOPS_PLATFORM_TARGET is nil' do + let(:platform_value) { nil } - it 'does not trigger the job' do - expect(build_names).not_to include(job_name) + it 'does not trigger the job' do + expect(build_names).not_to include(job_name) + end end - end - end - it_behaves_like 'no Kubernetes deployment job' + context 'when AUTO_DEVOPS_PLATFORM_TARGET is empty' do + let(:platform_value) { '' } - it_behaves_like 'no ECS job when AUTO_DEVOPS_PLATFORM_TARGET is not present' do - let(:job_name) { 'production_ecs' } - end + it 'does not trigger the job' do + expect(build_names).not_to include(job_name) + end + end + end - it 'creates an ECS deployment job for production only' do - expect(review_prod_build_names).to contain_exactly('production_ecs') - end + it_behaves_like 'no Kubernetes deployment job' - context 'with FARGATE as a launch type' do - let(:platform_value) { 'FARGATE' } + it_behaves_like 'no ECS job when AUTO_DEVOPS_PLATFORM_TARGET is not present' do + let(:job_name) { 'production_ecs' } + end - it 'creates a FARGATE deployment job for production only' do - expect(review_prod_build_names).to contain_exactly('production_fargate') + it 'creates an ECS deployment job for production only' do + expect(review_prod_build_names).to contain_exactly('production_ecs') end - end - context 'and we are not on the default branch' do - let(:platform_value) { 'ECS' } - let(:pipeline_branch) { 'patch-1' } + context 'with FARGATE as a launch type' do + let(:platform_value) { 'FARGATE' } - before do - project.repository.create_branch(pipeline_branch) + it 'creates a FARGATE deployment job for production only' do + expect(review_prod_build_names).to contain_exactly('production_fargate') + end end - %w(review_ecs review_fargate).each do |job| - it_behaves_like 'no ECS job when AUTO_DEVOPS_PLATFORM_TARGET is not present' do - let(:job_name) { job } + context 'and we are not on the default branch' do + let(:platform_value) { 'ECS' } + let(:pipeline_branch) { 'patch-1' } + + before do + project.repository.create_branch(pipeline_branch, default_branch) end - end - it 'creates an ECS deployment job for review only' do - expect(review_prod_build_names).to contain_exactly('review_ecs', 'stop_review_ecs') - end + %w(review_ecs review_fargate).each do |job| + it_behaves_like 'no ECS job when AUTO_DEVOPS_PLATFORM_TARGET is not present' do + let(:job_name) { job } + end + end - context 'with FARGATE as a launch type' do - let(:platform_value) { 'FARGATE' } + it 'creates an ECS deployment job for review only' do + expect(review_prod_build_names).to contain_exactly('review_ecs', 'stop_review_ecs') + end + + context 'with FARGATE as a launch type' do + let(:platform_value) { 'FARGATE' } - it 'creates an FARGATE deployment job for review only' do - expect(review_prod_build_names).to contain_exactly('review_fargate', 'stop_review_fargate') + it 'creates an FARGATE deployment job for review only' do + expect(review_prod_build_names).to contain_exactly('review_fargate', 'stop_review_fargate') + end end end - end - context 'and when the project has an active cluster' do - let(:cluster) { create(:cluster, :project, :provided_by_gcp, projects: [project]) } + context 'and when the project has an active cluster' do + let(:cluster) { create(:cluster, :project, :provided_by_gcp, projects: [project]) } - before do - allow(cluster).to receive(:active?).and_return(true) - end + before do + allow(cluster).to receive(:active?).and_return(true) + end - context 'on default branch' do - it 'triggers the deployment to Kubernetes, not to ECS' do - expect(build_names).not_to include('review') - expect(build_names).to include('production') - expect(build_names).not_to include('production_ecs') - expect(build_names).not_to include('review_ecs') + context 'on default branch' do + it 'triggers the deployment to Kubernetes, not to ECS' do + expect(build_names).not_to include('review') + expect(build_names).to include('production') + expect(build_names).not_to include('production_ecs') + expect(build_names).not_to include('review_ecs') + end end end - end - context 'when the platform target is EC2' do - let(:platform_value) { 'EC2' } + context 'when the platform target is EC2' do + let(:platform_value) { 'EC2' } - it 'contains the build_artifact job, not the build job' do - expect(build_names).to include('build_artifact') - expect(build_names).not_to include('build') + it 'contains the build_artifact job, not the build job' do + expect(build_names).to include('build_artifact') + expect(build_names).not_to include('build') + end end end - end - - context 'when the project has no active cluster' do - it 'only creates a build and a test stage' do - expect(pipeline.stages_names).to eq(%w(build test)) - end - it_behaves_like 'no Kubernetes deployment job' - end + context 'when the project has no active cluster' do + it 'only creates a build and a test stage' do + expect(pipeline.stages_names).to eq(%w(build test)) + end - context 'when the project has an active cluster' do - let!(:cluster) { create(:cluster, :project, :provided_by_gcp, projects: [project]) } - - describe 'deployment-related builds' do - context 'on default branch' do - it 'does not include rollout jobs besides production' do - expect(build_names).to include('production') - expect(build_names).not_to include('production_manual') - expect(build_names).not_to include('staging') - expect(build_names).not_to include('canary') - expect(build_names).not_to include('review') - expect(build_names).not_to include(a_string_matching(/rollout \d+%/)) - end + it_behaves_like 'no Kubernetes deployment job' + end - context 'when STAGING_ENABLED=1' do - before do - create(:ci_variable, project: project, key: 'STAGING_ENABLED', value: '1') - end + context 'when the project has an active cluster' do + let!(:cluster) { create(:cluster, :project, :provided_by_gcp, projects: [project]) } - it 'includes a staging job and a production_manual job' do - expect(build_names).not_to include('production') - expect(build_names).to include('production_manual') - expect(build_names).to include('staging') + describe 'deployment-related builds' do + context 'on default branch' do + it 'does not include rollout jobs besides production' do + expect(build_names).to include('production') + expect(build_names).not_to include('production_manual') + expect(build_names).not_to include('staging') expect(build_names).not_to include('canary') expect(build_names).not_to include('review') expect(build_names).not_to include(a_string_matching(/rollout \d+%/)) end + + context 'when STAGING_ENABLED=1' do + before do + create(:ci_variable, project: project, key: 'STAGING_ENABLED', value: '1') + end + + it 'includes a staging job and a production_manual job' do + expect(build_names).not_to include('production') + expect(build_names).to include('production_manual') + expect(build_names).to include('staging') + expect(build_names).not_to include('canary') + expect(build_names).not_to include('review') + expect(build_names).not_to include(a_string_matching(/rollout \d+%/)) + end + end + + context 'when CANARY_ENABLED=1' do + before do + create(:ci_variable, project: project, key: 'CANARY_ENABLED', value: '1') + end + + it 'includes a canary job and a production_manual job' do + expect(build_names).not_to include('production') + expect(build_names).to include('production_manual') + expect(build_names).not_to include('staging') + expect(build_names).to include('canary') + expect(build_names).not_to include('review') + expect(build_names).not_to include(a_string_matching(/rollout \d+%/)) + end + end end - context 'when CANARY_ENABLED=1' do + context 'outside of default branch' do + let(:pipeline_branch) { 'patch-1' } + before do - create(:ci_variable, project: project, key: 'CANARY_ENABLED', value: '1') + project.repository.create_branch(pipeline_branch, default_branch) end - it 'includes a canary job and a production_manual job' do + it 'does not include rollout jobs besides review' do expect(build_names).not_to include('production') - expect(build_names).to include('production_manual') + expect(build_names).not_to include('production_manual') expect(build_names).not_to include('staging') - expect(build_names).to include('canary') - expect(build_names).not_to include('review') + expect(build_names).not_to include('canary') + expect(build_names).to include('review') expect(build_names).not_to include(a_string_matching(/rollout \d+%/)) end end end - - context 'outside of default branch' do - let(:pipeline_branch) { 'patch-1' } - - before do - project.repository.create_branch(pipeline_branch) - end - - it 'does not include rollout jobs besides review' do - expect(build_names).not_to include('production') - expect(build_names).not_to include('production_manual') - expect(build_names).not_to include('staging') - expect(build_names).not_to include('canary') - expect(build_names).to include('review') - expect(build_names).not_to include(a_string_matching(/rollout \d+%/)) - end - end end end - end - describe 'build-pack detection' do - using RSpec::Parameterized::TableSyntax - - where(:case_name, :files, :variables, :include_build_names, :not_include_build_names) do - 'No match' | { 'README.md' => '' } | {} | %w() | %w(build test) - 'Buildpack' | { 'README.md' => '' } | { 'BUILDPACK_URL' => 'http://example.com' } | %w(build test) | %w() - 'Explicit set' | { 'README.md' => '' } | { 'AUTO_DEVOPS_EXPLICITLY_ENABLED' => '1' } | %w(build test) | %w() - 'Explicit unset' | { 'README.md' => '' } | { 'AUTO_DEVOPS_EXPLICITLY_ENABLED' => '0' } | %w() | %w(build test) - 'DOCKERFILE_PATH' | { 'README.md' => '' } | { 'DOCKERFILE_PATH' => 'Docker.file' } | %w(build test) | %w() - 'Dockerfile' | { 'Dockerfile' => '' } | {} | %w(build test) | %w() - 'Clojure' | { 'project.clj' => '' } | {} | %w(build test) | %w() - 'Go modules' | { 'go.mod' => '' } | {} | %w(build test) | %w() - 'Go gb' | { 'src/gitlab.com/gopackage.go' => '' } | {} | %w(build test) | %w() - 'Gradle' | { 'gradlew' => '' } | {} | %w(build test) | %w() - 'Java' | { 'pom.xml' => '' } | {} | %w(build test) | %w() - 'Multi-buildpack' | { '.buildpacks' => '' } | {} | %w(build test) | %w() - 'NodeJS' | { 'package.json' => '' } | {} | %w(build test) | %w() - 'PHP' | { 'composer.json' => '' } | {} | %w(build test) | %w() - 'Play' | { 'conf/application.conf' => '' } | {} | %w(build test) | %w() - 'Python' | { 'Pipfile' => '' } | {} | %w(build test) | %w() - 'Ruby' | { 'Gemfile' => '' } | {} | %w(build test) | %w() - 'Scala' | { 'build.sbt' => '' } | {} | %w(build test) | %w() - 'Static' | { '.static' => '' } | {} | %w(build test) | %w() - end + describe 'build-pack detection' do + using RSpec::Parameterized::TableSyntax + + where(:case_name, :files, :variables, :include_build_names, :not_include_build_names) do + 'No match' | { 'README.md' => '' } | {} | %w() | %w(build test) + 'Buildpack' | { 'README.md' => '' } | { 'BUILDPACK_URL' => 'http://example.com' } | %w(build test) | %w() + 'Explicit set' | { 'README.md' => '' } | { 'AUTO_DEVOPS_EXPLICITLY_ENABLED' => '1' } | %w(build test) | %w() + 'Explicit unset' | { 'README.md' => '' } | { 'AUTO_DEVOPS_EXPLICITLY_ENABLED' => '0' } | %w() | %w(build test) + 'DOCKERFILE_PATH' | { 'README.md' => '' } | { 'DOCKERFILE_PATH' => 'Docker.file' } | %w(build test) | %w() + 'Dockerfile' | { 'Dockerfile' => '' } | {} | %w(build test) | %w() + 'Clojure' | { 'project.clj' => '' } | {} | %w(build test) | %w() + 'Go modules' | { 'go.mod' => '' } | {} | %w(build test) | %w() + 'Go gb' | { 'src/gitlab.com/gopackage.go' => '' } | {} | %w(build test) | %w() + 'Gradle' | { 'gradlew' => '' } | {} | %w(build test) | %w() + 'Java' | { 'pom.xml' => '' } | {} | %w(build test) | %w() + 'Multi-buildpack' | { '.buildpacks' => '' } | {} | %w(build test) | %w() + 'NodeJS' | { 'package.json' => '' } | {} | %w(build test) | %w() + 'PHP' | { 'composer.json' => '' } | {} | %w(build test) | %w() + 'Play' | { 'conf/application.conf' => '' } | {} | %w(build test) | %w() + 'Python' | { 'Pipfile' => '' } | {} | %w(build test) | %w() + 'Ruby' | { 'Gemfile' => '' } | {} | %w(build test) | %w() + 'Scala' | { 'build.sbt' => '' } | {} | %w(build test) | %w() + 'Static' | { '.static' => '' } | {} | %w(build test) | %w() + end - with_them do - let(:project) { create(:project, :custom_repo, files: files) } - let(:user) { project.owner } - let(:service) { Ci::CreatePipelineService.new(project, user, ref: 'master' ) } - let(:pipeline) { service.execute(:push) } - let(:build_names) { pipeline.builds.pluck(:name) } + with_them do + let(:project) { create(:project, :custom_repo, files: files) } + let(:user) { project.owner } + let(:service) { Ci::CreatePipelineService.new(project, user, ref: default_branch ) } + let(:pipeline) { service.execute(:push) } + let(:build_names) { pipeline.builds.pluck(:name) } - before do - stub_ci_pipeline_yaml_file(template.content) - allow_any_instance_of(Ci::BuildScheduleWorker).to receive(:perform).and_return(true) - variables.each do |(key, value)| - create(:ci_variable, project: project, key: key, value: value) + before do + stub_application_setting(default_branch_name: default_branch) + stub_ci_pipeline_yaml_file(template.content) + allow_any_instance_of(Ci::BuildScheduleWorker).to receive(:perform).and_return(true) + variables.each do |(key, value)| + create(:ci_variable, project: project, key: key, value: value) + end end - end - it 'creates a pipeline with the expected jobs' do - expect(build_names).to include(*include_build_names) - expect(build_names).not_to include(*not_include_build_names) + it 'creates a pipeline with the expected jobs' do + expect(build_names).to include(*include_build_names) + expect(build_names).not_to include(*not_include_build_names) + end end end end diff --git a/spec/lib/gitlab/ci/trace_spec.rb b/spec/lib/gitlab/ci/trace_spec.rb index 92bf2519588..597e4ca9b03 100644 --- a/spec/lib/gitlab/ci/trace_spec.rb +++ b/spec/lib/gitlab/ci/trace_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' RSpec.describe Gitlab::Ci::Trace, :clean_gitlab_redis_shared_state, factory_default: :keep do - let_it_be(:project) { create_default(:project) } + let_it_be(:project) { create_default(:project).freeze } let_it_be_with_reload(:build) { create(:ci_build) } let(:trace) { described_class.new(build) } diff --git a/spec/lib/gitlab/ci/variables/collection/item_spec.rb b/spec/lib/gitlab/ci/variables/collection/item_spec.rb index 2e43f22830a..ca9dc95711d 100644 --- a/spec/lib/gitlab/ci/variables/collection/item_spec.rb +++ b/spec/lib/gitlab/ci/variables/collection/item_spec.rb @@ -32,6 +32,7 @@ RSpec.describe Gitlab::Ci::Variables::Collection::Item do it 'saves given value' do expect(subject[:key]).to eq variable_key expect(subject[:value]).to eq expected_value + expect(subject.value).to eq expected_value end end @@ -69,6 +70,47 @@ RSpec.describe Gitlab::Ci::Variables::Collection::Item do end end + describe '#depends_on' do + let(:item) { Gitlab::Ci::Variables::Collection::Item.new(**variable) } + + subject { item.depends_on } + + context 'table tests' do + using RSpec::Parameterized::TableSyntax + + where do + { + "no variable references": { + variable: { key: 'VAR', value: 'something' }, + expected_depends_on: nil + }, + "simple variable reference": { + variable: { key: 'VAR', value: 'something_$VAR2' }, + expected_depends_on: %w(VAR2) + }, + "complex expansion": { + variable: { key: 'VAR', value: 'something_${VAR2}_$VAR3' }, + expected_depends_on: %w(VAR2 VAR3) + }, + "complex expansion in raw variable": { + variable: { key: 'VAR', value: 'something_${VAR2}_$VAR3', raw: true }, + expected_depends_on: nil + }, + "complex expansions for Windows": { + variable: { key: 'variable3', value: 'key%variable%%variable2%' }, + expected_depends_on: %w(variable variable2) + } + } + end + + with_them do + it 'contains referenced variable names' do + is_expected.to eq(expected_depends_on) + end + end + end + end + describe '.fabricate' do it 'supports using a hash' do resource = described_class.fabricate(variable) @@ -118,6 +160,26 @@ RSpec.describe Gitlab::Ci::Variables::Collection::Item do end end + describe '#raw' do + it 'returns false when :raw is not specified' do + item = described_class.new(**variable) + + expect(item.raw).to eq false + end + + context 'when :raw is specified as true' do + let(:variable) do + { key: variable_key, value: variable_value, public: true, masked: false, raw: true } + end + + it 'returns true' do + item = described_class.new(**variable) + + expect(item.raw).to eq true + end + end + end + describe '#to_runner_variable' do context 'when variable is not a file-related' do it 'returns a runner-compatible hash representation' do @@ -139,5 +201,47 @@ RSpec.describe Gitlab::Ci::Variables::Collection::Item do .to eq(key: 'VAR', value: 'value', public: true, file: true, masked: false) end end + + context 'when variable is raw' do + it 'does not export raw value when it is false' do + runner_variable = described_class + .new(key: 'VAR', value: 'value', raw: false) + .to_runner_variable + + expect(runner_variable) + .to eq(key: 'VAR', value: 'value', public: true, masked: false) + end + + it 'exports raw value when it is true' do + runner_variable = described_class + .new(key: 'VAR', value: 'value', raw: true) + .to_runner_variable + + expect(runner_variable) + .to eq(key: 'VAR', value: 'value', public: true, raw: true, masked: false) + end + end + + context 'when referencing a variable' do + it '#depends_on contains names of dependencies' do + runner_variable = described_class.new(key: 'CI_VAR', value: '${CI_VAR_2}-123-$CI_VAR_3') + + expect(runner_variable.depends_on).to eq(%w(CI_VAR_2 CI_VAR_3)) + end + end + + context 'when assigned the raw attribute' do + it 'retains a true raw attribute' do + runner_variable = described_class.new(key: 'CI_VAR', value: '123', raw: true) + + expect(runner_variable).to eq(key: 'CI_VAR', value: '123', public: true, masked: false, raw: true) + end + + it 'does not retain a false raw attribute' do + runner_variable = described_class.new(key: 'CI_VAR', value: '123', raw: false) + + expect(runner_variable).to eq(key: 'CI_VAR', value: '123', public: true, masked: false) + end + end end end diff --git a/spec/lib/gitlab/ci/variables/collection/sort_spec.rb b/spec/lib/gitlab/ci/variables/collection/sort_spec.rb new file mode 100644 index 00000000000..73cf0e19d00 --- /dev/null +++ b/spec/lib/gitlab/ci/variables/collection/sort_spec.rb @@ -0,0 +1,185 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Variables::Collection::Sort do + describe '#initialize with non-Collection value' do + context 'when FF :variable_inside_variable is disabled' do + subject { Gitlab::Ci::Variables::Collection::Sort.new([]) } + + it 'raises ArgumentError' do + expect { subject }.to raise_error(ArgumentError, /Collection object was expected/) + end + end + + context 'when FF :variable_inside_variable is enabled' do + subject { Gitlab::Ci::Variables::Collection::Sort.new([]) } + + it 'raises ArgumentError' do + expect { subject }.to raise_error(ArgumentError, /Collection object was expected/) + end + end + end + + describe '#errors' do + context 'table tests' do + using RSpec::Parameterized::TableSyntax + + where do + { + "empty array": { + variables: [], + expected_errors: nil + }, + "simple expansions": { + variables: [ + { key: 'variable', value: 'value' }, + { key: 'variable2', value: 'result' }, + { key: 'variable3', value: 'key$variable$variable2' } + ], + expected_errors: nil + }, + "cyclic dependency": { + variables: [ + { key: 'variable', value: '$variable2' }, + { key: 'variable2', value: '$variable3' }, + { key: 'variable3', value: 'key$variable$variable2' } + ], + expected_errors: 'circular variable reference detected: ["variable", "variable2", "variable3"]' + }, + "array with raw variable": { + variables: [ + { key: 'variable', value: '$variable2' }, + { key: 'variable2', value: '$variable3' }, + { key: 'variable3', value: 'key$variable$variable2', raw: true } + ], + expected_errors: nil + }, + "variable containing escaped variable reference": { + variables: [ + { key: 'variable_a', value: 'value' }, + { key: 'variable_b', value: '$$variable_a' }, + { key: 'variable_c', value: '$variable_b' } + ], + expected_errors: nil + } + } + end + + with_them do + let(:collection) { Gitlab::Ci::Variables::Collection.new(variables) } + + subject { Gitlab::Ci::Variables::Collection::Sort.new(collection) } + + it 'errors matches expected errors' do + expect(subject.errors).to eq(expected_errors) + end + + it 'valid? matches expected errors' do + expect(subject.valid?).to eq(expected_errors.nil?) + end + + it 'does not raise' do + expect { subject }.not_to raise_error + end + end + end + end + + describe '#tsort' do + context 'table tests' do + using RSpec::Parameterized::TableSyntax + + where do + { + "empty array": { + variables: [], + result: [] + }, + "simple expansions, no reordering needed": { + variables: [ + { key: 'variable', value: 'value' }, + { key: 'variable2', value: 'result' }, + { key: 'variable3', value: 'key$variable$variable2' } + ], + result: %w[variable variable2 variable3] + }, + "complex expansion, reordering needed": { + variables: [ + { key: 'variable2', value: 'key${variable}' }, + { key: 'variable', value: 'value' } + ], + result: %w[variable variable2] + }, + "unused variables": { + variables: [ + { key: 'variable', value: 'value' }, + { key: 'variable4', value: 'key$variable$variable3' }, + { key: 'variable2', value: 'result2' }, + { key: 'variable3', value: 'result3' } + ], + result: %w[variable variable3 variable4 variable2] + }, + "missing variable": { + variables: [ + { key: 'variable2', value: 'key$variable' } + ], + result: %w[variable2] + }, + "complex expansions with missing variable": { + variables: [ + { key: 'variable4', value: 'key${variable}${variable2}${variable3}' }, + { key: 'variable', value: 'value' }, + { key: 'variable3', value: 'value3' } + ], + result: %w[variable variable3 variable4] + }, + "raw variable does not get resolved": { + variables: [ + { key: 'variable', value: '$variable2' }, + { key: 'variable2', value: '$variable3' }, + { key: 'variable3', value: 'key$variable$variable2', raw: true } + ], + result: %w[variable3 variable2 variable] + }, + "variable containing escaped variable reference": { + variables: [ + { key: 'variable_c', value: '$variable_b' }, + { key: 'variable_b', value: '$$variable_a' }, + { key: 'variable_a', value: 'value' } + ], + result: %w[variable_a variable_b variable_c] + } + } + end + + with_them do + let(:collection) { Gitlab::Ci::Variables::Collection.new(variables) } + + subject { Gitlab::Ci::Variables::Collection::Sort.new(collection).tsort } + + it 'returns correctly sorted variables' do + expect(subject.pluck(:key)).to eq(result) + end + end + end + + context 'cyclic dependency' do + let(:variables) do + [ + { key: 'variable2', value: '$variable3' }, + { key: 'variable3', value: 'key$variable$variable2' }, + { key: 'variable', value: '$variable2' } + ] + end + + let(:collection) { Gitlab::Ci::Variables::Collection.new(variables) } + + subject { Gitlab::Ci::Variables::Collection::Sort.new(collection).tsort } + + it 'raises TSort::Cyclic' do + expect { subject }.to raise_error(TSort::Cyclic) + end + end + end +end diff --git a/spec/lib/gitlab/ci/variables/collection/sorted_spec.rb b/spec/lib/gitlab/ci/variables/collection/sorted_spec.rb deleted file mode 100644 index 954273fd41e..00000000000 --- a/spec/lib/gitlab/ci/variables/collection/sorted_spec.rb +++ /dev/null @@ -1,259 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::Ci::Variables::Collection::Sorted do - describe '#errors' do - context 'when FF :variable_inside_variable is disabled' do - let_it_be(:project_with_flag_disabled) { create(:project) } - let_it_be(:project_with_flag_enabled) { create(:project) } - - before do - stub_feature_flags(variable_inside_variable: [project_with_flag_enabled]) - end - - context 'table tests' do - using RSpec::Parameterized::TableSyntax - - where do - { - "empty array": { - variables: [] - }, - "simple expansions": { - variables: [ - { key: 'variable', value: 'value' }, - { key: 'variable2', value: 'result' }, - { key: 'variable3', value: 'key$variable$variable2' } - ] - }, - "complex expansion": { - variables: [ - { key: 'variable', value: 'value' }, - { key: 'variable2', value: 'key${variable}' } - ] - }, - "complex expansions with missing variable for Windows": { - variables: [ - { key: 'variable', value: 'value' }, - { key: 'variable3', value: 'key%variable%%variable2%' } - ] - }, - "out-of-order variable reference": { - variables: [ - { key: 'variable2', value: 'key${variable}' }, - { key: 'variable', value: 'value' } - ] - }, - "array with cyclic dependency": { - variables: [ - { key: 'variable', value: '$variable2' }, - { key: 'variable2', value: '$variable3' }, - { key: 'variable3', value: 'key$variable$variable2' } - ] - } - } - end - - with_them do - subject { Gitlab::Ci::Variables::Collection::Sorted.new(variables, project_with_flag_disabled) } - - it 'does not report error' do - expect(subject.errors).to eq(nil) - end - - it 'valid? reports true' do - expect(subject.valid?).to eq(true) - end - end - end - end - - context 'when FF :variable_inside_variable is enabled' do - let_it_be(:project_with_flag_disabled) { create(:project) } - let_it_be(:project_with_flag_enabled) { create(:project) } - - before do - stub_feature_flags(variable_inside_variable: [project_with_flag_enabled]) - end - - context 'table tests' do - using RSpec::Parameterized::TableSyntax - - where do - { - "empty array": { - variables: [], - validation_result: nil - }, - "simple expansions": { - variables: [ - { key: 'variable', value: 'value' }, - { key: 'variable2', value: 'result' }, - { key: 'variable3', value: 'key$variable$variable2' } - ], - validation_result: nil - }, - "cyclic dependency": { - variables: [ - { key: 'variable', value: '$variable2' }, - { key: 'variable2', value: '$variable3' }, - { key: 'variable3', value: 'key$variable$variable2' } - ], - validation_result: 'circular variable reference detected: ["variable", "variable2", "variable3"]' - } - } - end - - with_them do - subject { Gitlab::Ci::Variables::Collection::Sorted.new(variables, project_with_flag_enabled) } - - it 'errors matches expected validation result' do - expect(subject.errors).to eq(validation_result) - end - - it 'valid? matches expected validation result' do - expect(subject.valid?).to eq(validation_result.nil?) - end - end - end - end - end - - describe '#sort' do - context 'when FF :variable_inside_variable is disabled' do - before do - stub_feature_flags(variable_inside_variable: false) - end - - context 'table tests' do - using RSpec::Parameterized::TableSyntax - - where do - { - "empty array": { - variables: [] - }, - "simple expansions": { - variables: [ - { key: 'variable', value: 'value' }, - { key: 'variable2', value: 'result' }, - { key: 'variable3', value: 'key$variable$variable2' } - ] - }, - "complex expansion": { - variables: [ - { key: 'variable', value: 'value' }, - { key: 'variable2', value: 'key${variable}' } - ] - }, - "complex expansions with missing variable for Windows": { - variables: [ - { key: 'variable', value: 'value' }, - { key: 'variable3', value: 'key%variable%%variable2%' } - ] - }, - "out-of-order variable reference": { - variables: [ - { key: 'variable2', value: 'key${variable}' }, - { key: 'variable', value: 'value' } - ] - }, - "array with cyclic dependency": { - variables: [ - { key: 'variable', value: '$variable2' }, - { key: 'variable2', value: '$variable3' }, - { key: 'variable3', value: 'key$variable$variable2' } - ] - } - } - end - - with_them do - let_it_be(:project) { create(:project) } - subject { Gitlab::Ci::Variables::Collection::Sorted.new(variables, project) } - - it 'does not expand variables' do - expect(subject.sort).to eq(variables) - end - end - end - end - - context 'when FF :variable_inside_variable is enabled' do - before do - stub_licensed_features(group_saml_group_sync: true) - stub_feature_flags(saml_group_links: true) - stub_feature_flags(variable_inside_variable: true) - end - - context 'table tests' do - using RSpec::Parameterized::TableSyntax - - where do - { - "empty array": { - variables: [], - result: [] - }, - "simple expansions, no reordering needed": { - variables: [ - { key: 'variable', value: 'value' }, - { key: 'variable2', value: 'result' }, - { key: 'variable3', value: 'key$variable$variable2' } - ], - result: %w[variable variable2 variable3] - }, - "complex expansion, reordering needed": { - variables: [ - { key: 'variable2', value: 'key${variable}' }, - { key: 'variable', value: 'value' } - ], - result: %w[variable variable2] - }, - "unused variables": { - variables: [ - { key: 'variable', value: 'value' }, - { key: 'variable4', value: 'key$variable$variable3' }, - { key: 'variable2', value: 'result2' }, - { key: 'variable3', value: 'result3' } - ], - result: %w[variable variable3 variable4 variable2] - }, - "missing variable": { - variables: [ - { key: 'variable2', value: 'key$variable' } - ], - result: %w[variable2] - }, - "complex expansions with missing variable": { - variables: [ - { key: 'variable4', value: 'key${variable}${variable2}${variable3}' }, - { key: 'variable', value: 'value' }, - { key: 'variable3', value: 'value3' } - ], - result: %w[variable variable3 variable4] - }, - "cyclic dependency causes original array to be returned": { - variables: [ - { key: 'variable2', value: '$variable3' }, - { key: 'variable3', value: 'key$variable$variable2' }, - { key: 'variable', value: '$variable2' } - ], - result: %w[variable2 variable3 variable] - } - } - end - - with_them do - let_it_be(:project) { create(:project) } - subject { Gitlab::Ci::Variables::Collection::Sorted.new(variables, project) } - - it 'sort returns correctly sorted variables' do - expect(subject.sort.map { |var| var[:key] }).to eq(result) - end - end - end - end - end -end diff --git a/spec/lib/gitlab/ci/variables/collection_spec.rb b/spec/lib/gitlab/ci/variables/collection_spec.rb index ac84313ad9f..7b77754190a 100644 --- a/spec/lib/gitlab/ci/variables/collection_spec.rb +++ b/spec/lib/gitlab/ci/variables/collection_spec.rb @@ -13,7 +13,7 @@ RSpec.describe Gitlab::Ci::Variables::Collection do end it 'can be initialized without an argument' do - expect(subject).to be_none + is_expected.to be_none end end @@ -21,13 +21,13 @@ RSpec.describe Gitlab::Ci::Variables::Collection do it 'appends a hash' do subject.append(key: 'VARIABLE', value: 'something') - expect(subject).to be_one + is_expected.to be_one end it 'appends a Ci::Variable' do subject.append(build(:ci_variable)) - expect(subject).to be_one + is_expected.to be_one end it 'appends an internal resource' do @@ -35,7 +35,7 @@ RSpec.describe Gitlab::Ci::Variables::Collection do subject.append(collection.first) - expect(subject).to be_one + is_expected.to be_one end it 'returns self' do @@ -98,6 +98,50 @@ RSpec.describe Gitlab::Ci::Variables::Collection do end end + describe '#[]' do + variable = { key: 'VAR', value: 'value', public: true, masked: false } + + collection = described_class.new([variable]) + + it 'returns nil for a non-existent variable name' do + expect(collection['UNKNOWN_VAR']).to be_nil + end + + it 'returns Item for an existent variable name' do + expect(collection['VAR']).to be_an_instance_of(Gitlab::Ci::Variables::Collection::Item) + expect(collection['VAR'].to_runner_variable).to eq(variable) + end + end + + describe '#size' do + it 'returns zero for empty collection' do + collection = described_class.new([]) + + expect(collection.size).to eq(0) + end + + it 'returns 2 for collection with 2 variables' do + collection = described_class.new( + [ + { key: 'VAR1', value: 'value', public: true, masked: false }, + { key: 'VAR2', value: 'value', public: true, masked: false } + ]) + + expect(collection.size).to eq(2) + end + + it 'returns 3 for collection with 2 duplicate variables' do + collection = described_class.new( + [ + { key: 'VAR1', value: 'value', public: true, masked: false }, + { key: 'VAR2', value: 'value', public: true, masked: false }, + { key: 'VAR1', value: 'value', public: true, masked: false } + ]) + + expect(collection.size).to eq(3) + end + end + describe '#to_runner_variables' do it 'creates an array of hashes in a runner-compatible format' do collection = described_class.new([{ key: 'TEST', value: '1' }]) @@ -121,4 +165,338 @@ RSpec.describe Gitlab::Ci::Variables::Collection do expect(collection.to_hash).not_to include(TEST1: 'test-1') end end + + describe '#reject' do + let(:collection) do + described_class.new + .append(key: 'CI_JOB_NAME', value: 'test-1') + .append(key: 'CI_BUILD_ID', value: '1') + .append(key: 'TEST1', value: 'test-3') + end + + subject { collection.reject { |var| var[:key] =~ /\ACI_(JOB|BUILD)/ } } + + it 'returns a Collection instance' do + is_expected.to be_an_instance_of(described_class) + end + + it 'returns correctly filtered Collection' do + comp = collection.to_runner_variables.reject { |var| var[:key] =~ /\ACI_(JOB|BUILD)/ } + expect(subject.to_runner_variables).to eq(comp) + end + end + + describe '#expand_value' do + let(:collection) do + Gitlab::Ci::Variables::Collection.new + .append(key: 'CI_JOB_NAME', value: 'test-1') + .append(key: 'CI_BUILD_ID', value: '1') + .append(key: 'RAW_VAR', value: '$TEST1', raw: true) + .append(key: 'TEST1', value: 'test-3') + end + + context 'table tests' do + using RSpec::Parameterized::TableSyntax + + where do + { + "empty value": { + value: '', + result: '', + keep_undefined: false + }, + "simple expansions": { + value: 'key$TEST1-$CI_BUILD_ID', + result: 'keytest-3-1', + keep_undefined: false + }, + "complex expansion": { + value: 'key${TEST1}-${CI_JOB_NAME}', + result: 'keytest-3-test-1', + keep_undefined: false + }, + "complex expansions with raw variable": { + value: 'key${RAW_VAR}-${CI_JOB_NAME}', + result: 'key$TEST1-test-1', + keep_undefined: false + }, + "missing variable not keeping original": { + value: 'key${MISSING_VAR}-${CI_JOB_NAME}', + result: 'key-test-1', + keep_undefined: false + }, + "missing variable keeping original": { + value: 'key${MISSING_VAR}-${CI_JOB_NAME}', + result: 'key${MISSING_VAR}-test-1', + keep_undefined: true + } + } + end + + with_them do + subject { collection.expand_value(value, keep_undefined: keep_undefined) } + + it 'matches expected expansion' do + is_expected.to eq(result) + end + end + end + end + + describe '#sort_and_expand_all' do + context 'when FF :variable_inside_variable is disabled' do + let_it_be(:project_with_flag_disabled) { create(:project) } + let_it_be(:project_with_flag_enabled) { create(:project) } + + before do + stub_feature_flags(variable_inside_variable: [project_with_flag_enabled]) + end + + context 'table tests' do + using RSpec::Parameterized::TableSyntax + + where do + { + "empty array": { + variables: [], + keep_undefined: false + }, + "simple expansions": { + variables: [ + { key: 'variable', value: 'value' }, + { key: 'variable2', value: 'result' }, + { key: 'variable3', value: 'key$variable$variable2' } + ], + keep_undefined: false + }, + "complex expansion": { + variables: [ + { key: 'variable', value: 'value' }, + { key: 'variable2', value: 'key${variable}' } + ], + keep_undefined: false + }, + "out-of-order variable reference": { + variables: [ + { key: 'variable2', value: 'key${variable}' }, + { key: 'variable', value: 'value' } + ], + keep_undefined: false + }, + "complex expansions with raw variable": { + variables: [ + { key: 'variable3', value: 'key_${variable}_${variable2}' }, + { key: 'variable', value: '$variable2', raw: true }, + { key: 'variable2', value: 'value2' } + ], + keep_undefined: false + }, + "array with cyclic dependency": { + variables: [ + { key: 'variable', value: '$variable2' }, + { key: 'variable2', value: '$variable3' }, + { key: 'variable3', value: 'key$variable$variable2' } + ], + keep_undefined: true + } + } + end + + with_them do + let(:collection) { Gitlab::Ci::Variables::Collection.new(variables, keep_undefined: keep_undefined) } + + subject { collection.sort_and_expand_all(project_with_flag_disabled) } + + it 'returns Collection' do + is_expected.to be_an_instance_of(Gitlab::Ci::Variables::Collection) + end + + it 'does not expand variables' do + var_hash = variables.pluck(:key, :value).to_h + expect(subject.to_hash).to eq(var_hash) + end + end + end + end + + context 'when FF :variable_inside_variable is enabled' do + let_it_be(:project_with_flag_disabled) { create(:project) } + let_it_be(:project_with_flag_enabled) { create(:project) } + + before do + stub_feature_flags(variable_inside_variable: [project_with_flag_enabled]) + end + + context 'table tests' do + using RSpec::Parameterized::TableSyntax + + where do + { + "empty array": { + variables: [], + keep_undefined: false, + result: [] + }, + "simple expansions": { + variables: [ + { key: 'variable', value: 'value' }, + { key: 'variable2', value: 'result' }, + { key: 'variable3', value: 'key$variable$variable2' }, + { key: 'variable4', value: 'key$variable$variable3' } + ], + keep_undefined: false, + result: [ + { key: 'variable', value: 'value' }, + { key: 'variable2', value: 'result' }, + { key: 'variable3', value: 'keyvalueresult' }, + { key: 'variable4', value: 'keyvaluekeyvalueresult' } + ] + }, + "complex expansion": { + variables: [ + { key: 'variable', value: 'value' }, + { key: 'variable2', value: 'key${variable}' } + ], + keep_undefined: false, + result: [ + { key: 'variable', value: 'value' }, + { key: 'variable2', value: 'keyvalue' } + ] + }, + "unused variables": { + variables: [ + { key: 'variable', value: 'value' }, + { key: 'variable2', value: 'result2' }, + { key: 'variable3', value: 'result3' }, + { key: 'variable4', value: 'key$variable$variable3' } + ], + keep_undefined: false, + result: [ + { key: 'variable', value: 'value' }, + { key: 'variable2', value: 'result2' }, + { key: 'variable3', value: 'result3' }, + { key: 'variable4', value: 'keyvalueresult3' } + ] + }, + "complex expansions": { + variables: [ + { key: 'variable', value: 'value' }, + { key: 'variable2', value: 'result' }, + { key: 'variable3', value: 'key${variable}${variable2}' } + ], + keep_undefined: false, + result: [ + { key: 'variable', value: 'value' }, + { key: 'variable2', value: 'result' }, + { key: 'variable3', value: 'keyvalueresult' } + ] + }, + "out-of-order expansion": { + variables: [ + { key: 'variable3', value: 'key$variable2$variable' }, + { key: 'variable', value: 'value' }, + { key: 'variable2', value: 'result' } + ], + keep_undefined: false, + result: [ + { key: 'variable2', value: 'result' }, + { key: 'variable', value: 'value' }, + { key: 'variable3', value: 'keyresultvalue' } + ] + }, + "out-of-order complex expansion": { + variables: [ + { key: 'variable', value: 'value' }, + { key: 'variable2', value: 'result' }, + { key: 'variable3', value: 'key${variable2}${variable}' } + ], + keep_undefined: false, + result: [ + { key: 'variable', value: 'value' }, + { key: 'variable2', value: 'result' }, + { key: 'variable3', value: 'keyresultvalue' } + ] + }, + "missing variable": { + variables: [ + { key: 'variable2', value: 'key$variable' } + ], + keep_undefined: false, + result: [ + { key: 'variable2', value: 'key' } + ] + }, + "missing variable keeping original": { + variables: [ + { key: 'variable2', value: 'key$variable' } + ], + keep_undefined: true, + result: [ + { key: 'variable2', value: 'key$variable' } + ] + }, + "complex expansions with missing variable keeping original": { + variables: [ + { key: 'variable4', value: 'key${variable}${variable2}${variable3}' }, + { key: 'variable', value: 'value' }, + { key: 'variable3', value: 'value3' } + ], + keep_undefined: true, + result: [ + { key: 'variable', value: 'value' }, + { key: 'variable3', value: 'value3' }, + { key: 'variable4', value: 'keyvalue${variable2}value3' } + ] + }, + "complex expansions with raw variable": { + variables: [ + { key: 'variable3', value: 'key_${variable}_${variable2}' }, + { key: 'variable', value: '$variable2', raw: true }, + { key: 'variable2', value: 'value2' } + ], + keep_undefined: false, + result: [ + { key: 'variable', value: '$variable2', raw: true }, + { key: 'variable2', value: 'value2' }, + { key: 'variable3', value: 'key_$variable2_value2' } + ] + }, + "cyclic dependency causes original array to be returned": { + variables: [ + { key: 'variable', value: '$variable2' }, + { key: 'variable2', value: '$variable3' }, + { key: 'variable3', value: 'key$variable$variable2' } + ], + keep_undefined: false, + result: [ + { key: 'variable', value: '$variable2' }, + { key: 'variable2', value: '$variable3' }, + { key: 'variable3', value: 'key$variable$variable2' } + ] + } + } + end + + with_them do + let(:collection) { Gitlab::Ci::Variables::Collection.new(variables) } + + subject { collection.sort_and_expand_all(project_with_flag_enabled, keep_undefined: keep_undefined) } + + it 'returns Collection' do + is_expected.to be_an_instance_of(Gitlab::Ci::Variables::Collection) + end + + it 'expands variables' do + var_hash = result.to_h { |env| [env.fetch(:key), env.fetch(:value)] } + .with_indifferent_access + expect(subject.to_hash).to eq(var_hash) + end + + it 'preserves raw attribute' do + expect(subject.pluck(:key, :raw).to_h).to eq(collection.pluck(:key, :raw).to_h) + end + end + end + end + end end diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb index 9498453852a..5462a587d16 100644 --- a/spec/lib/gitlab/ci/yaml_processor_spec.rb +++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb @@ -1368,6 +1368,155 @@ module Gitlab end end + context 'with multiple_cache_per_job FF disabled' do + before do + stub_feature_flags(multiple_cache_per_job: false) + end + describe 'cache' do + context 'when cache definition has unknown keys' do + let(:config) do + YAML.dump( + { cache: { untracked: true, invalid: 'key' }, + rspec: { script: 'rspec' } }) + end + + it_behaves_like 'returns errors', 'cache config contains unknown keys: invalid' + end + + it "returns cache when defined globally" do + config = YAML.dump({ + cache: { paths: ["logs/", "binaries/"], untracked: true, key: 'key' }, + rspec: { + script: "rspec" + } + }) + + config_processor = Gitlab::Ci::YamlProcessor.new(config).execute + + expect(config_processor.stage_builds_attributes("test").size).to eq(1) + expect(config_processor.stage_builds_attributes("test").first[:cache]).to eq( + paths: ["logs/", "binaries/"], + untracked: true, + key: 'key', + policy: 'pull-push', + when: 'on_success' + ) + end + + it "returns cache when defined in default context" do + config = YAML.dump( + { + default: { + cache: { paths: ["logs/", "binaries/"], untracked: true, key: { files: ['file'] } } + }, + rspec: { + script: "rspec" + } + }) + + config_processor = Gitlab::Ci::YamlProcessor.new(config).execute + + expect(config_processor.stage_builds_attributes("test").size).to eq(1) + expect(config_processor.stage_builds_attributes("test").first[:cache]).to eq( + paths: ["logs/", "binaries/"], + untracked: true, + key: { files: ['file'] }, + policy: 'pull-push', + when: 'on_success' + ) + end + + it 'returns cache key when defined in a job' do + config = YAML.dump({ + rspec: { + cache: { paths: ['logs/', 'binaries/'], untracked: true, key: 'key' }, + script: 'rspec' + } + }) + + config_processor = Gitlab::Ci::YamlProcessor.new(config).execute + + expect(config_processor.stage_builds_attributes('test').size).to eq(1) + expect(config_processor.stage_builds_attributes('test').first[:cache]).to eq( + paths: ['logs/', 'binaries/'], + untracked: true, + key: 'key', + policy: 'pull-push', + when: 'on_success' + ) + end + + it 'returns cache files' do + config = YAML.dump( + rspec: { + cache: { + paths: ['logs/', 'binaries/'], + untracked: true, + key: { files: ['file'] } + }, + script: 'rspec' + } + ) + + config_processor = Gitlab::Ci::YamlProcessor.new(config).execute + + expect(config_processor.stage_builds_attributes('test').size).to eq(1) + expect(config_processor.stage_builds_attributes('test').first[:cache]).to eq( + paths: ['logs/', 'binaries/'], + untracked: true, + key: { files: ['file'] }, + policy: 'pull-push', + when: 'on_success' + ) + end + + it 'returns cache files with prefix' do + config = YAML.dump( + rspec: { + cache: { + paths: ['logs/', 'binaries/'], + untracked: true, + key: { files: ['file'], prefix: 'prefix' } + }, + script: 'rspec' + } + ) + + config_processor = Gitlab::Ci::YamlProcessor.new(config).execute + + expect(config_processor.stage_builds_attributes('test').size).to eq(1) + expect(config_processor.stage_builds_attributes('test').first[:cache]).to eq( + paths: ['logs/', 'binaries/'], + untracked: true, + key: { files: ['file'], prefix: 'prefix' }, + policy: 'pull-push', + when: 'on_success' + ) + end + + it "overwrite cache when defined for a job and globally" do + config = YAML.dump({ + cache: { paths: ["logs/", "binaries/"], untracked: true, key: 'global' }, + rspec: { + script: "rspec", + cache: { paths: ["test/"], untracked: false, key: 'local' } + } + }) + + config_processor = Gitlab::Ci::YamlProcessor.new(config).execute + + expect(config_processor.stage_builds_attributes("test").size).to eq(1) + expect(config_processor.stage_builds_attributes("test").first[:cache]).to eq( + paths: ["test/"], + untracked: false, + key: 'local', + policy: 'pull-push', + when: 'on_success' + ) + end + end + end + describe 'cache' do context 'when cache definition has unknown keys' do let(:config) do @@ -1381,22 +1530,22 @@ module Gitlab it "returns cache when defined globally" do config = YAML.dump({ - cache: { paths: ["logs/", "binaries/"], untracked: true, key: 'key' }, - rspec: { - script: "rspec" - } - }) + cache: { paths: ["logs/", "binaries/"], untracked: true, key: 'key' }, + rspec: { + script: "rspec" + } + }) config_processor = Gitlab::Ci::YamlProcessor.new(config).execute expect(config_processor.stage_builds_attributes("test").size).to eq(1) - expect(config_processor.stage_builds_attributes("test").first[:cache]).to eq( + expect(config_processor.stage_builds_attributes("test").first[:cache]).to eq([ paths: ["logs/", "binaries/"], untracked: true, key: 'key', policy: 'pull-push', when: 'on_success' - ) + ]) end it "returns cache when defined in default context" do @@ -1413,32 +1562,46 @@ module Gitlab config_processor = Gitlab::Ci::YamlProcessor.new(config).execute expect(config_processor.stage_builds_attributes("test").size).to eq(1) - expect(config_processor.stage_builds_attributes("test").first[:cache]).to eq( + expect(config_processor.stage_builds_attributes("test").first[:cache]).to eq([ paths: ["logs/", "binaries/"], untracked: true, key: { files: ['file'] }, policy: 'pull-push', when: 'on_success' - ) + ]) end - it 'returns cache key when defined in a job' do + it 'returns cache key/s when defined in a job' do config = YAML.dump({ - rspec: { - cache: { paths: ['logs/', 'binaries/'], untracked: true, key: 'key' }, - script: 'rspec' - } - }) + rspec: { + cache: [ + { paths: ['binaries/'], untracked: true, key: 'keya' }, + { paths: ['logs/', 'binaries/'], untracked: true, key: 'key' } + ], + script: 'rspec' + } + }) config_processor = Gitlab::Ci::YamlProcessor.new(config).execute expect(config_processor.stage_builds_attributes('test').size).to eq(1) expect(config_processor.stage_builds_attributes('test').first[:cache]).to eq( - paths: ['logs/', 'binaries/'], - untracked: true, - key: 'key', - policy: 'pull-push', - when: 'on_success' + [ + { + paths: ['binaries/'], + untracked: true, + key: 'keya', + policy: 'pull-push', + when: 'on_success' + }, + { + paths: ['logs/', 'binaries/'], + untracked: true, + key: 'key', + policy: 'pull-push', + when: 'on_success' + } + ] ) end @@ -1446,10 +1609,10 @@ module Gitlab config = YAML.dump( rspec: { cache: { - paths: ['logs/', 'binaries/'], - untracked: true, - key: { files: ['file'] } - }, + paths: ['binaries/'], + untracked: true, + key: { files: ['file'] } + }, script: 'rspec' } ) @@ -1457,13 +1620,13 @@ module Gitlab config_processor = Gitlab::Ci::YamlProcessor.new(config).execute expect(config_processor.stage_builds_attributes('test').size).to eq(1) - expect(config_processor.stage_builds_attributes('test').first[:cache]).to eq( - paths: ['logs/', 'binaries/'], + expect(config_processor.stage_builds_attributes('test').first[:cache]).to eq([ + paths: ['binaries/'], untracked: true, key: { files: ['file'] }, policy: 'pull-push', when: 'on_success' - ) + ]) end it 'returns cache files with prefix' do @@ -1481,34 +1644,34 @@ module Gitlab config_processor = Gitlab::Ci::YamlProcessor.new(config).execute expect(config_processor.stage_builds_attributes('test').size).to eq(1) - expect(config_processor.stage_builds_attributes('test').first[:cache]).to eq( + expect(config_processor.stage_builds_attributes('test').first[:cache]).to eq([ paths: ['logs/', 'binaries/'], untracked: true, key: { files: ['file'], prefix: 'prefix' }, policy: 'pull-push', when: 'on_success' - ) + ]) end it "overwrite cache when defined for a job and globally" do config = YAML.dump({ - cache: { paths: ["logs/", "binaries/"], untracked: true, key: 'global' }, - rspec: { - script: "rspec", - cache: { paths: ["test/"], untracked: false, key: 'local' } - } - }) + cache: { paths: ["logs/", "binaries/"], untracked: true, key: 'global' }, + rspec: { + script: "rspec", + cache: { paths: ["test/"], untracked: false, key: 'local' } + } + }) config_processor = Gitlab::Ci::YamlProcessor.new(config).execute expect(config_processor.stage_builds_attributes("test").size).to eq(1) - expect(config_processor.stage_builds_attributes("test").first[:cache]).to eq( + expect(config_processor.stage_builds_attributes("test").first[:cache]).to eq([ paths: ["test/"], untracked: false, key: 'local', policy: 'pull-push', when: 'on_success' - ) + ]) end end @@ -1926,8 +2089,8 @@ module Gitlab only: { refs: %w[branches tags] }, options: { script: ["test"] }, needs_attributes: [ - { name: "build1", artifacts: true }, - { name: "build2", artifacts: true } + { name: "build1", artifacts: true, optional: false }, + { name: "build2", artifacts: true, optional: false } ], when: "on_success", allow_failure: false, @@ -1941,7 +2104,7 @@ module Gitlab let(:needs) do [ { job: 'parallel', artifacts: false }, - { job: 'build1', artifacts: true }, + { job: 'build1', artifacts: true, optional: true }, 'build2' ] end @@ -1968,10 +2131,10 @@ module Gitlab only: { refs: %w[branches tags] }, options: { script: ["test"] }, needs_attributes: [ - { name: "parallel 1/2", artifacts: false }, - { name: "parallel 2/2", artifacts: false }, - { name: "build1", artifacts: true }, - { name: "build2", artifacts: true } + { name: "parallel 1/2", artifacts: false, optional: false }, + { name: "parallel 2/2", artifacts: false, optional: false }, + { name: "build1", artifacts: true, optional: true }, + { name: "build2", artifacts: true, optional: false } ], when: "on_success", allow_failure: false, @@ -1993,8 +2156,8 @@ module Gitlab only: { refs: %w[branches tags] }, options: { script: ["test"] }, needs_attributes: [ - { name: "parallel 1/2", artifacts: true }, - { name: "parallel 2/2", artifacts: true } + { name: "parallel 1/2", artifacts: true, optional: false }, + { name: "parallel 2/2", artifacts: true, optional: false } ], when: "on_success", allow_failure: false, @@ -2022,10 +2185,10 @@ module Gitlab only: { refs: %w[branches tags] }, options: { script: ["test"] }, needs_attributes: [ - { name: "build1", artifacts: true }, - { name: "build2", artifacts: true }, - { name: "parallel 1/2", artifacts: true }, - { name: "parallel 2/2", artifacts: true } + { name: "build1", artifacts: true, optional: false }, + { name: "build2", artifacts: true, optional: false }, + { name: "parallel 1/2", artifacts: true, optional: false }, + { name: "parallel 2/2", artifacts: true, optional: false } ], when: "on_success", allow_failure: false, |