gitlab_quality-test_tooling 1.13.0 → 1.14.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0b5576ca28a8e8cbf76564017a53418fd059747789bbbb8146835285d8f5b37c
4
- data.tar.gz: b1d7c6dd581dccabe91f64687b420a83f5561aa407e1e4583e7a5146183890f4
3
+ metadata.gz: 9d4626e6b818482d139f6256f13044ae3434b93989c377526485aae69f32b9eb
4
+ data.tar.gz: 148291a52850f6b6b5612dc0e1c46d2a6af0115d771c3ad3c9d1a0af7dbe2280
5
5
  SHA512:
6
- metadata.gz: d245b9be5a9a127630d934096277c339e649e9e719727f871d8b794245b385e6f4dbfba3175c546f44b2ee833099ef5398738dcaa631bab39b44eb9734fe338f
7
- data.tar.gz: 33a391d891853d45e75198462d504641d090e0f619e31a9fbf5c613b4fe5cef6443d6b465e9e2ffc27d643a3d8d8426030bb10c872a83ca0a2def1b0232ae0ae
6
+ metadata.gz: a2fca44f7bf0ccda3cf905e561b8d52f7462df04c35f54a92d61fed7f4e6121e82bd2d3d60757f46b1c8d2cd30e788ac72aad4fd9d96e5607e50960437c7c58c
7
+ data.tar.gz: 29a5e42c5c8f34b76e2aabb6280f0150a4aedf5d34a43bf44aef4be25aacb71d6a5f1bd9942f0593ec90626db3079cc2ad2fd4ee40ef084e9de9bd738cd427d4
data/Gemfile.lock CHANGED
@@ -1,14 +1,15 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- gitlab_quality-test_tooling (1.13.0)
5
- activesupport (>= 6.1, < 7.2)
4
+ gitlab_quality-test_tooling (1.14.1)
5
+ activesupport (>= 6.1, < 7.1)
6
6
  amatch (~> 0.4.1)
7
7
  gitlab (~> 4.19)
8
8
  http (~> 5.0)
9
9
  nokogiri (~> 1.10)
10
10
  parallel (>= 1, < 2)
11
11
  rainbow (>= 3, < 4)
12
+ rspec-parameterized (~> 1.0.0)
12
13
  table_print (= 1.5.7)
13
14
  zeitwerk (>= 2, < 3)
14
15
 
@@ -20,15 +21,16 @@ GEM
20
21
  i18n (>= 1.6, < 2)
21
22
  minitest (>= 5.1)
22
23
  tzinfo (~> 2.0)
23
- zeitwerk (~> 2.3)
24
- addressable (2.8.4)
24
+ addressable (2.8.6)
25
25
  public_suffix (>= 2.0.2, < 6.0)
26
26
  amatch (0.4.1)
27
27
  mize
28
28
  tins (~> 1.0)
29
29
  ast (2.4.2)
30
30
  backport (1.2.0)
31
- benchmark (0.2.1)
31
+ benchmark (0.3.0)
32
+ binding_of_caller (1.0.0)
33
+ debug_inspector (>= 0.0.1)
32
34
  byebug (11.1.3)
33
35
  claide (1.1.0)
34
36
  claide-plugins (0.9.2)
@@ -38,60 +40,60 @@ GEM
38
40
  climate_control (1.2.0)
39
41
  coderay (1.1.3)
40
42
  colored2 (3.1.2)
41
- concurrent-ruby (1.2.2)
43
+ concurrent-ruby (1.2.3)
42
44
  cork (0.3.0)
43
45
  colored2 (~> 3.1)
44
46
  crack (0.4.5)
45
47
  rexml
46
- danger (9.3.0)
48
+ danger (9.4.2)
47
49
  claide (~> 1.0)
48
50
  claide-plugins (>= 0.9.2)
49
51
  colored2 (~> 3.1)
50
52
  cork (~> 0.1)
51
53
  faraday (>= 0.9.0, < 3.0)
52
54
  faraday-http-cache (~> 2.0)
53
- git (~> 1.13.0)
55
+ git (~> 1.13)
54
56
  kramdown (~> 2.3)
55
57
  kramdown-parser-gfm (~> 1.0)
56
58
  no_proxy_fix
57
- octokit (~> 5.0)
59
+ octokit (>= 4.0)
58
60
  terminal-table (>= 1, < 4)
59
61
  danger-gitlab (8.0.0)
60
62
  danger
61
63
  gitlab (~> 4.2, >= 4.2.0)
64
+ debug_inspector (1.2.0)
62
65
  diff-lcs (1.5.0)
63
66
  docile (1.4.0)
64
- domain_name (0.5.20190701)
65
- unf (>= 0.0.5, < 1.0.0)
67
+ domain_name (0.6.20240107)
66
68
  e2mmap (0.1.0)
67
- faraday (2.7.4)
68
- faraday-net_http (>= 2.0, < 3.1)
69
- ruby2_keywords (>= 0.0.4)
70
- faraday-http-cache (2.5.0)
69
+ faraday (2.9.0)
70
+ faraday-net_http (>= 2.0, < 3.2)
71
+ faraday-http-cache (2.5.1)
71
72
  faraday (>= 0.8)
72
- faraday-net_http (3.0.2)
73
- ffi (1.15.5)
73
+ faraday-net_http (3.1.0)
74
+ net-http
75
+ ffi (1.16.3)
74
76
  ffi-compiler (1.0.1)
75
77
  ffi (>= 1.0.0)
76
78
  rake
77
79
  formatador (1.1.0)
78
- git (1.13.2)
80
+ git (1.19.1)
79
81
  addressable (~> 2.8)
80
82
  rchardet (~> 1.8)
81
83
  gitlab (4.19.0)
82
84
  httparty (~> 0.20)
83
85
  terminal-table (>= 1.5.1)
84
- gitlab-dangerfiles (3.8.0)
86
+ gitlab-dangerfiles (3.13.0)
85
87
  danger (>= 8.4.5)
86
88
  danger-gitlab (>= 8.0.0)
87
89
  rake
88
- gitlab-styles (10.0.0)
89
- rubocop (~> 1.43.0)
90
+ gitlab-styles (10.1.0)
91
+ rubocop (~> 1.50.2)
90
92
  rubocop-graphql (~> 0.18)
