diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-05-20 17:34:42 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-05-20 17:34:42 +0300 |
commit | 9f46488805e86b1bc341ea1620b866016c2ce5ed (patch) | |
tree | f9748c7e287041e37d6da49e0a29c9511dc34768 /spec/lib/gitlab/ci | |
parent | dfc92d081ea0332d69c8aca2f0e745cb48ae5e6d (diff) |
Add latest changes from gitlab-org/gitlab@13-0-stable-ee
Diffstat (limited to 'spec/lib/gitlab/ci')
22 files changed, 1702 insertions, 179 deletions
diff --git a/spec/lib/gitlab/ci/config/entry/artifacts_spec.rb b/spec/lib/gitlab/ci/config/entry/artifacts_spec.rb index 513a9b8f2b4..8cfd07df777 100644 --- a/spec/lib/gitlab/ci/config/entry/artifacts_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/artifacts_spec.rb @@ -123,25 +123,53 @@ describe Gitlab::Ci::Config::Entry::Artifacts do end end end + end - context 'when feature flag :ci_expose_arbitrary_artifacts_in_mr is disabled' do + describe 'excluded artifacts' do + context 'when configuration is valid and the feature is enabled' do before do - stub_feature_flags(ci_expose_arbitrary_artifacts_in_mr: false) + stub_feature_flags(ci_artifacts_exclude: true) end - context 'when syntax is correct' do - let(:config) { { expose_as: 'Test results', paths: ['test.txt'] } } + context 'when configuration is valid' do + let(:config) { { untracked: true, exclude: ['some/directory/'] } } - it 'is valid' do - expect(entry.errors).to be_empty + it 'correctly parses the configuration' do + expect(entry).to be_valid + expect(entry.value).to eq config end end - context 'when syntax for :expose_as is incorrect' do - let(:config) { { paths: %w[results.txt], expose_as: '' } } + context 'when configuration is not valid' do + let(:config) { { untracked: true, exclude: 1234 } } + + it 'returns an error' do + expect(entry).not_to be_valid + expect(entry.errors) + .to include 'artifacts exclude should be an array of strings' + end + end + end + + context 'when artifacts/exclude feature is disabled' do + before do + stub_feature_flags(ci_artifacts_exclude: false) + end + + context 'when configuration has been provided' do + let(:config) { { untracked: true, exclude: ['some/directory/'] } } + + it 'returns an error' do + expect(entry).not_to be_valid + expect(entry.errors).to include 'artifacts exclude feature is disabled' + end + end + + context 'when configuration is not present' do + let(:config) { { untracked: true } } - it 'is valid' do - expect(entry.errors).to be_empty + it 'is a valid configuration' do + expect(entry).to be_valid end end end diff --git a/spec/lib/gitlab/ci/config/entry/reports_spec.rb b/spec/lib/gitlab/ci/config/entry/reports_spec.rb index 9bba3eb2b77..8c6c91d919e 100644 --- a/spec/lib/gitlab/ci/config/entry/reports_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/reports_spec.rb @@ -47,6 +47,8 @@ describe Gitlab::Ci::Config::Entry::Reports do :dotenv | 'build.dotenv' :cobertura | 'cobertura-coverage.xml' :terraform | 'tfplan.json' + :accessibility | 'gl-accessibility.json' + :cluster_applications | 'gl-cluster-applications.json' end with_them do diff --git a/spec/lib/gitlab/ci/config/entry/trigger_spec.rb b/spec/lib/gitlab/ci/config/entry/trigger_spec.rb index 752c3f59a95..dfd9807583c 100644 --- a/spec/lib/gitlab/ci/config/entry/trigger_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/trigger_spec.rb @@ -114,19 +114,6 @@ describe Gitlab::Ci::Config::Entry::Trigger do .to match /config contains unknown keys: branch/ end end - - context 'when feature flag is off' do - before do - stub_feature_flags(ci_parent_child_pipeline: false) - end - - let(:config) { { include: 'path/to/config.yml' } } - - it 'is returns an error if include is used' do - expect(subject.errors.first) - .to match /config must specify project/ - end - end end context 'when config contains unknown keys' do diff --git a/spec/lib/gitlab/ci/cron_parser_spec.rb b/spec/lib/gitlab/ci/cron_parser_spec.rb index 385df72fa41..8f9f3d7fa37 100644 --- a/spec/lib/gitlab/ci/cron_parser_spec.rb +++ b/spec/lib/gitlab/ci/cron_parser_spec.rb @@ -7,198 +7,240 @@ describe Gitlab::Ci::CronParser do it { is_expected.to be > Time.now } end - describe '#next_time_from' do - subject { described_class.new(cron, cron_timezone).next_time_from(Time.now) } + shared_examples_for "returns time in the past" do + it { is_expected.to be < Time.now } + end - context 'when cron and cron_timezone are valid' do - context 'when specific time' do - let(:cron) { '3 4 5 6 *' } - let(:cron_timezone) { 'UTC' } + shared_examples_for 'when cron and cron_timezone are valid' do |returns_time_for_epoch| + context 'when specific time' do + let(:cron) { '3 4 5 6 *' } + let(:cron_timezone) { 'UTC' } - it_behaves_like "returns time in the future" + it_behaves_like returns_time_for_epoch - it 'returns exact time' do - expect(subject.min).to eq(3) - expect(subject.hour).to eq(4) - expect(subject.day).to eq(5) - expect(subject.month).to eq(6) - end + it 'returns exact time' do + expect(subject.min).to eq(3) + expect(subject.hour).to eq(4) + expect(subject.day).to eq(5) + expect(subject.month).to eq(6) end + end - context 'when specific day of week' do - let(:cron) { '* * * * 0' } - let(:cron_timezone) { 'UTC' } + context 'when specific day of week' do + let(:cron) { '* * * * 0' } + let(:cron_timezone) { 'UTC' } - it_behaves_like "returns time in the future" + it_behaves_like returns_time_for_epoch - it 'returns exact day of week' do - expect(subject.wday).to eq(0) - end + it 'returns exact day of week' do + expect(subject.wday).to eq(0) end + end - context 'when slash used' do - let(:cron) { '*/10 */6 */10 */10 *' } - let(:cron_timezone) { 'UTC' } + context 'when slash used' do + let(:cron) { '*/10 */6 */10 */10 *' } + let(:cron_timezone) { 'UTC' } - it_behaves_like "returns time in the future" + it_behaves_like returns_time_for_epoch - it 'returns specific time' do - expect(subject.min).to be_in([0, 10, 20, 30, 40, 50]) - expect(subject.hour).to be_in([0, 6, 12, 18]) - expect(subject.day).to be_in([1, 11, 21, 31]) - expect(subject.month).to be_in([1, 11]) - end + it 'returns specific time' do + expect(subject.min).to be_in([0, 10, 20, 30, 40, 50]) + expect(subject.hour).to be_in([0, 6, 12, 18]) + expect(subject.day).to be_in([1, 11, 21, 31]) + expect(subject.month).to be_in([1, 11]) end + end - context 'when range used' do - let(:cron) { '0,20,40 * 1-5 * *' } - let(:cron_timezone) { 'UTC' } + context 'when range used' do + let(:cron) { '0,20,40 * 1-5 * *' } + let(:cron_timezone) { 'UTC' } - it_behaves_like "returns time in the future" + it_behaves_like returns_time_for_epoch - it 'returns specific time' do - expect(subject.min).to be_in([0, 20, 40]) - expect(subject.day).to be_in((1..5).to_a) - end + it 'returns specific time' do + expect(subject.min).to be_in([0, 20, 40]) + expect(subject.day).to be_in((1..5).to_a) end + end - context 'when cron_timezone is TZInfo format' do - before do - allow(Time).to receive(:zone) - .and_return(ActiveSupport::TimeZone['UTC']) - end + context 'when cron_timezone is TZInfo format' do + before do + allow(Time).to receive(:zone) + .and_return(ActiveSupport::TimeZone['UTC']) + end - let(:hour_in_utc) do - ActiveSupport::TimeZone[cron_timezone] - .now.change(hour: 0).in_time_zone('UTC').hour - end + let(:hour_in_utc) do + ActiveSupport::TimeZone[cron_timezone] + .now.change(hour: 0).in_time_zone('UTC').hour + end - context 'when cron_timezone is US/Pacific' do - let(:cron) { '* 0 * * *' } - let(:cron_timezone) { 'US/Pacific' } + context 'when cron_timezone is US/Pacific' do + let(:cron) { '* 0 * * *' } + let(:cron_timezone) { 'US/Pacific' } - it_behaves_like "returns time in the future" + it_behaves_like returns_time_for_epoch - context 'when PST (Pacific Standard Time)' do - it 'converts time in server time zone' do - Timecop.freeze(Time.utc(2017, 1, 1)) do - expect(subject.hour).to eq(hour_in_utc) - end + context 'when PST (Pacific Standard Time)' do + it 'converts time in server time zone' do + Timecop.freeze(Time.utc(2017, 1, 1)) do + expect(subject.hour).to eq(hour_in_utc) end end + end - context 'when PDT (Pacific Daylight Time)' do - it 'converts time in server time zone' do - Timecop.freeze(Time.utc(2017, 6, 1)) do - expect(subject.hour).to eq(hour_in_utc) - end + context 'when PDT (Pacific Daylight Time)' do + it 'converts time in server time zone' do + Timecop.freeze(Time.utc(2017, 6, 1)) do + expect(subject.hour).to eq(hour_in_utc) end end end end + end - context 'when cron_timezone is ActiveSupport::TimeZone format' do - before do - allow(Time).to receive(:zone) - .and_return(ActiveSupport::TimeZone['UTC']) - end + context 'when cron_timezone is ActiveSupport::TimeZone format' do + before do + allow(Time).to receive(:zone) + .and_return(ActiveSupport::TimeZone['UTC']) + end - let(:hour_in_utc) do - ActiveSupport::TimeZone[cron_timezone] - .now.change(hour: 0).in_time_zone('UTC').hour - end + let(:hour_in_utc) do + ActiveSupport::TimeZone[cron_timezone] + .now.change(hour: 0).in_time_zone('UTC').hour + end - context 'when cron_timezone is Berlin' do - let(:cron) { '* 0 * * *' } - let(:cron_timezone) { 'Berlin' } + context 'when cron_timezone is Berlin' do + let(:cron) { '* 0 * * *' } + let(:cron_timezone) { 'Berlin' } - it_behaves_like "returns time in the future" + it_behaves_like returns_time_for_epoch - context 'when CET (Central European Time)' do - it 'converts time in server time zone' do - Timecop.freeze(Time.utc(2017, 1, 1)) do - expect(subject.hour).to eq(hour_in_utc) - end + context 'when CET (Central European Time)' do + it 'converts time in server time zone' do + Timecop.freeze(Time.utc(2017, 1, 1)) do + expect(subject.hour).to eq(hour_in_utc) end end + end - context 'when CEST (Central European Summer Time)' do - it 'converts time in server time zone' do - Timecop.freeze(Time.utc(2017, 6, 1)) do - expect(subject.hour).to eq(hour_in_utc) - end + context 'when CEST (Central European Summer Time)' do + it 'converts time in server time zone' do + Timecop.freeze(Time.utc(2017, 6, 1)) do + expect(subject.hour).to eq(hour_in_utc) end end end + end + end + end - context 'when cron_timezone is Eastern Time (US & Canada)' do - let(:cron) { '* 0 * * *' } - let(:cron_timezone) { 'Eastern Time (US & Canada)' } + shared_examples_for 'when cron_timezone is Eastern Time (US & Canada)' do |returns_time_for_epoch, year| + let(:cron) { '* 0 * * *' } + let(:cron_timezone) { 'Eastern Time (US & Canada)' } - it_behaves_like "returns time in the future" + before do + allow(Time).to receive(:zone) + .and_return(ActiveSupport::TimeZone['UTC']) + end - context 'when EST (Eastern Standard Time)' do - it 'converts time in server time zone' do - Timecop.freeze(Time.utc(2017, 1, 1)) do - expect(subject.hour).to eq(hour_in_utc) - end - end - end + let(:hour_in_utc) do + ActiveSupport::TimeZone[cron_timezone] + .now.change(hour: 0).in_time_zone('UTC').hour + end - context 'when EDT (Eastern Daylight Time)' do - it 'converts time in server time zone' do - Timecop.freeze(Time.utc(2017, 6, 1)) do - expect(subject.hour).to eq(hour_in_utc) - end - end - end + it_behaves_like returns_time_for_epoch - context 'when time crosses a Daylight Savings boundary' do - let(:cron) { '* 0 1 12 *'} - - # Note this previously only failed if the time zone is set - # to a zone that observes Daylight Savings - # (e.g. America/Chicago) at the start of the test. Stubbing - # TZ doesn't appear to be enough. - it 'generates day without TZInfo::AmbiguousTime error' do - Timecop.freeze(Time.utc(2020, 1, 1)) do - expect(subject.year).to eq(2020) - expect(subject.month).to eq(12) - expect(subject.day).to eq(1) - end - end - end + context 'when EST (Eastern Standard Time)' do + it 'converts time in server time zone' do + Timecop.freeze(Time.utc(2017, 1, 1)) do + expect(subject.hour).to eq(hour_in_utc) end end end - context 'when cron and cron_timezone are invalid' do - let(:cron) { 'invalid_cron' } - let(:cron_timezone) { 'invalid_cron_timezone' } + context 'when EDT (Eastern Daylight Time)' do + it 'converts time in server time zone' do + Timecop.freeze(Time.utc(2017, 6, 1)) do + expect(subject.hour).to eq(hour_in_utc) + end + end + end - it { is_expected.to be_nil } + context 'when time crosses a Daylight Savings boundary' do + let(:cron) { '* 0 1 12 *'} + + # Note this previously only failed if the time zone is set + # to a zone that observes Daylight Savings + # (e.g. America/Chicago) at the start of the test. Stubbing + # TZ doesn't appear to be enough. + it 'generates day without TZInfo::AmbiguousTime error' do + Timecop.freeze(Time.utc(2020, 1, 1)) do + expect(subject.year).to eq(year) + expect(subject.month).to eq(12) + expect(subject.day).to eq(1) + end + end end + end - context 'when cron syntax is quoted' do - let(:cron) { "'0 * * * *'" } - let(:cron_timezone) { 'UTC' } + shared_examples_for 'when cron and cron_timezone are invalid' do + let(:cron) { 'invalid_cron' } + let(:cron_timezone) { 'invalid_cron_timezone' } - it { expect(subject).to be_nil } - end + it { is_expected.to be_nil } + end - context 'when cron syntax is rufus-scheduler syntax' do - let(:cron) { 'every 3h' } - let(:cron_timezone) { 'UTC' } + shared_examples_for 'when cron syntax is quoted' do + let(:cron) { "'0 * * * *'" } + let(:cron_timezone) { 'UTC' } - it { expect(subject).to be_nil } - end + it { expect(subject).to be_nil } + end - context 'when cron is scheduled to a non existent day' do - let(:cron) { '0 12 31 2 *' } - let(:cron_timezone) { 'UTC' } + shared_examples_for 'when cron syntax is rufus-scheduler syntax' do + let(:cron) { 'every 3h' } + let(:cron_timezone) { 'UTC' } - it { expect(subject).to be_nil } - end + it { expect(subject).to be_nil } + end + + shared_examples_for 'when cron is scheduled to a non existent day' do + let(:cron) { '0 12 31 2 *' } + let(:cron_timezone) { 'UTC' } + + it { expect(subject).to be_nil } + end + + describe '#next_time_from' do + subject { described_class.new(cron, cron_timezone).next_time_from(Time.now) } + + it_behaves_like 'when cron and cron_timezone are valid', 'returns time in the future' + + it_behaves_like 'when cron_timezone is Eastern Time (US & Canada)', 'returns time in the future', 2020 + + it_behaves_like 'when cron and cron_timezone are invalid' + + it_behaves_like 'when cron syntax is quoted' + + it_behaves_like 'when cron syntax is rufus-scheduler syntax' + + it_behaves_like 'when cron is scheduled to a non existent day' + end + + describe '#previous_time_from' do + subject { described_class.new(cron, cron_timezone).previous_time_from(Time.now) } + + it_behaves_like 'when cron and cron_timezone are valid', 'returns time in the past' + + it_behaves_like 'when cron_timezone is Eastern Time (US & Canada)', 'returns time in the past', 2019 + + it_behaves_like 'when cron and cron_timezone are invalid' + + it_behaves_like 'when cron syntax is quoted' + + it_behaves_like 'when cron syntax is rufus-scheduler syntax' + + it_behaves_like 'when cron is scheduled to a non existent day' end describe '#cron_valid?' do diff --git a/spec/lib/gitlab/ci/parsers/accessibility/pa11y_spec.rb b/spec/lib/gitlab/ci/parsers/accessibility/pa11y_spec.rb new file mode 100644 index 00000000000..4d87e3b201a --- /dev/null +++ b/spec/lib/gitlab/ci/parsers/accessibility/pa11y_spec.rb @@ -0,0 +1,118 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' + +describe Gitlab::Ci::Parsers::Accessibility::Pa11y do + describe '#parse!' do + subject { described_class.new.parse!(pa11y, accessibility_report) } + + let(:accessibility_report) { Gitlab::Ci::Reports::AccessibilityReports.new } + + context "when data is pa11y style JSON" do + context "when there are no URLs provided" do + let(:pa11y) do + { + "total": 1, + "passes": 0, + "errors": 0, + "results": { + "": [ + { + "message": "Protocol error (Page.navigate): Cannot navigate to invalid URL" + } + ] + } + }.to_json + end + + it "returns an accessibility report" do + expect { subject }.not_to raise_error + + expect(accessibility_report.errors_count).to eq(0) + expect(accessibility_report.passes_count).to eq(0) + expect(accessibility_report.scans_count).to eq(0) + expect(accessibility_report.urls).to be_empty + expect(accessibility_report.error_message).to eq("Empty URL detected in gl-accessibility.json") + end + end + + context "when there are no errors" do + let(:pa11y) do + { + "total": 1, + "passes": 1, + "errors": 0, + "results": { + "http://pa11y.org/": [] + } + }.to_json + end + + it "returns an accessibility report" do + expect { subject }.not_to raise_error + + expect(accessibility_report.urls['http://pa11y.org/']).to be_empty + expect(accessibility_report.errors_count).to eq(0) + expect(accessibility_report.passes_count).to eq(1) + expect(accessibility_report.scans_count).to eq(1) + end + end + + context "when there are errors" do + let(:pa11y) do + { + "total": 1, + "passes": 0, + "errors": 1, + "results": { + "https://about.gitlab.com/": [ + { + "code": "WCAG2AA.Principle4.Guideline4_1.4_1_2.H91.A.NoContent", + "type": "error", + "typeCode": 1, + "message": "Anchor element found with a valid href attribute, but no link content has been supplied.", + "context": "<a href=\"/\" class=\"navbar-brand animated\"><svg height=\"36\" viewBox=\"0 0 1...</a>", + "selector": "#main-nav > div:nth-child(1) > a", + "runner": "htmlcs", + "runnerExtras": {} + } + ] + } + }.to_json + end + + it "returns an accessibility report" do + expect { subject }.not_to raise_error + + expect(accessibility_report.errors_count).to eq(1) + expect(accessibility_report.passes_count).to eq(0) + expect(accessibility_report.scans_count).to eq(1) + expect(accessibility_report.urls['https://about.gitlab.com/']).to be_present + expect(accessibility_report.urls['https://about.gitlab.com/'].first[:code]).to be_present + end + end + end + + context "when data is not a valid JSON string" do + let(:pa11y) do + { + "total": 1, + "passes": 1, + "errors": 0, + "results": { + "http://pa11y.org/": [] + } + } + end + + it "sets error_message" do + expect { subject }.not_to raise_error + + expect(accessibility_report.error_message).to include('Pa11y parsing failed') + expect(accessibility_report.errors_count).to eq(0) + expect(accessibility_report.passes_count).to eq(0) + expect(accessibility_report.scans_count).to eq(0) + end + end + end +end diff --git a/spec/lib/gitlab/ci/parsers/terraform/tfplan_spec.rb b/spec/lib/gitlab/ci/parsers/terraform/tfplan_spec.rb new file mode 100644 index 00000000000..19cd75e586c --- /dev/null +++ b/spec/lib/gitlab/ci/parsers/terraform/tfplan_spec.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Ci::Parsers::Terraform::Tfplan do + describe '#parse!' do + let_it_be(:artifact) { create(:ci_job_artifact, :terraform) } + + let(:reports) { Gitlab::Ci::Reports::TerraformReports.new } + + context 'when data is tfplan.json' do + context 'when there is no data' do + it 'raises an error' do + plan = '{}' + + expect { subject.parse!(plan, reports, artifact: artifact) }.to raise_error( + described_class::TfplanParserError + ) + end + end + + context 'when there is data' do + it 'parses JSON and returns a report' do + plan = '{ "create": 0, "update": 1, "delete": 0 }' + + expect { subject.parse!(plan, reports, artifact: artifact) }.not_to raise_error + + expect(reports.plans).to match( + a_hash_including( + 'tfplan.json' => a_hash_including( + 'create' => 0, + 'update' => 1, + 'delete' => 0 + ) + ) + ) + end + end + end + + context 'when data is not tfplan.json' do + it 'raises an error' do + plan = { 'create' => 0, 'update' => 1, 'delete' => 0 }.to_s + + expect { subject.parse!(plan, reports, artifact: artifact) }.to raise_error( + described_class::TfplanParserError + ) + end + end + end +end diff --git a/spec/lib/gitlab/ci/parsers/test/junit_spec.rb b/spec/lib/gitlab/ci/parsers/test/junit_spec.rb index b4be5a41cd7..7b7ace02bba 100644 --- a/spec/lib/gitlab/ci/parsers/test/junit_spec.rb +++ b/spec/lib/gitlab/ci/parsers/test/junit_spec.rb @@ -215,8 +215,64 @@ describe Gitlab::Ci::Parsers::Test::Junit do context 'when data is not JUnit style XML' do let(:junit) { { testsuite: 'abc' }.to_json } - it 'raises an error' do - expect { subject }.to raise_error(described_class::JunitParserError) + it 'attaches an error to the TestSuite object' do + expect { subject }.not_to raise_error + expect(test_cases).to be_empty + end + end + + context 'when data is malformed JUnit XML' do + let(:junit) do + <<-EOF.strip_heredoc + <testsuite> + <testcase classname='Calculator' name='sumTest1' time='0.01'></testcase> + <testcase classname='Calculator' name='sumTest2' time='0.02'></testcase + </testsuite> + EOF + end + + it 'attaches an error to the TestSuite object' do + expect { subject }.not_to raise_error + expect(test_suite.suite_error).to eq("JUnit XML parsing failed: 4:1: FATAL: expected '>'") + end + + it 'returns 0 tests cases' do + subject + + expect(test_cases).to be_empty + expect(test_suite.total_count).to eq(0) + expect(test_suite.success_count).to eq(0) + expect(test_suite.error_count).to eq(0) + end + + it 'returns a failure status' do + subject + + expect(test_suite.total_status).to eq(Gitlab::Ci::Reports::TestCase::STATUS_ERROR) + end + end + + context 'when data is not XML' do + let(:junit) { double(:random_trash) } + + it 'attaches an error to the TestSuite object' do + expect { subject }.not_to raise_error + expect(test_suite.suite_error).to eq('JUnit data parsing failed: no implicit conversion of RSpec::Mocks::Double into String') + end + + it 'returns 0 tests cases' do + subject + + expect(test_cases).to be_empty + expect(test_suite.total_count).to eq(0) + expect(test_suite.success_count).to eq(0) + expect(test_suite.error_count).to eq(0) + end + + it 'returns a failure status' do + subject + + expect(test_suite.total_status).to eq(Gitlab::Ci::Reports::TestCase::STATUS_ERROR) end end diff --git a/spec/lib/gitlab/ci/parsers_spec.rb b/spec/lib/gitlab/ci/parsers_spec.rb index 9d6896b3cb4..0a266e7a206 100644 --- a/spec/lib/gitlab/ci/parsers_spec.rb +++ b/spec/lib/gitlab/ci/parsers_spec.rb @@ -22,6 +22,22 @@ describe Gitlab::Ci::Parsers do end end + context 'when file_type is accessibility' do + let(:file_type) { 'accessibility' } + + it 'fabricates the class' do + is_expected.to be_a(described_class::Accessibility::Pa11y) + end + end + + context 'when file_type is terraform' do + let(:file_type) { 'terraform' } + + it 'fabricates the class' do + is_expected.to be_a(described_class::Terraform::Tfplan) + end + end + context 'when file_type does not exist' do let(:file_type) { 'undefined' } diff --git a/spec/lib/gitlab/ci/pipeline/chain/sequence_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/sequence_spec.rb index 9033b71b19f..f82e49f9323 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/sequence_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/sequence_spec.rb @@ -5,11 +5,13 @@ require 'spec_helper' describe Gitlab::Ci::Pipeline::Chain::Sequence do let_it_be(:project) { create(:project) } let_it_be(:user) { create(:user) } + let(:pipeline) { build_stubbed(:ci_pipeline) } let(:command) { Gitlab::Ci::Pipeline::Chain::Command.new } let(:first_step) { spy('first step') } let(:second_step) { spy('second step') } let(:sequence) { [first_step, second_step] } + let(:histogram) { spy('prometheus metric') } subject do described_class.new(pipeline, command, sequence) @@ -52,5 +54,13 @@ describe Gitlab::Ci::Pipeline::Chain::Sequence do it 'returns a pipeline object' do expect(subject.build!).to eq pipeline end + + it 'adds sequence duration to duration histogram' do + allow(command).to receive(:duration_histogram).and_return(histogram) + + subject.build! + + expect(histogram).to have_received(:observe) + 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 new file mode 100644 index 00000000000..31a330f46b1 --- /dev/null +++ b/spec/lib/gitlab/ci/reports/accessibility_reports_comparer_spec.rb @@ -0,0 +1,270 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Ci::Reports::AccessibilityReportsComparer do + let(:comparer) { described_class.new(base_reports, head_reports) } + let(:base_reports) { Gitlab::Ci::Reports::AccessibilityReports.new } + let(:head_reports) { Gitlab::Ci::Reports::AccessibilityReports.new } + let(:url) { "https://gitlab.com" } + let(:single_error) do + [ + { + "code" => "WCAG2AA.Principle4.Guideline4_1.4_1_2.H91.A.NoContent", + "type" => "error", + "typeCode" => 1, + "message" => "Anchor element found with a valid href attribute, but no link content has been supplied.", + "context" => %{<a href="/" class="navbar-brand animated"><svg height="36" viewBox="0 0 1...</a>}, + "selector" => "#main-nav > div:nth-child(1) > a", + "runner" => "htmlcs", + "runnerExtras" => {} + } + ] + end + let(:different_error) do + [ + { + "code" => "WCAG2AA.Principle1.Guideline1_4.1_4_3.G18.Fail", + "type" => "error", + "typeCode" => 1, + "message" => "This element has insufficient contrast at this conformance level.", + "context" => %{<a href="/stages-devops-lifecycle/" class="main-nav-link">Product</a>}, + "selector" => "#main-nav > div:nth-child(2) > ul > li:nth-child(1) > a", + "runner" => "htmlcs", + "runnerExtras" => {} + } + ] + end + + describe '#status' do + subject { comparer.status } + + context 'when head report has an error' do + before do + head_reports.add_url(url, single_error) + end + + it 'returns status failed' do + expect(subject).to eq(described_class::STATUS_FAILED) + end + end + + context 'when head reports does not have errors' do + before do + head_reports.add_url(url, []) + end + + it 'returns status success' do + expect(subject).to eq(described_class::STATUS_SUCCESS) + end + end + end + + describe '#errors_count' do + subject { comparer.errors_count } + + context 'when head report has an error' do + before do + head_reports.add_url(url, single_error) + end + + it 'returns the number of new errors' do + expect(subject).to eq(1) + end + end + + context 'when head reports does not have an error' do + before do + head_reports.add_url(url, []) + end + + it 'returns the number new errors' do + expect(subject).to eq(0) + end + end + end + + describe '#resolved_count' do + subject { comparer.resolved_count } + + context 'when base reports has an error and head has a different error' do + before do + base_reports.add_url(url, single_error) + head_reports.add_url(url, different_error) + end + + it 'returns the resolved count' do + expect(subject).to eq(1) + end + end + + context 'when base reports has errors head has no errors' do + before do + base_reports.add_url(url, single_error) + head_reports.add_url(url, []) + end + + it 'returns the resolved count' do + expect(subject).to eq(1) + end + end + + context 'when base reports has errors and head has the same error' do + before do + base_reports.add_url(url, single_error) + head_reports.add_url(url, single_error) + end + + it 'returns zero' do + expect(subject).to eq(0) + end + end + + context 'when base reports does not have errors and head has errors' do + before do + head_reports.add_url(url, single_error) + end + + it 'returns the number of resolved errors' do + expect(subject).to eq(0) + end + end + end + + describe '#total_count' do + subject { comparer.total_count } + + context 'when base reports has an error' do + before do + base_reports.add_url(url, single_error) + end + + it 'returns the error count' do + expect(subject).to eq(1) + end + end + + context 'when head report has an error' do + before do + head_reports.add_url(url, single_error) + end + + it 'returns the error count' do + expect(subject).to eq(1) + end + end + + context 'when base report has errors and head report has errors' do + before do + base_reports.add_url(url, single_error) + head_reports.add_url(url, different_error) + end + + it 'returns the error count' do + expect(subject).to eq(2) + end + end + end + + describe '#existing_errors' do + subject { comparer.existing_errors } + + context 'when base report has errors and head has a different error' do + before do + base_reports.add_url(url, single_error) + head_reports.add_url(url, different_error) + end + + it 'returns the existing errors' do + expect(subject.size).to eq(1) + expect(subject.first["code"]).to eq("WCAG2AA.Principle4.Guideline4_1.4_1_2.H91.A.NoContent") + end + end + + context 'when base report does not have errors and head has errors' do + before do + base_reports.add_url(url, []) + head_reports.add_url(url, single_error) + end + + it 'returns an empty array' do + expect(subject).to be_empty + end + end + end + + describe '#new_errors' do + subject { comparer.new_errors } + + context 'when base reports has errors and head has more errors' do + before do + base_reports.add_url(url, single_error) + head_reports.add_url(url, single_error + different_error) + end + + it 'returns new errors between base and head reports' do + expect(subject.size).to eq(1) + expect(subject.first["code"]).to eq("WCAG2AA.Principle1.Guideline1_4.1_4_3.G18.Fail") + end + end + + context 'when base reports has an error and head has no errors' do + before do + base_reports.add_url(url, single_error) + head_reports.add_url(url, []) + end + + it 'returns an empty array' do + expect(subject).to be_empty + end + end + + context 'when base reports does not have errors and head has errors' do + before do + head_reports.add_url(url, single_error) + end + + it 'returns the new error' do + expect(subject.size).to eq(1) + expect(subject.first["code"]).to eq("WCAG2AA.Principle4.Guideline4_1.4_1_2.H91.A.NoContent") + end + end + end + + describe '#resolved_errors' do + subject { comparer.resolved_errors } + + context 'when base report has errors and head has more errors' do + before do + base_reports.add_url(url, single_error) + head_reports.add_url(url, single_error + different_error) + end + + it 'returns an empty array' do + expect(subject).to be_empty + end + end + + context 'when base reports has errors and head has a different error' do + before do + base_reports.add_url(url, single_error) + head_reports.add_url(url, different_error) + end + + it 'returns the resolved errors' do + expect(subject.size).to eq(1) + expect(subject.first["code"]).to eq("WCAG2AA.Principle4.Guideline4_1.4_1_2.H91.A.NoContent") + end + end + + context 'when base reports does not have errors and head has errors' do + before do + head_reports.add_url(url, single_error) + end + + it 'returns an empty array' do + expect(subject).to be_empty + end + end + end +end diff --git a/spec/lib/gitlab/ci/reports/accessibility_reports_spec.rb b/spec/lib/gitlab/ci/reports/accessibility_reports_spec.rb new file mode 100644 index 00000000000..0dc13b464b1 --- /dev/null +++ b/spec/lib/gitlab/ci/reports/accessibility_reports_spec.rb @@ -0,0 +1,232 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Ci::Reports::AccessibilityReports do + let(:accessibility_report) { described_class.new } + let(:url) { 'https://gitlab.com' } + let(:data) do + [ + { + "code": "WCAG2AA.Principle4.Guideline4_1.4_1_2.H91.A.NoContent", + "type": "error", + "typeCode": 1, + "message": "Anchor element found with a valid href attribute, but no link content has been supplied.", + "context": %{<a href="/customers/worldline"><svg viewBox="0 0 509 89" xmln...</a>}, + "selector": "html > body > div:nth-child(9) > div:nth-child(2) > a:nth-child(17)", + "runner": "htmlcs", + "runnerExtras": {} + }, + { + "code": "WCAG2AA.Principle4.Guideline4_1.4_1_2.H91.A.NoContent", + "type": "error", + "typeCode": 1, + "message": "Anchor element found with a valid href attribute, but no link content has been supplied.", + "context": %{<a href="/customers/equinix"><svg xmlns="http://www.w3.org/...</a>}, + "selector": "html > body > div:nth-child(9) > div:nth-child(2) > a:nth-child(18)", + "runner": "htmlcs", + "runnerExtras": {} + } + ] + end + + describe '#scans_count' do + subject { accessibility_report.scans_count } + + context 'when data has errors' do + let(:different_url) { 'https://about.gitlab.com' } + + before do + accessibility_report.add_url(url, data) + accessibility_report.add_url(different_url, data) + end + + it 'returns the scans_count' do + expect(subject).to eq(2) + end + end + + context 'when data has no errors' do + before do + accessibility_report.add_url(url, []) + end + + it 'returns the scans_count' do + expect(subject).to eq(1) + end + end + + context 'when data has no url' do + before do + accessibility_report.add_url("", []) + end + + it 'returns the scans_count' do + expect(subject).to eq(0) + end + end + end + + describe '#passes_count' do + subject { accessibility_report.passes_count } + + context 'when data has errors' do + before do + accessibility_report.add_url(url, data) + end + + it 'returns the passes_count' do + expect(subject).to eq(0) + end + end + + context 'when data has no errors' do + before do + accessibility_report.add_url(url, []) + end + + it 'returns the passes_count' do + expect(subject).to eq(1) + end + end + + context 'when data has no url' do + before do + accessibility_report.add_url("", []) + end + + it 'returns the scans_count' do + expect(subject).to eq(0) + end + end + end + + describe '#errors_count' do + subject { accessibility_report.errors_count } + + context 'when data has errors' do + let(:different_url) { 'https://about.gitlab.com' } + + before do + accessibility_report.add_url(url, data) + accessibility_report.add_url(different_url, data) + end + + it 'returns the errors_count' do + expect(subject).to eq(4) + end + end + + context 'when data has no errors' do + before do + accessibility_report.add_url(url, []) + end + + it 'returns the errors_count' do + expect(subject).to eq(0) + end + end + + context 'when data has no url' do + before do + accessibility_report.add_url("", []) + end + + it 'returns the errors_count' do + expect(subject).to eq(0) + end + end + end + + describe '#add_url' do + subject { accessibility_report.add_url(url, data) } + + context 'when data has errors' do + it 'adds urls and data to accessibility report' do + expect { subject }.not_to raise_error + + expect(accessibility_report.urls.keys).to eq([url]) + expect(accessibility_report.urls.values.flatten.size).to eq(2) + end + end + + context 'when data does not have errors' do + let(:data) { [] } + + it 'adds data to accessibility report' do + expect { subject }.not_to raise_error + + expect(accessibility_report.urls.keys).to eq([url]) + expect(accessibility_report.urls.values.flatten.size).to eq(0) + end + end + + context 'when url does not exist' do + let(:url) { '' } + let(:data) { [{ message: "Protocol error (Page.navigate): Cannot navigate to invalid URL" }] } + + it 'sets error_message and decreases total' do + expect { subject }.not_to raise_error + + expect(accessibility_report.scans_count).to eq(0) + expect(accessibility_report.error_message).to eq('Empty URL detected in gl-accessibility.json') + end + end + end + + describe '#set_error_message' do + let(:set_accessibility_error) { accessibility_report.set_error_message('error') } + + context 'when error is nil' do + it 'returns the error' do + expect(set_accessibility_error).to eq('error') + end + + it 'sets the error' do + set_accessibility_error + + expect(accessibility_report.error_message).to eq('error') + end + end + + context 'when a error has already been set' do + before do + accessibility_report.set_error_message('old error') + end + + it 'overwrites the existing message' do + expect { set_accessibility_error }.to change(accessibility_report, :error_message).from('old error').to('error') + end + end + end + + describe '#all_errors' do + subject { accessibility_report.all_errors } + + context 'when data has errors' do + before do + accessibility_report.add_url(url, data) + end + + it 'returns all errors' do + expect(subject.size).to eq(2) + end + end + + context 'when data has no errors' do + before do + accessibility_report.add_url(url, []) + end + + it 'returns an empty array' do + expect(subject).to eq([]) + end + end + + context 'when accessibility report has no data' do + it 'returns an empty array' do + expect(subject).to eq([]) + end + end + end +end diff --git a/spec/lib/gitlab/ci/reports/terraform_reports_spec.rb b/spec/lib/gitlab/ci/reports/terraform_reports_spec.rb new file mode 100644 index 00000000000..061029299ac --- /dev/null +++ b/spec/lib/gitlab/ci/reports/terraform_reports_spec.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Ci::Reports::TerraformReports do + it 'initializes plans with and empty hash' do + expect(subject.plans).to eq({}) + end + + describe '#add_plan' do + context 'when providing two unique plans' do + it 'returns two plans' do + subject.add_plan('a/tfplan.json', { 'create' => 0, 'update' => 1, 'delete' => 0 }) + subject.add_plan('b/tfplan.json', { 'create' => 0, 'update' => 1, 'delete' => 0 }) + + expect(subject.plans).to eq({ + 'a/tfplan.json' => { 'create' => 0, 'update' => 1, 'delete' => 0 }, + 'b/tfplan.json' => { 'create' => 0, 'update' => 1, 'delete' => 0 } + }) + end + end + + context 'when providing the same plan twice' do + it 'returns the last added plan' do + subject.add_plan('tfplan.json', { 'create' => 0, 'update' => 0, 'delete' => 0 }) + subject.add_plan('tfplan.json', { 'create' => 0, 'update' => 1, 'delete' => 0 }) + + expect(subject.plans).to eq({ + 'tfplan.json' => { 'create' => 0, 'update' => 1, 'delete' => 0 } + }) + end + end + end +end diff --git a/spec/lib/gitlab/ci/reports/test_case_spec.rb b/spec/lib/gitlab/ci/reports/test_case_spec.rb index c0652288cca..b5883867983 100644 --- a/spec/lib/gitlab/ci/reports/test_case_spec.rb +++ b/spec/lib/gitlab/ci/reports/test_case_spec.rb @@ -62,7 +62,7 @@ describe Gitlab::Ci::Reports::TestCase do end context 'when attachment is present' do - let(:attachment_test_case) { build(:test_case, :with_attachment) } + let(:attachment_test_case) { build(:test_case, :failed_with_attachment) } it "initializes the attachment if present" do expect(attachment_test_case.attachment).to eq("some/path.png") diff --git a/spec/lib/gitlab/ci/reports/test_reports_spec.rb b/spec/lib/gitlab/ci/reports/test_reports_spec.rb index 638acde69eb..e51728496e1 100644 --- a/spec/lib/gitlab/ci/reports/test_reports_spec.rb +++ b/spec/lib/gitlab/ci/reports/test_reports_spec.rb @@ -127,7 +127,7 @@ describe Gitlab::Ci::Reports::TestReports do context 'when test suites contain an attachment' do let(:test_case_succes) { build(:test_case) } - let(:test_case_with_attachment) { build(:test_case, :with_attachment) } + let(:test_case_with_attachment) { build(:test_case, :failed_with_attachment) } before do test_reports.get_suite('rspec').add_test_case(test_case_succes) @@ -141,6 +141,29 @@ describe Gitlab::Ci::Reports::TestReports do end end + describe '#suite_errors' do + subject { test_reports.suite_errors } + + context 'when a suite has normal spec errors or failures' do + before do + test_reports.get_suite('junit').add_test_case(create_test_case_java_success) + test_reports.get_suite('junit').add_test_case(create_test_case_java_failed) + test_reports.get_suite('junit').add_test_case(create_test_case_java_error) + end + + it { is_expected.to be_empty } + end + + context 'when there is an error test case' do + before do + test_reports.get_suite('rspec').add_test_case(create_test_case_rspec_success) + test_reports.get_suite('junit').set_suite_error('Existential parsing error') + end + + it { is_expected.to eq({ 'junit' => 'Existential parsing error' }) } + end + end + Gitlab::Ci::Reports::TestCase::STATUS_TYPES.each do |status_type| describe "##{status_type}_count" do subject { test_reports.public_send("#{status_type}_count") } diff --git a/spec/lib/gitlab/ci/reports/test_suite_spec.rb b/spec/lib/gitlab/ci/reports/test_suite_spec.rb index 9d9774afc82..e0b2593353a 100644 --- a/spec/lib/gitlab/ci/reports/test_suite_spec.rb +++ b/spec/lib/gitlab/ci/reports/test_suite_spec.rb @@ -101,7 +101,7 @@ describe Gitlab::Ci::Reports::TestSuite do end context 'when test cases contain an attachment' do - let(:test_case_with_attachment) { build(:test_case, :with_attachment)} + let(:test_case_with_attachment) { build(:test_case, :failed_with_attachment)} before do test_suite.add_test_case(test_case_with_attachment) @@ -114,6 +114,31 @@ describe Gitlab::Ci::Reports::TestSuite do end end + describe '#set_suite_error' do + let(:set_suite_error) { test_suite.set_suite_error('message') } + + context 'when @suite_error is nil' do + it 'returns message' do + expect(set_suite_error).to eq('message') + end + + it 'sets the new message' do + set_suite_error + expect(test_suite.suite_error).to eq('message') + end + end + + context 'when a suite_error has already been set' do + before do + test_suite.set_suite_error('old message') + end + + it 'overwrites the existing message' do + expect { set_suite_error }.to change(test_suite, :suite_error).from('old message').to('message') + end + end + end + Gitlab::Ci::Reports::TestCase::STATUS_TYPES.each do |status_type| describe "##{status_type}" do subject { test_suite.public_send("#{status_type}") } diff --git a/spec/lib/gitlab/ci/templates/Jobs/browser_performance_testing_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/Jobs/browser_performance_testing_gitlab_ci_yaml_spec.rb new file mode 100644 index 00000000000..54c3500b0a0 --- /dev/null +++ b/spec/lib/gitlab/ci/templates/Jobs/browser_performance_testing_gitlab_ci_yaml_spec.rb @@ -0,0 +1,85 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'Jobs/Browser-Performance-Testing.gitlab-ci.yml' do + subject(:template) do + <<~YAML + stages: + - test + - performance + + include: + - template: 'Jobs/Browser-Performance-Testing.gitlab-ci.yml' + + placeholder: + script: + - keep pipeline validator happy by having a job when stages are intentionally empty + YAML + end + + describe 'the created pipeline' do + let(:user) { create(:admin) } + let(:project) do + create(:project, :repository, variables: [ + build(:ci_variable, key: 'CI_KUBERNETES_ACTIVE', value: 'true') + ]) + end + + let(:default_branch) { 'master' } + let(:pipeline_ref) { default_branch } + let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_ref) } + let(:pipeline) { service.execute!(:push) } + let(:build_names) { pipeline.builds.pluck(:name) } + + before do + stub_ci_pipeline_yaml_file(template) + + allow_any_instance_of(Ci::BuildScheduleWorker).to receive(:perform).and_return(true) + allow(project).to receive(:default_branch).and_return(default_branch) + end + + it 'has no errors' do + expect(pipeline.errors).to be_empty + end + + shared_examples_for 'performance job on tag or branch' do + it 'by default' do + expect(build_names).to include('performance') + end + + it 'when PERFORMANCE_DISABLED' do + create(:ci_variable, project: project, key: 'PERFORMANCE_DISABLED', value: '1') + + expect(build_names).not_to include('performance') + end + end + + context 'on master' do + it_behaves_like 'performance job on tag or branch' + end + + context 'on another branch' do + let(:pipeline_ref) { 'feature' } + + it_behaves_like 'performance job on tag or branch' + end + + context 'on tag' do + let(:pipeline_ref) { 'v1.0.0' } + + it_behaves_like 'performance job on tag or branch' + end + + context 'on merge request' do + let(:service) { MergeRequests::CreatePipelineService.new(project, user) } + let(:merge_request) { create(:merge_request, :simple, source_project: project) } + let(:pipeline) { service.execute(merge_request) } + + it 'has no jobs' do + expect(pipeline).to be_merge_request_event + expect(build_names).to be_empty + end + end + end +end diff --git a/spec/lib/gitlab/ci/templates/Jobs/build_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/Jobs/build_gitlab_ci_yaml_spec.rb new file mode 100644 index 00000000000..b2a9e3f5cf4 --- /dev/null +++ b/spec/lib/gitlab/ci/templates/Jobs/build_gitlab_ci_yaml_spec.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'Jobs/Build.gitlab-ci.yml' do + subject(:template) { Gitlab::Template::GitlabCiYmlTemplate.find('Jobs/Build') } + + describe 'the created pipeline' do + let_it_be(:user) { create(:admin) } + let_it_be(:project) { create(:project, :repository) } + + let(:default_branch) { 'master' } + let(:pipeline_ref) { default_branch } + let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_ref) } + 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 + + context 'on master' do + it 'creates the build job' do + expect(build_names).to contain_exactly('build') + end + end + + context 'on another branch' do + let(:pipeline_ref) { 'feature' } + + it 'creates the build job' do + expect(build_names).to contain_exactly('build') + end + end + + context 'on tag' do + let(:pipeline_ref) { 'v1.0.0' } + + it 'creates the build job' do + expect(pipeline).to be_tag + expect(build_names).to contain_exactly('build') + end + end + + context 'on merge request' do + let(:service) { MergeRequests::CreatePipelineService.new(project, user) } + let(:merge_request) { create(:merge_request, :simple, source_project: project) } + let(:pipeline) { service.execute(merge_request) } + + it 'has no jobs' do + expect(pipeline).to be_merge_request_event + expect(build_names).to be_empty + end + end + end +end diff --git a/spec/lib/gitlab/ci/templates/Jobs/code_quality_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/Jobs/code_quality_gitlab_ci_yaml_spec.rb new file mode 100644 index 00000000000..9c5b2fd5099 --- /dev/null +++ b/spec/lib/gitlab/ci/templates/Jobs/code_quality_gitlab_ci_yaml_spec.rb @@ -0,0 +1,86 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'Jobs/Code-Quality.gitlab-ci.yml' do + subject(:template) { Gitlab::Template::GitlabCiYmlTemplate.find('Jobs/Code-Quality') } + + describe 'the created pipeline' do + let_it_be(:user) { create(:admin) } + let_it_be(:project) { create(:project, :repository) } + + let(:default_branch) { 'master' } + let(:pipeline_ref) { default_branch } + let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_ref) } + 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 + + context 'on master' do + it 'creates the code_quality job' do + expect(build_names).to contain_exactly('code_quality') + end + end + + context 'on another branch' do + let(:pipeline_ref) { 'feature' } + + it 'creates the code_quality job' do + expect(build_names).to contain_exactly('code_quality') + end + end + + context 'on tag' do + let(:pipeline_ref) { 'v1.0.0' } + + it 'creates the code_quality job' do + expect(pipeline).to be_tag + expect(build_names).to contain_exactly('code_quality') + end + end + + context 'on merge request' do + let(:service) { MergeRequests::CreatePipelineService.new(project, user) } + let(:merge_request) { create(:merge_request, :simple, source_project: project) } + let(:pipeline) { service.execute(merge_request) } + + it 'has no jobs' do + expect(pipeline).to be_merge_request_event + expect(build_names).to be_empty + end + end + + context 'CODE_QUALITY_DISABLED is set' do + before do + create(:ci_variable, key: 'CODE_QUALITY_DISABLED', value: 'true', project: project) + end + + context 'on master' do + it 'has no jobs' do + expect { pipeline }.to raise_error(Ci::CreatePipelineService::CreateError) + end + end + + context 'on another branch' do + let(:pipeline_ref) { 'feature' } + + it 'has no jobs' do + expect { pipeline }.to raise_error(Ci::CreatePipelineService::CreateError) + end + end + + context 'on tag' do + let(:pipeline_ref) { 'v1.0.0' } + + it 'has no jobs' do + expect { pipeline }.to raise_error(Ci::CreatePipelineService::CreateError) + end + end + end + end +end diff --git a/spec/lib/gitlab/ci/templates/Jobs/deploy_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/Jobs/deploy_gitlab_ci_yaml_spec.rb new file mode 100644 index 00000000000..a6ae23c85d3 --- /dev/null +++ b/spec/lib/gitlab/ci/templates/Jobs/deploy_gitlab_ci_yaml_spec.rb @@ -0,0 +1,222 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'Jobs/Deploy.gitlab-ci.yml' do + subject(:template) do + <<~YAML + stages: + - test + - review + - staging + - canary + - production + - incremental rollout 10% + - incremental rollout 25% + - incremental rollout 50% + - incremental rollout 100% + - cleanup + + include: + - template: Jobs/Deploy.gitlab-ci.yml + + placeholder: + script: + - echo "Ensure at least one job to keep pipeline validator happy" + YAML + end + + describe 'the created pipeline' do + let(:user) { create(:admin) } + let(:project) { create(:project, :repository) } + + let(:default_branch) { 'master' } + let(:pipeline_ref) { default_branch } + let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_ref) } + let(:pipeline) { service.execute!(:push) } + let(:build_names) { pipeline.builds.pluck(:name) } + + before do + stub_ci_pipeline_yaml_file(template) + + allow_any_instance_of(Ci::BuildScheduleWorker).to receive(:perform).and_return(true) + allow(project).to receive(:default_branch).and_return(default_branch) + end + + context 'with no cluster' do + it 'does not create any kubernetes deployment jobs' do + expect(build_names).to eq %w(placeholder) + end + end + + context 'with only a disabled cluster' do + let!(:cluster) { create(:cluster, :project, :provided_by_gcp, enabled: false, projects: [project]) } + + it 'does not create any kubernetes deployment jobs' do + expect(build_names).to eq %w(placeholder) + end + end + + context 'with an active cluster' do + let!(:cluster) { create(:cluster, :project, :provided_by_gcp, projects: [project]) } + + context 'on master' do + it 'by default' do + expect(build_names).to include('production') + expect(build_names).not_to include('review') + end + + it 'when CANARY_ENABLED' do + create(:ci_variable, project: project, key: 'CANARY_ENABLED', value: 'true') + + expect(build_names).to include('production_manual') + expect(build_names).to include('canary') + expect(build_names).not_to include('production') + end + + it 'when STAGING_ENABLED' do + create(:ci_variable, project: project, key: 'STAGING_ENABLED', value: 'true') + + expect(build_names).to include('production_manual') + expect(build_names).to include('staging') + expect(build_names).not_to include('production') + end + + it 'when INCREMENTAL_ROLLOUT_MODE == timed' do + create(:ci_variable, project: project, key: 'INCREMENTAL_ROLLOUT_ENABLED', value: 'true') + create(:ci_variable, project: project, key: 'INCREMENTAL_ROLLOUT_MODE', value: 'timed') + + expect(build_names).not_to include('production_manual') + expect(build_names).not_to include('production') + expect(build_names).not_to include( + 'rollout 10%', + 'rollout 25%', + 'rollout 50%', + 'rollout 100%' + ) + expect(build_names).to include( + 'timed rollout 10%', + 'timed rollout 25%', + 'timed rollout 50%', + 'timed rollout 100%' + ) + end + + it 'when INCREMENTAL_ROLLOUT_ENABLED' do + create(:ci_variable, project: project, key: 'INCREMENTAL_ROLLOUT_ENABLED', value: 'true') + + expect(build_names).not_to include('production_manual') + expect(build_names).not_to include('production') + expect(build_names).not_to include( + 'timed rollout 10%', + 'timed rollout 25%', + 'timed rollout 50%', + 'timed rollout 100%' + ) + expect(build_names).to include( + 'rollout 10%', + 'rollout 25%', + 'rollout 50%', + 'rollout 100%' + ) + end + + it 'when INCREMENTAL_ROLLOUT_MODE == manual' do + create(:ci_variable, project: project, key: 'INCREMENTAL_ROLLOUT_MODE', value: 'manual') + + expect(build_names).not_to include('production_manual') + expect(build_names).not_to include('production') + expect(build_names).not_to include( + 'timed rollout 10%', + 'timed rollout 25%', + 'timed rollout 50%', + 'timed rollout 100%' + ) + expect(build_names).to include( + 'rollout 10%', + 'rollout 25%', + 'rollout 50%', + 'rollout 100%' + ) + end + end + + shared_examples_for 'review app deployment' do + it 'creates the review and stop_review jobs but no production jobs' do + expect(build_names).to include('review') + expect(build_names).to include('stop_review') + 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('timed rollout 10%') + expect(build_names).not_to include('timed rollout 25%') + expect(build_names).not_to include('timed rollout 50%') + expect(build_names).not_to include('timed rollout 100%') + expect(build_names).not_to include('rollout 10%') + expect(build_names).not_to include('rollout 25%') + expect(build_names).not_to include('rollout 50%') + expect(build_names).not_to include('rollout 100%') + end + + it 'does not include review when REVIEW_DISABLED' do + create(:ci_variable, project: project, key: 'REVIEW_DISABLED', value: 'true') + + expect(build_names).not_to include('review') + expect(build_names).not_to include('stop_review') + end + end + + context 'on branch' do + let(:pipeline_ref) { 'feature' } + + before do + allow_any_instance_of(Gitlab::Ci::Pipeline::Chain::Validate::Repository).to receive(:perform!).and_return(true) + end + + it_behaves_like 'review app deployment' + + context 'when INCREMENTAL_ROLLOUT_ENABLED' do + before do + create(:ci_variable, project: project, key: 'INCREMENTAL_ROLLOUT_ENABLED', value: 'true') + end + + it_behaves_like 'review app deployment' + end + + context 'when INCREMENTAL_ROLLOUT_MODE == "timed"' do + before do + create(:ci_variable, project: project, key: 'INCREMENTAL_ROLLOUT_MODE', value: 'timed') + end + + it_behaves_like 'review app deployment' + end + + context 'when INCREMENTAL_ROLLOUT_MODE == "manual"' do + before do + create(:ci_variable, project: project, key: 'INCREMENTAL_ROLLOUT_MODE', value: 'manual') + end + + it_behaves_like 'review app deployment' + end + end + + context 'on tag' do + let(:pipeline_ref) { 'v1.0.0' } + + it_behaves_like 'review app deployment' + end + + context 'on merge request' do + let(:service) { MergeRequests::CreatePipelineService.new(project, user) } + let(:merge_request) { create(:merge_request, :simple, source_project: project) } + let(:pipeline) { service.execute(merge_request) } + + it 'has no jobs' do + expect(pipeline).to be_merge_request_event + expect(build_names).to be_empty + end + end + end + end +end diff --git a/spec/lib/gitlab/ci/templates/Jobs/test_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/Jobs/test_gitlab_ci_yaml_spec.rb new file mode 100644 index 00000000000..2186bf038eb --- /dev/null +++ b/spec/lib/gitlab/ci/templates/Jobs/test_gitlab_ci_yaml_spec.rb @@ -0,0 +1,86 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'Jobs/Test.gitlab-ci.yml' do + subject(:template) { Gitlab::Template::GitlabCiYmlTemplate.find('Jobs/Test') } + + describe 'the created pipeline' do + let_it_be(:user) { create(:admin) } + let_it_be(:project) { create(:project, :repository) } + + let(:default_branch) { 'master' } + let(:pipeline_ref) { default_branch } + let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_ref) } + 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 + + context 'on master' do + it 'creates the test job' do + expect(build_names).to contain_exactly('test') + end + end + + context 'on another branch' do + let(:pipeline_ref) { 'feature' } + + it 'creates the test job' do + expect(build_names).to contain_exactly('test') + end + end + + context 'on tag' do + let(:pipeline_ref) { 'v1.0.0' } + + it 'creates the test job' do + expect(pipeline).to be_tag + expect(build_names).to contain_exactly('test') + end + end + + context 'on merge request' do + let(:service) { MergeRequests::CreatePipelineService.new(project, user) } + let(:merge_request) { create(:merge_request, :simple, source_project: project) } + let(:pipeline) { service.execute(merge_request) } + + it 'has no jobs' do + expect(pipeline).to be_merge_request_event + expect(build_names).to be_empty + end + end + + context 'TEST_DISABLED is set' do + before do + create(:ci_variable, key: 'TEST_DISABLED', value: 'true', project: project) + end + + context 'on master' do + it 'has no jobs' do + expect { pipeline }.to raise_error(Ci::CreatePipelineService::CreateError) + end + end + + context 'on another branch' do + let(:pipeline_ref) { 'feature' } + + it 'has no jobs' do + expect { pipeline }.to raise_error(Ci::CreatePipelineService::CreateError) + end + end + + context 'on tag' do + let(:pipeline_ref) { 'v1.0.0' } + + it 'has no jobs' do + expect { pipeline }.to raise_error(Ci::CreatePipelineService::CreateError) + end + 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 0c5d172f17c..af6ec25b9d6 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 @@ -20,16 +20,8 @@ describe 'Auto-DevOps.gitlab-ci.yml' do allow(project).to receive(:default_branch).and_return(default_branch) end - it 'creates a build and a test job' do - expect(build_names).to include('build', 'test') - 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 'does not create any deployment-related builds' do + 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') @@ -39,13 +31,95 @@ describe 'Auto-DevOps.gitlab-ci.yml' do end end - context 'when the project has an active cluster' do - let(:cluster) { create(:cluster, :project, :provided_by_gcp, projects: [project]) } + it 'creates a build and a test job' do + expect(build_names).to include('build', 'test') + end + + context 'when the project is set for deployment to AWS' do + let(:platform_value) { 'ECS' } before do - allow(cluster).to receive(:active?).and_return(true) + create(:ci_variable, project: project, key: 'AUTO_DEVOPS_PLATFORM_TARGET', value: platform_value) + 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 } + + it 'does not trigger the job' do + expect(build_names).not_to include(job_name) + end + end + + context 'when AUTO_DEVOPS_PLATFORM_TARGET is empty' do + let(:platform_value) { '' } + + it 'does not trigger the job' do + expect(build_names).not_to include(job_name) + end + end + end + + it_behaves_like 'no Kubernetes deployment job' + + it_behaves_like 'no ECS job when AUTO_DEVOPS_PLATFORM_TARGET is not present' do + let(:job_name) { 'production_ecs' } + end + + it 'creates an ECS deployment job for production only' do + expect(build_names).not_to include('review_ecs') + expect(build_names).to include('production_ecs') end + 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) + end + + it_behaves_like 'no ECS job when AUTO_DEVOPS_PLATFORM_TARGET is not present' do + let(:job_name) { 'review_ecs' } + end + + it 'creates an ECS deployment job for review only' do + expect(build_names).to include('review_ecs') + expect(build_names).not_to include('production_ecs') + expect(build_names).not_to include('review') + expect(build_names).not_to include('production') + end + end + + 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 + + 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 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 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 diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb index 70c3c5ab339..c93bb901981 100644 --- a/spec/lib/gitlab/ci/yaml_processor_spec.rb +++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb @@ -1364,6 +1364,24 @@ module Gitlab expect { described_class.new(config) }.to raise_error(described_class::ValidationError) end + + it 'populates a build options with complete artifacts configuration' do + stub_feature_flags(ci_artifacts_exclude: true) + + config = <<~YAML + test: + script: echo "Hello World" + artifacts: + paths: + - my/test + exclude: + - my/test/something + YAML + + attributes = Gitlab::Ci::YamlProcessor.new(config).build_attributes('test') + + expect(attributes.dig(*%i[options artifacts exclude])).to eq(%w[my/test/something]) + end end describe "release" do @@ -2264,14 +2282,14 @@ module Gitlab config = YAML.dump({ rspec: { script: "test", type: "acceptance" } }) expect do Gitlab::Ci::YamlProcessor.new(config) - end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "rspec job: stage parameter should be .pre, build, test, deploy, .post") + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "rspec job: chosen stage does not exist; available stages are .pre, build, test, deploy, .post") end it "returns errors if job stage is not a defined stage" do config = YAML.dump({ types: %w(build test), rspec: { script: "test", type: "acceptance" } }) expect do Gitlab::Ci::YamlProcessor.new(config) - end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "rspec job: stage parameter should be .pre, build, test, .post") + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "rspec job: chosen stage does not exist; available stages are .pre, build, test, .post") end it "returns errors if stages is not an array" do |