gitlab-qa 10.3.0 → 12.2.1

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.
Files changed (91) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -1
  3. data/.gitlab/changelog_config.yml +13 -0
  4. data/.gitlab/merge_request_templates/Release.md +13 -34
  5. data/.gitlab-ci.yml +3 -1
  6. data/.rubocop.yml +13 -2
  7. data/.rubocop_todo.yml +57 -97
  8. data/Dangerfile +1 -5
  9. data/Gemfile.lock +47 -39
  10. data/README.md +1 -2
  11. data/docs/release_process.md +1 -1
  12. data/docs/running_against_remote_grid.md +42 -3
  13. data/docs/what_tests_can_be_run.md +5 -2
  14. data/gitlab-qa.gemspec +5 -3
  15. data/lib/gitlab/qa/component/base.rb +9 -9
  16. data/lib/gitlab/qa/component/gitaly.rb +7 -5
  17. data/lib/gitlab/qa/component/gitaly_cluster.rb +15 -9
  18. data/lib/gitlab/qa/component/gitlab.rb +48 -21
  19. data/lib/gitlab/qa/component/mail_hog.rb +1 -0
  20. data/lib/gitlab/qa/component/praefect.rb +41 -31
  21. data/lib/gitlab/qa/component/selenoid.rb +14 -7
  22. data/lib/gitlab/qa/component/specs.rb +11 -5
  23. data/lib/gitlab/qa/component/staging.rb +4 -4
  24. data/lib/gitlab/qa/component/telegraf.rb +2 -1
  25. data/lib/gitlab/qa/docker/engine.rb +6 -3
  26. data/lib/gitlab/qa/docker/volumes.rb +1 -1
  27. data/lib/gitlab/qa/release.rb +4 -4
  28. data/lib/gitlab/qa/runner.rb +10 -3
  29. data/lib/gitlab/qa/runtime/env.rb +47 -62
  30. data/lib/gitlab/qa/runtime/logger.rb +1 -1
  31. data/lib/gitlab/qa/runtime/omnibus_configuration.rb +1 -0
  32. data/lib/gitlab/qa/runtime/omnibus_configurations/decomposition_single_db.rb +1 -2
  33. data/lib/gitlab/qa/runtime/omnibus_configurations/object_storage_gcs.rb +2 -1
  34. data/lib/gitlab/qa/runtime/scenario.rb +1 -5
  35. data/lib/gitlab/qa/scenario/actable.rb +4 -4
  36. data/lib/gitlab/qa/scenario/test/instance/airgapped.rb +2 -4
  37. data/lib/gitlab/qa/scenario/test/instance/deployment_base.rb +2 -1
  38. data/lib/gitlab/qa/scenario/test/integration/gitaly_cluster.rb +0 -2
  39. data/lib/gitlab/qa/scenario/test/integration/group_saml.rb +1 -1
  40. data/lib/gitlab/qa/scenario/test/integration/ldap.rb +5 -6
  41. data/lib/gitlab/qa/scenario/test/integration/mtls.rb +20 -6
  42. data/lib/gitlab/qa/scenario/test/integration/oauth.rb +13 -4
  43. data/lib/gitlab/qa/scenario/test/integration/praefect.rb +16 -10
  44. data/lib/gitlab/qa/scenario/test/integration/registry_with_cdn.rb +5 -2
  45. data/lib/gitlab/qa/scenario/test/omnibus/update_from_previous.rb +1 -1
  46. data/lib/gitlab/qa/scenario/test/omnibus/upgrade.rb +1 -3
  47. data/lib/gitlab/qa/scenario/test/sanity/version.rb +1 -1
  48. data/lib/gitlab/qa/support/config_scripts.rb +1 -1
  49. data/lib/gitlab/qa/support/gitlab_version_info.rb +30 -10
  50. data/lib/gitlab/qa/support/shell_command.rb +1 -0
  51. data/lib/gitlab/qa/test_logger.rb +2 -2
  52. data/lib/gitlab/qa/version.rb +1 -1
  53. data/lib/gitlab/qa.rb +0 -1
  54. data/support/data/admin_access_token_seed.rb +5 -1
  55. data/support/data/license_usage_seed.rb +3 -1
  56. metadata +16 -45
  57. data/bin/slack +0 -14
  58. data/exe/gitlab-qa-report +0 -10
  59. data/lib/gitlab/qa/report/base_test_results.rb +0 -39
  60. data/lib/gitlab/qa/report/find_set_dri.rb +0 -43
  61. data/lib/gitlab/qa/report/generate_test_session.rb +0 -275
  62. data/lib/gitlab/qa/report/gitlab_issue_client.rb +0 -190
  63. data/lib/gitlab/qa/report/gitlab_issue_dry_client.rb +0 -28
  64. data/lib/gitlab/qa/report/j_unit_test_results.rb +0 -27
  65. data/lib/gitlab/qa/report/json_test_results.rb +0 -29
  66. data/lib/gitlab/qa/report/prepare_stage_reports.rb +0 -86
  67. data/lib/gitlab/qa/report/relate_failure_issue.rb +0 -374
  68. data/lib/gitlab/qa/report/report_as_issue.rb +0 -176
  69. data/lib/gitlab/qa/report/report_results.rb +0 -64
  70. data/lib/gitlab/qa/report/results_in_issues.rb +0 -126
  71. data/lib/gitlab/qa/report/results_in_testcases.rb +0 -111
  72. data/lib/gitlab/qa/report/results_reporter_shared.rb +0 -70
  73. data/lib/gitlab/qa/report/summary_table.rb +0 -43
  74. data/lib/gitlab/qa/report/test_result.rb +0 -184
  75. data/lib/gitlab/qa/report/update_screenshot_path.rb +0 -63
  76. data/lib/gitlab/qa/reporter.rb +0 -131
  77. data/lib/gitlab/qa/runtime/omnibus_configurations/packages.rb +0 -17
  78. data/lib/gitlab/qa/runtime/token_finder.rb +0 -44
  79. data/lib/gitlab/qa/slack/post_to_slack.rb +0 -30
  80. data/lib/gitlab/qa/system_logs/finders/json_log_finder.rb +0 -65
  81. data/lib/gitlab/qa/system_logs/finders/rails/api_log_finder.rb +0 -21
  82. data/lib/gitlab/qa/system_logs/finders/rails/application_log_finder.rb +0 -21
  83. data/lib/gitlab/qa/system_logs/finders/rails/exception_log_finder.rb +0 -21
  84. data/lib/gitlab/qa/system_logs/finders/rails/graphql_log_finder.rb +0 -21
  85. data/lib/gitlab/qa/system_logs/log_types/log.rb +0 -38
  86. data/lib/gitlab/qa/system_logs/log_types/rails/api_log.rb +0 -34
  87. data/lib/gitlab/qa/system_logs/log_types/rails/application_log.rb +0 -27
  88. data/lib/gitlab/qa/system_logs/log_types/rails/exception_log.rb +0 -23
  89. data/lib/gitlab/qa/system_logs/log_types/rails/graphql_log.rb +0 -30
  90. data/lib/gitlab/qa/system_logs/shared_fields.rb +0 -29
  91. data/lib/gitlab/qa/system_logs/system_logs_formatter.rb +0 -65
