gitlab_quality-test_tooling 3.0.0 → 3.7.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 +2 -4
- data/README.md +47 -14
- data/exe/sync-category-owners +95 -0
- data/exe/test-coverage +59 -15
- data/lib/gitlab_quality/test_tooling/code_coverage/README.md +162 -0
- data/lib/gitlab_quality/test_tooling/code_coverage/artifacts.rb +5 -2
- data/lib/gitlab_quality/test_tooling/code_coverage/category_owners.rb +32 -28
- data/lib/gitlab_quality/test_tooling/code_coverage/click_house/category_owners_table.rb +102 -35
- data/lib/gitlab_quality/test_tooling/code_coverage/click_house/coverage_metrics_table.rb +44 -37
- data/lib/gitlab_quality/test_tooling/code_coverage/click_house/table.rb +17 -0
- data/lib/gitlab_quality/test_tooling/code_coverage/click_house/test_file_mappings_table.rb +52 -0
- data/lib/gitlab_quality/test_tooling/code_coverage/coverage_data.rb +77 -34
- data/lib/gitlab_quality/test_tooling/code_coverage/lcov_file.rb +11 -1
- data/lib/gitlab_quality/test_tooling/code_coverage/responsibility_classifier.rb +47 -0
- data/lib/gitlab_quality/test_tooling/code_coverage/responsibility_patterns_config.rb +46 -0
- data/lib/gitlab_quality/test_tooling/code_coverage/test_file_mapping_data.rb +46 -0
- data/lib/gitlab_quality/test_tooling/report/results_in_test_cases.rb +2 -4
- data/lib/gitlab_quality/test_tooling/runtime/env.rb +4 -0
- data/lib/gitlab_quality/test_tooling/test_meta/processor/meta_processor.rb +1 -1
- data/lib/gitlab_quality/test_tooling/test_meta/test_meta_updater.rb +4 -4
- data/lib/gitlab_quality/test_tooling/test_metrics_exporter/config.rb +8 -0
- data/lib/gitlab_quality/test_tooling/test_metrics_exporter/test_metrics.rb +36 -10
- data/lib/gitlab_quality/test_tooling/test_metrics_exporter/utils.rb +2 -0
- data/lib/gitlab_quality/test_tooling/test_quarantine/quarantine_formatter.rb +38 -0
- data/lib/gitlab_quality/test_tooling/test_quarantine/quarantine_helper.rb +76 -0
- data/lib/gitlab_quality/test_tooling/test_result/base_test_result.rb +15 -1
- data/lib/gitlab_quality/test_tooling/version.rb +1 -1
- metadata +11 -28
- data/exe/existing-test-health-issue +0 -59
- data/exe/generate-test-session +0 -70
- data/lib/gitlab_quality/test_tooling/report/generate_test_session.rb +0 -288
- data/lib/gitlab_quality/test_tooling/report/test_health_issue_finder.rb +0 -79
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3cb2e6580b8b15e78116b52ffd4fed91e0ab7db14ce914eb90f34ace0ca84741
|
|
4
|
+
data.tar.gz: 14c55d5e6048c27a891dded80da8d23c3c3b71df15b04c8ff1c98404643bb19e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 607a792c6df20d566ab3ec3dba96b80ed8ae8c08c6c6fe30ab9cbc39ccc2201c0934b86641ff137a2b9726d2b3386ef0ed22cb76f160faeacd74aef91c4007a8
|
|
7
|
+
data.tar.gz: 39d357544ea50bbc8968a224b699110f6957774140e7c8074fa9f28a5cd1cf03b1cdb6f0f0e22fd9a2b97ad06b0d54d183d06db043ff52a048a30758e928294f
|
data/Gemfile.lock
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
gitlab_quality-test_tooling (3.
|
|
5
|
-
activesupport (>= 7.0
|
|
4
|
+
gitlab_quality-test_tooling (3.7.0)
|
|
5
|
+
activesupport (>= 7.0)
|
|
6
6
|
amatch (~> 0.4.1)
|
|
7
7
|
fog-google (~> 1.24, >= 1.24.1)
|
|
8
8
|
gitlab (>= 4.19, < 7.0)
|
|
9
9
|
http (~> 5.0)
|
|
10
|
-
influxdb-client (~> 3.1)
|
|
11
10
|
nokogiri (~> 1.10)
|
|
12
11
|
parallel (>= 1, < 2)
|
|
13
12
|
rainbow (>= 3, < 4)
|
|
@@ -200,7 +199,6 @@ GEM
|
|
|
200
199
|
httpclient (2.8.3)
|
|
201
200
|
i18n (1.14.6)
|
|
202
201
|
concurrent-ruby (~> 1.0)
|
|
203
|
-
influxdb-client (3.1.0)
|
|
204
202
|
jaro_winkler (1.6.0)
|
|
205
203
|
json (2.7.2)
|
|
206
204
|
jwt (2.9.3)
|
data/README.md
CHANGED
|
@@ -185,20 +185,6 @@ Usage: exe/failed-test-issues [options]
|
|
|
185
185
|
-h, --help Show the usage
|
|
186
186
|
```
|
|
187
187
|
|
|
188
|
-
### `exe/existing-test-health-issue`
|
|
189
|
-
|
|
190
|
-
```shell
|
|
191
|
-
Purpose: Checks whether tests coming from the rspec JSON report files has an existing test health issue opened.
|
|
192
|
-
Usage: exe/existing-test-health-issue [options]
|
|
193
|
-
-i, --input-files INPUT_FILES JSON rspec-retry report files
|
|
194
|
-
-p, --project PROJECT Can be an integer or a group/project string
|
|
195
|
-
-t, --token TOKEN A valid access token with `api` scope and Maintainer permission in PROJECT
|
|
196
|
-
--health-problem-type PROBLEM_TYPE
|
|
197
|
-
Look for the given health problem type (failures, pass-after-retry, slow)
|
|
198
|
-
-v, --version Show the version
|
|
199
|
-
-h, --help Show the usage
|
|
200
|
-
```
|
|
201
|
-
|
|
202
188
|
### `exe/detect-infrastructure-failures`
|
|
203
189
|
|
|
204
190
|
```shell
|
|
@@ -290,6 +276,53 @@ Usage: exe/feature-readiness-evaluation [options]
|
|
|
290
276
|
-h, --help Show the usage
|
|
291
277
|
```
|
|
292
278
|
|
|
279
|
+
### `exe/test-coverage`
|
|
280
|
+
|
|
281
|
+
```shell
|
|
282
|
+
Purpose: Export test coverage metrics to ClickHouse
|
|
283
|
+
Usage: exe/test-coverage [options]
|
|
284
|
+
|
|
285
|
+
Options:
|
|
286
|
+
--test-reports GLOB Glob pattern for test JSON reports (RSpec or Jest) (e.g., "reports/**/*.json")
|
|
287
|
+
--coverage-report PATH Path to the LCOV coverage report (e.g., "coverage/lcov/gitlab.lcov")
|
|
288
|
+
--test-map PATH Path to the test map file (e.g., "crystalball/packed-mapping.json.gz")
|
|
289
|
+
--clickhouse-url URL ClickHouse server URL
|
|
290
|
+
--clickhouse-database DATABASE
|
|
291
|
+
ClickHouse database name
|
|
292
|
+
--clickhouse-username USERNAME
|
|
293
|
+
ClickHouse username
|
|
294
|
+
--clickhouse-shared-database DATABASE
|
|
295
|
+
ClickHouse shared database name
|
|
296
|
+
--responsibility-patterns PATH
|
|
297
|
+
Path to YAML file with responsibility classification patterns
|
|
298
|
+
|
|
299
|
+
Environment variables:
|
|
300
|
+
GLCI_CLICKHOUSE_METRICS_PASSWORD ClickHouse password (required, not passed via CLI for security)
|
|
301
|
+
|
|
302
|
+
-h, --help Show the usage
|
|
303
|
+
-v, --version Show the version
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
### `exe/sync-category-owners`
|
|
307
|
+
|
|
308
|
+
```shell
|
|
309
|
+
Purpose: Sync feature category ownership data from stages.yml to ClickHouse
|
|
310
|
+
Usage: exe/sync-category-owners [options]
|
|
311
|
+
|
|
312
|
+
Options:
|
|
313
|
+
--clickhouse-url URL ClickHouse server URL
|
|
314
|
+
--clickhouse-database DATABASE
|
|
315
|
+
ClickHouse database name
|
|
316
|
+
--clickhouse-username USERNAME
|
|
317
|
+
ClickHouse username
|
|
318
|
+
|
|
319
|
+
Environment variables:
|
|
320
|
+
GLCI_CLICKHOUSE_METRICS_PASSWORD ClickHouse password (required)
|
|
321
|
+
|
|
322
|
+
-h, --help Show the usage
|
|
323
|
+
-v, --version Show the version
|
|
324
|
+
```
|
|
325
|
+
|
|
293
326
|
## Development
|
|
294
327
|
|
|
295
328
|
### Initial setup
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "optparse"
|
|
5
|
+
require "uri"
|
|
6
|
+
|
|
7
|
+
require_relative "../lib/gitlab_quality/test_tooling"
|
|
8
|
+
|
|
9
|
+
require_relative '../lib/gitlab_quality/test_tooling/code_coverage/category_owners'
|
|
10
|
+
require_relative '../lib/gitlab_quality/test_tooling/code_coverage/click_house/category_owners_table'
|
|
11
|
+
|
|
12
|
+
params = {}
|
|
13
|
+
required_params = [:clickhouse_url, :clickhouse_database, :clickhouse_username]
|
|
14
|
+
|
|
15
|
+
options = OptionParser.new do |opts|
|
|
16
|
+
opts.banner = "Usage: #{$PROGRAM_NAME} [options]"
|
|
17
|
+
|
|
18
|
+
opts.separator ""
|
|
19
|
+
opts.separator "Syncs feature category ownership data from stages.yml to ClickHouse."
|
|
20
|
+
opts.separator ""
|
|
21
|
+
opts.separator "Options:"
|
|
22
|
+
|
|
23
|
+
opts.on('--clickhouse-url URL', 'ClickHouse server URL') do |url|
|
|
24
|
+
params[:clickhouse_url] = url
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
opts.on('--clickhouse-database DATABASE', 'ClickHouse database name') do |database|
|
|
28
|
+
params[:clickhouse_database] = database
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
opts.on('--clickhouse-username USERNAME', 'ClickHouse username') do |username|
|
|
32
|
+
params[:clickhouse_username] = username
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
opts.separator ""
|
|
36
|
+
opts.separator "Environment variables:"
|
|
37
|
+
opts.separator " GLCI_CLICKHOUSE_METRICS_PASSWORD ClickHouse password (required)"
|
|
38
|
+
opts.separator ""
|
|
39
|
+
|
|
40
|
+
opts.on('-h', '--help', 'Show the usage') do
|
|
41
|
+
puts opts
|
|
42
|
+
exit
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
opts.on_tail('-v', '--version', 'Show the version') do
|
|
46
|
+
require_relative "../lib/gitlab_quality/test_tooling/version"
|
|
47
|
+
puts "#{$PROGRAM_NAME} : #{GitlabQuality::TestTooling::VERSION}"
|
|
48
|
+
exit
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
opts.parse(ARGV)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
if params.any? && (required_params - params.keys).none?
|
|
55
|
+
clickhouse_password = ENV.fetch('GLCI_CLICKHOUSE_METRICS_PASSWORD', nil)
|
|
56
|
+
if clickhouse_password.to_s.strip.empty?
|
|
57
|
+
puts "Error: GLCI_CLICKHOUSE_METRICS_PASSWORD environment variable must be set and not empty"
|
|
58
|
+
exit 1
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
[:clickhouse_url, :clickhouse_database, :clickhouse_username].each do |param|
|
|
62
|
+
if params[param].to_s.strip.empty?
|
|
63
|
+
puts "Error: --#{param.to_s.tr('_', '-')} cannot be empty"
|
|
64
|
+
exit 1
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
begin
|
|
69
|
+
uri = URI.parse(params[:clickhouse_url])
|
|
70
|
+
unless uri.is_a?(URI::HTTP) || uri.is_a?(URI::HTTPS)
|
|
71
|
+
puts "Error: --clickhouse-url must be a valid HTTP or HTTPS URL"
|
|
72
|
+
exit 1
|
|
73
|
+
end
|
|
74
|
+
rescue URI::InvalidURIError
|
|
75
|
+
puts "Error: --clickhouse-url is not a valid URL format"
|
|
76
|
+
exit 1
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
category_owners = GitlabQuality::TestTooling::CodeCoverage::CategoryOwners.new
|
|
80
|
+
|
|
81
|
+
category_owners_table = GitlabQuality::TestTooling::CodeCoverage::ClickHouse::CategoryOwnersTable.new(
|
|
82
|
+
url: params[:clickhouse_url],
|
|
83
|
+
database: params[:clickhouse_database],
|
|
84
|
+
username: params[:clickhouse_username],
|
|
85
|
+
password: clickhouse_password
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
category_owners_table.push(category_owners.as_db_table)
|
|
89
|
+
|
|
90
|
+
puts "Successfully synced feature category ownership data to ClickHouse"
|
|
91
|
+
else
|
|
92
|
+
puts "Missing argument(s). Required arguments are: #{required_params.map { |p| "--#{p.to_s.tr('_', '-')}" }.join(', ')}"
|
|
93
|
+
puts options
|
|
94
|
+
exit 1
|
|
95
|
+
end
|
data/exe/test-coverage
CHANGED
|
@@ -3,21 +3,26 @@
|
|
|
3
3
|
|
|
4
4
|
require "optparse"
|
|
5
5
|
require "uri"
|
|
6
|
+
require "yaml"
|
|
6
7
|
|
|
7
8
|
require_relative "../lib/gitlab_quality/test_tooling"
|
|
8
9
|
|
|
9
10
|
require_relative '../lib/gitlab_quality/test_tooling/code_coverage/category_owners'
|
|
10
11
|
require_relative '../lib/gitlab_quality/test_tooling/code_coverage/click_house/category_owners_table'
|
|
11
12
|
require_relative '../lib/gitlab_quality/test_tooling/code_coverage/click_house/coverage_metrics_table'
|
|
13
|
+
require_relative '../lib/gitlab_quality/test_tooling/code_coverage/click_house/test_file_mappings_table'
|
|
12
14
|
require_relative '../lib/gitlab_quality/test_tooling/code_coverage/coverage_data'
|
|
13
15
|
require_relative '../lib/gitlab_quality/test_tooling/code_coverage/lcov_file'
|
|
14
16
|
require_relative '../lib/gitlab_quality/test_tooling/code_coverage/artifacts'
|
|
15
17
|
require_relative '../lib/gitlab_quality/test_tooling/code_coverage/test_report'
|
|
16
18
|
require_relative '../lib/gitlab_quality/test_tooling/code_coverage/test_map'
|
|
19
|
+
require_relative '../lib/gitlab_quality/test_tooling/code_coverage/test_file_mapping_data'
|
|
17
20
|
require_relative '../lib/gitlab_quality/test_tooling/code_coverage/source_file_classifier'
|
|
21
|
+
require_relative '../lib/gitlab_quality/test_tooling/code_coverage/responsibility_classifier'
|
|
22
|
+
require_relative '../lib/gitlab_quality/test_tooling/code_coverage/responsibility_patterns_config'
|
|
18
23
|
|
|
19
24
|
params = {}
|
|
20
|
-
required_params = [:test_reports, :coverage_report, :test_map, :clickhouse_url, :clickhouse_database, :clickhouse_username]
|
|
25
|
+
required_params = [:test_reports, :coverage_report, :test_map, :clickhouse_url, :clickhouse_database, :clickhouse_username, :clickhouse_shared_database, :responsibility_patterns]
|
|
21
26
|
|
|
22
27
|
options = OptionParser.new do |opts|
|
|
23
28
|
opts.banner = "Usage: #{$PROGRAM_NAME} [options]"
|
|
@@ -50,6 +55,14 @@ options = OptionParser.new do |opts|
|
|
|
50
55
|
params[:clickhouse_username] = username
|
|
51
56
|
end
|
|
52
57
|
|
|
58
|
+
opts.on('--clickhouse-shared-database DATABASE', 'ClickHouse shared database name') do |database|
|
|
59
|
+
params[:clickhouse_shared_database] = database
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
opts.on('--responsibility-patterns PATH', 'Path to YAML file with responsibility classification patterns') do |path|
|
|
63
|
+
params[:responsibility_patterns] = path
|
|
64
|
+
end
|
|
65
|
+
|
|
53
66
|
opts.separator ""
|
|
54
67
|
opts.separator "Environment variables:"
|
|
55
68
|
opts.separator " GLCI_CLICKHOUSE_METRICS_PASSWORD ClickHouse password (required, not passed via CLI for security)"
|
|
@@ -78,7 +91,7 @@ if params.any? && (required_params - params.keys).none?
|
|
|
78
91
|
exit 1
|
|
79
92
|
end
|
|
80
93
|
|
|
81
|
-
[:clickhouse_url, :clickhouse_database, :clickhouse_username].each do |param|
|
|
94
|
+
[:clickhouse_url, :clickhouse_database, :clickhouse_username, :clickhouse_shared_database].each do |param|
|
|
82
95
|
if params[param].to_s.strip.empty?
|
|
83
96
|
puts "Error: --#{param.to_s.tr('_', '-')} cannot be empty"
|
|
84
97
|
exit 1
|
|
@@ -107,7 +120,9 @@ if params.any? && (required_params - params.keys).none?
|
|
|
107
120
|
|
|
108
121
|
code_coverage_by_source_file = GitlabQuality::TestTooling::CodeCoverage::LcovFile.new(coverage_report).parsed_content
|
|
109
122
|
|
|
110
|
-
|
|
123
|
+
test_map_parser = GitlabQuality::TestTooling::CodeCoverage::TestMap.new(test_map)
|
|
124
|
+
source_file_to_tests = test_map_parser.source_to_tests
|
|
125
|
+
test_to_sources = test_map_parser.test_to_sources
|
|
111
126
|
|
|
112
127
|
# Process test reports
|
|
113
128
|
tests_to_categories = artifacts.test_reports.reduce({}) do |combined_hash, test_report_file|
|
|
@@ -121,12 +136,31 @@ if params.any? && (required_params - params.keys).none?
|
|
|
121
136
|
source_file_classifier = GitlabQuality::TestTooling::CodeCoverage::SourceFileClassifier.new
|
|
122
137
|
source_file_types = source_file_classifier.classify(code_coverage_by_source_file.keys)
|
|
123
138
|
|
|
139
|
+
# Load responsibility patterns from config file
|
|
140
|
+
begin
|
|
141
|
+
patterns_config = GitlabQuality::TestTooling::CodeCoverage::ResponsibilityPatternsConfig.new(
|
|
142
|
+
params[:responsibility_patterns]
|
|
143
|
+
)
|
|
144
|
+
rescue GitlabQuality::TestTooling::CodeCoverage::ResponsibilityPatternsConfig::ConfigError => e
|
|
145
|
+
puts "Error: #{e.message}"
|
|
146
|
+
exit 1
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Classify test files as responsible or dependent
|
|
150
|
+
responsibility_classifier = GitlabQuality::TestTooling::CodeCoverage::ResponsibilityClassifier.new(
|
|
151
|
+
test_to_sources,
|
|
152
|
+
responsible_patterns: patterns_config.responsible_patterns,
|
|
153
|
+
dependent_patterns: patterns_config.dependent_patterns
|
|
154
|
+
)
|
|
155
|
+
test_classifications = responsibility_classifier.classify_tests
|
|
156
|
+
|
|
124
157
|
coverage_data = GitlabQuality::TestTooling::CodeCoverage::CoverageData.new(
|
|
125
158
|
code_coverage_by_source_file,
|
|
126
159
|
source_file_to_tests,
|
|
127
160
|
tests_to_categories,
|
|
128
|
-
category_owners.
|
|
129
|
-
source_file_types
|
|
161
|
+
category_owners.feature_categories_to_teams,
|
|
162
|
+
source_file_types,
|
|
163
|
+
test_classifications
|
|
130
164
|
)
|
|
131
165
|
|
|
132
166
|
clickhouse_data = {
|
|
@@ -136,18 +170,28 @@ if params.any? && (required_params - params.keys).none?
|
|
|
136
170
|
password: clickhouse_password
|
|
137
171
|
}
|
|
138
172
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
if ENV['CLICKHOUSE_PUSH_CATEGORY_DATA'] == 'true'
|
|
146
|
-
category_owners_table.truncate
|
|
147
|
-
category_owners_table.push(category_owners.as_db_table)
|
|
148
|
-
end
|
|
173
|
+
shared_clickhouse_data = {
|
|
174
|
+
url: params[:clickhouse_url],
|
|
175
|
+
database: params[:clickhouse_shared_database],
|
|
176
|
+
username: params[:clickhouse_username],
|
|
177
|
+
password: clickhouse_password
|
|
178
|
+
}
|
|
149
179
|
|
|
180
|
+
category_owners_table = GitlabQuality::TestTooling::CodeCoverage::ClickHouse::CategoryOwnersTable.new(**shared_clickhouse_data)
|
|
181
|
+
coverage_metrics_table = GitlabQuality::TestTooling::CodeCoverage::ClickHouse::CoverageMetricsTable.new(
|
|
182
|
+
category_owners_table: category_owners_table,
|
|
183
|
+
**clickhouse_data
|
|
184
|
+
)
|
|
150
185
|
coverage_metrics_table.push(coverage_data.as_db_table)
|
|
186
|
+
|
|
187
|
+
# Export test-to-file mappings
|
|
188
|
+
test_file_mapping_data = GitlabQuality::TestTooling::CodeCoverage::TestFileMappingData.new(
|
|
189
|
+
test_to_sources,
|
|
190
|
+
tests_to_categories: tests_to_categories,
|
|
191
|
+
feature_categories_to_teams: category_owners.feature_categories_to_teams
|
|
192
|
+
)
|
|
193
|
+
test_file_mappings_table = GitlabQuality::TestTooling::CodeCoverage::ClickHouse::TestFileMappingsTable.new(**shared_clickhouse_data)
|
|
194
|
+
test_file_mappings_table.push(test_file_mapping_data.as_db_table)
|
|
151
195
|
else
|
|
152
196
|
puts "Missing argument(s). Required arguments are: #{required_params}\nPassed arguments are: #{params}\n"
|
|
153
197
|
puts options
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# Code Coverage Module
|
|
2
|
+
|
|
3
|
+
Exports test coverage data to ClickHouse, enriched with:
|
|
4
|
+
|
|
5
|
+
- **Feature category ownership** - group, stage, and section for each covered file
|
|
6
|
+
- **Responsibility classification** - whether coverage comes from unit or integration tests
|
|
7
|
+
- **Test-to-file mappings** - which source files each test covers
|
|
8
|
+
|
|
9
|
+
## How It Works
|
|
10
|
+
|
|
11
|
+
### Feature Category Attribution
|
|
12
|
+
|
|
13
|
+
Coverage data is enriched with feature category ownership by joining three data sources:
|
|
14
|
+
|
|
15
|
+
1. **Coverage Report** (LCOV) - which source files have coverage and their percentages
|
|
16
|
+
2. **Test Map** - which test files cover each source file
|
|
17
|
+
3. **Test Reports** (JSON) - which feature category each test file belongs to
|
|
18
|
+
|
|
19
|
+
```mermaid
|
|
20
|
+
flowchart LR
|
|
21
|
+
subgraph Inputs
|
|
22
|
+
A["<b>Coverage Report</b><br/>user.rb: 85%"]
|
|
23
|
+
B["<b>Test Map</b><br/>user.rb → user_spec.rb"]
|
|
24
|
+
C["<b>Test Reports</b><br/>user_spec.rb → user_profile"]
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
subgraph Output
|
|
28
|
+
E["<b>ClickHouse Record</b><br/>file: user.rb<br/>feature_category: user_profile<br/>coverage: 85%"]
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
A --> D((Join))
|
|
32
|
+
B --> D
|
|
33
|
+
C --> D
|
|
34
|
+
D --> E
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
This enables **multi-category attribution**: if a source file is covered by tests from
|
|
38
|
+
multiple feature categories, it creates a separate record for each category in ClickHouse.
|
|
39
|
+
|
|
40
|
+
### Why All Three Inputs Are Required
|
|
41
|
+
|
|
42
|
+
| Input | Provides | Without it |
|
|
43
|
+
|-------|----------|------------|
|
|
44
|
+
| Coverage Report | Line/branch coverage percentages | No coverage metrics |
|
|
45
|
+
| Test Map | Source file → test file relationships | No feature category attribution (all records have `category=NULL`) |
|
|
46
|
+
| Test Reports | Test file → feature category metadata | No feature category attribution (all records have `category=NULL`) |
|
|
47
|
+
|
|
48
|
+
## Responsibility Classification
|
|
49
|
+
|
|
50
|
+
Tests are classified as either **responsible** or **dependent**:
|
|
51
|
+
|
|
52
|
+
- **Responsible**: Unit tests that directly test a component in isolation
|
|
53
|
+
- **Dependent**: Integration/E2E tests that exercise a component through other layers
|
|
54
|
+
|
|
55
|
+
This classification is tracked per (source_file, feature_category) combination using two boolean columns:
|
|
56
|
+
|
|
57
|
+
| is_responsible | is_dependent | Meaning |
|
|
58
|
+
|----------------|--------------|---------|
|
|
59
|
+
| `true` | `true` | Source file has both unit AND integration test coverage from this feature category |
|
|
60
|
+
| `true` | `false` | Source file has only unit test coverage from this feature category |
|
|
61
|
+
| `false` | `true` | Source file has only integration test coverage from this feature category |
|
|
62
|
+
| `nil` | `nil` | No test mapping exists for this source file |
|
|
63
|
+
|
|
64
|
+
### Configuration
|
|
65
|
+
|
|
66
|
+
This gem is designed to be reusable across different projects. Classification patterns
|
|
67
|
+
are project-specific and must be provided via a YAML config file, since different
|
|
68
|
+
codebases have different test directory structures. The config file defines regex
|
|
69
|
+
patterns for matching test file paths:
|
|
70
|
+
|
|
71
|
+
> **Note:** The table above describes the *semantic meaning* of the flags. The patterns
|
|
72
|
+
> you configure determine *which tests* produce those flags for your project.
|
|
73
|
+
|
|
74
|
+
```yaml
|
|
75
|
+
# responsibility_patterns.yml
|
|
76
|
+
responsible:
|
|
77
|
+
- "^spec/(models|controllers|services)/" # Backend unit tests
|
|
78
|
+
- "^spec/frontend/" # Frontend unit tests
|
|
79
|
+
- "_test\\.go$" # Go unit tests
|
|
80
|
+
|
|
81
|
+
dependent:
|
|
82
|
+
- "^spec/(requests|features|integration)/" # Backend integration tests
|
|
83
|
+
- "^spec/frontend_integration/" # Frontend integration tests
|
|
84
|
+
- "^qa/" # E2E tests
|
|
85
|
+
- "_integration_test\\.go$" # Go integration tests
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
**Pattern matching rules:**
|
|
89
|
+
1. Dependent patterns are checked first (higher priority)
|
|
90
|
+
2. If no pattern matches, the test defaults to "dependent"
|
|
91
|
+
3. Patterns are Ruby regexes (escape special characters like `.` with `\\`)
|
|
92
|
+
|
|
93
|
+
**Why dependent has priority:** We use a conservative approach. `is_responsible: true`
|
|
94
|
+
makes a stronger claim ("this file has unit test coverage") than `is_dependent: true`.
|
|
95
|
+
If a test matches both patterns or no patterns, defaulting to "dependent" avoids
|
|
96
|
+
incorrectly inflating unit test coverage metrics. It's safer to under-claim than over-claim.
|
|
97
|
+
|
|
98
|
+
### Example: GitLab Configuration
|
|
99
|
+
|
|
100
|
+
```yaml
|
|
101
|
+
# .gitlab/coverage/responsibility_patterns.yml
|
|
102
|
+
responsible:
|
|
103
|
+
# Backend unit test directories
|
|
104
|
+
- "^spec/(models|controllers|services|workers|helpers|mailers|policies|presenters|uploaders|validators|lib|graphql|serializers|components)/"
|
|
105
|
+
- "^ee/spec/(models|controllers|services|workers|helpers|mailers|policies|presenters|uploaders|validators|lib|graphql|serializers|components)/"
|
|
106
|
+
# Frontend unit tests
|
|
107
|
+
- "^spec/frontend/"
|
|
108
|
+
- "^ee/spec/frontend/"
|
|
109
|
+
# Go unit tests
|
|
110
|
+
- "_test\\.go$"
|
|
111
|
+
|
|
112
|
+
dependent:
|
|
113
|
+
# Backend integration tests
|
|
114
|
+
- "^spec/(requests|features|system|integration)/"
|
|
115
|
+
- "^ee/spec/(requests|features|system|integration)/"
|
|
116
|
+
# Frontend integration tests
|
|
117
|
+
- "^spec/frontend_integration/"
|
|
118
|
+
- "^ee/spec/frontend_integration/"
|
|
119
|
+
# E2E tests
|
|
120
|
+
- "^qa/"
|
|
121
|
+
# Go integration tests
|
|
122
|
+
- "_integration_test\\.go$"
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Example: Standard Rails Project
|
|
126
|
+
|
|
127
|
+
```yaml
|
|
128
|
+
# config/responsibility_patterns.yml
|
|
129
|
+
responsible:
|
|
130
|
+
- "^test/(models|controllers|services|helpers|mailers)/"
|
|
131
|
+
- "^test/unit/"
|
|
132
|
+
|
|
133
|
+
dependent:
|
|
134
|
+
- "^test/(integration|system)/"
|
|
135
|
+
- "^spec/features/"
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Test-to-File Mappings
|
|
139
|
+
|
|
140
|
+
When a test map is provided, the module also exports test-to-source-file relationships
|
|
141
|
+
to a separate `test_file_mappings` table. This enables:
|
|
142
|
+
|
|
143
|
+
- **Coverage context for tests** - see which source files a specific test covers
|
|
144
|
+
- **Impact analysis** - understand which files would lose coverage if a test is quarantined
|
|
145
|
+
- **Flaky test triage** - correlate flaky tests with the source files they cover
|
|
146
|
+
|
|
147
|
+
## CLI
|
|
148
|
+
|
|
149
|
+
Example usage:
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
test-coverage \
|
|
153
|
+
--test-reports 'rspec/*.json' \
|
|
154
|
+
--coverage-report 'coverage/lcov.info' \
|
|
155
|
+
--test-map 'mapping.json' \
|
|
156
|
+
--responsibility-patterns 'config/responsibility_patterns.yml' \
|
|
157
|
+
--clickhouse-url 'https://clickhouse.example.com' \
|
|
158
|
+
--clickhouse-database 'coverage' \
|
|
159
|
+
--clickhouse-username 'user'
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
See `exe/test-coverage --help` for full usage.
|
|
@@ -11,7 +11,8 @@ module GitlabQuality
|
|
|
11
11
|
class Artifacts
|
|
12
12
|
# Loads coverage artifacts from the filesystem
|
|
13
13
|
#
|
|
14
|
-
# @param test_reports [String] Glob pattern for test JSON report files (RSpec or Jest)
|
|
14
|
+
# @param test_reports [String] Glob pattern(s) for test JSON report files (RSpec or Jest).
|
|
15
|
+
# Supports comma-separated patterns (e.g., "jest/**/*.json,rspec/**/*.json")
|
|
15
16
|
# @param coverage_report [String] Path to the LCOV coverage report file (e.g., "coverage/lcov/gitlab.lcov")
|
|
16
17
|
# @param test_map [String] Path to the test map file, gzipped or plain JSON (e.g., "crystalball/packed-mapping.json.gz")
|
|
17
18
|
def initialize(coverage_report:, test_map:, test_reports:)
|
|
@@ -54,7 +55,9 @@ module GitlabQuality
|
|
|
54
55
|
|
|
55
56
|
def test_reports_paths
|
|
56
57
|
@test_reports_paths ||= begin
|
|
57
|
-
|
|
58
|
+
# Support comma-separated glob patterns (e.g., "jest/**/*.json,rspec/**/*.json")
|
|
59
|
+
patterns = @test_reports_glob.split(',').map(&:strip).reject(&:empty?)
|
|
60
|
+
paths = Dir.glob(patterns)
|
|
58
61
|
|
|
59
62
|
raise "No test reports found matching pattern: #{@test_reports_glob}" if paths.empty?
|
|
60
63
|
|
|
@@ -15,17 +15,17 @@ module GitlabQuality
|
|
|
15
15
|
BASE_DELAY = 1 # seconds
|
|
16
16
|
MAX_RETRIES = 3
|
|
17
17
|
|
|
18
|
-
# @return [Hash]
|
|
18
|
+
# @return [Hash] Feature category ownership hierarchy, section -> stage -> group -> [feature_categories]
|
|
19
19
|
# @example Return value
|
|
20
20
|
# {
|
|
21
21
|
# "team_planning" => { # section
|
|
22
22
|
# "project_management" => { # stage
|
|
23
23
|
# "plan" => [ # group
|
|
24
|
-
# "dev", #
|
|
25
|
-
# "service_desk" #
|
|
24
|
+
# "dev", # feature_category
|
|
25
|
+
# "service_desk" # feature_category
|
|
26
26
|
# ],
|
|
27
27
|
# "product_planning" => [ # group
|
|
28
|
-
# "portfolio_management", #
|
|
28
|
+
# "portfolio_management", # feature_category
|
|
29
29
|
# ...
|
|
30
30
|
# ]
|
|
31
31
|
# }
|
|
@@ -35,7 +35,7 @@ module GitlabQuality
|
|
|
35
35
|
attr_reader :hierarchy
|
|
36
36
|
|
|
37
37
|
def initialize
|
|
38
|
-
@
|
|
38
|
+
@feature_categories_map = {}
|
|
39
39
|
@hierarchy = {}
|
|
40
40
|
|
|
41
41
|
yaml_file = fetch_yaml_file
|
|
@@ -43,12 +43,12 @@ module GitlabQuality
|
|
|
43
43
|
populate_ownership_hierarchy(yaml_content)
|
|
44
44
|
end
|
|
45
45
|
|
|
46
|
-
# @return [Array<Hash>] Flattened category ownership
|
|
46
|
+
# @return [Array<Hash>] Flattened feature category ownership
|
|
47
47
|
# @example Return value
|
|
48
48
|
# [
|
|
49
|
-
# {
|
|
50
|
-
# {
|
|
51
|
-
# {
|
|
49
|
+
# { feature_category: "team_planning", group: "project_management", stage: "plan", section: "dev" },
|
|
50
|
+
# { feature_category: "service_desk", group: "project_management", stage: "plan", section: "dev" },
|
|
51
|
+
# { feature_category: "portfolio_management", group: "product_planning", stage: "plan", section: "dev" }
|
|
52
52
|
# ...
|
|
53
53
|
# ]
|
|
54
54
|
def as_db_table
|
|
@@ -58,10 +58,10 @@ module GitlabQuality
|
|
|
58
58
|
stages.each do |stage, groups|
|
|
59
59
|
next unless groups
|
|
60
60
|
|
|
61
|
-
groups.each do |group,
|
|
62
|
-
Array(
|
|
61
|
+
groups.each do |group, feature_categories|
|
|
62
|
+
Array(feature_categories).each do |feature_category|
|
|
63
63
|
flattened_hierarchy << {
|
|
64
|
-
|
|
64
|
+
feature_category: feature_category,
|
|
65
65
|
group: group,
|
|
66
66
|
stage: stage,
|
|
67
67
|
section: section
|
|
@@ -72,7 +72,7 @@ module GitlabQuality
|
|
|
72
72
|
end
|
|
73
73
|
end
|
|
74
74
|
|
|
75
|
-
# @return [Hash] Mapping of categories to teams (i.e., groups, stages, sections)
|
|
75
|
+
# @return [Hash] Mapping of feature categories to teams (i.e., groups, stages, sections)
|
|
76
76
|
# @example Return value
|
|
77
77
|
# {
|
|
78
78
|
# "team_planning" => { group: "project_management", stage: "plan", section: "dev" },
|
|
@@ -80,9 +80,9 @@ module GitlabQuality
|
|
|
80
80
|
# "portfolio_management" => { group: "product_planning", stage: "plan", section: "dev" },
|
|
81
81
|
# ...
|
|
82
82
|
# }
|
|
83
|
-
def
|
|
84
|
-
|
|
85
|
-
@
|
|
83
|
+
def feature_categories_to_teams
|
|
84
|
+
populate_feature_categories_map(hierarchy)
|
|
85
|
+
@feature_categories_map
|
|
86
86
|
end
|
|
87
87
|
|
|
88
88
|
private
|
|
@@ -116,35 +116,39 @@ module GitlabQuality
|
|
|
116
116
|
groups = stage_data['groups'] || {}
|
|
117
117
|
next unless section
|
|
118
118
|
|
|
119
|
-
groups.each
|
|
119
|
+
groups.each do |group, group_data|
|
|
120
|
+
add_hierarchy_entry(section, stage, group, group_data['categories'])
|
|
121
|
+
add_hierarchy_entry(section, stage, group, group_data['maintained_categories'])
|
|
122
|
+
end
|
|
120
123
|
end
|
|
121
124
|
end
|
|
122
125
|
|
|
123
126
|
def add_hierarchy_entry(section, stage, group, categories)
|
|
124
127
|
@hierarchy[section] ||= {}
|
|
125
128
|
@hierarchy[section][stage] ||= {}
|
|
126
|
-
@hierarchy[section][stage][group]
|
|
129
|
+
@hierarchy[section][stage][group] ||= []
|
|
130
|
+
@hierarchy[section][stage][group].concat(Array(categories))
|
|
127
131
|
end
|
|
128
132
|
|
|
129
|
-
def
|
|
133
|
+
def populate_feature_categories_map(data, current_section = nil, current_stage = nil, current_group = nil)
|
|
130
134
|
case data
|
|
131
135
|
when Hash # Sections / Stages / Groups
|
|
132
136
|
data.each do |key, value|
|
|
133
137
|
if current_section.nil? # Sections
|
|
134
|
-
|
|
138
|
+
populate_feature_categories_map(value, key, nil, nil)
|
|
135
139
|
elsif current_stage.nil? # Stages
|
|
136
|
-
|
|
140
|
+
populate_feature_categories_map(value, current_section, key, nil)
|
|
137
141
|
elsif current_group.nil? # Groups
|
|
138
|
-
|
|
139
|
-
else #
|
|
140
|
-
|
|
142
|
+
populate_feature_categories_map(value, current_section, current_stage, key)
|
|
143
|
+
else # Feature categories
|
|
144
|
+
populate_feature_categories_map(value, current_section, current_stage, current_group)
|
|
141
145
|
end
|
|
142
146
|
end
|
|
143
|
-
when Array #
|
|
144
|
-
data.each do |
|
|
145
|
-
next unless
|
|
147
|
+
when Array # Feature categories array
|
|
148
|
+
data.each do |feature_category|
|
|
149
|
+
next unless feature_category.is_a?(String)
|
|
146
150
|
|
|
147
|
-
@
|
|
151
|
+
@feature_categories_map[feature_category] = {
|
|
148
152
|
section: current_section,
|
|
149
153
|
stage: current_stage,
|
|
150
154
|
group: current_group
|