Commit ccc73d1e authored by Sean McGivern's avatar Sean McGivern

Merge branch 'eb-cobertura-background-fix' into 'master'

Implement smart cobertura class path correction

See merge request gitlab-org/gitlab!48048
parents 126e1816 a47e4a01
...@@ -916,8 +916,20 @@ module Ci ...@@ -916,8 +916,20 @@ module Ci
end end
def collect_coverage_reports!(coverage_report) 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| 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 end
coverage_report coverage_report
......
...@@ -972,7 +972,7 @@ module Ci ...@@ -972,7 +972,7 @@ module Ci
def coverage_reports def coverage_reports
Gitlab::Ci::Reports::CoverageReports.new.tap do |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) build.collect_coverage_reports!(coverage_reports)
end end
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 ...@@ -5,50 +5,113 @@ module Gitlab
module Parsers module Parsers
module Coverage module Coverage
class Cobertura 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) 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 rescue Nokogiri::XML::SyntaxError
raise CoberturaParserError, "XML parsing failed" raise InvalidXMLError, "XML parsing failed"
rescue
raise CoberturaParserError, "Cobertura parsing failed"
end end
private private
def parse_all(root, coverage_report) def parse_all(root, coverage_report, context)
return unless root.present? return unless root.present?
root.each do |key, value| root.each do |key, value|
parse_node(key, value, coverage_report) parse_node(key, value, coverage_report, context)
end end
end end
def parse_node(key, value, coverage_report) def parse_node(key, value, coverage_report, context)
return if key == 'sources' if key == 'sources' && value['source'].present?
parse_sources(value['source'], context)
if key == 'class' elsif key == 'package'
Array.wrap(value).each do |item| 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 end
elsif value.is_a?(Hash) elsif value.is_a?(Hash)
parse_all(value, coverage_report) parse_all(value, coverage_report, context)
elsif value.is_a?(Array) elsif value.is_a?(Array)
value.each do |item| value.each do |item|
parse_all(item, coverage_report) parse_all(item, coverage_report, context)
end end
end end
end end
def parse_class(file, coverage_report) 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
# 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? return unless file["filename"].present? && file["lines"].present?
parsed_lines = parse_lines(file["lines"]) 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 end
def parse_lines(lines) def parse_lines(lines)
...@@ -58,6 +121,27 @@ module Gitlab ...@@ -58,6 +121,27 @@ module Gitlab
# Using `Integer()` here to raise exception on invalid values # Using `Integer()` here to raise exception on invalid values
[Integer(line["number"]), Integer(line["hits"])] [Integer(line["number"]), Integer(line["hits"])]
end 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 end
end end
......
...@@ -229,6 +229,16 @@ FactoryBot.define do ...@@ -229,6 +229,16 @@ FactoryBot.define do
end end
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 trait :coverage_with_corrupted_data do
file_type { :cobertura } file_type { :cobertura }
file_format { :gzip } file_format { :gzip }
......
...@@ -4,207 +4,690 @@ require 'fast_spec_helper' ...@@ -4,207 +4,690 @@ require 'fast_spec_helper'
RSpec.describe Gitlab::Ci::Parsers::Coverage::Cobertura do RSpec.describe Gitlab::Ci::Parsers::Coverage::Cobertura do
describe '#parse!' 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(: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 context 'when data is Cobertura style XML' do
context 'when there is no <class>' do shared_examples_for 'ignoring sources, project_path, and worktree_paths' do
let(:cobertura) { '' } context 'when there is no <class>' do
let(:classes_xml) { '' }
it 'parses XML and returns empty coverage' do it 'parses XML and returns empty coverage' do
expect { subject }.not_to raise_error expect { parse_report }.not_to raise_error
expect(coverage_report.files).to eq({}) expect(coverage_report.files).to eq({})
end
end end
end
context 'when there is a <sources>' do context 'when there is a single <class>' do
shared_examples_for 'ignoring sources' do context 'with no lines' do
it 'parses XML without errors' do let(:classes_xml) do
expect { subject }.not_to raise_error <<~EOF
<packages><package name="app"><classes>
<class filename="app.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
expect(coverage_report.files).to eq({}) 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 a single file with coverage' do
expect { parse_report }.not_to raise_error
expect(coverage_report.files).to eq({ 'app.rb' => { 1 => 2 } })
end
end
context 'without a package parent' do
let(:classes_xml) do
<<~EOF
<packages>
<class filename="app.rb"><lines>
<line number="1" hits="2"/>
</lines></class>
</packages>
EOF
end
it 'parses XML and returns a single file with coverage' do
expect { parse_report }.not_to raise_error
expect(coverage_report.files).to eq({ 'app.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="app.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 coverage' do
expect { parse_report }.not_to raise_error
expect(coverage_report.files).to eq({ 'app.rb' => { 1 => 2, 2 => 0 } })
end
end end
end end
context 'and has a single source' do context 'when there are multiple <class>' do
let(:cobertura) do context 'without a package parent' do
<<-EOF.strip_heredoc 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(: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>
<class filename="app.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' do
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(: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>
<class filename="app.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' do
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(: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>
<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.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="app.rb"><methods/><lines>
<line number="1" hits="2"/>
<line number="2" hits="0"/>
</lines></class>
<class filename="app.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 { 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> <sources>
<source>project/src</source> <source>/usr/local/go/src</source>
</sources> </sources>
EOF EOF
end end
it_behaves_like 'ignoring sources' it_behaves_like 'ignoring sources, project_path, and worktree_paths'
end end
context 'and has multiple sources' do context 'and has multiple sources with a pattern for Go projects' do
let(:cobertura) do let(:project_path) { 'local/go' } # Make sure we're not making false positives
<<-EOF.strip_heredoc let(:sources_xml) do
<<~EOF
<sources> <sources>
<source>project/src/foo</source> <source>/usr/local/go/src</source>
<source>project/src/bar</source> <source>/go/src</source>
</sources> </sources>
EOF EOF
end end
it_behaves_like 'ignoring sources' it_behaves_like 'ignoring sources, project_path, and worktree_paths'
end end
end
context 'when there is a single <class>' do context 'and has a single source but already is at the project root path' do
context 'with no lines' do let(:sources_xml) do
let(:cobertura) do <<~EOF
<<-EOF.strip_heredoc <sources>
<classes><class filename="app.rb"></class></classes> <source>builds/#{project_path}</source>
</sources>
EOF EOF
end end
it 'parses XML and returns empty coverage' do it_behaves_like 'ignoring sources, project_path, and worktree_paths'
expect { subject }.not_to raise_error end
expect(coverage_report.files).to eq({}) 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 end
it_behaves_like 'ignoring sources, project_path, and worktree_paths'
end end
context 'with a single line' do context 'and has a single source that is not at the project root path' do
let(:cobertura) do let(:sources_xml) do
<<-EOF.strip_heredoc <<~EOF
<classes> <sources>
<class filename="app.rb"><lines> <source>builds/#{project_path}/app</source>
<line number="1" hits="2"/> </sources>
</lines></class>
</classes>
EOF EOF
end end
it 'parses XML and returns a single file with coverage' do context 'when there is no <class>' do
expect { subject }.not_to raise_error let(:classes_xml) { '' }
expect(coverage_report.files).to eq({ 'app.rb' => { 1 => 2 } }) it 'parses XML and returns empty coverage' do
end expect { parse_report }.not_to raise_error
end
context 'with multipe lines and methods info' do expect(coverage_report.files).to eq({})
let(:cobertura) do end
<<-EOF.strip_heredoc
<classes>
<class filename="app.rb"><methods/><lines>
<line number="1" hits="2"/>
<line number="2" hits="0"/>
</lines></class>
</classes>
EOF
end end
it 'parses XML and returns a single file with coverage' do context 'when there is a single <class>' do
expect { subject }.not_to raise_error 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
expect(coverage_report.files).to eq({ 'app.rb' => { 1 => 2, 2 => 0 } }) 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 { parse_report }.to raise_error(described_class::InvalidLineInformationError)
end
end
end end
end end
end
context 'when there are multipe <class>' do context 'and has multiple sources that are not at the project root path' do
context 'with the same filename and different lines' do let(:sources_xml) do
let(:cobertura) do <<~EOF
<<-EOF.strip_heredoc <sources>
<classes> <source>builds/#{project_path}/app1/</source>
<class filename="app.rb"><methods/><lines> <source>builds/#{project_path}/app2/</source>
<line number="1" hits="2"/> </sources>
<line number="2" hits="0"/>
</lines></class>
<class filename="app.rb"><methods/><lines>
<line number="6" hits="1"/>
<line number="7" hits="1"/>
</lines></class>
</classes>
EOF EOF
end end
it 'parses XML and returns a single file with merged coverage' do context 'and a class filename is available under multiple extracted sources' do
expect { subject }.not_to raise_error let(:paths) { ['app1/user.rb', 'app2/user.rb'] }
expect(coverage_report.files).to eq({ 'app.rb' => { 1 => 2, 2 => 0, 6 => 1, 7 => 1 } }) 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 end
end
context 'with the same filename and lines' do context 'and a class filename is available under one of the extracted sources' do
let(:cobertura) do let(:paths) { ['app1/member.rb', 'app2/user.rb', 'app2/pet.rb'] }
<<-EOF.strip_heredoc
<packages><package><classes> let(:classes_xml) do
<class filename="app.rb"><methods/><lines> <<~EOF
<line number="1" hits="2"/> <packages><package name="app"><classes>
<line number="2" hits="0"/> <class filename="user.rb"><lines>
</lines></class> <line number="1" hits="2"/>
<class filename="app.rb"><methods/><lines> </lines></class>
<line number="1" hits="1"/> </classes></package></packages>
<line number="2" hits="1"/> EOF
</lines></class> end
</classes></package></packages>
EOF 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 end
it 'parses XML and returns a single file with summed-up coverage' do context 'and a class filename is not found under any of the extracted sources' do
expect { subject }.not_to raise_error let(:paths) { ['app1/member.rb', 'app2/pet.rb'] }
expect(coverage_report.files).to eq({ 'app.rb' => { 1 => 3, 2 => 1 } }) 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 end
end
context 'with missing filename' do context 'and a class filename is not found under any of the extracted sources within the iteratable limit' do
let(:cobertura) do let(:paths) { ['app2/user.rb'] }
<<-EOF.strip_heredoc
<classes> let(:classes_xml) do
<class filename="app.rb"><methods/><lines> <<~EOF
<line number="1" hits="2"/> <packages><package name="app"><classes>
<line number="2" hits="0"/> <class filename="record.rb"><lines>
</lines></class> <line number="1" hits="2"/>
<class><methods/><lines> </lines></class>
<line number="6" hits="1"/> <class filename="user.rb"><lines>
<line number="7" hits="1"/> <line number="1" hits="2"/>
</lines></class> </lines></class>
</classes> </classes></package></packages>
EOF 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
end
it 'parses XML and ignores class with missing name' do shared_examples_for 'non-smart parsing' do
expect { subject }.not_to raise_error let(:sources_xml) do
<<~EOF
<sources>
<source>builds/foo/bar/app</source>
</sources>
EOF
end
expect(coverage_report.files).to eq({ 'app.rb' => { 1 => 2, 2 => 0 } }) let(:classes_xml) do
end <<~EOF
<packages><package name="app"><classes>
<class filename="user.rb"><lines>
<line number="1" hits="2"/>
</lines></class>
</classes></package></packages>
EOF
end end
context 'with invalid line information' do it 'parses XML and returns filenames unchanged just as how they are found in the class node' do
let(:cobertura) do expect { parse_report }.not_to raise_error
<<-EOF.strip_heredoc
<classes>
<class filename="app.rb"><methods/><lines>
<line number="1" hits="2"/>
<line number="2" hits="0"/>
</lines></class>
<class filename="app.rb"><methods/><lines>
<line null="test" hits="1"/>
<line number="7" hits="1"/>
</lines></class>
</classes>
EOF
end
it 'raises an error' do expect(coverage_report.files).to eq({ 'user.rb' => { 1 => 2 } })
expect { subject }.to raise_error(described_class::CoberturaParserError)
end
end end
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 end
context 'when data is not Cobertura style XML' do context 'when data is not Cobertura style XML' do
let(:cobertura) { { coverage: '12%' }.to_json } let(:cobertura) { { coverage: '12%' }.to_json }
it 'raises an error' do 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 end
end end
......
...@@ -4059,13 +4059,40 @@ RSpec.describe Ci::Build do ...@@ -4059,13 +4059,40 @@ RSpec.describe Ci::Build do
end end
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 context 'when there is a corrupted Cobertura coverage report' do
before do before do
create(:ci_job_artifact, :coverage_with_corrupted_data, job: build, project: build.project) create(:ci_job_artifact, :coverage_with_corrupted_data, job: build, project: build.project)
end end
it 'raises an error' do 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 end
end end
......
...@@ -3418,6 +3418,16 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do ...@@ -3418,6 +3418,16 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
]) ])
end 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 context 'when builds are retried' do
let!(:build_rspec) { create(:ci_build, :retried, :success, name: 'rspec', pipeline: pipeline, project: project) } 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) } let!(:build_golang) { create(:ci_build, :retried, :success, name: 'golang', pipeline: pipeline, project: project) }
......
...@@ -7,7 +7,8 @@ RSpec.describe ::Ci::Pipelines::CreateArtifactService do ...@@ -7,7 +7,8 @@ RSpec.describe ::Ci::Pipelines::CreateArtifactService do
subject { described_class.new.execute(pipeline) } subject { described_class.new.execute(pipeline) }
context 'when pipeline has coverage reports' do 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 context 'when pipeline is finished' do
it 'creates a pipeline artifact' 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