diff options
Diffstat (limited to 'spec/lib/gitlab/ci')
38 files changed, 1692 insertions, 294 deletions
diff --git a/spec/lib/gitlab/ci/ansi2html_spec.rb b/spec/lib/gitlab/ci/ansi2html_spec.rb index f29a39e4e66..bf1f2bae7da 100644 --- a/spec/lib/gitlab/ci/ansi2html_spec.rb +++ b/spec/lib/gitlab/ci/ansi2html_spec.rb @@ -213,6 +213,7 @@ RSpec.describe Gitlab::Ci::Ansi2html do " data-timestamp=\"#{section_start_time.to_i}\" data-section=\"#{class_name(section_name)}\"" \ ' role="button"></div>' end + let(:section_end_html) do "<div class=\"section-end\" data-section=\"#{class_name(section_name)}\"></div>" end diff --git a/spec/lib/gitlab/ci/build/artifacts/expire_in_parser_spec.rb b/spec/lib/gitlab/ci/build/artifacts/expire_in_parser_spec.rb new file mode 100644 index 00000000000..0e26a9fa571 --- /dev/null +++ b/spec/lib/gitlab/ci/build/artifacts/expire_in_parser_spec.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Build::Artifacts::ExpireInParser do + describe '.validate_duration' do + subject { described_class.validate_duration(value) } + + context 'with never' do + let(:value) { 'never' } + + it { is_expected.to be_truthy } + end + + context 'with never value camelized' do + let(:value) { 'Never' } + + it { is_expected.to be_truthy } + end + + context 'with a duration' do + let(:value) { '1 Day' } + + it { is_expected.to be_truthy } + end + + context 'without a duration' do + let(:value) { 'something' } + + it { is_expected.to be_falsy } + end + end + + describe '#seconds_from_now' do + subject { described_class.new(value).seconds_from_now } + + context 'with never' do + let(:value) { 'never' } + + it { is_expected.to be_nil } + end + + context 'with an empty string' do + let(:value) { '' } + + it { is_expected.to be_nil } + end + + context 'with a duration' do + let(:value) { '1 day' } + + it { is_expected.to be_like_time(1.day.from_now) } + end + end +end diff --git a/spec/lib/gitlab/ci/build/auto_retry_spec.rb b/spec/lib/gitlab/ci/build/auto_retry_spec.rb new file mode 100644 index 00000000000..cfa8c9cd938 --- /dev/null +++ b/spec/lib/gitlab/ci/build/auto_retry_spec.rb @@ -0,0 +1,127 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Build::AutoRetry do + let(:auto_retry) { described_class.new(build) } + + describe '#allowed?' do + using RSpec::Parameterized::TableSyntax + + let(:build) { create(:ci_build) } + + subject { auto_retry.allowed? } + + where(:description, :retry_count, :options, :failure_reason, :result) do + "retries are disabled" | 0 | { max: 0 } | nil | false + "max equals count" | 2 | { max: 2 } | nil | false + "max is higher than count" | 1 | { max: 2 } | nil | true + "max is a string" | 1 | { max: '2' } | nil | true + "matching failure reason" | 0 | { when: %w[api_failure], max: 2 } | :api_failure | true + "not matching with always" | 0 | { when: %w[always], max: 2 } | :api_failure | true + "not matching reason" | 0 | { when: %w[script_error], max: 2 } | :api_failure | false + "scheduler failure override" | 1 | { when: %w[scheduler_failure], max: 1 } | :scheduler_failure | false + "default for scheduler failure" | 1 | {} | :scheduler_failure | true + end + + with_them do + before do + allow(build).to receive(:retries_count) { retry_count } + + build.options[:retry] = options + build.failure_reason = failure_reason + allow(build).to receive(:retryable?).and_return(true) + end + + it { is_expected.to eq(result) } + end + + context 'when build is not retryable' do + before do + allow(build).to receive(:retryable?).and_return(false) + end + + specify { expect(subject).to eq(false) } + end + end + + describe '#options_retry_max' do + subject(:result) { auto_retry.send(:options_retry_max) } + + context 'with retries max config option' do + let(:build) { create(:ci_build, options: { retry: { max: 1 } }) } + + context 'when build_metadata_config is set' do + before do + stub_feature_flags(ci_build_metadata_config: true) + end + + it 'returns the number of configured max retries' do + expect(result).to eq 1 + end + end + + context 'when build_metadata_config is not set' do + before do + stub_feature_flags(ci_build_metadata_config: false) + end + + it 'returns the number of configured max retries' do + expect(result).to eq 1 + end + end + end + + context 'without retries max config option' do + let(:build) { create(:ci_build) } + + it 'returns nil' do + expect(result).to be_nil + end + end + + context 'when build is degenerated' do + let(:build) { create(:ci_build, :degenerated) } + + it 'returns nil' do + expect(result).to be_nil + end + end + + context 'with integer only config option' do + let(:build) { create(:ci_build, options: { retry: 1 }) } + + it 'returns the number of configured max retries' do + expect(result).to eq 1 + end + end + end + + describe '#options_retry_when' do + subject(:result) { auto_retry.send(:options_retry_when) } + + context 'with retries when config option' do + let(:build) { create(:ci_build, options: { retry: { when: ['some_reason'] } }) } + + it 'returns the configured when' do + expect(result).to eq ['some_reason'] + end + end + + context 'without retries when config option' do + let(:build) { create(:ci_build) } + + it 'returns always array' do + expect(result).to eq ['always'] + end + end + + context 'with integer only config option' do + let(:build) { create(:ci_build, options: { retry: 1 }) } + + it 'returns always array' do + expect(result).to eq ['always'] + 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 180c52ee1ab..ca02eaee0a0 100644 --- a/spec/lib/gitlab/ci/config/entry/job_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/job_spec.rb @@ -30,7 +30,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Job do %i[before_script script stage type after_script cache image services only except rules needs variables artifacts environment coverage retry interruptible timeout release tags - inherit] + inherit parallel] end it { is_expected.to include(*result) } @@ -73,6 +73,45 @@ RSpec.describe Gitlab::Ci::Config::Entry::Job do it { is_expected.to be_falsey } end + + context 'when config does not contain script' do + let(:name) { :build } + + let(:config) do + { before_script: "cd ${PROJ_DIR} " } + end + + it { is_expected.to be_truthy } + end + + context 'when using the default job without script' do + let(:name) { :default } + let(:config) do + { before_script: "cd ${PROJ_DIR} " } + end + + it { is_expected.to be_falsey } + end + + context 'when using the default job with script' do + let(:name) { :default } + let(:config) do + { + before_script: "cd ${PROJ_DIR} ", + script: "ls" + } + end + + it { is_expected.to be_truthy } + end + + context 'there are no shared keys between jobs and bridges' do + subject(:shared_values) do + described_class::ALLOWED_KEYS & Gitlab::Ci::Config::Entry::Bridge::ALLOWED_KEYS + end + + it { is_expected.to be_empty } + end end describe 'validations' do @@ -202,56 +241,47 @@ RSpec.describe Gitlab::Ci::Config::Entry::Job do context 'when parallel value is not correct' do context 'when it is not a numeric value' do - let(:config) { { parallel: true } } + let(:config) { { script: 'echo', parallel: true } } it 'returns error about invalid type' do expect(entry).not_to be_valid - expect(entry.errors).to include 'job parallel is not a number' + expect(entry.errors).to include 'parallel should be an integer or a hash' end end context 'when it is lower than two' do - let(:config) { { parallel: 1 } } + let(:config) { { script: 'echo', parallel: 1 } } it 'returns error about value too low' do expect(entry).not_to be_valid expect(entry.errors) - .to include 'job parallel must be greater than or equal to 2' + .to include 'parallel config must be greater than or equal to 2' end end - context 'when it is bigger than 50' do - let(:config) { { parallel: 51 } } + context 'when it is an empty hash' do + let(:config) { { script: 'echo', parallel: {} } } - it 'returns error about value too high' do + it 'returns error about missing matrix' do expect(entry).not_to be_valid expect(entry.errors) - .to include 'job parallel must be less than or equal to 50' + .to include 'parallel config missing required keys: matrix' end end + end - context 'when it is not an integer' do - let(:config) { { parallel: 1.5 } } - - it 'returns error about wrong value' do - expect(entry).not_to be_valid - expect(entry.errors).to include 'job parallel must be an integer' - end + context 'when it uses both "when:" and "rules:"' do + let(:config) do + { + script: 'echo', + when: 'on_failure', + rules: [{ if: '$VARIABLE', when: 'on_success' }] + } end - context 'when it uses both "when:" and "rules:"' do - let(:config) do - { - script: 'echo', - when: 'on_failure', - rules: [{ if: '$VARIABLE', when: 'on_success' }] - } - end - - it 'returns an error about when: being combined with rules' do - expect(entry).not_to be_valid - expect(entry.errors).to include 'job config key may not be used with `rules`: when' - end + it 'returns an error about when: being combined with rules' do + expect(entry).not_to be_valid + expect(entry.errors).to include 'job config key may not be used with `rules`: when' end end diff --git a/spec/lib/gitlab/ci/config/entry/processable_spec.rb b/spec/lib/gitlab/ci/config/entry/processable_spec.rb index fdf6008f89f..ac8dd2a3267 100644 --- a/spec/lib/gitlab/ci/config/entry/processable_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/processable_spec.rb @@ -230,6 +230,12 @@ RSpec.describe Gitlab::Ci::Config::Entry::Processable do end end + shared_examples 'has no warnings' do + it 'does not raise the warning' do + expect(entry.warnings).to be_empty + end + end + context 'when workflow rules is used' do let(:workflow) { double('workflow', 'has_rules?' => true) } @@ -254,6 +260,86 @@ RSpec.describe Gitlab::Ci::Config::Entry::Processable do end end + context 'when workflow rules is not used' do + let(:workflow) { double('workflow', 'has_rules?' => false) } + let(:feature_flag_value) { true } + + before do + stub_feature_flags(ci_raise_job_rules_without_workflow_rules_warning: feature_flag_value) + entry.compose!(deps) + end + + context 'when rules are valid' do + let(:config) do + { + script: 'ls', + rules: [ + { if: '$CI_COMMIT_BRANCH', when: 'on_success' }, + last_rule + ] + } + end + + context 'when last rule contains only `when`' do + let(:last_rule) { { when: when_value } } + + context 'and its value is not `never`' do + let(:when_value) { 'on_success' } + + it 'raises a warning' do + expect(entry.warnings).to contain_exactly(/may allow multiple pipelines/) + end + + context 'when feature flag is disabled' do + let(:feature_flag_value) { false } + + it_behaves_like 'has no warnings' + end + end + + context 'and its value is `never`' do + let(:when_value) { 'never' } + + it_behaves_like 'has no warnings' + end + end + + context 'when last rule does not contain only `when`' do + let(:last_rule) { { if: '$CI_MERGE_REQUEST_ID', when: 'always' } } + + it_behaves_like 'has no warnings' + end + end + + context 'when rules are invalid' do + let(:config) { { script: 'ls', rules: { when: 'always' } } } + + it_behaves_like 'has no warnings' + end + end + + context 'when workflow rules is used' do + let(:workflow) { double('workflow', 'has_rules?' => true) } + + before do + entry.compose!(deps) + end + + context 'when last rule contains only `when' do + let(:config) do + { + script: 'ls', + rules: [ + { if: '$CI_COMMIT_BRANCH', when: 'on_success' }, + { when: 'always' } + ] + } + end + + it_behaves_like 'has no warnings' + end + end + context 'with inheritance' do context 'of variables' do let(:config) do diff --git a/spec/lib/gitlab/ci/config/entry/product/matrix_spec.rb b/spec/lib/gitlab/ci/config/entry/product/matrix_spec.rb new file mode 100644 index 00000000000..39697884e3b --- /dev/null +++ b/spec/lib/gitlab/ci/config/entry/product/matrix_spec.rb @@ -0,0 +1,188 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' +require_dependency 'active_model' + +RSpec.describe ::Gitlab::Ci::Config::Entry::Product::Matrix do + subject(:matrix) { described_class.new(config) } + + describe 'validations' do + before do + matrix.compose! + end + + context 'when entry config value is correct' do + let(:config) do + [ + { 'VAR_1' => [1, 2, 3], 'VAR_2' => [4, 5, 6] }, + { 'VAR_3' => %w[a b], 'VAR_4' => %w[c d] } + ] + end + + describe '#valid?' do + it { is_expected.to be_valid } + end + end + + context 'when entry config generates too many jobs' do + let(:config) do + [ + { + 'VAR_1' => (1..10).to_a, + 'VAR_2' => (11..20).to_a + } + ] + end + + describe '#valid?' do + it { is_expected.not_to be_valid } + end + + describe '#errors' do + it 'returns error about too many jobs' do + expect(matrix.errors) + .to include('matrix config generates too many jobs (maximum is 50)') + end + end + end + + context 'when entry config has only one variable' do + let(:config) do + [ + { + 'VAR_1' => %w[test] + } + ] + end + + describe '#valid?' do + it { is_expected.not_to be_valid } + end + + describe '#errors' do + it 'returns error about too many jobs' do + expect(matrix.errors) + .to include('variables config requires at least 2 items') + end + end + + describe '#value' do + before do + matrix.compose! + end + + it 'returns the value without raising an error' do + expect(matrix.value).to eq([{ 'VAR_1' => ['test'] }]) + end + end + end + + context 'when config value has wrong type' do + let(:config) { {} } + + describe '#valid?' do + it { is_expected.not_to be_valid } + end + + describe '#errors' do + it 'returns error about incorrect type' do + expect(matrix.errors) + .to include('matrix config should be an array of hashes') + end + end + end + end + + describe '.compose!' do + context 'when valid job entries composed' do + let(:config) do + [ + { PROVIDER: 'aws', STACK: %w[monitoring app1 app2] }, + { STACK: %w[monitoring backup app], PROVIDER: 'ovh' }, + { PROVIDER: 'gcp', STACK: %w[data processing], ARGS: 'normal' }, + { PROVIDER: 'vultr', STACK: 'data', ARGS: 'store' } + ] + end + + before do + matrix.compose! + end + + describe '#value' do + it 'returns key value' do + expect(matrix.value).to match( + [ + { 'PROVIDER' => %w[aws], 'STACK' => %w[monitoring app1 app2] }, + { 'PROVIDER' => %w[ovh], 'STACK' => %w[monitoring backup app] }, + { 'ARGS' => %w[normal], 'PROVIDER' => %w[gcp], 'STACK' => %w[data processing] }, + { 'ARGS' => %w[store], 'PROVIDER' => %w[vultr], 'STACK' => %w[data] } + ] + ) + end + end + + describe '#descendants' do + it 'creates valid descendant nodes' do + expect(matrix.descendants.count).to eq(config.size) + expect(matrix.descendants) + .to all(be_an_instance_of(::Gitlab::Ci::Config::Entry::Product::Variables)) + end + end + end + + context 'with empty config' do + let(:config) { [] } + + before do + matrix.compose! + end + + describe '#value' do + it 'returns empty value' do + expect(matrix.value).to eq([]) + end + end + end + end + + describe '#number_of_generated_jobs' do + before do + matrix.compose! + end + + subject { matrix.number_of_generated_jobs } + + context 'with empty config' do + let(:config) { [] } + + it { is_expected.to be_zero } + end + + context 'with only one variable' do + let(:config) do + [{ 'VAR_1' => (1..10).to_a }] + end + + it { is_expected.to eq(10) } + end + + context 'with two variables' do + let(:config) do + [{ 'VAR_1' => (1..10).to_a, 'VAR_2' => (1..5).to_a }] + end + + it { is_expected.to eq(50) } + end + + context 'with two sets of variables' do + let(:config) do + [ + { 'VAR_1' => (1..10).to_a, 'VAR_2' => (1..5).to_a }, + { 'VAR_3' => (1..2).to_a, 'VAR_4' => (1..3).to_a } + ] + end + + it { is_expected.to eq(56) } + end + end +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 new file mode 100644 index 00000000000..bc09e20d748 --- /dev/null +++ b/spec/lib/gitlab/ci/config/entry/product/parallel_spec.rb @@ -0,0 +1,94 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' +require_dependency 'active_model' + +RSpec.describe ::Gitlab::Ci::Config::Entry::Product::Parallel do + subject(:parallel) { described_class.new(config) } + + context 'with invalid config' do + 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 'when it is not a numeric value' do + let(:config) { true } + + it_behaves_like 'invalid config', /should be an integer or a hash/ + end + + context 'when it is lower than two' do + let(:config) { 1 } + + it_behaves_like 'invalid config', /must be greater than or equal to 2/ + end + + context 'when it is bigger than 50' do + let(:config) { 51 } + + it_behaves_like 'invalid config', /must be less than or equal to 50/ + end + + context 'when it is not an integer' do + let(:config) { 1.5 } + + it_behaves_like 'invalid config', /must be an integer/ + end + + context 'with empty hash config' do + let(:config) { {} } + + it_behaves_like 'invalid config', /matrix builds config missing required keys: matrix/ + end + end + + context 'with numeric config' do + context 'when job is specified' do + let(:config) { 2 } + + describe '#valid?' do + it { is_expected.to be_valid } + end + + describe '#value' do + it 'returns job needs configuration' do + expect(parallel.value).to match(number: config) + end + end + end + end + + context 'with matrix builds config' do + context 'when matrix is specified' do + let(:config) do + { + matrix: [ + { PROVIDER: 'aws', STACK: %w[monitoring app1 app2] }, + { PROVIDER: 'gcp', STACK: %w[data processing] } + ] + } + end + + describe '#valid?' do + it { is_expected.to be_valid } + end + + describe '#value' do + it 'returns job needs configuration' do + expect(parallel.value).to match(matrix: [ + { PROVIDER: 'aws', STACK: %w[monitoring app1 app2] }, + { PROVIDER: 'gcp', STACK: %w[data processing] } + ]) + end + end + end + end +end diff --git a/spec/lib/gitlab/ci/config/entry/product/variables_spec.rb b/spec/lib/gitlab/ci/config/entry/product/variables_spec.rb new file mode 100644 index 00000000000..230b001d620 --- /dev/null +++ b/spec/lib/gitlab/ci/config/entry/product/variables_spec.rb @@ -0,0 +1,88 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' +require_dependency 'active_model' + +RSpec.describe Gitlab::Ci::Config::Entry::Product::Variables do + let(:entry) { described_class.new(config) } + + describe 'validations' do + context 'when entry config value is correct' do + let(:config) do + { + 'VARIABLE_1' => 1, + 'VARIABLE_2' => 'value 2', + 'VARIABLE_3' => :value_3, + :VARIABLE_4 => 'value 4', + 5 => ['value 5'], + 'VARIABLE_6' => ['value 6'] + } + end + + describe '#value' do + it 'returns hash with key value strings' do + expect(entry.value).to match({ + 'VARIABLE_1' => ['1'], + 'VARIABLE_2' => ['value 2'], + 'VARIABLE_3' => ['value_3'], + 'VARIABLE_4' => ['value 4'], + '5' => ['value 5'], + 'VARIABLE_6' => ['value 6'] + }) + end + end + + describe '#errors' do + it 'does not append errors' do + expect(entry.errors).to be_empty + end + end + + describe '#valid?' do + it 'is valid' do + expect(entry).to be_valid + end + end + end + + context 'when entry value is not correct' do + shared_examples 'invalid variables' do |message| + describe '#errors' do + it 'saves errors' do + expect(entry.errors).to include(message) + end + end + + describe '#valid?' do + it 'is not valid' do + expect(entry).not_to be_valid + end + end + end + + context 'with array' do + let(:config) { [:VAR, 'test'] } + + it_behaves_like 'invalid variables', /should be a hash of key value pairs/ + end + + context 'with empty array' do + let(:config) { { VAR: 'test', VAR2: [] } } + + it_behaves_like 'invalid variables', /should be a hash of key value pairs/ + end + + context 'with nested array' do + let(:config) { { VAR: 'test', VAR2: [1, [2]] } } + + it_behaves_like 'invalid variables', /should be a hash of key value pairs/ + end + + context 'with only one variable' do + let(:config) { { VAR: 'test' } } + + it_behaves_like 'invalid variables', /variables config requires at least 2 items/ + end + end + end +end diff --git a/spec/lib/gitlab/ci/config/entry/service_spec.rb b/spec/lib/gitlab/ci/config/entry/service_spec.rb index 9fbc14c19b9..ec137ef2ae4 100644 --- a/spec/lib/gitlab/ci/config/entry/service_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/service_spec.rb @@ -95,6 +95,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Service do let(:config) do { name: 'postgresql:9.5', alias: 'db', command: %w(cmd run), entrypoint: %w(/bin/sh run), ports: ports } end + let(:entry) { described_class.new(config, { with_image_ports: image_ports }) } let(:image_ports) { false } diff --git a/spec/lib/gitlab/ci/config/external/file/local_spec.rb b/spec/lib/gitlab/ci/config/external/file/local_spec.rb index 993a07568de..fdd29afe2d6 100644 --- a/spec/lib/gitlab/ci/config/external/file/local_spec.rb +++ b/spec/lib/gitlab/ci/config/external/file/local_spec.rb @@ -92,6 +92,7 @@ RSpec.describe Gitlab::Ci::Config::External::File::Local do - bundle install --jobs $(nproc) "${FLAGS[@]}" HEREDOC end + let(:location) { '/lib/gitlab/ci/templates/existent-file.yml' } before do diff --git a/spec/lib/gitlab/ci/config/external/processor_spec.rb b/spec/lib/gitlab/ci/config/external/processor_spec.rb index b2cf36b2597..9786e050399 100644 --- a/spec/lib/gitlab/ci/config/external/processor_spec.rb +++ b/spec/lib/gitlab/ci/config/external/processor_spec.rb @@ -128,6 +128,7 @@ RSpec.describe Gitlab::Ci::Config::External::Processor do remote_file ] end + let(:values) do { include: external_files, diff --git a/spec/lib/gitlab/ci/config/normalizer/factory_spec.rb b/spec/lib/gitlab/ci/config/normalizer/factory_spec.rb new file mode 100644 index 00000000000..e355740222f --- /dev/null +++ b/spec/lib/gitlab/ci/config/normalizer/factory_spec.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' + +RSpec.describe Gitlab::Ci::Config::Normalizer::Factory do + describe '#create' do + context 'when no strategy applies' do + subject(:subject) { described_class.new(nil, nil).create } # rubocop:disable Rails/SaveBang + + it { is_expected.to be_empty } + end + end +end diff --git a/spec/lib/gitlab/ci/config/normalizer/matrix_strategy_spec.rb b/spec/lib/gitlab/ci/config/normalizer/matrix_strategy_spec.rb new file mode 100644 index 00000000000..bab604c4504 --- /dev/null +++ b/spec/lib/gitlab/ci/config/normalizer/matrix_strategy_spec.rb @@ -0,0 +1,102 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' + +RSpec.describe Gitlab::Ci::Config::Normalizer::MatrixStrategy do + describe '.applies_to?' do + subject { described_class.applies_to?(config) } + + context 'with hash that has :matrix key' do + let(:config) { { matrix: [] } } + + it { is_expected.to be_truthy } + end + + context 'with hash that does not have :matrix key' do + let(:config) { { number: [] } } + + it { is_expected.to be_falsey } + end + + context 'with a number' do + let(:config) { 5 } + + it { is_expected.to be_falsey } + end + end + + describe '.build_from' do + subject { described_class.build_from('test', config) } + + let(:config) do + { + matrix: [ + { 'PROVIDER' => %w[aws], 'STACK' => %w[app1 app2] }, + { 'PROVIDER' => %w[ovh gcp], 'STACK' => %w[app] } + ] + } + end + + it { expect(subject.size).to eq(4) } + + it 'has attributes' do + expect(subject.map(&:attributes)).to match_array( + [ + { + name: 'test 1/4', + instance: 1, + parallel: { total: 4 }, + variables: { + 'PROVIDER' => 'aws', + 'STACK' => 'app1' + } + }, + { + name: 'test 2/4', + instance: 2, + parallel: { total: 4 }, + variables: { + 'PROVIDER' => 'aws', + 'STACK' => 'app2' + } + }, + { + name: 'test 3/4', + instance: 3, + parallel: { total: 4 }, + variables: { + 'PROVIDER' => 'ovh', + 'STACK' => 'app' + } + }, + { + name: 'test 4/4', + instance: 4, + parallel: { total: 4 }, + variables: { + 'PROVIDER' => 'gcp', + 'STACK' => 'app' + } + } + ] + ) + end + + it 'has parallelized name' do + expect(subject.map(&:name)).to match_array( + ['test 1/4', 'test 2/4', 'test 3/4', 'test 4/4'] + ) + end + + it 'has details' do + expect(subject.map(&:name_with_details)).to match_array( + [ + 'test (PROVIDER=aws; STACK=app1)', + 'test (PROVIDER=aws; STACK=app2)', + 'test (PROVIDER=gcp; STACK=app)', + 'test (PROVIDER=ovh; STACK=app)' + ] + ) + end + end +end diff --git a/spec/lib/gitlab/ci/config/normalizer/number_strategy_spec.rb b/spec/lib/gitlab/ci/config/normalizer/number_strategy_spec.rb new file mode 100644 index 00000000000..06f47fe11c6 --- /dev/null +++ b/spec/lib/gitlab/ci/config/normalizer/number_strategy_spec.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' + +RSpec.describe Gitlab::Ci::Config::Normalizer::NumberStrategy do + describe '.applies_to?' do + subject { described_class.applies_to?(config) } + + context 'with numbers' do + let(:config) { 5 } + + it { is_expected.to be_truthy } + end + + context 'with hash that has :number key' do + let(:config) { { number: 5 } } + + it { is_expected.to be_truthy } + end + + context 'with a float number' do + let(:config) { 5.5 } + + it { is_expected.to be_falsey } + end + + context 'with hash that does not have :number key' do + let(:config) { { matrix: 5 } } + + it { is_expected.to be_falsey } + end + end + + describe '.build_from' do + subject { described_class.build_from('test', config) } + + shared_examples 'parallelized job' do + it { expect(subject.size).to eq(3) } + + it 'has attributes' do + expect(subject.map(&:attributes)).to match_array( + [ + { name: 'test 1/3', instance: 1, parallel: { total: 3 } }, + { name: 'test 2/3', instance: 2, parallel: { total: 3 } }, + { name: 'test 3/3', instance: 3, parallel: { total: 3 } } + ] + ) + end + + it 'has parallelized name' do + expect(subject.map(&:name)).to match_array( + ['test 1/3', 'test 2/3', 'test 3/3']) + end + end + + context 'with numbers' do + let(:config) { 3 } + + it_behaves_like 'parallelized job' + end + + context 'with hash that has :number key' do + let(:config) { { number: 3 } } + + it_behaves_like 'parallelized job' + end + end +end diff --git a/spec/lib/gitlab/ci/config/normalizer_spec.rb b/spec/lib/gitlab/ci/config/normalizer_spec.rb index d3d165ba00f..949af8cdc4c 100644 --- a/spec/lib/gitlab/ci/config/normalizer_spec.rb +++ b/spec/lib/gitlab/ci/config/normalizer_spec.rb @@ -4,66 +4,13 @@ require 'fast_spec_helper' RSpec.describe Gitlab::Ci::Config::Normalizer do let(:job_name) { :rspec } - let(:job_config) { { script: 'rspec', parallel: 5, name: 'rspec' } } + let(:job_config) { { script: 'rspec', parallel: parallel_config, name: 'rspec', variables: variables_config } } let(:config) { { job_name => job_config } } - let(:expanded_job_names) do - [ - "rspec 1/5", - "rspec 2/5", - "rspec 3/5", - "rspec 4/5", - "rspec 5/5" - ] - end - describe '.normalize_jobs' do subject { described_class.new(config).normalize_jobs } - it 'does not have original job' do - is_expected.not_to include(job_name) - end - - it 'has parallelized jobs' do - is_expected.to include(*expanded_job_names.map(&:to_sym)) - end - - it 'sets job instance in options' do - expect(subject.values).to all(include(:instance)) - end - - it 'parallelizes jobs with original config' do - original_config = config[job_name].except(:name) - configs = subject.values.map { |config| config.except(:name, :instance) } - - expect(configs).to all(eq(original_config)) - end - - context 'when the job is not parallelized' do - let(:job_config) { { script: 'rspec', name: 'rspec' } } - - it 'returns the same hash' do - is_expected.to eq(config) - end - end - - context 'when there is a job with a slash in it' do - let(:job_name) { :"rspec 35/2" } - - it 'properly parallelizes job names' do - job_names = [ - :"rspec 35/2 1/5", - :"rspec 35/2 2/5", - :"rspec 35/2 3/5", - :"rspec 35/2 4/5", - :"rspec 35/2 5/5" - ] - - is_expected.to include(*job_names) - end - end - - context 'for dependencies' do + shared_examples 'parallel dependencies' do context "when job has dependencies on parallelized jobs" do let(:config) do { @@ -91,9 +38,7 @@ RSpec.describe Gitlab::Ci::Config::Normalizer do end it "parallelizes dependencies" do - job_names = ["rspec 1/5", "rspec 2/5", "rspec 3/5", "rspec 4/5", "rspec 5/5"] - - expect(subject[:final_job][:dependencies]).to include(*job_names) + expect(subject[:final_job][:dependencies]).to include(*expanded_job_names) end it "includes the regular job in dependencies" do @@ -102,14 +47,14 @@ RSpec.describe Gitlab::Ci::Config::Normalizer do end end - context 'for needs' do + shared_examples 'parallel needs' do let(:expanded_job_attributes) do expanded_job_names.map do |job_name| { name: job_name, extra: :key } end end - context "when job has needs on parallelized jobs" do + context 'when job has needs on parallelized jobs' do let(:config) do { job_name => job_config, @@ -124,12 +69,12 @@ RSpec.describe Gitlab::Ci::Config::Normalizer do } end - it "parallelizes needs" do + it 'parallelizes needs' do expect(subject.dig(:other_job, :needs, :job)).to eq(expanded_job_attributes) end end - context "when there are dependencies which are both parallelized and not" do + context 'when there are dependencies which are both parallelized and not' do let(:config) do { job_name => job_config, @@ -141,21 +86,157 @@ RSpec.describe Gitlab::Ci::Config::Normalizer do needs: { job: [ { name: job_name.to_s, extra: :key }, - { name: "other_job", extra: :key } + { name: 'other_job', extra: :key } ] } } } end - it "parallelizes dependencies" do + it 'parallelizes dependencies' do expect(subject.dig(:final_job, :needs, :job)).to include(*expanded_job_attributes) end - it "includes the regular job in dependencies" do + it 'includes the regular job in dependencies' do expect(subject.dig(:final_job, :needs, :job)).to include(name: 'other_job', extra: :key) end end end + + context 'with parallel config as integer' do + let(:variables_config) { {} } + let(:parallel_config) { 5 } + + let(:expanded_job_names) do + [ + 'rspec 1/5', + 'rspec 2/5', + 'rspec 3/5', + 'rspec 4/5', + 'rspec 5/5' + ] + end + + it 'does not have original job' do + is_expected.not_to include(job_name) + end + + it 'has parallelized jobs' do + is_expected.to include(*expanded_job_names.map(&:to_sym)) + end + + it 'sets job instance in options' do + expect(subject.values).to all(include(:instance)) + end + + it 'parallelizes jobs with original config' do + original_config = config[job_name] + .except(:name) + .deep_merge(parallel: { total: parallel_config }) + + configs = subject.values.map { |config| config.except(:name, :instance) } + + expect(configs).to all(eq(original_config)) + end + + context 'when the job is not parallelized' do + let(:job_config) { { script: 'rspec', name: 'rspec' } } + + it 'returns the same hash' do + is_expected.to eq(config) + end + end + + context 'when there is a job with a slash in it' do + let(:job_name) { :"rspec 35/2" } + + it 'properly parallelizes job names' do + job_names = [ + :"rspec 35/2 1/5", + :"rspec 35/2 2/5", + :"rspec 35/2 3/5", + :"rspec 35/2 4/5", + :"rspec 35/2 5/5" + ] + + is_expected.to include(*job_names) + end + end + + it_behaves_like 'parallel dependencies' + it_behaves_like 'parallel needs' + end + + context 'with parallel matrix config' do + let(:variables_config) do + { + USER_VARIABLE: 'user value' + } + end + + let(:parallel_config) do + { + matrix: [ + { + VAR_1: [1], + VAR_2: [2, 3] + } + ] + } + end + + let(:expanded_job_names) do + [ + 'rspec 1/2', + 'rspec 2/2' + ] + end + + it 'does not have original job' do + is_expected.not_to include(job_name) + end + + it 'has parallelized jobs' do + is_expected.to include(*expanded_job_names.map(&:to_sym)) + end + + it 'sets job instance in options' do + expect(subject.values).to all(include(:instance)) + end + + it 'sets job variables', :aggregate_failures do + expect(subject.values[0]).to match( + a_hash_including(variables: { VAR_1: 1, VAR_2: 2, USER_VARIABLE: 'user value' }) + ) + + expect(subject.values[1]).to match( + a_hash_including(variables: { VAR_1: 1, VAR_2: 3, USER_VARIABLE: 'user value' }) + ) + end + + it 'parallelizes jobs with original config' do + configs = subject.values.map do |config| + config.except(:name, :instance, :variables) + end + + original_config = config[job_name] + .except(:name, :variables) + .deep_merge(parallel: { total: 2 }) + + expect(configs).to all(match(a_hash_including(original_config))) + end + + it_behaves_like 'parallel dependencies' + it_behaves_like 'parallel needs' + end + + context 'when parallel config does not matches a factory' do + let(:variables_config) { {} } + let(:parallel_config) { } + + it 'does not alter the job config' do + is_expected.to match(config) + end + end end end diff --git a/spec/lib/gitlab/ci/parsers/coverage/cobertura_spec.rb b/spec/lib/gitlab/ci/parsers/coverage/cobertura_spec.rb index 08a3fbd7867..45e87466532 100644 --- a/spec/lib/gitlab/ci/parsers/coverage/cobertura_spec.rb +++ b/spec/lib/gitlab/ci/parsers/coverage/cobertura_spec.rb @@ -19,6 +19,41 @@ RSpec.describe Gitlab::Ci::Parsers::Coverage::Cobertura do end end + context 'when there is a <sources>' do + shared_examples_for 'ignoring sources' do + it 'parses XML without errors' do + expect { subject }.not_to raise_error + + expect(coverage_report.files).to eq({}) + end + end + + context 'and has a single source' do + let(:cobertura) do + <<-EOF.strip_heredoc + <sources> + <source>project/src</source> + </sources> + EOF + end + + it_behaves_like 'ignoring sources' + end + + context 'and has multiple sources' do + let(:cobertura) do + <<-EOF.strip_heredoc + <sources> + <source>project/src/foo</source> + <source>project/src/bar</source> + </sources> + EOF + end + + it_behaves_like 'ignoring sources' + end + end + context 'when there is a single <class>' do context 'with no lines' do let(:cobertura) do diff --git a/spec/lib/gitlab/ci/pipeline/chain/sequence_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/sequence_spec.rb index 5d20b1b8fda..cc4aaffb0a4 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/sequence_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/sequence_spec.rb @@ -23,9 +23,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Sequence do end it 'does not process the second step' do - subject.build! do |pipeline, sequence| - expect(sequence).not_to be_complete - end + subject.build! expect(second_step).not_to have_received(:perform!) end @@ -43,9 +41,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Sequence do end it 'iterates through entire sequence' do - subject.build! do |pipeline, sequence| - expect(sequence).to be_complete - end + subject.build! expect(first_step).to have_received(:perform!) expect(second_step).to have_received(:perform!) diff --git a/spec/lib/gitlab/ci/pipeline/chain/validate/external_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/validate/external_spec.rb index 931c62701ce..de580d2e148 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/validate/external_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/validate/external_spec.rb @@ -41,9 +41,10 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Validate::External do ) end + let(:save_incompleted) { true } let(:command) do Gitlab::Ci::Pipeline::Chain::Command.new( - project: project, current_user: user, config_processor: yaml_processor + project: project, current_user: user, config_processor: yaml_processor, save_incompleted: save_incompleted ) end @@ -84,6 +85,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Validate::External do perform! expect(pipeline.status).to eq('failed') + expect(pipeline).to be_persisted expect(pipeline.errors.to_a).to include('External validation failed') end @@ -98,6 +100,30 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Validate::External do perform! end + + context 'when save_incompleted is false' do + let(:save_incompleted) { false} + + it 'adds errors to the pipeline without dropping it' do + perform! + + expect(pipeline.status).to eq('pending') + expect(pipeline).not_to be_persisted + expect(pipeline.errors.to_a).to include('External validation failed') + end + + it 'breaks the chain' do + perform! + + expect(step.break?).to be true + end + + it 'logs the authorization' do + expect(Gitlab::AppLogger).to receive(:info).with(message: 'Pipeline not authorized', project_id: project.id, user_id: user.id) + + perform! + end + end end end diff --git a/spec/lib/gitlab/ci/pipeline/chain/validate/repository_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/validate/repository_spec.rb index 1dc2e0a1822..7eefb4d7876 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/validate/repository_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/validate/repository_spec.rb @@ -68,6 +68,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Validate::Repository do proj.repository.add_tag(user, 'master', 'master') end end + let(:command) do Gitlab::Ci::Pipeline::Chain::Command.new( project: project, current_user: user, origin_ref: 'master') diff --git a/spec/lib/gitlab/ci/pipeline/expression/lexeme/and_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/lexeme/and_spec.rb index 6601537a2d3..1448b045b18 100644 --- a/spec/lib/gitlab/ci/pipeline/expression/lexeme/and_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/expression/lexeme/and_spec.rb @@ -24,7 +24,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Expression::Lexeme::And do describe '.type' do it 'is an operator' do - expect(described_class.type).to eq :operator + expect(described_class.type).to eq :logical_operator end end diff --git a/spec/lib/gitlab/ci/pipeline/expression/lexeme/equals_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/lexeme/equals_spec.rb index 2bed47f0a87..ab223ae41fa 100644 --- a/spec/lib/gitlab/ci/pipeline/expression/lexeme/equals_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/expression/lexeme/equals_spec.rb @@ -27,7 +27,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Expression::Lexeme::Equals do describe '.type' do it 'is an operator' do - expect(described_class.type).to eq :operator + expect(described_class.type).to eq :logical_operator end end diff --git a/spec/lib/gitlab/ci/pipeline/expression/lexeme/matches_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/lexeme/matches_spec.rb index efcea0b0e09..0da04d8dcf7 100644 --- a/spec/lib/gitlab/ci/pipeline/expression/lexeme/matches_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/expression/lexeme/matches_spec.rb @@ -28,7 +28,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Expression::Lexeme::Matches do describe '.type' do it 'is an operator' do - expect(described_class.type).to eq :operator + expect(described_class.type).to eq :logical_operator end end diff --git a/spec/lib/gitlab/ci/pipeline/expression/lexeme/not_equals_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/lexeme/not_equals_spec.rb index a81e1713ef0..3cde4c5d9dc 100644 --- a/spec/lib/gitlab/ci/pipeline/expression/lexeme/not_equals_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/expression/lexeme/not_equals_spec.rb @@ -27,7 +27,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Expression::Lexeme::NotEquals do describe '.type' do it 'is an operator' do - expect(described_class.type).to eq :operator + expect(described_class.type).to eq :logical_operator end end diff --git a/spec/lib/gitlab/ci/pipeline/expression/lexeme/not_matches_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/lexeme/not_matches_spec.rb index f44fe19f86d..9bff2355d58 100644 --- a/spec/lib/gitlab/ci/pipeline/expression/lexeme/not_matches_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/expression/lexeme/not_matches_spec.rb @@ -28,7 +28,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Expression::Lexeme::NotMatches do describe '.type' do it 'is an operator' do - expect(described_class.type).to eq :operator + expect(described_class.type).to eq :logical_operator end end diff --git a/spec/lib/gitlab/ci/pipeline/expression/lexeme/or_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/lexeme/or_spec.rb index 7fe445975eb..c7d89c4e1e9 100644 --- a/spec/lib/gitlab/ci/pipeline/expression/lexeme/or_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/expression/lexeme/or_spec.rb @@ -24,7 +24,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Expression::Lexeme::Or do describe '.type' do it 'is an operator' do - expect(described_class.type).to eq :operator + expect(described_class.type).to eq :logical_operator end end diff --git a/spec/lib/gitlab/ci/pipeline/expression/lexeme/pattern_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/lexeme/pattern_spec.rb index 1a56a91c471..fa4f8a20984 100644 --- a/spec/lib/gitlab/ci/pipeline/expression/lexeme/pattern_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/expression/lexeme/pattern_spec.rb @@ -70,7 +70,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Expression::Lexeme::Pattern do .to eq Gitlab::UntrustedRegexp.new('pattern') end - it 'is a eager scanner for regexp boundaries' do + it 'is an eager scanner for regexp boundaries' do scanner = StringScanner.new('/some .* / pattern/') token = described_class.scan(scanner) diff --git a/spec/lib/gitlab/ci/pipeline/expression/lexer_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/lexer_spec.rb index 61c6ced4dac..6e242faa885 100644 --- a/spec/lib/gitlab/ci/pipeline/expression/lexer_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/expression/lexer_spec.rb @@ -81,6 +81,35 @@ RSpec.describe Gitlab::Ci::Pipeline::Expression::Lexer do with_them do it { is_expected.to eq(tokens) } end + + context 'with parentheses are used' do + where(:expression, :tokens) do + '($PRESENT_VARIABLE =~ /my var/) && $EMPTY_VARIABLE =~ /nope/' | ['(', '$PRESENT_VARIABLE', '=~', '/my var/', ')', '&&', '$EMPTY_VARIABLE', '=~', '/nope/'] + '$PRESENT_VARIABLE =~ /my var/ || ($EMPTY_VARIABLE =~ /nope/)' | ['$PRESENT_VARIABLE', '=~', '/my var/', '||', '(', '$EMPTY_VARIABLE', '=~', '/nope/', ')'] + '($PRESENT_VARIABLE && (null || $EMPTY_VARIABLE == ""))' | ['(', '$PRESENT_VARIABLE', '&&', '(', 'null', '||', '$EMPTY_VARIABLE', '==', '""', ')', ')'] + end + + with_them do + context 'when ci_if_parenthesis_enabled is enabled' do + before do + stub_feature_flags(ci_if_parenthesis_enabled: true) + end + + it { is_expected.to eq(tokens) } + end + + context 'when ci_if_parenthesis_enabled is disabled' do + before do + stub_feature_flags(ci_if_parenthesis_enabled: false) + end + + it do + expect { subject } + .to raise_error described_class::SyntaxError + end + end + end + end end end diff --git a/spec/lib/gitlab/ci/pipeline/expression/parser_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/parser_spec.rb index 1704cabfd2e..3394a75ac0a 100644 --- a/spec/lib/gitlab/ci/pipeline/expression/parser_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/expression/parser_spec.rb @@ -1,51 +1,79 @@ # frozen_string_literal: true -require 'fast_spec_helper' +require 'spec_helper' RSpec.describe Gitlab::Ci::Pipeline::Expression::Parser do + before do + stub_feature_flags(ci_if_parenthesis_enabled: true) + end + describe '#tree' do - context 'when using two operators' do - it 'returns a reverse descent parse tree' do - expect(described_class.seed('$VAR1 == "123"').tree) - .to be_a Gitlab::Ci::Pipeline::Expression::Lexeme::Equals + context 'validates simple operators' do + using RSpec::Parameterized::TableSyntax + + where(:expression, :result_tree) do + '$VAR1 == "123"' | 'equals($VAR1, "123")' + '$VAR1 == "123" == $VAR2' | 'equals(equals($VAR1, "123"), $VAR2)' + '$VAR' | '$VAR' + '"some value"' | '"some value"' + 'null' | 'null' + '$VAR1 || $VAR2 && $VAR3' | 'or($VAR1, and($VAR2, $VAR3))' + '$VAR1 && $VAR2 || $VAR3' | 'or(and($VAR1, $VAR2), $VAR3)' + '$VAR1 && $VAR2 || $VAR3 && $VAR4' | 'or(and($VAR1, $VAR2), and($VAR3, $VAR4))' + '$VAR1 && ($VAR2 || $VAR3) && $VAR4' | 'and(and($VAR1, or($VAR2, $VAR3)), $VAR4)' end - end - context 'when using three operators' do - it 'returns a reverse descent parse tree' do - expect(described_class.seed('$VAR1 == "123" == $VAR2').tree) - .to be_a Gitlab::Ci::Pipeline::Expression::Lexeme::Equals + with_them do + it { expect(described_class.seed(expression).tree.inspect).to eq(result_tree) } end end - context 'when using a single variable token' do - it 'returns a single token instance' do - expect(described_class.seed('$VAR').tree) - .to be_a Gitlab::Ci::Pipeline::Expression::Lexeme::Variable + context 'when combining && and OR operators' do + subject { described_class.seed('$VAR1 == "a" || $VAR2 == "b" && $VAR3 == "c" || $VAR4 == "d" && $VAR5 == "e"').tree } + + context 'when parenthesis engine is enabled' do + before do + stub_feature_flags(ci_if_parenthesis_enabled: true) + end + + it 'returns operations in a correct order' do + expect(subject.inspect) + .to eq('or(or(equals($VAR1, "a"), and(equals($VAR2, "b"), equals($VAR3, "c"))), and(equals($VAR4, "d"), equals($VAR5, "e")))') + end + end + + context 'when parenthesis engine is disabled (legacy)' do + before do + stub_feature_flags(ci_if_parenthesis_enabled: false) + end + + it 'returns operations in a invalid order' do + expect(subject.inspect) + .to eq('or(equals($VAR1, "a"), and(equals($VAR2, "b"), or(equals($VAR3, "c"), and(equals($VAR4, "d"), equals($VAR5, "e")))))') + end end end - context 'when using a single string token' do - it 'returns a single token instance' do - expect(described_class.seed('"some value"').tree) - .to be_a Gitlab::Ci::Pipeline::Expression::Lexeme::String + context 'when using parenthesis' do + subject { described_class.seed('(($VAR1 == "a" || $VAR2 == "b") && $VAR3 == "c" || $VAR4 == "d") && $VAR5 == "e"').tree } + + before do + stub_feature_flags(ci_if_parenthesis_enabled: true) + end + + it 'returns operations in a correct order' do + expect(subject.inspect) + .to eq('and(or(and(or(equals($VAR1, "a"), equals($VAR2, "b")), equals($VAR3, "c")), equals($VAR4, "d")), equals($VAR5, "e"))') end end context 'when expression is empty' do - it 'returns a null token' do + it 'raises a parsing error' do expect { described_class.seed('').tree } .to raise_error Gitlab::Ci::Pipeline::Expression::Parser::ParseError end end - context 'when expression is null' do - it 'returns a null token' do - expect(described_class.seed('null').tree) - .to be_a Gitlab::Ci::Pipeline::Expression::Lexeme::Null - end - end - context 'when two value tokens have no operator' do it 'raises a parsing error' do expect { described_class.seed('$VAR "text"').tree } @@ -66,5 +94,42 @@ RSpec.describe Gitlab::Ci::Pipeline::Expression::Parser do .to raise_error Gitlab::Ci::Pipeline::Expression::Lexeme::Operator::OperatorError end end + + context 'when parenthesis are unmatched' do + context 'when parenthesis engine is enabled' do + before do + stub_feature_flags(ci_if_parenthesis_enabled: true) + end + + where(:expression) do + [ + '$VAR == (', + '$VAR2 == ("aa"', + '$VAR2 == ("aa"))', + '$VAR2 == "aa")', + '(($VAR2 == "aa")', + '($VAR2 == "aa"))' + ] + end + + with_them do + it 'raises a ParseError' do + expect { described_class.seed(expression).tree } + .to raise_error Gitlab::Ci::Pipeline::Expression::Parser::ParseError + end + end + end + + context 'when parenthesis engine is disabled' do + before do + stub_feature_flags(ci_if_parenthesis_enabled: false) + end + + it 'raises an SyntaxError' do + expect { described_class.seed('$VAR == (').tree } + .to raise_error Gitlab::Ci::Pipeline::Expression::Lexer::SyntaxError + end + 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 642d6816030..cf3644c9ad5 100644 --- a/spec/lib/gitlab/ci/pipeline/expression/statement_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/expression/statement_spec.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true -require 'fast_spec_helper' -require 'rspec-parameterized' +require 'spec_helper' RSpec.describe Gitlab::Ci::Pipeline::Expression::Statement do subject do @@ -109,6 +108,17 @@ RSpec.describe Gitlab::Ci::Pipeline::Expression::Statement do '$UNDEFINED_VARIABLE || $PRESENT_VARIABLE' | 'my variable' '$UNDEFINED_VARIABLE == null || $PRESENT_VARIABLE' | true '$PRESENT_VARIABLE || $UNDEFINED_VARIABLE == null' | 'my variable' + + '($PRESENT_VARIABLE)' | 'my variable' + '(($PRESENT_VARIABLE))' | 'my variable' + '(($PRESENT_VARIABLE && null) || $EMPTY_VARIABLE == "")' | true + '($PRESENT_VARIABLE) && (null || $EMPTY_VARIABLE == "")' | true + '("string" || "test") == "string"' | true + '(null || ("test" == "string"))' | false + '("string" == ("test" && "string"))' | true + '("string" == ("test" || "string"))' | false + '("string" == "test" || "string")' | "string" + '("string" == ("string" || (("1" == "1") && ("2" == "3"))))' | true end with_them do diff --git a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb index 2dea554fe56..733ab30132d 100644 --- a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb @@ -928,29 +928,51 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do end end - context 'when lower limit of needs is reached' do - before do - stub_feature_flags(ci_dag_limit_needs: true) - end + context 'when using 101 needs' do + let(:needs_count) { 101 } - let(:needs_count) { described_class::LOW_NEEDS_LIMIT + 1 } + context 'when ci_plan_needs_size_limit is disabled' do + before do + stub_feature_flags(ci_plan_needs_size_limit: false) + end - it "returns an error" do - expect(subject.errors).to contain_exactly( - "rspec: one job can only need 10 others, but you have listed 11. See needs keyword documentation for more details") + it "returns an error" do + expect(subject.errors).to contain_exactly( + "rspec: one job can only need 10 others, but you have listed 101. See needs keyword documentation for more details") + end end - end - context 'when upper limit of needs is reached' do - before do - stub_feature_flags(ci_dag_limit_needs: false) - end + context 'when ci_plan_needs_size_limit is enabled' do + before do + stub_feature_flags(ci_plan_needs_size_limit: true) + end - let(:needs_count) { described_class::HARD_NEEDS_LIMIT + 1 } + it "returns an error" do + expect(subject.errors).to contain_exactly( + "rspec: one job can only need 50 others, but you have listed 101. See needs keyword documentation for more details") + end - it "returns an error" do - expect(subject.errors).to contain_exactly( - "rspec: one job can only need 50 others, but you have listed 51. See needs keyword documentation for more details") + context 'when ci_needs_size_limit is set to 100' do + before do + project.actual_limits.update!(ci_needs_size_limit: 100) + end + + it "returns an error" do + expect(subject.errors).to contain_exactly( + "rspec: one job can only need 100 others, but you have listed 101. See needs keyword documentation for more details") + end + end + + context 'when ci_needs_size_limit is set to 0' do + before do + project.actual_limits.update!(ci_needs_size_limit: 0) + end + + it "returns an error" do + expect(subject.errors).to contain_exactly( + "rspec: one job can only need 0 others, but you have listed 101. See needs keyword documentation for more details") + end + end end end end diff --git a/spec/lib/gitlab/ci/reports/accessibility_reports_comparer_spec.rb b/spec/lib/gitlab/ci/reports/accessibility_reports_comparer_spec.rb index 240ede790e0..650ae41320b 100644 --- a/spec/lib/gitlab/ci/reports/accessibility_reports_comparer_spec.rb +++ b/spec/lib/gitlab/ci/reports/accessibility_reports_comparer_spec.rb @@ -21,6 +21,7 @@ RSpec.describe Gitlab::Ci::Reports::AccessibilityReportsComparer do } ] end + let(:different_error) do [ { diff --git a/spec/lib/gitlab/ci/reports/test_report_summary_spec.rb b/spec/lib/gitlab/ci/reports/test_report_summary_spec.rb index 70d82851125..555682cc006 100644 --- a/spec/lib/gitlab/ci/reports/test_report_summary_spec.rb +++ b/spec/lib/gitlab/ci/reports/test_report_summary_spec.rb @@ -11,68 +11,8 @@ RSpec.describe Gitlab::Ci::Reports::TestReportSummary do subject { test_report_summary.total } context 'when test report summary has several build report results' do - it 'returns test suite summary object' do - expect(subject).to be_a_kind_of(Gitlab::Ci::Reports::TestSuiteSummary) - end - end - end - - describe '#total_time' do - subject { test_report_summary.total_time } - - context 'when test report summary has several build report results' do - it 'returns the total' do - expect(subject).to eq(0.84) - end - end - end - - describe '#total_count' do - subject { test_report_summary.total_count } - - context 'when test report summary has several build report results' do - it 'returns the total count' do - expect(subject).to eq(4) - end - end - end - - describe '#success_count' do - subject { test_report_summary.success_count } - - context 'when test suite summary has several build report results' do - it 'returns the total success' do - expect(subject).to eq(2) - end - end - end - - describe '#failed_count' do - subject { test_report_summary.failed_count } - - context 'when test suite summary has several build report results' do - it 'returns the total failed' do - expect(subject).to eq(0) - end - end - end - - describe '#error_count' do - subject { test_report_summary.error_count } - - context 'when test suite summary has several build report results' do - it 'returns the total errored' do - expect(subject).to eq(2) - end - end - end - - describe '#skipped_count' do - subject { test_report_summary.skipped_count } - - context 'when test suite summary has several build report results' do - it 'returns the total skipped' do - expect(subject).to eq(0) + it 'returns all the total count in a hash' do + expect(subject).to include(:time, :count, :success, :failed, :skipped, :error) end end end diff --git a/spec/lib/gitlab/ci/reports/test_suite_spec.rb b/spec/lib/gitlab/ci/reports/test_suite_spec.rb index c4c4d2c3704..fbe3473f6b0 100644 --- a/spec/lib/gitlab/ci/reports/test_suite_spec.rb +++ b/spec/lib/gitlab/ci/reports/test_suite_spec.rb @@ -50,9 +50,11 @@ RSpec.describe Gitlab::Ci::Reports::TestSuite do before do test_suite.add_test_case(test_case_success) test_suite.add_test_case(test_case_failed) + test_suite.add_test_case(test_case_skipped) + test_suite.add_test_case(test_case_error) end - it { is_expected.to eq(2) } + it { is_expected.to eq(4) } end describe '#total_status' do 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 12c96acdcf3..a98d3db4e82 100644 --- a/spec/lib/gitlab/ci/reports/test_suite_summary_spec.rb +++ b/spec/lib/gitlab/ci/reports/test_suite_summary_spec.rb @@ -86,4 +86,14 @@ RSpec.describe Gitlab::Ci::Reports::TestSuiteSummary do 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) + end + end + end end diff --git a/spec/lib/gitlab/ci/runner_instructions_spec.rb b/spec/lib/gitlab/ci/runner_instructions_spec.rb new file mode 100644 index 00000000000..32ee2ceb040 --- /dev/null +++ b/spec/lib/gitlab/ci/runner_instructions_spec.rb @@ -0,0 +1,217 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::RunnerInstructions do + using RSpec::Parameterized::TableSyntax + + let(:params) { {} } + let(:user) { create(:user) } + + describe 'OS' do + Gitlab::Ci::RunnerInstructions::OS.each do |name, subject| + context name do + it 'has the required fields' do + expect(subject).to have_key(:human_readable_name) + expect(subject).to have_key(:download_locations) + expect(subject).to have_key(:install_script_template_path) + expect(subject).to have_key(:runner_executable) + end + + it 'has a valid script' do + expect(File.read(subject[:install_script_template_path]).length).not_to eq(0) + end + end + end + end + + describe 'OTHER_ENVIRONMENTS' do + Gitlab::Ci::RunnerInstructions::OTHER_ENVIRONMENTS.each do |name, subject| + context name do + it 'has the required fields' do + expect(subject).to have_key(:human_readable_name) + expect(subject).to have_key(:installation_instructions_url) + end + end + end + end + + describe '#install_script' do + subject { described_class.new(current_user: user, **params) } + + context 'invalid params' do + where(:current_params, :expected_error_message) do + { os: nil, arch: nil } | 'Missing OS' + { os: 'linux', arch: nil } | 'Missing arch' + { os: nil, arch: 'amd64' } | 'Missing OS' + { os: 'non_existing_os', arch: 'amd64' } | 'Invalid OS' + { os: 'linux', arch: 'non_existing_arch' } | 'Architecture not found for OS' + { os: 'windows', arch: 'non_existing_arch' } | 'Architecture not found for OS' + end + + with_them do + let(:params) { current_params } + + it 'raises argument error' do + result = subject.install_script + + expect(result).to be_nil + expect(subject.errors).to include(expected_error_message) + end + end + end + + context 'with valid params' do + where(:os, :arch) do + 'linux' | 'amd64' + 'linux' | '386' + 'linux' | 'arm' + 'linux' | 'arm64' + 'windows' | 'amd64' + 'windows' | '386' + 'osx' | 'amd64' + end + + with_them do + let(:params) { { os: os, arch: arch } } + + it 'returns string containing correct params' do + result = subject.install_script + + expect(result).to be_a(String) + + if os == 'osx' + expect(result).to include("darwin-#{arch}") + else + expect(result).to include("#{os}-#{arch}") + end + end + end + end + end + + describe '#register_command' do + let(:params) { { os: 'linux', arch: 'foo' } } + + where(:commands) do + Gitlab::Ci::RunnerInstructions::OS.map do |name, values| + { name => values[:runner_executable] } + end + end + + context 'group' do + let(:group) { create(:group) } + + subject { described_class.new(current_user: user, group: group, **params) } + + context 'user is owner' do + before do + group.add_owner(user) + end + + with_them do + let(:params) { { os: commands.each_key.first, arch: 'foo' } } + + it 'have correct configurations' do + result = subject.register_command + + expect(result).to include("#{commands[commands.each_key.first]} register") + expect(result).to include("--registration-token #{group.runners_token}") + expect(result).to include("--url #{Gitlab::Routing.url_helpers.root_url(only_path: false)}") + end + end + end + + context 'user is not owner' do + where(:user_permission) do + [:maintainer, :developer, :reporter, :guest] + end + + with_them do + before do + create(:group_member, user_permission, group: group, user: user) + end + + it 'raises error' do + result = subject.register_command + + expect(result).to be_nil + expect(subject.errors).to include("Gitlab::Access::AccessDeniedError") + end + end + end + end + + context 'project' do + let(:project) { create(:project) } + + subject { described_class.new(current_user: user, project: project, **params) } + + context 'user is maintainer' do + before do + project.add_maintainer(user) + end + + with_them do + let(:params) { { os: commands.each_key.first, arch: 'foo' } } + + it 'have correct configurations' do + result = subject.register_command + + expect(result).to include("#{commands[commands.each_key.first]} register") + expect(result).to include("--registration-token #{project.runners_token}") + expect(result).to include("--url #{Gitlab::Routing.url_helpers.root_url(only_path: false)}") + end + end + end + + context 'user is not maintainer' do + where(:user_permission) do + [:developer, :reporter, :guest] + end + + with_them do + before do + create(:project_member, user_permission, project: project, user: user) + end + + it 'raises error' do + result = subject.register_command + + expect(result).to be_nil + expect(subject.errors).to include("Gitlab::Access::AccessDeniedError") + end + end + end + end + + context 'instance' do + subject { described_class.new(current_user: user, **params) } + + context 'user is admin' do + let(:user) { create(:user, :admin) } + + with_them do + let(:params) { { os: commands.each_key.first, arch: 'foo' } } + + it 'have correct configurations' do + result = subject.register_command + + expect(result).to include("#{commands[commands.each_key.first]} register") + expect(result).to include("--registration-token #{Gitlab::CurrentSettings.runners_registration_token}") + expect(result).to include("--url #{Gitlab::Routing.url_helpers.root_url(only_path: false)}") + end + end + end + + context 'user is not admin' do + it 'raises error' do + result = subject.register_command + + expect(result).to be_nil + expect(subject.errors).to include("Gitlab::Access::AccessDeniedError") + end + end + end + end +end diff --git a/spec/lib/gitlab/ci/status/composite_spec.rb b/spec/lib/gitlab/ci/status/composite_spec.rb index 47bbc4169b6..e1dcd05373f 100644 --- a/spec/lib/gitlab/ci/status/composite_spec.rb +++ b/spec/lib/gitlab/ci/status/composite_spec.rb @@ -16,48 +16,61 @@ RSpec.describe Gitlab::Ci::Status::Composite do end describe '#status' do - shared_examples 'compares composite with SQL status' do - it 'returns exactly the same result' do - builds = Ci::Build.where(id: all_statuses) + using RSpec::Parameterized::TableSyntax - expect(composite_status.status).to eq(builds.legacy_status) - expect(composite_status.warnings?).to eq(builds.failed_but_allowed.any?) + shared_examples 'compares status and warnings' do + let(:composite_status) do + described_class.new(all_statuses) + end + + it 'returns status and warnings?' do + expect(composite_status.status).to eq(result) + expect(composite_status.warnings?).to eq(has_warnings) end end - shared_examples 'validate all combinations' do |perms| - Ci::HasStatus::STATUSES_ENUM.keys.combination(perms).each do |statuses| - context "with #{statuses.join(",")}" do - it_behaves_like 'compares composite with SQL status' do - let(:all_statuses) do - statuses.map { |status| @statuses[status] } - end - - let(:composite_status) do - described_class.new(all_statuses) - end - end - - Ci::HasStatus::STATUSES_ENUM.each do |allow_failure_status, _| - context "and allow_failure #{allow_failure_status}" do - it_behaves_like 'compares composite with SQL status' do - let(:all_statuses) do - statuses.map { |status| @statuses[status] } + - [@statuses_with_allow_failure[allow_failure_status]] - end - - let(:composite_status) do - described_class.new(all_statuses) - end - end - end - end + context 'allow_failure: false' do + where(:build_statuses, :result, :has_warnings) do + %i(skipped) | 'skipped' | false + %i(skipped success) | 'success' | false + %i(created) | 'created' | false + %i(preparing) | 'preparing' | false + %i(canceled success skipped) | 'canceled' | false + %i(pending created skipped) | 'pending' | false + %i(pending created skipped success) | 'running' | false + %i(running created skipped success) | 'running' | false + %i(success waiting_for_resource) | 'waiting_for_resource' | false + %i(success manual) | 'manual' | false + %i(success scheduled) | 'scheduled' | false + %i(created preparing) | 'preparing' | false + %i(created success pending) | 'running' | false + %i(skipped success failed) | 'failed' | false + end + + with_them do + let(:all_statuses) do + build_statuses.map { |status| @statuses[status] } end + + it_behaves_like 'compares status and warnings' end end - it_behaves_like 'validate all combinations', 0 - it_behaves_like 'validate all combinations', 1 - it_behaves_like 'validate all combinations', 2 + context 'allow_failure: true' do + where(:build_statuses, :result, :has_warnings) do + %i(manual) | 'skipped' | false + %i(skipped failed) | 'success' | true + %i(created failed) | 'created' | true + %i(preparing manual) | 'preparing' | 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 diff --git a/spec/lib/gitlab/ci/trace/stream_spec.rb b/spec/lib/gitlab/ci/trace/stream_spec.rb index 568c10bbac2..e28469c9404 100644 --- a/spec/lib/gitlab/ci/trace/stream_spec.rb +++ b/spec/lib/gitlab/ci/trace/stream_spec.rb @@ -101,7 +101,7 @@ RSpec.describe Gitlab::Ci::Trace::Stream, :clean_gitlab_redis_cache do describe '#append' do shared_examples_for 'appends' do - it "truncates and append content" do + it "truncates and appends content" do stream.append(+"89", 4) stream.seek(0) diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb index 5c6d748d66c..1c81cc83cd1 100644 --- a/spec/lib/gitlab/ci/yaml_processor_spec.rb +++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb @@ -443,15 +443,15 @@ module Gitlab context 'when a warning is raised in a given entry' do let(:config) do <<-EOYML - rspec: - script: rspec - rules: - - if: '$VAR == "value"' + rspec: + script: echo + rules: + - when: always EOYML end it 'is propagated all the way up to the processor' do - expect(subject.warnings).to contain_exactly('jobs:rspec uses `rules` without defining `workflow:rules`') + expect(subject.warnings).to contain_exactly(/jobs:rspec may allow multiple pipelines to run/) end end @@ -461,7 +461,7 @@ module Gitlab rspec: script: rspec rules: - - if: '$VAR == "value"' + - when: always invalid: script: echo artifacts: @@ -473,7 +473,7 @@ module Gitlab expect { subject }.to raise_error do |error| expect(error).to be_a(described_class::ValidationError) expect(error.message).to eq('jobs:invalid:artifacts config should be a hash') - expect(error.warnings).to contain_exactly('jobs:rspec uses `rules` without defining `workflow:rules`') + expect(error.warnings).to contain_exactly(/jobs:rspec may allow multiple pipelines to run/) end end end @@ -485,7 +485,7 @@ module Gitlab rspec: script: rspec rules: - - if: '$VAR == "value"' + - when: always EOYML end @@ -516,7 +516,7 @@ module Gitlab stage: custom_stage script: rspec rules: - - if: '$VAR == "value"' + - when: always EOYML end @@ -530,7 +530,7 @@ module Gitlab stage: build script: echo rules: - - if: '$VAR == "value"' + - when: always test: stage: test script: echo @@ -549,7 +549,7 @@ module Gitlab script: echo needs: [test] rules: - - if: '$VAR == "value"' + - when: always test: stage: test script: echo @@ -571,7 +571,7 @@ module Gitlab rspec: script: rspec rules: - - if: '$VAR == "value"' + - when: always EOYML end @@ -942,6 +942,7 @@ module Gitlab let(:variables) do { 'VAR1' => 'value1', 'VAR2' => 'value2' } end + let(:config) do { variables: variables, @@ -962,9 +963,11 @@ module Gitlab let(:global_variables) do { 'VAR1' => 'global1', 'VAR3' => 'global3', 'VAR4' => 'global4' } end + let(:job_variables) do { 'VAR1' => 'value1', 'VAR2' => 'value2' } end + let(:config) do { before_script: ['pwd'], @@ -1269,27 +1272,104 @@ module Gitlab end describe 'Parallel' do + let(:config) do + YAML.dump(rspec: { script: 'rspec', + parallel: parallel, + variables: { 'VAR1' => 1 } }) + end + + let(:config_processor) { Gitlab::Ci::YamlProcessor.new(config) } + let(:builds) { config_processor.stage_builds_attributes('test') } + context 'when job is parallelized' do let(:parallel) { 5 } - let(:config) do - YAML.dump(rspec: { script: 'rspec', - parallel: parallel }) - end - it 'returns parallelized jobs' do - config_processor = Gitlab::Ci::YamlProcessor.new(config) - builds = config_processor.stage_builds_attributes('test') build_options = builds.map { |build| build[:options] } expect(builds.size).to eq(5) - expect(build_options).to all(include(:instance, parallel: parallel)) + expect(build_options).to all(include(:instance, parallel: { number: parallel, total: parallel })) end it 'does not have the original job' do - config_processor = Gitlab::Ci::YamlProcessor.new(config) - builds = config_processor.stage_builds_attributes('test') + expect(builds).not_to include(:rspec) + end + end + + context 'with build matrix' do + let(:parallel) do + { + matrix: [ + { 'PROVIDER' => 'aws', 'STACK' => %w[monitoring app1 app2] }, + { 'PROVIDER' => 'ovh', 'STACK' => %w[monitoring backup app] }, + { 'PROVIDER' => 'gcp', 'STACK' => %w[data processing] } + ] + } + end + + it 'returns the number of parallelized jobs' do + expect(builds.size).to eq(8) + end + + it 'returns the parallel config' do + build_options = builds.map { |build| build[:options] } + parallel_config = { + matrix: parallel[:matrix].map { |var| var.transform_values { |v| Array(v).flatten }}, + total: build_options.size + } + + expect(build_options).to all(include(:instance, parallel: parallel_config)) + end + it 'sets matrix variables' do + build_variables = builds.map { |build| build[:yaml_variables] } + expected_variables = [ + [ + { key: 'VAR1', value: '1' }, + { key: 'PROVIDER', value: 'aws' }, + { key: 'STACK', value: 'monitoring' } + ], + [ + { key: 'VAR1', value: '1' }, + { key: 'PROVIDER', value: 'aws' }, + { key: 'STACK', value: 'app1' } + ], + [ + { key: 'VAR1', value: '1' }, + { key: 'PROVIDER', value: 'aws' }, + { key: 'STACK', value: 'app2' } + ], + [ + { key: 'VAR1', value: '1' }, + { key: 'PROVIDER', value: 'ovh' }, + { key: 'STACK', value: 'monitoring' } + ], + [ + { key: 'VAR1', value: '1' }, + { key: 'PROVIDER', value: 'ovh' }, + { key: 'STACK', value: 'backup' } + ], + [ + { key: 'VAR1', value: '1' }, + { key: 'PROVIDER', value: 'ovh' }, + { key: 'STACK', value: 'app' } + ], + [ + { key: 'VAR1', value: '1' }, + { key: 'PROVIDER', value: 'gcp' }, + { key: 'STACK', value: 'data' } + ], + [ + { key: 'VAR1', value: '1' }, + { key: 'PROVIDER', value: 'gcp' }, + { key: 'STACK', value: 'processing' } + ] + ].map { |vars| vars.map { |var| a_hash_including(var) } } + + expect(build_variables).to match(expected_variables) + end + + it 'does not have the original job' do expect(builds).not_to include(:rspec) end end @@ -1482,6 +1562,21 @@ module Gitlab }) end + it "returns artifacts with expire_in never keyword" do + config = YAML.dump({ + rspec: { + script: "rspec", + artifacts: { paths: ["releases/"], expire_in: "never" } + } + }) + + config_processor = Gitlab::Ci::YamlProcessor.new(config) + builds = config_processor.stage_builds_attributes("test") + + expect(builds.size).to eq(1) + expect(builds.first[:options][:artifacts][:expire_in]).to eq('never') + end + %w[on_success on_failure always].each do |when_state| it "returns artifacts for when #{when_state} defined" do config = YAML.dump({ @@ -1564,26 +1659,9 @@ module Gitlab } end - context 'with feature flag active' do - before do - stub_feature_flags(ci_release_generation: true) - end - - it "returns release info" do - expect(processor.stage_builds_attributes('release').first[:options]) - .to eq(config[:release].except(:stage, :only)) - end - end - - context 'with feature flag inactive' do - before do - stub_feature_flags(ci_release_generation: false) - end - - it 'raises error' do - expect { processor }.to raise_error( - 'jobs:release config release features are not enabled: release') - end + it "returns release info" do + expect(processor.stage_builds_attributes('release').first[:options]) + .to eq(config[:release].except(:stage, :only)) end end @@ -1998,6 +2076,7 @@ module Gitlab { job: "build2" } ] end + let(:dependencies) { %w(build3) } it { expect { subject }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, 'jobs:test1 dependencies the build3 should be part of needs') } @@ -2407,6 +2486,14 @@ module Gitlab end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs config should contain at least one visible job") end + it "returns errors if the job script is not defined" do + config = YAML.dump({ rspec: { before_script: "test" } }) + + expect do + Gitlab::Ci::YamlProcessor.new(config) + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec script can't be blank") + end + it "returns errors if there are no visible jobs defined" do config = YAML.dump({ before_script: ["bundle update"], '.hidden'.to_sym => { script: 'ls' } }) expect do @@ -2619,6 +2706,14 @@ module Gitlab .to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, 'rspec: unknown keys in `extends` (something)') end + + it 'returns errors if parallel is invalid' do + config = YAML.dump({ rspec: { parallel: 'test', script: 'test' } }) + + expect { Gitlab::Ci::YamlProcessor.new(config) } + .to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, + 'jobs:rspec:parallel should be an integer or a hash') + end end describe "#validation_message" do |