gitlab_quality-test_tooling 1.5.4 → 1.8.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 +21 -0
- data/exe/flaky-test-issues +54 -0
- data/exe/relate-failure-issue +9 -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 +47 -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_metric/json_test_metric.rb +46 -0
- data/lib/gitlab_quality/test_tooling/test_metrics/json_test_metric_collection.rb +45 -0
- 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 +22 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4ee3407afd5274be75c1bb63c7f7aeeb0d2353cf62f0fb75c1393bd2103c8c1e
|
|
4
|
+
data.tar.gz: a4965dd9d979a58c4202c6f25315d60e08d597d51322db491a077a0ae797cf34
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1619807ec2456f02c9d4a1c09dfb0aeba598de7d51fe7a0377c1e5a9f3f9fc82cf607fa34387bb5ee1691ad28b5637e75c18c38ce068269c3bba76e3c902df22
|
|
7
|
+
data.tar.gz: '0940057d402dfb8fbcd3a6d2d09f7ff698e26d140ebdda659fcc5347445574fa7f398b2bd91e8a39c872b0f8a9f368fa1f734e59e10262790ed2d507fb97e514'
|
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.8.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
|
@@ -77,14 +77,20 @@ Usage: exe/prepare-stage-reports [options]
|
|
|
77
77
|
Purpose: Relate test failures to failure issues from RSpec report files (JSON or JUnit XML)
|
|
78
78
|
Usage: exe/relate-failure-issue [options]
|
|
79
79
|
-i, --input-files INPUT_FILES RSpec report files (JSON or JUnit XML)
|
|
80
|
+
-m METRICS_FILES, Test metrics files (JSON)
|
|
81
|
+
--metrics-files
|
|
80
82
|
--max-diff-ratio MAX_DIFF_RATO
|
|
81
83
|
Max stacktrace diff ratio for failure issues detection
|
|
82
84
|
-p, --project PROJECT Can be an integer or a group/project string
|
|
83
85
|
-t, --token TOKEN A valid access token with `api` scope and Maintainer permission in PROJECT
|
|
86
|
+
-r RELATED_ISSUES_FILE, The file path for the related issues
|
|
87
|
+
--related-issues-file
|
|
84
88
|
--system-log-files SYSTEM_LOG_FILES
|
|
85
89
|
Include errors from system logs in failure issues
|
|
86
90
|
--base-issue-labels BASE_ISSUE_LABELS
|
|
87
91
|
Labels to add to new failure issues
|
|
92
|
+
--exclude-labels-for-search EXCLUDE_LABELS_FOR_SEARCH
|
|
93
|
+
Labels to exclude when searching for existing issues
|
|
88
94
|
--confidential Makes created new issues confidential
|
|
89
95
|
--dry-run Perform a dry-run (don't create or update issues)
|
|
90
96
|
-v, --version Show the version
|
|
@@ -133,6 +139,21 @@ Usage: exe/slow-test-issue [options]
|
|
|
133
139
|
-h, --help Show the usage
|
|
134
140
|
```
|
|
135
141
|
|
|
142
|
+
### `exe/flaky-test-issues`
|
|
143
|
+
|
|
144
|
+
```shell
|
|
145
|
+
Purpose: Purpose: Create flaky test issues for any passed test coming from rspec-retry JSON report files
|
|
146
|
+
Usage: exe/flaky-test-issue [options]
|
|
147
|
+
-i, --input-files INPUT_FILES JSON rspec-retry report files
|
|
148
|
+
-p, --project PROJECT Can be an integer or a group/project string
|
|
149
|
+
-m MERGE_REQUEST_IID, An integer merge request IID
|
|
150
|
+
--merge_request_iid
|
|
151
|
+
-t, --token TOKEN A valid access token with `api` scope and Maintainer permission in PROJECT
|
|
152
|
+
--dry-run Perform a dry-run (don't create note)
|
|
153
|
+
-v, --version Show the version
|
|
154
|
+
-h, --help Show the usage
|
|
155
|
+
```
|
|
156
|
+
|
|
136
157
|
### `exe/slow-test-merge-request-report-note`
|
|
137
158
|
|
|
138
159
|
```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
|
@@ -15,6 +15,10 @@ options = OptionParser.new do |opts|
|
|
|
15
15
|
params[:input_files] = input_files
|
|
16
16
|
end
|
|
17
17
|
|
|
18
|
+
opts.on('-m', '--metrics-files METRICS_FILES', String, 'Test metrics files (JSON)') do |metrics_files|
|
|
19
|
+
params[:metrics_files] = metrics_files
|
|
20
|
+
end
|
|
21
|
+
|
|
18
22
|
opts.on('--max-diff-ratio MAX_DIFF_RATO', Float, 'Max stacktrace diff ratio for failure issues detection') do |max_diff_ratio|
|
|
19
23
|
params[:max_diff_ratio] = max_diff_ratio
|
|
20
24
|
end
|
|
@@ -41,6 +45,11 @@ options = OptionParser.new do |opts|
|
|
|
41
45
|
params[:base_issue_labels] = base_issue_labels.split(',')
|
|
42
46
|
end
|
|
43
47
|
|
|
48
|
+
opts.on('--exclude-labels-for-search EXCLUDE_LABELS_FOR_SEARCH', String,
|
|
49
|
+
'Labels to exclude when searching for existing issues') do |exclude_labels_for_search|
|
|
50
|
+
params[:exclude_labels_for_search] = exclude_labels_for_search.split(',')
|
|
51
|
+
end
|
|
52
|
+
|
|
44
53
|
opts.on('--confidential', "Makes created new issues confidential") do
|
|
45
54
|
params[:confidential] = true
|
|
46
55
|
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,26 @@ 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
|
+
metrics_files: [],
|
|
45
|
+
**kwargs)
|
|
41
46
|
super
|
|
42
47
|
@max_diff_ratio = max_diff_ratio.to_f
|
|
43
48
|
@system_logs = Dir.glob(system_logs)
|
|
44
49
|
@base_issue_labels = Set.new(base_issue_labels)
|
|
50
|
+
@exclude_labels_for_search = Set.new(exclude_labels_for_search)
|
|
45
51
|
@issue_type = 'issue'
|
|
46
52
|
@commented_issue_list = Set.new
|
|
53
|
+
@metrics_files = Array(metrics_files)
|
|
47
54
|
end
|
|
48
55
|
|
|
49
56
|
private
|
|
50
57
|
|
|
51
|
-
attr_reader :max_diff_ratio, :system_logs, :base_issue_labels
|
|
58
|
+
attr_reader :max_diff_ratio, :system_logs, :base_issue_labels, :exclude_labels_for_search, :metrics_files
|
|
52
59
|
|
|
53
60
|
def run!
|
|
54
61
|
puts "Reporting test failures in `#{files.join(',')}` as issues in project `#{project}` via the API at `#{Runtime::Env.gitlab_api_base}`."
|
|
@@ -61,16 +68,39 @@ module GitlabQuality
|
|
|
61
68
|
write_issues_log_file
|
|
62
69
|
end
|
|
63
70
|
|
|
71
|
+
def test_metric_collections
|
|
72
|
+
@test_metric_collections ||= Dir.glob(metrics_files).map do |path|
|
|
73
|
+
TestMetrics::JsonTestMetricCollection.new(path)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
64
77
|
def process_test_results(test_results)
|
|
65
78
|
systemic_failures = systemic_failures_for_test_results(test_results)
|
|
66
79
|
|
|
67
80
|
test_results.each do |test|
|
|
68
81
|
collect_issues(test, relate_failure_to_issue(test)) if should_report?(test, systemic_failures)
|
|
82
|
+
|
|
83
|
+
copy_failure_issue_to_test_metrics(test) if metrics_files.any?
|
|
69
84
|
end
|
|
70
85
|
|
|
71
86
|
test_results.write
|
|
72
87
|
end
|
|
73
88
|
|
|
89
|
+
def copy_failure_issue_to_test_metrics(test)
|
|
90
|
+
failure_issue = test.failure_issue
|
|
91
|
+
|
|
92
|
+
return unless failure_issue
|
|
93
|
+
|
|
94
|
+
test_metric_collections.find do |test_metric_collection|
|
|
95
|
+
test_metric = test_metric_collection.metric_for_test_id(test.example_id)
|
|
96
|
+
|
|
97
|
+
if test_metric
|
|
98
|
+
test_metric.fields['failure_issue'] = failure_issue
|
|
99
|
+
test_metric_collection.write
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
74
104
|
def systemic_failures_for_test_results(test_results)
|
|
75
105
|
test_results
|
|
76
106
|
.flat_map { |test| test.failures.map { |failure| failure['message'].lines.first.chomp } }
|
|
@@ -136,35 +166,15 @@ module GitlabQuality
|
|
|
136
166
|
|
|
137
167
|
def pipeline_issues_with_similar_stacktrace(test)
|
|
138
168
|
search_labels = (base_issue_labels + Set.new(%w[test failure::new])).to_a
|
|
169
|
+
not_labels = exclude_labels_for_search.to_a
|
|
139
170
|
gitlab.find_issues(options: { labels: search_labels,
|
|
171
|
+
not: { labels: not_labels },
|
|
140
172
|
created_after: past_timestamp(2) }).select do |issue|
|
|
141
173
|
job_url_from_issue = failed_issue_job_url(issue)
|
|
142
174
|
|
|
143
175
|
next if pipeline != pipeline_env_from_job_url(job_url_from_issue)
|
|
144
176
|
|
|
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
|
|
177
|
+
compare_stack_traces(cleaned_stack_trace_from_test(test), cleaned_stack_trace_from_issue(issue)) < max_diff_ratio
|
|
168
178
|
end
|
|
169
179
|
end
|
|
170
180
|
|
|
@@ -184,7 +194,11 @@ module GitlabQuality
|
|
|
184
194
|
end
|
|
185
195
|
|
|
186
196
|
def failure_issues(test)
|
|
187
|
-
|
|
197
|
+
find_issues_for_test(
|
|
198
|
+
test,
|
|
199
|
+
labels: base_issue_labels + Set.new(%w[test]),
|
|
200
|
+
not_labels: exclude_labels_for_search
|
|
201
|
+
)
|
|
188
202
|
end
|
|
189
203
|
|
|
190
204
|
def full_stacktrace(test)
|
|
@@ -205,7 +219,7 @@ module GitlabQuality
|
|
|
205
219
|
remove_unique_resource_names(relevant_issue_stacktrace)
|
|
206
220
|
end
|
|
207
221
|
|
|
208
|
-
def
|
|
222
|
+
def cleaned_stack_trace_from_test(test)
|
|
209
223
|
test_failure_stacktrace = sanitize_stacktrace(full_stacktrace(test),
|
|
210
224
|
FAILURE_STACKTRACE_REGEX) || full_stacktrace(test)
|
|
211
225
|
remove_unique_resource_names(test_failure_stacktrace)
|
|
@@ -221,7 +235,7 @@ module GitlabQuality
|
|
|
221
235
|
end
|
|
222
236
|
|
|
223
237
|
def find_relevant_failure_issues(test) # rubocop:disable Metrics/AbcSize
|
|
224
|
-
clean_first_test_failure_stacktrace =
|
|
238
|
+
clean_first_test_failure_stacktrace = cleaned_stack_trace_from_test(test)
|
|
225
239
|
# Search with the `search` param returns 500 errors, so we filter by `base_issue_labels` and then filter further in Ruby
|
|
226
240
|
failure_issues(test).each_with_object({}) do |issue, memo|
|
|
227
241
|
clean_relevant_issue_stacktrace = cleaned_stack_trace_from_issue(issue)
|
|
@@ -289,7 +303,7 @@ module GitlabQuality
|
|
|
289
303
|
"```\n#{full_stacktrace(test)}\n```",
|
|
290
304
|
screenshot_section(test),
|
|
291
305
|
system_log_errors_section(test),
|
|
292
|
-
|
|
306
|
+
initial_reports_section(test)
|
|
293
307
|
].compact.join("\n\n")
|
|
294
308
|
end
|
|
295
309
|
|
|
@@ -312,18 +326,6 @@ module GitlabQuality
|
|
|
312
326
|
section
|
|
313
327
|
end
|
|
314
328
|
|
|
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
329
|
def up_to_date_labels(test:, issue: nil, new_labels: Set.new)
|
|
328
330
|
(Set.new(base_issue_labels) + (super << pipeline_name_label)).to_a
|
|
329
331
|
end
|
|
@@ -348,7 +350,7 @@ module GitlabQuality
|
|
|
348
350
|
state_event = issue.state == 'closed' ? 'reopen' : nil
|
|
349
351
|
|
|
350
352
|
issue_attrs = {
|
|
351
|
-
description:
|
|
353
|
+
description: add_report_to_issue_description(issue, test),
|
|
352
354
|
labels: up_to_date_labels(test: test, issue: issue)
|
|
353
355
|
}
|
|
354
356
|
issue_attrs[:state_event] = state_event if state_event
|
|
@@ -357,26 +359,6 @@ module GitlabQuality
|
|
|
357
359
|
puts " => Added a report in '#{issue.title}': #{issue.web_url}!"
|
|
358
360
|
end
|
|
359
361
|
|
|
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
362
|
def new_issue_title(test)
|
|
381
363
|
"Failure in #{super}"
|
|
382
364
|
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)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GitlabQuality
|
|
4
|
+
module TestTooling
|
|
5
|
+
module TestMetric
|
|
6
|
+
class JsonTestMetric
|
|
7
|
+
attr_reader :metric
|
|
8
|
+
|
|
9
|
+
def initialize(metric)
|
|
10
|
+
@metric = metric
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def name
|
|
14
|
+
metric.fetch('name')
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def time
|
|
18
|
+
metric.fetch('time')
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def tags
|
|
22
|
+
@tags ||= metric.fetch('tags')
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def fields
|
|
26
|
+
@fields ||= metric.fetch('fields')
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def to_json(*options)
|
|
30
|
+
as_json.to_json(*options)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def as_json
|
|
36
|
+
{
|
|
37
|
+
name: name,
|
|
38
|
+
time: time,
|
|
39
|
+
tags: tags,
|
|
40
|
+
fields: fields
|
|
41
|
+
}
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
|
|
5
|
+
module GitlabQuality
|
|
6
|
+
module TestTooling
|
|
7
|
+
module TestMetrics
|
|
8
|
+
class JsonTestMetricCollection
|
|
9
|
+
include Enumerable
|
|
10
|
+
|
|
11
|
+
attr_reader :path, :metrics
|
|
12
|
+
|
|
13
|
+
def initialize(path)
|
|
14
|
+
@path = path
|
|
15
|
+
@metrics = process
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def metric_for_test_id(test_id)
|
|
19
|
+
metrics.find do |metric|
|
|
20
|
+
metric.fields['id'] == test_id
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def write
|
|
25
|
+
File.write(path, JSON.pretty_generate(metrics))
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def parse
|
|
31
|
+
JSON.parse(File.read(path))
|
|
32
|
+
rescue JSON::ParserError
|
|
33
|
+
Runtime::Logger.debug("#{self.class.name}##{__method__} attempted to parse invalid JSON at path: #{path}")
|
|
34
|
+
{}
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def process
|
|
38
|
+
parse.map do |test|
|
|
39
|
+
GitlabQuality::TestTooling::TestMetric::JsonTestMetric.new(test)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -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.8.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-30 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
|
|
@@ -404,6 +422,8 @@ files:
|
|
|
404
422
|
- lib/gitlab_quality/test_tooling/system_logs/log_types/rails/graphql_log.rb
|
|
405
423
|
- lib/gitlab_quality/test_tooling/system_logs/shared_fields.rb
|
|
406
424
|
- lib/gitlab_quality/test_tooling/system_logs/system_logs_formatter.rb
|
|
425
|
+
- lib/gitlab_quality/test_tooling/test_metric/json_test_metric.rb
|
|
426
|
+
- lib/gitlab_quality/test_tooling/test_metrics/json_test_metric_collection.rb
|
|
407
427
|
- lib/gitlab_quality/test_tooling/test_result/base_test_result.rb
|
|
408
428
|
- lib/gitlab_quality/test_tooling/test_result/j_unit_test_result.rb
|
|
409
429
|
- lib/gitlab_quality/test_tooling/test_result/json_test_result.rb
|