gitlab_quality-test_tooling 0.6.2 → 0.8.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: 80c8edb6daa8bf924d294ef291d82fd60c8545898aaa0eac7e71b98dad4dc1c2
4
- data.tar.gz: 9e414922f797ce645e951f3fc26adb394a7a7f48f2c01bf7ddd7d218a1229486
3
+ metadata.gz: ed3ddf73e69dbf985ff11b7192c25055281b4559b53f3961ff7c4de033872e70
4
+ data.tar.gz: 30de36dd668dd20ab52a19c12cbf3b68f87041cd12bf495692686f2e813a3332
5
5
  SHA512:
6
- metadata.gz: 573774ab0d7be7d23d2c262f0246377b824604240930ba451b088af914e7b166b8844e63425f3583a16f45bdc73be3779e5e5eb081218ff934fcb2e87698a8bf
7
- data.tar.gz: f0ad9eb31e28e2c9f21c317b6d84e78645c36f5fff30baa8ada905d17f202710e087125a373f8fc38dd06612c08764839d2a35aaa835d133211219ee2778c37a
6
+ metadata.gz: 1dcb1a3f77b47b5ecafd5a425b2de398cedc6caf3b7fa4e4f0a351d467f723329e7382a592936064ff4969b25976e9b3b88553610d33b88db2e22a42c0e2917d
7
+ data.tar.gz: 0bb812067345d488cfea73ad4d25e396aeea79eaa2a0790de5607e20e3ce08a1e2a96990f8ee1523d52d1c95112e7b99755dfa8857731337a05466d90bbb0925
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- gitlab_quality-test_tooling (0.6.2)
4
+ gitlab_quality-test_tooling (0.8.0)
5
5
  activesupport (~> 6.1)
6
6
  gitlab (~> 4.19)
7
7
  http (~> 5.0)
@@ -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 = { issue_type: issue_type, description: description, labels: labels }
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
- # - Post a note to the latest failed job, e.g. https://gitlab.com/gitlab-org/gitlab/-/issues/408333#note_1361882769
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
- FAILED_JOB_DESCRIPTION_REGEX = %r{First happened in https?://\S+\.}m
26
- FAILED_JOB_NOTE_REGEX = %r{Failed most recently in \D+ pipeline: https?://\S+}
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 = ['Net::ReadTimeout', '403 Forbidden - Your account has been blocked'].freeze
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: [], **kwargs)
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 " => Searching issues for test '#{test.name}'..."
76
+ puts " => Relating issues for test '#{test.name}'..."
62
77
 
63
78
  begin
64
- issue, issue_already_commented = find_and_link_issue(test)
65
- return create_issue(test) unless issue || test.quarantine?
79
+ issue = find_issue_and_update_reports(test)
66
80
 
67
- update_labels(issue, test) unless issue_already_commented
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 find_and_link_issue(test)
87
+ def find_issue_and_update_reports(test)
74
88
  issue, diff_ratio = find_failure_issue(test)
75
- return [false, true] unless issue
89
+ return unless issue
76
90
 
77
- issue_already_commented = issue_already_commented?(issue)
78
- if issue_already_commented
79
- puts " => Failure already commented on issue."
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
- post_or_update_failed_job_note(issue, test)
96
+ update_reports(issue, test)
83
97
  @commented_issue_list.add(issue.web_url)
84
98
  end
85
99
 
86
- [issue, issue_already_commented]
100
+ issue
87
101
  end
88
102
 
89
- def issue_already_commented?(issue)
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 " => Similar failure issues have already been opened for same pipeline environment"
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
- issue = super
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
- gitlab.find_issues(options: { state: 'opened', labels: (base_issue_labels + %w[test failure::new]).join(','),
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
- existing_note = existing_failure_note(issue)
135
- if existing_note
136
- job_url_string = existing_note.body
137
- matched = job_url_string.match(FAILED_JOB_NOTE_REGEX)
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
- return unless matched
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
- job_url = matched[0].chop.split.last
146
- puts "=> Found failed job url in the issue: #{job_url}"
147
- job_url
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
- gitlab.find_issues(options: { state: 'opened', labels: base_issue_labels + %w[test] }).select do |issue|
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].split('First happened in')[0].gsub(/^\s*#.*$/, '').gsub(/^[[:space:]]+/,
239
- '').strip
247
+ stacktrace_match[:stacktrace].gsub(/^\s*#.*$/, '').gsub(/^[[:space:]]+/, '').strip
240
248
  else
241
- puts " => [DEBUG] Stacktrace doesn't match the expected regex (#{regex}):\n----------------\n#{stacktrace}\n----------------\n"
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
- puts " => No system logs or correlation id provided, skipping this section in issue description" if section.empty?
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
- up_to_date_labels(test: test, new_labels: NEW_ISSUE_LABELS)
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 post_or_update_failed_job_note(issue, test)
303
- current_note = "Failed most recently in #{pipeline} pipeline: #{test.ci_job_url}"
304
- existing_note = existing_failure_note(issue)
332
+ def new_issue_assignee_id(test)
333
+ return unless test.product_group?
305
334
 
306
- if existing_note
307
- gitlab.edit_issue_note(issue_iid: issue.iid, note_id: existing_note.id, note: current_note)
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
- puts " => Linked #{test.ci_job_url} to #{issue.web_url}."
338
+ gitlab.find_user_id(username: dri)
313
339
  end
314
340
 
315
- def new_issue_title(test)
316
- "Failure in #{super}"
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 existing_failure_note(issue, content = "Failed most recently in")
320
- gitlab.find_issue_notes(iid: issue.iid)&.find do |note|
321
- note.body.include?(content)
322
- end
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: #{relative_url.markdown}"
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 IGNORE_EXCEPTIONS.any? { |e| exception['message'].include?(e) } }
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 | `#{test.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
- issue = gitlab.create_issue(
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.web_url.sub('/issues/', '/quality/test_cases/') : issue.web_url
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?
@@ -108,7 +108,11 @@ module GitlabQuality
108
108
  end
109
109
 
110
110
  def product_group
111
- report['product_group'] if product_group?
111
+ report['product_group']
112
+ end
113
+
114
+ def feature_category
115
+ report['feature_category']
112
116
  end
113
117
 
114
118
  private
@@ -2,6 +2,6 @@
2
2
 
3
3
  module GitlabQuality
4
4
  module TestTooling
5
- VERSION = "0.6.2"
5
+ VERSION = "0.8.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: 0.6.2
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-06 00:00:00.000000000 Z
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