gitlab_quality-test_tooling 3.17.0 → 3.18.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a3a2a28593ef844ad32b920eae54b731a9981de4821ed9f9eba2776e8cad1056
4
- data.tar.gz: f50c339ba813f7f96bc60de1b7607ef8da9667734fc1e263794a5e6e37434470
3
+ metadata.gz: 3770b676c5ff745323a47749eb04f305d435cff9769a9b567a1cc76289d5b2bb
4
+ data.tar.gz: 9cab37f5980a19ce80980dec4f97d867ca7bfe6e5524b383c71ea8d01509bbc8
5
5
  SHA512:
6
- metadata.gz: 1a4ce06d8ba98f5799aee426090cebe56eae29053febf14029300d2b9cfe9788549b0e4041e0c6f217f4db02775ce0340f836507da117ee80240354508d42204
7
- data.tar.gz: 01ff166aa1fa3c6bcd1a8e5da11b0205c9747814c5fa8e33265cb18c37afe6366274c56f5032c32c0864b2e201269ead352d90e72aae13c6bd7e8f36aafabed0
6
+ metadata.gz: 42a46bd31d9d7bdf985ad8d430c02e0d7037e116b426ff6358f089ae619f08680cf091758cbe6b5b229612be29e691a5ee760a57fb87b0da91324860b3939590
7
+ data.tar.gz: 71e1eda39f86d10c71d4d14770a925eaad6dc09d019c1cbcf7d769c6e23a73de79cce7fab02412b53ddfee8d4f6e0fa56fa55ae9880636491ffa9aa1024121ae
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- gitlab_quality-test_tooling (3.17.0)
4
+ gitlab_quality-test_tooling (3.18.0)
5
5
  activesupport (>= 7.0)
6
6
  amatch (~> 0.4.1)
7
7
  fog-google (~> 1.24, >= 1.24.1)
data/README.md CHANGED
@@ -264,6 +264,12 @@ Usage: exe/feature-readiness-evaluation [options]
264
264
 
265
265
  ### `exe/test-coverage`
266
266
 
