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