gitlab_quality-test_tooling 1.21.1 → 1.23.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 +2 -2
- data/README.md +21 -0
- data/exe/failed-test-issues +68 -0
- data/lib/gitlab_quality/test_tooling/gitlab_client/issues_client.rb +12 -2
- data/lib/gitlab_quality/test_tooling/gitlab_client/issues_dry_client.rb +6 -2
- data/lib/gitlab_quality/test_tooling/report/concerns/issue_reports.rb +55 -37
- data/lib/gitlab_quality/test_tooling/report/failed_test_issue.rb +241 -0
- data/lib/gitlab_quality/test_tooling/report/flaky_test_issue.rb +26 -11
- data/lib/gitlab_quality/test_tooling/report/relate_failure_issue.rb +15 -38
- data/lib/gitlab_quality/test_tooling/report/report_as_issue.rb +3 -6
- data/lib/gitlab_quality/test_tooling/report/results_in_issues.rb +1 -1
- data/lib/gitlab_quality/test_tooling/report/slow_test_issue.rb +8 -6
- data/lib/gitlab_quality/test_tooling/stack_trace_comparator.rb +36 -0
- data/lib/gitlab_quality/test_tooling/test_result/base_test_result.rb +16 -0
- data/lib/gitlab_quality/test_tooling/version.rb +1 -1
- metadata +20 -16
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c63a7a25d0e3a05f515954353898bb8d0dcb9e7a90634087e738f5cb16174c57
|
4
|
+
data.tar.gz: 386833833dc0fd5cdc6334d3d613a38544fbc885c5ccb396070614f0e889555f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: bb13a1c06a626b1152115262e0606480e78eb9aaafd6ed5e673cd72d6b1a268ab7659c020dbab743ebb872e71c1332905319d0ed6970cbde8bf0168c208fa2c3
|
7
|
+
data.tar.gz: 40652c63561438f37f5610e5b22f93c6a0404c53d4dc65b17713bbf5469bf769dd13691361f02f5aa8229aab706f7e349db6abf663cf45745aaa040456461db7
|
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.23.0)
|
5
5
|
activesupport (>= 6.1, < 7.2)
|
6
6
|
amatch (~> 0.4.1)
|
7
7
|
gitlab (~> 4.19)
|
@@ -328,4 +328,4 @@ DEPENDENCIES
|
|
328
328
|
webmock (= 3.7.0)
|
329
329
|
|
330
330
|
BUNDLED WITH
|
331
|
-
2.5.
|
331
|
+
2.5.4
|
data/README.md
CHANGED
@@ -156,6 +156,27 @@ Usage: exe/knapsack-report-issues [options]
|
|
156
156
|
-h, --help Show the usage
|
157
157
|
```
|
158
158
|
|
159
|
+
### `exe/failed-test-issues`
|
160
|
+
|
161
|
+
```shell
|
162
|
+
Purpose: Relate test failures to failure issues from RSpec report files (JSON or JUnit XML)
|
163
|
+
Usage: exe/failed-test-issues [options]
|
164
|
+
-i, --input-files INPUT_FILES RSpec report files (JSON or JUnit XML)
|
165
|
+
-p, --project PROJECT Can be an integer or a group/project string
|
166
|
+
-t, --token TOKEN A valid access token with `api` scope and Maintainer permission in PROJECT
|
167
|
+
--max-diff-ratio MAX_DIFF_RATO
|
168
|
+
Max stacktrace diff ratio for failure issues detection
|
169
|
+
-r RELATED_ISSUES_FILE, The file path for the related issues
|
170
|
+
--related-issues-file
|
171
|
+
--base-issue-labels BASE_ISSUE_LABELS
|
172
|
+
Labels to add to new failure issues
|
173
|
+
--exclude-labels-for-search EXCLUDE_LABELS_FOR_SEARCH
|
174
|
+
Labels to exclude when searching for existing issues
|
175
|
+
--dry-run Perform a dry-run (don't create or update issues)
|
176
|
+
-v, --version Show the version
|
177
|
+
-h, --help Show the usage
|
178
|
+
```
|
179
|
+
|
159
180
|
### `exe/flaky-test-issues`
|
160
181
|
|
161
182
|
```shell
|
@@ -0,0 +1,68 @@
|
|
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, 'RSpec report files (JSON or JUnit XML)') 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('-t', '--token TOKEN', String, 'A valid access token with `api` scope and Maintainer permission in PROJECT') do |token|
|
23
|
+
params[:token] = token
|
24
|
+
end
|
25
|
+
|
26
|
+
opts.on('--max-diff-ratio MAX_DIFF_RATO', Float, 'Max stacktrace diff ratio for failure issues detection') do |max_diff_ratio|
|
27
|
+
params[:max_diff_ratio] = max_diff_ratio
|
28
|
+
end
|
29
|
+
|
30
|
+
opts.on('-r', '--related-issues-file RELATED_ISSUES_FILE', String, 'The file path for the related issues') do |related_issues_file|
|
31
|
+
params[:related_issues_file] = related_issues_file
|
32
|
+
end
|
33
|
+
|
34
|
+
opts.on('--base-issue-labels BASE_ISSUE_LABELS', String,
|
35
|
+
'Labels to add to new failure issues') do |base_issue_labels|
|
36
|
+
params[:base_issue_labels] = base_issue_labels.split(',')
|
37
|
+
end
|
38
|
+
|
39
|
+
opts.on('--exclude-labels-for-search EXCLUDE_LABELS_FOR_SEARCH', String,
|
40
|
+
'Labels to exclude when searching for existing issues') do |exclude_labels_for_search|
|
41
|
+
params[:exclude_labels_for_search] = exclude_labels_for_search.split(',')
|
42
|
+
end
|
43
|
+
|
44
|
+
opts.on('--dry-run', "Perform a dry-run (don't create or update issues)") do
|
45
|
+
params[:dry_run] = true
|
46
|
+
end
|
47
|
+
|
48
|
+
opts.on_tail('-v', '--version', 'Show the version') do
|
49
|
+
require_relative "../lib/gitlab_quality/test_tooling/version"
|
50
|
+
puts "#{$PROGRAM_NAME} : #{GitlabQuality::TestTooling::VERSION}"
|
51
|
+
exit
|
52
|
+
end
|
53
|
+
|
54
|
+
opts.on_tail('-h', '--help', 'Show the usage') do
|
55
|
+
puts "Purpose: Relate test failures to failure issues from RSpec report files (JSON or JUnit XML)"
|
56
|
+
puts opts
|
57
|
+
exit
|
58
|
+
end
|
59
|
+
|
60
|
+
opts.parse(ARGV)
|
61
|
+
end
|
62
|
+
|
63
|
+
if params.any?
|
64
|
+
GitlabQuality::TestTooling::Report::FailedTestIssue.new(**params).invoke!
|
65
|
+
else
|
66
|
+
puts options
|
67
|
+
exit 1
|
68
|
+
end
|
@@ -13,6 +13,10 @@ module Gitlab
|
|
13
13
|
get("/projects/#{url_encode(project)}/issues/#{issue_id}/discussions", query: options)
|
14
14
|
end
|
15
15
|
|
16
|
+
def create_issue_discussion(project, issue_iid, options = {})
|
17
|
+
post("/projects/#{url_encode(project)}/issues/#{issue_iid}/discussions", query: options)
|
18
|
+
end
|
19
|
+
|
16
20
|
def add_note_to_issue_discussion_as_thread(project, issue_id, discussion_id, options = {})
|
17
21
|
post("/projects/#{url_encode(project)}/issues/#{issue_id}/discussions/#{discussion_id}/notes", query: options)
|
18
22
|
end
|
@@ -93,9 +97,15 @@ module GitlabQuality
|
|
93
97
|
end
|
94
98
|
end
|
95
99
|
|
96
|
-
def
|
100
|
+
def create_issue_discussion(iid:, note:)
|
101
|
+
handle_gitlab_client_exceptions do
|
102
|
+
client.create_issue_discussion(project, iid, body: note)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def add_note_to_issue_discussion_as_thread(iid:, discussion_id:, note:)
|
97
107
|
handle_gitlab_client_exceptions do
|
98
|
-
client.add_note_to_issue_discussion_as_thread(project, iid, discussion_id, body:
|
108
|
+
client.add_note_to_issue_discussion_as_thread(project, iid, discussion_id, body: note)
|
99
109
|
end
|
100
110
|
end
|
101
111
|
|
@@ -23,8 +23,12 @@ module GitlabQuality
|
|
23
23
|
puts "The following note would have been edited on #{project}##{issue_iid} (note #{note_id}) issue: #{note}"
|
24
24
|
end
|
25
25
|
|
26
|
-
def
|
27
|
-
puts "The following discussion
|
26
|
+
def create_issue_discussion(iid:, note:)
|
27
|
+
puts "The following discussion would have been posted on #{project}##{iid} issue: #{note}"
|
28
|
+
end
|
29
|
+
|
30
|
+
def add_note_to_issue_discussion_as_thread(iid:, discussion_id:, note:)
|
31
|
+
puts "The following discussion note would have been posted on #{project}##{iid} (discussion #{discussion_id}) issue: #{note}"
|
28
32
|
end
|
29
33
|
|
30
34
|
def upload_file(file_fullpath:)
|
@@ -12,11 +12,57 @@ module GitlabQuality
|
|
12
12
|
REPORT_ITEM_REGEX = /^1\. \d{4}-\d{2}-\d{2}: #{JOB_URL_REGEX} \((?<pipeline_url>\S+)\)/
|
13
13
|
LATEST_REPORTS_TO_SHOW = 10
|
14
14
|
|
15
|
+
class ReportsList
|
16
|
+
def initialize(preserved_content:, section_header:, reports:, extra_content:)
|
17
|
+
@preserved_content = preserved_content
|
18
|
+
@section_header = section_header
|
19
|
+
@reports = reports
|
20
|
+
@extra_content = extra_content
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.report_list_item(test, item_extra_content: nil)
|
24
|
+
"1. #{Time.new.utc.strftime('%F')}: #{test.ci_job_url} (#{ENV.fetch('CI_PIPELINE_URL', 'pipeline url is missing')}) #{item_extra_content}".strip
|
25
|
+
end
|
26
|
+
|
27
|
+
def reports_count
|
28
|
+
reports.size
|
29
|
+
end
|
30
|
+
|
31
|
+
def to_s
|
32
|
+
[
|
33
|
+
preserved_content,
|
34
|
+
"#{section_header} (#{reports_count})",
|
35
|
+
reports_list(reports),
|
36
|
+
extra_content
|
37
|
+
].reject(&:blank?).compact.join("\n\n")
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
attr_reader :preserved_content, :section_header, :reports, :extra_content
|
43
|
+
|
44
|
+
def reports_list(reports)
|
45
|
+
sorted_reports = reports.sort.reverse
|
46
|
+
|
47
|
+
if sorted_reports.size > LATEST_REPORTS_TO_SHOW
|
48
|
+
[
|
49
|
+
"Last 10 reports:",
|
50
|
+
sorted_reports[...LATEST_REPORTS_TO_SHOW].join("\n"),
|
51
|
+
"<details><summary>See #{sorted_reports.size - LATEST_REPORTS_TO_SHOW} more reports</summary>",
|
52
|
+
sorted_reports[LATEST_REPORTS_TO_SHOW..].join("\n"),
|
53
|
+
"</details>"
|
54
|
+
].join("\n\n")
|
55
|
+
else
|
56
|
+
sorted_reports.join("\n")
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
15
61
|
def initial_reports_section(test)
|
16
62
|
<<~REPORTS
|
17
63
|
### Reports (1)
|
18
64
|
|
19
|
-
#{report_list_item(test)}
|
65
|
+
#{ReportsList.report_list_item(test)}
|
20
66
|
REPORTS
|
21
67
|
end
|
22
68
|
|
@@ -27,14 +73,14 @@ module GitlabQuality
|
|
27
73
|
item_extra_content: nil,
|
28
74
|
reports_extra_content: nil)
|
29
75
|
preserved_content = current_reports_content.split(reports_section_header).first&.strip
|
30
|
-
reports = report_lines(current_reports_content) + [report_list_item(test, item_extra_content: item_extra_content)]
|
31
|
-
|
32
|
-
|
33
|
-
preserved_content,
|
34
|
-
|
35
|
-
|
36
|
-
reports_extra_content
|
37
|
-
|
76
|
+
reports = report_lines(current_reports_content) + [ReportsList.report_list_item(test, item_extra_content: item_extra_content)]
|
77
|
+
|
78
|
+
ReportsList.new(
|
79
|
+
preserved_content: preserved_content,
|
80
|
+
section_header: reports_section_header,
|
81
|
+
reports: reports,
|
82
|
+
extra_content: reports_extra_content
|
83
|
+
)
|
38
84
|
end
|
39
85
|
|
40
86
|
def failed_issue_job_url(issue)
|
@@ -55,40 +101,12 @@ module GitlabQuality
|
|
55
101
|
content.lines.grep(REPORT_ITEM_REGEX).map(&:strip)
|
56
102
|
end
|
57
103
|
|
58
|
-
def reports_list(reports)
|
59
|
-
sorted_reports = reports.sort.reverse
|
60
|
-
|
61
|
-
if sorted_reports.size > LATEST_REPORTS_TO_SHOW
|
62
|
-
[
|
63
|
-
"Last 10 reports:",
|
64
|
-
sorted_reports[...LATEST_REPORTS_TO_SHOW].join("\n"),
|
65
|
-
"<details><summary>See #{sorted_reports.size - LATEST_REPORTS_TO_SHOW} more reports</summary>",
|
66
|
-
sorted_reports[LATEST_REPORTS_TO_SHOW..].join("\n"),
|
67
|
-
"</details>"
|
68
|
-
].join("\n\n")
|
69
|
-
else
|
70
|
-
sorted_reports.join("\n")
|
71
|
-
end
|
72
|
-
end
|
73
|
-
|
74
|
-
def report_list_item(test, item_extra_content: nil)
|
75
|
-
"1. #{Time.new.utc.strftime('%F')}: #{test.ci_job_url} (#{ENV.fetch('CI_PIPELINE_URL', 'pipeline url is missing')}) #{item_extra_content}".strip
|
76
|
-
end
|
77
|
-
|
78
104
|
def job_urls_from_description(issue_description, regex)
|
79
105
|
issue_description.lines.filter_map do |line|
|
80
106
|
match = line.match(regex)
|
81
107
|
match[:job_url] if match
|
82
108
|
end
|
83
109
|
end
|
84
|
-
|
85
|
-
def test_captures_to_report_items(test_captures)
|
86
|
-
test_captures.map do |ci_job_url, _, _|
|
87
|
-
report_list_item(GitlabQuality::TestTooling::TestResult::JsonTestResult.new(
|
88
|
-
'ci_job_url' => ci_job_url
|
89
|
-
))
|
90
|
-
end
|
91
|
-
end
|
92
110
|
end
|
93
111
|
end
|
94
112
|
end
|
@@ -0,0 +1,241 @@
|
|
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 failed test coming from JSON test reports.
|
7
|
+
#
|
8
|
+
# - Takes the JSON test reports like rspec-*.json
|
9
|
+
# - Takes a project where failed test issues should be created
|
10
|
+
# - For every passed test in the report:
|
11
|
+
# - Find issue by test hash or create a new issue if no issue was found
|
12
|
+
# - Add a failure report in the "Failure reports" note
|
13
|
+
class FailedTestIssue < ReportAsIssue
|
14
|
+
include Concerns::GroupAndCategoryLabels
|
15
|
+
include Concerns::IssueReports
|
16
|
+
|
17
|
+
IDENTITY_LABELS = ['test', 'automation:bot-authored'].freeze
|
18
|
+
NEW_ISSUE_LABELS = Set.new(['type::maintenance', 'failure::new', 'priority::3', 'severity::3', *IDENTITY_LABELS]).freeze
|
19
|
+
SEARCH_LABELS = ['test'].freeze
|
20
|
+
FOUND_IN_MR_LABEL = '~"found:in MR"'
|
21
|
+
FOUND_IN_MASTER_LABEL = '~"found:master"'
|
22
|
+
REPORTS_DISCUSSION_HEADER = '### Failure reports'
|
23
|
+
REPORT_SECTION_HEADER = '#### Failure reports'
|
24
|
+
|
25
|
+
FAILURE_STACKTRACE_REGEX = %r{(?:(?:.*Failure/Error:(?<stacktrace>.+))|(?<stacktrace>.+))}m
|
26
|
+
ISSUE_STACKTRACE_REGEX = /##### Stack trace\s*(```)#{FAILURE_STACKTRACE_REGEX}(```)\n*/m
|
27
|
+
DEFAULT_MAX_DIFF_RATIO_FOR_DETECTION = 0.15
|
28
|
+
|
29
|
+
MultipleNotesFound = Class.new(StandardError)
|
30
|
+
|
31
|
+
def initialize(
|
32
|
+
token:,
|
33
|
+
input_files:,
|
34
|
+
base_issue_labels: nil,
|
35
|
+
dry_run: false,
|
36
|
+
project: nil,
|
37
|
+
max_diff_ratio: DEFAULT_MAX_DIFF_RATIO_FOR_DETECTION,
|
38
|
+
**_kwargs)
|
39
|
+
super(token: token, input_files: input_files, project: project, dry_run: dry_run)
|
40
|
+
|
41
|
+
@base_issue_labels = Set.new(base_issue_labels)
|
42
|
+
@max_diff_ratio = max_diff_ratio.to_f
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
attr_reader :base_issue_labels, :max_diff_ratio
|
48
|
+
|
49
|
+
def run!
|
50
|
+
puts "Reporting failed tests in `#{files.join(',')}` as issues in project `#{project}` via the API at `#{Runtime::Env.gitlab_api_base}`."
|
51
|
+
|
52
|
+
TestResults::Builder.new(files).test_results_per_file do |test_results|
|
53
|
+
puts "=> Reporting #{test_results.count} tests in #{test_results.path}"
|
54
|
+
|
55
|
+
process_test_results(test_results)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def process_test_results(test_results)
|
60
|
+
test_results.each do |test|
|
61
|
+
next unless test_is_applicable?(test)
|
62
|
+
|
63
|
+
puts " => Reporting failure for test '#{test.name}'..."
|
64
|
+
|
65
|
+
issues = find_issues_by_hash(test_hash(test), state: 'opened', labels: SEARCH_LABELS)
|
66
|
+
issues << create_issue(test) if issues.empty?
|
67
|
+
|
68
|
+
update_reports(issues, test)
|
69
|
+
collect_issues(test, issues)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def test_is_applicable?(test)
|
74
|
+
test.status == 'failed'
|
75
|
+
end
|
76
|
+
|
77
|
+
def up_to_date_labels(test:, issue: nil, new_labels: Set.new)
|
78
|
+
(base_issue_labels + super).to_a
|
79
|
+
end
|
80
|
+
|
81
|
+
def update_reports(issues, test)
|
82
|
+
issues.each do |issue|
|
83
|
+
puts " => Adding the failed test to the existing issue: #{issue.web_url}"
|
84
|
+
add_report_to_issue(issue: issue, test: test, related_issues: (issues - [issue]))
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def add_report_to_issue(issue:, test:, related_issues:) # rubocop:disable Metrics/AbcSize:
|
89
|
+
reports_discussion = find_or_create_reports_discussion(issue: issue)
|
90
|
+
current_reports_note = find_failure_discussion_note(issue: issue, test: test, reports_discussion: reports_discussion)
|
91
|
+
new_reports_list = add_report_for_test(current_reports_content: current_reports_note&.body.to_s, test: test)
|
92
|
+
|
93
|
+
note_body = [
|
94
|
+
new_reports_list.to_s,
|
95
|
+
identity_labels_quick_action,
|
96
|
+
relate_issues_quick_actions(related_issues)
|
97
|
+
].join("\n")
|
98
|
+
|
99
|
+
if current_reports_note
|
100
|
+
gitlab.edit_issue_note(
|
101
|
+
issue_iid: issue.iid,
|
102
|
+
note_id: current_reports_note.id,
|
103
|
+
note: note_body
|
104
|
+
)
|
105
|
+
else
|
106
|
+
gitlab.add_note_to_issue_discussion_as_thread(
|
107
|
+
iid: issue.iid,
|
108
|
+
discussion_id: reports_discussion.id,
|
109
|
+
note: note_body
|
110
|
+
)
|
111
|
+
end
|
112
|
+
rescue MultipleNotesFound => e
|
113
|
+
warn(e.message)
|
114
|
+
end
|
115
|
+
|
116
|
+
def find_or_create_reports_discussion(issue:)
|
117
|
+
reports_discussion = existing_reports_discussion(issue: issue)
|
118
|
+
return reports_discussion if reports_discussion
|
119
|
+
|
120
|
+
gitlab.create_issue_discussion(iid: issue.iid, note: REPORTS_DISCUSSION_HEADER)
|
121
|
+
end
|
122
|
+
|
123
|
+
def existing_reports_discussion(issue:)
|
124
|
+
gitlab.find_issue_discussions(iid: issue.iid).find do |discussion|
|
125
|
+
next if discussion.individual_note
|
126
|
+
next unless discussion.notes.first
|
127
|
+
|
128
|
+
discussion.notes.first.body.start_with?(REPORTS_DISCUSSION_HEADER)
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
def find_failure_discussion_note(issue:, test:, reports_discussion:)
|
133
|
+
return unless reports_discussion
|
134
|
+
|
135
|
+
relevant_notes = find_relevant_failure_discussion_note(issue: issue, test: test, reports_discussion: reports_discussion)
|
136
|
+
return if relevant_notes.empty?
|
137
|
+
|
138
|
+
best_matching_note, smaller_diff_ratio = relevant_notes.min_by { |_, diff_ratio| diff_ratio }
|
139
|
+
|
140
|
+
raise(MultipleNotesFound, %(Too many issues found for test '#{test.name}' (`#{test.file}`)!)) unless relevant_notes.values.count(smaller_diff_ratio) == 1
|
141
|
+
|
142
|
+
# Re-instantiate a `Gitlab::ObjectifiedHash` object after having converted it to a hash in #find_relevant_failure_issues above.
|
143
|
+
best_matching_note = Gitlab::ObjectifiedHash.new(best_matching_note)
|
144
|
+
|
145
|
+
test.failure_issue ||= "#{issue.web_url}#note_#{best_matching_note.id}"
|
146
|
+
|
147
|
+
best_matching_note
|
148
|
+
end
|
149
|
+
|
150
|
+
def find_relevant_failure_discussion_note(issue:, test:, reports_discussion:)
|
151
|
+
return [] unless reports_discussion.notes.size > 1
|
152
|
+
|
153
|
+
clean_test_stacktrace = cleaned_stack_trace_from_test(test: test)
|
154
|
+
|
155
|
+
# We're skipping the first note of the discussion as this is the "non-collapsible note", aka
|
156
|
+
# the "header note", which doesn't contain any stack trace.
|
157
|
+
reports_discussion.notes[1..].each_with_object({}) do |note, memo|
|
158
|
+
clean_note_stacktrace = cleaned_stack_trace_from_note(issue: issue, note: note)
|
159
|
+
diff_ratio = diff_ratio_between_test_and_note_stacktraces(
|
160
|
+
issue: issue,
|
161
|
+
note: note,
|
162
|
+
test_stacktrace: clean_test_stacktrace,
|
163
|
+
note_stacktrace: clean_note_stacktrace)
|
164
|
+
|
165
|
+
memo[note.to_h] = diff_ratio if diff_ratio
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
def cleaned_stack_trace_from_test(test:)
|
170
|
+
sanitize_stacktrace(stacktrace: test.full_stacktrace, regex: FAILURE_STACKTRACE_REGEX) || test.full_stacktrace
|
171
|
+
end
|
172
|
+
|
173
|
+
def cleaned_stack_trace_from_note(issue:, note:)
|
174
|
+
note_stacktrace = sanitize_stacktrace(stacktrace: note.body, regex: ISSUE_STACKTRACE_REGEX)
|
175
|
+
return note_stacktrace if note_stacktrace
|
176
|
+
|
177
|
+
puts " => [DEBUG] Stacktrace couldn't be found for #{issue.web_url}#note_#{note.id}!"
|
178
|
+
end
|
179
|
+
|
180
|
+
def sanitize_stacktrace(stacktrace:, regex:)
|
181
|
+
stacktrace_match = stacktrace.match(regex)
|
182
|
+
|
183
|
+
if stacktrace_match
|
184
|
+
stacktrace_match[:stacktrace].gsub(/^\s*#.*$/, '').gsub(/^[[:space:]]+/, '').strip
|
185
|
+
else
|
186
|
+
puts " => [DEBUG] Stacktrace doesn't match the regex (#{regex})!"
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
def diff_ratio_between_test_and_note_stacktraces(issue:, note:, test_stacktrace:, note_stacktrace:)
|
191
|
+
return if note_stacktrace.nil?
|
192
|
+
|
193
|
+
stack_trace_comparator = StackTraceComparator.new(test_stacktrace, note_stacktrace)
|
194
|
+
|
195
|
+
if stack_trace_comparator.lower_or_equal_to_diff_ratio?(max_diff_ratio)
|
196
|
+
puts " => [DEBUG] Note #{issue.web_url}#note_#{note.id} has an acceptable diff ratio of #{stack_trace_comparator.diff_percent}%."
|
197
|
+
# The `Gitlab::ObjectifiedHash` class overrides `#hash` which is used by `Hash#[]=` to compute the hash key.
|
198
|
+
# 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.
|
199
|
+
# See:
|
200
|
+
# - https://gitlab.com/gitlab-org/gitlab-qa/-/merge_requests/587#note_453336995
|
201
|
+
# - https://github.com/NARKOZ/gitlab/commit/cbdbd1e32623f018a8fae39932a8e3bc4d929abb?_pjax=%23js-repo-pjax-container#r44484494
|
202
|
+
stack_trace_comparator.diff_ratio
|
203
|
+
else
|
204
|
+
puts " => [DEBUG] Found note #{issue.web_url}#note_#{note.id} but stacktraces are too different (#{stack_trace_comparator.diff_percent}%).\n"
|
205
|
+
puts " => [DEBUG] Issue stacktrace:\n----------------\n#{note_stacktrace}\n----------------\n"
|
206
|
+
puts " => [DEBUG] Failure stacktrace:\n----------------\n#{test_stacktrace}\n----------------\n"
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
def add_report_for_test(current_reports_content:, test:)
|
211
|
+
increment_reports(
|
212
|
+
current_reports_content: current_reports_content,
|
213
|
+
test: test,
|
214
|
+
reports_section_header: REPORT_SECTION_HEADER,
|
215
|
+
item_extra_content: found_label,
|
216
|
+
reports_extra_content: "##### Stack trace\n\n```\n#{test.full_stacktrace}\n```"
|
217
|
+
)
|
218
|
+
end
|
219
|
+
|
220
|
+
def found_label
|
221
|
+
if ENV.key?('CI_MERGE_REQUEST_IID')
|
222
|
+
FOUND_IN_MR_LABEL
|
223
|
+
else
|
224
|
+
FOUND_IN_MASTER_LABEL
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
def identity_labels_quick_action
|
229
|
+
labels_list = IDENTITY_LABELS.map { |label| %(~"#{label}") }.join(' ')
|
230
|
+
%(/label #{labels_list})
|
231
|
+
end
|
232
|
+
|
233
|
+
def relate_issues_quick_actions(issues)
|
234
|
+
issues.map do |issue|
|
235
|
+
"/relate #{issue.web_url}"
|
236
|
+
end.join("\n")
|
237
|
+
end
|
238
|
+
end
|
239
|
+
end
|
240
|
+
end
|
241
|
+
end
|
@@ -4,14 +4,13 @@ module GitlabQuality
|
|
4
4
|
module TestTooling
|
5
5
|
module Report
|
6
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
|
8
|
-
# we retried failing specs.
|
7
|
+
# We expect the test reports to come from a new RSpec process where we retried failing specs.
|
9
8
|
#
|
10
|
-
# - Takes the JSON test reports like rspec-*.json
|
9
|
+
# - Takes the JSON test reports like rspec-*.json
|
11
10
|
# - Takes a project where flaky test issues should be created
|
12
11
|
# - For every passed test in the report:
|
13
|
-
# - Find issue by test hash
|
14
|
-
# -
|
12
|
+
# - Find issue by test hash or create a new issue if no issue was found
|
13
|
+
# - Add a flakiness report in the "Flakiness reports" note
|
15
14
|
class FlakyTestIssue < ReportAsIssue
|
16
15
|
include Concerns::GroupAndCategoryLabels
|
17
16
|
include Concerns::IssueReports
|
@@ -84,17 +83,20 @@ module GitlabQuality
|
|
84
83
|
end
|
85
84
|
|
86
85
|
def add_report_to_issue(issue:, test:, related_issues:)
|
87
|
-
|
86
|
+
current_reports_note = existing_reports_note(issue: issue)
|
87
|
+
new_reports_list = add_report_for_test(current_reports_content: current_reports_note&.body.to_s, test: test)
|
88
|
+
|
88
89
|
note_body = [
|
89
|
-
|
90
|
+
new_reports_list.to_s,
|
91
|
+
flakiness_status_labels_quick_action(new_reports_list.reports_count),
|
90
92
|
identity_labels_quick_action,
|
91
93
|
relate_issues_quick_actions(related_issues)
|
92
94
|
].join("\n")
|
93
95
|
|
94
|
-
if
|
96
|
+
if current_reports_note
|
95
97
|
gitlab.edit_issue_note(
|
96
98
|
issue_iid: issue.iid,
|
97
|
-
note_id:
|
99
|
+
note_id: current_reports_note.id,
|
98
100
|
note: note_body
|
99
101
|
)
|
100
102
|
else
|
@@ -108,9 +110,9 @@ module GitlabQuality
|
|
108
110
|
end
|
109
111
|
end
|
110
112
|
|
111
|
-
def
|
113
|
+
def add_report_for_test(current_reports_content:, test:)
|
112
114
|
increment_reports(
|
113
|
-
current_reports_content:
|
115
|
+
current_reports_content: current_reports_content,
|
114
116
|
test: test,
|
115
117
|
reports_section_header: REPORT_SECTION_HEADER,
|
116
118
|
item_extra_content: found_label,
|
@@ -126,6 +128,19 @@ module GitlabQuality
|
|
126
128
|
end
|
127
129
|
end
|
128
130
|
|
131
|
+
def flakiness_status_labels_quick_action(reports_count)
|
132
|
+
case reports_count
|
133
|
+
when 1000..Float::INFINITY
|
134
|
+
'/label ~"flakiness::1"'
|
135
|
+
when 500..999
|
136
|
+
'/label ~"flakiness::2"'
|
137
|
+
when 10..499
|
138
|
+
'/label ~"flakiness::3"'
|
139
|
+
else
|
140
|
+
'/label ~"flakiness::4"'
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
129
144
|
def identity_labels_quick_action
|
130
145
|
labels_list = IDENTITY_LABELS.map { |label| %(~"#{label}") }.join(' ')
|
131
146
|
%(/label #{labels_list})
|
@@ -2,7 +2,6 @@
|
|
2
2
|
|
3
3
|
require 'nokogiri'
|
4
4
|
require 'rubygems/text'
|
5
|
-
require 'amatch'
|
6
5
|
|
7
6
|
module GitlabQuality
|
8
7
|
module TestTooling
|
@@ -17,7 +16,6 @@ module GitlabQuality
|
|
17
16
|
include TestTooling::Concerns::FindSetDri
|
18
17
|
include Concerns::GroupAndCategoryLabels
|
19
18
|
include Concerns::IssueReports
|
20
|
-
include Amatch
|
21
19
|
|
22
20
|
DEFAULT_MAX_DIFF_RATIO_FOR_DETECTION = 0.15
|
23
21
|
SYSTEMIC_EXCEPTIONS_THRESHOLD = 10
|
@@ -26,10 +24,6 @@ module GitlabQuality
|
|
26
24
|
ISSUE_STACKTRACE_REGEX = /### Stack trace\s*(```)#{FAILURE_STACKTRACE_REGEX}(```)\n*\n###/m
|
27
25
|
|
28
26
|
NEW_ISSUE_LABELS = Set.new(%w[test failure::new priority::2]).freeze
|
29
|
-
IGNORED_FAILURES = [
|
30
|
-
'Net::ReadTimeout',
|
31
|
-
'403 Forbidden - Your account has been blocked'
|
32
|
-
].freeze
|
33
27
|
SCREENSHOT_IGNORED_ERRORS = ['500 Internal Server Error', 'fabricate_via_api!', 'Error Code 500'].freeze
|
34
28
|
|
35
29
|
MultipleIssuesFound = Class.new(StandardError)
|
@@ -168,7 +162,9 @@ module GitlabQuality
|
|
168
162
|
|
169
163
|
next if pipeline != pipeline_env_from_job_url(job_url_from_issue)
|
170
164
|
|
171
|
-
|
165
|
+
stack_trace_comparator = StackTraceComparator.new(cleaned_stack_trace_from_test(test), cleaned_stack_trace_from_issue(issue))
|
166
|
+
|
167
|
+
stack_trace_comparator.lower_than_diff_ratio?(max_diff_ratio)
|
172
168
|
end
|
173
169
|
end
|
174
170
|
|
@@ -196,17 +192,6 @@ module GitlabQuality
|
|
196
192
|
)
|
197
193
|
end
|
198
194
|
|
199
|
-
def full_stacktrace(test)
|
200
|
-
test.failures.each do |failure|
|
201
|
-
message = failure['message'] || ""
|
202
|
-
message_lines = failure['message_lines'] || []
|
203
|
-
|
204
|
-
next if IGNORED_FAILURES.any? { |e| message.include?(e) }
|
205
|
-
|
206
|
-
return message_lines.empty? ? message : message_lines.join("\n")
|
207
|
-
end
|
208
|
-
end
|
209
|
-
|
210
195
|
def cleaned_stack_trace_from_issue(issue)
|
211
196
|
relevant_issue_stacktrace = find_issue_stacktrace(issue)
|
212
197
|
return unless relevant_issue_stacktrace
|
@@ -215,20 +200,11 @@ module GitlabQuality
|
|
215
200
|
end
|
216
201
|
|
217
202
|
def cleaned_stack_trace_from_test(test)
|
218
|
-
test_failure_stacktrace = sanitize_stacktrace(full_stacktrace
|
219
|
-
FAILURE_STACKTRACE_REGEX) || full_stacktrace
|
203
|
+
test_failure_stacktrace = sanitize_stacktrace(test.full_stacktrace,
|
204
|
+
FAILURE_STACKTRACE_REGEX) || test.full_stacktrace
|
220
205
|
remove_unique_resource_names(test_failure_stacktrace)
|
221
206
|
end
|
222
207
|
|
223
|
-
def compare_stack_traces(stack_trace_first, stack_trace_second)
|
224
|
-
calculate_diff_ratio(stack_trace_first, stack_trace_second)
|
225
|
-
end
|
226
|
-
|
227
|
-
def calculate_diff_ratio(stack_trace_first, stack_trace_second)
|
228
|
-
distance = Levenshtein.new(stack_trace_first).match(stack_trace_second)
|
229
|
-
distance.zero? ? 0.0 : (distance.to_f / stack_trace_first.size).round(3)
|
230
|
-
end
|
231
|
-
|
232
208
|
def find_relevant_failure_issues(test) # rubocop:disable Metrics/AbcSize
|
233
209
|
clean_first_test_failure_stacktrace = cleaned_stack_trace_from_test(test)
|
234
210
|
# Search with the `search` param returns 500 errors, so we filter by `base_issue_labels` and then filter further in Ruby
|
@@ -236,17 +212,18 @@ module GitlabQuality
|
|
236
212
|
clean_relevant_issue_stacktrace = cleaned_stack_trace_from_issue(issue)
|
237
213
|
next if clean_relevant_issue_stacktrace.nil?
|
238
214
|
|
239
|
-
|
240
|
-
|
241
|
-
|
215
|
+
stack_trace_comparator = StackTraceComparator.new(clean_first_test_failure_stacktrace, clean_relevant_issue_stacktrace)
|
216
|
+
|
217
|
+
if stack_trace_comparator.lower_or_equal_to_diff_ratio?(max_diff_ratio)
|
218
|
+
puts " => [DEBUG] Issue #{issue.web_url} has an acceptable diff ratio of #{stack_trace_comparator.diff_percent}%."
|
242
219
|
# The `Gitlab::ObjectifiedHash` class overrides `#hash` which is used by `Hash#[]=` to compute the hash key.
|
243
220
|
# 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.
|
244
221
|
# See:
|
245
222
|
# - https://gitlab.com/gitlab-org/gitlab-qa/-/merge_requests/587#note_453336995
|
246
223
|
# - https://github.com/NARKOZ/gitlab/commit/cbdbd1e32623f018a8fae39932a8e3bc4d929abb?_pjax=%23js-repo-pjax-container#r44484494
|
247
|
-
memo[issue.to_h] = diff_ratio
|
224
|
+
memo[issue.to_h] = stack_trace_comparator.diff_ratio
|
248
225
|
else
|
249
|
-
puts " => [DEBUG] Found issue #{issue.web_url} but stacktraces are too different (#{
|
226
|
+
puts " => [DEBUG] Found issue #{issue.web_url} but stacktraces are too different (#{stack_trace_comparator.diff_percent}%).\n"
|
250
227
|
puts " => [DEBUG] Issue stacktrace:\n----------------\n#{clean_relevant_issue_stacktrace}\n----------------\n"
|
251
228
|
puts " => [DEBUG] Failure stacktrace:\n----------------\n#{clean_first_test_failure_stacktrace}\n----------------\n"
|
252
229
|
end
|
@@ -295,7 +272,7 @@ module GitlabQuality
|
|
295
272
|
def new_issue_description(test)
|
296
273
|
super + [
|
297
274
|
"\n### Stack trace",
|
298
|
-
"```\n#{full_stacktrace
|
275
|
+
"```\n#{test.full_stacktrace}\n```",
|
299
276
|
screenshot_section(test),
|
300
277
|
system_log_errors_section(test),
|
301
278
|
initial_reports_section(test)
|
@@ -345,7 +322,7 @@ module GitlabQuality
|
|
345
322
|
state_event = issue.state == 'closed' ? 'reopen' : nil
|
346
323
|
|
347
324
|
issue_attrs = {
|
348
|
-
description: increment_reports(current_reports_content: issue.description, test: test),
|
325
|
+
description: increment_reports(current_reports_content: issue.description, test: test).to_s,
|
349
326
|
labels: up_to_date_labels(test: test, issue: issue)
|
350
327
|
}
|
351
328
|
issue_attrs[:state_event] = state_event if state_event
|
@@ -357,7 +334,7 @@ module GitlabQuality
|
|
357
334
|
def screenshot_section(test)
|
358
335
|
return unless test.screenshot?
|
359
336
|
|
360
|
-
failure = full_stacktrace
|
337
|
+
failure = test.full_stacktrace
|
361
338
|
return if SCREENSHOT_IGNORED_ERRORS.any? { |e| failure.include?(e) }
|
362
339
|
|
363
340
|
relative_url = gitlab.upload_file(file_fullpath: test.screenshot_image)
|
@@ -374,7 +351,7 @@ module GitlabQuality
|
|
374
351
|
return false unless test.failures?
|
375
352
|
|
376
353
|
puts " => Systemic failures detected: #{systemic_failure_messages}" if systemic_failure_messages.any?
|
377
|
-
failure_to_ignore = IGNORED_FAILURES + systemic_failure_messages
|
354
|
+
failure_to_ignore = TestResult::BaseTestResult::IGNORED_FAILURES + systemic_failure_messages
|
378
355
|
|
379
356
|
reason = ignored_failure_reason(test.failures, failure_to_ignore)
|
380
357
|
|
@@ -105,13 +105,10 @@ module GitlabQuality
|
|
105
105
|
due_date: new_issue_due_date(test),
|
106
106
|
confidential: confidential
|
107
107
|
}.compact
|
108
|
-
issue = gitlab.create_issue(**attrs)
|
109
108
|
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
issue
|
109
|
+
gitlab.create_issue(**attrs).tap do |issue|
|
110
|
+
puts "Created new #{issue_type}: #{issue&.web_url}"
|
111
|
+
end
|
115
112
|
end
|
116
113
|
|
117
114
|
def issue_labels(issue)
|
@@ -64,17 +64,19 @@ module GitlabQuality
|
|
64
64
|
end
|
65
65
|
|
66
66
|
def add_report_to_issue(issue:, test:, related_issues:)
|
67
|
-
|
67
|
+
current_reports_note = existing_reports_note(issue: issue)
|
68
|
+
new_reports_list = add_report_for_test(current_reports_content: current_reports_note&.body.to_s, test: test)
|
69
|
+
|
68
70
|
note_body = [
|
69
|
-
|
71
|
+
new_reports_list.to_s,
|
70
72
|
identity_labels_quick_action,
|
71
73
|
relate_issues_quick_actions(related_issues)
|
72
74
|
].join("\n")
|
73
75
|
|
74
|
-
if
|
76
|
+
if current_reports_note
|
75
77
|
gitlab.edit_issue_note(
|
76
78
|
issue_iid: issue.iid,
|
77
|
-
note_id:
|
79
|
+
note_id: current_reports_note.id,
|
78
80
|
note: note_body
|
79
81
|
)
|
80
82
|
else
|
@@ -88,9 +90,9 @@ module GitlabQuality
|
|
88
90
|
end
|
89
91
|
end
|
90
92
|
|
91
|
-
def
|
93
|
+
def add_report_for_test(current_reports_content:, test:)
|
92
94
|
increment_reports(
|
93
|
-
current_reports_content:
|
95
|
+
current_reports_content: current_reports_content,
|
94
96
|
test: test,
|
95
97
|
reports_section_header: REPORT_SECTION_HEADER,
|
96
98
|
item_extra_content: "(#{test.run_time} seconds) #{found_label}",
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'amatch'
|
4
|
+
|
5
|
+
module GitlabQuality
|
6
|
+
module TestTooling
|
7
|
+
class StackTraceComparator
|
8
|
+
include Amatch
|
9
|
+
|
10
|
+
def initialize(first_trace, second_trace)
|
11
|
+
@first_trace = first_trace
|
12
|
+
@second_trace = second_trace
|
13
|
+
end
|
14
|
+
|
15
|
+
def diff_ratio
|
16
|
+
@diff_ratio ||= (1 - first_trace.levenshtein_similar(second_trace))
|
17
|
+
end
|
18
|
+
|
19
|
+
def diff_percent
|
20
|
+
(diff_ratio * 100).round(2)
|
21
|
+
end
|
22
|
+
|
23
|
+
def lower_than_diff_ratio?(max_diff_ratio)
|
24
|
+
diff_ratio < max_diff_ratio
|
25
|
+
end
|
26
|
+
|
27
|
+
def lower_or_equal_to_diff_ratio?(max_diff_ratio)
|
28
|
+
diff_ratio <= max_diff_ratio
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
attr_reader :first_trace, :second_trace
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -4,6 +4,11 @@ module GitlabQuality
|
|
4
4
|
module TestTooling
|
5
5
|
module TestResult
|
6
6
|
class BaseTestResult
|
7
|
+
IGNORED_FAILURES = [
|
8
|
+
'Net::ReadTimeout',
|
9
|
+
'403 Forbidden - Your account has been blocked'
|
10
|
+
].freeze
|
11
|
+
|
7
12
|
attr_reader :report
|
8
13
|
|
9
14
|
def initialize(report)
|
@@ -41,6 +46,17 @@ module GitlabQuality
|
|
41
46
|
def failures?
|
42
47
|
failures.any?
|
43
48
|
end
|
49
|
+
|
50
|
+
def full_stacktrace
|
51
|
+
failures.each do |failure|
|
52
|
+
message = failure['message'] || ""
|
53
|
+
message_lines = failure['message_lines'] || []
|
54
|
+
|
55
|
+
next if IGNORED_FAILURES.any? { |e| message.include?(e) }
|
56
|
+
|
57
|
+
return message_lines.empty? ? message : message_lines.join("\n")
|
58
|
+
end
|
59
|
+
end
|
44
60
|
end
|
45
61
|
end
|
46
62
|
end
|
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.23.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: 2024-04-
|
11
|
+
date: 2024-04-15 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: climate_control
|
@@ -308,6 +308,20 @@ dependencies:
|
|
308
308
|
- - "<"
|
309
309
|
- !ruby/object:Gem::Version
|
310
310
|
version: '4'
|
311
|
+
- !ruby/object:Gem::Dependency
|
312
|
+
name: rspec-parameterized
|
313
|
+
requirement: !ruby/object:Gem::Requirement
|
314
|
+
requirements:
|
315
|
+
- - "~>"
|
316
|
+
- !ruby/object:Gem::Version
|
317
|
+
version: 1.0.0
|
318
|
+
type: :runtime
|
319
|
+
prerelease: false
|
320
|
+
version_requirements: !ruby/object:Gem::Requirement
|
321
|
+
requirements:
|
322
|
+
- - "~>"
|
323
|
+
- !ruby/object:Gem::Version
|
324
|
+
version: 1.0.0
|
311
325
|
- !ruby/object:Gem::Dependency
|
312
326
|
name: table_print
|
313
327
|
requirement: !ruby/object:Gem::Requirement
|
@@ -342,24 +356,11 @@ dependencies:
|
|
342
356
|
- - "<"
|
343
357
|
- !ruby/object:Gem::Version
|
344
358
|
version: '3'
|
345
|
-
- !ruby/object:Gem::Dependency
|
346
|
-
name: rspec-parameterized
|
347
|
-
requirement: !ruby/object:Gem::Requirement
|
348
|
-
requirements:
|
349
|
-
- - "~>"
|
350
|
-
- !ruby/object:Gem::Version
|
351
|
-
version: 1.0.0
|
352
|
-
type: :runtime
|
353
|
-
prerelease: false
|
354
|
-
version_requirements: !ruby/object:Gem::Requirement
|
355
|
-
requirements:
|
356
|
-
- - "~>"
|
357
|
-
- !ruby/object:Gem::Version
|
358
|
-
version: 1.0.0
|
359
359
|
description: A collection of test-related tools.
|
360
360
|
email:
|
361
361
|
- quality@gitlab.com
|
362
362
|
executables:
|
363
|
+
- failed-test-issues
|
363
364
|
- flaky-test-issues
|
364
365
|
- generate-test-session
|
365
366
|
- knapsack-report-issues
|
@@ -388,6 +389,7 @@ files:
|
|
388
389
|
- LICENSE.txt
|
389
390
|
- README.md
|
390
391
|
- Rakefile
|
392
|
+
- exe/failed-test-issues
|
391
393
|
- exe/flaky-test-issues
|
392
394
|
- exe/generate-test-session
|
393
395
|
- exe/knapsack-report-issues
|
@@ -421,6 +423,7 @@ files:
|
|
421
423
|
- lib/gitlab_quality/test_tooling/report/concerns/issue_reports.rb
|
422
424
|
- lib/gitlab_quality/test_tooling/report/concerns/results_reporter.rb
|
423
425
|
- lib/gitlab_quality/test_tooling/report/concerns/utils.rb
|
426
|
+
- lib/gitlab_quality/test_tooling/report/failed_test_issue.rb
|
424
427
|
- lib/gitlab_quality/test_tooling/report/flaky_test_issue.rb
|
425
428
|
- lib/gitlab_quality/test_tooling/report/generate_test_session.rb
|
426
429
|
- lib/gitlab_quality/test_tooling/report/issue_logger.rb
|
@@ -438,6 +441,7 @@ files:
|
|
438
441
|
- lib/gitlab_quality/test_tooling/runtime/logger.rb
|
439
442
|
- lib/gitlab_quality/test_tooling/slack/post_to_slack.rb
|
440
443
|
- lib/gitlab_quality/test_tooling/slack/post_to_slack_dry.rb
|
444
|
+
- lib/gitlab_quality/test_tooling/stack_trace_comparator.rb
|
441
445
|
- lib/gitlab_quality/test_tooling/summary_table.rb
|
442
446
|
- lib/gitlab_quality/test_tooling/support/http_request.rb
|
443
447
|
- lib/gitlab_quality/test_tooling/system_logs/finders/json_log_finder.rb
|