diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-12-04 18:09:36 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-12-04 18:09:36 +0300 |
commit | e0655935eb32ba057b6ced978940076681d71177 (patch) | |
tree | b931975665fc002f346bdd3c5242fe644316f86c /lib/gitlab/ci/parsers | |
parent | 8322f051740fba113c5c1f6fba96b9c943240746 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'lib/gitlab/ci/parsers')
-rw-r--r-- | lib/gitlab/ci/parsers/coverage/cobertura.rb | 118 |
1 files changed, 101 insertions, 17 deletions
diff --git a/lib/gitlab/ci/parsers/coverage/cobertura.rb b/lib/gitlab/ci/parsers/coverage/cobertura.rb index 934c797580c..1edcbac2f25 100644 --- a/lib/gitlab/ci/parsers/coverage/cobertura.rb +++ b/lib/gitlab/ci/parsers/coverage/cobertura.rb @@ -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_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? 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 |