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 +4 -4
- data/Gemfile.lock +2 -2
- data/README.md +21 -0
- data/exe/failed-test-issues +68 -0
- data/lib/gitlab_quality/test_tooling/gitlab_client/issues_client.rb +12 -2
- data/lib/gitlab_quality/test_tooling/gitlab_client/issues_dry_client.rb +6 -2
- data/lib/gitlab_quality/test_tooling/report/failed_test_issue.rb +265 -0
- data/lib/gitlab_quality/test_tooling/report/flaky_test_issue.rb +4 -5
- data/lib/gitlab_quality/test_tooling/report/report_as_issue.rb +3 -6
- data/lib/gitlab_quality/test_tooling/report/results_in_issues.rb +1 -1
- data/lib/gitlab_quality/test_tooling/version.rb +1 -1
- metadata +19 -16
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 755ed1e2f1e0a7ac2e1d1264eeed686c76fb23c90d5eb6ebfbcaa64f931045f6
|
4
|
+
data.tar.gz: 5bbc1d0b7b93d8db268e2b0b0081b3878ff4117e97b2a3426023180cdec945ec
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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.
|
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.
|
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
|
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:
|
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
|
27
|
-
puts "The following discussion
|
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
|
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
|
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
|
-
# -
|
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
|
-
|
111
|
-
|
112
|
-
|
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)
|
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.
|
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-
|
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
|