267
+ Runs in one of three modes:
268
+
269
+ - **Full coverage-metrics export** (default, no `--per-test-coverage` / `--run-aggregation`): reads the LCOV report, Crystalball test map, and test reports, then exports the coverage-metrics and test-file-mapping tables. Requires the full argument set.
270
+ - **Per-test coverage** (`--per-test-coverage GLOB`): inserts per-test rows into `code_coverage.test_coverage_per_file` and runs the daily `test_health_risk` aggregation unless `--skip-aggregation`. Needs only the `--clickhouse-*` options; `--test-reports` and `--jest-quarantine-file` are optional enrichment.
271
+ - **Aggregation only** (`--run-aggregation`): runs the daily `test_health_risk` aggregation and exits. Needs only the `--clickhouse-*` options. Pair with `--skip-aggregation` on batched per-test inserts to aggregate once at the end instead of once per batch.
272
+
267
273
  ```shell
268
274
  Purpose: Export test coverage metrics to ClickHouse
269
275
  Usage: exe/test-coverage [options]
@@ -281,6 +287,10 @@ Options:
281
287
  ClickHouse shared database name
282
288
  --responsibility-patterns PATH
283
289
  Path to YAML file with responsibility classification patterns
290
+ --per-test-coverage GLOB Per-test coverage mode (see above). Glob for per-test coverage JSON/NDJSON files.
291
+ --jest-quarantine-file PATH Optional. Path to quarantined_vue3_specs.txt, ingested before the aggregation.
292
+ --skip-aggregation Optional. With --per-test-coverage, insert rows without running the aggregation.
293
+ --run-aggregation Run only the daily test_health_risk aggregation, then exit.
284
294
 
285
295
  Environment variables:
286
296
  GLCI_CLICKHOUSE_METRICS_PASSWORD ClickHouse password (required, not passed via CLI for security)
data/exe/test-coverage CHANGED
@@ -21,12 +21,12 @@ require_relative '../lib/gitlab_quality/test_tooling/code_coverage/test_report'
21
21
  require_relative '../lib/gitlab_quality/test_tooling/code_coverage/test_map'
22
22
  require_relative '../lib/gitlab_quality/test_tooling/code_coverage/test_file_mapping_data'
23
23
  require_relative '../lib/gitlab_quality/test_tooling/code_coverage/per_test_coverage_data'
24
+ require_relative '../lib/gitlab_quality/test_tooling/code_coverage/per_test_coverage_exporter'
24
25
  require_relative '../lib/gitlab_quality/test_tooling/code_coverage/source_file_classifier'
25
26
  require_relative '../lib/gitlab_quality/test_tooling/code_coverage/responsibility_classifier'
26
27
  require_relative '../lib/gitlab_quality/test_tooling/code_coverage/responsibility_patterns_config'
27
28
 
28
29
  params = {}
29
- required_params = [:test_reports, :coverage_report, :test_map, :clickhouse_url, :clickhouse_database, :clickhouse_username, :clickhouse_shared_database, :responsibility_patterns]
30
30
 
31
31
  options = OptionParser.new do |opts|
32
32
  opts.banner = "Usage: #{$PROGRAM_NAME} [options]"
@@ -68,19 +68,35 @@ options = OptionParser.new do |opts|
68
68
  end
69
69
 
70
70
  opts.on('--per-test-coverage GLOB',
71
- 'Optional. Glob pattern for per-test coverage JSON files. ' \
72
- 'When provided, populates code_coverage.test_coverage_per_file and runs the ' \
73
- 'daily test_health_risk aggregation. (e.g., "tmp/per-test-coverage-*.json")') do |pattern|
71
+ 'Per-test coverage mode. Glob for per-test coverage JSON/NDJSON files. ' \
72
+ 'Inserts rows into code_coverage.test_coverage_per_file and runs the daily ' \
73
+ 'test_health_risk aggregation unless --skip-aggregation. Needs only the ' \
74
+ '--clickhouse-* options; --test-reports and --jest-quarantine-file are ' \
75
+ 'optional enrichment. (e.g., "tmp/per-test-coverage-*.ndjson")') do |pattern|
74
76
  params[:per_test_coverage] = pattern
75
77
  end
76
78
 
77
79
  opts.on('--jest-quarantine-file PATH',
78
80
  'Optional. Path to quarantined_vue3_specs.txt. When provided alongside ' \
79
81
  '--per-test-coverage, populates code_coverage.jest_quarantined_tests_today ' \
80
- 'before the nightly aggregation so Jest tests count toward is_quarantined.') do |path|
82
+ 'before the aggregation so Jest tests count toward is_quarantined.') do |path|
81
83
  params[:jest_quarantine_file] = path
82
84
  end
83
85
 
86
+ opts.on('--skip-aggregation',
87
+ 'Optional. With --per-test-coverage, insert rows (and ingest the jest ' \
88
+ 'quarantine file) without running the test_health_risk aggregation. Use ' \
89
+ 'when streaming many batches, then run --run-aggregation once at the end.') do
90
+ params[:skip_aggregation] = true
91
+ end
92
+
93
+ opts.on('--run-aggregation',
94
+ 'Run only the daily test_health_risk aggregation over ' \
95
+ 'code_coverage.test_coverage_per_file, then exit. Needs only the ' \
96
+ '--clickhouse-* options.') do
97
+ params[:run_aggregation] = true
98
+ end
99
+
84
100
  opts.separator ""
85
101
  opts.separator "Environment variables:"
86
102
  opts.separator " GLCI_CLICKHOUSE_METRICS_PASSWORD ClickHouse password (required, not passed via CLI for security)"
@@ -102,31 +118,74 @@ options = OptionParser.new do |opts|
102
118
  opts.parse(ARGV)
103
119
  end
104
120
 
105
- if params.any? && (required_params - params.keys).none?
106
- clickhouse_password = ENV.fetch('GLCI_CLICKHOUSE_METRICS_PASSWORD', nil)
107
- if clickhouse_password.to_s.strip.empty?
108
- puts "Error: GLCI_CLICKHOUSE_METRICS_PASSWORD environment variable must be set and not empty"
109
- exit 1
121
+ mode =
122
+ if params[:run_aggregation]
123
+ puts "Warning: --per-test-coverage is ignored in --run-aggregation mode." if params[:per_test_coverage]
124
+ :aggregation
125
+ elsif params[:per_test_coverage]
126
+ :per_test
127
+ else
128
+ :full_export
110
129
  end
111
130
 
112
- [:clickhouse_url, :clickhouse_database, :clickhouse_username, :clickhouse_shared_database].each do |param|
113
- if params[param].to_s.strip.empty?
114
- puts "Error: --#{param.to_s.tr('_', '-')} cannot be empty"
115
- exit 1
116
- end
117
- end
131
+ # Per-test and aggregation-only modes are self-contained: they touch only the
132
+ # ClickHouse connection, not the LCOV report, Crystalball test map, or
133
+ # responsibility patterns the full coverage-metrics export needs.
134
+ clickhouse_required = [:clickhouse_url, :clickhouse_database, :clickhouse_username]
135
+ full_export_required =
136
+ [:test_reports, :coverage_report, :test_map, :responsibility_patterns, :clickhouse_shared_database] + clickhouse_required
137
+ required_params = mode == :full_export ? full_export_required : clickhouse_required
118
138
 
119
- begin
120
- uri = URI.parse(params[:clickhouse_url])
121
- unless uri.is_a?(URI::HTTP) || uri.is_a?(URI::HTTPS)
122
- puts "Error: --clickhouse-url must be a valid HTTP or HTTPS URL"
123
- exit 1
124
- end
125
- rescue URI::InvalidURIError
126
- puts "Error: --clickhouse-url is not a valid URL format"
139
+ unless params.any? && (required_params - params.keys).none?
140
+ puts "Missing argument(s). Required arguments are: #{required_params}\nPassed arguments are: #{params}\n"
141
+ puts options
142
+ exit 1
143
+ end
144
+
145
+ clickhouse_password = ENV.fetch('GLCI_CLICKHOUSE_METRICS_PASSWORD', nil)
146
+ if clickhouse_password.to_s.strip.empty?
147
+ puts "Error: GLCI_CLICKHOUSE_METRICS_PASSWORD environment variable must be set and not empty"
148
+ exit 1
149
+ end
150
+
151
+ non_empty_params = mode == :full_export ? clickhouse_required + [:clickhouse_shared_database] : clickhouse_required
152
+ non_empty_params.each do |param|
153
+ next unless params[param].to_s.strip.empty?
154
+
155
+ puts "Error: --#{param.to_s.tr('_', '-')} cannot be empty"
156
+ exit 1
157
+ end
158
+
159
+ begin
160
+ uri = URI.parse(params[:clickhouse_url])
161
+ unless uri.is_a?(URI::HTTP) || uri.is_a?(URI::HTTPS)
162
+ puts "Error: --clickhouse-url must be a valid HTTP or HTTPS URL"
127
163
  exit 1
128
164
  end
165
+ rescue URI::InvalidURIError
166
+ puts "Error: --clickhouse-url is not a valid URL format"
167
+ exit 1
168
+ end
129
169
 
170
+ clickhouse_data = {
171
+ url: params[:clickhouse_url],
172
+ database: params[:clickhouse_database],
173
+ username: params[:clickhouse_username],
174
+ password: clickhouse_password
175
+ }
176
+
177
+ case mode
178
+ when :aggregation
179
+ GitlabQuality::TestTooling::CodeCoverage::ClickHouse::TestHealthRiskAggregator.new(**clickhouse_data).run
180
+ when :per_test
181
+ GitlabQuality::TestTooling::CodeCoverage::PerTestCoverageExporter.new(
182
+ coverage_glob: params[:per_test_coverage],
183
+ clickhouse: clickhouse_data,
184
+ test_reports: params[:test_reports],
185
+ jest_quarantine_file: params[:jest_quarantine_file],
186
+ skip_aggregation: params.fetch(:skip_aggregation, false)
187
+ ).run
188
+ when :full_export
130
189
  artifacts = GitlabQuality::TestTooling::CodeCoverage::Artifacts.new(
131
190
  coverage_report: params[:coverage_report],
132
191
  test_map: params[:test_map],
@@ -181,13 +240,6 @@ if params.any? && (required_params - params.keys).none?
181
240
  test_classifications
182
241
  )
183
242
 
184
- clickhouse_data = {
185
- url: params[:clickhouse_url],
186
- database: params[:clickhouse_database],
187
- username: params[:clickhouse_username],
188
- password: clickhouse_password
189
- }
190
-
191
243
  shared_clickhouse_data = {
192
244
  url: params[:clickhouse_url],
193
245
  database: params[:clickhouse_shared_database],
@@ -210,36 +262,4 @@ if params.any? && (required_params - params.keys).none?
210
262
  )
211
263
  test_file_mappings_table = GitlabQuality::TestTooling::CodeCoverage::ClickHouse::TestFileMappingsTable.new(**shared_clickhouse_data)
212
264
  test_file_mappings_table.push(test_file_mapping_data.as_db_table)
213
-
214
- # Per-test coverage export (optional). Only runs when --per-test-coverage
215
- # was provided AND at least one matching artifact exists.
216
- if params[:per_test_coverage]
217
- per_test_files = Dir.glob(params[:per_test_coverage])
218
- if per_test_files.any?
219
- per_test_data = GitlabQuality::TestTooling::CodeCoverage::PerTestCoverageData.new(
220
- per_test_files,
221
- tests_to_categories: tests_to_categories,
222
- feature_categories_to_teams: category_owners.feature_categories_to_teams,
223
- captured_sha: ENV.fetch('CI_COMMIT_SHA', '')
224
- )
225
- per_test_coverage_table = GitlabQuality::TestTooling::CodeCoverage::ClickHouse::PerTestCoverageTable.new(**clickhouse_data)
226
- per_test_coverage_table.push(per_test_data.as_db_table)
227
-
228
- if params[:jest_quarantine_file] && File.exist?(params[:jest_quarantine_file])
229
- jest_qt = GitlabQuality::TestTooling::CodeCoverage::ClickHouse::JestQuarantinedTestsTable.new(**clickhouse_data)
230
- jest_qt.populate(quarantine_file: params[:jest_quarantine_file])
231
- elsif params[:jest_quarantine_file]
232
- puts "Jest quarantine file not found at #{params[:jest_quarantine_file]}; skipping jest quarantine ingestion."
233
- end
234
-
235
- aggregator = GitlabQuality::TestTooling::CodeCoverage::ClickHouse::TestHealthRiskAggregator.new(**clickhouse_data)
236
- aggregator.run
237
- else
238
- puts "No per-test coverage artifacts matched #{params[:per_test_coverage]}; skipping per-test export and aggregation."
239
- end
240
- end
241
- else
242
- puts "Missing argument(s). Required arguments are: #{required_params}\nPassed arguments are: #{params}\n"
243
- puts options
244
- exit 1
245
265
  end
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'logger'
5
+
6
+ module GitlabQuality
7
+ module TestTooling
8
+ module CodeCoverage
9
+ # Standalone per-test coverage export: inserts per-test, per-source-file
10
+ # rows into `code_coverage.test_coverage_per_file` and, unless skipped,
11
+ # runs the daily `test_health_risk` aggregation.
12
+ #
13
+ # Unlike the full coverage-metrics export, this path needs only ClickHouse
14
+ # credentials and the per-test coverage glob, not the LCOV report, the
15
+ # Crystalball test map, or the responsibility patterns. Category enrichment
16
+ # (feature_category/group/stage/section per row) is optional: pass
17
+ # `test_reports` to map test files to feature categories via the test
18
+ # report JSON, otherwise those columns stay blank.
19
+ #
20
+ # `skip_aggregation` lets a batched caller insert without re-running the
21
+ # table-wide aggregation on every batch. The streaming export invokes this
22
+ # once per artifact batch with `skip_aggregation: true`, then runs the
23
+ # aggregation once at the end.
24
+ class PerTestCoverageExporter
25
+ # @param clickhouse [Hash] connection params (:url, :database, :username, :password)
26
+ def initialize(
27
+ coverage_glob:, clickhouse:,
28
+ test_reports: nil, jest_quarantine_file: nil,
29
+ captured_sha: ENV.fetch('CI_COMMIT_SHA', ''), skip_aggregation: false, logger: nil)
30
+ @coverage_glob = coverage_glob
31
+ @clickhouse = clickhouse
32
+ @test_reports = test_reports
33
+ @jest_quarantine_file = jest_quarantine_file
34
+ @captured_sha = captured_sha.to_s
35
+ @skip_aggregation = skip_aggregation
36
+ @logger = logger || ::Logger.new($stdout)
37
+ end
38
+
39
+ # @return [void]
40
+ def run
41
+ coverage_files = Dir.glob(coverage_glob)
42
+ if coverage_files.empty?
43
+ logger.info(
44
+ "#{ClickHouse::LOG_PREFIX} No per-test coverage artifacts matched #{coverage_glob}; nothing to export."
45
+ )
46
+ return
47
+ end
48
+
49
+ insert(coverage_files)
50
+ ingest_jest_quarantine
51
+ aggregate unless skip_aggregation
52
+ end
53
+
54
+ private
55
+
56
+ attr_reader :coverage_glob, :clickhouse, :test_reports, :jest_quarantine_file,
57
+ :captured_sha, :skip_aggregation, :logger
58
+
59
+ def insert(coverage_files)
60
+ tests_to_categories, feature_categories_to_teams = resolve_categories
61
+
62
+ data = PerTestCoverageData.new(
63
+ coverage_files,
64
+ tests_to_categories: tests_to_categories,
65
+ feature_categories_to_teams: feature_categories_to_teams,
66
+ captured_sha: captured_sha
67
+ )
68
+
69
+ ClickHouse::PerTestCoverageTable.new(**clickhouse_credentials).push(data.as_db_table)
70
+ end
71
+
72
+ def ingest_jest_quarantine
73
+ return unless jest_quarantine_file
74
+
75
+ unless File.exist?(jest_quarantine_file)
76
+ logger.info(
77
+ "#{ClickHouse::LOG_PREFIX} Jest quarantine file not found at #{jest_quarantine_file}; " \
78
+ "skipping jest quarantine ingestion."
79
+ )
80
+ return
81
+ end
82
+
83
+ ClickHouse::JestQuarantinedTestsTable.new(**clickhouse_credentials)
84
+ .populate(quarantine_file: jest_quarantine_file)
85
+ end
86
+
87
+ def aggregate
88
+ ClickHouse::TestHealthRiskAggregator.new(**clickhouse_credentials).run
89
+ end
90
+
91
+ # `tests_to_categories` comes from the test report JSON; without it, rows
92
+ # carry blank categories. `CategoryOwners` fetches stages.yml over HTTP,
93
+ # so only build it when there are reports to enrich.
94
+ def resolve_categories
95
+ return [{}, {}] unless test_reports
96
+
97
+ # Comma-separated patterns, mirroring Artifacts#test_reports.
98
+ patterns = test_reports.split(',').map(&:strip).reject(&:empty?)
99
+ report_files = Dir.glob(patterns)
100
+ return [{}, {}] if report_files.empty?
101
+
102
+ [tests_to_categories_from(report_files), CategoryOwners.new.feature_categories_to_teams]
103
+ end
104
+
105
+ def tests_to_categories_from(report_files)
106
+ report_files.each_with_object({}) do |file, combined|
107
+ # Category enrichment is optional, so a malformed or vanished report
108
+ # shouldn't abort the per-test insert (the rows are the point). Warn
109
+ # and skip the file, leaving those tests with blank categories.
110
+ begin
111
+ report = TestReport.new(JSON.parse(File.read(file)))
112
+ rescue JSON::ParserError, Errno::ENOENT => e
113
+ logger.warn("#{ClickHouse::LOG_PREFIX} Skipping unreadable test report #{file}: #{e.message}")
114
+ next
115
+ end
116
+
117
+ combined.merge!(report.tests_to_categories) { |_, old_val, new_val| (old_val + new_val).uniq }
118
+ end
119
+ end
120
+
121
+ def clickhouse_credentials
122
+ clickhouse.merge(logger: logger)
123
+ end
124
+ end
125
+ end
126
+ end
127
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module GitlabQuality
4
4
  module TestTooling
5
- VERSION = "3.17.0"
5
+ VERSION = "3.18.0"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gitlab_quality-test_tooling
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.17.0
4
+ version: 3.18.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - GitLab Quality
@@ -502,6 +502,7 @@ files:
502
502
  - lib/gitlab_quality/test_tooling/code_coverage/coverage_data.rb
503
503
  - lib/gitlab_quality/test_tooling/code_coverage/lcov_file.rb
504
504
  - lib/gitlab_quality/test_tooling/code_coverage/per_test_coverage_data.rb
505
+ - lib/gitlab_quality/test_tooling/code_coverage/per_test_coverage_exporter.rb
505
506
  - lib/gitlab_quality/test_tooling/code_coverage/responsibility_classifier.rb
506
507
  - lib/gitlab_quality/test_tooling/code_coverage/responsibility_patterns_config.rb
507
508
  - lib/gitlab_quality/test_tooling/code_coverage/rspec_report.rb