Commit f2dd6a1c authored by Shinya Maeda's avatar Shinya Maeda

Merge branch 'mo-expose-attachment-to-test-report' into 'master'

Expose attachment_url to TestCaseEntity

See merge request gitlab-org/gitlab!27243
parents f56ec37a 9c5ac8f2
...@@ -873,7 +873,7 @@ module Ci ...@@ -873,7 +873,7 @@ 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_report(Ci::JobArtifact::TEST_REPORT_FILE_TYPES) 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.fabricate!(file_type).parse!(blob, test_suite, job: self)
end end
end end
end end
......
...@@ -813,7 +813,7 @@ module Ci ...@@ -813,7 +813,7 @@ module Ci
def test_reports def test_reports
Gitlab::Ci::Reports::TestReports.new.tap do |test_reports| Gitlab::Ci::Reports::TestReports.new.tap do |test_reports|
builds.latest.with_reports(Ci::JobArtifact.test_reports).each do |build| builds.latest.with_reports(Ci::JobArtifact.test_reports).preload(:project).find_each do |build|
build.collect_test_reports!(test_reports) build.collect_test_reports!(test_reports)
end end
end end
......
# frozen_string_literal: true # frozen_string_literal: true
class TestCaseEntity < Grape::Entity class TestCaseEntity < Grape::Entity
include API::Helpers::RelatedResourcesHelpers
expose :status expose :status
expose :name expose :name
expose :classname expose :classname
expose :execution_time expose :execution_time
expose :system_output expose :system_output
expose :stack_trace expose :stack_trace
expose :attachment_url, if: -> (*) { can_read_screenshots? } do |test_case|
expose_url(test_case.attachment_url)
end
private
alias_method :test_case, :object
def can_read_screenshots?
Feature.enabled?(:junit_pipeline_screenshots_view, options[:project]) && test_case.has_attachment?
end
end end
...@@ -8,11 +8,11 @@ module Gitlab ...@@ -8,11 +8,11 @@ module Gitlab
JunitParserError = Class.new(Gitlab::Ci::Parsers::ParserError) JunitParserError = Class.new(Gitlab::Ci::Parsers::ParserError)
ATTACHMENT_TAG_REGEX = /\[\[ATTACHMENT\|(?<path>.+?)\]\]/.freeze ATTACHMENT_TAG_REGEX = /\[\[ATTACHMENT\|(?<path>.+?)\]\]/.freeze
def parse!(xml_data, test_suite) def parse!(xml_data, test_suite, **args)
root = Hash.from_xml(xml_data) root = Hash.from_xml(xml_data)
all_cases(root) do |test_case| all_cases(root) do |test_case|
test_case = create_test_case(test_case) test_case = create_test_case(test_case, args)
test_suite.add_test_case(test_case) test_suite.add_test_case(test_case)
end end
rescue Nokogiri::XML::SyntaxError rescue Nokogiri::XML::SyntaxError
...@@ -46,7 +46,7 @@ module Gitlab ...@@ -46,7 +46,7 @@ module Gitlab
[testcase].flatten.compact.map(&blk) [testcase].flatten.compact.map(&blk)
end end
def create_test_case(data) def create_test_case(data, args)
if data['failure'] if data['failure']
status = ::Gitlab::Ci::Reports::TestCase::STATUS_FAILED status = ::Gitlab::Ci::Reports::TestCase::STATUS_FAILED
system_output = data['failure'] system_output = data['failure']
...@@ -66,7 +66,8 @@ module Gitlab ...@@ -66,7 +66,8 @@ module Gitlab
execution_time: data['time'], execution_time: data['time'],
status: status, status: status,
system_output: system_output, system_output: system_output,
attachment: attachment attachment: attachment,
job: args.fetch(:job)
) )
end end
......
...@@ -10,9 +10,10 @@ module Gitlab ...@@ -10,9 +10,10 @@ module Gitlab
STATUS_ERROR = 'error' STATUS_ERROR = 'error'
STATUS_TYPES = [STATUS_SUCCESS, STATUS_FAILED, STATUS_SKIPPED, STATUS_ERROR].freeze STATUS_TYPES = [STATUS_SUCCESS, STATUS_FAILED, STATUS_SKIPPED, STATUS_ERROR].freeze
attr_reader :name, :classname, :execution_time, :status, :file, :system_output, :stack_trace, :key, :attachment attr_reader :name, :classname, :execution_time, :status, :file, :system_output, :stack_trace, :key, :attachment, :job
def initialize(name:, classname:, execution_time:, status:, file: nil, system_output: nil, stack_trace: nil, attachment: nil) # rubocop: disable Metrics/ParameterLists
def initialize(name:, classname:, execution_time:, status:, file: nil, system_output: nil, stack_trace: nil, attachment: nil, job: nil)
@name = name @name = name
@classname = classname @classname = classname
@file = file @file = file
...@@ -22,12 +23,24 @@ module Gitlab ...@@ -22,12 +23,24 @@ module Gitlab
@stack_trace = stack_trace @stack_trace = stack_trace
@key = sanitize_key_name("#{classname}_#{name}") @key = sanitize_key_name("#{classname}_#{name}")
@attachment = attachment @attachment = attachment
@job = job
end end
# rubocop: enable Metrics/ParameterLists
def has_attachment? def has_attachment?
attachment.present? attachment.present?
end end
def attachment_url
return unless has_attachment?
Rails.application.routes.url_helpers.file_project_job_artifacts_path(
job.project,
job.id,
attachment
)
end
private private
def sanitize_key_name(key) def sanitize_key_name(key)
......
...@@ -705,13 +705,45 @@ describe Projects::PipelinesController do ...@@ -705,13 +705,45 @@ describe Projects::PipelinesController do
end end
describe 'GET test_report.json' do describe 'GET test_report.json' do
subject(:get_test_report_json) do let(:pipeline) { create(:ci_pipeline, project: project) }
get :test_report, params: {
namespace_id: project.namespace, context 'with attachments' do
project_id: project, let(:blob) do
id: pipeline.id <<~EOF
}, <testsuites>
format: :json <testsuite>
<testcase classname='Calculator' name='sumTest1' time='0.01'>
<failure>Some failure</failure>
<system-out>[[ATTACHMENT|some/path.png]]</system-out>
</testcase>
</testsuite>
</testsuites>
EOF
end
before do
allow_any_instance_of(Ci::JobArtifact).to receive(:each_blob).and_yield(blob)
end
it 'does not have N+1 problem with attachments' do
get_test_report_json
create(:ci_build, name: 'rspec', pipeline: pipeline).tap do |build|
create(:ci_job_artifact, :junit, job: build)
end
clear_controller_memoization
control_count = ActiveRecord::QueryRecorder.new { get_test_report_json }.count
create(:ci_build, name: 'karma', pipeline: pipeline).tap do |build|
create(:ci_job_artifact, :junit, job: build)
end
clear_controller_memoization
expect { get_test_report_json }.not_to exceed_query_limit(control_count)
end
end end
context 'when feature is enabled' do context 'when feature is enabled' do
...@@ -772,6 +804,20 @@ describe Projects::PipelinesController do ...@@ -772,6 +804,20 @@ describe Projects::PipelinesController do
expect(response.body).to be_empty expect(response.body).to be_empty
end end
end end
def get_test_report_json
get :test_report, params: {
namespace_id: project.namespace,
project_id: project,
id: pipeline.id
},
format: :json
end
def clear_controller_memoization
controller.clear_memoization(:pipeline_test_report)
controller.instance_variable_set(:@pipeline, nil)
end
end end
describe 'GET test_report_count.json' do describe 'GET test_report_count.json' do
......
...@@ -9,6 +9,7 @@ FactoryBot.define do ...@@ -9,6 +9,7 @@ FactoryBot.define do
status { "success" } status { "success" }
system_output { nil } system_output { nil }
attachment { nil } attachment { nil }
association :job, factory: :ci_build
trait :with_attachment do trait :with_attachment do
attachment { "some/path.png" } attachment { "some/path.png" }
...@@ -24,7 +25,8 @@ FactoryBot.define do ...@@ -24,7 +25,8 @@ FactoryBot.define do
execution_time: execution_time, execution_time: execution_time,
status: status, status: status,
system_output: system_output, system_output: system_output,
attachment: attachment attachment: attachment,
job: job
) )
end end
end end
......
...@@ -10,7 +10,8 @@ ...@@ -10,7 +10,8 @@
"classname": { "type": "string" }, "classname": { "type": "string" },
"execution_time": { "type": "float" }, "execution_time": { "type": "float" },
"system_output": { "type": ["string", "null"] }, "system_output": { "type": ["string", "null"] },
"stack_trace": { "type": ["string", "null"] } "stack_trace": { "type": ["string", "null"] },
"attachment_url": { "type": ["string", "null"] }
}, },
"additionalProperties": false "additionalProperties": false
} }
...@@ -4,10 +4,11 @@ require 'fast_spec_helper' ...@@ -4,10 +4,11 @@ require 'fast_spec_helper'
describe Gitlab::Ci::Parsers::Test::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, args) }
let(:test_suite) { Gitlab::Ci::Reports::TestSuite.new('rspec') } let(:test_suite) { Gitlab::Ci::Reports::TestSuite.new('rspec') }
let(:test_cases) { flattened_test_cases(test_suite) } let(:test_cases) { flattened_test_cases(test_suite) }
let(:args) { { job: { id: 1, project: "project" } } }
context 'when data is JUnit style XML' do context 'when data is JUnit style XML' do
context 'when there are no <testcases> in <testsuite>' do context 'when there are no <testcases> in <testsuite>' do
...@@ -205,7 +206,7 @@ describe Gitlab::Ci::Parsers::Test::Junit do ...@@ -205,7 +206,7 @@ describe Gitlab::Ci::Parsers::Test::Junit do
end end
end end
context 'when data contains an attachment tag' do context 'when attachment is specified in failed test case' do
let(:junit) do let(:junit) do
<<~EOF <<~EOF
<testsuites> <testsuites>
...@@ -219,11 +220,15 @@ describe Gitlab::Ci::Parsers::Test::Junit do ...@@ -219,11 +220,15 @@ describe Gitlab::Ci::Parsers::Test::Junit do
EOF EOF
end end
it 'add attachment to a test case' do it 'assigns correct attributes to the test case' do
expect { subject }.not_to raise_error expect { subject }.not_to raise_error
expect(test_cases[0].has_attachment?).to be_truthy expect(test_cases[0].has_attachment?).to be_truthy
expect(test_cases[0].attachment).to eq("some/path.png") expect(test_cases[0].attachment).to eq("some/path.png")
expect(test_cases[0].job).to be_present
expect(test_cases[0].job[:id]).to eq(1)
expect(test_cases[0].job[:project]).to eq("project")
end end
end end
......
...@@ -15,7 +15,8 @@ describe Gitlab::Ci::Reports::TestCase do ...@@ -15,7 +15,8 @@ describe Gitlab::Ci::Reports::TestCase do
file: 'spec/trace_spec.rb', file: 'spec/trace_spec.rb',
execution_time: 1.23, execution_time: 1.23,
status: described_class::STATUS_SUCCESS, status: described_class::STATUS_SUCCESS,
system_output: nil system_output: nil,
job: build(:ci_build)
} }
end end
...@@ -28,6 +29,7 @@ describe Gitlab::Ci::Reports::TestCase do ...@@ -28,6 +29,7 @@ describe Gitlab::Ci::Reports::TestCase do
expect(test_case.execution_time).to eq(1.23) expect(test_case.execution_time).to eq(1.23)
expect(test_case.status).to eq(described_class::STATUS_SUCCESS) expect(test_case.status).to eq(described_class::STATUS_SUCCESS)
expect(test_case.system_output).to be_nil expect(test_case.system_output).to be_nil
expect(test_case.job).to be_present
end end
end end
...@@ -99,6 +101,22 @@ describe Gitlab::Ci::Reports::TestCase do ...@@ -99,6 +101,22 @@ describe Gitlab::Ci::Reports::TestCase do
it '#has_attachment?' do it '#has_attachment?' do
expect(attachment_test_case.has_attachment?).to be_truthy expect(attachment_test_case.has_attachment?).to be_truthy
end end
it '#attachment_url' do
expect(attachment_test_case.attachment_url).to match(/file\/some\/path.png/)
end
end
context 'when attachment is missing' do
let(:test_case) { build(:test_case) }
it '#has_attachment?' do
expect(test_case.has_attachment?).to be_falsy
end
it '#attachment_url' do
expect(test_case.attachment_url).to be_nil
end
end end
end end
end end
...@@ -31,5 +31,49 @@ describe TestCaseEntity do ...@@ -31,5 +31,49 @@ describe TestCaseEntity do
expect(subject[:execution_time]).to eq(2.22) expect(subject[:execution_time]).to eq(2.22)
end end
end end
context 'when feature is enabled' do
before do
stub_feature_flags(junit_pipeline_screenshots_view: true)
end
context 'when attachment is present' do
let(:test_case) { build(:test_case, :with_attachment) }
it 'returns the attachment_url' do
expect(subject).to include(:attachment_url)
end
end
context 'when attachment is not present' do
let(:test_case) { build(:test_case) }
it 'returns a nil attachment_url' do
expect(subject[:attachment_url]).to be_nil
end
end
end
context 'when feature is disabled' do
before do
stub_feature_flags(junit_pipeline_screenshots_view: false)
end
context 'when attachment is present' do
let(:test_case) { build(:test_case, :with_attachment) }
it 'returns no attachment_url' do
expect(subject).not_to include(:attachment_url)
end
end
context 'when attachment is not present' do
let(:test_case) { build(:test_case) }
it 'returns no attachment_url' do
expect(subject).not_to include(:attachment_url)
end
end
end
end end
end end
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