Commit a47e4a01 authored by Erick Bajao's avatar Erick Bajao

Implement smart cobertura class path correction

As we parse the cobertura XML, based on the given project full path and
pipeline worktree paths, we will make some assumptions on how to
determine the correct path of filenames that are not relative to the
project root.
parent 2978c49f
......@@ -916,8 +916,20 @@ module Ci
end
def collect_coverage_reports!(coverage_report)
project_path, worktree_paths = if Feature.enabled?(:smart_cobertura_parser, project)
# If the flag is disabled, we intentionally pass nil
# for both project_path and worktree_paths to fallback
# to the non-smart behavior of the parser
[project.full_path, pipeline.all_worktree_paths]
end
each_report(Ci::JobArtifact::COVERAGE_REPORT_FILE_TYPES) do |file_type, blob|
Gitlab::Ci::Parsers.fabricate!(file_type).parse!(blob, coverage_report)
Gitlab::Ci::Parsers.fabricate!(file_type).parse!(
blob,
coverage_report,
project_path: project_path,
worktree_paths: worktree_paths
)
end
coverage_report
......
......@@ -975,7 +975,7 @@ module Ci
def coverage_reports
Gitlab::Ci::Reports::CoverageReports.new.tap do |coverage_reports|
latest_report_builds(Ci::JobArtifact.coverage_reports).each do |build|
latest_report_builds(Ci::JobArtifact.coverage_reports).includes(:project).find_each do |build|
build.collect_coverage_reports!(coverage_reports)
end
end
......
---
title: Implement smart cobertura class path correction
merge_request: 48048
author:
type: changed
---
name: smart_cobertura_parser
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/48048
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/284822
milestone: '13.7'
type: development
group: group::testing
default_enabled: false
......@@ -5,50 +5,113 @@ module Gitlab
module Parsers
module Coverage
class Cobertura
CoberturaParserError = Class.new(Gitlab::Ci::Parsers::ParserError)
InvalidXMLError = Class.new(Gitlab::Ci::Parsers::ParserError)
InvalidLineInformationError = Class.new(Gitlab::Ci::Parsers::ParserError)
def parse!(xml_data, coverage_report)
GO_SOURCE_PATTERN = '/usr/local/go/src'
MAX_SOURCES = 100
def parse!(xml_data, coverage_report, project_path: nil, worktree_paths: nil)
root = Hash.from_xml(xml_data)
parse_all(root, coverage_report)
context = {
project_path: project_path,
paths: worktree_paths&.to_set,
sources: []
}
parse_all(root, coverage_report, context)
rescue Nokogiri::XML::SyntaxError
raise CoberturaParserError, "XML parsing failed"
rescue
raise CoberturaParserError, "Cobertura parsing failed"
raise InvalidXMLError, "XML parsing failed"
end
private
def parse_all(root, coverage_report)
def parse_all(root, coverage_report, context)
return unless root.present?
root.each do |key, value|
parse_node(key, value, coverage_report)
parse_node(key, value, coverage_report, context)
end
end
def parse_node(key, value, coverage_report)
return if key == 'sources'
if key == 'class'
def parse_node(key, value, coverage_report, context)
if key == 'sources' && value['source'].present?
parse_sources(value['source'], context)
elsif key == 'package'
Array.wrap(value).each do |item|
parse_class(item, coverage_report)
parse_package(item, coverage_report, context)
end
elsif key == 'class'
# This means the cobertura XML does not have classes within package nodes.
# This is possible in some cases like in simple JS project structures
# running Jest.
Array.wrap(value).each do |item|
parse_class(item, coverage_report, context)
end
elsif value.is_a?(Hash)
parse_all(value, coverage_report)
parse_all(value, coverage_report, context)
elsif value.is_a?(Array)
value.each do |item|
parse_all(item, coverage_report)
parse_all(item, coverage_report, context)
end
end
end
def parse_sources(sources, context)
return unless context[:project_path] && context[:paths]
sources = Array.wrap(sources)
# TODO: Go cobertura has a different format with how their packages
# are included in the filename. So we can't rely on the sources.
# We'll deal with this later.
return if sources.include?(GO_SOURCE_PATTERN)
sources.each do |source|
source = build_source_path(source, context)
context[:sources] << source if source.present?
end
end
def build_source_path(source, context)
# | raw source | extracted |
# |-----------------------------|------------|
# | /builds/foo/test/SampleLib/ | SampleLib/ |
# | /builds/foo/test/something | something |
# | /builds/foo/test/ | nil |
# | /builds/foo/test | nil |
source.split("#{context[:project_path]}/", 2)[1]
end
def parse_package(package, coverage_report, context)
classes = package.dig('classes', 'class')
return unless classes.present?
matched_filenames = Array.wrap(classes).map do |item|
parse_class(item, coverage_report, context)
end
def parse_class(file, coverage_report)
# Remove these filenames from the paths to avoid conflict
# with other packages that may contain the same class filenames
remove_matched_filenames(matched_filenames, context)
end
def remove_matched_filenames(filenames, context)
return unless context[:paths]
filenames.each { |f| context[:paths].delete(f) }
end
def parse_class(file, coverage_report, context)
return unless file["filename"].present? && file["lines"].present?
parsed_lines = parse_lines(file["lines"])
filename = determine_filename(file["filename"], context)
coverage_report.add_file(filename, Hash[parsed_lines]) if filename
coverage_report.add_file(file["filename"], Hash[parsed_lines])
filename
end
def parse_lines(lines)
......@@ -58,6 +121,27 @@ module Gitlab
# Using `Integer()` here to raise exception on invalid values
[Integer(line["number"]), Integer(line["hits"])]
end
rescue
raise InvalidLineInformationError, "Line information had invalid values"
end
def determine_filename(filename, context)
return filename unless context[:sources].any?
full_filename = nil
context[:sources].each_with_index do |source, index|
break if index >= MAX_SOURCES
break if full_filename = check_source(source, filename, context)
end
full_filename
end
def check_source(source, filename, context)
full_path = File.join(source, filename)
return full_path if context[:paths].include?(full_path)
end
end
end
......
......@@ -229,6 +229,16 @@ FactoryBot.define do
end
end
trait :coverage_with_paths_not_relative_to_project_root do
file_type { :cobertura }
file_format { :gzip }
after(:build) do |artifact, evaluator|
artifact.file = fixture_file_upload(
Rails.root.join('spec/fixtures/cobertura/coverage_with_paths_not_relative_to_project_root.xml.gz'), 'application/x-gzip')
end
end
trait :coverage_with_corrupted_data do
file_type { :cobertura }
file_format { :gzip }
......
......@@ -4,114 +4,132 @@ require 'fast_spec_helper'
RSpec.describe Gitlab::Ci::Parsers::Coverage::Cobertura do
describe '#parse!' do
subject { described_class.new.parse!(cobertura, coverage_report) }
subject(:parse_report) { described_class.new.parse!(cobertura, coverage_report, project_path: project_path, worktree_paths: paths) }
let(:coverage_report) { Gitlab::Ci::Reports::CoverageReports.new }
let(:project_path) { 'foo/bar' }
let(:paths) { ['app/user.rb'] }
let(:cobertura) do
<<~EOF
<coverage>
#{sources_xml}
#{classes_xml}
</coverage>
EOF
end
context 'when data is Cobertura style XML' do
shared_examples_for 'ignoring sources, project_path, and worktree_paths' do
context 'when there is no <class>' do
let(:cobertura) { '' }
let(:classes_xml) { '' }
it 'parses XML and returns empty coverage' do
expect { subject }.not_to raise_error
expect(coverage_report.files).to eq({})
end
end
context 'when there is a <sources>' do
shared_examples_for 'ignoring sources' do
it 'parses XML without errors' do
expect { subject }.not_to raise_error
expect { parse_report }.not_to raise_error
expect(coverage_report.files).to eq({})
end
end
context 'and has a single source' do
let(:cobertura) do
<<-EOF.strip_heredoc
<sources>
<source>project/src</source>
</sources>
context 'when there is a single <class>' do
context 'with no lines' do
let(:classes_xml) do
<<~EOF
<packages><package name="app"><classes>
<class filename="app.rb"></class>
</classes></package></packages>
EOF
end
it_behaves_like 'ignoring sources'
end
context 'and has multiple sources' do
let(:cobertura) do
<<-EOF.strip_heredoc
<sources>
<source>project/src/foo</source>
<source>project/src/bar</source>
</sources>
EOF
end
it 'parses XML and returns empty coverage' do
expect { parse_report }.not_to raise_error
it_behaves_like 'ignoring sources'
expect(coverage_report.files).to eq({})
end
end
context 'when there is a single <class>' do
context 'with no lines' do
let(:cobertura) do
<<-EOF.strip_heredoc
<classes><class filename="app.rb"></class></classes>
context 'with a single line' do
let(:classes_xml) do
<<~EOF
<packages><package name="app"><classes>
<class filename="app.rb"><lines>
<line number="1" hits="2"/>
</lines></class>
</classes></package></packages>
EOF
end
it 'parses XML and returns empty coverage' do
expect { subject }.not_to raise_error
it 'parses XML and returns a single file with coverage' do
expect { parse_report }.not_to raise_error
expect(coverage_report.files).to eq({})
expect(coverage_report.files).to eq({ 'app.rb' => { 1 => 2 } })
end
end
context 'with a single line' do
let(:cobertura) do
<<-EOF.strip_heredoc
<classes>
context 'without a package parent' do
let(:classes_xml) do
<<~EOF
<packages>
<class filename="app.rb"><lines>
<line number="1" hits="2"/>
</lines></class>
</classes>
</packages>
EOF
end
it 'parses XML and returns a single file with coverage' do
expect { subject }.not_to raise_error
expect { parse_report }.not_to raise_error
expect(coverage_report.files).to eq({ 'app.rb' => { 1 => 2 } })
end
end
context 'with multipe lines and methods info' do
let(:cobertura) do
<<-EOF.strip_heredoc
<classes>
context 'with multiple lines and methods info' do
let(:classes_xml) do
<<~EOF
<packages><package name="app"><classes>
<class filename="app.rb"><methods/><lines>
<line number="1" hits="2"/>
<line number="2" hits="0"/>
</lines></class>
</classes>
</classes></package></packages>
EOF
end
it 'parses XML and returns a single file with coverage' do
expect { subject }.not_to raise_error
expect { parse_report }.not_to raise_error
expect(coverage_report.files).to eq({ 'app.rb' => { 1 => 2, 2 => 0 } })
end
end
end
context 'when there are multipe <class>' do
context 'when there are multiple <class>' do
context 'without a package parent' do
let(:classes_xml) do
<<~EOF
<packages>
<class filename="app.rb"><methods/><lines>
<line number="1" hits="2"/>
</lines></class>
<class filename="foo.rb"><methods/><lines>
<line number="6" hits="1"/>
</lines></class>
</packages>
EOF
end
it 'parses XML and returns coverage information per class' do
expect { parse_report }.not_to raise_error
expect(coverage_report.files).to eq({ 'app.rb' => { 1 => 2 }, 'foo.rb' => { 6 => 1 } })
end
end
context 'with the same filename and different lines' do
let(:cobertura) do
<<-EOF.strip_heredoc
<classes>
let(:classes_xml) do
<<~EOF
<packages><package name="app"><classes>
<class filename="app.rb"><methods/><lines>
<line number="1" hits="2"/>
<line number="2" hits="0"/>
......@@ -120,21 +138,21 @@ RSpec.describe Gitlab::Ci::Parsers::Coverage::Cobertura do
<line number="6" hits="1"/>
<line number="7" hits="1"/>
</lines></class>
</classes>
</classes></package></packages>
EOF
end
it 'parses XML and returns a single file with merged coverage' do
expect { subject }.not_to raise_error
expect { parse_report }.not_to raise_error
expect(coverage_report.files).to eq({ 'app.rb' => { 1 => 2, 2 => 0, 6 => 1, 7 => 1 } })
end
end
context 'with the same filename and lines' do
let(:cobertura) do
<<-EOF.strip_heredoc
<packages><package><classes>
let(:classes_xml) do
<<~EOF
<packages><package name="app"><classes>
<class filename="app.rb"><methods/><lines>
<line number="1" hits="2"/>
<line number="2" hits="0"/>
......@@ -148,16 +166,16 @@ RSpec.describe Gitlab::Ci::Parsers::Coverage::Cobertura do
end
it 'parses XML and returns a single file with summed-up coverage' do
expect { subject }.not_to raise_error
expect { parse_report }.not_to raise_error
expect(coverage_report.files).to eq({ 'app.rb' => { 1 => 3, 2 => 1 } })
end
end
context 'with missing filename' do
let(:cobertura) do
<<-EOF.strip_heredoc
<classes>
let(:classes_xml) do
<<~EOF
<packages><package name="app"><classes>
<class filename="app.rb"><methods/><lines>
<line number="1" hits="2"/>
<line number="2" hits="0"/>
......@@ -166,21 +184,21 @@ RSpec.describe Gitlab::Ci::Parsers::Coverage::Cobertura do
<line number="6" hits="1"/>
<line number="7" hits="1"/>
</lines></class>
</classes>
</classes></package></packages>
EOF
end
it 'parses XML and ignores class with missing name' do
expect { subject }.not_to raise_error
expect { parse_report }.not_to raise_error
expect(coverage_report.files).to eq({ 'app.rb' => { 1 => 2, 2 => 0 } })
end
end
context 'with invalid line information' do
let(:cobertura) do
<<-EOF.strip_heredoc
<classes>
let(:classes_xml) do
<<~EOF
<packages><package name="app"><classes>
<class filename="app.rb"><methods/><lines>
<line number="1" hits="2"/>
<line number="2" hits="0"/>
......@@ -189,22 +207,487 @@ RSpec.describe Gitlab::Ci::Parsers::Coverage::Cobertura do
<line null="test" hits="1"/>
<line number="7" hits="1"/>
</lines></class>
</classes>
</classes></package></packages>
EOF
end
it 'raises an error' do
expect { parse_report }.to raise_error(described_class::InvalidLineInformationError)
end
end
end
end
context 'when there is no <sources>' do
let(:sources_xml) { '' }
it_behaves_like 'ignoring sources, project_path, and worktree_paths'
end
context 'when there is a <sources>' do
context 'and has a single source with a pattern for Go projects' do
let(:project_path) { 'local/go' } # Make sure we're not making false positives
let(:sources_xml) do
<<~EOF
<sources>
<source>/usr/local/go/src</source>
</sources>
EOF
end
it_behaves_like 'ignoring sources, project_path, and worktree_paths'
end
context 'and has multiple sources with a pattern for Go projects' do
let(:project_path) { 'local/go' } # Make sure we're not making false positives
let(:sources_xml) do
<<~EOF
<sources>
<source>/usr/local/go/src</source>
<source>/go/src</source>
</sources>
EOF
end
it_behaves_like 'ignoring sources, project_path, and worktree_paths'
end
context 'and has a single source but already is at the project root path' do
let(:sources_xml) do
<<~EOF
<sources>
<source>builds/#{project_path}</source>
</sources>
EOF
end
it_behaves_like 'ignoring sources, project_path, and worktree_paths'
end
context 'and has multiple sources but already are at the project root path' do
let(:sources_xml) do
<<~EOF
<sources>
<source>builds/#{project_path}/</source>
<source>builds/somewhere/#{project_path}</source>
</sources>
EOF
end
it_behaves_like 'ignoring sources, project_path, and worktree_paths'
end
context 'and has a single source that is not at the project root path' do
let(:sources_xml) do
<<~EOF
<sources>
<source>builds/#{project_path}/app</source>
</sources>
EOF
end
context 'when there is no <class>' do
let(:classes_xml) { '' }
it 'parses XML and returns empty coverage' do
expect { parse_report }.not_to raise_error
expect(coverage_report.files).to eq({})
end
end
context 'when there is a single <class>' do
context 'with no lines' do
let(:classes_xml) do
<<~EOF
<packages><package name="app"><classes>
<class filename="user.rb"></class>
</classes></package></packages>
EOF
end
it 'parses XML and returns empty coverage' do
expect { parse_report }.not_to raise_error
expect(coverage_report.files).to eq({})
end
end
context 'with a single line but the filename cannot be determined based on extracted source and worktree paths' do
let(:classes_xml) do
<<~EOF
<packages><package name="app"><classes>
<class filename="member.rb"><lines>
<line number="1" hits="2"/>
</lines></class>
</classes></package></packages>
EOF
end
it 'parses XML and returns empty coverage' do
expect { parse_report }.not_to raise_error
expect(coverage_report.files).to eq({})
end
end
context 'with a single line' do
let(:classes_xml) do
<<~EOF
<packages><package name="app"><classes>
<class filename="user.rb"><lines>
<line number="1" hits="2"/>
</lines></class>
</classes></package></packages>
EOF
end
it 'parses XML and returns a single file with the filename relative to project root' do
expect { parse_report }.not_to raise_error
expect(coverage_report.files).to eq({ 'app/user.rb' => { 1 => 2 } })
end
end
context 'with multiple lines and methods info' do
let(:classes_xml) do
<<~EOF
<packages><package name="app"><classes>
<class filename="user.rb"><methods/><lines>
<line number="1" hits="2"/>
<line number="2" hits="0"/>
</lines></class>
</classes></package></packages>
EOF
end
it 'parses XML and returns a single file with the filename relative to project root' do
expect { parse_report }.not_to raise_error
expect(coverage_report.files).to eq({ 'app/user.rb' => { 1 => 2, 2 => 0 } })
end
end
end
context 'when there are multiple <class>' do
context 'with the same filename but the filename cannot be determined based on extracted source and worktree paths' do
let(:classes_xml) do
<<~EOF
<packages><package name="app"><classes>
<class filename="member.rb"><methods/><lines>
<line number="1" hits="2"/>
<line number="2" hits="0"/>
</lines></class>
<class filename="member.rb"><methods/><lines>
<line number="6" hits="1"/>
<line number="7" hits="1"/>
</lines></class>
</classes></package></packages>
EOF
end
it 'parses XML and returns empty coverage' do
expect { parse_report }.not_to raise_error
expect(coverage_report.files).to eq({})
end
end
context 'without a parent package' do
let(:classes_xml) do
<<~EOF
<packages>
<class filename="user.rb"><methods/><lines>
<line number="1" hits="2"/>
<line number="2" hits="0"/>
</lines></class>
<class filename="user.rb"><methods/><lines>
<line number="6" hits="1"/>
<line number="7" hits="1"/>
</lines></class>
</packages>
EOF
end
it 'parses XML and returns coverage information with the filename relative to project root' do
expect { parse_report }.not_to raise_error
expect(coverage_report.files).to eq({ 'app/user.rb' => { 1 => 2, 2 => 0, 6 => 1, 7 => 1 } })
end
end
context 'with the same filename and different lines' do
let(:classes_xml) do
<<~EOF
<packages><package name="app"><classes>
<class filename="user.rb"><methods/><lines>
<line number="1" hits="2"/>
<line number="2" hits="0"/>
</lines></class>
<class filename="user.rb"><methods/><lines>
<line number="6" hits="1"/>
<line number="7" hits="1"/>
</lines></class>
</classes></package></packages>
EOF
end
it 'parses XML and returns a single file with merged coverage, and with the filename relative to project root' do
expect { parse_report }.not_to raise_error
expect(coverage_report.files).to eq({ 'app/user.rb' => { 1 => 2, 2 => 0, 6 => 1, 7 => 1 } })
end
end
context 'with the same filename and lines' do
let(:classes_xml) do
<<~EOF
<packages><package name="app"><classes>
<class filename="user.rb"><methods/><lines>
<line number="1" hits="2"/>
<line number="2" hits="0"/>
</lines></class>
<class filename="user.rb"><methods/><lines>
<line number="1" hits="1"/>
<line number="2" hits="1"/>
</lines></class>
</classes></package></packages>
EOF
end
it 'parses XML and returns a single file with summed-up coverage, and with the filename relative to project root' do
expect { parse_report }.not_to raise_error
expect(coverage_report.files).to eq({ 'app/user.rb' => { 1 => 3, 2 => 1 } })
end
end
context 'with missing filename' do
let(:classes_xml) do
<<~EOF
<packages><package name="app"><classes>
<class filename="user.rb"><methods/><lines>
<line number="1" hits="2"/>
<line number="2" hits="0"/>
</lines></class>
<class><methods/><lines>
<line number="6" hits="1"/>
<line number="7" hits="1"/>
</lines></class>
</classes></package></packages>
EOF
end
it 'parses XML and ignores class with missing name' do
expect { parse_report }.not_to raise_error
expect(coverage_report.files).to eq({ 'app/user.rb' => { 1 => 2, 2 => 0 } })
end
end
context 'with filename that cannot be determined based on extracted source and worktree paths' do
let(:classes_xml) do
<<~EOF
<packages><package name="app"><classes>
<class filename="user.rb"><methods/><lines>
<line number="1" hits="2"/>
<line number="2" hits="0"/>
</lines></class>
<class filename="member.rb"><methods/><lines>
<line number="6" hits="1"/>
<line number="7" hits="1"/>
</lines></class>
</classes></package></packages>
EOF
end
it 'parses XML and ignores class with undetermined filename' do
expect { parse_report }.not_to raise_error
expect(coverage_report.files).to eq({ 'app/user.rb' => { 1 => 2, 2 => 0 } })
end
end
context 'with invalid line information' do
let(:classes_xml) do
<<~EOF
<packages><package name="app"><classes>
<class filename="user.rb"><methods/><lines>
<line number="1" hits="2"/>
<line number="2" hits="0"/>
</lines></class>
<class filename="user.rb"><methods/><lines>
<line null="test" hits="1"/>
<line number="7" hits="1"/>
</lines></class>
</classes></package></packages>
EOF
end
it 'raises an error' do
expect { subject }.to raise_error(described_class::CoberturaParserError)
expect { parse_report }.to raise_error(described_class::InvalidLineInformationError)
end
end
end
end
context 'and has multiple sources that are not at the project root path' do
let(:sources_xml) do
<<~EOF
<sources>
<source>builds/#{project_path}/app1/</source>
<source>builds/#{project_path}/app2/</source>
</sources>
EOF
end
context 'and a class filename is available under multiple extracted sources' do
let(:paths) { ['app1/user.rb', 'app2/user.rb'] }
let(:classes_xml) do
<<~EOF
<package name="app1">
<classes>
<class filename="user.rb"><lines>
<line number="1" hits="2"/>
</lines></class>
</classes>
</package>
<package name="app2">
<classes>
<class filename="user.rb"><lines>
<line number="2" hits="3"/>
</lines></class>
</classes>
</package>
EOF
end
it 'parses XML and returns the files with the filename relative to project root' do
expect { parse_report }.not_to raise_error
expect(coverage_report.files).to eq({
'app1/user.rb' => { 1 => 2 },
'app2/user.rb' => { 2 => 3 }
})
end
end
context 'and a class filename is available under one of the extracted sources' do
let(:paths) { ['app1/member.rb', 'app2/user.rb', 'app2/pet.rb'] }
let(:classes_xml) do
<<~EOF
<packages><package name="app"><classes>
<class filename="user.rb"><lines>
<line number="1" hits="2"/>
</lines></class>
</classes></package></packages>
EOF
end
it 'parses XML and returns a single file with the filename relative to project root using the extracted source where it is first found under' do
expect { parse_report }.not_to raise_error
expect(coverage_report.files).to eq({ 'app2/user.rb' => { 1 => 2 } })
end
end
context 'and a class filename is not found under any of the extracted sources' do
let(:paths) { ['app1/member.rb', 'app2/pet.rb'] }
let(:classes_xml) do
<<~EOF
<packages><package name="app"><classes>
<class filename="user.rb"><lines>
<line number="1" hits="2"/>
</lines></class>
</classes></package></packages>
EOF
end
it 'parses XML and returns empty coverage' do
expect { parse_report }.not_to raise_error
expect(coverage_report.files).to eq({})
end
end
context 'and a class filename is not found under any of the extracted sources within the iteratable limit' do
let(:paths) { ['app2/user.rb'] }
let(:classes_xml) do
<<~EOF
<packages><package name="app"><classes>
<class filename="record.rb"><lines>
<line number="1" hits="2"/>
</lines></class>
<class filename="user.rb"><lines>
<line number="1" hits="2"/>
</lines></class>
</classes></package></packages>
EOF
end
before do
stub_const("#{described_class}::MAX_SOURCES", 1)
end
it 'parses XML and returns empty coverage' do
expect { parse_report }.not_to raise_error
expect(coverage_report.files).to eq({})
end
end
end
end
shared_examples_for 'non-smart parsing' do
let(:sources_xml) do
<<~EOF
<sources>
<source>builds/foo/bar/app</source>
</sources>
EOF
end
let(:classes_xml) do
<<~EOF
<packages><package name="app"><classes>
<class filename="user.rb"><lines>
<line number="1" hits="2"/>
</lines></class>
</classes></package></packages>
EOF
end
it 'parses XML and returns filenames unchanged just as how they are found in the class node' do
expect { parse_report }.not_to raise_error
expect(coverage_report.files).to eq({ 'user.rb' => { 1 => 2 } })
end
end
context 'when project_path is not present' do
let(:project_path) { nil }
let(:paths) { ['app/user.rb'] }
it_behaves_like 'non-smart parsing'
end
context 'when worktree_paths is not present' do
let(:project_path) { 'foo/bar' }
let(:paths) { nil }
it_behaves_like 'non-smart parsing'
end
end
context 'when data is not Cobertura style XML' do
let(:cobertura) { { coverage: '12%' }.to_json }
it 'raises an error' do
expect { subject }.to raise_error(described_class::CoberturaParserError)
expect { parse_report }.to raise_error(described_class::InvalidXMLError)
end
end
end
......
......@@ -4059,13 +4059,40 @@ RSpec.describe Ci::Build do
end
end
context 'when there is a Cobertura coverage report with class filename paths not relative to project root' do
before do
allow(build.project).to receive(:full_path).and_return('root/javademo')
allow(build.pipeline).to receive(:all_worktree_paths).and_return(['src/main/java/com/example/javademo/User.java'])
create(:ci_job_artifact, :coverage_with_paths_not_relative_to_project_root, job: build, project: build.project)
end
it 'parses blobs and add the results to the coverage report with corrected paths' do
expect { subject }.not_to raise_error
expect(coverage_report.files.keys).to match_array(['src/main/java/com/example/javademo/User.java'])
end
context 'and smart_cobertura_parser feature flag is disabled' do
before do
stub_feature_flags(smart_cobertura_parser: false)
end
it 'parses blobs and add the results to the coverage report with unmodified paths' do
expect { subject }.not_to raise_error
expect(coverage_report.files.keys).to match_array(['com/example/javademo/User.java'])
end
end
end
context 'when there is a corrupted Cobertura coverage report' do
before do
create(:ci_job_artifact, :coverage_with_corrupted_data, job: build, project: build.project)
end
it 'raises an error' do
expect { subject }.to raise_error(Gitlab::Ci::Parsers::Coverage::Cobertura::CoberturaParserError)
expect { subject }.to raise_error(Gitlab::Ci::Parsers::Coverage::Cobertura::InvalidLineInformationError)
end
end
end
......
......@@ -3426,6 +3426,16 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
])
end
it 'does not execute N+1 queries' do
single_build_pipeline = create(:ci_empty_pipeline, status: :created, project: project)
single_rspec = create(:ci_build, :success, name: 'rspec', pipeline: single_build_pipeline, project: project)
create(:ci_job_artifact, :cobertura, job: single_rspec, project: project)
control = ActiveRecord::QueryRecorder.new { single_build_pipeline.coverage_reports }
expect { subject }.not_to exceed_query_limit(control)
end
context 'when builds are retried' do
let!(:build_rspec) { create(:ci_build, :retried, :success, name: 'rspec', pipeline: pipeline, project: project) }
let!(:build_golang) { create(:ci_build, :retried, :success, name: 'golang', pipeline: pipeline, project: project) }
......
......@@ -7,7 +7,8 @@ RSpec.describe ::Ci::Pipelines::CreateArtifactService do
subject { described_class.new.execute(pipeline) }
context 'when pipeline has coverage reports' do
let(:pipeline) { create(:ci_pipeline, :with_coverage_reports) }
let(:project) { create(:project, :repository) }
let(:pipeline) { create(:ci_pipeline, :with_coverage_reports, project: project) }
context 'when pipeline is finished' do
it 'creates a pipeline artifact' 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