diff options
Diffstat (limited to 'spec/models/ci/build_spec.rb')
-rw-r--r-- | spec/models/ci/build_spec.rb | 425 |
1 files changed, 357 insertions, 68 deletions
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index 6e8845cdcf4..767f0ad9e65 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Ci::Build, :models do +describe Ci::Build do let(:user) { create(:user) } let(:project) { create(:project, :repository) } let(:build) { create(:ci_build, pipeline: pipeline) } @@ -21,6 +21,18 @@ describe Ci::Build, :models do it { is_expected.to respond_to(:has_trace?) } it { is_expected.to respond_to(:trace) } + describe '.manual_actions' do + let!(:manual_but_created) { create(:ci_build, :manual, status: :created, pipeline: pipeline) } + let!(:manual_but_succeeded) { create(:ci_build, :manual, status: :success, pipeline: pipeline) } + let!(:manual_action) { create(:ci_build, :manual, pipeline: pipeline) } + + subject { described_class.manual_actions } + + it { is_expected.to include(manual_action) } + it { is_expected.to include(manual_but_succeeded) } + it { is_expected.not_to include(manual_but_created) } + end + describe '#actionize' do context 'when build is a created' do before do @@ -95,12 +107,18 @@ describe Ci::Build, :models do it { is_expected.to be_truthy } context 'is expired' do - before { build.update(artifacts_expire_at: Time.now - 7.days) } + before do + build.update(artifacts_expire_at: Time.now - 7.days) + end + it { is_expected.to be_falsy } end context 'is not expired' do - before { build.update(artifacts_expire_at: Time.now + 7.days) } + before do + build.update(artifacts_expire_at: Time.now + 7.days) + end + it { is_expected.to be_truthy } end end @@ -110,13 +128,17 @@ describe Ci::Build, :models do subject { build.artifacts_expired? } context 'is expired' do - before { build.update(artifacts_expire_at: Time.now - 7.days) } + before do + build.update(artifacts_expire_at: Time.now - 7.days) + end it { is_expected.to be_truthy } end context 'is not expired' do - before { build.update(artifacts_expire_at: Time.now + 7.days) } + before do + build.update(artifacts_expire_at: Time.now + 7.days) + end it { is_expected.to be_falsey } end @@ -141,7 +163,9 @@ describe Ci::Build, :models do context 'when artifacts_expire_at is specified' do let(:expire_at) { Time.now + 7.days } - before { build.artifacts_expire_at = expire_at } + before do + build.artifacts_expire_at = expire_at + end it { is_expected.to be_within(5).of(expire_at - Time.now) } end @@ -201,7 +225,7 @@ describe Ci::Build, :models do it 'expects to have retried builds instead the original ones' do project.add_developer(user) - retried_rspec = Ci::Build.retry(rspec_test, user) + retried_rspec = described_class.retry(rspec_test, user) expect(staging.depends_on_builds.map(&:id)) .to contain_exactly(build.id, retried_rspec.id, rubocop_test.id) @@ -596,9 +620,9 @@ describe Ci::Build, :models do describe '#first_pending' do let!(:first) { create(:ci_build, pipeline: pipeline, status: 'pending', created_at: Date.yesterday) } let!(:second) { create(:ci_build, pipeline: pipeline, status: 'pending') } - subject { Ci::Build.first_pending } + subject { described_class.first_pending } - it { is_expected.to be_a(Ci::Build) } + it { is_expected.to be_a(described_class) } it('returns with the first pending build') { is_expected.to eq(first) } end @@ -778,6 +802,47 @@ describe Ci::Build, :models do end end + describe 'build auto retry feature' do + describe '#retries_count' do + subject { create(:ci_build, name: 'test', pipeline: pipeline) } + + context 'when build has been retried several times' do + before do + create(:ci_build, :retried, name: 'test', pipeline: pipeline) + create(:ci_build, :retried, name: 'test', pipeline: pipeline) + end + + it 'reports a correct retry count value' do + expect(subject.retries_count).to eq 2 + end + end + + context 'when build has not been retried' do + it 'returns zero' do + expect(subject.retries_count).to eq 0 + end + end + end + + describe '#retries_max' do + context 'when max retries value is defined' do + subject { create(:ci_build, options: { retry: 1 }) } + + it 'returns a number of configured max retries' do + expect(subject.retries_max).to eq 1 + end + end + + context 'when max retries value is not defined' do + subject { create(:ci_build) } + + it 'returns zero' do + expect(subject.retries_max).to eq 0 + end + end + end + end + describe '#keep_artifacts!' do let(:build) { create(:ci_build, artifacts_expire_at: Time.now + 7.days) } @@ -839,8 +904,8 @@ describe Ci::Build, :models do pipeline2 = create(:ci_pipeline, project: project) @build2 = create(:ci_build, pipeline: pipeline2) - allow(@merge_request).to receive(:commits_sha). - and_return([pipeline.sha, pipeline2.sha]) + allow(@merge_request).to receive(:commit_shas) + .and_return([pipeline.sha, pipeline2.sha]) allow(MergeRequest).to receive_message_chain(:includes, :where, :reorder).and_return([@merge_request]) end @@ -880,7 +945,7 @@ describe Ci::Build, :models do end context 'when build is retried' do - let!(:new_build) { Ci::Build.retry(build, user) } + let!(:new_build) { described_class.retry(build, user) } it 'does not return any of them' do is_expected.not_to include(build, new_build) @@ -888,7 +953,11 @@ describe Ci::Build, :models do end context 'when other build is retried' do - let!(:retried_build) { Ci::Build.retry(other_build, user) } + let!(:retried_build) { described_class.retry(other_build, user) } + + before do + retried_build.success + end it 'returns a retried build' do is_expected.to contain_exactly(retried_build) @@ -897,22 +966,30 @@ describe Ci::Build, :models do end describe '#persisted_environment' do - before do - @environment = create(:environment, project: project, name: "foo-#{project.default_branch}") + let!(:environment) do + create(:environment, project: project, name: "foo-#{project.default_branch}") end subject { build.persisted_environment } - context 'referenced literally' do - let(:build) { create(:ci_build, pipeline: pipeline, environment: "foo-#{project.default_branch}") } + context 'when referenced literally' do + let(:build) do + create(:ci_build, pipeline: pipeline, environment: "foo-#{project.default_branch}") + end - it { is_expected.to eq(@environment) } + it { is_expected.to eq(environment) } end - context 'referenced with a variable' do - let(:build) { create(:ci_build, pipeline: pipeline, environment: "foo-$CI_COMMIT_REF_NAME") } + context 'when referenced with a variable' do + let(:build) do + create(:ci_build, pipeline: pipeline, environment: "foo-$CI_COMMIT_REF_NAME") + end + + it { is_expected.to eq(environment) } + end - it { is_expected.to eq(@environment) } + context 'when there is no environment' do + it { is_expected.to be_nil } end end @@ -923,26 +1000,8 @@ describe Ci::Build, :models do project.add_developer(user) end - context 'when build is manual' do - it 'enqueues a build' do - new_build = build.play(user) - - expect(new_build).to be_pending - expect(new_build).to eq(build) - end - end - - context 'when build is passed' do - before do - build.update(status: 'success') - end - - it 'creates a new build' do - new_build = build.play(user) - - expect(new_build).to be_pending - expect(new_build).not_to eq(build) - end + it 'enqueues the build' do + expect(build.play(user)).to be_pending end end @@ -980,13 +1039,17 @@ describe Ci::Build, :models do describe '#ref_slug' do { - 'master' => 'master', - '1-foo' => '1-foo', - 'fix/1-foo' => 'fix-1-foo', - 'fix-1-foo' => 'fix-1-foo', - 'a' * 63 => 'a' * 63, - 'a' * 64 => 'a' * 63, - 'FOO' => 'foo', + 'master' => 'master', + '1-foo' => '1-foo', + 'fix/1-foo' => 'fix-1-foo', + 'fix-1-foo' => 'fix-1-foo', + 'a' * 63 => 'a' * 63, + 'a' * 64 => 'a' * 63, + 'FOO' => 'foo', + '-' + 'a' * 61 + '-' => 'a' * 61, + '-' + 'a' * 62 + '-' => 'a' * 62, + '-' + 'a' * 63 + '-' => 'a' * 62, + 'a' * 62 + ' ' => 'a' * 62 }.each do |ref, slug| it "transforms #{ref} to #{slug}" do build.ref = ref @@ -1045,7 +1108,9 @@ describe Ci::Build, :models do describe '#has_expiring_artifacts?' do context 'when artifacts have expiration date set' do - before { build.update(artifacts_expire_at: 1.day.from_now) } + before do + build.update(artifacts_expire_at: 1.day.from_now) + end it 'has expiring artifacts' do expect(build).to have_expiring_artifacts @@ -1053,7 +1118,9 @@ describe Ci::Build, :models do end context 'when artifacts do not have expiration date set' do - before { build.update(artifacts_expire_at: nil) } + before do + build.update(artifacts_expire_at: nil) + end it 'does not have expiring artifacts' do expect(build).not_to have_expiring_artifacts @@ -1153,12 +1220,14 @@ describe Ci::Build, :models do { key: 'CI_PROJECT_ID', value: project.id.to_s, public: true }, { key: 'CI_PROJECT_NAME', value: project.path, public: true }, { key: 'CI_PROJECT_PATH', value: project.full_path, public: true }, + { key: 'CI_PROJECT_PATH_SLUG', value: project.full_path_slug, public: true }, { key: 'CI_PROJECT_NAMESPACE', value: project.namespace.full_path, public: true }, { key: 'CI_PROJECT_URL', value: project.web_url, public: true }, { key: 'CI_PIPELINE_ID', value: pipeline.id.to_s, public: true }, + { key: 'CI_CONFIG_PATH', value: pipeline.ci_yaml_file_path, public: true }, { key: 'CI_REGISTRY_USER', value: 'gitlab-ci-token', public: true }, { key: 'CI_REGISTRY_PASSWORD', value: build.token, public: false }, - { key: 'CI_REPOSITORY_URL', value: build.repo_url, public: false }, + { key: 'CI_REPOSITORY_URL', value: build.repo_url, public: false } ] end @@ -1190,11 +1259,6 @@ describe Ci::Build, :models do end context 'when build has an environment' do - before do - build.update(environment: 'production') - create(:environment, project: build.project, name: 'production', slug: 'prod-slug') - end - let(:environment_variables) do [ { key: 'CI_ENVIRONMENT_NAME', value: 'production', public: true }, @@ -1202,7 +1266,66 @@ describe Ci::Build, :models do ] end - it { environment_variables.each { |v| is_expected.to include(v) } } + let!(:environment) do + create(:environment, + project: build.project, + name: 'production', + slug: 'prod-slug', + external_url: '') + end + + before do + build.update(environment: 'production') + end + + shared_examples 'containing environment variables' do + it { environment_variables.each { |v| is_expected.to include(v) } } + end + + context 'when no URL was set' do + it_behaves_like 'containing environment variables' + + it 'does not have CI_ENVIRONMENT_URL' do + keys = subject.map { |var| var[:key] } + + expect(keys).not_to include('CI_ENVIRONMENT_URL') + end + end + + context 'when an URL was set' do + let(:url) { 'http://host/test' } + + before do + environment_variables << + { key: 'CI_ENVIRONMENT_URL', value: url, public: true } + end + + context 'when the URL was set from the job' do + before do + build.update(options: { environment: { url: url } }) + end + + it_behaves_like 'containing environment variables' + + context 'when variables are used in the URL, it does not expand' do + let(:url) { 'http://$CI_PROJECT_NAME-$CI_ENVIRONMENT_SLUG' } + + it_behaves_like 'containing environment variables' + + it 'puts $CI_ENVIRONMENT_URL in the last so all other variables are available to be used when runners are trying to expand it' do + expect(subject.last).to eq(environment_variables.last) + end + end + end + + context 'when the URL was not set from the job, but environment' do + before do + environment.update(external_url: url) + end + + it_behaves_like 'containing environment variables' + end + end end context 'when build started manually' do @@ -1229,16 +1352,102 @@ describe Ci::Build, :models do it { is_expected.to include(tag_variable) } end - context 'when secure variable is defined' do - let(:secure_variable) do + context 'when secret variable is defined' do + let(:secret_variable) do + { key: 'SECRET_KEY', value: 'secret_value', public: false } + end + + before do + create(:ci_variable, + secret_variable.slice(:key, :value).merge(project: project)) + end + + it { is_expected.to include(secret_variable) } + end + + context 'when protected variable is defined' do + let(:protected_variable) do + { key: 'PROTECTED_KEY', value: 'protected_value', public: false } + end + + before do + create(:ci_variable, + :protected, + protected_variable.slice(:key, :value).merge(project: project)) + end + + context 'when the branch is protected' do + before do + create(:protected_branch, project: build.project, name: build.ref) + end + + it { is_expected.to include(protected_variable) } + end + + context 'when the tag is protected' do + before do + create(:protected_tag, project: build.project, name: build.ref) + end + + it { is_expected.to include(protected_variable) } + end + + context 'when the ref is not protected' do + it { is_expected.not_to include(protected_variable) } + end + end + + context 'when group secret variable is defined' do + let(:secret_variable) do { key: 'SECRET_KEY', value: 'secret_value', public: false } end + let(:group) { create(:group, :access_requestable) } + + before do + build.project.update(group: group) + + create(:ci_group_variable, + secret_variable.slice(:key, :value).merge(group: group)) + end + + it { is_expected.to include(secret_variable) } + end + + context 'when group protected variable is defined' do + let(:protected_variable) do + { key: 'PROTECTED_KEY', value: 'protected_value', public: false } + end + + let(:group) { create(:group, :access_requestable) } + before do - build.project.variables << Ci::Variable.new(key: 'SECRET_KEY', value: 'secret_value') + build.project.update(group: group) + + create(:ci_group_variable, + :protected, + protected_variable.slice(:key, :value).merge(group: group)) + end + + context 'when the branch is protected' do + before do + create(:protected_branch, project: build.project, name: build.ref) + end + + it { is_expected.to include(protected_variable) } end - it { is_expected.to include(secure_variable) } + context 'when the tag is protected' do + before do + create(:protected_tag, project: build.project, name: build.ref) + end + + it { is_expected.to include(protected_variable) } + end + + context 'when the ref is not protected' do + it { is_expected.not_to include(protected_variable) } + end end context 'when build is for triggers' do @@ -1259,6 +1468,29 @@ describe Ci::Build, :models do it { is_expected.to include(predefined_trigger_variable) } end + context 'when pipeline has a variable' do + let!(:pipeline_variable) { create(:ci_pipeline_variable, pipeline: pipeline) } + + it { is_expected.to include(pipeline_variable.to_runner_variable) } + end + + context 'when a job was triggered by a pipeline schedule' do + let(:pipeline_schedule) { create(:ci_pipeline_schedule, project: project) } + + let!(:pipeline_schedule_variable) do + create(:ci_pipeline_schedule_variable, + key: 'SCHEDULE_VARIABLE_KEY', + pipeline_schedule: pipeline_schedule) + end + + before do + pipeline_schedule.pipelines << pipeline + pipeline_schedule.reload + end + + it { is_expected.to include(pipeline_schedule_variable.to_runner_variable) } + end + context 'when yaml_variables are undefined' do before do build.yaml_variables = nil @@ -1359,20 +1591,46 @@ describe Ci::Build, :models do it { is_expected.to include(deployment_variable) } end + context 'when project has custom CI config path' do + let(:ci_config_path) { { key: 'CI_CONFIG_PATH', value: 'custom', public: true } } + + before do + project.update(ci_config_path: 'custom') + end + + it { is_expected.to include(ci_config_path) } + end + context 'returns variables in valid order' do + let(:build_pre_var) { { key: 'build', value: 'value' } } + let(:project_pre_var) { { key: 'project', value: 'value' } } + let(:pipeline_pre_var) { { key: 'pipeline', value: 'value' } } + let(:build_yaml_var) { { key: 'yaml', value: 'value' } } + before do - allow(build).to receive(:predefined_variables) { ['predefined'] } - allow(project).to receive(:predefined_variables) { ['project'] } - allow(pipeline).to receive(:predefined_variables) { ['pipeline'] } - allow(build).to receive(:yaml_variables) { ['yaml'] } - allow(project).to receive(:secret_variables) { ['secret'] } + allow(build).to receive(:predefined_variables) { [build_pre_var] } + allow(project).to receive(:predefined_variables) { [project_pre_var] } + allow(pipeline).to receive(:predefined_variables) { [pipeline_pre_var] } + allow(build).to receive(:yaml_variables) { [build_yaml_var] } + + allow(project).to receive(:secret_variables_for) + .with(ref: 'master', environment: nil) do + [create(:ci_variable, key: 'secret', value: 'value')] + end end - it { is_expected.to eq(%w[predefined project pipeline yaml secret]) } + it do + is_expected.to eq( + [build_pre_var, + project_pre_var, + pipeline_pre_var, + build_yaml_var, + { key: 'secret', value: 'value', public: false }]) + end end end - describe 'State transition: any => [:pending]' do + describe 'state transition: any => [:pending]' do let(:build) { create(:ci_build, :created) } it 'queues BuildQueueWorker' do @@ -1381,4 +1639,35 @@ describe Ci::Build, :models do build.enqueue end end + + describe 'state transition when build fails' do + context 'when build is configured to be retried' do + subject { create(:ci_build, :running, options: { retry: 3 }) } + + it 'retries builds and assigns a same user to it' do + expect(described_class).to receive(:retry) + .with(subject, subject.user) + + subject.drop! + end + end + + context 'when build is not configured to be retried' do + subject { create(:ci_build, :running) } + + it 'does not retry build' do + expect(described_class).not_to receive(:retry) + + subject.drop! + end + + it 'does not count retries when not necessary' do + expect(described_class).not_to receive(:retry) + expect_any_instance_of(described_class) + .not_to receive(:retries_count) + + subject.drop! + end + end + end end |