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.
Files changed (33) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +2 -4
  3. data/README.md +47 -14
  4. data/exe/sync-category-owners +95 -0
  5. data/exe/test-coverage +59 -15
  6. data/lib/gitlab_quality/test_tooling/code_coverage/README.md +162 -0
  7. data/lib/gitlab_quality/test_tooling/code_coverage/artifacts.rb +5 -2
  8. data/lib/gitlab_quality/test_tooling/code_coverage/category_owners.rb +32 -28
  9. data/lib/gitlab_quality/test_tooling/code_coverage/click_house/category_owners_table.rb +102 -35
  10. data/lib/gitlab_quality/test_tooling/code_coverage/click_house/coverage_metrics_table.rb +44 -37
  11. data/lib/gitlab_quality/test_tooling/code_coverage/click_house/table.rb +17 -0
  12. data/lib/gitlab_quality/test_tooling/code_coverage/click_house/test_file_mappings_table.rb +52 -0
  13. data/lib/gitlab_quality/test_tooling/code_coverage/coverage_data.rb +77 -34
  14. data/lib/gitlab_quality/test_tooling/code_coverage/lcov_file.rb +11 -1
  15. data/lib/gitlab_quality/test_tooling/code_coverage/responsibility_classifier.rb +47 -0
  16. data/lib/gitlab_quality/test_tooling/code_coverage/responsibility_patterns_config.rb +46 -0
  17. data/lib/gitlab_quality/test_tooling/code_coverage/test_file_mapping_data.rb +46 -0
  18. data/lib/gitlab_quality/test_tooling/report/results_in_test_cases.rb +2 -4
  19. data/lib/gitlab_quality/test_tooling/runtime/env.rb +4 -0
  20. data/lib/gitlab_quality/test_tooling/test_meta/processor/meta_processor.rb +1 -1
  21. data/lib/gitlab_quality/test_tooling/test_meta/test_meta_updater.rb +4 -4
  22. data/lib/gitlab_quality/test_tooling/test_metrics_exporter/config.rb +8 -0
  23. data/lib/gitlab_quality/test_tooling/test_metrics_exporter/test_metrics.rb +36 -10
  24. data/lib/gitlab_quality/test_tooling/test_metrics_exporter/utils.rb +2 -0
  25. data/lib/gitlab_quality/test_tooling/test_quarantine/quarantine_formatter.rb +38 -0
  26. data/lib/gitlab_quality/test_tooling/test_quarantine/quarantine_helper.rb +76 -0
  27. data/lib/gitlab_quality/test_tooling/test_result/base_test_result.rb +15 -1
  28. data/lib/gitlab_quality/test_tooling/version.rb +1 -1
  29. metadata +11 -28
  30. data/exe/existing-test-health-issue +0 -59
  31. data/exe/generate-test-session +0 -70
  32. data/lib/gitlab_quality/test_tooling/report/generate_test_session.rb +0 -288
  33. 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: f037a73b3fd4a445324a37084915ea2d020475cca6f4e3b1d23045f290351082
4
- data.tar.gz: e32bc045856df09004e725b9b84317c74ffb66b59ed640b078e292f46a3ff565
3
+ metadata.gz: 3cb2e6580b8b15e78116b52ffd4fed91e0ab7db14ce914eb90f34ace0ca84741
4
+ data.tar.gz: 14c55d5e6048c27a891dded80da8d23c3c3b71df15b04c8ff1c98404643bb19e
5
5
  SHA512:
6
- metadata.gz: 400c1e51bf57a3f10cdb36c083444153326da8710edd828839881867aed4d9282da4b21fa16357f49bb498ac48199dda888e422e1ddf4f5453769cb5bdc92211
7
- data.tar.gz: cc071d0ca3c54a0d3408f6fc91165348c403ab5109c473d7bc518701a17f90d992f02139614dfb99fd53cac7e0ffef688241c7c232cf373344269ffe3e8b4436
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.0.0)
5
- activesupport (>= 7.0, < 7.3)
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
- source_file_to_tests = GitlabQuality::TestTooling::CodeCoverage::TestMap.new(test_map).source_to_tests
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.categories_to_teams,
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
- category_owners_table = GitlabQuality::TestTooling::CodeCoverage::ClickHouse::CategoryOwnersTable.new(**clickhouse_data)
140
- coverage_metrics_table = GitlabQuality::TestTooling::CodeCoverage::ClickHouse::CoverageMetricsTable.new(**clickhouse_data)
141
-
142
- category_owners_table.create if ENV['CLICKHOUSE_CREATE_CATEGORY_OWNERS_TABLE'] == 'true'
143
- coverage_metrics_table.create if ENV['CLICKHOUSE_CREATE_COVERAGE_METRICS_TABLE'] == 'true'
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) (e.g., "reports/**/*.json")
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
- paths = Dir.glob(@test_reports_glob)
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] Category ownership hierarchy, section -> stage -> group -> [categories]
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", # category
25
- # "service_desk" # category
24
+ # "dev", # feature_category
25
+ # "service_desk" # feature_category
26
26
  # ],
27
27
  # "product_planning" => [ # group
28
- # "portfolio_management", # category
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
- @categories_map = {}
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
- # { category: "team_planning", group: "project_management", stage: "plan", section: "dev" },
50
- # { category: "service_desk", group: "project_management", stage: "plan", section: "dev" },
51
- # { category: "portfolio_management", group: "product_planning", stage: "plan", section: "dev" }
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, categories|
62
- Array(categories).each do |category|
61
+ groups.each do |group, feature_categories|
62
+ Array(feature_categories).each do |feature_category|
63
63
  flattened_hierarchy << {
64
- category: category,
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 categories_to_teams
84
- populate_categories_map(hierarchy)
85
- @categories_map
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 { |group, group_data| add_hierarchy_entry(section, stage, group, group_data['categories']) }
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] = categories || []
129
+ @hierarchy[section][stage][group] ||= []
130
+ @hierarchy[section][stage][group].concat(Array(categories))
127
131
  end
128
132
 
129
- def populate_categories_map(data, current_section = nil, current_stage = nil, current_group = nil)
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
- populate_categories_map(value, key, nil, nil)
138
+ populate_feature_categories_map(value, key, nil, nil)
135
139
  elsif current_stage.nil? # Stages
136
- populate_categories_map(value, current_section, key, nil)
140
+ populate_feature_categories_map(value, current_section, key, nil)
137
141
  elsif current_group.nil? # Groups
138
- populate_categories_map(value, current_section, current_stage, key)
139
- else # Categories
140
- populate_categories_map(value, current_section, current_stage, current_group)
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 # Categories array
144
- data.each do |category|
145
- next unless category.is_a?(String)
147
+ when Array # Feature categories array
148
+ data.each do |feature_category|
149
+ next unless feature_category.is_a?(String)
146
150
 
147
- @categories_map[category] = {
151
+ @feature_categories_map[feature_category] = {
148
152
  section: current_section,
149
153
  stage: current_stage,
150
154
  group: current_group