91
93
  rubocop-performance (~> 1.15)
92
94
  rubocop-rails (~> 2.17)
93
- rubocop-rspec (~> 2.18)
94
- guard (2.18.0)
95
+ rubocop-rspec (~> 2.22)
96
+ guard (2.18.1)
95
97
  formatador (>= 0.2.4)
96
98
  listen (>= 2.7, < 4.0)
97
99
  lumberjack (>= 1.0.12, < 2.0)
@@ -105,7 +107,7 @@ GEM
105
107
  guard (~> 2.1)
106
108
  guard-compat (~> 1.1)
107
109
  rspec (>= 2.99.0, < 4.0)
108
- hashdiff (1.0.1)
110
+ hashdiff (1.1.0)
109
111
  http (5.1.1)
110
112
  addressable (~> 2.8)
111
113
  http-cookie (~> 1.0)
@@ -117,45 +119,52 @@ GEM
117
119
  httparty (0.21.0)
118
120
  mini_mime (>= 1.0.0)
119
121
  multi_xml (>= 0.5.2)
120
- i18n (1.13.0)
122
+ i18n (1.14.1)
121
123
  concurrent-ruby (~> 1.0)
122
- jaro_winkler (1.5.4)
123
- json (2.6.3)
124
+ jaro_winkler (1.5.6)
125
+ json (2.7.1)
124
126
  kramdown (2.4.0)
125
127
  rexml
126
128
  kramdown-parser-gfm (1.1.0)
127
129
  kramdown (~> 2.0)
128
- lefthook (1.3.11)
130
+ lefthook (1.6.1)
129
131
  listen (3.8.0)
130
132
  rb-fsevent (~> 0.10, >= 0.10.3)
131
133
  rb-inotify (~> 0.9, >= 0.9.10)
132
134
  llhttp-ffi (0.4.0)
133
135
  ffi-compiler (~> 1.0)
134
136
  rake (~> 13.0)
135
- lumberjack (1.2.8)
137
+ lumberjack (1.2.10)
136
138
  method_source (1.0.0)
137
- mini_mime (1.1.2)
138
- mini_portile2 (2.8.1)
139
- minitest (5.18.0)
139
+ mini_mime (1.1.5)
140
+ mini_portile2 (2.8.5)
141
+ minitest (5.21.2)
140
142
  mize (0.4.1)
141
143
  protocol (~> 2.0)
142
144
  multi_xml (0.6.0)
143
145
  nap (1.1.0)
144
146
  nenv (0.3.0)
147
+ net-http (0.4.1)
148
+ uri
145
149
  no_proxy_fix (0.1.2)
146
- nokogiri (1.14.3)
147
- mini_portile2 (~> 2.8.0)
150
+ nokogiri (1.16.0)
151
+ mini_portile2 (~> 2.8.2)
148
152
  racc (~> 1.4)
149
153
  notiffany (0.1.3)
150
154
  nenv (~> 0.1)
151
155
  shellany (~> 0.0)
152
- octokit (5.6.1)
156
+ octokit (8.0.0)
153
157
  faraday (>= 1, < 3)
154
158
  sawyer (~> 0.9)
155
159
  open4 (1.3.4)
156
- parallel (1.23.0)
157
- parser (3.2.2.1)
160
+ parallel (1.24.0)
161
+ parser (3.3.0.5)
158
162
  ast (~> 2.4.1)
163
+ racc
164
+ proc_to_ast (0.1.0)
165
+ coderay
166
+ parser
167
+ unparser
159
168
  protocol (2.0.0)
160
169
  ruby_parser (~> 3.0)
161
170
  pry (0.14.2)
@@ -164,20 +173,20 @@ GEM
164
173
  pry-byebug (3.10.1)
165
174
  byebug (~> 11.0)
166
175
  pry (>= 0.13, < 0.15)
167
- public_suffix (5.0.1)
168
- racc (1.6.2)
169
- rack (3.0.7)
176
+ public_suffix (5.0.4)
177
+ racc (1.7.3)
178
+ rack (3.0.8)
170
179
  rainbow (3.1.1)
171
- rake (13.0.6)
180
+ rake (13.1.0)
172
181
  rb-fsevent (0.11.2)
173
182
  rb-inotify (0.10.1)
174
183
  ffi (~> 1.0)
175
184
  rbs (2.8.4)
176
185
  rchardet (1.8.0)
177
- regexp_parser (2.8.0)
186
+ regexp_parser (2.9.0)
178
187
  reverse_markdown (2.1.1)
179
188
  nokogiri
180
- rexml (3.2.5)
189
+ rexml (3.2.6)
181
190
  rspec (3.12.0)
182
191
  rspec-core (~> 3.12.0)
183
192
  rspec-expectations (~> 3.12.0)
@@ -187,44 +196,59 @@ GEM
187
196
  rspec-expectations (3.12.3)
188
197
  diff-lcs (>= 1.2.0, < 2.0)
189
198
  rspec-support (~> 3.12.0)
190
- rspec-mocks (3.12.5)
199
+ rspec-mocks (3.12.6)
191
200
  diff-lcs (>= 1.2.0, < 2.0)
192
201
  rspec-support (~> 3.12.0)
193
- rspec-support (3.12.0)
194
- rubocop (1.43.0)
202
+ rspec-parameterized (1.0.0)
203
+ rspec-parameterized-core (< 2)
204
+ rspec-parameterized-table_syntax (< 2)
205
+ rspec-parameterized-core (1.0.0)
206
+ parser
207
+ proc_to_ast
208
+ rspec (>= 2.13, < 4)
209
+ unparser
210
+ rspec-parameterized-table_syntax (1.0.1)
211
+ binding_of_caller
212
+ rspec-parameterized-core (< 2)
213
+ rspec-support (3.12.1)
214
+ rubocop (1.50.2)
195
215
  json (~> 2.3)
196
216
  parallel (~> 1.10)
197
217
  parser (>= 3.2.0.0)
198
218
  rainbow (>= 2.2.2, < 4.0)
199
219
  regexp_parser (>= 1.8, < 3.0)
200
220
  rexml (>= 3.2.5, < 4.0)
