gitlab-qa 10.4.1 → 11.1.0

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