gitlab-qa 10.3.0 → 12.2.1

Sign up to get free protection for your applications and to get access to all the features.
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