Commit cfedc0a9 authored by Olivier Gonzalez's avatar Olivier Gonzalez Committed by Kamil Trzciński

Extend reports to support security features

parent 79498893
...@@ -68,8 +68,12 @@ module Ci ...@@ -68,8 +68,12 @@ module Ci
'', Ci::JobArtifact.select(1).where('ci_builds.id = ci_job_artifacts.job_id').archive) '', Ci::JobArtifact.select(1).where('ci_builds.id = ci_job_artifacts.job_id').archive)
end end
scope :with_existing_job_artifacts, ->(query) do
where('EXISTS (?)', ::Ci::JobArtifact.select(1).where('ci_builds.id = ci_job_artifacts.job_id').merge(query))
end
scope :with_archived_trace, ->() do scope :with_archived_trace, ->() do
where('EXISTS (?)', Ci::JobArtifact.select(1).where('ci_builds.id = ci_job_artifacts.job_id').trace) with_existing_job_artifacts(Ci::JobArtifact.trace)
end end
scope :without_archived_trace, ->() do scope :without_archived_trace, ->() do
...@@ -77,10 +81,12 @@ module Ci ...@@ -77,10 +81,12 @@ module Ci
end end
scope :with_test_reports, ->() do scope :with_test_reports, ->() do
includes(:job_artifacts_junit) # Prevent N+1 problem when iterating each ci_job_artifact row with_existing_job_artifacts(Ci::JobArtifact.test_reports)
.where('EXISTS (?)', Ci::JobArtifact.select(1).where('ci_builds.id = ci_job_artifacts.job_id').test_reports) .eager_load_job_artifacts
end end
scope :eager_load_job_artifacts, -> { includes(:job_artifacts) }
scope :with_artifacts_stored_locally, -> { with_artifacts_archive.where(artifacts_file_store: [nil, LegacyArtifactUploader::Store::LOCAL]) } scope :with_artifacts_stored_locally, -> { with_artifacts_archive.where(artifacts_file_store: [nil, LegacyArtifactUploader::Store::LOCAL]) }
scope :with_archived_trace_stored_locally, -> { with_archived_trace.where(artifacts_file_store: [nil, LegacyArtifactUploader::Store::LOCAL]) } scope :with_archived_trace_stored_locally, -> { with_archived_trace.where(artifacts_file_store: [nil, LegacyArtifactUploader::Store::LOCAL]) }
scope :with_artifacts_not_expired, ->() { with_artifacts_archive.where('artifacts_expire_at IS NULL OR artifacts_expire_at > ?', Time.now) } scope :with_artifacts_not_expired, ->() { with_artifacts_archive.where('artifacts_expire_at IS NULL OR artifacts_expire_at > ?', Time.now) }
...@@ -404,8 +410,8 @@ module Ci ...@@ -404,8 +410,8 @@ module Ci
trace.exist? trace.exist?
end end
def has_test_reports? def has_job_artifacts?
job_artifacts.test_reports.any? job_artifacts.any?
end end
def has_old_trace? def has_old_trace?
...@@ -469,28 +475,23 @@ module Ci ...@@ -469,28 +475,23 @@ module Ci
end end
end end
def erase_artifacts! # and use that for `ExpireBuildInstanceArtifactsWorker`?
remove_artifacts_file! def erase_erasable_artifacts!
remove_artifacts_metadata! job_artifacts.erasable.destroy_all # rubocop: disable DestroyAll
save erase_old_artifacts!
end
def erase_test_reports!
# TODO: Use fast_destroy_all in the context of https://gitlab.com/gitlab-org/gitlab-ce/issues/35240
job_artifacts_junit&.destroy
end end
def erase(opts = {}) def erase(opts = {})
return false unless erasable? return false unless erasable?
erase_artifacts! job_artifacts.destroy_all # rubocop: disable DestroyAll
erase_test_reports! erase_old_artifacts!
erase_trace! erase_trace!
update_erased!(opts[:erased_by]) update_erased!(opts[:erased_by])
end end
def erasable? def erasable?
complete? && (artifacts? || has_test_reports? || has_trace?) complete? && (artifacts? || has_job_artifacts? || has_trace?)
end end
def erased? def erased?
...@@ -648,8 +649,8 @@ module Ci ...@@ -648,8 +649,8 @@ module Ci
def collect_test_reports!(test_reports) def collect_test_reports!(test_reports)
test_reports.get_suite(group_name).tap do |test_suite| test_reports.get_suite(group_name).tap do |test_suite|
each_test_report do |file_type, blob| each_report(Ci::JobArtifact::TEST_REPORT_FILE_TYPES) do |file_type, blob|
Gitlab::Ci::Parsers.fabricate!(file_type).parse!(blob, test_suite) Gitlab::Ci::Parsers::Test.fabricate!(file_type).parse!(blob, test_suite)
end end
end end
end end
...@@ -669,6 +670,13 @@ module Ci ...@@ -669,6 +670,13 @@ module Ci
private private
def erase_old_artifacts!
# TODO: To be removed once we get rid of
remove_artifacts_file!
remove_artifacts_metadata!
save
end
def successful_deployment_status def successful_deployment_status
if success? && last_deployment&.last? if success? && last_deployment&.last?
return :last return :last
...@@ -679,14 +687,19 @@ module Ci ...@@ -679,14 +687,19 @@ module Ci
:creating :creating
end end
def each_test_report def each_report(report_types)
Ci::JobArtifact::TEST_REPORT_FILE_TYPES.each do |file_type| job_artifacts_for_types(report_types).each do |report_artifact|
public_send("job_artifacts_#{file_type}").each_blob do |blob| # rubocop:disable GitlabSecurity/PublicSend report_artifact.each_blob do |blob|
yield file_type, blob yield report_artifact.file_type, blob
end end
end end
end end
def job_artifacts_for_types(report_types)
# Use select to leverage cached associations and avoid N+1 queries
job_artifacts.select { |artifact| artifact.file_type.in?(report_types) }
end
def update_artifacts_size def update_artifacts_size
self.artifacts_size = legacy_artifacts_file&.size self.artifacts_size = legacy_artifacts_file&.size
end end
......
...@@ -9,8 +9,28 @@ module Ci ...@@ -9,8 +9,28 @@ module Ci
NotSupportedAdapterError = Class.new(StandardError) NotSupportedAdapterError = Class.new(StandardError)
TEST_REPORT_FILE_TYPES = %w[junit].freeze TEST_REPORT_FILE_TYPES = %w[junit].freeze
DEFAULT_FILE_NAMES = { junit: 'junit.xml' }.freeze NON_ERASABLE_FILE_TYPES = %w[trace].freeze
TYPE_AND_FORMAT_PAIRS = { archive: :zip, metadata: :gzip, trace: :raw, junit: :gzip }.freeze DEFAULT_FILE_NAMES = {
archive: nil,
metadata: nil,
trace: nil,
junit: 'junit.xml',
sast: 'gl-sast-report.json',
dependency_scanning: 'gl-dependency-scanning-report.json',
container_scanning: 'gl-container-scanning-report.json',
dast: 'gl-dast-report.json'
}.freeze
TYPE_AND_FORMAT_PAIRS = {
archive: :zip,
metadata: :gzip,
trace: :raw,
junit: :gzip,
sast: :gzip,
dependency_scanning: :gzip,
container_scanning: :gzip,
dast: :gzip
}.freeze
belongs_to :project belongs_to :project
belongs_to :job, class_name: "Ci::Build", foreign_key: :job_id belongs_to :job, class_name: "Ci::Build", foreign_key: :job_id
...@@ -27,8 +47,18 @@ module Ci ...@@ -27,8 +47,18 @@ module Ci
scope :with_files_stored_locally, -> { where(file_store: [nil, ::JobArtifactUploader::Store::LOCAL]) } scope :with_files_stored_locally, -> { where(file_store: [nil, ::JobArtifactUploader::Store::LOCAL]) }
scope :with_file_types, -> (file_types) do
types = self.file_types.select { |file_type| file_types.include?(file_type) }.values
where(file_type: types)
end
scope :test_reports, -> do scope :test_reports, -> do
types = self.file_types.select { |file_type| TEST_REPORT_FILE_TYPES.include?(file_type) }.values with_file_types(TEST_REPORT_FILE_TYPES)
end
scope :erasable, -> do
types = self.file_types.reject { |file_type| NON_ERASABLE_FILE_TYPES.include?(file_type) }.values
where(file_type: types) where(file_type: types)
end end
...@@ -39,7 +69,11 @@ module Ci ...@@ -39,7 +69,11 @@ module Ci
archive: 1, archive: 1,
metadata: 2, metadata: 2,
trace: 3, trace: 3,
junit: 4 junit: 4,
sast: 5, ## EE-specific
dependency_scanning: 6, ## EE-specific
container_scanning: 7, ## EE-specific
dast: 8 ## EE-specific
} }
enum file_format: { enum file_format: {
......
...@@ -13,7 +13,7 @@ class ExpireBuildInstanceArtifactsWorker ...@@ -13,7 +13,7 @@ class ExpireBuildInstanceArtifactsWorker
return unless build&.project && !build.project.pending_delete return unless build&.project && !build.project.pending_delete
Rails.logger.info "Removing artifacts for build #{build.id}..." Rails.logger.info "Removing artifacts for build #{build.id}..."
build.erase_artifacts! build.erase_erasable_artifacts!
end end
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
end end
---
title: Extend reports feature to support Security Products
merge_request: 21892
author:
type: added
# frozen_string_literal: true
module Gitlab module Gitlab
module Ci module Ci
class Config class Config
...@@ -9,7 +11,7 @@ module Gitlab ...@@ -9,7 +11,7 @@ module Gitlab
include Validatable include Validatable
include Attributable include Attributable
ALLOWED_KEYS = %i[junit].freeze ALLOWED_KEYS = %i[junit sast dependency_scanning container_scanning dast].freeze
attributes ALLOWED_KEYS attributes ALLOWED_KEYS
...@@ -19,6 +21,10 @@ module Gitlab ...@@ -19,6 +21,10 @@ module Gitlab
with_options allow_nil: true do with_options allow_nil: true do
validates :junit, array_of_strings_or_string: true validates :junit, array_of_strings_or_string: true
validates :sast, array_of_strings_or_string: true
validates :dependency_scanning, array_of_strings_or_string: true
validates :container_scanning, array_of_strings_or_string: true
validates :dast, array_of_strings_or_string: true
end end
end end
......
module Gitlab
module Ci
module Parsers
def self.fabricate!(file_type)
"Gitlab::Ci::Parsers::#{file_type.classify}".constantize.new
end
end
end
end
module Gitlab
module Ci
module Parsers
class Junit
JunitParserError = Class.new(StandardError)
def parse!(xml_data, test_suite)
root = Hash.from_xml(xml_data)
all_cases(root) do |test_case|
test_case = create_test_case(test_case)
test_suite.add_test_case(test_case)
end
rescue REXML::ParseException => e
raise JunitParserError, "XML parsing failed: #{e.message}"
rescue => e
raise JunitParserError, "JUnit parsing failed: #{e.message}"
end
private
def all_cases(root, parent = nil, &blk)
return unless root.present?
[root].flatten.compact.map do |node|
next unless node.is_a?(Hash)
# we allow only one top-level 'testsuites'
all_cases(node['testsuites'], root, &blk) unless parent
# we require at least one level of testsuites or testsuite
each_case(node['testcase'], &blk) if parent
# we allow multiple nested 'testsuite' (eg. PHPUnit)
all_cases(node['testsuite'], root, &blk)
end
end
def each_case(testcase, &blk)
return unless testcase.present?
[testcase].flatten.compact.map(&blk)
end
def create_test_case(data)
if data['failure']
status = ::Gitlab::Ci::Reports::TestCase::STATUS_FAILED
system_output = data['failure']
else
status = ::Gitlab::Ci::Reports::TestCase::STATUS_SUCCESS
system_output = nil
end
::Gitlab::Ci::Reports::TestCase.new(
classname: data['classname'],
name: data['name'],
file: data['file'],
execution_time: data['time'],
status: status,
system_output: system_output
)
end
end
end
end
end
# frozen_string_literal: true
module Gitlab
module Ci
module Parsers
module Test
ParserNotFoundError = Class.new(StandardError)
PARSERS = {
junit: ::Gitlab::Ci::Parsers::Test::Junit
}.freeze
def self.fabricate!(file_type)
PARSERS.fetch(file_type.to_sym).new
rescue KeyError
raise ParserNotFoundError, "Cannot find any parser matching file type '#{file_type}'"
end
end
end
end
end
# frozen_string_literal: true
module Gitlab
module Ci
module Parsers
module Test
class Junit
JunitParserError = Class.new(StandardError)
def parse!(xml_data, test_suite)
root = Hash.from_xml(xml_data)
all_cases(root) do |test_case|
test_case = create_test_case(test_case)
test_suite.add_test_case(test_case)
end
rescue REXML::ParseException => e
raise JunitParserError, "XML parsing failed: #{e.message}"
rescue => e
raise JunitParserError, "JUnit parsing failed: #{e.message}"
end
private
def all_cases(root, parent = nil, &blk)
return unless root.present?
[root].flatten.compact.map do |node|
next unless node.is_a?(Hash)
# we allow only one top-level 'testsuites'
all_cases(node['testsuites'], root, &blk) unless parent
# we require at least one level of testsuites or testsuite
each_case(node['testcase'], &blk) if parent
# we allow multiple nested 'testsuite' (eg. PHPUnit)
all_cases(node['testsuite'], root, &blk)
end
end
def each_case(testcase, &blk)
return unless testcase.present?
[testcase].flatten.compact.map(&blk)
end
def create_test_case(data)
if data['failure']
status = ::Gitlab::Ci::Reports::TestCase::STATUS_FAILED
system_output = data['failure']
else
status = ::Gitlab::Ci::Reports::TestCase::STATUS_SUCCESS
system_output = nil
end
::Gitlab::Ci::Reports::TestCase.new(
classname: data['classname'],
name: data['name'],
file: data['file'],
execution_time: data['time'],
status: status,
system_output: system_output
)
end
end
end
end
end
end
...@@ -14,6 +14,33 @@ FactoryBot.define do ...@@ -14,6 +14,33 @@ FactoryBot.define do
artifact.project ||= artifact.job.project artifact.project ||= artifact.job.project
end end
trait :raw do
file_format :raw
after(:build) do |artifact, _|
artifact.file = fixture_file_upload(
Rails.root.join('spec/fixtures/trace/sample_trace'), 'text/plain')
end
end
trait :zip do
file_format :zip
after(:build) do |artifact, _|
artifact.file = fixture_file_upload(
Rails.root.join('spec/fixtures/ci_build_artifacts.zip'), 'application/zip')
end
end
trait :gzip do
file_format :gzip
after(:build) do |artifact, _|
artifact.file = fixture_file_upload(
Rails.root.join('spec/fixtures/ci_build_artifacts_metadata.gz'), 'application/x-gzip')
end
end
trait :archive do trait :archive do
file_type :archive file_type :archive
file_format :zip file_format :zip
......
...@@ -3,27 +3,53 @@ require 'spec_helper' ...@@ -3,27 +3,53 @@ require 'spec_helper'
describe Gitlab::Ci::Config::Entry::Reports do describe Gitlab::Ci::Config::Entry::Reports do
let(:entry) { described_class.new(config) } let(:entry) { described_class.new(config) }
describe 'validates ALLOWED_KEYS' do
let(:artifact_file_types) { Ci::JobArtifact.file_types }
described_class::ALLOWED_KEYS.each do |keyword, _|
it "expects #{keyword} to be an artifact file_type" do
expect(artifact_file_types).to include(keyword)
end
end
end
describe 'validation' do describe 'validation' do
context 'when entry config value is correct' do context 'when entry config value is correct' do
let(:config) { { junit: %w[junit.xml] } } using RSpec::Parameterized::TableSyntax
describe '#value' do shared_examples 'a valid entry' do |keyword, file|
it 'returns artifacs configuration' do describe '#value' do
expect(entry.value).to eq config it 'returns artifacs configuration' do
expect(entry.value).to eq({ "#{keyword}": [file] } )
end
end end
end
describe '#valid?' do describe '#valid?' do
it 'is valid' do it 'is valid' do
expect(entry).to be_valid expect(entry).to be_valid
end
end end
end end
context 'when value is not array' do where(:keyword, :file) do
let(:config) { { junit: 'junit.xml' } } :junit | 'junit.xml'
:sast | 'gl-sast-report.json'
:dependency_scanning | 'gl-dependency-scanning-report.json'
:container_scanning | 'gl-container-scanning-report.json'
:dast | 'gl-dast-report.json'
end
with_them do
context 'when value is an array' do
let(:config) { { "#{keyword}": [file] } }
it 'converts to array' do it_behaves_like 'a valid entry', params[:keyword], params[:file]
expect(entry.value).to eq({ junit: ['junit.xml'] } ) end
context 'when value is not array' do
let(:config) { { "#{keyword}": file } }
it_behaves_like 'a valid entry', params[:keyword], params[:file]
end end
end end
end end
...@@ -31,11 +57,13 @@ describe Gitlab::Ci::Config::Entry::Reports do ...@@ -31,11 +57,13 @@ describe Gitlab::Ci::Config::Entry::Reports do
context 'when entry value is not correct' do context 'when entry value is not correct' do
describe '#errors' do describe '#errors' do
context 'when value of attribute is invalid' do context 'when value of attribute is invalid' do
let(:config) { { junit: 10 } } where(key: described_class::ALLOWED_KEYS) do
let(:config) { { "#{key}": 10 } }
it 'reports error' do it 'reports error' do
expect(entry.errors) expect(entry.errors)
.to include 'reports junit should be an array of strings or a string' .to include "reports #{key} should be an array of strings or a string"
end
end end
end end
......
require 'fast_spec_helper' require 'fast_spec_helper'
describe Gitlab::Ci::Parsers::Junit do describe Gitlab::Ci::Parsers::Test::Junit do
describe '#parse!' do describe '#parse!' do
subject { described_class.new.parse!(junit, test_suite) } subject { described_class.new.parse!(junit, test_suite) }
......
require 'spec_helper' require 'spec_helper'
describe Gitlab::Ci::Parsers do describe Gitlab::Ci::Parsers::Test do
describe '.fabricate!' do describe '.fabricate!' do
subject { described_class.fabricate!(file_type) } subject { described_class.fabricate!(file_type) }
...@@ -16,7 +16,7 @@ describe Gitlab::Ci::Parsers do ...@@ -16,7 +16,7 @@ describe Gitlab::Ci::Parsers do
let(:file_type) { 'undefined' } let(:file_type) { 'undefined' }
it 'raises an error' do it 'raises an error' do
expect { subject }.to raise_error(NameError) expect { subject }.to raise_error(Gitlab::Ci::Parsers::Test::ParserNotFoundError)
end end
end end
end end
......
...@@ -176,9 +176,7 @@ describe Ci::Build do ...@@ -176,9 +176,7 @@ describe Ci::Build do
it 'does not execute a query for selecting job artifact one by one' do it 'does not execute a query for selecting job artifact one by one' do
recorded = ActiveRecord::QueryRecorder.new do recorded = ActiveRecord::QueryRecorder.new do
subject.each do |build| subject.each do |build|
Ci::JobArtifact::TEST_REPORT_FILE_TYPES.each do |file_type| build.job_artifacts.map { |a| a.file.exists? }
build.public_send("job_artifacts_#{file_type}").file.exists?
end
end end
end end
...@@ -550,44 +548,22 @@ describe Ci::Build do ...@@ -550,44 +548,22 @@ describe Ci::Build do
end end
end end
describe '#has_test_reports?' do describe '#has_job_artifacts?' do
subject { build.has_test_reports? } subject { build.has_job_artifacts? }
context 'when build has a test report' do context 'when build has a job artifact' do
let(:build) { create(:ci_build, :test_reports) } let(:build) { create(:ci_build, :artifacts) }
it { is_expected.to be_truthy } it { is_expected.to be_truthy }
end end
context 'when build does not have test reports' do context 'when build does not have job artifacts' do
let(:build) { create(:ci_build, :artifacts) } let(:build) { create(:ci_build, :legacy_artifacts) }
it { is_expected.to be_falsy } it { is_expected.to be_falsy }
end end
end end
describe '#erase_test_reports!' do
subject { build.erase_test_reports! }
context 'when build has a test report' do
let!(:build) { create(:ci_build, :test_reports) }
it 'removes a test report' do
subject
expect(build.has_test_reports?).to be_falsy
end
end
context 'when build does not have test reports' do
let!(:build) { create(:ci_build, :artifacts) }
it 'does not erase anything' do
expect { subject }.not_to change { Ci::JobArtifact.count }
end
end
end
describe '#has_old_trace?' do describe '#has_old_trace?' do
subject { build.has_old_trace? } subject { build.has_old_trace? }
...@@ -850,8 +826,8 @@ describe Ci::Build do ...@@ -850,8 +826,8 @@ describe Ci::Build do
expect(build.artifacts_metadata.exists?).to be_falsy expect(build.artifacts_metadata.exists?).to be_falsy
end end
it 'removes test reports' do it 'removes all job_artifacts' do
expect(build.job_artifacts.test_reports.count).to eq(0) expect(build.job_artifacts.count).to eq(0)
end end
it 'erases build trace in trace file' do it 'erases build trace in trace file' do
...@@ -1022,6 +998,32 @@ describe Ci::Build do ...@@ -1022,6 +998,32 @@ describe Ci::Build do
end end
end end
describe '#erase_erasable_artifacts!' do
let!(:build) { create(:ci_build, :success) }
subject { build.erase_erasable_artifacts! }
before do
Ci::JobArtifact.file_types.keys.each do |file_type|
create(:ci_job_artifact, job: build, file_type: file_type, file_format: Ci::JobArtifact::TYPE_AND_FORMAT_PAIRS[file_type.to_sym])
end
end
it "erases erasable artifacts" do
subject
expect(build.job_artifacts.erasable).to be_empty
end
it "keeps non erasable artifacts" do
subject
Ci::JobArtifact::NON_ERASABLE_FILE_TYPES.each do |file_type|
expect(build.send("job_artifacts_#{file_type}")).not_to be_nil
end
end
end
describe '#first_pending' do describe '#first_pending' do
let!(:first) { create(:ci_build, pipeline: pipeline, status: 'pending', created_at: Date.yesterday) } let!(:first) { create(:ci_build, pipeline: pipeline, status: 'pending', created_at: Date.yesterday) }
let!(:second) { create(:ci_build, pipeline: pipeline, status: 'pending') } let!(:second) { create(:ci_build, pipeline: pipeline, status: 'pending') }
...@@ -2844,16 +2846,10 @@ describe Ci::Build do ...@@ -2844,16 +2846,10 @@ describe Ci::Build do
end end
it 'raises an error' do it 'raises an error' do
expect { subject }.to raise_error(Gitlab::Ci::Parsers::Junit::JunitParserError) expect { subject }.to raise_error(Gitlab::Ci::Parsers::Test::Junit::JunitParserError)
end end
end end
end end
context 'when build does not have test reports' do
it 'raises an error' do
expect { subject }.to raise_error(NoMethodError)
end
end
end end
describe '#artifacts_metadata_entry' do describe '#artifacts_metadata_entry' do
......
...@@ -31,6 +31,22 @@ describe Ci::JobArtifact do ...@@ -31,6 +31,22 @@ describe Ci::JobArtifact do
end end
end end
describe '.erasable' do
subject { described_class.erasable }
context 'when there is am erasable artifact' do
let!(:artifact) { create(:ci_job_artifact, :junit) }
it { is_expected.to eq([artifact]) }
end
context 'when there are no erasable artifacts' do
let!(:artifact) { create(:ci_job_artifact, :trace) }
it { is_expected.to be_empty }
end
end
describe 'callbacks' do describe 'callbacks' do
subject { create(:ci_job_artifact, :archive) } subject { create(:ci_job_artifact, :archive) }
...@@ -106,34 +122,46 @@ describe Ci::JobArtifact do ...@@ -106,34 +122,46 @@ describe Ci::JobArtifact do
describe 'validates file format' do describe 'validates file format' do
subject { artifact } subject { artifact }
context 'when archive type with zip format' do described_class::TYPE_AND_FORMAT_PAIRS.except(:trace).each do |file_type, file_format|
let(:artifact) { build(:ci_job_artifact, :archive, file_format: :zip) } context "when #{file_type} type with #{file_format} format" do
let(:artifact) { build(:ci_job_artifact, file_type: file_type, file_format: file_format) }
it { is_expected.to be_valid } it { is_expected.to be_valid }
end end
context 'when archive type with gzip format' do context "when #{file_type} type without format specification" do
let(:artifact) { build(:ci_job_artifact, :archive, file_format: :gzip) } let(:artifact) { build(:ci_job_artifact, file_type: file_type, file_format: nil) }
it { is_expected.not_to be_valid } it { is_expected.not_to be_valid }
end end
context 'when archive type without format specification' do context "when #{file_type} type with other formats" do
let(:artifact) { build(:ci_job_artifact, :archive, file_format: nil) } described_class.file_formats.except(file_format).values.each do |other_format|
let(:artifact) { build(:ci_job_artifact, file_type: file_type, file_format: other_format) }
it { is_expected.not_to be_valid } it { is_expected.not_to be_valid }
end
end
end end
end
context 'when junit type with zip format' do describe 'validates DEFAULT_FILE_NAMES' do
let(:artifact) { build(:ci_job_artifact, :junit, file_format: :zip) } subject { described_class::DEFAULT_FILE_NAMES }
it { is_expected.not_to be_valid } described_class.file_types.each do |file_type, _|
it "expects #{file_type} to be included" do
is_expected.to include(file_type.to_sym)
end
end end
end
context 'when junit type with gzip format' do describe 'validates TYPE_AND_FORMAT_PAIRS' do
let(:artifact) { build(:ci_job_artifact, :junit, file_format: :gzip) } subject { described_class::TYPE_AND_FORMAT_PAIRS }
it { is_expected.to be_valid } described_class.file_types.each do |file_type, _|
it "expects #{file_type} to be included" do
expect(described_class.file_formats).to include(subject[file_type.to_sym])
end
end end
end end
......
...@@ -3,7 +3,6 @@ require 'spec_helper' ...@@ -3,7 +3,6 @@ require 'spec_helper'
describe Ci::BuildRunnerPresenter do describe Ci::BuildRunnerPresenter do
let(:presenter) { described_class.new(build) } let(:presenter) { described_class.new(build) }
let(:archive) { { paths: ['sample.txt'] } } let(:archive) { { paths: ['sample.txt'] } }
let(:junit) { { junit: ['junit.xml'] } }
let(:archive_expectation) do let(:archive_expectation) do
{ {
...@@ -14,16 +13,6 @@ describe Ci::BuildRunnerPresenter do ...@@ -14,16 +13,6 @@ describe Ci::BuildRunnerPresenter do
} }
end end
let(:junit_expectation) do
{
name: 'junit.xml',
artifact_type: :junit,
artifact_format: :gzip,
paths: ['junit.xml'],
when: 'always'
}
end
describe '#artifacts' do describe '#artifacts' do
context "when option contains archive-type artifacts" do context "when option contains archive-type artifacts" do
let(:build) { create(:ci_build, options: { artifacts: archive } ) } let(:build) { create(:ci_build, options: { artifacts: archive } ) }
...@@ -49,20 +38,44 @@ describe Ci::BuildRunnerPresenter do ...@@ -49,20 +38,44 @@ describe Ci::BuildRunnerPresenter do
end end
end end
context "when option has 'junit' keyword" do context "with reports" do
let(:build) { create(:ci_build, options: { artifacts: { reports: junit } } ) } Ci::JobArtifact::DEFAULT_FILE_NAMES.each do |file_type, filename|
let(:report) { { "#{file_type}": [filename] } }
let(:build) { create(:ci_build, options: { artifacts: { reports: report } } ) }
let(:report_expectation) do
{
name: filename,
artifact_type: :"#{file_type}",
artifact_format: :gzip,
paths: [filename],
when: 'always'
}
end
it 'presents correct hash' do it 'presents correct hash' do
expect(presenter.artifacts.first).to include(junit_expectation) expect(presenter.artifacts.first).to include(report_expectation)
end
end end
end end
context "when option has both archive and reports specification" do context "when option has both archive and reports specification" do
let(:build) { create(:ci_build, options: { script: 'echo', artifacts: { **archive, reports: junit } } ) } let(:report) { { junit: ['junit.xml'] } }
let(:build) { create(:ci_build, options: { script: 'echo', artifacts: { **archive, reports: report } } ) }
let(:report_expectation) do
{
name: 'junit.xml',
artifact_type: :junit,
artifact_format: :gzip,
paths: ['junit.xml'],
when: 'always'
}
end
it 'presents correct hash' do it 'presents correct hash' do
expect(presenter.artifacts.first).to include(archive_expectation) expect(presenter.artifacts.first).to include(archive_expectation)
expect(presenter.artifacts.second).to include(junit_expectation) expect(presenter.artifacts.second).to include(report_expectation)
end end
context "when archive specifies 'expire_in' keyword" do context "when archive specifies 'expire_in' keyword" do
...@@ -70,7 +83,7 @@ describe Ci::BuildRunnerPresenter do ...@@ -70,7 +83,7 @@ describe Ci::BuildRunnerPresenter do
it 'inherits expire_in from archive' do it 'inherits expire_in from archive' do
expect(presenter.artifacts.first).to include({ **archive_expectation, expire_in: '3 mins 4 sec' }) expect(presenter.artifacts.first).to include({ **archive_expectation, expire_in: '3 mins 4 sec' })
expect(presenter.artifacts.second).to include({ **junit_expectation, expire_in: '3 mins 4 sec' }) expect(presenter.artifacts.second).to include({ **report_expectation, expire_in: '3 mins 4 sec' })
end end
end end
end end
......
...@@ -721,7 +721,7 @@ describe API::Jobs do ...@@ -721,7 +721,7 @@ describe API::Jobs do
expect(job.trace.exist?).to be_falsy expect(job.trace.exist?).to be_falsy
expect(job.artifacts_file.exists?).to be_falsy expect(job.artifacts_file.exists?).to be_falsy
expect(job.artifacts_metadata.exists?).to be_falsy expect(job.artifacts_metadata.exists?).to be_falsy
expect(job.has_test_reports?).to be_falsy expect(job.has_job_artifacts?).to be_falsy
end end
it 'updates job' do it 'updates job' do
......
...@@ -24,7 +24,9 @@ describe Ci::RetryBuildService do ...@@ -24,7 +24,9 @@ describe Ci::RetryBuildService do
artifacts_file artifacts_metadata artifacts_size created_at artifacts_file artifacts_metadata artifacts_size created_at
updated_at started_at finished_at queued_at erased_by updated_at started_at finished_at queued_at erased_by
erased_at auto_canceled_by job_artifacts job_artifacts_archive erased_at auto_canceled_by job_artifacts job_artifacts_archive
job_artifacts_metadata job_artifacts_trace job_artifacts_junit].freeze job_artifacts_metadata job_artifacts_trace job_artifacts_junit
job_artifacts_sast job_artifacts_dependency_scanning
job_artifacts_container_scanning job_artifacts_dast].freeze
IGNORE_ACCESSORS = IGNORE_ACCESSORS =
%i[type lock_version target_url base_tags trace_sections %i[type lock_version target_url base_tags trace_sections
...@@ -38,9 +40,8 @@ describe Ci::RetryBuildService do ...@@ -38,9 +40,8 @@ describe Ci::RetryBuildService do
let(:another_pipeline) { create(:ci_empty_pipeline, project: project) } let(:another_pipeline) { create(:ci_empty_pipeline, project: project) }
let(:build) do let(:build) do
create(:ci_build, :failed, :artifacts, :test_reports, :expired, :erased, create(:ci_build, :failed, :expired, :erased, :queued, :coverage, :tags,
:queued, :coverage, :tags, :allowed_to_fail, :on_tag, :allowed_to_fail, :on_tag, :triggered, :teardown_environment,
:triggered, :trace_artifact, :teardown_environment,
description: 'my-job', stage: 'test', stage_id: stage.id, description: 'my-job', stage: 'test', stage_id: stage.id,
pipeline: pipeline, auto_canceled_by: another_pipeline) pipeline: pipeline, auto_canceled_by: another_pipeline)
end end
...@@ -50,6 +51,15 @@ describe Ci::RetryBuildService do ...@@ -50,6 +51,15 @@ describe Ci::RetryBuildService do
# can reset one of the fields when assigning another. We plan to deprecate # can reset one of the fields when assigning another. We plan to deprecate
# and remove legacy `stage` column in the future. # and remove legacy `stage` column in the future.
build.update(stage: 'test', stage_id: stage.id) build.update(stage: 'test', stage_id: stage.id)
# Make sure we have one instance for every possible job_artifact_X
# associations to check they are correctly rejected on build duplication.
Ci::JobArtifact::TYPE_AND_FORMAT_PAIRS.each do |file_type, file_format|
create(:ci_job_artifact, file_format,
file_type: file_type, job: build, expire_at: build.artifacts_expire_at)
end
build.reload
end end
describe 'clone accessors' do describe 'clone accessors' do
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment