gitlab_quality-test_tooling 3.22.1 → 3.23.1

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: 753fbf9038d32ba4c43b76dbeac70b9a7bec17cd58e62b92f1c6c6279c1105a6
4
- data.tar.gz: 6d10b736a6a40453ccc70edce8aad4c360a50df4d27a662cde93d38fd107de59
3
+ metadata.gz: 49a063bdf7dc0b0055d926d178df7a5274c067f352a5feae0d5f04695702ea01
4
+ data.tar.gz: 426701ca7b4f8b55c433bfd78b173f1e731c1f55dafd099584acb0366b6f77de
5
5
  SHA512:
6
- metadata.gz: 54cd62d62cbcb29923e63ebafeccfa3911086a37220a59b16efe2d2b652587c598731360a827f79a27fe0a92f8e702c4943b3e03810c8e51ac1052d6a7bacba3
7
- data.tar.gz: 2991e98253c031c1cf0d98e04178d09c53f4c331e1b23c244e567cfcb79552da6c104f91fb3ad42c23284f0654bb192367823e7159db2c06c7aaa5d387c3757d
6
+ metadata.gz: a3a5eb95bc469a879dacb6db1e3a456b35373391ed034df6a4adec4840533666613367de04510f4a8b9e1c60f755bfc5140b6a32048a597ec822bfc1c10cfd92
7
+ data.tar.gz: 00136e319209d433a0ff36c7c4a583ea54848653a5b1290244ba8d9108e47c0e3f7aad4a736cd7f3b51d42afe8d3beb16074c655cb41939f0135e555cfd96efb
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- gitlab_quality-test_tooling (3.22.1)
4
+ gitlab_quality-test_tooling (3.23.1)
5
5
  activesupport (>= 7.0)
6
6
  amatch (~> 0.4.1)
7
7
  csv (~> 3.0)
@@ -19,30 +19,33 @@ PATH
19
19
  GEM
20
20
  remote: https://rubygems.org/
21
21
  specs:
22
- activesupport (7.1.4)
22
+ activesupport (7.2.4)
23
23
  base64
24
+ benchmark (>= 0.3)
24
25
  bigdecimal
25
- concurrent-ruby (~> 1.0, >= 1.0.2)
26
- connection_pool (>= 2.2.5)
26
+ concurrent-ruby (~> 1.0, >= 1.3.1)
27
+ connection_pool (>= 2.2.5, < 3)
27
28
  drb
28
29
  i18n (>= 1.6, < 2)
29
- minitest (>= 5.1)
30
- mutex_m
31
- tzinfo (~> 2.0)
32
- addressable (2.8.7)
33
- public_suffix (>= 2.0.2, < 7.0)
30
+ logger (>= 1.4.2)
31
+ minitest (>= 5.1, < 6)
32
+ securerandom (>= 0.3)
33
+ tzinfo (~> 2.0, >= 2.0.5)
34
+ addressable (2.9.0)
35
+ public_suffix (>= 2.0.2, < 8.0)
34
36
  amatch (0.4.1)
35
37
  mize
36
38
  tins (~> 1.0)
37
39
  ast (2.4.2)
38
40
  backport (1.2.0)
39
- base64 (0.2.0)
41
+ base64 (0.3.0)
40
42
  benchmark (0.3.0)
41
- bigdecimal (3.1.8)
43
+ bigdecimal (4.1.3)
42
44
  binding_of_caller (1.0.1)
43
45
  debug_inspector (>= 1.2.0)
44
46
  builder (3.3.0)
45
- byebug (11.1.3)
47
+ byebug (13.0.0)
48
+ reline (>= 0.6.0)
46
49
  claide (1.1.0)
47
50
  claide-plugins (0.9.2)
48
51
  cork
@@ -51,14 +54,14 @@ GEM
51
54
  climate_control (1.2.0)
52
55
  coderay (1.1.3)
53
56
  colored2 (3.1.2)
54
- concurrent-ruby (1.3.4)
55
- connection_pool (2.4.1)
57
+ concurrent-ruby (1.3.8)
58
+ connection_pool (2.5.5)
56
59
  cork (0.3.0)
57
60
  colored2 (~> 3.1)
58
61
  crack (1.0.0)
59
62
  bigdecimal
60
63
  rexml
61
- csv (3.3.0)
64
+ csv (3.3.6)
62
65
  danger (9.5.0)
63
66
  claide (~> 1.0)
64
67
  claide-plugins (>= 0.9.2)
@@ -79,18 +82,20 @@ GEM
79
82
  diff-lcs (1.5.1)
80
83
  docile (1.4.1)
81
84
  domain_name (0.6.20240107)
82
- drb (2.2.1)
85
+ drb (2.2.3)
83
86
  e2mmap (0.1.0)
84
87
  excon (1.6.0)
85
88
  logger
86
- faraday (2.12.0)
87
- faraday-net_http (>= 2.0, < 3.4)
89
+ faraday (2.14.4)
90
+ faraday-net_http (>= 2.0, < 3.5)
88
91
  json
