gitlab_quality-test_tooling 2.26.0 → 2.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Gemfile.lock +1 -1
- data/exe/test-coverage +91 -34
- data/lib/gitlab_quality/test_tooling/code_coverage/artifacts.rb +43 -2
- data/lib/gitlab_quality/test_tooling/code_coverage/test_report.rb +43 -0
- data/lib/gitlab_quality/test_tooling/test_result/base_test_result.rb +14 -1
- data/lib/gitlab_quality/test_tooling/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9627a5294f93832f5513406c664eb60589037f0317bca00ad7dd7d9a6668ee85
|
|
4
|
+
data.tar.gz: 540672e53766c7c64743575b77600ffeda9916e7aa8337d269405eadb80bec12
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a5c0623b34d029ac924e8cb52ba3b8e22999c246c17b947c29995ccdb61d1b7738fa57428afea6a8bf77138b235e780dc6d2e53e270b5691762e083025249782
|
|
7
|
+
data.tar.gz: 1e9f61e86067efcd19c3f6e945d75e8bbc797383c3c7e0f0b5f9aa656edf020fa42a4236f49a02b63a6651af2a3f81a9b1433cde5853d4d2f0b16d9875304f5a
|
data/Gemfile.lock
CHANGED
data/exe/test-coverage
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
4
|
require "optparse"
|
|
5
|
+
require "uri"
|
|
5
6
|
|
|
6
7
|
require_relative "../lib/gitlab_quality/test_tooling"
|
|
7
8
|
|
|
@@ -12,15 +13,24 @@ require_relative '../lib/gitlab_quality/test_tooling/code_coverage/coverage_data
|
|
|
12
13
|
require_relative '../lib/gitlab_quality/test_tooling/code_coverage/lcov_file'
|
|
13
14
|
require_relative '../lib/gitlab_quality/test_tooling/code_coverage/artifacts'
|
|
14
15
|
require_relative '../lib/gitlab_quality/test_tooling/code_coverage/rspec_report'
|
|
16
|
+
require_relative '../lib/gitlab_quality/test_tooling/code_coverage/test_report'
|
|
15
17
|
require_relative '../lib/gitlab_quality/test_tooling/code_coverage/test_map'
|
|
16
18
|
|
|
17
19
|
params = {}
|
|
18
|
-
required_params = [:
|
|
20
|
+
required_params = [:coverage_report, :test_map, :clickhouse_url, :clickhouse_database, :clickhouse_username]
|
|
19
21
|
|
|
20
22
|
options = OptionParser.new do |opts|
|
|
21
23
|
opts.banner = "Usage: #{$PROGRAM_NAME} [options]"
|
|
22
24
|
|
|
23
|
-
opts.
|
|
25
|
+
opts.separator ""
|
|
26
|
+
opts.separator "Options:"
|
|
27
|
+
|
|
28
|
+
opts.on('--test-reports GLOB',
|
|
29
|
+
'Glob pattern for test JSON reports (RSpec or Jest) (e.g., "reports/**/*.json"). Takes precedence over --rspec-reports if both are provided.') do |pattern|
|
|
30
|
+
params[:test_reports] = pattern
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
opts.on('--rspec-reports GLOB', '[DEPRECATED] Use --test-reports instead. Glob pattern for RSpec JSON reports. Ignored if --test-reports is also provided.') do |pattern|
|
|
24
34
|
params[:rspec_reports] = pattern
|
|
25
35
|
end
|
|
26
36
|
|
|
@@ -32,6 +42,23 @@ options = OptionParser.new do |opts|
|
|
|
32
42
|
params[:test_map] = path
|
|
33
43
|
end
|
|
34
44
|
|
|
45
|
+
opts.on('--clickhouse-url URL', 'ClickHouse server URL') do |url|
|
|
46
|
+
params[:clickhouse_url] = url
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
opts.on('--clickhouse-database DATABASE', 'ClickHouse database name') do |database|
|
|
50
|
+
params[:clickhouse_database] = database
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
opts.on('--clickhouse-username USERNAME', 'ClickHouse username') do |username|
|
|
54
|
+
params[:clickhouse_username] = username
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
opts.separator ""
|
|
58
|
+
opts.separator "Environment variables:"
|
|
59
|
+
opts.separator " GLCI_CLICKHOUSE_METRICS_PASSWORD ClickHouse password (required, not passed via CLI for security)"
|
|
60
|
+
opts.separator ""
|
|
61
|
+
|
|
35
62
|
opts.on('-h', '--help', 'Show the usage') do
|
|
36
63
|
puts opts
|
|
37
64
|
puts "\nExamples:"
|
|
@@ -49,24 +76,64 @@ options = OptionParser.new do |opts|
|
|
|
49
76
|
end
|
|
50
77
|
|
|
51
78
|
if params.any? && (required_params - params.keys).none?
|
|
79
|
+
clickhouse_password = ENV.fetch('GLCI_CLICKHOUSE_METRICS_PASSWORD', nil)
|
|
80
|
+
if clickhouse_password.to_s.strip.empty?
|
|
81
|
+
puts "Error: GLCI_CLICKHOUSE_METRICS_PASSWORD environment variable must be set and not empty"
|
|
82
|
+
exit 1
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
[:clickhouse_url, :clickhouse_database, :clickhouse_username].each do |param|
|
|
86
|
+
if params[param].to_s.strip.empty?
|
|
87
|
+
puts "Error: --#{param.to_s.tr('_', '-')} cannot be empty"
|
|
88
|
+
exit 1
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
begin
|
|
93
|
+
uri = URI.parse(params[:clickhouse_url])
|
|
94
|
+
unless uri.is_a?(URI::HTTP) || uri.is_a?(URI::HTTPS)
|
|
95
|
+
puts "Error: --clickhouse-url must be a valid HTTP or HTTPS URL"
|
|
96
|
+
exit 1
|
|
97
|
+
end
|
|
98
|
+
rescue URI::InvalidURIError
|
|
99
|
+
puts "Error: --clickhouse-url is not a valid URL format"
|
|
100
|
+
exit 1
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Validate that at least one of test_reports or rspec_reports is provided
|
|
104
|
+
if params[:test_reports].nil? && params[:rspec_reports].nil?
|
|
105
|
+
puts "Error: At least one of --test-reports or --rspec-reports must be provided"
|
|
106
|
+
exit 1
|
|
107
|
+
end
|
|
108
|
+
|
|
52
109
|
artifacts = GitlabQuality::TestTooling::CodeCoverage::Artifacts.new(
|
|
53
|
-
rspec_reports: params[:rspec_reports],
|
|
54
110
|
coverage_report: params[:coverage_report],
|
|
55
|
-
test_map: params[:test_map]
|
|
111
|
+
test_map: params[:test_map],
|
|
112
|
+
test_reports: params[:test_reports],
|
|
113
|
+
rspec_reports: params[:rspec_reports]
|
|
56
114
|
)
|
|
57
115
|
|
|
58
116
|
coverage_report = artifacts.coverage_report
|
|
59
|
-
rspec_reports = artifacts.rspec_reports
|
|
60
117
|
test_map = artifacts.test_map
|
|
61
118
|
|
|
62
119
|
code_coverage_by_source_file = GitlabQuality::TestTooling::CodeCoverage::LcovFile.new(coverage_report).parsed_content
|
|
63
120
|
|
|
64
121
|
source_file_to_tests = GitlabQuality::TestTooling::CodeCoverage::TestMap.new(test_map).source_to_tests
|
|
65
122
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
123
|
+
# Process test reports using the new unified approach or fall back to legacy rspec_reports
|
|
124
|
+
tests_to_categories = if params[:test_reports]
|
|
125
|
+
# Use new TestReport class for unified test reports
|
|
126
|
+
artifacts.test_reports.reduce({}) do |combined_hash, test_report_file|
|
|
127
|
+
file_categories = GitlabQuality::TestTooling::CodeCoverage::TestReport.new(test_report_file).tests_to_categories
|
|
128
|
+
combined_hash.merge(file_categories) { |_, old_val, new_val| (old_val + new_val).uniq }
|
|
129
|
+
end
|
|
130
|
+
else
|
|
131
|
+
# Fall back to legacy RspecReport for backward compatibility
|
|
132
|
+
artifacts.rspec_reports.reduce({}) do |combined_hash, rspec_report_file|
|
|
133
|
+
file_categories = GitlabQuality::TestTooling::CodeCoverage::RspecReport.new(rspec_report_file).tests_to_categories
|
|
134
|
+
combined_hash.merge(file_categories) { |_, old_val, new_val| (old_val + new_val).uniq }
|
|
135
|
+
end
|
|
136
|
+
end
|
|
70
137
|
|
|
71
138
|
category_owners = GitlabQuality::TestTooling::CodeCoverage::CategoryOwners.new
|
|
72
139
|
|
|
@@ -77,35 +144,25 @@ if params.any? && (required_params - params.keys).none?
|
|
|
77
144
|
category_owners.categories_to_teams
|
|
78
145
|
)
|
|
79
146
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
url: ENV.fetch('CLICKHOUSE_URL', nil),
|
|
87
|
-
database: ENV.fetch('CLICKHOUSE_DATABASE', nil),
|
|
88
|
-
username: ENV.fetch('CLICKHOUSE_USERNAME', nil),
|
|
89
|
-
password: ENV.fetch('CLICKHOUSE_PASSWORD', nil)
|
|
90
|
-
}
|
|
147
|
+
clickhouse_data = {
|
|
148
|
+
url: params[:clickhouse_url],
|
|
149
|
+
database: params[:clickhouse_database],
|
|
150
|
+
username: params[:clickhouse_username],
|
|
151
|
+
password: clickhouse_password
|
|
152
|
+
}
|
|
91
153
|
|
|
92
|
-
|
|
93
|
-
|
|
154
|
+
category_owners_table = GitlabQuality::TestTooling::CodeCoverage::ClickHouse::CategoryOwnersTable.new(**clickhouse_data)
|
|
155
|
+
coverage_metrics_table = GitlabQuality::TestTooling::CodeCoverage::ClickHouse::CoverageMetricsTable.new(**clickhouse_data)
|
|
94
156
|
|
|
95
|
-
|
|
96
|
-
|
|
157
|
+
category_owners_table.create if ENV['CLICKHOUSE_CREATE_CATEGORY_OWNERS_TABLE'] == 'true'
|
|
158
|
+
coverage_metrics_table.create if ENV['CLICKHOUSE_CREATE_COVERAGE_METRICS_TABLE'] == 'true'
|
|
97
159
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
coverage_metrics_table.push(coverage_data.as_db_table)
|
|
104
|
-
else
|
|
105
|
-
puts "ClickHouse configuration not found.\n" \
|
|
106
|
-
'Set CLICKHOUSE_URL, CLICKHOUSE_DATABASE, CLICKHOUSE_USERNAME, ' \
|
|
107
|
-
'CLICKHOUSE_PASSWORD environment variables to enable ClickHouse export.'
|
|
160
|
+
if ENV['CLICKHOUSE_PUSH_CATEGORY_DATA'] == 'true'
|
|
161
|
+
category_owners_table.truncate
|
|
162
|
+
category_owners_table.push(category_owners.as_db_table)
|
|
108
163
|
end
|
|
164
|
+
|
|
165
|
+
coverage_metrics_table.push(coverage_data.as_db_table)
|
|
109
166
|
else
|
|
110
167
|
puts "Missing argument(s). Required arguments are: #{required_params}\nPassed arguments are: #{params}\n"
|
|
111
168
|
puts options
|
|
@@ -10,15 +10,34 @@ module GitlabQuality
|
|
|
10
10
|
class Artifacts
|
|
11
11
|
# Loads coverage artifacts from the filesystem
|
|
12
12
|
#
|
|
13
|
-
# @param
|
|
13
|
+
# @param test_reports [String, nil] Glob pattern for test JSON report files (RSpec or Jest) (e.g., "reports/**/*.json")
|
|
14
|
+
# @param rspec_reports [String, nil] [DEPRECATED] Use test_reports instead. Glob pattern for RSpec JSON report files
|
|
14
15
|
# @param coverage_report [String] Path to the LCOV coverage report file (e.g., "coverage/lcov/gitlab.lcov")
|
|
15
16
|
# @param test_map [String] Path to the test map file, gzipped or plain JSON (e.g., "crystalball/packed-mapping.json.gz")
|
|
16
|
-
def initialize(
|
|
17
|
+
def initialize(coverage_report:, test_map:, test_reports: nil, rspec_reports: nil)
|
|
18
|
+
@test_reports_glob = test_reports
|
|
17
19
|
@rspec_reports_glob = rspec_reports
|
|
18
20
|
@coverage_report_path = coverage_report
|
|
19
21
|
@test_map_path = test_map
|
|
20
22
|
end
|
|
21
23
|
|
|
24
|
+
# Loads and parses test JSON report files (RSpec or Jest)
|
|
25
|
+
#
|
|
26
|
+
# @return [Array<Hash>] Array of parsed JSON test reports
|
|
27
|
+
# @raise [RuntimeError] If no test reports are found or if JSON parsing fails
|
|
28
|
+
def test_reports
|
|
29
|
+
@test_report_files ||= test_reports_paths.map do |report_path|
|
|
30
|
+
JSON.parse(File.read(report_path))
|
|
31
|
+
rescue JSON::ParserError => e
|
|
32
|
+
raise "Invalid JSON in test report file #{report_path}: #{e.message}"
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Loads and parses RSpec JSON report files
|
|
37
|
+
#
|
|
38
|
+
# @deprecated Use {#test_reports} instead
|
|
39
|
+
# @return [Array<Hash>] Array of parsed JSON RSpec reports
|
|
40
|
+
# @raise [RuntimeError] If no RSpec reports are found or if JSON parsing fails
|
|
22
41
|
def rspec_reports
|
|
23
42
|
@rspec_report_files ||= rspec_reports_paths.map do |report_path|
|
|
24
43
|
JSON.parse(File.read(report_path))
|
|
@@ -27,17 +46,39 @@ module GitlabQuality
|
|
|
27
46
|
end
|
|
28
47
|
end
|
|
29
48
|
|
|
49
|
+
# Loads the LCOV coverage report file
|
|
50
|
+
#
|
|
51
|
+
# @return [String] Raw content of the LCOV coverage report
|
|
52
|
+
# @raise [RuntimeError] If the coverage report file is not found
|
|
30
53
|
def coverage_report
|
|
31
54
|
@coverage_report ||= read_coverage_reports
|
|
32
55
|
end
|
|
33
56
|
|
|
57
|
+
# Loads and parses the test map file (supports gzipped or plain JSON)
|
|
58
|
+
#
|
|
59
|
+
# @return [Hash] Parsed test map data
|
|
60
|
+
# @raise [RuntimeError] If the test map file is not found or cannot be parsed
|
|
34
61
|
def test_map
|
|
35
62
|
@test_map ||= fetch_test_map
|
|
36
63
|
end
|
|
37
64
|
|
|
38
65
|
private
|
|
39
66
|
|
|
67
|
+
def test_reports_paths
|
|
68
|
+
return [] if @test_reports_glob.nil? || @test_reports_glob.empty?
|
|
69
|
+
|
|
70
|
+
@test_reports_paths ||= begin
|
|
71
|
+
paths = Dir.glob(@test_reports_glob)
|
|
72
|
+
|
|
73
|
+
raise "No test reports found matching pattern: #{@test_reports_glob}" if paths.empty?
|
|
74
|
+
|
|
75
|
+
paths
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
40
79
|
def rspec_reports_paths
|
|
80
|
+
return [] if @rspec_reports_glob.nil? || @rspec_reports_glob.empty?
|
|
81
|
+
|
|
41
82
|
@rspec_reports_paths ||= begin
|
|
42
83
|
paths = Dir.glob(@rspec_reports_glob)
|
|
43
84
|
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
|
|
5
|
+
module GitlabQuality
|
|
6
|
+
module TestTooling
|
|
7
|
+
module CodeCoverage
|
|
8
|
+
class TestReport
|
|
9
|
+
# @param [Hash<String, Object>] test_report The content of a test
|
|
10
|
+
# report (RSpec or Jest)
|
|
11
|
+
def initialize(test_report)
|
|
12
|
+
@test_report = test_report
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# @return [Array<Hash<String, String>>] Content of the "examples"
|
|
16
|
+
# section of the test report
|
|
17
|
+
def examples
|
|
18
|
+
@examples ||= @test_report['examples']
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# @return [Hash<String, Array<String>>] Test files mapped to all feature
|
|
22
|
+
# categories they belong to
|
|
23
|
+
# @example Return value
|
|
24
|
+
# {
|
|
25
|
+
# "spec/path/to/file_spec.rb" => [
|
|
26
|
+
# "feature_category1", "feature_category2"
|
|
27
|
+
# ],
|
|
28
|
+
# ...
|
|
29
|
+
# }
|
|
30
|
+
def tests_to_categories
|
|
31
|
+
@tests_to_categories ||= examples.to_a.filter_map do |example|
|
|
32
|
+
next unless example.is_a?(Hash)
|
|
33
|
+
|
|
34
|
+
file_path = example['file_path']
|
|
35
|
+
next unless file_path.is_a?(String)
|
|
36
|
+
|
|
37
|
+
[file_path.gsub('./', ''), Array(example['feature_category']).compact]
|
|
38
|
+
end.to_h
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -8,6 +8,7 @@ module GitlabQuality
|
|
|
8
8
|
"Net::ReadTimeout",
|
|
9
9
|
"403 Forbidden - Your account has been blocked",
|
|
10
10
|
"API failed (502) with `GitLab is not responding",
|
|
11
|
+
"Error Code: 502",
|
|
11
12
|
"unexpected token at 'GitLab is not responding'",
|
|
12
13
|
"GitLab: Internal API error (502).",
|
|
13
14
|
"could not be found (502)",
|
|
@@ -127,14 +128,26 @@ module GitlabQuality
|
|
|
127
128
|
end
|
|
128
129
|
|
|
129
130
|
def full_stacktrace
|
|
131
|
+
page_error_failure = ""
|
|
132
|
+
first_non_ignored_failure = ""
|
|
133
|
+
|
|
130
134
|
failures.each do |failure|
|
|
131
135
|
message = failure['message'] || ""
|
|
132
136
|
message_lines = failure['message_lines'] || []
|
|
133
137
|
|
|
134
138
|
next if IGNORED_FAILURES.any? { |e| message.include?(e) }
|
|
135
139
|
|
|
136
|
-
|
|
140
|
+
formatted_failure = message_lines.empty? ? message : message_lines.join("\n")
|
|
141
|
+
|
|
142
|
+
if message.include?("PageErrorChecker")
|
|
143
|
+
page_error_failure = formatted_failure
|
|
144
|
+
elsif first_non_ignored_failure.empty?
|
|
145
|
+
first_non_ignored_failure = formatted_failure
|
|
146
|
+
end
|
|
137
147
|
end
|
|
148
|
+
|
|
149
|
+
# Return PageErrorChecker failure if found, otherwise first non-ignored failure
|
|
150
|
+
page_error_failure.empty? ? first_non_ignored_failure : page_error_failure
|
|
138
151
|
end
|
|
139
152
|
|
|
140
153
|
def calls_shared_examples?
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: gitlab_quality-test_tooling
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.
|
|
4
|
+
version: 2.27.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- GitLab Quality
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2025-11-
|
|
11
|
+
date: 2025-11-13 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: climate_control
|
|
@@ -499,6 +499,7 @@ files:
|
|
|
499
499
|
- lib/gitlab_quality/test_tooling/code_coverage/lcov_file.rb
|
|
500
500
|
- lib/gitlab_quality/test_tooling/code_coverage/rspec_report.rb
|
|
501
501
|
- lib/gitlab_quality/test_tooling/code_coverage/test_map.rb
|
|
502
|
+
- lib/gitlab_quality/test_tooling/code_coverage/test_report.rb
|
|
502
503
|
- lib/gitlab_quality/test_tooling/code_coverage/utils.rb
|
|
503
504
|
- lib/gitlab_quality/test_tooling/concerns/find_set_dri.rb
|
|
504
505
|
- lib/gitlab_quality/test_tooling/failed_jobs_table.rb
|