gitlab-qa 10.4.1 → 11.1.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.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. data/.gitlab/changelog_config.yml +13 -0
  3. data/.gitlab/merge_request_templates/Release.md +9 -36
  4. data/.rubocop_todo.yml +0 -12
  5. data/Dangerfile +1 -5
  6. data/Gemfile.lock +4 -4
  7. data/README.md +1 -2
  8. data/docs/running_against_remote_grid.md +39 -2
  9. data/gitlab-qa.gemspec +1 -1
  10. data/lib/gitlab/qa/component/gitaly_cluster.rb +0 -1
  11. data/lib/gitlab/qa/component/gitlab.rb +10 -9
  12. data/lib/gitlab/qa/component/selenoid.rb +8 -3
  13. data/lib/gitlab/qa/runtime/env.rb +21 -41
  14. data/lib/gitlab/qa/scenario/test/instance/airgapped.rb +0 -2
  15. data/lib/gitlab/qa/scenario/test/integration/gitaly_cluster.rb +0 -2
  16. data/lib/gitlab/qa/scenario/test/integration/mtls.rb +0 -1
  17. data/lib/gitlab/qa/scenario/test/integration/praefect.rb +0 -2
  18. data/lib/gitlab/qa/scenario/test/integration/registry_with_cdn.rb +2 -2
  19. data/lib/gitlab/qa/version.rb +1 -1
  20. data/lib/gitlab/qa.rb +0 -1
  21. data/support/data/admin_access_token_seed.rb +4 -1
  22. metadata +5 -39
  23. data/bin/slack +0 -14
  24. data/exe/gitlab-qa-report +0 -10
  25. data/lib/gitlab/qa/report/base_test_results.rb +0 -39
  26. data/lib/gitlab/qa/report/find_set_dri.rb +0 -43
  27. data/lib/gitlab/qa/report/generate_test_session.rb +0 -275
  28. data/lib/gitlab/qa/report/gitlab_issue_client.rb +0 -190
  29. data/lib/gitlab/qa/report/gitlab_issue_dry_client.rb +0 -28
  30. data/lib/gitlab/qa/report/j_unit_test_results.rb +0 -27
  31. data/lib/gitlab/qa/report/json_test_results.rb +0 -29
  32. data/lib/gitlab/qa/report/prepare_stage_reports.rb +0 -86
  33. data/lib/gitlab/qa/report/relate_failure_issue.rb +0 -374
  34. data/lib/gitlab/qa/report/report_as_issue.rb +0 -176
  35. data/lib/gitlab/qa/report/report_results.rb +0 -64
  36. data/lib/gitlab/qa/report/results_in_issues.rb +0 -126
  37. data/lib/gitlab/qa/report/results_in_testcases.rb +0 -111
  38. data/lib/gitlab/qa/report/results_reporter_shared.rb +0 -70
  39. data/lib/gitlab/qa/report/summary_table.rb +0 -43
  40. data/lib/gitlab/qa/report/test_result.rb +0 -184
  41. data/lib/gitlab/qa/report/update_screenshot_path.rb +0 -63
  42. data/lib/gitlab/qa/reporter.rb +0 -131
  43. data/lib/gitlab/qa/runtime/token_finder.rb +0 -44
  44. data/lib/gitlab/qa/slack/post_to_slack.rb +0 -30
  45. data/lib/gitlab/qa/system_logs/finders/json_log_finder.rb +0 -65
  46. data/lib/gitlab/qa/system_logs/finders/rails/api_log_finder.rb +0 -21
  47. data/lib/gitlab/qa/system_logs/finders/rails/application_log_finder.rb +0 -21
  48. data/lib/gitlab/qa/system_logs/finders/rails/exception_log_finder.rb +0 -21
  49. data/lib/gitlab/qa/system_logs/finders/rails/graphql_log_finder.rb +0 -21
  50. data/lib/gitlab/qa/system_logs/log_types/log.rb +0 -38
  51. data/lib/gitlab/qa/system_logs/log_types/rails/api_log.rb +0 -34
  52. data/lib/gitlab/qa/system_logs/log_types/rails/application_log.rb +0 -27
  53. data/lib/gitlab/qa/system_logs/log_types/rails/exception_log.rb +0 -23
  54. data/lib/gitlab/qa/system_logs/log_types/rails/graphql_log.rb +0 -30
  55. data/lib/gitlab/qa/system_logs/shared_fields.rb +0 -29
  56. data/lib/gitlab/qa/system_logs/system_logs_formatter.rb +0 -65
