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 +4 -4
- data/Gemfile.lock +67 -57
- data/README.md +2 -1
- data/exe/post-to-slack +8 -2
- data/exe/test-coverage +0 -10
- data/lib/gitlab_quality/test_tooling/code_coverage/README.md +0 -9
- data/lib/gitlab_quality/test_tooling/gitlab_client/gitlab_client.rb +5 -1
- data/lib/gitlab_quality/test_tooling/report/report_as_issue.rb +1 -1
- data/lib/gitlab_quality/test_tooling/slack/post_to_slack.rb +29 -2
- data/lib/gitlab_quality/test_tooling/test_meta/test_meta_updater.rb +1 -0
- data/lib/gitlab_quality/test_tooling/version.rb +1 -1
- metadata +4 -6
- data/lib/gitlab_quality/test_tooling/code_coverage/click_house/test_file_mappings_table.rb +0 -263
- data/lib/gitlab_quality/test_tooling/code_coverage/test_file_mapping_data.rb +0 -49
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 49a063bdf7dc0b0055d926d178df7a5274c067f352a5feae0d5f04695702ea01
|
|
4
|
+
data.tar.gz: 426701ca7b4f8b55c433bfd78b173f1e731c1f55dafd099584acb0366b6f77de
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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.
|
|
22
|
+
activesupport (7.2.4)
|
|
23
23
|
base64
|
|
24
|
+
benchmark (>= 0.3)
|
|
24
25
|
bigdecimal
|
|
25
|
-
concurrent-ruby (~> 1.0, >= 1.
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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.
|
|
41
|
+
base64 (0.3.0)
|
|
40
42
|
benchmark (0.3.0)
|
|
41
|
-
bigdecimal (
|
|
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 (
|
|
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.
|
|
55
|
-
connection_pool (2.
|
|
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.
|
|
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.
|
|
85
|
+
drb (2.2.3)
|
|
83
86
|
e2mmap (0.1.0)
|
|
84
87
|
excon (1.6.0)
|
|
85
88
|
logger
|
|
86
|
-
faraday (2.
|
|
87
|
-
faraday-net_http (>= 2.0, < 3.
|
|
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.
|
|
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.
|
|
147
|
+
google-apis-compute_v1 (0.155.0)
|
|
143
148
|
google-apis-core (>= 0.15.0, < 2.a)
|
|
144
|
-
google-apis-core (
|
|
145
|
-
addressable (~> 2.
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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 (>=
|
|
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
|
-
|
|
172
|
+
google-logging-utils (0.2.0)
|
|
173
|
+
googleauth (1.17.4)
|
|
167
174
|
faraday (>= 1.0, < 3.a)
|
|
168
|
-
google-cloud-env (~> 2.
|
|
169
|
-
|
|
170
|
-
|
|
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.
|
|
206
|
+
httparty (0.24.3)
|
|
199
207
|
csv
|
|
200
208
|
mini_mime (>= 1.0.0)
|
|
201
209
|
multi_xml (>= 0.5.2)
|
|
202
|
-
|
|
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.
|
|
207
|
-
jwt (
|
|
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.
|
|
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.
|
|
230
|
-
minitest (5.
|
|
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.
|
|
234
|
-
bigdecimal (
|
|
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.
|
|
239
|
-
uri
|
|
245
|
+
net-http (0.9.1)
|
|
246
|
+
uri (>= 0.11.1)
|
|
240
247
|
netrc (0.11.0)
|
|
241
|
-
nokogiri (1.
|
|
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.
|
|
264
|
-
byebug (~>
|
|
265
|
-
pry (>= 0.13, < 0.
|
|
266
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
364
|
+
signet (0.22.0)
|
|
354
365
|
addressable (~> 2.8)
|
|
355
366
|
faraday (>= 0.17.5, < 3.a)
|
|
356
|
-
jwt (>= 1.5, <
|
|
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 (
|
|
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.
|
|
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
|
|
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
|
-
|
|
19
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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:',
|
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.
|
|
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-
|
|
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.
|
|
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.
|
|
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
|