gitlab_quality-test_tooling 1.21.1 → 1.22.0

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: 3bd25aa86cb58d8b69f852509893b2d7f28a183474d6b4ac96732d8bd3efca23
4
- data.tar.gz: f17357f3f9e1c97089ac675c918682341978a60ac40cace79557722c2ce4cdff
3
+ metadata.gz: 755ed1e2f1e0a7ac2e1d1264eeed686c76fb23c90d5eb6ebfbcaa64f931045f6
4
+ data.tar.gz: 5bbc1d0b7b93d8db268e2b0b0081b3878ff4117e97b2a3426023180cdec945ec
5
5
  SHA512:
6
- metadata.gz: a2d8710d330b9dd79a54ff78de39e15e76b014ce70de89791991723a6a272222edda6a751bb0449ba1a820163713046e617c9d45d4fa6a895f0eb4f8d86889bc
7
- data.tar.gz: 2eda2f409021c5d8f8c710a66294266fcda7a8ef46d3580ab680175718d8468c62b4fea6c375cfcb51cd1fe899f9c83918962f767b9c534916cfcb68fab1c676
6
+ metadata.gz: 3078229a51d390624b6e148444735f38581383319e377db26986d7d1b77d2b53447b440d1fc3eecb8eaf52e92a3cc0c927e4ab44930e34bfb83165e88b011097
7
+ data.tar.gz: a0ac07071de3a7fe50144f2527faff4a6e5c5530477681b5ebf1137fc1d89aa8991f2aa88ef255bd0ffa84c3a3d8440ab91e3b3b7ef4c28fed9ae3b11c7709e0
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- gitlab_quality-test_tooling (1.21.1)
4
+ gitlab_quality-test_tooling (1.22.0)
5
5
  activesupport (>= 6.1, < 7.2)
6
6
  amatch (~> 0.4.1)
7
7
  gitlab (~> 4.19)
@@ -328,4 +328,4 @@ DEPENDENCIES
328
328
  webmock (= 3.7.0)
329
329
 
330
330
  BUNDLED WITH
331
- 2.5.6
331
+ 2.5.4
data/README.md CHANGED
@@ -156,6 +156,27 @@ Usage: exe/knapsack-report-issues [options]
156
156
  -h, --help Show the usage