201
- rubocop-ast (>= 1.24.1, < 2.0)
221
+ rubocop-ast (>= 1.28.0, < 2.0)
202
222
  ruby-progressbar (~> 1.7)
203
223
  unicode-display_width (>= 2.4.0, < 3.0)
204
- rubocop-ast (1.28.0)
224
+ rubocop-ast (1.30.0)
205
225
  parser (>= 3.2.1.0)
206
- rubocop-capybara (2.18.0)
226
+ rubocop-capybara (2.20.0)
227
+ rubocop (~> 1.41)
228
+ rubocop-factory_bot (2.25.1)
207
229
  rubocop (~> 1.41)
208
230
  rubocop-graphql (0.19.0)
209
231
  rubocop (>= 0.87, < 2)
210
- rubocop-performance (1.17.1)
211
- rubocop (>= 1.7.0, < 2.0)
212
- rubocop-ast (>= 0.4.0)
213
- rubocop-rails (2.19.1)
232
+ rubocop-performance (1.20.2)
233
+ rubocop (>= 1.48.1, < 2.0)
234
+ rubocop-ast (>= 1.30.0, < 2.0)
235
+ rubocop-rails (2.23.1)
214
236
  activesupport (>= 4.2.0)
215
237
  rack (>= 1.1)
216
238
  rubocop (>= 1.33.0, < 2.0)
217
- rubocop-rspec (2.20.0)
218
- rubocop (~> 1.33)
239
+ rubocop-ast (>= 1.30.0, < 2.0)
240
+ rubocop-rspec (2.26.1)
241
+ rubocop (~> 1.40)
219
242
  rubocop-capybara (~> 2.17)
243
+ rubocop-factory_bot (~> 2.22)
220
244
  ruby-progressbar (1.13.0)
221
- ruby2_keywords (0.0.5)
222
- ruby_parser (3.20.3)
245
+ ruby_parser (3.21.0)
246
+ racc (~> 1.5)
223
247
  sexp_processor (~> 4.16)
224
248
  sawyer (0.9.2)
225
249
  addressable (>= 2.3.5)
226
250
  faraday (>= 0.17.3, < 3)
227
- sexp_processor (4.17.0)
251
+ sexp_processor (4.17.1)
228
252
  shellany (0.0.1)
229
253
  simplecov (0.22.0)
230
254
  docile (~> 1.1)
@@ -235,7 +259,7 @@ GEM
235
259
  simplecov (~> 0.19)
236
260
  simplecov-html (0.12.3)
237
261
  simplecov_json_formatter (0.1.4)
238
- solargraph (0.49.0)
262
+ solargraph (0.50.0)
239
263
  backport (~> 1.2)
240
264
  benchmark
241
265
  bundler (~> 2.0)
@@ -255,23 +279,24 @@ GEM
255
279
  table_print (1.5.7)
256
280
  terminal-table (3.0.2)
257
281
  unicode-display_width (>= 1.1.1, < 3)
258
- thor (1.2.1)
259
- tilt (2.1.0)
260
- timecop (0.9.6)
282
+ thor (1.3.0)
283
+ tilt (2.3.0)
284
+ timecop (0.9.8)
261
285
  tins (1.32.1)
262
286
  sync
263
287
  tzinfo (2.0.6)
264
288
  concurrent-ruby (~> 1.0)
265
- unf (0.1.4)
266
- unf_ext
267
- unf_ext (0.0.8.2)
268
- unicode-display_width (2.4.2)
289
+ unicode-display_width (2.5.0)
290
+ unparser (0.6.12)
291
+ diff-lcs (~> 1.3)
292
+ parser (>= 3.2.2.4)
293
+ uri (0.13.0)
269
294
  webmock (3.7.0)
270
295
  addressable (>= 2.3.6)
271
296
  crack (>= 0.3.2)
272
297
  hashdiff (>= 0.4.0, < 2.0.0)
273
298
  yard (0.9.34)
274
- zeitwerk (2.6.7)
299
+ zeitwerk (2.6.12)
275
300
 
276
301
  PLATFORMS
277
302
  ruby
data/README.md CHANGED
@@ -188,6 +188,22 @@ Usage: exe/slow-test-merge-request-report-note [options]
188
188
  -h, --help Show the usage