@@ -1,374 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'nokogiri'
4
- require 'active_support/core_ext/enumerable'
5
- require 'rubygems/text'
6
- require 'active_support/core_ext/integer/time'
7
-
8
- module Gitlab
9
- module QA
10
- module Report
11
- # Uses the API to create or update GitLab issues with the results of tests from RSpec report files.
12
- class RelateFailureIssue < ReportAsIssue
13
- include FindSetDri
14
-
15
- DEFAULT_MAX_DIFF_RATIO_FOR_DETECTION = 0.15
16
- SPAM_THRESHOLD_FOR_FAILURE_ISSUES = 3
17
- FAILURE_STACKTRACE_REGEX = %r{((.*Failure\/Error:(?<stacktrace>.+))|(?<stacktrace>.+))}m.freeze
18
- ISSUE_STACKTRACE_REGEX = /### Stack trace\s*(```)#{FAILURE_STACKTRACE_REGEX}(```)/m.freeze
19
- FAILED_JOB_DESCRIPTION_REGEX = %r{First happened in https?:\/\/\S+\.}m.freeze
20
- FAILED_JOB_NOTE_REGEX = %r{Failed most recently in \D+ pipeline: https?:\/\/\S+}.freeze
21
- NEW_ISSUE_LABELS = Set.new(%w[QA Quality test failure::new priority::2]).freeze
22
- IGNORE_EXCEPTIONS = ['Net::ReadTimeout', '403 Forbidden - Your account has been blocked'].freeze
23
-
24
- MultipleIssuesFound = Class.new(StandardError)
25
-
26
- def initialize(system_logs: [], max_diff_ratio: DEFAULT_MAX_DIFF_RATIO_FOR_DETECTION, **kwargs)
27
- super
28
- @system_logs = Dir.glob(system_logs)
29
- @max_diff_ratio = max_diff_ratio.to_f
30
- @issue_type = 'issue'
31
- @commented_issue_list = Set.new
32
- end
33
-
34
- private
35
-
36
- attr_reader :max_diff_ratio
37
-
38
- def run!
39
- puts "Reporting test failures in `#{files.join(',')}` as issues in project `#{project}` via the API at `#{Runtime::Env.gitlab_api_base}`."
40
-
41
- test_results_per_file do |test_results|
42
- puts "=> Reporting tests in #{test_results.path}"
43
-
44
- test_results.each do |test|
45
- relate_failure_to_issue(test) if should_report?(test)
46
- end
47
-
48
- test_results.write
49
- end
50
- end
51
-
52
- def relate_failure_to_issue(test)
53
- puts " => Searching issues for test '#{test.name}'..."
54
-
55
- begin
56
- issue, issue_already_commented = find_and_link_issue(test)
57
- return create_issue(test) unless issue || test.quarantine?
58
-
59
- update_labels(issue, test) unless issue_already_commented
60
- rescue MultipleIssuesFound => e
61
- warn(e.message)
62
- end
63
- end
64
-
65
- def find_and_link_issue(test)
66
- issue, diff_ratio = find_failure_issue(test)
67
- return [false, true] unless issue
68
-
69
- issue_already_commented = issue_already_commented?(issue)
70
- if issue_already_commented
71
- puts " => Failure already commented on issue."
72
- else
73
- puts " => Found issue #{issue.web_url} for test '#{test.name}' with a diff ratio of #{(diff_ratio * 100).round(2)}%."
74
- post_or_update_failed_job_note(issue, test)
75
- @commented_issue_list.add(issue.web_url)
76
- end
77
-
78
- [issue, issue_already_commented]
79
- end
80
-
81
- def create_issue(test)
82
- similar_issues = pipeline_issues_with_similar_stacktrace(test)
83
-
84
- if similar_issues.size >= SPAM_THRESHOLD_FOR_FAILURE_ISSUES
85
- puts " => Similar failure issues have already been opened for same pipeline environment"
86
- puts " => Will not create new issue for this failing spec"
87
- similar_issues.each do |similar_issue|
88
- puts "Please check issue: #{similar_issue.web_url}"
89
- gitlab.create_issue_note(iid: similar_issue.iid, note: "This failed job is most likely related: #{test.ci_job_url}")
90
- end
91
- return
92
- end
93
-
94
- issue = super
95
- puts "for test '#{test.name}'."
96
-
97
- post_or_update_failed_job_note(issue, test)
98
-
99
- assign_dri(issue, test)
100
-
101
- issue
102
- end
103
-
104
- def pipeline_issues_with_similar_stacktrace(test)
105
- gitlab.find_issues(options: { state: 'opened', labels: 'QA,failure::new', created_after: past_timestamp(2) }).select do |issue|
106
- job_url_from_issue = failed_issue_job_url(issue)
107
- next unless pipeline == pipeline_env_from_job_url(job_url_from_issue)
108
-
109
- stack_trace_from_issue = cleaned_stack_trace_from_issue(issue)
110
- stack_trace_from_test = cleaned_stacktrace_from_test(test)
111
- diff_ratio = compare_stack_traces(stack_trace_from_test, stack_trace_from_issue)
112
- diff_ratio < max_diff_ratio
113
- end
114
- end
115
-
116
- def failed_issue_job_url(issue)
117
- existing_note = existing_failure_note(issue)
118
- if existing_note
119
- job_url_string = existing_note.body
120
- matched = job_url_string.match(FAILED_JOB_NOTE_REGEX)
121
- else
122
- job_url_string = issue.description
123
- matched = job_url_string.match(FAILED_JOB_DESCRIPTION_REGEX)
124
- end
125
-
126
- return unless matched
127
-
128
- job_url = matched[0].chop.split(" ").last
129
- puts "=> Found failed job url in the issue: #{job_url}"
130
- job_url
131
- end
132
-
133
- def pipeline_env_from_job_url(job_url)
134
- return if job_url.nil?
135
-
136
- if job_url.include?('/quality/')
137
- job_url.partition('/quality/').last.partition('/').first
138
- else
139
- 'master'
140
- end
141
- end
142
-
143
- def past_timestamp(hours_ago)
144
- timestamp = Time.now - (hours_ago * 60 * 60)
145
- timestamp.utc.strftime("%Y-%m-%dT%H:%M:%SZ")
146
- end
147
-
148
- def failure_issues(test)
149
- gitlab.find_issues(options: { state: 'opened', labels: 'QA' }).select do |issue|
150
- issue_title = issue.title.strip
151
- issue_title.include?(test.name) || issue_title.include?(partial_file_path(test.file))
152
- end
153
- end
154
-
155
- def full_stacktrace(test)
156
- if test.failures.first['message_lines'].empty? || test.failures.first['message_lines'].instance_of?(String)
157
- test.failures.first['message']
158
- else
159
- test.failures.first['message_lines'].join("\n")
160
- end
161
- end
162
-
163
- def cleaned_stack_trace_from_issue(issue)
164
- relevant_issue_stacktrace = find_issue_stacktrace(issue)
165
- return unless relevant_issue_stacktrace
166
-
167
- remove_unique_resource_names(relevant_issue_stacktrace)
168
- end
169
-
170
- def cleaned_stacktrace_from_test(test)
171
- first_test_failure_stacktrace = sanitize_stacktrace(full_stacktrace(test), FAILURE_STACKTRACE_REGEX) || full_stacktrace(test)
172
- remove_unique_resource_names(first_test_failure_stacktrace)
173
- end
174
-
175
- def compare_stack_traces(stack_trace_first, stack_trace_second)
176
- calculate_diff_ratio(stack_trace_first, stack_trace_second)
177
- end
178
-
179
- def calculate_diff_ratio(stack_trace_first, stack_trace_second)
180
- ld = Class.new.extend(Gem::Text).method(:levenshtein_distance)
181
- distance = ld.call(stack_trace_first, stack_trace_second)
182
- distance.zero? ? 0.0 : (distance.to_f / stack_trace_first.size).round(3)
183
- end
184
-
185
- def find_relevant_failure_issues(test) # rubocop:disable Metrics/AbcSize
186
- clean_first_test_failure_stacktrace = cleaned_stacktrace_from_test(test)
187
- # Search with the `search` param returns 500 errors, so we filter by ~QA and then filter further in Ruby
188
- failure_issues(test).each_with_object({}) do |issue, memo|
189
- clean_relevant_issue_stacktrace = cleaned_stack_trace_from_issue(issue)
190
- next if clean_relevant_issue_stacktrace.nil?
191
-
192
- diff_ratio = compare_stack_traces(clean_first_test_failure_stacktrace, clean_relevant_issue_stacktrace)
193
- if diff_ratio <= max_diff_ratio
194
- puts " => [DEBUG] Issue #{issue.web_url} has an acceptable diff ratio of #{(diff_ratio * 100).round(2)}%."
195
- # The `Gitlab::ObjectifiedHash` class overrides `#hash` which is used by `Hash#[]=` to compute the hash key.
196
- # 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.
197
- # See:
198
- # - https://gitlab.com/gitlab-org/gitlab-qa/-/merge_requests/587#note_453336995
199
- # - https://github.com/NARKOZ/gitlab/commit/cbdbd1e32623f018a8fae39932a8e3bc4d929abb?_pjax=%23js-repo-pjax-container#r44484494
200
- memo[issue.to_h] = diff_ratio
201
- else
202
- puts " => [DEBUG] Found issue #{issue.web_url} but stacktraces are too different (#{(diff_ratio * 100).round(2)}%).\n"
203
- puts " => [DEBUG] Issue stacktrace:\n----------------\n#{clean_relevant_issue_stacktrace}\n----------------\n"
204
- puts " => [DEBUG] Failure stacktrace:\n----------------\n#{clean_first_test_failure_stacktrace}\n----------------\n"
205
- end
206
- end
207
- end
208
-
209
- def find_issue_stacktrace(issue)
210
- issue_stacktrace = sanitize_stacktrace(issue.description, ISSUE_STACKTRACE_REGEX)
211
- return issue_stacktrace if issue_stacktrace
212
-
213
- puts " => [DEBUG] Stacktrace couldn't be found for #{issue.web_url}!"
214
- end
215
-
216
- def sanitize_stacktrace(stacktrace, regex)
217
- stacktrace_match = stacktrace.match(regex)
218
-
219
- if stacktrace_match
220
- stacktrace_match[:stacktrace].split('First happened in')[0].gsub(/^\s*#.*$/, '').gsub(/^[[:space:]]+/, '').strip
221
- else
222
- puts " => [DEBUG] Stacktrace doesn't match the expected regex (#{regex}):\n----------------\n#{stacktrace}\n----------------\n"
223
- end
224
- end
225
-
226
- def remove_unique_resource_names(stacktrace)
227
- stacktrace.gsub(/qa-(test|user)-[a-z0-9-]+/, '<unique-test-resource>').gsub(/(?:-|_)(?:\d+[a-z]|[a-z]+\d)[a-z\d]{4,}/, '<unique-hash>')
228
- end
229
-
230
- def find_failure_issue(test)
231
- relevant_issues = find_relevant_failure_issues(test)
232
-
233
- return nil if relevant_issues.empty?
234
-
235
- best_matching_issue, smaller_diff_ratio = relevant_issues.min_by { |_, diff_ratio| diff_ratio }
236
-
237
- unless relevant_issues.values.count(smaller_diff_ratio) == 1 # rubocop:disable Style/IfUnlessModifier
238
- raise(MultipleIssuesFound, %(Too many issues found for test '#{test.name}' (`#{test.file}`)!))
239
- end
240
-
241
- # Re-instantiate a `Gitlab::ObjectifiedHash` object after having converted it to a hash in #find_relevant_failure_issues above.
242
- best_matching_issue = Gitlab::ObjectifiedHash.new(best_matching_issue)
243
-
244
- test.failure_issue ||= best_matching_issue.web_url
245
-
246
- [best_matching_issue, smaller_diff_ratio]
247
- end
248
-
249
- def new_issue_description(test)
250
- super + [
251
- "\n\n### Stack trace",
252
- "```\n#{full_stacktrace(test)}\n```",
253
- "First happened in #{test.ci_job_url}.",
254
- "Related test case: #{test.testcase}.",
255
- screenshot_section(test),
256
- system_log_errors_section(test)
257
- ].join("\n\n")
258
- end
259
-
260
- def system_log_errors_section(test)
261
- correlation_id = test.failures.first['correlation_id']
262
- section = ''
263
-
264
- if @system_logs.any? && !correlation_id.nil?
265
- section = SystemLogs::SystemLogsFormatter.new(
266
- @system_logs,
267
- correlation_id
268
- ).system_logs_summary_markdown
269
- end
270
-
271
- puts " => No system logs or correlation id provided, skipping this section in issue description" if section.empty?
272
-
273
- section
274
- end
275
-
276
- def new_issue_labels(test)
277
- up_to_date_labels(test: test, new_labels: NEW_ISSUE_LABELS)
278
- end
279
-
280
- def up_to_date_labels(test:, issue: nil, new_labels: Set.new)
281
- super << pipeline_name_label
282
- end
283
-
284
- def post_or_update_failed_job_note(issue, test)
285
- current_note = "Failed most recently in #{pipeline} pipeline: #{test.ci_job_url}"
286
- existing_note = existing_failure_note(issue)
287
-
288
- return if existing_note && current_note == existing_note.body
289
-
290
- if existing_note
291
- gitlab.edit_issue_note(issue_iid: issue.iid, note_id: existing_note.id, note: current_note)
292
- else
293
- gitlab.create_issue_note(iid: issue.iid, note: current_note)
294
- end
295
-
296
- puts " => Linked #{test.ci_job_url} to #{issue.web_url}."
297
- end
298
-
299
- def new_issue_title(test)
300
- "Failure in #{super}"
301
- end
302
-
303
- def existing_failure_note(issue)
304
- gitlab.find_issue_notes(iid: issue.iid)&.find do |note|
305
- note.body.include?('Failed most recently in')
306
- end
307
- end
308
-
309
- def screenshot_section(test)
310
- section = ''
311
-
312
- failure = full_stacktrace(test)
313
-
314
- if test.screenshot? && !['500 Internal Server Error', 'fabricate_via_api!', 'Error Code 500'].any? { |e| failure.include?(e) }
315
- relative_url = gitlab.upload_file(file_fullpath: test.failure_screenshot)
316
-
317
- section = "### Screenshot: #{relative_url.markdown}" if relative_url
318
- end
319
-
320
- section
321
- end
322
-
323
- def assign_dri(issue, test)
324
- if test.product_group?
325
- dri = set_dri_via_group(test.product_group, test)
326
- dri_id = gitlab.find_user_id(username: dri)
327
- gitlab.edit_issue(iid: issue.iid, options: { assignee_id: dri_id, due_date: Date.today + 1.month })
328
- puts " => Assigning #{dri} as DRI for the issue."
329
- else
330
- puts " => No product group metadata found for test '#{test.name}'"
331
- end
332
- end
333
-
334
- # Checks if a test failure should be reported.
335
- #
336
- # @return [Boolean] false if the test was skipped or failed because of a transient error that can be ignored.
337
- # Otherwise returns true.
338
- def should_report?(test)
339
- return false if test.failures.empty?
340
-
341
- if test.report.key?('exceptions')
342
- reason = ignore_failure_reason(test.report['exceptions'])
343
-
344
- if reason
345
- puts "Failure reporting skipped because #{reason}"
346
-
347
- return false
348
- end
349
- end
350
-
351
- true
352
- end
353
-
354
- # Determine any reason to ignore a failure.
355
- #
356
- # @param [Array<Hash>] exceptions the exceptions associated with the failure.
357
- # @return [String] the reason to ignore the exceptions, or `nil` if any exceptions should not be ignored.
358
- def ignore_failure_reason(exceptions)
359
- exception_messages = exceptions
360
- .filter_map { |exception| exception['message'] if IGNORE_EXCEPTIONS.any? { |e| exception['message'].include?(e) } }
361
- .compact
362
- return if exception_messages.empty? || exception_messages.size < exceptions.size
363
-
364
- msg = exception_messages.many? ? 'the errors were' : 'the error was'
365
- "#{msg} #{exception_messages.join(', ')}"
366
- end
367
-
368
- def issue_already_commented?(issue)
369
- @commented_issue_list.include?(issue.web_url)
370
- end
371
- end
372
- end
373
- end
374
- end
@@ -1,176 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'set'
4
-
5
- module Gitlab
6
- module QA
7
- module Report
8
- class ReportAsIssue
9
- MAX_TITLE_LENGTH = 255
10
-
11
- def initialize(token:, input_files:, project: nil, dry_run: false, **kwargs)
12
- @project = project
13
- @gitlab = (dry_run ? GitlabIssueDryClient : GitlabIssueClient).new(token: token, project: project)
14
- @files = Array(input_files)
15
- end
16
-
17
- def invoke!
18
- validate_input!
19
-
20
- run!
21
- end
22
-
23
- private
24
-
25
- attr_reader :gitlab, :files, :project, :issue_type
26
-
27
- def run!
28
- raise NotImplementedError
29
- end
30
-
31
- def new_issue_title(test)
32
- "#{partial_file_path(test.file)} | #{search_safe(test.name)}".strip
33
- end
34
-
35
- def new_issue_description(test)
36
- "### Full description\n\n#{search_safe(test.name)}\n\n### File path\n\n#{test.file}"
37
- end
38
-
39
- def new_issue_labels(test)
40
- []
41
- end
42
-
43
- def validate_input!
44
- assert_project!
45
- assert_input_files!(files)
46
- gitlab.assert_user_permission!
47
- end
48
-
49
- def assert_project!
50
- return if project
51
-
52
- abort "Please provide a valid project ID or path with the `-p/--project` option!"
53
- end
54
-
55
- def assert_input_files!(files)
56
- return if Dir.glob(files).any?
57
-
58
- abort "Please provide valid JUnit report files. No files were found matching `#{files.join(',')}`"
59
- end
60
-
61
- def test_results_per_file
62
- Dir.glob(files).each do |path|
63
- extension = File.extname(path)
64
-
65
- test_results =
66
- case extension
67
- when '.json'
68
- Report::JsonTestResults.new(path)
69
- when '.xml'
70
- Report::JUnitTestResults.new(path)
71
- else
72
- raise "Unknown extension #{extension}"
73
- end
74
-
75
- yield test_results
76
- end
77
- end
78
-
79
- def create_issue(test)
80
- issue = gitlab.create_issue(
81
- title: title_from_test(test),
82
- description: new_issue_description(test),
83
- labels: new_issue_labels(test).to_a,
84
- issue_type: issue_type
85
- )
86
-
87
- new_link = issue_type == 'test_case' ? issue.web_url.sub('/issues/', '/quality/test_cases/') : issue.web_url
88
-
89
- puts "Created new #{issue_type}: #{new_link}"
90
-
91
- issue
92
- end
93
-
94
- def issue_labels(issue)
95
- issue&.labels&.to_set || Set.new
96
- end
97
-
98
- def update_labels(issue, test, new_labels = Set.new)
99
- labels = up_to_date_labels(test: test, issue: issue, new_labels: new_labels)
100
-
101
- return if issue_labels(issue) == labels
102
-
103
- gitlab.edit_issue(iid: issue.iid, options: { labels: labels.to_a })
104
- end
105
-
106
- def up_to_date_labels(test:, issue: nil, new_labels: Set.new)
107
- labels = issue_labels(issue)
108
- labels |= new_labels
109
- ee_test?(test) ? labels << 'Enterprise Edition' : labels.delete('Enterprise Edition')
110
-
111
- if test.quarantine?
112
- labels << 'quarantine'
113
- labels << "quarantine::#{test.quarantine_type}"
114
- else
115
- labels.delete_if { |label| label.include?('quarantine') }
116
- end
117
-
118
- labels
119
- end
120
-
121
- def pipeline_name_label
122
- case pipeline
123
- when 'production'
124
- 'found:gitlab.com'
125
- when 'canary', 'staging'
126
- "found:#{pipeline}.gitlab.com"
127
- when 'staging-canary'
128
- "found:canary.staging.gitlab.com"
129
- when 'preprod'
130
- 'found:pre.gitlab.com'
131
- when 'nightly', QA::Runtime::Env.default_branch, 'staging-ref', 'release'
132
- "found:#{pipeline}"
133
- else
134
- raise "No `found:*` label for the `#{pipeline}` pipeline!"
135
- end
136
- end
137
-
138
- def ee_test?(test)
139
- test.file =~ %r{features/ee/(api|browser_ui)}
140
- end
141
-
142
- def partial_file_path(path)
143
- path.match(/((ee|api|browser_ui).*)/i)[1]
144
- end
145
-
146
- def title_from_test(test)
147
- title = new_issue_title(test)
148
-
149
- return title unless title.length > MAX_TITLE_LENGTH
150
-
151
- "#{title[0...MAX_TITLE_LENGTH - 3]}..."
152
- end
153
-
154
- def search_safe(value)
155
- value.delete('"')
156
- end
157
-
158
- def pipeline
159
- # Gets the name of the pipeline the test was run in, to be used as the key of a scoped label
160
- #
161
- # Tests can be run in several pipelines:
162
- # gitlab, nightly, staging, canary, production, preprod, MRs, and the default branch (master/main)
163
- #
164
- # Some of those run in their own project, so CI_PROJECT_NAME is the name we need. Those are:
165
- # nightly, staging, canary, production, and preprod
166
- #
167
- # MR, master/main, and gitlab tests run in gitlab-qa, but we only want to report tests run on
168
- # master/main because the other pipelines will be monitored by the author of the MR that triggered them.
169
- # So we assume that we're reporting a master/main pipeline if the project name is 'gitlab'.
170
-
171
- @pipeline ||= Runtime::Env.pipeline_from_project_name
172
- end
173
- end
174
- end
175
- end
176
- end
@@ -1,64 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Gitlab
4
- module QA
5
- module Report
6
- # Uses the API to create or update GitLab test cases and issues with the results of tests from RSpec report files.
7
- class ReportResults < ReportAsIssue
8
- attr_accessor :testcase_project_reporter, :results_issue_project_reporter, :files, :test_case_project, :results_issue_project, :gitlab
9
-
10
- def initialize(token:, input_files:, test_case_project: nil, results_issue_project: nil, dry_run: false, **kwargs)
11
- @testcase_project_reporter = Gitlab::QA::Report::ResultsInTestCases.new(token: token, input_files: input_files, project: test_case_project, dry_run: dry_run, **kwargs)
12
- @results_issue_project_reporter = Gitlab::QA::Report::ResultsInIssues.new(token: token, input_files: input_files, project: results_issue_project, dry_run: dry_run, **kwargs)
13
- @test_case_project = test_case_project
14
- @results_issue_project = results_issue_project
15
- @files = Array(input_files)
16
- @gitlab = testcase_project_reporter.gitlab
17
- end
18
-
19
- def assert_project!
20
- return if test_case_project && results_issue_project
21
-
22
- abort "Please provide valid project IDs or paths with the `--results-issue-project` and `--test-case-project` options!"
23
- end
24
-
25
- # rubocop:disable Metrics/AbcSize
26
- def run!
27
- puts "Reporting test results in `#{files.join(',')}` as test cases in project `#{test_case_project}`"\
28
- " and issues in project `#{results_issue_project}` via the API at `#{Runtime::Env.gitlab_api_base}`."
29
-
30
- test_results_per_file do |test_results|
31
- puts "Reporting tests in #{test_results.path}"
32
-
33
- test_results.each do |test|
34
- next if test.file.include?('/features/sanity/') || test.skipped
35
-
36
- puts "Reporting test: #{test.file} | #{test.name}\n"
37
-
38
- report_test(test)
39
- end
40
-
41
- test_results.write
42
- end
43
- end
44
- # rubocop:enable Metrics/AbcSize
45
-
46
- private
47
-
48
- def report_test(test)
49
- testcase = testcase_project_reporter.find_or_create_testcase(test)
50
- # The API returns the test case with an issue URL since it is technically a type of issue.
51
- # This updates the URL to a valid test case link.
52
- test.testcase = testcase.web_url.sub('/issues/', '/quality/test_cases/')
53
-
54
- issue, is_new = results_issue_project_reporter.get_related_issue(testcase, test)
55
-
56
- testcase_project_reporter.add_issue_link_to_testcase(testcase, issue, test) if is_new
57
-
58
- testcase_project_reporter.update_testcase(testcase, test)
59
- results_issue_project_reporter.update_issue(issue, test)
60
- end
61
- end
62
- end
63
- end
64
- end