89
92
  logger
93
+ faraday-follow_redirects (0.5.0)
94
+ faraday (>= 1, < 3)
90
95
  faraday-http-cache (2.5.1)
91
96
  faraday (>= 0.8)
92
- faraday-net_http (3.3.0)
93
- net-http
97
+ faraday-net_http (3.4.4)
98
+ net-http (~> 0.5)
94
99
  ffi (1.17.0)
95
100
  ffi-compiler (1.3.2)
96
101
  ffi (>= 1.15.5)
@@ -139,16 +144,17 @@ GEM
139
144
  rubocop-rails (~> 2.26.0)
140
145
  rubocop-rspec (~> 3.0.4)
141
146
  rubocop-rspec_rails (~> 2.30.0)
142
- google-apis-compute_v1 (0.108.0)
147
+ google-apis-compute_v1 (0.155.0)
143
148
  google-apis-core (>= 0.15.0, < 2.a)
144
- google-apis-core (0.15.1)
145
- addressable (~> 2.5, >= 2.5.1)
146
- googleauth (~> 1.9)
147
- httpclient (>= 2.8.3, < 3.a)
148
- mini_mime (~> 1.0)
149
- mutex_m
149
+ google-apis-core (1.2.2)
150
+ addressable (~> 2.8, >= 2.8.7)
151
+ faraday (~> 2.13)
152
+ faraday-follow_redirects (~> 0.3)
153
+ googleauth (~> 1.14)
154
+ mini_mime (~> 1.1)
155
+ multi_json (~> 1.11)
150
156
  representable (~> 3.0)
151
- retriable (>= 2.0, < 4.a)
157
+ retriable (>= 3.1, < 5.0)
152
158
  google-apis-dns_v1 (0.44.0)
153
159
  google-apis-core (>= 0.15.0, < 2.a)
154
160
  google-apis-iamcredentials_v1 (0.21.0)
@@ -163,12 +169,14 @@ GEM
163
169
  google-apis-core (>= 0.15.0, < 2.a)
164
170
  google-cloud-env (2.2.1)
165
171
  faraday (>= 1.0, < 3.a)
166
- googleauth (1.11.1)
172
+ google-logging-utils (0.2.0)
173
+ googleauth (1.17.4)
167
174
  faraday (>= 1.0, < 3.a)
168
- google-cloud-env (~> 2.1)
169
- jwt (>= 1.4, < 3.0)
170
- multi_json (~> 1.11)
175
+ google-cloud-env (~> 2.2)
176
+ google-logging-utils (~> 0.1)
177
+ jwt (>= 1.4, < 4.0)
171
178
  os (>= 0.9, < 2.0)
179
+ pstore (~> 0.1)
172
180
  signet (>= 0.16, < 2.a)
173
181
  guard (2.18.1)
174
182
  formatador (>= 0.2.4)
@@ -195,16 +203,16 @@ GEM
195
203
  http-cookie (1.0.7)
196
204
  domain_name (~> 0.5)
197
205
  http-form_data (2.3.0)
198
- httparty (0.22.0)
206
+ httparty (0.24.3)
199
207
  csv
200
208
  mini_mime (>= 1.0.0)
201
209
  multi_xml (>= 0.5.2)
202
- httpclient (2.8.3)
203
- i18n (1.14.6)
210
+ i18n (1.15.2)
204
211
  concurrent-ruby (~> 1.0)
212
+ io-console (0.9.4)
205
213
  jaro_winkler (1.6.0)
206
- json (2.7.2)
207
- jwt (2.9.3)
214
+ json (2.21.2)
215
+ jwt (3.3.0)
208
216
  base64
209
217
  kramdown (2.4.0)
210
218
  rexml
@@ -218,7 +226,7 @@ GEM
218
226
  llhttp-ffi (0.5.0)
219
227
  ffi-compiler (~> 1.0)
220
228
  rake (~> 13.0)
221
- logger (1.6.1)
229
+ logger (1.7.0)
222
230
  lumberjack (1.2.10)
223
231
  method_source (1.1.0)
224
232
  mime-types (3.6.0)
@@ -226,19 +234,18 @@ GEM
226
234
  mime-types-data (~> 3.2015)
227
235
  mime-types-data (3.2024.1001)
228
236
  mini_mime (1.1.5)
229
- mini_portile2 (2.8.7)
230
- minitest (5.25.1)
237
+ mini_portile2 (2.8.9)
238
+ minitest (5.27.0)
231
239
  mize (0.6.0)
232
240
  multi_json (1.15.0)
233
- multi_xml (0.7.1)
234
- bigdecimal (~> 3.1)
235
- mutex_m (0.2.0)
241
+ multi_xml (0.9.1)
242
+ bigdecimal (>= 3.1, < 5)
236
243
  nap (1.1.0)
237
244
  nenv (0.3.0)