@@ -1,28 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Gitlab
4
- module QA
5
- module Report
6
- class GitlabIssueDryClient < GitlabIssueClient
7
- def create_issue(title:, description:, labels:, issue_type: 'issue')
8
- attrs = { description: description, labels: labels }
9
-
10
- puts "The following #{issue_type} would have been created:"
11
- puts "project: #{project}, title: #{title}, attrs: #{attrs}"
12
- end
13
-
14
- def edit_issue(iid:, options: {})
15
- puts "The #{project}##{iid} issue would have been updated with: #{options}"
16
- end
17
-
18
- def create_issue_note(iid:, note:)
19
- puts "The following note would have been posted on #{project}##{iid} issue: #{note}"
20
- end
21
-
22
- def add_note_to_issue_discussion_as_thread(iid:, discussion_id:, body:)
23
- puts "The following discussion note would have been posted on #{project}##{iid} (discussion #{discussion_id}) issue: #{body}"
24
- end
25
- end
26
- end
27
- end
28
- end
@@ -1,27 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'nokogiri'
4
-
5
- module Gitlab
6
- module QA
7
- module Report
8
- class JUnitTestResults < BaseTestResults
9
- def write
10
- # Ignore it for now
11
- end
12
-
13
- private
14
-
15
- def parse
16
- Nokogiri::XML.parse(File.read(path))
17
- end
18
-
19
- def process
20
- results.xpath('//testcase').map do |test|
21
- TestResult.from_junit(test)
22
- end
23
- end
24
- end
25
- end
26
- end
27
- end
@@ -1,29 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'json'
4
-
5
- module Gitlab
6
- module QA
7
- module Report
8
- class JsonTestResults < BaseTestResults
9
- def write
10
- json = results.merge('examples' => testcases.map(&:report))
11
-
12
- File.write(path, JSON.pretty_generate(json))
13
- end
14
-
15
- private
16
-
17
- def parse
18
- JSON.parse(File.read(path))
19
- end
20
-
21
- def process
22
- results['examples'].map do |test|
23
- TestResult.from_json(test)
24
- end
25
- end
26
- end
27
- end
28
- end
29
- end
@@ -1,86 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'nokogiri'
4
-
5
- module Gitlab
6
- module QA
7
- module Report
8
- class PrepareStageReports
9
- def initialize(input_files:)
10
- @input_files = input_files
11
- end
12
-
13
- # Create a new JUnit report file for each Stage, containing tests from that Stage alone
14
- def invoke!
15
- collate_test_cases(@input_files).each do |stage, tests|
16
- filename = "#{stage}.xml"
17
-
18
- File.write(filename, new_junit_report(tests))
19
-
20
- puts "Saved #{filename}"
21
- end
22
- end
23
-
24
- private
25
-
26
- def collate_test_cases(input_files)
27
- # Collect the test cases from the original reports and group them by Stage
28
- testcases = {}
29
-
30
- Dir.glob(input_files).each do |rspec_report_file|
31
- report = Nokogiri::XML(File.open(rspec_report_file))
32
- report.xpath('//testcase').each do |test|
33
- # The test file paths could start with any of
34
- # /qa/specs/features/api/<stage>
35
- # /qa/specs/features/browser_ui/<stage>
36
- # /qa/specs/features/ee/api/<stage>
37
- # /qa/specs/features/ee/browser_ui/<stage>
38
- # For now we assume the Stage is whatever follows api/ or browser_ui/
39
- test_path_match = test['file'].match(%r{(api|browser_ui)/([a-z0-9_]+)}i)
40
- next unless test_path_match
41
-
42
- stage = strip_number_prefix(test_path_match[2])
43
- testcases[stage] = [] unless testcases.key?(stage)
44
- testcases[stage] << test
45
- end
46
- end
47
-
48
- testcases
49
- end
50
-
51
- def strip_number_prefix(stage)
52
- stage.sub(/^\d+_/, '')
53
- end
54
-
55
- def new_junit_report(testcases)
56
- report = Nokogiri::XML::Document.new
57
- testsuite_node = report.create_element('testsuite', name: 'rspec', **collect_stats(testcases))
58
- report.root = testsuite_node
59
-
60
- testcases.each do |test|
61
- testsuite_node.add_child(test)
62
- end
63
-
64
- report.to_s
65
- end
66
-
67
- def collect_stats(testcases)
68
- stats = {
69
- tests: testcases.size,
70
- failures: 0,
71
- errors: 0,
72
- skipped: 0
73
- }
74
-
75
- testcases.each do |test|
76
- stats[:failures] += 1 unless test.search('failure').empty?
77
- stats[:errors] += 1 unless test.search('error').empty?
78
- stats[:skipped] += 1 unless test.search('skipped').empty?
79
- end
80
-
81
- stats
82
- end
83
- end
84
- end
85
- end
86
- end
@@ -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