gitlab_quality-test_tooling 1.21.1 → 1.22.0

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: 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