238
- net-http (0.4.1)
239
- uri
245
+ net-http (0.9.1)
246
+ uri (>= 0.11.1)
240
247
  netrc (0.11.0)
241
- nokogiri (1.16.7)
248
+ nokogiri (1.19.4)
242
249
  mini_portile2 (~> 2.8.2)
243
250
  racc (~> 1.4)
244
251
  notiffany (0.1.3)
@@ -260,12 +267,13 @@ GEM
260
267
  pry (0.14.2)
261
268
  coderay (~> 1.1)
262
269
  method_source (~> 1.0)
263
- pry-byebug (3.10.1)
264
- byebug (~> 11.0)
265
- pry (>= 0.13, < 0.15)
266
- public_suffix (6.0.1)
270
+ pry-byebug (3.12.0)
271
+ byebug (~> 13.0)
272
+ pry (>= 0.13, < 0.17)
273
+ pstore (0.2.1)
274
+ public_suffix (7.0.5)
267
275
  racc (1.8.1)
268
- rack (3.2.1)
276
+ rack (3.2.7)
269
277
  rainbow (3.1.1)
270
278
  rake (13.2.1)
271
279
  rb-fsevent (0.11.2)
@@ -274,6 +282,8 @@ GEM
274
282
  rbs (2.8.4)
275
283
  rchardet (1.8.0)
276
284
  regexp_parser (2.11.2)
285
+ reline (0.7.0)
286
+ io-console (~> 0.5)
277
287
  representable (3.2.0)
278
288
  declarative (< 0.1.0)
279
289
  trailblazer-option (>= 0.1.1, < 0.2.0)
@@ -286,7 +296,7 @@ GEM
286
296
  retriable (3.1.2)
287
297
  reverse_markdown (2.1.1)
288
298
  nokogiri
289
- rexml (3.3.8)
299
+ rexml (3.4.4)
290
300
  rouge (4.4.0)
291
301
  rspec (3.13.0)
292
302
  rspec-core (~> 3.13.0)
@@ -349,12 +359,12 @@ GEM
349
359
  sawyer (0.9.2)
350
360
  addressable (>= 2.3.5)
351
361
  faraday (>= 0.17.3, < 3)
362
+ securerandom (0.4.1)
352
363
  shellany (0.0.1)
353
- signet (0.19.0)
364
+ signet (0.22.0)
354
365
  addressable (~> 2.8)
355
366
  faraday (>= 0.17.5, < 3.a)
356
- jwt (>= 1.5, < 3.0)
357
- multi_json (~> 1.10)
367
+ jwt (>= 1.5, < 4.0)
358
368
  simplecov (0.22.0)
359
369
  docile (~> 1.1)
360
370
  simplecov-html (~> 0.11)
@@ -398,7 +408,7 @@ GEM
398
408
  unparser (0.6.15)
399
409
  diff-lcs (~> 1.3)
400
410
  parser (>= 3.3.0)
401
- uri (0.13.1)
411
+ uri (1.1.1)
402
412
  webmock (3.26.4)
403
413
  addressable (>= 2.8.0)
404
414
  crack (>= 0.3.2)
@@ -417,7 +427,7 @@ DEPENDENCIES
417
427
  gitlab_quality-test_tooling!
418
428
  guard-rspec (~> 4.7)
419
429
  lefthook (~> 1.3)
420
- pry-byebug (= 3.10.1)
430
+ pry-byebug (= 3.12.0)
421
431
  rake (~> 13.0)
422
432
  rest-client (~> 2.1.0)
423
433
  rspec (~> 3.12)
data/README.md CHANGED
@@ -51,6 +51,7 @@ Purpose: Post a message to Slack, and optionally add a test summary table based
51
51
  Usage: exe/post-to-slack [options]
52
52
  -w SLACK_WEBHOOK_URL, Slack webhook URL
53
53
  --slack-webhook-url
54
+ -b, --slack-bot-token TOKEN Slack bot token, used instead of the webhook URL when set
54
55
  -c, --channel CHANNEL Slack channel to post the message to
55
56
  -m, --message MESSAGE Post message to Slack
56
57
  -t FILES, Add a test summary table based on RSpec report files (JUnit XML)
@@ -247,7 +248,7 @@ Usage: exe/feature-readiness-evaluation [options]
247
248
 
248
249
  Runs in one of three modes:
249
250
 
250
- - **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.
251
+ - **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 table. Requires the full argument set.
251
252
  - **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.
252
253
  - **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.
253
254
 
data/exe/post-to-slack CHANGED
@@ -15,8 +15,14 @@ gitlab_api_token = nil
15
15
  options = OptionParser.new do |opts|
16
16
  opts.banner = "Usage: #{$PROGRAM_NAME} [options]"
17
17
 