189
189
  ```
190
190
 
191
+ ### `exe/update-test-meta`
192
+
193
+ ```shell
194
+ Purpose: Add quarantine or reliable meta to specs
195
+ Usage: exe/update-test-meta [options]
196
+ -u INPUT_FILES, File with list of unstable specs (JSON) to quarantine
197
+ --unstable-specs-file
198
+ -s INPUT_FILES, File with list of stable specs (JSON) to add :reliable meta
199
+ --stable-specs-file
200
+ -p, --project PROJECT Can be an integer or a group/project string
201
+ -t, --token TOKEN A valid access token with `api` scope and Maintainer permission in PROJECT
202
+ --dry-run Perform a dry-run (don't create branches, commits or MRs)
203
+ -v, --version Show the version
204
+ -h, --help Show the usage
205
+ ```
206
+
191
207
  ## Development
192
208
 
193
209
  ### Initial setup
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require "optparse"
6
+
7
+ require_relative "../lib/gitlab_quality/test_tooling"
8
+
9
+ params = {}
10
+
11
+ options = OptionParser.new do |opts|
12
+ opts.banner = "Usage: #{$PROGRAM_NAME} [options]"
13
+
14
+ opts.on('-u', '--unstable-specs-file INPUT_FILES', String, 'File with list of unstable specs (JSON) to quarantine') do |unstable_specs_file|
15
+ params[:unstable_specs_file] = unstable_specs_file
16
+ end
17
+
18
+ opts.on('-s', '--stable-specs-file INPUT_FILES', String, 'File with list of stable specs (JSON) to add :blocking meta') do |stable_specs_file|
19
+ params[:stable_specs_file] = stable_specs_file
20
+ end
21
+
22
+ opts.on('-p', '--project PROJECT', String, 'Can be an integer or a group/project string') do |project|
23
+ params[:project] = project
24
+ end
25
+
26
+ opts.on('-t', '--token TOKEN', String, 'A valid access token with `api` scope and Maintainer permission in PROJECT') do |token|
27
+ params[:token] = token
28
+ end
29
+
30
+ opts.on('--dry-run', "Perform a dry-run (don't create branches, commits or MRs)") do
31
+ params[:dry_run] = true
32
+ end
33
+
34
+ opts.on_tail('-v', '--version', 'Show the version') do
35
+ require_relative "../lib/gitlab_quality/test_tooling/version"
36
+ puts "#{$PROGRAM_NAME} : #{GitlabQuality::TestTooling::VERSION}"
37
+ exit
38
+ end
39
+
40
+ opts.on_tail('-h', '--help', 'Show the usage') do
41
+ puts "Purpose: Add quarantine or blocking meta to specs"
42
+ puts opts
43
+ exit
44
+ end
45
+
46
+ opts.parse(ARGV)
47
+ end
48
+
49
+ if params.any?
50
+ if params[:unstable_specs_file] && params[:stable_specs_file]
51
+ puts "Please provide only one of one of -u and -s"
52
+ exit 1
53
+ elsif !params[:unstable_specs_file] && !params[:stable_specs_file]
54
+ puts "Please provide at least one of one of -u and -s"
55
+ exit 1
56
+ end
57
+
58
+ if params[:unstable_specs_file]
59
+ params[:specs_file] = params.delete(:unstable_specs_file)
60
+ params[:processor] = GitlabQuality::TestTooling::TestMeta::Processor::AddToQuarantineProcessor
61
+ else
62
+ params[:specs_file] = params.delete(:stable_specs_file)
63
+ params[:processor] = GitlabQuality::TestTooling::TestMeta::Processor::AddToBlockingProcessor
64
+ end
65
+
66
+ GitlabQuality::TestTooling::TestMeta::TestMetaUpdater.new(**params).invoke!
67
+ else
68
+ puts options
69
+ exit 1
70
+ end
@@ -26,9 +26,10 @@ module GitlabQuality
26
26
  puts "The following note would have been updated id: #{id} with body: #{note} for mr_iid: #{merge_request_iid}"
27
27
  end
28
28
 
29
- def create_merge_request(title:, source_branch:, target_branch:, description:, labels:)
29
+ def create_merge_request(title:, source_branch:, target_branch:, description:, labels:, assignee_id:)
30
30
  puts "A merge request would be created with title: #{title} " \
31
- "source_branch: #{source_branch} target_branch: #{target_branch} description: #{description} labels: #{labels}"
31
+ "source_branch: #{source_branch} target_branch: #{target_branch} " \
32
+ "description: #{description} labels: #{labels}, assignee_id: #{assignee_id}"
32
33
  end
33
34
  end
34
35
  end
@@ -25,7 +25,7 @@ module GitlabQuality
25
25
 
26
26
  ENV_VARIABLES.each do |env_name, method_name|
27
27
  define_method(method_name) do
28
- env_var_value_if_defined(env_name) || (instance_variable_get("@#{method_name}") if instance_variable_defined?("@#{method_name}"))
28
+ env_var_value_if_defined(env_name) || (instance_variable_get(:"@#{method_name}") if instance_variable_defined?(:"@#{method_name}"))
29
29
  end
30
30
  end
31
31
 
@@ -0,0 +1,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GitlabQuality
4
+ module TestTooling
5
+ module TestMeta
6
+ module Processor
7
+ class AddToBlockingProcessor < MetaProcessor
8
+ BLOCKING_METADATA = ", :blocking%{suffix}"
9
+
10
+ class << self
11
+ # Execute the processor
12
+ #
13
+ # @param [Hash] spec the spec to update
14
+ # @param [TestMetaUpdater] context instance of TestMetaUpdater
15
+ def execute(spec, context) # rubocop:disable Metrics/AbcSize
16
+ @context = context
17
+
18
+ @file_path = spec["file_path"]
19
+ devops_stage = spec["stage"]
20
+ product_group = spec["product_group"]
21
+ @example_name = spec["name"]
22
+ @mr_title = format("%{prefix} %{example_name}", prefix: '[PROMOTE TO BLOCKING]', example_name: example_name)
23
+
24
+ return unless proceed_with_merge_request?
25
+
26
+ @file_contents = context.get_file_contents(file_path)
27
+
28
+ new_content, changed_line_no = add_blocking_metadata
29
+
30
+ return if changed_line_no.negative?
31
+
32
+ branch = context.create_branch("blocking-promotion-#{SecureRandom.hex(4)}", example_name, context.ref)
33
+
34
+ context.commit_changes(branch, <<~COMMIT_MESSAGE, file_path, new_content)
35
+ Promote end-to-end test to blocking
36
+
37
+ Promote to blocking: #{example_name}
38
+ COMMIT_MESSAGE
39
+
40
+ assignee_id, assignee_handle = context.fetch_dri_id(product_group, devops_stage)
41
+
42
+ merge_request = context.create_merge_request(mr_title, branch, assignee_id) do
43
+ <<~MARKDOWN
44
+ ## What does this MR do?
45
+
46
+ Promotes the test [`#{example_name}`](https://gitlab.com/#{context.project}/-/blob/#{context.ref}/#{file_path}#L#{changed_line_no + 1})
47
+ to the blocking bucket
48
+
49
+
50
+ /label ~"Quality" ~"QA" ~"type::maintenance"
51
+ /label ~"devops::#{devops_stage}"
52
+
53
+ <div align="center">
54
+ (This MR was automatically generated by [`gitlab_quality-test_tooling`](https://gitlab.com/gitlab-org/ruby/gems/gitlab_quality-test_tooling) at #{Time.now.utc})
55
+ </div>
56
+ MARKDOWN
57
+ end
58
+
59
+ context.post_note_on_merge_request(<<~MARKDOWN, merge_request.iid)
60
+ @#{assignee_handle} Please review this MR, approve and assign it to a maintainer.
61
+
62
+ If you think this MR should not be merged, please close it and add a note of the reason to the blocking report: #{context.report_issue}
63
+ MARKDOWN
64
+
65
+ merge_request
66
+ end
67
+
68
+ # Performs post processing. Takes a list of MRs and posts them in a note on report_issue
69
+ #
70
+ # @param [Gitlab::ObjectifiedHash] merge_requests
71
+ def post_process(merge_requests)
72
+ web_urls = merge_requests.compact.map { |mr| "- #{mr.web_url}\n" }.join
73
+
74
+ return if web_urls.empty?
75
+
76
+ context.post_note_on_report_issue(<<~ISSUE_NOTE)
77
+ The following merge requests have been created to promote stable specs to blocking:
78
+
79
+ #{web_urls}
80
+ ISSUE_NOTE
81
+ end
82
+
83
+ private
84
+
85
+ attr_reader :context, :file_path, :file_contents, :example_name, :mr_title
86
+
87
+ # Checks if there is already an MR open
88
+ #
89
+ # @return [Boolean]
90
+ def proceed_with_merge_request?
91
+ open_mrs = context.existing_merge_requests(title: mr_title)
92
+ if open_mrs.any?
93
+ puts " An open MR already exists for '#{example_name}': #{open_mrs.first['web_url']}. Will not proceed with creating MR."
94
+ return false
95
+ end
96
+
97
+ true
98
+ end
99
+
100
+ # Add blocking metadata to the file content and replace it
101
+ #
102
+ # @return [Array<String, Integer>] first value holds the new content, the second value holds the line number of the test
103
+ def add_blocking_metadata # rubocop:disable Metrics/AbcSize
104
+ matched_lines = context.find_example_match_lines(file_contents, example_name)
105
+
106
+ if matched_lines.any? { |line| line[0].include?(':blocking') }
107
+ puts "Example '#{example_name}' is already blocking"
108
+ return [file_contents, -1]
109
+ end
110
+
111
+ context.update_matched_line(matched_lines.last, file_contents.dup) do |line|
112
+ if line.include?(',')
113
+ line[line.index(',')] = format(BLOCKING_METADATA, suffix: ',')
114
+ else
115
+ line[line.rindex(' ')] = format(BLOCKING_METADATA, suffix: ' ')
116
+ end
117
+
118
+ line
119
+ end
120
+ end
121
+ end
122
+ end
123
+ end
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,184 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GitlabQuality
4
+ module TestTooling
5
+ module TestMeta
6
+ module Processor
7
+ class AddToQuarantineProcessor < MetaProcessor
8
+ QUARANTINE_METADATA = <<~META
9
+ ,
10
+ %{indentation}quarantine: {
11
+ %{indentation} issue: '%{issue_url}',
12
+ %{indentation} type: %{quarantine_type}
13
+ %{indentation}}%{suffix}
14
+ META
15
+
16
+ class << self
17
+ # Execute the processor
18
+ #
19
+ # @param [Hash<String,String>] spec the spec to update
20
+ # @option spec [String] :file_path the path to the spec file
21
+ # @option spec [String] :stage the stage of the test
22
+ # @option spec [String] :failure_issue the issue url of the failure
23
+ # @option spec [String] :name the name of the example
24
+ # @param [TestMetaUpdater] context instance of TestMetaUpdater
25
+ def execute(spec, context) # rubocop:disable Metrics/AbcSize
26
+ @context = context
27
+
28
+ @file_path = spec["file_path"]
29
+ devops_stage = spec["stage"]
30
+ @failure_issue_url = spec["failure_issue"]
31
+ @example_name = spec["name"]
32
+ @issue_id = failure_issue_url.split('/').last # split url segment, last segment of path is the issue id
33
+ @mr_title = format("%{prefix} %{example_name}", prefix: '[QUARANTINE]', example_name: example_name)
34
+ @failure_issue = context.fetch_issue(iid: issue_id)
35
+
36
+ return unless proceed_with_merge_request?
37
+
38
+ @file_contents = context.get_file_contents(file_path)
39
+
40
+ new_content, changed_line_no = add_quarantine_metadata
41
+
42
+ branch = context.create_branch("#{issue_id}-quarantine-#{SecureRandom.hex(4)}", example_name, context.ref)
43
+
44
+ context.commit_changes(branch, <<~COMMIT_MESSAGE, file_path, new_content)
45
+ Quarantine end-to-end test
46
+
47
+ Quarantine #{example_name}
48
+ COMMIT_MESSAGE
49
+
50
+ context.create_merge_request(mr_title, branch) do
51
+ <<~MARKDOWN
52
+ ## What does this MR do?
53
+
54
+ Quarantines the test [`#{example_name}`](https://gitlab.com/#{context.project}/-/blob/#{context.ref}/#{file_path}#L#{changed_line_no + 1})
55
+
56
+ This test was identified in the reliable e2e test report: #{context.report_issue}
57
+
58
+ ### E2E Test Failure issue(s)
59
+
60
+ #{failure_issue_url}
61
+
62
+ ### Check-list
63
+
64
+ - [ ] General code guidelines check-list
65
+ - [ ] [Code review guidelines](https://docs.gitlab.com/ee/development/code_review.html)
66
+ - [ ] [Style guides](https://docs.gitlab.com/ee/development/contributing/style_guides.html)
67
+ - [ ] Quarantine test check-list
68
+ - [ ] Follow the [Quarantining Tests guide](https://about.gitlab.com/handbook/engineering/infrastructure/test-platform/debugging-qa-test-failures/#quarantining-tests).
69
+ - [ ] Confirm the test has a [`quarantine:` tag with the specified quarantine type](https://about.gitlab.com/handbook/engineering/infrastructure/test-platform/debugging-qa-test-failures/#quarantined-test-types).
70
+ - [ ] Note if the test should be [quarantined for a specific environment](https://docs.gitlab.com/ee/development/testing_guide/end_to_end/execution_context_selection.html#quarantine-a-test-for-a-specific-environment).
71
+ - [ ] (Optionally) In case of an emergency (e.g. blocked deployments), consider adding labels to pick into auto-deploy (~"Pick into auto-deploy" ~"priority::1" ~"severity::1").
72
+ - [ ] To ensure a faster turnaround, ask in the `#quality_maintainers` Slack channel for someone to review and merge the merge request, rather than assigning it directly.
73
+
74
+ <!-- Base labels. -->
75
+ /label ~"Quality" ~"QA" ~"type::maintenance" ~"maintenance::pipelines"
76
+
77
+ <!--
78
+ Choose the stage that appears in the test path, e.g. ~"devops::create" for
79
+ `qa/specs/features/browser_ui/3_create/web_ide/add_file_template_spec.rb`.
80
+ -->
81
+ /label ~"devops::#{devops_stage}"
82
+
83
+ <div align="center">
84
+ (This MR was automatically generated by [`gitlab_quality-test_tooling`](https://gitlab.com/gitlab-org/ruby/gems/gitlab_quality-test_tooling) at #{Time.now.utc})
85
+ </div>
86
+ MARKDOWN
87
+ end
88
+ end
89
+
90
+ # Performs post processing. Takes a list of MRs and posts them in a note on report_issue and Slack
91
+ #
92
+ # @param [Gitlab::ObjectifiedHash] merge_requests
93
+ def post_process(merge_requests)
94
+ web_urls = merge_requests.compact.map { |mr| "- #{mr.web_url}\n" }.join
95
+
96
+ return if web_urls.empty?
97
+
98
+ context.post_note_on_report_issue(<<~ISSUE_NOTE)
99
+
100
+ The following merge requests have been created to quarantine the unstable tests:
101
+
102
+ #{web_urls}
103
+ ISSUE_NOTE
104
+
105
+ context.post_message_on_slack(<<~MSG)
106
+ *Action Required!* The following merge requests have been created to quarantine the unstable tests identified
107
+ in the reliable test report: #{context.report_issue}
108
+
109
+ #{web_urls}
110
+
111
+ Maintainers are requested to review and merge. Thank you.
112
+ MSG
113
+ end
114
+
115
+ private
116
+
117
+ attr_reader :context, :file_path, :file_contents, :failure_issue_url, :example_name, :issue_id, :mr_title, :failure_issue
118
+
119
+ # Checks if the failure issue is closed or if there is already an MR open
120
+ #
121
+ # @return [Boolean]
122
+ def proceed_with_merge_request?
123
+ if context.issue_is_closed?(failure_issue)
124
+ puts " Failure issue '#{failure_issue_url}' is closed. Will not proceed with creating MR."
125
+ return false
126
+ end
127
+
128
+ open_mrs = context.existing_merge_requests(title: mr_title)
129
+ if open_mrs.any?
130
+ puts " An open MR already exists for '#{example_name}': #{open_mrs.first['web_url']}. Will not proceed with creating MR."
131
+ return false
132
+ end
133
+
134
+ true
135
+ end
136
+
137
+ # Add quarantine metadata to the file content and replace it
138
+ #
139
+ # @return [Array<String, Integer>] first value holds the new content, the second value holds the line number of the test
140
+ def add_quarantine_metadata # rubocop:disable Metrics/AbcSize
141
+ matched_lines = context.find_example_match_lines(file_contents, example_name)
142
+
143
+ context.update_matched_line(matched_lines.last, file_contents.dup) do |line|
144
+ indentation = context.indentation(line)
145
+
146
+ if line.include?(',') && line.split.last != 'do'
147
+ line[line.index(',')] = format(QUARANTINE_METADATA.rstrip, issue_url: failure_issue_url, indentation: indentation, suffix: ',', quarantine_type: quarantine_type)
148
+ else
149
+ line[line.rindex(' ')] = format(QUARANTINE_METADATA.rstrip, issue_url: failure_issue_url, indentation: indentation, suffix: ' ', quarantine_type: quarantine_type)
150
+ end
151
+
152
+ line
153
+ end
154
+ end
155
+
156
+ # Returns the quarantine type based on the failure scoped label
157
+ #
158
+ # @return [String]
159
+ def quarantine_type
160
+ case context.issue_scoped_label(failure_issue, 'failure')&.split('::')&.last
161
+ when 'new', 'investigating'
162
+ ':investigating'
163
+ when 'external-dependency'
164
+ ':external_dependency'
165
+ when 'broken-test'
166
+ ':broken'
167
+ when 'bug'
168
+ ':bug'
169
+ when 'flaky-test'
170
+ ':flaky'
171
+ when 'stale-test'
172
+ ':stale'
173
+ when 'test-environment'
174
+ ':test_environment'
175
+ else
176
+ ':investigating'
177
+ end
178
+ end
179
+ end
180
+ end
181
+ end
182
+ end
183
+ end
184
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GitlabQuality
4
+ module TestTooling
5
+ module TestMeta
6
+ module Processor
7
+ class MetaProcessor
8
+ class << self
9
+ def execute
10
+ raise 'method not implemented'
11
+ end
12
+
13
+ def post_process
14
+ raise 'method not implemented'
15
+ end
16
+
17
+ private_class_method :new
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,256 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module GitlabQuality
6
+ module TestTooling
7
+ module TestMeta
8
+ class TestMetaUpdater
9
+ include TestTooling::Concerns::FindSetDri
10
+
11
+ attr_reader :project, :ref, :report_issue
12
+
13
+ TEST_PLATFORM_MAINTAINERS_SLACK_CHANNEL_ID = 'C0437FV9KBN' # test-platform-maintainers
14
+
15
+ def initialize(token:, project:, specs_file:, processor:, ref: 'master', dry_run: false)
16
+ @specs_file = specs_file
17
+ @token = token
18
+ @project = project
19
+ @ref = ref
20
+ @dry_run = dry_run
21
+ @processor = processor
22
+ end
23
+
24
+ def invoke!
25
+ JSON.parse(File.read(specs_file)).tap do |contents|
26
+ @report_issue = contents['report_issue']
27
+
28
+ results = []
29
+ contents['specs'].each do |spec|
30
+ results << processor.execute(spec, self)
31
+ end
32
+ processor.post_process(results)
33
+ end
34
+ end
35
+
36
+ # Fetch contents of file from the repository
37
+ #
38
+ # [String] file_path path to the file
39
+ # [String] contents of the file
40
+ def get_file_contents(file_path)
41
+ repository_files = GitlabClient::RepositoryFilesClient.new(token: token, project: project, file_path: file_path)
42
+ repository_files.file_contents
43
+ end
44
+
45
+ # Find all lines that contain any part of the example name
46
+ #
47
+ # @param [String] content the content of the spec file
48
+ # @param [String] example_name the name of example to find
49
+ # @return [Array<String, Integer>] first value holds the matched line, the second value holds the line number of matched line
50
+ def find_example_match_lines(content, example_name)
51
+ lines = content.split("\n")
52
+
53
+ matched_lines = []
54
+
55
+ lines.each_with_index do |line, line_index|
56
+ string_within_quotes = spec_desc_string_within_quotes(line)
57
+
58
+ matched_lines << [line, line_index] if string_within_quotes && example_name.include?(string_within_quotes)
59
+ rescue StandardError => e
60
+ puts "Error: #{e}"
61
+ end
62
+
63
+ matched_lines
64
+ end
65
+
66
+ # Update the provided matched_line with content from the block if given
67
+ #
68
+ # @param [Array<String, Integer>] matched_line first value holds the line content, the second value holds the line number
69
+ # @param [String] content full orignal content of the spec file
70
+ # @return [Array<String, Integer>] first value holds the new content, the second value holds the line number of the test
71
+ def update_matched_line(matched_line, content)
72
+ lines = content.split("\n")
73
+
74
+ begin
75
+ resulting_line = block_given? ? yield(matched_line[0]) : matched_line[0]
76
+ lines[matched_line[1]] = resulting_line
77
+ rescue StandardError => e
78
+ puts "Error: #{e}"
79
+ end
80
+
81
+ [lines.join("\n") << "\n", matched_line[1]]
82
+ end
83
+
84
+ # Create a branch from the ref
85
+ #
86
+ # @param [String] name_prefix the prefix to attach to the branch name
87
+ # @param [String] example_name the example
88
+ # @return [Gitlab::ObjectifiedHash] the new branch
89
+ def create_branch(name_prefix, example_name, ref)
90
+ branch_name = [name_prefix, example_name.gsub(/\W/, '-')]
91
+ @branches_client ||= (dry_run ? GitlabClient::BranchesDryClient : GitlabClient::BranchesClient).new(token: token, project: project)
92
+ @branches_client.create(branch_name.join('-'), ref)
93
+ end
94
+
95
+ # Commit changes to a branch
96
+ #
97
+ # @param [Gitlab::ObjectifiedHash] branch the branch to commit to
98
+ # @param [String] message the message to commit
99
+ # @param [String] new_content the new content to commit
100
+ # @return [Gitlab::ObjectifiedHash] the commit
101
+ def commit_changes(branch, message, file_path, new_content)
102
+ @commits_client ||= (dry_run ? GitlabClient::CommitsDryClient : GitlabClient::CommitsClient)
103
+ .new(token: token, project: project)
104
+ @commits_client.create(branch['name'], file_path, new_content, message)
105
+ end
106
+
107
+ # Create a Merge Request with a given branch
108
+ #
109
+ # @param [String] title_prefix the prefix of the title
110
+ # @param [String] example_name the example
111
+ # @param [Gitlab::ObjectifiedHash] branch the branch
112
+ # @param [Integer] assignee_id
113
+ # @return [Gitlab::ObjectifiedHash] the created merge request
114
+ def create_merge_request(title, branch, assignee_id = nil, labels = '')
115
+ description = yield
116
+
117
+ merge_request_client.create_merge_request(
118
+ title: title,
119
+ source_branch: branch['name'],
120
+ target_branch: ref,
121
+ description: description,
122
+ labels: labels,
123
+ assignee_id: assignee_id)
124
+ end
125
+
126
+ # Check if issue is closed
127
+ #
128
+ # @param [Gitlab::ObjectifiedHash] issue the issue
129
+ # @return [Boolean] True or False
130
+ def issue_is_closed?(issue)
131
+ issue['state'] == 'closed'
132
+ end
133
+
134
+ # Get scoped label from issue
135
+ #
136
+ # @param [Gitlab::ObjectifiedHash] issue the issue
137
+ # @param [String] scope
138
+ # @return [String] scoped label
139
+ def issue_scoped_label(issue, scope)
140
+ issue['labels'].detect { |label| label.match(/#{scope}::/) }
141
+ end
142
+
143
+ # Fetch an issue
144
+ #
145
+ # @param [String] iid: The iid of the issue
146
+ # @return [Gitlab::ObjectifiedHash]
147
+ def fetch_issue(iid:)
148
+ issue_client.find_issues(iid: iid).first
149
+ end
150
+
151
+ # Post note on report_issue
152
+ #
153
+ # @param [String] note the note to post
154
+ # @return [Gitlab::ObjectifiedHash]
155
+ def post_note_on_report_issue(note)
156
+ iid = report_issue&.split('/')&.last # split url segment, last segment of path is the issue id
157
+ if iid
158
+ issue_client.create_issue_note(iid: iid, note: note)
159
+ else
160
+ Runtime::Logger.info("#{self.class.name}##{__method__} Note was NOT posted on report issue: #{report_issue}")
161
+ end
162
+ end
163
+
164
+ # Post a note of merge reqest
165
+ #
166
+ # @param [String] note
167
+ # @param [Integer] merge_request_iid
168
+ # @return [Gitlab::ObjectifiedHash]
169
+ def post_note_on_merge_request(note, merge_request_iid)
170
+ merge_request_client.create_note(note: note, merge_request_iid: merge_request_iid)
171
+ end
172
+
173
+ # Fetch the id for the dri of the product group and stage
174
+ # The first item returned is the id of the assignee and the second item is the handle
175
+ #
176
+ # @param [String] product_group
177
+ # @param [String] devops_stage
178
+ # @return [Array<Integer, String>]
179
+ def fetch_dri_id(product_group, devops_stage)
180
+ assignee_handle = ENV.fetch('QA_TEST_DRI_HANDLE', nil) || set_dri_via_group(product_group, devops_stage)
181
+
182
+ [issue_client.find_user_id(username: assignee_handle), assignee_handle]
183
+ end
184
+
185
+ # Post a message on Slack
186
+ #
187
+ # @param [String] message the message to post
188
+ # @return [HTTP::Response]
189
+ def post_message_on_slack(message)
190
+ channel = ENV.fetch('SLACK_QA_CHANNEL', nil) || TEST_PLATFORM_MAINTAINERS_SLACK_CHANNEL_ID
191
+ slack_options = {
192
+ slack_webhook_url: ENV.fetch('CI_SLACK_WEBHOOK_URL', nil),
193
+ channel: channel,
194
+ username: "GitLab Quality Test Tooling",
195
+ icon_emoji: ':warning:',
196
+ message: message
197
+ }
198
+ puts "Posting Slack message to channel: #{channel}"
199
+
200
+ (dry_run ? GitlabQuality::TestTooling::Slack::PostToSlackDry : GitlabQuality::TestTooling::Slack::PostToSlack).new(**slack_options).invoke!
201
+ end
202
+
203
+ # Provide indentaiton based on the given line
204
+ #
205
+ # @param[String] line the line to use for indentation
206
+ # @return[String] indentation
207
+ def indentation(line)
208
+ # Indent the same number of spaces as the current line
209
+ no_of_spaces = line[/\A */].size
210
+ # If the first char on current line is not a quote, add two more spaces
211
+ no_of_spaces += /['"]/.match?(line.lstrip[0]) ? 0 : 2
212
+
213
+ " " * no_of_spaces
214
+ end
215
+
216
+ # Returns and existing merge request with the given title
217
+ #
218
+ # @param [String] title: Title of the merge request
219
+ # @return [Array<Gitlab::ObjectifiedHash>] Merge requests
220
+ def existing_merge_requests(title:)
221
+ merge_request_client.find(options: { search: title, in: 'title', state: 'opened' })
222
+ end
223
+
224
+ private
225
+
226
+ attr_reader :token, :specs_file, :dry_run, :processor
227
+
228
+ # Returns any test description string within single or double quotes
229
+ #
230
+ # @param [String] line the line to check for any quoted string
231
+ # @return [String] the match or nil if no match
232
+ def spec_desc_string_within_quotes(line)
233
+ match = line.match(/(?:it|describe|context|\s)+ ['"]([^'"]*)['"]/)
234
+ match ? match[1] : nil
235
+ end
236
+
237
+ # Returns the GitlabIssueClient or GitlabIssueDryClient based on the value of dry_run
238
+ #
239
+ # @return [GitlabIssueDryClient | GitlabIssueClient]
240
+ def issue_client
241
+ @issue_client ||= (dry_run ? GitlabClient::IssuesDryClient : GitlabClient::IssuesClient).new(token: token, project: project)
242
+ end
243
+
244
+ # Returns the MergeRequestDryClient or MergeRequest based on the value of dry_run
245
+ #
246
+ # @return [MergeRequestDryClient | MergeRequest]
247
+ def merge_request_client
248
+ @merge_request_client ||= (dry_run ? GitlabClient::MergeRequestsDryClient : GitlabClient::MergeRequestsClient).new(
249
+ token: token,
250
+ project: project
251
+ )
252
+ end
253
+ end
254
+ end
255
+ end
256
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module GitlabQuality
4
4
  module TestTooling
5
- VERSION = "1.13.0"
5
+ VERSION = "1.14.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: 1.13.0
4
+ version: 1.14.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: 2024-01-19 00:00:00.000000000 Z
11
+ date: 2024-01-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: climate_control
@@ -201,7 +201,7 @@ dependencies:
201
201
  version: '6.1'
202
202
  - - "<"
203
203
  - !ruby/object:Gem::Version
204
- version: '7.2'
204
+ version: '7.1'
205
205
  type: :runtime
206
206
  prerelease: false
207
207
  version_requirements: !ruby/object:Gem::Requirement
@@ -211,7 +211,7 @@ dependencies:
211
211
  version: '6.1'
212
212
  - - "<"
213
213
  - !ruby/object:Gem::Version
214
- version: '7.2'
214
+ version: '7.1'
215
215
  - !ruby/object:Gem::Dependency
216
216
  name: amatch
217
217
  requirement: !ruby/object:Gem::Requirement
@@ -342,6 +342,20 @@ dependencies:
342
342
  - - "<"
343
343
  - !ruby/object:Gem::Version
344
344
  version: '3'
345
+ - !ruby/object:Gem::Dependency
346
+ name: rspec-parameterized
347
+ requirement: !ruby/object:Gem::Requirement
348
+ requirements:
349
+ - - "~>"
350
+ - !ruby/object:Gem::Version
351
+ version: 1.0.0
352
+ type: :runtime
353
+ prerelease: false
354
+ version_requirements: !ruby/object:Gem::Requirement
355
+ requirements:
356
+ - - "~>"
357
+ - !ruby/object:Gem::Version
358
+ version: 1.0.0
345
359
  description: A collection of test-related tools.
346
360
  email:
347
361
  - quality@gitlab.com
@@ -356,6 +370,7 @@ executables:
356
370
  - slow-test-issues
357
371
  - slow-test-merge-request-report-note
358
372
  - update-screenshot-paths
373
+ - update-test-meta
359
374
  extensions: []
360
375
  extra_rdoc_files: []
361
376
  files:
@@ -383,6 +398,7 @@ files:
383
398
  - exe/slow-test-issues
384
399
  - exe/slow-test-merge-request-report-note
385
400
  - exe/update-screenshot-paths
401
+ - exe/update-test-meta
386
402
  - lefthook.yml
387
403
  - lib/gitlab_quality/test_tooling.rb
388
404
  - lib/gitlab_quality/test_tooling/concerns/find_set_dri.rb
@@ -436,6 +452,10 @@ files:
436
452
  - lib/gitlab_quality/test_tooling/system_logs/log_types/rails/graphql_log.rb
437
453
  - lib/gitlab_quality/test_tooling/system_logs/shared_fields.rb
438
454
  - lib/gitlab_quality/test_tooling/system_logs/system_logs_formatter.rb
455
+ - lib/gitlab_quality/test_tooling/test_meta/processor/add_to_blocking_processor.rb
456
+ - lib/gitlab_quality/test_tooling/test_meta/processor/add_to_quarantine_processor.rb
457
+ - lib/gitlab_quality/test_tooling/test_meta/processor/meta_processor.rb
458
+ - lib/gitlab_quality/test_tooling/test_meta/test_meta_updater.rb
439
459
  - lib/gitlab_quality/test_tooling/test_metric/json_test_metric.rb
440
460
  - lib/gitlab_quality/test_tooling/test_metrics/json_test_metric_collection.rb
441
461
  - lib/gitlab_quality/test_tooling/test_result/base_test_result.rb