gitlab_quality-test_tooling 1.13.0 → 1.14.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: 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