157
157
  ```
158
158
 
159
+ ### `exe/failed-test-issues`
160
+
161
+ ```shell
162
+ Purpose: Relate test failures to failure issues from RSpec report files (JSON or JUnit XML)
163
+ Usage: exe/failed-test-issues [options]
164
+ -i, --input-files INPUT_FILES RSpec report files (JSON or JUnit XML)
165
+ -p, --project PROJECT Can be an integer or a group/project string
166
+ -t, --token TOKEN A valid access token with `api` scope and Maintainer permission in PROJECT
167
+ --max-diff-ratio MAX_DIFF_RATO
168
+ Max stacktrace diff ratio for failure issues detection
169
+ -r RELATED_ISSUES_FILE, The file path for the related issues
170
+ --related-issues-file
171
+ --base-issue-labels BASE_ISSUE_LABELS
172
+ Labels to add to new failure issues
173
+ --exclude-labels-for-search EXCLUDE_LABELS_FOR_SEARCH
174
+ Labels to exclude when searching for existing issues
175
+ --dry-run Perform a dry-run (don't create or update issues)
176
+ -v, --version Show the version
177
+ -h, --help Show the usage
178
+ ```
179
+
159
180
  ### `exe/flaky-test-issues`
160
181
 
161
182
  ```shell
@@ -0,0 +1,68 @@
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('-i', '--input-files INPUT_FILES', String, 'RSpec report files (JSON or JUnit XML)') do |input_files|
15
+ params[:input_files] = input_files
16
+ end
17
+
18
+ opts.on('-p', '--project PROJECT', String, 'Can be an integer or a group/project string') do |project|
19
+ params[:project] = project
20
+ end
21
+
22
+ opts.on('-t', '--token TOKEN', String, 'A valid access token with `api` scope and Maintainer permission in PROJECT') do |token|
23
+ params[:token] = token
24
+ end
25
+
26
+ opts.on('--max-diff-ratio MAX_DIFF_RATO', Float, 'Max stacktrace diff ratio for failure issues detection') do |max_diff_ratio|
27
+ params[:max_diff_ratio] = max_diff_ratio
28
+ end
29
+
30
+ opts.on('-r', '--related-issues-file RELATED_ISSUES_FILE', String, 'The file path for the related issues') do |related_issues_file|
31
+ params[:related_issues_file] = related_issues_file
32
+ end
33
+
34
+ opts.on('--base-issue-labels BASE_ISSUE_LABELS', String,
35
+ 'Labels to add to new failure issues') do |base_issue_labels|
36
+ params[:base_issue_labels] = base_issue_labels.split(',')
37
+ end
38
+
39
+ opts.on('--exclude-labels-for-search EXCLUDE_LABELS_FOR_SEARCH', String,
40
+ 'Labels to exclude when searching for existing issues') do |exclude_labels_for_search|
41
+ params[:exclude_labels_for_search] = exclude_labels_for_search.split(',')
42
+ end
43
+
44
+ opts.on('--dry-run', "Perform a dry-run (don't create or update issues)") do
45
+ params[:dry_run] = true
46
+ end
47
+
48
+ opts.on_tail('-v', '--version', 'Show the version') do
49
+ require_relative "../lib/gitlab_quality/test_tooling/version"
50
+ puts "#{$PROGRAM_NAME} : #{GitlabQuality::TestTooling::VERSION}"
51
+ exit
52
+ end
53
+
54
+ opts.on_tail('-h', '--help', 'Show the usage') do
55
+ puts "Purpose: Relate test failures to failure issues from RSpec report files (JSON or JUnit XML)"
56
+ puts opts
57
+ exit
58
+ end
59
+
60
+ opts.parse(ARGV)
61
+ end
62
+
63
+ if params.any?
64
+ GitlabQuality::TestTooling::Report::FailedTestIssue.new(**params).invoke!
65
+ else
66
+ puts options
67
+ exit 1
68
+ end
@@ -13,6 +13,10 @@ module Gitlab
13
13
  get("/projects/#{url_encode(project)}/issues/#{issue_id}/discussions", query: options)
14
14
  end
15
15
 
16
+ def create_issue_discussion(project, issue_iid, options = {})
17
+ post("/projects/#{url_encode(project)}/issues/#{issue_iid}/discussions", query: options)
18
+ end
19
+
16
20
  def add_note_to_issue_discussion_as_thread(project, issue_id, discussion_id, options = {})
17
21
  post("/projects/#{url_encode(project)}/issues/#{issue_id}/discussions/#{discussion_id}/notes", query: options)
18
22
  end
@@ -93,9 +97,15 @@ module GitlabQuality
93
97
  end
94
98
  end
95
99
 
96
- def add_note_to_issue_discussion_as_thread(iid:, discussion_id:, body:)
100
+ def create_issue_discussion(iid:, note:)
101
+ handle_gitlab_client_exceptions do
102
+ client.create_issue_discussion(project, iid, body: note)
103
+ end
104
+ end
105
+
106
+ def add_note_to_issue_discussion_as_thread(iid:, discussion_id:, note:)
97
107
  handle_gitlab_client_exceptions do
98
- client.add_note_to_issue_discussion_as_thread(project, iid, discussion_id, body: body)
108
+ client.add_note_to_issue_discussion_as_thread(project, iid, discussion_id, body: note)
99
109
  end
100
110
  end
101
111
 
@@ -23,8 +23,12 @@ module GitlabQuality
23
23
  puts "The following note would have been edited on #{project}##{issue_iid} (note #{note_id}) issue: #{note}"
24
24
  end
25
25
 
26
- def add_note_to_issue_discussion_as_thread(iid:, discussion_id:, body:)
27
- puts "The following discussion note would have been posted on #{project}##{iid} (discussion #{discussion_id}) issue: #{body}"
26
+ def create_issue_discussion(iid:, note:)
27
+ puts "The following discussion would have been posted on #{project}##{iid} issue: #{note}"
28
+ end
29
+
30
+ def add_note_to_issue_discussion_as_thread(iid:, discussion_id:, note:)
31
+ puts "The following discussion note would have been posted on #{project}##{iid} (discussion #{discussion_id}) issue: #{note}"
28
32
  end
29
33
 
30
34
  def upload_file(file_fullpath:)
@@ -0,0 +1,265 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'amatch'
4
+
5
+ module GitlabQuality
6
+ module TestTooling
7
+ module Report
8
+ # Uses the API to create GitLab issues for any failed test coming from JSON test reports.
9
+ #
10
+ # - Takes the JSON test reports like rspec-*.json
11
+ # - Takes a project where failed test issues should be created
12
+ # - For every passed test in the report:
13
+ # - Find issue by test hash or create a new issue if no issue was found
14
+ # - Add a failure report in the "Failure reports" note
15
+ class FailedTestIssue < ReportAsIssue
16
+ include Concerns::GroupAndCategoryLabels
17
+ include Concerns::IssueReports
18
+ include Amatch
19
+
20
+ IDENTITY_LABELS = ['test', 'automation:bot-authored'].freeze
21
+ NEW_ISSUE_LABELS = Set.new(['type::maintenance', 'failure::new', 'priority::3', 'severity::3', *IDENTITY_LABELS]).freeze
22
+ SEARCH_LABELS = ['test'].freeze
23
+ FOUND_IN_MR_LABEL = '~"found:in MR"'
24
+ FOUND_IN_MASTER_LABEL = '~"found:master"'
25
+ REPORTS_DISCUSSION_HEADER = '### Failure reports'
26
+ REPORT_SECTION_HEADER = '#### Failure reports'
27
+
28
+ IGNORED_FAILURES = [
29
+ 'Net::ReadTimeout',
30
+ '403 Forbidden - Your account has been blocked'
31
+ ].freeze
32
+ FAILURE_STACKTRACE_REGEX = %r{(?:(?:.*Failure/Error:(?<stacktrace>.+))|(?<stacktrace>.+))}m
33
+ ISSUE_STACKTRACE_REGEX = /##### Stack trace\s*(```)#{FAILURE_STACKTRACE_REGEX}(```)\n*/m
34
+ DEFAULT_MAX_DIFF_RATIO_FOR_DETECTION = 0.15
35
+
36
+ MultipleNotesFound = Class.new(StandardError)
37
+
38
+ def initialize(
39
+ token:,
40
+ input_files:,
41
+ base_issue_labels: nil,
42
+ dry_run: false,
43
+ project: nil,
44
+ max_diff_ratio: DEFAULT_MAX_DIFF_RATIO_FOR_DETECTION,
45
+ **_kwargs)
46
+ super(token: token, input_files: input_files, project: project, dry_run: dry_run)
47
+
48
+ @base_issue_labels = Set.new(base_issue_labels)
49
+ @max_diff_ratio = max_diff_ratio.to_f
50
+ end
51
+
52
+ private
53
+
54
+ attr_reader :base_issue_labels, :max_diff_ratio
55
+
56
+ def run!
57
+ puts "Reporting failed tests in `#{files.join(',')}` as issues in project `#{project}` via the API at `#{Runtime::Env.gitlab_api_base}`."
58
+
59
+ TestResults::Builder.new(files).test_results_per_file do |test_results|
60
+ puts "=> Reporting #{test_results.count} tests in #{test_results.path}"
61
+
62
+ process_test_results(test_results)
63
+ end
64
+ end
65
+
66
+ def process_test_results(test_results)
67
+ test_results.each do |test|
68
+ next unless test_is_applicable?(test)
69
+
70
+ puts " => Reporting failure for test '#{test.name}'..."
71
+
72
+ issues = find_issues_by_hash(test_hash(test), state: 'opened', labels: SEARCH_LABELS)
73
+ issues << create_issue(test) if issues.empty?
74
+
75
+ update_reports(issues, test)
76
+ collect_issues(test, issues)
77
+ end
78
+ end
79
+
80
+ def test_is_applicable?(test)
81
+ test.status == 'failed'
82
+ end
83
+
84
+ def up_to_date_labels(test:, issue: nil, new_labels: Set.new)
85
+ (base_issue_labels + super).to_a
86
+ end
87
+
88
+ def update_reports(issues, test)
89
+ issues.each do |issue|
90
+ puts " => Adding the failed test to the existing issue: #{issue.web_url}"
91
+ add_report_to_issue(issue: issue, test: test, related_issues: (issues - [issue]))
92
+ end
93
+ end
94
+
95
+ def add_report_to_issue(issue:, test:, related_issues:)
96
+ reports_discussion = find_or_create_reports_discussion(issue: issue)
97
+ failure_discussion_note = find_failure_discussion_note(issue: issue, test: test, reports_discussion: reports_discussion)
98
+
99
+ note_body = [
100
+ report_body(reports_note: failure_discussion_note, test: test),
101
+ identity_labels_quick_action,
102
+ relate_issues_quick_actions(related_issues)
103
+ ].join("\n")
104
+
105
+ if failure_discussion_note
106
+ gitlab.edit_issue_note(
107
+ issue_iid: issue.iid,
108
+ note_id: failure_discussion_note.id,
109
+ note: note_body
110
+ )
111
+ else
112
+ gitlab.add_note_to_issue_discussion_as_thread(
113
+ iid: issue.iid,
114
+ discussion_id: reports_discussion.id,
115
+ note: note_body
116
+ )
117
+ end
118
+ rescue MultipleNotesFound => e
119
+ warn(e.message)
120
+ end
121
+
122
+ def find_or_create_reports_discussion(issue:)
123
+ reports_discussion = existing_reports_discussion(issue: issue)
124
+ return reports_discussion if reports_discussion
125
+
126
+ gitlab.create_issue_discussion(iid: issue.iid, note: REPORTS_DISCUSSION_HEADER)
127
+ end
128
+
129
+ def existing_reports_discussion(issue:)
130
+ gitlab.find_issue_discussions(iid: issue.iid).find do |discussion|
131
+ next if discussion.individual_note
132
+ next unless discussion.notes.first
133
+
134
+ discussion.notes.first.body.start_with?(REPORTS_DISCUSSION_HEADER)
135
+ end
136
+ end
137
+
138
+ def find_failure_discussion_note(issue:, test:, reports_discussion:)
139
+ return unless reports_discussion
140
+
141
+ relevant_notes = find_relevant_failure_discussion_note(issue: issue, test: test, reports_discussion: reports_discussion)
142
+ return if relevant_notes.empty?
143
+
144
+ best_matching_note, smaller_diff_ratio = relevant_notes.min_by { |_, diff_ratio| diff_ratio }
145
+
146
+ raise(MultipleNotesFound, %(Too many issues found for test '#{test.name}' (`#{test.file}`)!)) unless relevant_notes.values.count(smaller_diff_ratio) == 1
147
+
148
+ # Re-instantiate a `Gitlab::ObjectifiedHash` object after having converted it to a hash in #find_relevant_failure_issues above.
149
+ best_matching_note = Gitlab::ObjectifiedHash.new(best_matching_note)
150
+
151
+ test.failure_issue ||= "#{issue.web_url}#note_#{best_matching_note.id}"
152
+
153
+ best_matching_note
154
+ end
155
+
156
+ def find_relevant_failure_discussion_note(issue:, test:, reports_discussion:)
157
+ return [] unless reports_discussion.notes.size > 1
158
+
159
+ clean_test_stacktrace = cleaned_stack_trace_from_test(test: test)
160
+
161
+ reports_discussion.notes[1..].each_with_object({}) do |note, memo|
162
+ clean_note_stacktrace = cleaned_stack_trace_from_note(issue: issue, note: note)
163
+ diff_ratio = diff_ratio_between_test_and_note_stacktraces(
164
+ issue: issue,
165
+ note: note,
166
+ test_stacktrace: clean_test_stacktrace,
167
+ note_stacktrace: clean_note_stacktrace)
168
+
169
+ memo[note.to_h] = diff_ratio if diff_ratio
170
+ end
171
+ end
172
+
173
+ def cleaned_stack_trace_from_test(test:)
174
+ sanitize_stacktrace(stacktrace: full_stacktrace(test: test), regex: FAILURE_STACKTRACE_REGEX) || full_stacktrace(test: test)
175
+ end
176
+
177
+ def cleaned_stack_trace_from_note(issue:, note:)
178
+ note_stacktrace = sanitize_stacktrace(stacktrace: note.body, regex: ISSUE_STACKTRACE_REGEX)
179
+ return note_stacktrace if note_stacktrace
180
+
181
+ puts " => [DEBUG] Stacktrace couldn't be found for #{issue.web_url}#note_#{note.id}!"
182
+ end
183
+
184
+ def sanitize_stacktrace(stacktrace:, regex:)
185
+ stacktrace_match = stacktrace.match(regex)
186
+
187
+ if stacktrace_match
188
+ stacktrace_match[:stacktrace].gsub(/^\s*#.*$/, '').gsub(/^[[:space:]]+/, '').strip
189
+ else
190
+ puts " => [DEBUG] Stacktrace doesn't match the regex (#{regex})!"
191
+ end
192
+ end
193
+
194
+ def full_stacktrace(test:)
195
+ test.failures.each do |failure|
196
+ message = failure['message'] || ""
197
+ message_lines = failure['message_lines'] || []
198
+
199
+ next if IGNORED_FAILURES.any? { |e| message.include?(e) }
200
+
201
+ return message_lines.empty? ? message : message_lines.join("\n")
202
+ end
203
+ end
204
+
205
+ def diff_ratio_between_test_and_note_stacktraces(issue:, note:, test_stacktrace:, note_stacktrace:)
206
+ return if note_stacktrace.nil?
207
+
208
+ diff_ratio = compare_stack_traces(test_stacktrace, note_stacktrace)
209
+
210
+ if diff_ratio <= max_diff_ratio
211
+ puts " => [DEBUG] Note #{issue.web_url}#note_#{note.id} has an acceptable diff ratio of #{(diff_ratio * 100).round(2)}%."
212
+ # The `Gitlab::ObjectifiedHash` class overrides `#hash` which is used by `Hash#[]=` to compute the hash key.
213
+ # This leads to a `TypeError Exception: no implicit conversion of Hash into Integer` error, so we convert the object to a hash before using it as a Hash key.
214
+ # See:
215
+ # - https://gitlab.com/gitlab-org/gitlab-qa/-/merge_requests/587#note_453336995
216
+ # - https://github.com/NARKOZ/gitlab/commit/cbdbd1e32623f018a8fae39932a8e3bc4d929abb?_pjax=%23js-repo-pjax-container#r44484494
217
+ diff_ratio
218
+ else
219
+ puts " => [DEBUG] Found note #{issue.web_url}#note_#{note.id} but stacktraces are too different (#{(diff_ratio * 100).round(2)}%).\n"
220
+ puts " => [DEBUG] Issue stacktrace:\n----------------\n#{note_stacktrace}\n----------------\n"
221
+ puts " => [DEBUG] Failure stacktrace:\n----------------\n#{test_stacktrace}\n----------------\n"
222
+ end
223
+ end
224
+
225
+ def compare_stack_traces(stack_trace_first, stack_trace_second)
226
+ calculate_diff_ratio(stack_trace_first, stack_trace_second)
227
+ end
228
+
229
+ def calculate_diff_ratio(stack_trace_first, stack_trace_second)
230
+ distance = Levenshtein.new(stack_trace_first).match(stack_trace_second)
231
+ distance.zero? ? 0.0 : (distance.to_f / stack_trace_first.size).round(3)
232
+ end
233
+
234
+ def report_body(reports_note:, test:)
235
+ increment_reports(
236
+ current_reports_content: reports_note&.body.to_s,
237
+ test: test,
238
+ reports_section_header: REPORT_SECTION_HEADER,
239
+ item_extra_content: found_label,
240
+ reports_extra_content: "##### Stack trace\n\n```\n#{full_stacktrace(test: test)}\n```"
241
+ )
242
+ end
243
+
244
+ def found_label
245
+ if ENV.key?('CI_MERGE_REQUEST_IID')
246
+ FOUND_IN_MR_LABEL
247
+ else
248
+ FOUND_IN_MASTER_LABEL
249
+ end
250
+ end
251
+
252
+ def identity_labels_quick_action
253
+ labels_list = IDENTITY_LABELS.map { |label| %(~"#{label}") }.join(' ')
254
+ %(/label #{labels_list})
255
+ end
256
+
257
+ def relate_issues_quick_actions(issues)
258
+ issues.map do |issue|
259
+ "/relate #{issue.web_url}"
260
+ end.join("\n")
261
+ end
262
+ end
263
+ end
264
+ end
265
+ end
@@ -4,14 +4,13 @@ module GitlabQuality
4
4
  module TestTooling
5
5
  module Report
6
6
  # Uses the API to create GitLab issues for any passed test coming from JSON test reports.
7
- # We expect the test reports to come from rspec-retry, or from a new RSpec process where
8
- # we retried failing specs.
7
+ # We expect the test reports to come from a new RSpec process where we retried failing specs.
9
8
  #
10
- # - Takes the JSON test reports like rspec-*.json (typically from rspec-retry gem)`
9
+ # - Takes the JSON test reports like rspec-*.json
11
10
  # - Takes a project where flaky test issues should be created
12
11
  # - For every passed test in the report:
13
- # - Find issue by test hash
14
- # - Reopen issue if it already exists, but is closed
12
+ # - Find issue by test hash or create a new issue if no issue was found
13
+ # - Add a flakiness report in the "Flakiness reports" note
15
14
  class FlakyTestIssue < ReportAsIssue
16
15
  include Concerns::GroupAndCategoryLabels
17
16
  include Concerns::IssueReports
@@ -105,13 +105,10 @@ module GitlabQuality
105
105
  due_date: new_issue_due_date(test),
106
106
  confidential: confidential
107
107
  }.compact
108
- issue = gitlab.create_issue(**attrs)
109
108
 
110
- new_link = issue_type == 'test_case' ? issue&.web_url&.sub('/issues/', '/quality/test_cases/') : issue&.web_url
111
-
112
- puts "Created new #{issue_type}: #{new_link}"
113
-
114
- issue
109
+ gitlab.create_issue(**attrs).tap do |issue|
110
+ puts "Created new #{issue_type}: #{issue&.web_url}"
111
+ end
115
112
  end
116
113
 
117
114
  def issue_labels(issue)
@@ -46,7 +46,7 @@ module GitlabQuality
46
46
  gitlab.add_note_to_issue_discussion_as_thread(
47
47
  iid: issue.iid,
48
48
  discussion_id: discussion.id,
49
- body: failure_summary)
49
+ note: failure_summary)
50
50
  return true
51
51
  end
52
52
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  module GitlabQuality
4
4
  module TestTooling
5
- VERSION = "1.21.1"
5
+ VERSION = "1.22.0"
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.21.1
4
+ version: 1.22.0
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-04-03 00:00:00.000000000 Z
11
+ date: 2024-04-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: climate_control
@@ -308,6 +308,20 @@ dependencies:
308
308
  - - "<"
309
309
  - !ruby/object:Gem::Version
310
310
  version: '4'
311
+ - !ruby/object:Gem::Dependency
312
+ name: rspec-parameterized
313
+ requirement: !ruby/object:Gem::Requirement
314
+ requirements:
315
+ - - "~>"
316
+ - !ruby/object:Gem::Version
317
+ version: 1.0.0
318
+ type: :runtime
319
+ prerelease: false
320
+ version_requirements: !ruby/object:Gem::Requirement
321
+ requirements:
322
+ - - "~>"
323
+ - !ruby/object:Gem::Version
324
+ version: 1.0.0
311
325
  - !ruby/object:Gem::Dependency
312
326
  name: table_print
313
327
  requirement: !ruby/object:Gem::Requirement
@@ -342,24 +356,11 @@ dependencies:
342
356
  - - "<"
343
357
  - !ruby/object:Gem::Version
344
358
  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
359
359
  description: A collection of test-related tools.
360
360
  email:
361
361
  - quality@gitlab.com
362
362
  executables:
363
+ - failed-test-issues
363
364
  - flaky-test-issues
364
365
  - generate-test-session
365
366
  - knapsack-report-issues
@@ -388,6 +389,7 @@ files:
388
389
  - LICENSE.txt
389
390
  - README.md
390
391
  - Rakefile
392
+ - exe/failed-test-issues
391
393
  - exe/flaky-test-issues
392
394
  - exe/generate-test-session
393
395
  - exe/knapsack-report-issues
@@ -421,6 +423,7 @@ files:
421
423
  - lib/gitlab_quality/test_tooling/report/concerns/issue_reports.rb
422
424
  - lib/gitlab_quality/test_tooling/report/concerns/results_reporter.rb
423
425
  - lib/gitlab_quality/test_tooling/report/concerns/utils.rb
426
+ - lib/gitlab_quality/test_tooling/report/failed_test_issue.rb
424
427
  - lib/gitlab_quality/test_tooling/report/flaky_test_issue.rb
425
428
  - lib/gitlab_quality/test_tooling/report/generate_test_session.rb
426
429
  - lib/gitlab_quality/test_tooling/report/issue_logger.rb