diff options
author | Kamil Trzciński <ayufan@ayufan.eu> | 2018-08-03 21:39:48 +0300 |
---|---|---|
committer | Kamil Trzciński <ayufan@ayufan.eu> | 2018-08-03 21:39:48 +0300 |
commit | 99c033f2888a96267aa0443ad7a07f1f0e861992 (patch) | |
tree | a1d7bb68dfaba668987705538bfa4526c473010d /lib | |
parent | 53ecd2e147eb24f7e53385cb21f0c9759a482d6c (diff) | |
parent | fafd1764ca71156d261a604d64b43d531667cb17 (diff) |
Merge branch 'artifact-format-v2-with-parser' into 'master'
Parse junit.xml.gz and calculate the difference between head and base
See merge request gitlab-org/gitlab-ce!20576
Diffstat (limited to 'lib')
-rw-r--r-- | lib/gitlab/ci/build/artifacts/gzip_file_adapter.rb | 46 | ||||
-rw-r--r-- | lib/gitlab/ci/parsers.rb | 9 | ||||
-rw-r--r-- | lib/gitlab/ci/parsers/junit.rb | 69 | ||||
-rw-r--r-- | lib/gitlab/ci/reports/test_case.rb | 32 | ||||
-rw-r--r-- | lib/gitlab/ci/reports/test_reports.rb | 39 | ||||
-rw-r--r-- | lib/gitlab/ci/reports/test_reports_comparer.rb | 38 | ||||
-rw-r--r-- | lib/gitlab/ci/reports/test_suite.rb | 54 | ||||
-rw-r--r-- | lib/gitlab/ci/reports/test_suite_comparer.rb | 57 |
8 files changed, 344 insertions, 0 deletions
diff --git a/lib/gitlab/ci/build/artifacts/gzip_file_adapter.rb b/lib/gitlab/ci/build/artifacts/gzip_file_adapter.rb new file mode 100644 index 00000000000..65f65cdce08 --- /dev/null +++ b/lib/gitlab/ci/build/artifacts/gzip_file_adapter.rb @@ -0,0 +1,46 @@ +module Gitlab + module Ci + module Build + module Artifacts + class GzipFileAdapter + attr_reader :stream + + InvalidStreamError = Class.new(StandardError) + + def initialize(stream) + raise InvalidStreamError, "Stream is required" unless stream + + @stream = stream + end + + def each_blob + stream.seek(0) + + until stream.eof? + gzip(stream) do |gz| + yield gz.read, gz.orig_name + unused = gz.unused&.length.to_i + # pos has already reached to EOF at the moment + # We rewind the pos to the top of unused files + # to read next gzip stream, to support multistream archives + # https://golang.org/src/compress/gzip/gunzip.go#L117 + stream.seek(-unused, IO::SEEK_CUR) + end + end + end + + private + + def gzip(stream, &block) + gz = Zlib::GzipReader.new(stream) + yield(gz) + rescue Zlib::Error => e + raise InvalidStreamError, e.message + ensure + gz&.finish + end + end + end + end + end +end diff --git a/lib/gitlab/ci/parsers.rb b/lib/gitlab/ci/parsers.rb new file mode 100644 index 00000000000..a4eccc08dfc --- /dev/null +++ b/lib/gitlab/ci/parsers.rb @@ -0,0 +1,9 @@ +module Gitlab + module Ci + module Parsers + def self.fabricate!(file_type) + "Gitlab::Ci::Parsers::#{file_type.classify}".constantize.new + end + end + end +end diff --git a/lib/gitlab/ci/parsers/junit.rb b/lib/gitlab/ci/parsers/junit.rb new file mode 100644 index 00000000000..3c4668ec13b --- /dev/null +++ b/lib/gitlab/ci/parsers/junit.rb @@ -0,0 +1,69 @@ +module Gitlab + module Ci + module Parsers + class Junit + attr_reader :data + + JunitParserError = Class.new(StandardError) + + def parse!(xml_data, test_suite) + @data = Hash.from_xml(xml_data) + + each_suite do |testcases| + testcases.each do |testcase| + test_case = create_test_case(testcase) + test_suite.add_test_case(test_case) + end + end + rescue REXML::ParseException => e + raise JunitParserError, "XML parsing failed: #{e.message}" + rescue => e + raise JunitParserError, "JUnit parsing failed: #{e.message}" + end + + private + + def each_suite + testsuites.each do |testsuite| + yield testcases(testsuite) + end + end + + def testsuites + if data['testsuites'] + data['testsuites']['testsuite'] + else + [data['testsuite']] + end + end + + def testcases(testsuite) + if testsuite['testcase'].is_a?(Array) + testsuite['testcase'] + else + [testsuite['testcase']] + end + 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 diff --git a/lib/gitlab/ci/reports/test_case.rb b/lib/gitlab/ci/reports/test_case.rb new file mode 100644 index 00000000000..b4d08ed257f --- /dev/null +++ b/lib/gitlab/ci/reports/test_case.rb @@ -0,0 +1,32 @@ +module Gitlab + module Ci + module Reports + class TestCase + STATUS_SUCCESS = 'success'.freeze + STATUS_FAILED = 'failed'.freeze + STATUS_SKIPPED = 'skipped'.freeze + STATUS_ERROR = '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 + + def initialize(name:, classname:, execution_time:, status:, file: nil, system_output: nil, stack_trace: nil) + @name = name + @classname = classname + @file = file + @execution_time = execution_time.to_f + @status = status + @system_output = system_output + @stack_trace = stack_trace + @key = sanitize_key_name("#{classname}_#{name}") + end + + private + + def sanitize_key_name(key) + key.gsub(/[^0-9A-Za-z]/, '-') + end + end + end + end +end diff --git a/lib/gitlab/ci/reports/test_reports.rb b/lib/gitlab/ci/reports/test_reports.rb new file mode 100644 index 00000000000..c6e732e68eb --- /dev/null +++ b/lib/gitlab/ci/reports/test_reports.rb @@ -0,0 +1,39 @@ +module Gitlab + module Ci + module Reports + class TestReports + attr_reader :test_suites + + def initialize + @test_suites = {} + end + + def get_suite(suite_name) + test_suites[suite_name] ||= TestSuite.new(suite_name) + end + + def total_time + test_suites.values.sum(&:total_time) + end + + def total_count + test_suites.values.sum(&:total_count) + end + + def total_status + if failed_count > 0 || error_count > 0 + TestCase::STATUS_FAILED + else + TestCase::STATUS_SUCCESS + end + end + + TestCase::STATUS_TYPES.each do |status_type| + define_method("#{status_type}_count") do + test_suites.values.sum { |suite| suite.public_send("#{status_type}_count") } # rubocop:disable GitlabSecurity/PublicSend + end + end + end + end + end +end diff --git a/lib/gitlab/ci/reports/test_reports_comparer.rb b/lib/gitlab/ci/reports/test_reports_comparer.rb new file mode 100644 index 00000000000..c0943f5a51a --- /dev/null +++ b/lib/gitlab/ci/reports/test_reports_comparer.rb @@ -0,0 +1,38 @@ +module Gitlab + module Ci + module Reports + class TestReportsComparer + include Gitlab::Utils::StrongMemoize + + attr_reader :base_reports, :head_reports + + def initialize(base_reports, head_reports) + @base_reports = base_reports || TestReports.new + @head_reports = head_reports + end + + def suite_comparers + strong_memoize(:suite_comparers) do + head_reports.test_suites.map do |name, test_suite| + TestSuiteComparer.new(name, base_reports.get_suite(name), test_suite) + end + end + end + + def total_status + if suite_comparers.any? { |suite| suite.total_status == TestCase::STATUS_FAILED } + TestCase::STATUS_FAILED + else + TestCase::STATUS_SUCCESS + end + end + + %w(total_count resolved_count failed_count).each do |method| + define_method(method) do + suite_comparers.sum { |suite| suite.public_send(method) } # rubocop:disable GitlabSecurity/PublicSend + end + end + end + end + end +end diff --git a/lib/gitlab/ci/reports/test_suite.rb b/lib/gitlab/ci/reports/test_suite.rb new file mode 100644 index 00000000000..b722d0ba735 --- /dev/null +++ b/lib/gitlab/ci/reports/test_suite.rb @@ -0,0 +1,54 @@ +module Gitlab + module Ci + module Reports + class TestSuite + attr_reader :name + attr_reader :test_cases + attr_reader :total_time + + def initialize(name = nil) + @name = name + @test_cases = {} + @total_time = 0.0 + @duplicate_cases = [] + end + + def add_test_case(test_case) + @duplicate_cases << test_case if existing_key?(test_case) + + @test_cases[test_case.status] ||= {} + @test_cases[test_case.status][test_case.key] = test_case + @total_time += test_case.execution_time + end + + def total_count + test_cases.values.sum(&:count) + end + + def total_status + if failed_count > 0 || error_count > 0 + TestCase::STATUS_FAILED + else + TestCase::STATUS_SUCCESS + end + end + + TestCase::STATUS_TYPES.each do |status_type| + define_method("#{status_type}") do + test_cases[status_type] || {} + end + + define_method("#{status_type}_count") do + test_cases[status_type]&.length.to_i + end + end + + private + + def existing_key?(test_case) + @test_cases[test_case.status]&.key?(test_case.key) + end + end + end + end +end diff --git a/lib/gitlab/ci/reports/test_suite_comparer.rb b/lib/gitlab/ci/reports/test_suite_comparer.rb new file mode 100644 index 00000000000..642aa593092 --- /dev/null +++ b/lib/gitlab/ci/reports/test_suite_comparer.rb @@ -0,0 +1,57 @@ +module Gitlab + module Ci + module Reports + class TestSuiteComparer + include Gitlab::Utils::StrongMemoize + + attr_reader :name, :base_suite, :head_suite + + def initialize(name, base_suite, head_suite) + @name = name + @base_suite = base_suite || TestSuite.new + @head_suite = head_suite + end + + def new_failures + strong_memoize(:new_failures) do + head_suite.failed.reject do |key, _| + base_suite.failed.include?(key) + end.values + end + end + + def existing_failures + strong_memoize(:existing_failures) do + head_suite.failed.select do |key, _| + base_suite.failed.include?(key) + end.values + end + end + + def resolved_failures + strong_memoize(:resolved_failures) do + head_suite.success.select do |key, _| + base_suite.failed.include?(key) + end.values + end + end + + def total_count + head_suite.total_count + end + + def total_status + head_suite.total_status + end + + def resolved_count + resolved_failures.count + end + + def failed_count + new_failures.count + existing_failures.count + end + end + end + end +end |