gitlab_quality-test_tooling 1.5.4 → 1.7.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/Gemfile.lock +6 -1
- data/README.md +19 -0
- data/exe/flaky-test-issues +54 -0
- data/exe/relate-failure-issue +5 -0
- data/lib/gitlab_quality/test_tooling/gitlab_issue_client.rb +10 -0
- data/lib/gitlab_quality/test_tooling/report/concerns/issue_reports.rb +83 -0
- data/lib/gitlab_quality/test_tooling/report/flaky_test_issue.rb +97 -0
- data/lib/gitlab_quality/test_tooling/report/merge_request_slow_tests_report.rb +0 -2
- data/lib/gitlab_quality/test_tooling/report/relate_failure_issue.rb +22 -65
- data/lib/gitlab_quality/test_tooling/report/report_as_issue.rb +19 -9
- data/lib/gitlab_quality/test_tooling/report/slow_test_issue.rb +1 -1
- data/lib/gitlab_quality/test_tooling/test_result/json_test_result.rb +19 -1
- data/lib/gitlab_quality/test_tooling/version.rb +1 -1
- metadata +20 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6924fc592c0dbb5e1ae0aed8710f7f7714c336bdd1d1d31b343e6edddd358ead
|
4
|
+
data.tar.gz: 0ebb5fbe0aeff0417193d4521cf52a78d849b83d116e08118b9cf87ee2655a71
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f9a64cbae7e3a34ae66450ae7fe20181560dc6860950a92e53c5aaf61ab10343372044968b5a726bef51b082a45dd9150d6f161d93a45a4ccf75d550cf981be1
|
7
|
+
data.tar.gz: a77ea69469a02b3cbd089cade6be20347abc03cada1cf93fec8380456f000d083a914296e0b35faff573e60d73e4759abd1b5d224cb04e64b38862fe720cf65c
|
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
gitlab_quality-test_tooling (1.
|
4
|
+
gitlab_quality-test_tooling (1.7.0)
|
5
5
|
activesupport (>= 6.1, < 7.2)
|
6
6
|
amatch (~> 0.4.1)
|
7
7
|
gitlab (~> 4.19)
|
@@ -29,6 +29,7 @@ GEM
|
|
29
29
|
ast (2.4.2)
|
30
30
|
backport (1.2.0)
|
31
31
|
benchmark (0.2.1)
|
32
|
+
byebug (11.1.3)
|
32
33
|
claide (1.1.0)
|
33
34
|
claide-plugins (0.9.2)
|
34
35
|
cork
|
@@ -160,6 +161,9 @@ GEM
|
|
160
161
|
pry (0.14.2)
|
161
162
|
coderay (~> 1.1)
|
162
163
|
method_source (~> 1.0)
|
164
|
+
pry-byebug (3.10.1)
|
165
|
+
byebug (~> 11.0)
|
166
|
+
pry (>= 0.13, < 0.15)
|
163
167
|
public_suffix (5.0.1)
|
164
168
|
racc (1.6.2)
|
165
169
|
rack (3.0.7)
|
@@ -279,6 +283,7 @@ DEPENDENCIES
|
|
279
283
|
gitlab_quality-test_tooling!
|
280
284
|
guard-rspec (~> 4.7)
|
281
285
|
lefthook (~> 1.3)
|
286
|
+
pry-byebug (= 3.10.1)
|
282
287
|
rake (~> 13.0)
|
283
288
|
rspec (~> 3.12)
|
284
289
|
simplecov (~> 0.22)
|
data/README.md
CHANGED
@@ -81,10 +81,14 @@ Usage: exe/relate-failure-issue [options]
|
|
81
81
|
Max stacktrace diff ratio for failure issues detection
|
82
82
|
-p, --project PROJECT Can be an integer or a group/project string
|
83
83
|
-t, --token TOKEN A valid access token with `api` scope and Maintainer permission in PROJECT
|
84
|
+
-r RELATED_ISSUES_FILE, The file path for the related issues
|
85
|
+
--related-issues-file
|
84
86
|
--system-log-files SYSTEM_LOG_FILES
|
85
87
|
Include errors from system logs in failure issues
|
86
88
|
--base-issue-labels BASE_ISSUE_LABELS
|
87
89
|
Labels to add to new failure issues
|
90
|
+
--exclude-labels-for-search EXCLUDE_LABELS_FOR_SEARCH
|
91
|
+
Labels to exclude when searching for existing issues
|
88
92
|
--confidential Makes created new issues confidential
|
89
93
|
--dry-run Perform a dry-run (don't create or update issues)
|
90
94
|
-v, --version Show the version
|
@@ -133,6 +137,21 @@ Usage: exe/slow-test-issue [options]
|
|
133
137
|
-h, --help Show the usage
|
134
138
|
```
|
135
139
|
|
140
|
+
### `exe/flaky-test-issues`
|
141
|
+
|
142
|
+
```shell
|
143
|
+
Purpose: Purpose: Create flaky test issues for any passed test coming from rspec-retry JSON report files
|
144
|
+
Usage: exe/flaky-test-issue [options]
|
145
|
+
-i, --input-files INPUT_FILES JSON rspec-retry report files
|
146
|
+
-p, --project PROJECT Can be an integer or a group/project string
|
147
|
+
-m MERGE_REQUEST_IID, An integer merge request IID
|
148
|
+
--merge_request_iid
|
149
|
+
-t, --token TOKEN A valid access token with `api` scope and Maintainer permission in PROJECT
|
150
|
+
--dry-run Perform a dry-run (don't create note)
|
151
|
+
-v, --version Show the version
|
152
|
+
-h, --help Show the usage
|
153
|
+
```
|
154
|
+
|
136
155
|
### `exe/slow-test-merge-request-report-note`
|
137
156
|
|
138
157
|
```shell
|
@@ -0,0 +1,54 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "bundler/setup"
|
5
|
+
require "optparse"
|
6
|
+
|
7
|
+
require_relative "../lib/gitlab_quality/test_tooling"
|
8
|
+
|
9
|
+
params = {}
|
10
|
+
|
11
|
+
options = OptionParser.new do |opts|
|
12
|
+
opts.banner = "Usage: #{$PROGRAM_NAME} [options]"
|
13
|
+
|
14
|
+
opts.on('-i', '--input-files INPUT_FILES', String, 'JSON rspec-retry report files') do |input_files|
|
15
|
+
params[:input_files] = input_files
|
16
|
+
end
|
17
|
+
|
18
|
+
opts.on('-p', '--project PROJECT', String, 'Can be an integer or a group/project string') do |project|
|
19
|
+
params[:project] = project
|
20
|
+
end
|
21
|
+
|
22
|
+
opts.on('-m', '--merge_request_iid MERGE_REQUEST_IID', String, 'An integer merge request IID') do |merge_request_iid|
|
23
|
+
params[:merge_request_iid] = merge_request_iid
|
24
|
+
end
|
25
|
+
|
26
|
+
opts.on('-t', '--token TOKEN', String, 'A valid access token with `api` scope and Maintainer permission in PROJECT') do |token|
|
27
|
+
params[:token] = token
|
28
|
+
end
|
29
|
+
|
30
|
+
opts.on('--dry-run', "Perform a dry-run (don't create issues)") do
|
31
|
+
params[:dry_run] = true
|
32
|
+
end
|
33
|
+
|
34
|
+
opts.on_tail('-v', '--version', 'Show the version') do
|
35
|
+
require_relative "../lib/gitlab_quality/test_tooling/version"
|
36
|
+
puts "#{$PROGRAM_NAME} : #{GitlabQuality::TestTooling::VERSION}"
|
37
|
+
exit
|
38
|
+
end
|
39
|
+
|
40
|
+
opts.on_tail('-h', '--help', 'Show the usage') do
|
41
|
+
puts "Purpose: Create flaky test issues for any passed test coming from rspec-retry JSON report files."
|
42
|
+
puts opts
|
43
|
+
exit
|
44
|
+
end
|
45
|
+
|
46
|
+
opts.parse(ARGV)
|
47
|
+
end
|
48
|
+
|
49
|
+
if params.any?
|
50
|
+
GitlabQuality::TestTooling::Report::FlakyTestIssue.new(**params).invoke!
|
51
|
+
else
|
52
|
+
puts options
|
53
|
+
exit 1
|
54
|
+
end
|
data/exe/relate-failure-issue
CHANGED
@@ -41,6 +41,11 @@ options = OptionParser.new do |opts|
|
|
41
41
|
params[:base_issue_labels] = base_issue_labels.split(',')
|
42
42
|
end
|
43
43
|
|
44
|
+
opts.on('--exclude-labels-for-search EXCLUDE_LABELS_FOR_SEARCH', String,
|
45
|
+
'Labels to exclude when searching for existing issues') do |exclude_labels_for_search|
|
46
|
+
params[:exclude_labels_for_search] = exclude_labels_for_search.split(',')
|
47
|
+
end
|
48
|
+
|
44
49
|
opts.on('--confidential', "Makes created new issues confidential") do
|
45
50
|
params[:confidential] = true
|
46
51
|
end
|
@@ -55,6 +55,16 @@ module GitlabQuality
|
|
55
55
|
end
|
56
56
|
end
|
57
57
|
|
58
|
+
def find_issues_by_hash(test_hash, &select)
|
59
|
+
select ||= :itself
|
60
|
+
|
61
|
+
handle_gitlab_client_exceptions do
|
62
|
+
client.search_in_project(project, 'issues', test_hash)
|
63
|
+
.auto_paginate
|
64
|
+
.select(&select)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
58
68
|
def find_issue_discussions(iid:)
|
59
69
|
handle_gitlab_client_exceptions do
|
60
70
|
client.issue_discussions(project, iid, order_by: 'created_at', sort: 'asc').auto_paginate
|
@@ -0,0 +1,83 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GitlabQuality
|
4
|
+
module TestTooling
|
5
|
+
module Report
|
6
|
+
module Concerns
|
7
|
+
module IssueReports
|
8
|
+
JOB_URL_REGEX = %r{(?<job_url>https://(?<host>[\w.]+)/(?<project_path>[\w\-./]+)/-/jobs/\d+)}
|
9
|
+
FAILED_JOB_DESCRIPTION_REGEX = /First happened in #{JOB_URL_REGEX}\./m
|
10
|
+
REPORT_ITEM_REGEX = /^1\. \d{4}-\d{2}-\d{2}: #{JOB_URL_REGEX} \((?<pipeline_url>.+)\)$/
|
11
|
+
|
12
|
+
def initial_reports_section(test)
|
13
|
+
<<~REPORTS
|
14
|
+
### Reports (1)
|
15
|
+
|
16
|
+
#{report_list_item(test)}
|
17
|
+
REPORTS
|
18
|
+
end
|
19
|
+
|
20
|
+
def add_report_to_issue_description(issue, test)
|
21
|
+
issue_description = issue.description
|
22
|
+
|
23
|
+
# We include the number of reports in the header, for visibility.
|
24
|
+
new_issue_description =
|
25
|
+
if issue_description.include?('### Reports')
|
26
|
+
# We count the number of existing reports.
|
27
|
+
reports_count = issue_description
|
28
|
+
.scan(REPORT_ITEM_REGEX)
|
29
|
+
.size.to_i + 1
|
30
|
+
issue_description.sub(/^### Reports.*$/, "### Reports (#{reports_count})")
|
31
|
+
else # For issue with the legacy format, we add the Reports section
|
32
|
+
update_legacy_issue_description(issue_description)
|
33
|
+
end
|
34
|
+
|
35
|
+
[new_issue_description, report_list_item(test)].join("\n")
|
36
|
+
end
|
37
|
+
|
38
|
+
def failed_issue_job_url(issue)
|
39
|
+
job_urls_from_description(issue.description, REPORT_ITEM_REGEX).last ||
|
40
|
+
# Legacy format
|
41
|
+
job_urls_from_description(issue.description, FAILED_JOB_DESCRIPTION_REGEX).last
|
42
|
+
end
|
43
|
+
|
44
|
+
def failed_issue_job_urls(issue)
|
45
|
+
job_urls_from_description(issue.description, REPORT_ITEM_REGEX) +
|
46
|
+
# Legacy format
|
47
|
+
job_urls_from_description(issue.description, FAILED_JOB_DESCRIPTION_REGEX)
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def report_list_item(test)
|
53
|
+
"1. #{Time.new.utc.strftime('%F')}: #{test.ci_job_url} (#{ENV.fetch('CI_PIPELINE_URL', 'pipeline url is missing')})"
|
54
|
+
end
|
55
|
+
|
56
|
+
def job_urls_from_description(issue_description, regex)
|
57
|
+
issue_description.lines.filter_map do |line|
|
58
|
+
match = line.match(regex)
|
59
|
+
match[:job_url] if match
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def update_legacy_issue_description(issue_description)
|
64
|
+
test_captures = issue_description.scan(JOB_URL_REGEX)
|
65
|
+
reports_count = test_captures.size.to_i + 1
|
66
|
+
|
67
|
+
updated_description = "#{issue_description}\n\n### Reports (#{reports_count})\n"
|
68
|
+
updated_description = [updated_description, *test_captures_to_report_items(test_captures)].join("\n") unless test_captures.empty?
|
69
|
+
updated_description
|
70
|
+
end
|
71
|
+
|
72
|
+
def test_captures_to_report_items(test_captures)
|
73
|
+
test_captures.map do |ci_job_url, _, _|
|
74
|
+
report_list_item(GitlabQuality::TestTooling::TestResult::JsonTestResult.new(
|
75
|
+
'ci_job_url' => ci_job_url
|
76
|
+
))
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GitlabQuality
|
4
|
+
module TestTooling
|
5
|
+
module Report
|
6
|
+
# Uses the API to create GitLab issues for any passed test coming from JSON test reports.
|
7
|
+
# We expect the test reports to come from rspec-retry, or from a new RSpec process where
|
8
|
+
# we retried failing specs.
|
9
|
+
#
|
10
|
+
# - Takes the JSON test reports like rspec-*.json (typically from rspec-retry gem)`
|
11
|
+
# - Takes a project where flaky test issues should be created
|
12
|
+
# - For every passed test in the report:
|
13
|
+
# - Find issue by test hash
|
14
|
+
# - Reopen issue if it already exists, but is closed
|
15
|
+
class FlakyTestIssue < ReportAsIssue
|
16
|
+
include Concerns::IssueReports
|
17
|
+
|
18
|
+
NEW_ISSUE_LABELS = Set.new(['type::maintenance', 'priority::3', 'severity::3', 'failure::flaky-test']).freeze
|
19
|
+
FOUND_IN_MR_LABEL = 'found:in MR'
|
20
|
+
FOUND_IN_MASTER_LABEL = 'found:master'
|
21
|
+
|
22
|
+
def initialize(token:, input_files:, project: nil, merge_request_iid: nil, confidential: false, dry_run: false, **_kwargs)
|
23
|
+
super(token: token, input_files: input_files, project: project, confidential: confidential, dry_run: dry_run)
|
24
|
+
@merge_request_iid = merge_request_iid
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
attr_reader :merge_request_iid
|
30
|
+
|
31
|
+
def run!
|
32
|
+
puts "Reporting flaky tests in `#{files.join(',')}` as issues in project `#{project}` via the API at `#{Runtime::Env.gitlab_api_base}`."
|
33
|
+
|
34
|
+
TestResults::Builder.new(files).test_results_per_file do |test_results|
|
35
|
+
puts "=> Reporting #{test_results.count} tests in #{test_results.path}"
|
36
|
+
|
37
|
+
test_results.each do |test|
|
38
|
+
next if test.status != 'passed' # We only want failed tests that passed in the end
|
39
|
+
|
40
|
+
create_flaky_issue(test)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def new_issue_title(test)
|
46
|
+
"Flaky test in #{super}"
|
47
|
+
end
|
48
|
+
|
49
|
+
def new_issue_labels(_test)
|
50
|
+
found_label =
|
51
|
+
if !merge_request_iid || merge_request_iid.empty?
|
52
|
+
FOUND_IN_MASTER_LABEL
|
53
|
+
else
|
54
|
+
FOUND_IN_MR_LABEL
|
55
|
+
end
|
56
|
+
|
57
|
+
NEW_ISSUE_LABELS + [found_label]
|
58
|
+
end
|
59
|
+
|
60
|
+
def new_issue_description(test)
|
61
|
+
super + [
|
62
|
+
"Flaky tests were detected, please refer to the [Flaky tests documentation](https://docs.gitlab.com/ee/development/testing_guide/flaky_tests.html) " \
|
63
|
+
"to learn more about how to reproduce them.",
|
64
|
+
initial_reports_section(test)
|
65
|
+
].compact.join("\n\n")
|
66
|
+
end
|
67
|
+
|
68
|
+
def create_flaky_issue(test)
|
69
|
+
puts " => Finding existing issues for flaky test '#{test.name}' (run time: #{test.run_time} seconds)..."
|
70
|
+
|
71
|
+
issues = find_issues_by_hash(test_hash(test))
|
72
|
+
issues.each do |issue|
|
73
|
+
puts " => Existing issue link #{issue.web_url}."
|
74
|
+
|
75
|
+
puts " => Adding the flaky test to the existing issue..."
|
76
|
+
add_report_to_issue(issue, test)
|
77
|
+
|
78
|
+
if issue.state == 'closed'
|
79
|
+
puts " => Issue is closed. Reopening it."
|
80
|
+
reopen_issue(issue)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
create_issue(test) unless issues.any?
|
85
|
+
end
|
86
|
+
|
87
|
+
def add_report_to_issue(issue, test)
|
88
|
+
gitlab.edit_issue(iid: issue.iid, options: { description: add_report_to_issue_description(issue, test) })
|
89
|
+
end
|
90
|
+
|
91
|
+
def reopen_issue(issue)
|
92
|
+
gitlab.edit_issue(iid: issue.iid, options: { state_event: 'reopen' })
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -7,7 +7,6 @@ module GitlabQuality
|
|
7
7
|
SLOW_TEST_MESSAGE = '<!-- slow-test -->'
|
8
8
|
SLOW_TEST_LABEL = '/label ~"rspec:slow test detected"'
|
9
9
|
SLOW_TEST_NOTE_SOURCE_CODE = 'Generated by [`gitlab_quality-test_tooling`](https://gitlab.com/gitlab-org/ruby/gems/gitlab_quality-test_tooling/-/blob/main/lib/gitlab_quality/test_tooling/report/merge_request_slow_tests_report.rb).'
|
10
|
-
SLOW_TEST_NOTE_FEEDBACK = 'Please [share your feedback and suggestions](https://gitlab.com/gitlab-org/quality/engineering-productivity/team/-/issues/289).'
|
11
10
|
|
12
11
|
def initialize(token:, input_files:, merge_request_iid:, project: nil, dry_run: false, **_kwargs)
|
13
12
|
@project = project
|
@@ -70,7 +69,6 @@ module GitlabQuality
|
|
70
69
|
SLOW_TEST_MESSAGE,
|
71
70
|
SLOW_TEST_LABEL,
|
72
71
|
":tools: #{SLOW_TEST_NOTE_SOURCE_CODE}\n",
|
73
|
-
":recycle: #{SLOW_TEST_NOTE_FEEDBACK}\n",
|
74
72
|
"---\n",
|
75
73
|
":snail: Slow tests detected in this merge request. These slow tests might be related to this merge request's changes.",
|
76
74
|
"<details><summary>Click to expand</summary>\n",
|
@@ -18,6 +18,7 @@ module GitlabQuality
|
|
18
18
|
class RelateFailureIssue < ReportAsIssue
|
19
19
|
include Concerns::FindSetDri
|
20
20
|
include Concerns::GroupAndCategoryLabels
|
21
|
+
include Concerns::IssueReports
|
21
22
|
include Amatch
|
22
23
|
|
23
24
|
DEFAULT_MAX_DIFF_RATIO_FOR_DETECTION = 0.15
|
@@ -25,9 +26,7 @@ module GitlabQuality
|
|
25
26
|
SPAM_THRESHOLD_FOR_FAILURE_ISSUES = 3
|
26
27
|
FAILURE_STACKTRACE_REGEX = %r{(?:(?:.*Failure/Error:(?<stacktrace>.+))|(?<stacktrace>.+))}m
|
27
28
|
ISSUE_STACKTRACE_REGEX = /### Stack trace\s*(```)#{FAILURE_STACKTRACE_REGEX}(```)/m
|
28
|
-
|
29
|
-
FAILED_JOB_DESCRIPTION_REGEX = /First happened in #{JOB_URL_REGEX}\./m
|
30
|
-
REPORT_ITEM_REGEX = /^1\. \d{4}-\d{2}-\d{2}: #{JOB_URL_REGEX} \((?<pipeline_url>.+)\)$/
|
29
|
+
|
31
30
|
NEW_ISSUE_LABELS = Set.new(%w[test failure::new priority::2]).freeze
|
32
31
|
IGNORED_FAILURES = [
|
33
32
|
'Net::ReadTimeout',
|
@@ -37,18 +36,24 @@ module GitlabQuality
|
|
37
36
|
|
38
37
|
MultipleIssuesFound = Class.new(StandardError)
|
39
38
|
|
40
|
-
def initialize(
|
39
|
+
def initialize(
|
40
|
+
max_diff_ratio: DEFAULT_MAX_DIFF_RATIO_FOR_DETECTION,
|
41
|
+
system_logs: [],
|
42
|
+
base_issue_labels: nil,
|
43
|
+
exclude_labels_for_search: nil,
|
44
|
+
**kwargs)
|
41
45
|
super
|
42
46
|
@max_diff_ratio = max_diff_ratio.to_f
|
43
47
|
@system_logs = Dir.glob(system_logs)
|
44
48
|
@base_issue_labels = Set.new(base_issue_labels)
|
49
|
+
@exclude_labels_for_search = Set.new(exclude_labels_for_search)
|
45
50
|
@issue_type = 'issue'
|
46
51
|
@commented_issue_list = Set.new
|
47
52
|
end
|
48
53
|
|
49
54
|
private
|
50
55
|
|
51
|
-
attr_reader :max_diff_ratio, :system_logs, :base_issue_labels
|
56
|
+
attr_reader :max_diff_ratio, :system_logs, :base_issue_labels, :exclude_labels_for_search
|
52
57
|
|
53
58
|
def run!
|
54
59
|
puts "Reporting test failures in `#{files.join(',')}` as issues in project `#{project}` via the API at `#{Runtime::Env.gitlab_api_base}`."
|
@@ -136,35 +141,15 @@ module GitlabQuality
|
|
136
141
|
|
137
142
|
def pipeline_issues_with_similar_stacktrace(test)
|
138
143
|
search_labels = (base_issue_labels + Set.new(%w[test failure::new])).to_a
|
144
|
+
not_labels = exclude_labels_for_search.to_a
|
139
145
|
gitlab.find_issues(options: { labels: search_labels,
|
146
|
+
not: { labels: not_labels },
|
140
147
|
created_after: past_timestamp(2) }).select do |issue|
|
141
148
|
job_url_from_issue = failed_issue_job_url(issue)
|
142
149
|
|
143
150
|
next if pipeline != pipeline_env_from_job_url(job_url_from_issue)
|
144
151
|
|
145
|
-
|
146
|
-
stack_trace_from_test = cleaned_stacktrace_from_test(test)
|
147
|
-
diff_ratio = compare_stack_traces(stack_trace_from_test, stack_trace_from_issue)
|
148
|
-
diff_ratio < max_diff_ratio
|
149
|
-
end
|
150
|
-
end
|
151
|
-
|
152
|
-
def failed_issue_job_url(issue)
|
153
|
-
job_urls_from_description(issue.description, REPORT_ITEM_REGEX).last ||
|
154
|
-
# Legacy format
|
155
|
-
job_urls_from_description(issue.description, FAILED_JOB_DESCRIPTION_REGEX).last
|
156
|
-
end
|
157
|
-
|
158
|
-
def failed_issue_job_urls(issue)
|
159
|
-
job_urls_from_description(issue.description, REPORT_ITEM_REGEX) +
|
160
|
-
# Legacy format
|
161
|
-
job_urls_from_description(issue.description, FAILED_JOB_DESCRIPTION_REGEX)
|
162
|
-
end
|
163
|
-
|
164
|
-
def job_urls_from_description(issue_description, regex)
|
165
|
-
issue_description.lines.filter_map do |line|
|
166
|
-
match = line.match(regex)
|
167
|
-
match[:job_url] if match
|
152
|
+
compare_stack_traces(cleaned_stack_trace_from_test(test), cleaned_stack_trace_from_issue(issue)) < max_diff_ratio
|
168
153
|
end
|
169
154
|
end
|
170
155
|
|
@@ -184,7 +169,11 @@ module GitlabQuality
|
|
184
169
|
end
|
185
170
|
|
186
171
|
def failure_issues(test)
|
187
|
-
|
172
|
+
find_issues_for_test(
|
173
|
+
test,
|
174
|
+
labels: base_issue_labels + Set.new(%w[test]),
|
175
|
+
not_labels: exclude_labels_for_search
|
176
|
+
)
|
188
177
|
end
|
189
178
|
|
190
179
|
def full_stacktrace(test)
|
@@ -205,7 +194,7 @@ module GitlabQuality
|
|
205
194
|
remove_unique_resource_names(relevant_issue_stacktrace)
|
206
195
|
end
|
207
196
|
|
208
|
-
def
|
197
|
+
def cleaned_stack_trace_from_test(test)
|
209
198
|
test_failure_stacktrace = sanitize_stacktrace(full_stacktrace(test),
|
210
199
|
FAILURE_STACKTRACE_REGEX) || full_stacktrace(test)
|
211
200
|
remove_unique_resource_names(test_failure_stacktrace)
|
@@ -221,7 +210,7 @@ module GitlabQuality
|
|
221
210
|
end
|
222
211
|
|
223
212
|
def find_relevant_failure_issues(test) # rubocop:disable Metrics/AbcSize
|
224
|
-
clean_first_test_failure_stacktrace =
|
213
|
+
clean_first_test_failure_stacktrace = cleaned_stack_trace_from_test(test)
|
225
214
|
# Search with the `search` param returns 500 errors, so we filter by `base_issue_labels` and then filter further in Ruby
|
226
215
|
failure_issues(test).each_with_object({}) do |issue, memo|
|
227
216
|
clean_relevant_issue_stacktrace = cleaned_stack_trace_from_issue(issue)
|
@@ -289,7 +278,7 @@ module GitlabQuality
|
|
289
278
|
"```\n#{full_stacktrace(test)}\n```",
|
290
279
|
screenshot_section(test),
|
291
280
|
system_log_errors_section(test),
|
292
|
-
|
281
|
+
initial_reports_section(test)
|
293
282
|
].compact.join("\n\n")
|
294
283
|
end
|
295
284
|
|
@@ -312,18 +301,6 @@ module GitlabQuality
|
|
312
301
|
section
|
313
302
|
end
|
314
303
|
|
315
|
-
def reports_section(test)
|
316
|
-
<<~REPORTS
|
317
|
-
### Reports (1)
|
318
|
-
|
319
|
-
#{report_list_item(test)}
|
320
|
-
REPORTS
|
321
|
-
end
|
322
|
-
|
323
|
-
def report_list_item(test)
|
324
|
-
"1. #{Time.new.utc.strftime('%F')}: #{test.ci_job_url} (#{ENV.fetch('CI_PIPELINE_URL', 'pipeline url is missing')})"
|
325
|
-
end
|
326
|
-
|
327
304
|
def up_to_date_labels(test:, issue: nil, new_labels: Set.new)
|
328
305
|
(Set.new(base_issue_labels) + (super << pipeline_name_label)).to_a
|
329
306
|
end
|
@@ -348,7 +325,7 @@ module GitlabQuality
|
|
348
325
|
state_event = issue.state == 'closed' ? 'reopen' : nil
|
349
326
|
|
350
327
|
issue_attrs = {
|
351
|
-
description:
|
328
|
+
description: add_report_to_issue_description(issue, test),
|
352
329
|
labels: up_to_date_labels(test: test, issue: issue)
|
353
330
|
}
|
354
331
|
issue_attrs[:state_event] = state_event if state_event
|
@@ -357,26 +334,6 @@ module GitlabQuality
|
|
357
334
|
puts " => Added a report in '#{issue.title}': #{issue.web_url}!"
|
358
335
|
end
|
359
336
|
|
360
|
-
def up_to_date_issue_description(issue_description, test)
|
361
|
-
# We include the number of reports in the header, for visibility.
|
362
|
-
new_issue_description =
|
363
|
-
if issue_description.include?('### Reports')
|
364
|
-
# We count the number of existing reports.
|
365
|
-
reports_count = issue_description
|
366
|
-
.scan(REPORT_ITEM_REGEX)
|
367
|
-
.size.to_i + 1
|
368
|
-
issue_description.sub(/^### Reports.*$/, "### Reports (#{reports_count})")
|
369
|
-
else # For issue with the legacy format, we add the Reports section
|
370
|
-
reports_count = issue_description
|
371
|
-
.scan(JOB_URL_REGEX)
|
372
|
-
.size.to_i + 1
|
373
|
-
|
374
|
-
"#{issue_description}\n\n### Reports (#{reports_count})"
|
375
|
-
end
|
376
|
-
|
377
|
-
"#{new_issue_description}\n#{report_list_item(test)}"
|
378
|
-
end
|
379
|
-
|
380
337
|
def new_issue_title(test)
|
381
338
|
"Failure in #{super}"
|
382
339
|
end
|
@@ -39,7 +39,10 @@ module GitlabQuality
|
|
39
39
|
end
|
40
40
|
|
41
41
|
def test_hash(test)
|
42
|
-
|
42
|
+
# Should not be more than 50 characters if we want it indexed.
|
43
|
+
#
|
44
|
+
# See https://gitlab.com/gitlab-org/ruby/gems/gitlab_quality-test_tooling/-/issues/27#note_1607276486
|
45
|
+
OpenSSL::Digest.hexdigest('SHA256', "#{test.file}#{test.name}")[..40]
|
43
46
|
end
|
44
47
|
|
45
48
|
def new_issue_description(test)
|
@@ -48,7 +51,8 @@ module GitlabQuality
|
|
48
51
|
|
49
52
|
| Field | Value |
|
50
53
|
| ------ | ------ |
|
51
|
-
| File | #{test.test_file_link} |
|
54
|
+
| File URL | #{test.test_file_link} |
|
55
|
+
| Filename | `#{test.file}` |
|
52
56
|
| Description | `#{test.name}` |
|
53
57
|
| Test level | #{test.level} |
|
54
58
|
| Hash | `#{test_hash(test)}` |
|
@@ -134,15 +138,21 @@ module GitlabQuality
|
|
134
138
|
labels
|
135
139
|
end
|
136
140
|
|
137
|
-
def
|
138
|
-
|
141
|
+
def find_issues_by_hash(test_hash)
|
142
|
+
gitlab.find_issues_by_hash(test_hash)
|
143
|
+
end
|
144
|
+
|
145
|
+
def find_issues_for_test(test, labels:, not_labels: Set.new, state: nil)
|
146
|
+
search_options = { labels: labels.to_a, not: { labels: not_labels.to_a } }
|
139
147
|
search_options[:state] = state if state
|
140
148
|
|
141
|
-
gitlab.find_issues(options: search_options).find_all
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
149
|
+
gitlab.find_issues(options: search_options).find_all { |issue| issue_match_test?(issue, test) }
|
150
|
+
end
|
151
|
+
|
152
|
+
def issue_match_test?(issue, test)
|
153
|
+
issue_title = issue.title.strip
|
154
|
+
test_file_path_found = !test.file.to_s.empty? && issue_title.include?(partial_file_path(test.file))
|
155
|
+
issue_title.include?(test.name) || test_file_path_found
|
146
156
|
end
|
147
157
|
|
148
158
|
def pipeline_name_label
|
@@ -68,7 +68,7 @@ module GitlabQuality
|
|
68
68
|
def create_slow_issue(test)
|
69
69
|
puts " => Finding existing issues for slow test '#{test.name}' (run time: #{test.run_time} seconds)..."
|
70
70
|
|
71
|
-
issues =
|
71
|
+
issues = find_issues_for_test(test, labels: SEARCH_LABELS, state: 'opened')
|
72
72
|
|
73
73
|
if issues.blank?
|
74
74
|
issues << create_issue(test)
|
@@ -18,7 +18,25 @@ module GitlabQuality
|
|
18
18
|
].freeze
|
19
19
|
|
20
20
|
def name
|
21
|
-
|
21
|
+
# If we see a string representation of an object in a test full_description, we discard it.
|
22
|
+
#
|
23
|
+
# This is to ensure that tests would have a reproducible name, in case they don't have a name.
|
24
|
+
#
|
25
|
+
# Test example:
|
26
|
+
#
|
27
|
+
# it { is_expected.to eq(secondary_node) }
|
28
|
+
#
|
29
|
+
# Would have its full_description as follows:
|
30
|
+
#
|
31
|
+
# Gitlab::Geo.proxied_site on a primary for a proxied request with a proxy extra data header
|
32
|
+
# for an existing site is expected to eq #<GeoNode id: 116, primary: false, oauth_application_id: 97
|
33
|
+
# , enabled: true, access_key: [FILTERED], e...pdated_at: "2023-10-10 08:49:49.797128469 +0000",
|
34
|
+
# sync_object_storage: true, secret_access_key: nil>
|
35
|
+
#
|
36
|
+
# Which would change for every test run due to the timestamps.
|
37
|
+
#
|
38
|
+
# See https://gitlab.com/gitlab-org/ruby/gems/gitlab_quality-test_tooling/-/merge_requests/77#note_1608793804
|
39
|
+
report.fetch('full_description').split('#<').first
|
22
40
|
end
|
23
41
|
|
24
42
|
def file
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: gitlab_quality-test_tooling
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.7.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- GitLab Quality
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-11-
|
11
|
+
date: 2023-11-29 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: climate_control
|
@@ -80,6 +80,20 @@ dependencies:
|
|
80
80
|
- - "~>"
|
81
81
|
- !ruby/object:Gem::Version
|
82
82
|
version: '1.3'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: pry-byebug
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - '='
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: 3.10.1
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - '='
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: 3.10.1
|
83
97
|
- !ruby/object:Gem::Dependency
|
84
98
|
name: rake
|
85
99
|
requirement: !ruby/object:Gem::Requirement
|
@@ -332,6 +346,7 @@ description: A collection of test-related tools.
|
|
332
346
|
email:
|
333
347
|
- quality@gitlab.com
|
334
348
|
executables:
|
349
|
+
- flaky-test-issues
|
335
350
|
- generate-test-session
|
336
351
|
- post-to-slack
|
337
352
|
- prepare-stage-reports
|
@@ -357,6 +372,7 @@ files:
|
|
357
372
|
- LICENSE.txt
|
358
373
|
- README.md
|
359
374
|
- Rakefile
|
375
|
+
- exe/flaky-test-issues
|
360
376
|
- exe/generate-test-session
|
361
377
|
- exe/post-to-slack
|
362
378
|
- exe/prepare-stage-reports
|
@@ -374,8 +390,10 @@ files:
|
|
374
390
|
- lib/gitlab_quality/test_tooling/labels_inference.rb
|
375
391
|
- lib/gitlab_quality/test_tooling/report/concerns/find_set_dri.rb
|
376
392
|
- lib/gitlab_quality/test_tooling/report/concerns/group_and_category_labels.rb
|
393
|
+
- lib/gitlab_quality/test_tooling/report/concerns/issue_reports.rb
|
377
394
|
- lib/gitlab_quality/test_tooling/report/concerns/results_reporter.rb
|
378
395
|
- lib/gitlab_quality/test_tooling/report/concerns/utils.rb
|
396
|
+
- lib/gitlab_quality/test_tooling/report/flaky_test_issue.rb
|
379
397
|
- lib/gitlab_quality/test_tooling/report/generate_test_session.rb
|
380
398
|
- lib/gitlab_quality/test_tooling/report/issue_logger.rb
|
381
399
|
- lib/gitlab_quality/test_tooling/report/merge_request_slow_tests_report.rb
|