18
- opts.on('-w', '--slack-webhook-url SLACK_WEBHOOK_URL', String, 'Slack webhook URL') do |slack_webhook_url|
19
- params[:slack_webhook_url] = slack_webhook_url
18
+ # No String type on the webhook URL and bot token: OptionParser's String rejects "",
19
+ # and CI passes an unset variable as "".
20
+ opts.on('-w', '--slack-webhook-url SLACK_WEBHOOK_URL', 'Slack webhook URL') do |slack_webhook_url|
21
+ params[:slack_webhook_url] = slack_webhook_url unless slack_webhook_url.empty?
22
+ end
23
+
24
+ opts.on('-b', '--slack-bot-token TOKEN', 'Slack bot token, used instead of the webhook URL when set') do |token|
25
+ params[:slack_bot_token] = token unless token.empty?
20
26
  end
21
27
 
22
28
  opts.on('-a', '--gitlab-api-token TOKEN', String, 'GitLab API token') do |token|
data/exe/test-coverage CHANGED
@@ -11,7 +11,6 @@ require_relative '../lib/gitlab_quality/test_tooling/code_coverage/category_owne
11
11
  require_relative '../lib/gitlab_quality/test_tooling/code_coverage/click_house/category_owners_table'
12
12
  require_relative '../lib/gitlab_quality/test_tooling/code_coverage/click_house/coverage_metrics_table'
13
13
  require_relative '../lib/gitlab_quality/test_tooling/code_coverage/click_house/per_test_coverage_table'
14
- require_relative '../lib/gitlab_quality/test_tooling/code_coverage/click_house/test_file_mappings_table'
15
14
  require_relative '../lib/gitlab_quality/test_tooling/code_coverage/click_house/jest_quarantined_tests_table'
16
15
  require_relative '../lib/gitlab_quality/test_tooling/code_coverage/click_house/test_health_risk_aggregator'
17
16
  require_relative '../lib/gitlab_quality/test_tooling/code_coverage/click_house/test_health_score_snapshotter'
@@ -20,7 +19,6 @@ require_relative '../lib/gitlab_quality/test_tooling/code_coverage/lcov_file'
20
19
  require_relative '../lib/gitlab_quality/test_tooling/code_coverage/artifacts'
21
20
  require_relative '../lib/gitlab_quality/test_tooling/code_coverage/test_report'
22
21
  require_relative '../lib/gitlab_quality/test_tooling/code_coverage/test_map'
23
- require_relative '../lib/gitlab_quality/test_tooling/code_coverage/test_file_mapping_data'
24
22
  require_relative '../lib/gitlab_quality/test_tooling/code_coverage/per_test_coverage_data'
25
23
  require_relative '../lib/gitlab_quality/test_tooling/code_coverage/per_test_coverage_exporter'
26
24
  require_relative '../lib/gitlab_quality/test_tooling/code_coverage/source_file_classifier'
@@ -279,12 +277,4 @@ when :full_export
279
277
  **clickhouse_data
280
278
  )
281
279
  coverage_metrics_table.push(coverage_data.as_db_table)
282
-
283
- # Export test-to-file mappings
284
- test_file_mapping_data = GitlabQuality::TestTooling::CodeCoverage::TestFileMappingData.new(
285
- test_to_sources,
286
- tests_to_categories: tests_to_categories
287
- )
288
- test_file_mappings_table = GitlabQuality::TestTooling::CodeCoverage::ClickHouse::TestFileMappingsTable.new(**shared_clickhouse_data)
289
- test_file_mappings_table.push(test_file_mapping_data.as_db_table)
290
280
  end
@@ -135,15 +135,6 @@ dependent:
135
135
  - "^spec/features/"
