gitlab_quality-test_tooling 0.6.2 → 0.8.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 +1 -1
- data/lib/gitlab_quality/test_tooling/gitlab_issue_client.rb +8 -2
- data/lib/gitlab_quality/test_tooling/labels_inference.rb +53 -0
- data/lib/gitlab_quality/test_tooling/report/relate_failure_issue.rb +139 -93
- data/lib/gitlab_quality/test_tooling/report/report_as_issue.rb +26 -6
- data/lib/gitlab_quality/test_tooling/test_results/test_result.rb +5 -1
- data/lib/gitlab_quality/test_tooling/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ed3ddf73e69dbf985ff11b7192c25055281b4559b53f3961ff7c4de033872e70
|
4
|
+
data.tar.gz: 30de36dd668dd20ab52a19c12cbf3b68f87041cd12bf495692686f2e813a3332
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1dcb1a3f77b47b5ecafd5a425b2de398cedc6caf3b7fa4e4f0a351d467f723329e7382a592936064ff4969b25976e9b3b88553610d33b88db2e22a42c0e2917d
|
7
|
+
data.tar.gz: 0bb812067345d488cfea73ad4d25e396aeea79eaa2a0790de5607e20e3ce08a1e2a96990f8ee1523d52d1c95112e7b99755dfa8857731337a05466d90bbb0925
|
data/Gemfile.lock
CHANGED
@@ -61,8 +61,14 @@ module GitlabQuality
|
|
61
61
|
end
|
62
62
|
end
|
63
63
|
|
64
|
-
def create_issue(title:, description:, labels:, issue_type: 'issue')
|
65
|
-
attrs = {
|
64
|
+
def create_issue(title:, description:, labels:, issue_type: 'issue', assignee_id: nil, due_date: nil)
|
65
|
+
attrs = {
|
66
|
+
issue_type: issue_type,
|
67
|
+
description: description,
|
68
|
+
labels: labels,
|
69
|
+
assignee_id: assignee_id,
|
70
|
+
due_date: due_date
|
71
|
+
}.compact
|
66
72
|
|
67
73
|
handle_gitlab_client_exceptions do
|
68
74
|
client.create_issue(project, title, attrs)
|
@@ -0,0 +1,53 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'json'
|
5
|
+
require 'httparty'
|
6
|
+
|
7
|
+
module GitlabQuality
|
8
|
+
module TestTooling
|
9
|
+
class LabelsInference
|
10
|
+
WWW_GITLAB_COM_SITE = 'https://about.gitlab.com'
|
11
|
+
WWW_GITLAB_COM_GROUPS_JSON = "#{WWW_GITLAB_COM_SITE}/groups.json".freeze
|
12
|
+
WWW_GITLAB_COM_CATEGORIES_JSON = "#{WWW_GITLAB_COM_SITE}/categories.json".freeze
|
13
|
+
FEATURE_CATEGORY_METADATA_REGEX = /(?<=feature_category: :)\w+/
|
14
|
+
|
15
|
+
def infer_labels_from_product_group(product_group)
|
16
|
+
[groups_mapping.dig(product_group, 'label')].compact.to_set
|
17
|
+
end
|
18
|
+
|
19
|
+
def infer_labels_from_feature_category(feature_category)
|
20
|
+
[
|
21
|
+
categories_mapping.dig(feature_category, 'label'),
|
22
|
+
*infer_labels_from_product_group(categories_mapping.dig(feature_category, 'group'))
|
23
|
+
].compact.to_set
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def categories_mapping
|
29
|
+
@categories_mapping ||= self.class.fetch_json(WWW_GITLAB_COM_CATEGORIES_JSON)
|
30
|
+
end
|
31
|
+
|
32
|
+
def groups_mapping
|
33
|
+
@groups_mapping ||= self.class.fetch_json(WWW_GITLAB_COM_GROUPS_JSON)
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.fetch_json(json_url)
|
37
|
+
json = with_retries { HTTParty.get(json_url, format: :plain) }
|
38
|
+
JSON.parse(json)
|
39
|
+
rescue JSON::ParserError
|
40
|
+
Runtime::Logger.debug("#{self.class.name}##{__method__} attempted to parse invalid JSON:\n\n#{json}")
|
41
|
+
{}
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.with_retries(attempts: 3)
|
45
|
+
yield
|
46
|
+
rescue Errno::ECONNRESET, OpenSSL::SSL::SSLError, Net::OpenTimeout
|
47
|
+
retry if (attempts -= 1).positive?
|
48
|
+
raise
|
49
|
+
end
|
50
|
+
private_class_method :with_retries
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -13,28 +13,32 @@ module GitlabQuality
|
|
13
13
|
# - Takes the JSON test run reports, e.g. `$CI_PROJECT_DIR/gitlab-qa-run-*/**/rspec-*.json`
|
14
14
|
# - Takes a project where failure issues should be created
|
15
15
|
# - Find issue by title (with test description or test file), then further filter by stack trace, then pick the better-matching one
|
16
|
-
# -
|
17
|
-
# - Update labels
|
16
|
+
# - Add the failed job to the issue description, and update labels
|
18
17
|
class RelateFailureIssue < ReportAsIssue
|
19
18
|
include Concerns::FindSetDri
|
20
19
|
|
21
20
|
DEFAULT_MAX_DIFF_RATIO_FOR_DETECTION = 0.15
|
21
|
+
SYSTEMIC_EXCEPTIONS_THRESHOLD = 10
|
22
22
|
SPAM_THRESHOLD_FOR_FAILURE_ISSUES = 3
|
23
23
|
FAILURE_STACKTRACE_REGEX = %r{(?:(?:.*Failure/Error:(?<stacktrace>.+))|(?<stacktrace>.+))}m
|
24
24
|
ISSUE_STACKTRACE_REGEX = /### Stack trace\s*(```)#{FAILURE_STACKTRACE_REGEX}(```)/m
|
25
|
-
|
26
|
-
|
25
|
+
JOB_URL_REGEX = %r{(?<job_url>https://(?<host>[\w.]+)/(?<project_path>[\w\-./]+)/-/jobs/\d+)}
|
26
|
+
FAILED_JOB_DESCRIPTION_REGEX = /First happened in #{JOB_URL_REGEX}\./m
|
27
|
+
REPORT_ITEM_REGEX = /^1\. \d{4}-\d{2}-\d{2}: #{JOB_URL_REGEX} \((?<pipeline_url>.+)\)$/
|
27
28
|
NEW_ISSUE_LABELS = Set.new(%w[test failure::new priority::2]).freeze
|
28
|
-
IGNORE_EXCEPTIONS = [
|
29
|
+
IGNORE_EXCEPTIONS = [
|
30
|
+
'Net::ReadTimeout',
|
31
|
+
'403 Forbidden - Your account has been blocked'
|
32
|
+
].freeze
|
29
33
|
SCREENSHOT_IGNORED_ERRORS = ['500 Internal Server Error', 'fabricate_via_api!', 'Error Code 500'].freeze
|
30
34
|
|
31
35
|
MultipleIssuesFound = Class.new(StandardError)
|
32
36
|
|
33
|
-
def initialize(max_diff_ratio: DEFAULT_MAX_DIFF_RATIO_FOR_DETECTION, system_logs: [], base_issue_labels:
|
37
|
+
def initialize(max_diff_ratio: DEFAULT_MAX_DIFF_RATIO_FOR_DETECTION, system_logs: [], base_issue_labels: Set.new, **kwargs)
|
34
38
|
super
|
35
39
|
@max_diff_ratio = max_diff_ratio.to_f
|
36
40
|
@system_logs = Dir.glob(system_logs)
|
37
|
-
@base_issue_labels = base_issue_labels
|
41
|
+
@base_issue_labels = Set.new(base_issue_labels)
|
38
42
|
@issue_type = 'issue'
|
39
43
|
@commented_issue_list = Set.new
|
40
44
|
end
|
@@ -47,46 +51,58 @@ module GitlabQuality
|
|
47
51
|
puts "Reporting test failures in `#{files.join(',')}` as issues in project `#{project}` via the API at `#{Runtime::Env.gitlab_api_base}`."
|
48
52
|
|
49
53
|
TestResults::Builder.new(files).test_results_per_file do |test_results|
|
50
|
-
puts "=> Reporting tests in #{test_results.path}"
|
54
|
+
puts "=> Reporting #{test_results.count} tests in #{test_results.path}"
|
55
|
+
|
56
|
+
systemic_exceptions = systemic_exceptions_for_test_results(test_results)
|
51
57
|
|
52
58
|
test_results.each do |test|
|
53
|
-
relate_failure_to_issue(test) if should_report?(test)
|
59
|
+
relate_failure_to_issue(test) if should_report?(test, systemic_exceptions)
|
54
60
|
end
|
55
61
|
|
56
62
|
test_results.write
|
57
63
|
end
|
58
64
|
end
|
59
65
|
|
66
|
+
def systemic_exceptions_for_test_results(test_results)
|
67
|
+
test_results
|
68
|
+
.flat_map { |test| test.report['exceptions']&.map { |exception| exception['message'] } }
|
69
|
+
.compact
|
70
|
+
.tally
|
71
|
+
.select { |_e, count| count >= SYSTEMIC_EXCEPTIONS_THRESHOLD }
|
72
|
+
.keys
|
73
|
+
end
|
74
|
+
|
60
75
|
def relate_failure_to_issue(test)
|
61
|
-
puts "
|
76
|
+
puts " => Relating issues for test '#{test.name}'..."
|
62
77
|
|
63
78
|
begin
|
64
|
-
issue
|
65
|
-
return create_issue(test) unless issue || test.quarantine?
|
79
|
+
issue = find_issue_and_update_reports(test)
|
66
80
|
|
67
|
-
|
81
|
+
create_issue(test) unless issue || test.quarantine?
|
68
82
|
rescue MultipleIssuesFound => e
|
69
83
|
warn(e.message)
|
70
84
|
end
|
71
85
|
end
|
72
86
|
|
73
|
-
def
|
87
|
+
def find_issue_and_update_reports(test)
|
74
88
|
issue, diff_ratio = find_failure_issue(test)
|
75
|
-
return
|
89
|
+
return unless issue
|
76
90
|
|
77
|
-
|
78
|
-
if
|
79
|
-
puts "
|
91
|
+
failure_already_reported = failure_already_reported?(issue, test)
|
92
|
+
if failure_already_reported
|
93
|
+
puts " => Failure already reported on issue."
|
80
94
|
else
|
81
95
|
puts " => Found issue #{issue.web_url} for test '#{test.name}' with a diff ratio of #{(diff_ratio * 100).round(2)}%."
|
82
|
-
|
96
|
+
update_reports(issue, test)
|
83
97
|
@commented_issue_list.add(issue.web_url)
|
84
98
|
end
|
85
99
|
|
86
|
-
|
100
|
+
issue
|
87
101
|
end
|
88
102
|
|
89
|
-
def
|
103
|
+
def failure_already_reported?(issue, test)
|
104
|
+
@commented_issue_list.add(issue.web_url) if failed_issue_job_urls(issue).include?(test.ci_job_url)
|
105
|
+
|
90
106
|
@commented_issue_list.include?(issue.web_url)
|
91
107
|
end
|
92
108
|
|
@@ -94,33 +110,23 @@ module GitlabQuality
|
|
94
110
|
similar_issues = pipeline_issues_with_similar_stacktrace(test)
|
95
111
|
|
96
112
|
if similar_issues.size >= SPAM_THRESHOLD_FOR_FAILURE_ISSUES
|
97
|
-
puts "
|
98
|
-
puts " => Will not create new issue for this failing spec"
|
113
|
+
puts " => Similar failure issues have already been opened for the same pipeline environment, we won't create new issue"
|
99
114
|
similar_issues.each do |similar_issue|
|
100
|
-
puts "Please check issue: #{similar_issue.web_url}"
|
101
|
-
|
102
|
-
unless existing_failure_note(similar_issue, test.ci_job_url)
|
103
|
-
gitlab.create_issue_note(iid: similar_issue.iid,
|
104
|
-
note: "This failed job is most likely related: #{test.ci_job_url}")
|
105
|
-
end
|
115
|
+
puts " => Please check issue: #{similar_issue.web_url}"
|
116
|
+
update_reports(similar_issue, test)
|
106
117
|
end
|
107
118
|
return
|
108
119
|
end
|
109
120
|
|
110
|
-
|
111
|
-
puts "for test '#{test.name}'."
|
112
|
-
|
113
|
-
post_or_update_failed_job_note(issue, test)
|
114
|
-
|
115
|
-
assign_dri(issue, test)
|
116
|
-
|
117
|
-
issue
|
121
|
+
super
|
118
122
|
end
|
119
123
|
|
120
124
|
def pipeline_issues_with_similar_stacktrace(test)
|
121
|
-
|
125
|
+
search_labels = (base_issue_labels + Set.new(%w[test failure::new])).to_a
|
126
|
+
gitlab.find_issues(options: { state: 'opened', labels: search_labels,
|
122
127
|
created_after: past_timestamp(2) }).select do |issue|
|
123
128
|
job_url_from_issue = failed_issue_job_url(issue)
|
129
|
+
|
124
130
|
next if pipeline != pipeline_env_from_job_url(job_url_from_issue)
|
125
131
|
|
126
132
|
stack_trace_from_issue = cleaned_stack_trace_from_issue(issue)
|
@@ -131,20 +137,22 @@ module GitlabQuality
|
|
131
137
|
end
|
132
138
|
|
133
139
|
def failed_issue_job_url(issue)
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
else
|
139
|
-
job_url_string = issue.description
|
140
|
-
matched = job_url_string.match(FAILED_JOB_DESCRIPTION_REGEX)
|
141
|
-
end
|
140
|
+
job_urls_from_description(issue.description, REPORT_ITEM_REGEX).last ||
|
141
|
+
# Legacy format
|
142
|
+
job_urls_from_description(issue.description, FAILED_JOB_DESCRIPTION_REGEX).last
|
143
|
+
end
|
142
144
|
|
143
|
-
|
145
|
+
def failed_issue_job_urls(issue)
|
146
|
+
job_urls_from_description(issue.description, REPORT_ITEM_REGEX) +
|
147
|
+
# Legacy format
|
148
|
+
job_urls_from_description(issue.description, FAILED_JOB_DESCRIPTION_REGEX)
|
149
|
+
end
|
144
150
|
|
145
|
-
|
146
|
-
|
147
|
-
|
151
|
+
def job_urls_from_description(issue_description, regex)
|
152
|
+
issue_description.lines.filter_map do |line|
|
153
|
+
match = line.match(regex)
|
154
|
+
match[:job_url] if match
|
155
|
+
end
|
148
156
|
end
|
149
157
|
|
150
158
|
def pipeline_env_from_job_url(job_url)
|
@@ -163,7 +171,8 @@ module GitlabQuality
|
|
163
171
|
end
|
164
172
|
|
165
173
|
def failure_issues(test)
|
166
|
-
|
174
|
+
search_labels = (base_issue_labels + Set.new(%w[test])).to_a
|
175
|
+
gitlab.find_issues(options: { state: 'opened', labels: search_labels }).select do |issue|
|
167
176
|
issue_title = issue.title.strip
|
168
177
|
issue_title.include?(test.name) || issue_title.include?(partial_file_path(test.file))
|
169
178
|
end
|
@@ -235,15 +244,14 @@ module GitlabQuality
|
|
235
244
|
stacktrace_match = stacktrace.match(regex)
|
236
245
|
|
237
246
|
if stacktrace_match
|
238
|
-
stacktrace_match[:stacktrace].
|
239
|
-
'').strip
|
247
|
+
stacktrace_match[:stacktrace].gsub(/^\s*#.*$/, '').gsub(/^[[:space:]]+/, '').strip
|
240
248
|
else
|
241
|
-
puts " => [DEBUG] Stacktrace doesn't match the
|
249
|
+
puts " => [DEBUG] Stacktrace doesn't match the regex (#{regex}):\n----------------\n#{stacktrace}\n----------------\n"
|
242
250
|
end
|
243
251
|
end
|
244
252
|
|
245
253
|
def remove_unique_resource_names(stacktrace)
|
246
|
-
stacktrace.gsub(/qa-(test|user)-[a-z0-9-]+/, '<unique-test-resource>').gsub(
|
254
|
+
stacktrace.gsub(/(QA User |qa-(test|user)-)[a-z0-9-]+/, '<unique-test-resource>').gsub(
|
247
255
|
/(?:-|_)(?:\d+[a-z]|[a-z]+\d)[a-z\d]{4,}/, '<unique-hash>')
|
248
256
|
end
|
249
257
|
|
@@ -268,10 +276,9 @@ module GitlabQuality
|
|
268
276
|
super + [
|
269
277
|
"\n### Stack trace",
|
270
278
|
"```\n#{full_stacktrace(test)}\n```",
|
271
|
-
"First happened in #{test.ci_job_url}.",
|
272
|
-
("Related test case: #{test.testcase}." if test.testcase),
|
273
279
|
screenshot_section(test),
|
274
|
-
system_log_errors_section(test)
|
280
|
+
system_log_errors_section(test),
|
281
|
+
reports_section(test)
|
275
282
|
].compact.join("\n\n")
|
276
283
|
end
|
277
284
|
|
@@ -286,40 +293,87 @@ module GitlabQuality
|
|
286
293
|
).system_logs_summary_markdown
|
287
294
|
end
|
288
295
|
|
289
|
-
|
296
|
+
if section.empty?
|
297
|
+
puts " => No system logs or correlation id provided, skipping this section in issue description"
|
298
|
+
return
|
299
|
+
end
|
290
300
|
|
291
301
|
section
|
292
302
|
end
|
293
303
|
|
304
|
+
def reports_section(test)
|
305
|
+
<<~REPORTS
|
306
|
+
### Reports (1)
|
307
|
+
|
308
|
+
#{report_list_item(test)}
|
309
|
+
REPORTS
|
310
|
+
end
|
311
|
+
|
312
|
+
def report_list_item(test)
|
313
|
+
"1. #{Time.new.utc.strftime('%F')}: #{test.ci_job_url} (#{ENV.fetch('CI_PIPELINE_URL', 'pipeline url is missing')})"
|
314
|
+
end
|
315
|
+
|
316
|
+
def labels_inference
|
317
|
+
@labels_inference ||= GitlabQuality::TestTooling::LabelsInference.new
|
318
|
+
end
|
319
|
+
|
294
320
|
def new_issue_labels(test)
|
295
|
-
|
321
|
+
puts " => [DEBUG] product_group: #{test.product_group}; feature_category: #{test.feature_category}"
|
322
|
+
new_labels = NEW_ISSUE_LABELS +
|
323
|
+
labels_inference.infer_labels_from_product_group(test.product_group) +
|
324
|
+
labels_inference.infer_labels_from_feature_category(test.feature_category)
|
325
|
+
up_to_date_labels(test: test, new_labels: new_labels)
|
296
326
|
end
|
297
327
|
|
298
328
|
def up_to_date_labels(test:, issue: nil, new_labels: Set.new)
|
299
|
-
base_issue_labels + (super << pipeline_name_label).to_a
|
329
|
+
(Set.new(base_issue_labels) + (super << pipeline_name_label)).to_a
|
300
330
|
end
|
301
331
|
|
302
|
-
def
|
303
|
-
|
304
|
-
existing_note = existing_failure_note(issue)
|
332
|
+
def new_issue_assignee_id(test)
|
333
|
+
return unless test.product_group?
|
305
334
|
|
306
|
-
|
307
|
-
|
308
|
-
else
|
309
|
-
gitlab.create_issue_note(iid: issue.iid, note: current_note)
|
310
|
-
end
|
335
|
+
dri = set_dri_via_group(test.product_group, test)
|
336
|
+
puts " => Assigning #{dri} as DRI for the issue."
|
311
337
|
|
312
|
-
|
338
|
+
gitlab.find_user_id(username: dri)
|
313
339
|
end
|
314
340
|
|
315
|
-
def
|
316
|
-
|
341
|
+
def new_issue_due_date(test)
|
342
|
+
return unless test.product_group?
|
343
|
+
|
344
|
+
Date.today + 1.month
|
317
345
|
end
|
318
346
|
|
319
|
-
def
|
320
|
-
gitlab.
|
321
|
-
|
322
|
-
|
347
|
+
def update_reports(issue, test)
|
348
|
+
gitlab.edit_issue(iid: issue.iid, options: {
|
349
|
+
description: up_to_date_issue_description(issue.description, test),
|
350
|
+
labels: up_to_date_labels(test: test, issue: issue)
|
351
|
+
})
|
352
|
+
puts " => Added a report in '#{issue.title}': #{issue.web_url}!"
|
353
|
+
end
|
354
|
+
|
355
|
+
def up_to_date_issue_description(issue_description, test)
|
356
|
+
# We include the number of reports in the header, for visibility.
|
357
|
+
new_issue_description =
|
358
|
+
if issue_description.include?('### Reports')
|
359
|
+
# We count the number of existing reports.
|
360
|
+
reports_count = issue_description
|
361
|
+
.scan(REPORT_ITEM_REGEX)
|
362
|
+
.size.to_i + 1
|
363
|
+
issue_description.sub(/^### Reports.*$/, "### Reports (#{reports_count})")
|
364
|
+
else # For issue with the legacy format, we add the Reports section
|
365
|
+
reports_count = issue_description
|
366
|
+
.scan(JOB_URL_REGEX)
|
367
|
+
.size.to_i + 1
|
368
|
+
|
369
|
+
"#{issue_description}\n\n### Reports (#{reports_count})"
|
370
|
+
end
|
371
|
+
|
372
|
+
"#{new_issue_description}\n#{report_list_item(test)}"
|
373
|
+
end
|
374
|
+
|
375
|
+
def new_issue_title(test)
|
376
|
+
"Failure in #{super}"
|
323
377
|
end
|
324
378
|
|
325
379
|
def screenshot_section(test)
|
@@ -331,32 +385,24 @@ module GitlabQuality
|
|
331
385
|
relative_url = gitlab.upload_file(file_fullpath: test.failure_screenshot)
|
332
386
|
return unless relative_url
|
333
387
|
|
334
|
-
"### Screenshot
|
335
|
-
end
|
336
|
-
|
337
|
-
def assign_dri(issue, test)
|
338
|
-
if test.product_group?
|
339
|
-
dri = set_dri_via_group(test.product_group, test)
|
340
|
-
dri_id = gitlab.find_user_id(username: dri)
|
341
|
-
gitlab.edit_issue(iid: issue.iid, options: { assignee_id: dri_id, due_date: Date.today + 1.month })
|
342
|
-
puts " => Assigning #{dri} as DRI for the issue."
|
343
|
-
else
|
344
|
-
puts " => No product group metadata found for test '#{test.name}'"
|
345
|
-
end
|
388
|
+
"### Screenshot\n\n#{relative_url.markdown}"
|
346
389
|
end
|
347
390
|
|
348
391
|
# Checks if a test failure should be reported.
|
349
392
|
#
|
350
393
|
# @return [TrueClass|FalseClass] false if the test was skipped or failed because of a transient error that can be ignored.
|
351
394
|
# Otherwise returns true.
|
352
|
-
def should_report?(test)
|
395
|
+
def should_report?(test, systemic_exceptions)
|
353
396
|
return false if test.failures.empty?
|
354
397
|
|
398
|
+
puts " => Systemic exceptions detected: #{systemic_exceptions}" if systemic_exceptions.any?
|
399
|
+
exceptions_to_ignore = IGNORE_EXCEPTIONS + systemic_exceptions
|
400
|
+
|
355
401
|
if test.report.key?('exceptions')
|
356
|
-
reason = ignore_failure_reason(test.report['exceptions'])
|
402
|
+
reason = ignore_failure_reason(test.report['exceptions'], exceptions_to_ignore)
|
357
403
|
|
358
404
|
if reason
|
359
|
-
puts "Failure reporting skipped because #{reason}"
|
405
|
+
puts " => Failure reporting skipped because #{reason}"
|
360
406
|
|
361
407
|
return false
|
362
408
|
end
|
@@ -369,9 +415,9 @@ module GitlabQuality
|
|
369
415
|
#
|
370
416
|
# @param [Array<Hash>] exceptions the exceptions associated with the failure.
|
371
417
|
# @return [String] the reason to ignore the exceptions, or `nil` if any exceptions should not be ignored.
|
372
|
-
def ignore_failure_reason(exceptions)
|
418
|
+
def ignore_failure_reason(exceptions, ignored_exceptions)
|
373
419
|
exception_messages = exceptions
|
374
|
-
.filter_map { |exception| exception['message'] if
|
420
|
+
.filter_map { |exception| exception['message'] if ignored_exceptions.any? { |e| exception['message'].include?(e) } }
|
375
421
|
.compact
|
376
422
|
return if exception_messages.empty? || exception_messages.size < exceptions.size
|
377
423
|
|
@@ -8,6 +8,8 @@ module GitlabQuality
|
|
8
8
|
class ReportAsIssue
|
9
9
|
include Concerns::Utils
|
10
10
|
|
11
|
+
FILE_BASE_URL = "https://gitlab.com/gitlab-org/gitlab/-/blob/master/"
|
12
|
+
|
11
13
|
def initialize(token:, input_files:, project: nil, dry_run: false, **_kwargs)
|
12
14
|
@project = project
|
13
15
|
@gitlab = (dry_run ? GitlabIssueDryClient : GitlabIssueClient).new(token: token, project: project)
|
@@ -38,16 +40,31 @@ module GitlabQuality
|
|
38
40
|
|
39
41
|
| Field | Value |
|
40
42
|
| ------ | ------ |
|
41
|
-
| File |
|
43
|
+
| File | #{test_file_link(test)} |
|
42
44
|
| Description | `#{test.name}` |
|
43
45
|
| Hash | `#{failed_test_hash(test)}` |
|
46
|
+
#{"| Test case | #{test.testcase} |" if test.testcase}
|
44
47
|
DESCRIPTION
|
45
48
|
end
|
46
49
|
|
50
|
+
def test_file_link(test)
|
51
|
+
path_prefix = test.file.start_with?('qa/') ? 'qa/' : ''
|
52
|
+
|
53
|
+
"[`#{path_prefix}#{test.file}`](#{FILE_BASE_URL}#{path_prefix}#{test.file})"
|
54
|
+
end
|
55
|
+
|
47
56
|
def new_issue_labels(_test)
|
48
57
|
[]
|
49
58
|
end
|
50
59
|
|
60
|
+
def new_issue_assignee_id(_test)
|
61
|
+
nil
|
62
|
+
end
|
63
|
+
|
64
|
+
def new_issue_due_date(_test)
|
65
|
+
nil
|
66
|
+
end
|
67
|
+
|
51
68
|
def validate_input!
|
52
69
|
assert_project!
|
53
70
|
assert_input_files!(files)
|
@@ -67,14 +84,17 @@ module GitlabQuality
|
|
67
84
|
end
|
68
85
|
|
69
86
|
def create_issue(test)
|
70
|
-
|
87
|
+
attrs = {
|
71
88
|
title: title_from_test(test),
|
72
89
|
description: new_issue_description(test),
|
73
90
|
labels: new_issue_labels(test).to_a,
|
74
|
-
issue_type: issue_type
|
75
|
-
|
91
|
+
issue_type: issue_type,
|
92
|
+
assignee_id: new_issue_assignee_id(test),
|
93
|
+
due_date: new_issue_due_date(test)
|
94
|
+
}.compact
|
95
|
+
issue = gitlab.create_issue(**attrs)
|
76
96
|
|
77
|
-
new_link = issue_type == 'test_case' ? issue
|
97
|
+
new_link = issue_type == 'test_case' ? issue&.web_url&.sub('/issues/', '/quality/test_cases/') : issue&.web_url
|
78
98
|
|
79
99
|
puts "Created new #{issue_type}: #{new_link}"
|
80
100
|
|
@@ -95,7 +115,7 @@ module GitlabQuality
|
|
95
115
|
|
96
116
|
def up_to_date_labels(test:, issue: nil, new_labels: Set.new)
|
97
117
|
labels = issue_labels(issue)
|
98
|
-
labels |= new_labels
|
118
|
+
labels |= new_labels.to_set
|
99
119
|
ee_test?(test) ? labels << 'Enterprise Edition' : labels.delete('Enterprise Edition')
|
100
120
|
|
101
121
|
if test.quarantine?
|
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: 0.
|
4
|
+
version: 0.8.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: 2023-06-
|
11
|
+
date: 2023-06-09 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: climate_control
|
@@ -344,6 +344,7 @@ files:
|
|
344
344
|
- lib/gitlab_quality/test_tooling.rb
|
345
345
|
- lib/gitlab_quality/test_tooling/gitlab_issue_client.rb
|
346
346
|
- lib/gitlab_quality/test_tooling/gitlab_issue_dry_client.rb
|
347
|
+
- lib/gitlab_quality/test_tooling/labels_inference.rb
|
347
348
|
- lib/gitlab_quality/test_tooling/report/concerns/find_set_dri.rb
|
348
349
|
- lib/gitlab_quality/test_tooling/report/concerns/results_reporter.rb
|
349
350
|
- lib/gitlab_quality/test_tooling/report/concerns/utils.rb
|