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.
- checksums.yaml +4 -4
- data/.gitlab/changelog_config.yml +13 -0
- data/.gitlab/merge_request_templates/Release.md +9 -36
- data/.rubocop_todo.yml +0 -12
- data/Dangerfile +1 -5
- data/Gemfile.lock +4 -4
- data/README.md +1 -2
- data/docs/running_against_remote_grid.md +39 -2
- data/gitlab-qa.gemspec +1 -1
- data/lib/gitlab/qa/component/gitaly_cluster.rb +0 -1
- data/lib/gitlab/qa/component/gitlab.rb +10 -9
- data/lib/gitlab/qa/component/selenoid.rb +8 -3
- data/lib/gitlab/qa/runtime/env.rb +21 -41
- data/lib/gitlab/qa/scenario/test/instance/airgapped.rb +0 -2
- data/lib/gitlab/qa/scenario/test/integration/gitaly_cluster.rb +0 -2
- data/lib/gitlab/qa/scenario/test/integration/mtls.rb +0 -1
- data/lib/gitlab/qa/scenario/test/integration/praefect.rb +0 -2
- data/lib/gitlab/qa/scenario/test/integration/registry_with_cdn.rb +2 -2
- data/lib/gitlab/qa/version.rb +1 -1
- data/lib/gitlab/qa.rb +0 -1
- data/support/data/admin_access_token_seed.rb +4 -1
- metadata +5 -39
- data/bin/slack +0 -14
- data/exe/gitlab-qa-report +0 -10
- data/lib/gitlab/qa/report/base_test_results.rb +0 -39
- data/lib/gitlab/qa/report/find_set_dri.rb +0 -43
- data/lib/gitlab/qa/report/generate_test_session.rb +0 -275
- data/lib/gitlab/qa/report/gitlab_issue_client.rb +0 -190
- data/lib/gitlab/qa/report/gitlab_issue_dry_client.rb +0 -28
- data/lib/gitlab/qa/report/j_unit_test_results.rb +0 -27
- data/lib/gitlab/qa/report/json_test_results.rb +0 -29
- data/lib/gitlab/qa/report/prepare_stage_reports.rb +0 -86
- data/lib/gitlab/qa/report/relate_failure_issue.rb +0 -374
- data/lib/gitlab/qa/report/report_as_issue.rb +0 -176
- data/lib/gitlab/qa/report/report_results.rb +0 -64
- data/lib/gitlab/qa/report/results_in_issues.rb +0 -126
- data/lib/gitlab/qa/report/results_in_testcases.rb +0 -111
- data/lib/gitlab/qa/report/results_reporter_shared.rb +0 -70
- data/lib/gitlab/qa/report/summary_table.rb +0 -43
- data/lib/gitlab/qa/report/test_result.rb +0 -184
- data/lib/gitlab/qa/report/update_screenshot_path.rb +0 -63
- data/lib/gitlab/qa/reporter.rb +0 -131
- data/lib/gitlab/qa/runtime/token_finder.rb +0 -44
- data/lib/gitlab/qa/slack/post_to_slack.rb +0 -30
- data/lib/gitlab/qa/system_logs/finders/json_log_finder.rb +0 -65
- data/lib/gitlab/qa/system_logs/finders/rails/api_log_finder.rb +0 -21
- data/lib/gitlab/qa/system_logs/finders/rails/application_log_finder.rb +0 -21
- data/lib/gitlab/qa/system_logs/finders/rails/exception_log_finder.rb +0 -21
- data/lib/gitlab/qa/system_logs/finders/rails/graphql_log_finder.rb +0 -21
- data/lib/gitlab/qa/system_logs/log_types/log.rb +0 -38
- data/lib/gitlab/qa/system_logs/log_types/rails/api_log.rb +0 -34
- data/lib/gitlab/qa/system_logs/log_types/rails/application_log.rb +0 -27
- data/lib/gitlab/qa/system_logs/log_types/rails/exception_log.rb +0 -23
- data/lib/gitlab/qa/system_logs/log_types/rails/graphql_log.rb +0 -30
- data/lib/gitlab/qa/system_logs/shared_fields.rb +0 -29
- 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
|