136
136
  ```
137
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
138
  ## CLI
148
139
 
149
140
  Example usage:
@@ -40,13 +40,14 @@ module GitlabQuality
40
40
  end
41
41
 
42
42
  def post_exception_to_slack(error)
43
- return unless ENV['CI_SLACK_WEBHOOK_URL']
43
+ return if ENV.values_at('CI_SLACK_BOT_TOKEN', 'CI_SLACK_WEBHOOK_URL').all? { |value| value.to_s.empty? }
44
44
 
45
45
  pipeline = Runtime::Env.pipeline_from_project_name
46
46
  channel = Runtime::Env.slack_alerts_channel
47
47
 
48
48
  slack_options = {
49
49
  slack_webhook_url: ENV.fetch('CI_SLACK_WEBHOOK_URL', nil),
50
+ slack_bot_token: ENV.fetch('CI_SLACK_BOT_TOKEN', nil),
50
51
  channel: channel,
51
52
  username: "GitLab Quality Test Tooling",
52
53
  icon_emoji: ':ci_failing:',
@@ -59,6 +60,9 @@ module GitlabQuality
59
60
  puts "Posting Slack message to channel: #{channel}"
60
61
 
61
62
  GitlabQuality::TestTooling::Slack::PostToSlack.new(**slack_options).invoke!
63
+ # The caller re-raises the GitLab error after this, so a failed alert must not replace it.
64
+ rescue StandardError => e
65
+ warn("Failed to post the error to Slack: #{e.message}")
62
66
  end
63
67
 
64
68
  def ignore_gitlab_client_exceptions
@@ -122,7 +122,7 @@ module GitlabQuality
122
122
  issue_attrs = {}
123
123
 
124
124
  new_description = new_issue_description(test)
125
- issue_attrs[:description] = new_description if issue.description != new_description
125
+ issue_attrs[:description] = new_description unless issue.description.to_s.include?(new_description.strip)
126
126
 
127
127
  new_labels = up_to_date_labels(test: test, issue: issue).to_a
128
128
  issue_attrs[:add_labels] = new_labels if (new_labels - issue.labels).any?
@@ -8,9 +8,12 @@ module GitlabQuality
8
8
  class PostToSlack
9
9
  MAX_PATTERN_MESSAGE_LENGTH = 150
10
10
  MAX_GROUPED_FAILURES_TO_DISPLAY = 10
11
+ POST_MESSAGE_URL = 'https://slack.com/api/chat.postMessage'
11
12
 
12
- def initialize(slack_webhook_url:, channel:, message:, username:, icon_emoji:, environment_issues_file: nil)
13
+ # Posts with slack_bot_token when it is set and not empty, otherwise to slack_webhook_url.
14
+ def initialize(channel:, message:, username:, icon_emoji:, slack_webhook_url: nil, slack_bot_token: nil, environment_issues_file: nil)
13
15
  @slack_webhook_url = slack_webhook_url
16
+ @slack_bot_token = slack_bot_token
14
17
  @channel = channel
15
18
  @message = message
16
19
  @username = username
@@ -25,6 +28,8 @@ module GitlabQuality
25
28
  params['icon_emoji'] = icon_emoji
26
29
  params['text'] = build_message
27
30
 
31
+ return post_with_token(params) unless slack_bot_token.to_s.empty?
32
+
28
33
  Support::HttpRequest.make_http_request(
29
34
  method: 'post',
30
35
  url: slack_webhook_url,
@@ -35,7 +40,29 @@ module GitlabQuality
35
40
 
36
41
  private
37
42
 
38
- attr_reader :slack_webhook_url, :channel, :message, :username, :icon_emoji, :environment_issues_file
43
+ attr_reader :slack_webhook_url, :slack_bot_token, :channel, :message, :username, :icon_emoji, :environment_issues_file
44
+
45
+ # chat.postMessage answers HTTP 200 with {"ok":false,...} on errors, so the body decides.
46
+ def post_with_token(params)
47
+ response = Support::HttpRequest.make_http_request(
48
+ method: 'post',
49
+ url: POST_MESSAGE_URL,
50
+ params: params,
51
+ headers: { 'Authorization' => "Bearer #{slack_bot_token}" },
52
+ show_response: true
53
+ )
54
+
55
+ raise "Slack did not accept the message: #{response.body}" unless accepted?(response.body.to_s)
56
+
57
+ response
58
+ end
59
+
60
+ def accepted?(body)
61
+ parsed = JSON.parse(body)
62
+ parsed.is_a?(Hash) && parsed['ok'] == true
63
+ rescue JSON::ParserError
64
+ false
65
+ end
39
66
 
40
67
  def build_message
41
68
  messages = []
@@ -295,6 +295,7 @@ module GitlabQuality
295
295
  channel = Runtime::Env.slack_alerts_channel
296
296
  slack_options = {
297
297
  slack_webhook_url: ENV.fetch('CI_SLACK_WEBHOOK_URL', nil),
298
+ slack_bot_token: ENV.fetch('CI_SLACK_BOT_TOKEN', nil),
298
299
  channel: channel,
299
300
  username: "GitLab Quality Test Tooling",
300
301
  icon_emoji: ':warning:',
@@ -2,6 +2,6 @@
2
2
 
3
3
  module GitlabQuality
4
4
  module TestTooling
5
- VERSION = "3.22.1"
5
+ VERSION = "3.23.1"
6
6
  end
7
7
  end
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: 3.22.1
4
+ version: 3.23.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - GitLab Quality
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-10-07 00:00:00.000000000 Z
11
+ date: 2026-10-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: climate_control
@@ -86,14 +86,14 @@ dependencies:
86
86
  requirements:
87
87
  - - '='
88
88
  - !ruby/object:Gem::Version
89
- version: 3.10.1
89
+ version: 3.12.0
90
90
  type: :development
91
91
  prerelease: false
92
92
  version_requirements: !ruby/object:Gem::Requirement
93
93
  requirements:
94
94
  - - '='
95
95
  - !ruby/object:Gem::Version
96
- version: 3.10.1
96
+ version: 3.12.0
97
97
  - !ruby/object:Gem::Dependency
98
98
  name: rake
99
99
  requirement: !ruby/object:Gem::Requirement
@@ -502,7 +502,6 @@ files:
502
502
  - lib/gitlab_quality/test_tooling/code_coverage/click_house/jest_quarantined_tests_table.rb
503
503
  - lib/gitlab_quality/test_tooling/code_coverage/click_house/per_test_coverage_table.rb
504
504
  - lib/gitlab_quality/test_tooling/code_coverage/click_house/table.rb
505
- - lib/gitlab_quality/test_tooling/code_coverage/click_house/test_file_mappings_table.rb
506
505
  - lib/gitlab_quality/test_tooling/code_coverage/click_house/test_health_risk_aggregation.sql
507
506
  - lib/gitlab_quality/test_tooling/code_coverage/click_house/test_health_risk_aggregator.rb
508
507
  - lib/gitlab_quality/test_tooling/code_coverage/click_house/test_health_score_snapshot.sql
@@ -515,7 +514,6 @@ files:
515
514
  - lib/gitlab_quality/test_tooling/code_coverage/responsibility_patterns_config.rb
516
515
  - lib/gitlab_quality/test_tooling/code_coverage/rspec_report.rb
517
516
  - lib/gitlab_quality/test_tooling/code_coverage/source_file_classifier.rb
518
- - lib/gitlab_quality/test_tooling/code_coverage/test_file_mapping_data.rb
519
517
  - lib/gitlab_quality/test_tooling/code_coverage/test_map.rb
520
518
  - lib/gitlab_quality/test_tooling/code_coverage/test_report.rb
521
519
  - lib/gitlab_quality/test_tooling/code_coverage/utils.rb
@@ -1,263 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative 'table'
4
-
5
- module GitlabQuality
6
- module TestTooling
7
- module CodeCoverage
8
- module ClickHouse
9
- class TestFileMappingsTable < GitlabQuality::TestTooling::CodeCoverage::ClickHouse::Table
10
- TABLE_NAME = "test_file_mappings"
11
-
12
- # Override push to skip mappings already stored with the same values.
13
- #
14
- # @param data [Array<Hash>] Code coverage related data to be pushed to ClickHouse
15
- # @return [nil]
16
- def push(data) # rubocop:disable Metrics/AbcSize
17
- return logger.warn("#{LOG_PREFIX} No data found, skipping ClickHouse export!") if data.empty?
18
-
19
- logger.debug("#{LOG_PREFIX} Starting data export to ClickHouse")
20
- @dropped_source_files = []
21
- sanitized_data = sanitize(data)
22
- report_dropped_source_files(sanitized_data.size)
23
-
24
- return logger.warn("#{LOG_PREFIX} No valid data found after sanitization, skipping ClickHouse export!") if sanitized_data.empty?
25
-
26
- # Filter out records that already exist with identical values
27
- new_or_changed_records = filter_duplicates(sanitized_data)
28
-
29
- if new_or_changed_records.empty?
30
- logger.info("#{LOG_PREFIX} None of the #{sanitized_data.size} mappings need writing to #{full_table_name}, skipping insert")
31
- return
32
- end
33
-
34
- client.insert_json_data(table_name, new_or_changed_records)
35
- skipped_count = sanitized_data.size - new_or_changed_records.size
36
- logger.info("#{LOG_PREFIX} Successfully pushed #{new_or_changed_records.size} new/changed records " \
37
- "to #{full_table_name} (#{skipped_count} not written)")
38
- rescue StandardError => e
39
- logger.error("#{LOG_PREFIX} Error occurred while pushing data to #{full_table_name}: #{e.message}")
40
- raise
41
- end
42
-
43
- private
44
-
45
- # @return [Boolean] True if the record is valid, false otherwise
46
- def valid_record?(record)
47
- valid_test_file?(record) && valid_source_file?(record)
48
- end
49
-
50
- # @return [Boolean] True if the test_file field is present
51
- def valid_test_file?(record)
52
- return true unless record[:test_file].blank?
53
-
54
- logger.warn("#{LOG_PREFIX} Skipping record with nil/empty test_file: #{record}")
55
- false
56
- end
57
-
58
- # @return [Boolean] True if the source_file field is present
59
- def valid_source_file?(record)
60
- return true unless record[:source_file].blank?
61
-
62
- logger.warn("#{LOG_PREFIX} Skipping record with nil/empty source_file: #{record}")
63
- false
64
- end
65
-
66
- # @return [Hash, nil] Transformed mapping data including timestamp and CI metadata,
67
- # or nil when the source path cannot be made repo-relative
68
- def sanitized_data_record(record)
69
- source_file = relative_source_file(record[:source_file])
70
-
71
- if source_file.nil?
72
- dropped_source_files << record[:source_file]
73
- return
74
- end
75
-
76
- {
77
- timestamp: time,
78
- test_file: record[:test_file],
79
- source_file: source_file,
80
- # CI_PROJECT_PATH is set by GitLab CI runner and considered trusted infrastructure input.
81
- # In non-CI environments, this may be user-controlled, but sanitize_for_clickhouse
82
- # provides SQL injection protection.
83
- ci_project_path: ENV.fetch('CI_PROJECT_PATH', nil),
84
- category: record[:category] || ''
85
- }
86
- end
87
-
88
- def dropped_source_files
89
- @dropped_source_files ||= []
90
- end
91
-
92
- # Reports the records that relative_source_file could not make repo-relative, once
93
- # for the whole run, and raises when none survived. A CI_PROJECT_DIR that does not
94
- # match the artifact's paths drops every mapping, so warning per record buries the
95
- # job trace in one line per mapping, and warning alone would let a wholly empty
96
- # export finish green.
97
- #
98
- # @return [void]
99
- def report_dropped_source_files(kept_count)
100
- return if dropped_source_files.empty?
101
-
102
- message = "#{LOG_PREFIX} Skipped #{dropped_source_files.size} records with " \
103
- "non-relative source_file, samples: #{dropped_source_files.first(3).join(', ')}"
104
-
105
- raise message if kept_count.zero?
106
-
107
- logger.warn(message)
108
- end
109
-
110
- # The repo-relative form of a source path, which is what the table's
111
- # valid_source_file_format constraint accepts. Returns nil when the path cannot be
112
- # made relative or normalizes to nothing, and the caller drops that record, because
113
- # ClickHouse rejects the whole insert batch on a constraint violation rather than
114
- # the offending row.
115
- #
116
- # @return [String, nil]
117
- def relative_source_file(path)
118
- ci_project_dir = ENV.fetch('CI_PROJECT_DIR', nil)
119
-
120
- relative = path
121
- relative = relative.delete_prefix("#{ci_project_dir.chomp('/')}/") if ci_project_dir.present?
122
- relative = relative.sub(%r{\A(\./)+}, '')
123
-
124
- relative.presence unless relative.start_with?('/')
125
- end
126
-
127
- # The records worth writing: mappings this project has not stored yet, plus ones
128
- # whose category changed, except that a blank never replaces a stored category.
129
- # Re-sending an unchanged mapping costs a row that survives until the table's TTL
130
- # expires it, because section and stage are still part of the sort key and the
131
- # values written here no longer match the stored ones.
132
- #
133
- # If the stored categories cannot be read, see #insertable_without_lookup.
134
- #
135
- # @param records [Array<Hash>] Sanitized records to insert
136
- # @return [Array<Hash>] Records that are new or have changed values
137
- def filter_duplicates(records) # rubocop:disable Metrics/AbcSize
138
- return records if records.empty?
139
-
140
- # Assumes all records belong to the same project (batch from single pipeline run)
141
- ci_project_path = records.first[:ci_project_path]
142
- return records if ci_project_path.blank? # Can't query without project path
143
-
144
- logger.debug("#{LOG_PREFIX} Checking for duplicate mappings in #{full_table_name}")
145
-
146
- existing_mappings = fetch_existing_mappings(ci_project_path, records)
147
- return insertable_without_lookup(records) if existing_mappings.nil?
148
-
149
- blank_suppressed = 0
150
-
151
- new_or_changed = records.select do |record|
152
- key = "#{record[:test_file]}|#{record[:source_file]}"
153
- existing = existing_mappings[key]
154
-
155
- # New mapping - doesn't exist yet
156
- next true if existing.nil?
157
-
158
- # A blank category means this pipeline's rspec reports didn't cover the test,
159
- # not that the test has no category: the mappings come from the Crystalball map,
160
- # which lists every test file regardless of what ran. Writing the blank erases a
161
- # known category, and readers resolving ownership through category get nothing.
162
- if record[:category].blank? && existing[:category].present?
163
- blank_suppressed += 1
164
- next false
165
- end
166
-
167
- existing[:category] != record[:category]
168
- end
169
-
170
- log_blank_suppressions(blank_suppressed)
171
- logger.debug("#{LOG_PREFIX} Found #{new_or_changed.size} new/changed mappings out of #{records.size} total")
172
- new_or_changed
173
- rescue StandardError => e
174
- logger.warn("#{LOG_PREFIX} Failed to check for duplicates: #{e.message}")
175
- insertable_without_lookup(records)
176
- end
177
-
178
- # What to write when the stored categories could not be read. Records carrying a
179
- # category still go in, because a failed read must not silently drop mappings.
180
- # Blanks are held back: with no stored value to compare against they look new, and
181
- # writing one replaces whatever category is already there. The mapping comes back
182
- # on the next export that can read.
183
- #
184
- # @param records [Array<Hash>] Sanitized records to insert
185
- # @return [Array<Hash>] Records that carry a category
186
- def insertable_without_lookup(records)
187
- with_category = records.reject { |record| record[:category].blank? }
188
-
189
- logger.warn("#{LOG_PREFIX} No duplicate check possible; inserting " \
190
- "#{with_category.size} of #{records.size} mappings and holding back " \
191
- "#{records.size - with_category.size} with no category")
192
-
193
- with_category
194
- end
195
-
196
- # Reported at info because it is the only signal that a pipeline's reports were
197
- # incomplete. A steady non-zero count across runs means categories are arriving
198
- # from fewer reports than there are test files in the map.
199
- #
200
- # @param count [Integer] Mappings whose stored category was kept over an incoming blank
201
- # @return [nil]
202
- def log_blank_suppressions(count)
203
- return unless count.positive?
204
-
205
- logger.info("#{LOG_PREFIX} Kept the stored category for #{count} mappings " \
206
- "this pipeline's reports did not cover")
207
- end
208
-
209
- # The category currently stored against each of this project's mappings. Only
210
- # filter_duplicates reads it.
211
- #
212
- # @param ci_project_path [String] The CI project path to filter by
213
- # @param records [Array<Hash>] Records being inserted (used to limit query scope)
214
- # @return [Hash, nil] "test_file|source_file" => { category: String }, or nil when
215
- # the read fails. An empty Hash means the table holds nothing for this project.
216
- def fetch_existing_mappings(ci_project_path, records) # rubocop:disable Metrics/AbcSize
217
- # Query for ALL latest mappings for this project
218
- # We filter in Ruby rather than building dynamic SQL to avoid injection risks
219
- # Aggregate rather than matching rows against a set of the latest timestamps.
220
- # The set form has to materialise one tuple per stored mapping before it can
221
- # filter anything, which exceeds the server's memory limit at this table's
222
- # size; argMax streams and peaks at roughly a quarter of it.
223
- sql = <<~SQL
224
- SELECT
225
- test_file,
226
- source_file,
227
- argMax(category, timestamp) AS category
228
- FROM #{full_table_name}
229
- WHERE ci_project_path = {project_path:String}
230
- GROUP BY test_file, source_file
231
- SQL
232
-
233
- # Substitute project_path value using manual escaping (client doesn't support parameterized queries)
234
- sql_with_params = sql.strip.gsub('{project_path:String}', "'#{sanitize_for_clickhouse(ci_project_path)}'")
235
- results = client.query(sql_with_params)
236
-
237
- # Build lookup hash, filtering to only the test_files we're checking if dataset is small
238
- test_files_set = records.to_set { |r| r[:test_file] } if records.size <= 10_000
239
-
240
- results.each_with_object({}) do |row, hash|
241
- # Skip if we're doing selective filtering and this test_file isn't in our set
242
- next if test_files_set && !test_files_set.include?(row['test_file'])
243
-
244
- key = "#{row['test_file']}|#{row['source_file']}"
245
- hash[key] = { category: row['category'] }
246
- end
247
- rescue StandardError => e
248
- logger.warn("#{LOG_PREFIX} Failed to fetch existing mappings: #{e.message}")
249
- nil # nil, not {}: an empty table and an unreadable one need different handling
250
- end
251
-
252
- # Sanitize string for ClickHouse by escaping single quotes and backslashes
253
- # This protects against SQL injection when we must use string interpolation
254
- # @param str [String] String to sanitize
255
- # @return [String] Sanitized string
256
- def sanitize_for_clickhouse(str)
257
- str.to_s.gsub(/\\/, '\\\\\\\\').gsub("'", "''") # rubocop:disable Style/RedundantRegexpArgument
258
- end
259
- end
260
- end
261
- end
262
- end
263
- end
@@ -1,49 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module GitlabQuality
4
- module TestTooling
5
- module CodeCoverage
6
- class TestFileMappingData
7
- # @param [Hash<String, Array<String>>] test_to_sources Test files
8
- # mapped to all source files they cover
9
- # @param [Hash<String, Array<String>>] tests_to_categories Test files
10
- # mapped to their feature categories
11
- def initialize(test_to_sources, tests_to_categories: {})
12
- @test_to_sources = test_to_sources
13
- @tests_to_categories = tests_to_categories
14
- end
15
-
16
- # Rows for shared.test_file_mappings, one per test-and-source pair.
17
- #
18
- # Carries no group, stage or section on purpose. Those describe the category
19
- # rather than the mapping and change whenever stages.yml is reorganised, so
20
- # storing them on every mapping multiplies rows for no gain.
21
- #
22
- # Readers resolve ownership from shared.category_owners through category. That
23
- # table keeps every historical row, up to 57 for one category, so a plain join
24
- # fans out; dedupe it with ORDER BY category, timestamp DESC LIMIT 1 BY category.
25
- #
26
- # @return [Array<Hash<Symbol, String>>] Mapping data formatted for database insertion
27
- # @example Return value
28
- # [
29
- # { test_file: "spec/models/user_spec.rb", source_file: "app/models/user.rb",
30
- # category: "team_planning" },
31
- # ...
32
- # ]
33
- def as_db_table
34
- @test_to_sources.flat_map do |test_file, source_files|
35
- category = @tests_to_categories[test_file]&.first || ''
36
-
37
- source_files.map do |source_file|
38
- {
39
- test_file: test_file,
40
- source_file: source_file,
41
- category: category
42
- }
43
- end
44
- end
45
- end
46
- end
47
- end
48
- end
49
- end