gitlab_quality-test_tooling 1.27.1 → 1.29.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/.rubocop.yml +11 -2
- data/Gemfile.lock +2 -2
- data/exe/post-to-slack +21 -3
- data/lib/gitlab_quality/test_tooling/gitlab_client/repository_files_client.rb +4 -0
- data/lib/gitlab_quality/test_tooling/report/concerns/issue_reports.rb +61 -8
- data/lib/gitlab_quality/test_tooling/report/failed_test_issue.rb +16 -109
- data/lib/gitlab_quality/test_tooling/report/flaky_test_issue.rb +13 -106
- data/lib/gitlab_quality/test_tooling/report/generate_test_session.rb +1 -1
- data/lib/gitlab_quality/test_tooling/report/health_problem_reporter.rb +194 -0
- data/lib/gitlab_quality/test_tooling/report/merge_request_slow_tests_report.rb +1 -1
- data/lib/gitlab_quality/test_tooling/report/relate_failure_issue.rb +1 -1
- data/lib/gitlab_quality/test_tooling/report/report_as_issue.rb +12 -6
- data/lib/gitlab_quality/test_tooling/report/report_results.rb +1 -1
- data/lib/gitlab_quality/test_tooling/report/slow_test_issue.rb +13 -102
- data/lib/gitlab_quality/test_tooling/summary_table.rb +9 -5
- data/lib/gitlab_quality/test_tooling/test_metrics_exporter/test_metrics.rb +3 -1
- data/lib/gitlab_quality/test_tooling/test_result/base_test_result.rb +30 -1
- data/lib/gitlab_quality/test_tooling/test_results/base_test_results.rb +5 -2
- data/lib/gitlab_quality/test_tooling/test_results/builder.rb +7 -4
- data/lib/gitlab_quality/test_tooling/test_results/j_unit_test_results.rb +1 -1
- data/lib/gitlab_quality/test_tooling/test_results/json_test_results.rb +1 -1
- data/lib/gitlab_quality/test_tooling/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ca3b15faac90436068b21df05071cbcd06dd1a71d33b3220a58d17201a893a2d
|
4
|
+
data.tar.gz: dee6c41cfe097f6af193890eee46a95a17f1fa7724377fb8529f75142931bbb1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ab45b4d7059cfb911b103f952f0e072dd578057a24fb539ac322087b09d4cd646ecdeb0c7a528427cd378f67a056d374a663c1effb62945a2001b18fea26af8c
|
7
|
+
data.tar.gz: 387923350e8554c3fd99d17d3a1ea502768d4cfc251805a5c76a17bca950ef28e616ec161eb329c0482b678f5e3a037c3235ca6562eb93008e57c6963838a3ba
|
data/.rubocop.yml
CHANGED
@@ -8,8 +8,8 @@ inherit_from:
|
|
8
8
|
<% end %>
|
9
9
|
|
10
10
|
AllCops:
|
11
|
-
#
|
12
|
-
TargetRubyVersion:
|
11
|
+
# The oldest supported Ruby version.
|
12
|
+
TargetRubyVersion: 3.1
|
13
13
|
Exclude:
|
14
14
|
- 'vendor/**/*'
|
15
15
|
- 'tmp/**/*'
|
@@ -63,3 +63,12 @@ Layout/SpaceBeforeFirstArg:
|
|
63
63
|
|
64
64
|
RSpec/MultipleMemoizedHelpers:
|
65
65
|
Enabled: false
|
66
|
+
|
67
|
+
# Short-hand Hash syntax does not work prior 3.1.
|
68
|
+
# See https://gitlab.com/gitlab-org/gitlab/-/issues/435940#note_1703307479
|
69
|
+
Style/HashSyntax:
|
70
|
+
EnforcedShorthandSyntax: never
|
71
|
+
|
72
|
+
# Anonymous block forwarding does not work prior 3.1.
|
73
|
+
Naming/BlockForwarding:
|
74
|
+
Enabled: false
|
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.29.0)
|
5
5
|
activesupport (>= 7.0, < 7.2)
|
6
6
|
amatch (~> 0.4.1)
|
7
7
|
gitlab (~> 4.19)
|
@@ -330,4 +330,4 @@ DEPENDENCIES
|
|
330
330
|
webmock (= 3.7.0)
|
331
331
|
|
332
332
|
BUNDLED WITH
|
333
|
-
2.5.
|
333
|
+
2.5.6
|
data/exe/post-to-slack
CHANGED
@@ -7,6 +7,7 @@ require "optparse"
|
|
7
7
|
require_relative "../lib/gitlab_quality/test_tooling"
|
8
8
|
|
9
9
|
params = {}
|
10
|
+
summary_table_opts = {}
|
10
11
|
|
11
12
|
messages = []
|
12
13
|
gitlab_api_token = nil
|
@@ -30,8 +31,24 @@ options = OptionParser.new do |opts|
|
|
30
31
|
messages << message
|
31
32
|
end
|
32
33
|
|
34
|
+
opts.on('-s', '--sort-by SORT', String,
|
35
|
+
'Used with the `--include-summary-table` flag — An optional sort for the test summary table. This flag must be positioned before `--include-summary-table`') do |sort_by|
|
36
|
+
valid_options = ["Dev Stage", "Total", "Failures", "Errors", "Skipped", "Result"].freeze
|
37
|
+
|
38
|
+
raise ArgumentError, "Invalid sort option: #{sort_by}. Valid options are: #{valid_options.join(', ')}" unless valid_options.include?(sort_by)
|
39
|
+
|
40
|
+
summary_table_opts[:sort_by] = sort_by
|
41
|
+
end
|
42
|
+
|
43
|
+
opts.on('-d', '--sort-direction [asc|desc]', String,
|
44
|
+
'Used with the `--sort-by` flag — Define the sort direction of the test summary table (default: `asc`)') do |sort_direction|
|
45
|
+
raise ArgumentError, "Invalid sort direction: #{sort_direction}. Valid options are: asc, desc" unless %w[asc desc].include?(sort_direction.downcase)
|
46
|
+
|
47
|
+
summary_table_opts[:sort_direction] = sort_direction.downcase.to_sym
|
48
|
+
end
|
49
|
+
|
33
50
|
opts.on('-t', '--include-summary-table FILES', String, 'Add a test summary table based on RSpec report files (JUnit XML)') do |files|
|
34
|
-
|
51
|
+
params[:summary_table_files] = files
|
35
52
|
end
|
36
53
|
|
37
54
|
opts.on('-j', '--include-failed-jobs-table', 'Add a list of failed jobs in the pipeline') do
|
@@ -71,9 +88,10 @@ options = OptionParser.new do |opts|
|
|
71
88
|
opts.parse(ARGV)
|
72
89
|
end
|
73
90
|
|
74
|
-
params[:message] = messages.join("\n")
|
75
|
-
|
76
91
|
if params.any?
|
92
|
+
messages << GitlabQuality::TestTooling::SummaryTable.create(input_files: params.delete(:summary_table_files), **summary_table_opts) if params.key?(:summary_table_files)
|
93
|
+
params[:message] = messages.join("\n")
|
94
|
+
|
77
95
|
GitlabQuality::TestTooling::Slack::PostToSlack.new(**params).invoke!
|
78
96
|
else
|
79
97
|
puts options
|
@@ -9,8 +9,9 @@ module GitlabQuality
|
|
9
9
|
module IssueReports
|
10
10
|
JOB_URL_REGEX = %r{(?<job_url>https://(?<host>[\w.]+)/(?<project_path>[\w\-./]+)/-/jobs/\d+)}
|
11
11
|
FAILED_JOB_DESCRIPTION_REGEX = /First happened in #{JOB_URL_REGEX}\./m
|
12
|
-
REPORT_ITEM_REGEX = /^1\.
|
12
|
+
REPORT_ITEM_REGEX = /^1\. (?<report_date>\d{4}-\d{2}-\d{2}): #{JOB_URL_REGEX} \((?<pipeline_url>\S+)\) ?(?<extra_content>.*)$/
|
13
13
|
LATEST_REPORTS_TO_SHOW = 10
|
14
|
+
DAILY_REPORTS_THRESHOLDS = 10
|
14
15
|
|
15
16
|
class ReportsList
|
16
17
|
def initialize(preserved_content:, section_header:, reports:, extra_content:)
|
@@ -21,7 +22,12 @@ module GitlabQuality
|
|
21
22
|
end
|
22
23
|
|
23
24
|
def self.report_list_item(test, item_extra_content: nil)
|
24
|
-
|
25
|
+
ReportListItem.new(job_url: test.ci_job_url, extra_content: item_extra_content)
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.parse_report_date_from_string(date_string)
|
29
|
+
parsed_time = Time.strptime(date_string, '%F')
|
30
|
+
Time.utc(parsed_time.year, parsed_time.month, parsed_time.day)
|
25
31
|
end
|
26
32
|
|
27
33
|
def reports_count
|
@@ -32,21 +38,27 @@ module GitlabQuality
|
|
32
38
|
[
|
33
39
|
preserved_content,
|
34
40
|
"#{section_header} (#{reports_count})",
|
35
|
-
reports_list
|
41
|
+
reports_list,
|
36
42
|
extra_content
|
37
43
|
].reject(&:blank?).compact.join("\n\n")
|
38
44
|
end
|
39
45
|
|
46
|
+
def spiked_in_short_period?
|
47
|
+
latest_report = sorted_reports.first
|
48
|
+
|
49
|
+
reports_for_latest_report_day = sorted_reports.count { |report| report.report_date == latest_report.report_date }
|
50
|
+
|
51
|
+
reports_for_latest_report_day >= DAILY_REPORTS_THRESHOLDS
|
52
|
+
end
|
53
|
+
|
40
54
|
private
|
41
55
|
|
42
56
|
attr_reader :preserved_content, :section_header, :reports, :extra_content
|
43
57
|
|
44
|
-
def reports_list
|
45
|
-
sorted_reports = reports.sort.reverse
|
46
|
-
|
58
|
+
def reports_list
|
47
59
|
if sorted_reports.size > LATEST_REPORTS_TO_SHOW
|
48
60
|
[
|
49
|
-
"Last
|
61
|
+
"Last #{LATEST_REPORTS_TO_SHOW} reports:",
|
50
62
|
sorted_reports[...LATEST_REPORTS_TO_SHOW].join("\n"),
|
51
63
|
"<details><summary>See #{sorted_reports.size - LATEST_REPORTS_TO_SHOW} more reports</summary>",
|
52
64
|
sorted_reports[LATEST_REPORTS_TO_SHOW..].join("\n"),
|
@@ -56,6 +68,41 @@ module GitlabQuality
|
|
56
68
|
sorted_reports.join("\n")
|
57
69
|
end
|
58
70
|
end
|
71
|
+
|
72
|
+
def sorted_reports
|
73
|
+
@sorted_reports ||= reports.sort.reverse
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
class ReportListItem
|
78
|
+
attr_reader :report_date
|
79
|
+
|
80
|
+
def initialize(job_url:, report_date: now, pipeline_url: default_pipeline_url, extra_content: '')
|
81
|
+
@job_url = job_url
|
82
|
+
@report_date = report_date
|
83
|
+
@pipeline_url = pipeline_url
|
84
|
+
@extra_content = extra_content
|
85
|
+
end
|
86
|
+
|
87
|
+
def to_s
|
88
|
+
"1. #{report_date}: #{job_url} (#{pipeline_url}) #{extra_content}".strip
|
89
|
+
end
|
90
|
+
|
91
|
+
def <=>(other)
|
92
|
+
to_s <=> other.to_s
|
93
|
+
end
|
94
|
+
|
95
|
+
private
|
96
|
+
|
97
|
+
attr_reader :job_url, :pipeline_url, :extra_content
|
98
|
+
|
99
|
+
def default_pipeline_url
|
100
|
+
ENV.fetch('CI_PIPELINE_URL', 'pipeline url is missing')
|
101
|
+
end
|
102
|
+
|
103
|
+
def now
|
104
|
+
Time.new.utc.strftime('%F')
|
105
|
+
end
|
59
106
|
end
|
60
107
|
|
61
108
|
def initial_reports_section(test)
|
@@ -98,7 +145,13 @@ module GitlabQuality
|
|
98
145
|
private
|
99
146
|
|
100
147
|
def report_lines(content)
|
101
|
-
content.lines.
|
148
|
+
content.lines.filter_map do |line|
|
149
|
+
match = line.match(REPORT_ITEM_REGEX)
|
150
|
+
next unless match
|
151
|
+
|
152
|
+
match_data_hash = match.named_captures.transform_keys(&:to_sym)
|
153
|
+
ReportListItem.new(**match_data_hash.slice(:job_url, :report_date, :pipeline_url, :extra_content))
|
154
|
+
end
|
102
155
|
end
|
103
156
|
|
104
157
|
def job_urls_from_description(issue_description, regex)
|
@@ -10,16 +10,9 @@ module GitlabQuality
|
|
10
10
|
# - For every passed test in the report:
|
11
11
|
# - Find issue by test hash or create a new issue if no issue was found
|
12
12
|
# - Add a failure report in the "Failure reports" note
|
13
|
-
class FailedTestIssue <
|
14
|
-
include Concerns::GroupAndCategoryLabels
|
15
|
-
include Concerns::IssueReports
|
16
|
-
|
13
|
+
class FailedTestIssue < HealthProblemReporter
|
17
14
|
IDENTITY_LABELS = ['test', 'automation:bot-authored'].freeze
|
18
15
|
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
16
|
REPORT_SECTION_HEADER = '#### Failure reports'
|
24
17
|
|
25
18
|
FAILURE_STACKTRACE_REGEX = %r{(?:(?:.*Failure/Error:(?<stacktrace>.+))|(?<stacktrace>.+))}m
|
@@ -42,93 +35,36 @@ module GitlabQuality
|
|
42
35
|
|
43
36
|
attr_reader :base_issue_labels, :max_diff_ratio
|
44
37
|
|
45
|
-
def
|
46
|
-
|
47
|
-
|
48
|
-
TestResults::Builder.new(files).test_results_per_file do |test_results|
|
49
|
-
puts "=> Reporting #{test_results.count} tests in #{test_results.path}"
|
50
|
-
|
51
|
-
process_test_results(test_results)
|
52
|
-
end
|
53
|
-
end
|
54
|
-
|
55
|
-
def process_test_results(test_results)
|
56
|
-
test_results.each do |test|
|
57
|
-
next unless test_is_applicable?(test)
|
58
|
-
|
59
|
-
puts " => Reporting failure for test '#{test.name}'..."
|
60
|
-
|
61
|
-
issues = find_issues_by_hash(test_hash(test), state: 'opened', labels: SEARCH_LABELS)
|
62
|
-
|
63
|
-
if issues.empty?
|
64
|
-
issues << create_issue(test)
|
65
|
-
else
|
66
|
-
# Keep issues description up-to-date
|
67
|
-
update_issues(issues, test)
|
68
|
-
end
|
69
|
-
|
70
|
-
update_reports(issues, test)
|
71
|
-
collect_issues(test, issues)
|
72
|
-
end
|
38
|
+
def problem_type
|
39
|
+
'failed'
|
73
40
|
end
|
74
41
|
|
75
42
|
def test_is_applicable?(test)
|
76
43
|
test.status == 'failed'
|
77
44
|
end
|
78
45
|
|
79
|
-
def
|
80
|
-
|
46
|
+
def report_in_discussion?
|
47
|
+
true
|
81
48
|
end
|
82
49
|
|
83
|
-
def
|
84
|
-
|
85
|
-
puts " => Adding the failed test to the existing issue: #{issue.web_url}"
|
86
|
-
add_report_to_issue(issue: issue, test: test, related_issues: (issues - [issue]))
|
87
|
-
end
|
50
|
+
def identity_labels
|
51
|
+
IDENTITY_LABELS
|
88
52
|
end
|
89
53
|
|
90
|
-
def
|
91
|
-
|
92
|
-
current_reports_note = find_failure_discussion_note(issue: issue, test: test, reports_discussion: reports_discussion)
|
93
|
-
new_reports_list = add_report_for_test(current_reports_content: current_reports_note&.body.to_s, test: test)
|
94
|
-
|
95
|
-
note_body = [
|
96
|
-
new_reports_list.to_s,
|
97
|
-
identity_labels_quick_action,
|
98
|
-
relate_issues_quick_actions(related_issues)
|
99
|
-
].join("\n")
|
100
|
-
|
101
|
-
if current_reports_note
|
102
|
-
gitlab.edit_issue_note(
|
103
|
-
issue_iid: issue.iid,
|
104
|
-
note_id: current_reports_note.id,
|
105
|
-
note: note_body
|
106
|
-
)
|
107
|
-
else
|
108
|
-
gitlab.add_note_to_issue_discussion_as_thread(
|
109
|
-
iid: issue.iid,
|
110
|
-
discussion_id: reports_discussion.id,
|
111
|
-
note: note_body
|
112
|
-
)
|
113
|
-
end
|
114
|
-
rescue MultipleNotesFound => e
|
115
|
-
warn(e.message)
|
54
|
+
def report_section_header
|
55
|
+
REPORT_SECTION_HEADER
|
116
56
|
end
|
117
57
|
|
118
|
-
def
|
119
|
-
|
120
|
-
return reports_discussion if reports_discussion
|
121
|
-
|
122
|
-
gitlab.create_issue_discussion(iid: issue.iid, note: REPORTS_DISCUSSION_HEADER)
|
58
|
+
def reports_extra_content(test)
|
59
|
+
"##### Stack trace\n\n```\n#{test.full_stacktrace}\n```"
|
123
60
|
end
|
124
61
|
|
125
|
-
def
|
126
|
-
|
127
|
-
|
128
|
-
next unless discussion.notes.first
|
62
|
+
def health_problem_status_label_quick_action(reports_list)
|
63
|
+
'/label ~"severity::1"' if reports_list.spiked_in_short_period?
|
64
|
+
end
|
129
65
|
|
130
|
-
|
131
|
-
|
66
|
+
def up_to_date_labels(test:, issue: nil, new_labels: Set.new)
|
67
|
+
(base_issue_labels + super).to_a
|
132
68
|
end
|
133
69
|
|
134
70
|
def find_failure_discussion_note(issue:, test:, reports_discussion:)
|
@@ -208,35 +144,6 @@ module GitlabQuality
|
|
208
144
|
puts " => [DEBUG] Failure stacktrace:\n----------------\n#{test_stacktrace}\n----------------\n"
|
209
145
|
end
|
210
146
|
end
|
211
|
-
|
212
|
-
def add_report_for_test(current_reports_content:, test:)
|
213
|
-
increment_reports(
|
214
|
-
current_reports_content: current_reports_content,
|
215
|
-
test: test,
|
216
|
-
reports_section_header: REPORT_SECTION_HEADER,
|
217
|
-
item_extra_content: found_label,
|
218
|
-
reports_extra_content: "##### Stack trace\n\n```\n#{test.full_stacktrace}\n```"
|
219
|
-
)
|
220
|
-
end
|
221
|
-
|
222
|
-
def found_label
|
223
|
-
if ENV.key?('CI_MERGE_REQUEST_IID')
|
224
|
-
FOUND_IN_MR_LABEL
|
225
|
-
else
|
226
|
-
FOUND_IN_MASTER_LABEL
|
227
|
-
end
|
228
|
-
end
|
229
|
-
|
230
|
-
def identity_labels_quick_action
|
231
|
-
labels_list = IDENTITY_LABELS.map { |label| %(~"#{label}") }.join(' ')
|
232
|
-
%(/label #{labels_list})
|
233
|
-
end
|
234
|
-
|
235
|
-
def relate_issues_quick_actions(issues)
|
236
|
-
issues.map do |issue|
|
237
|
-
"/relate #{issue.web_url}"
|
238
|
-
end.join("\n")
|
239
|
-
end
|
240
147
|
end
|
241
148
|
end
|
242
149
|
end
|
@@ -11,15 +11,9 @@ module GitlabQuality
|
|
11
11
|
# - For every passed test in the report:
|
12
12
|
# - Find issue by test hash or create a new issue if no issue was found
|
13
13
|
# - Add a flakiness report in the "Flakiness reports" note
|
14
|
-
class FlakyTestIssue <
|
15
|
-
include Concerns::GroupAndCategoryLabels
|
16
|
-
include Concerns::IssueReports
|
17
|
-
|
14
|
+
class FlakyTestIssue < HealthProblemReporter
|
18
15
|
IDENTITY_LABELS = ['test', 'failure::flaky-test', 'automation:bot-authored'].freeze
|
19
16
|
NEW_ISSUE_LABELS = Set.new(['type::maintenance', 'priority::3', 'severity::3', *IDENTITY_LABELS]).freeze
|
20
|
-
SEARCH_LABELS = ['test'].freeze
|
21
|
-
FOUND_IN_MR_LABEL = '~"found:in MR"'
|
22
|
-
FOUND_IN_MASTER_LABEL = '~"found:master"'
|
23
17
|
REPORT_SECTION_HEADER = '### Flakiness reports'
|
24
18
|
REPORTS_DOCUMENTATION = <<~DOC
|
25
19
|
Flaky tests were detected. Please refer to the [Flaky tests reproducibility instructions](https://docs.gitlab.com/ee/development/testing_guide/flaky_tests.html#how-to-reproduce-a-flaky-test-locally)
|
@@ -38,108 +32,28 @@ module GitlabQuality
|
|
38
32
|
|
39
33
|
attr_reader :base_issue_labels
|
40
34
|
|
41
|
-
def
|
42
|
-
|
43
|
-
|
44
|
-
TestResults::Builder.new(files).test_results_per_file do |test_results|
|
45
|
-
puts "=> Reporting #{test_results.count} tests in #{test_results.path}"
|
46
|
-
|
47
|
-
process_test_results(test_results)
|
48
|
-
end
|
49
|
-
end
|
50
|
-
|
51
|
-
def process_test_results(test_results)
|
52
|
-
test_results.each do |test|
|
53
|
-
next unless test_is_applicable?(test)
|
54
|
-
|
55
|
-
puts " => Reporting flakiness for test '#{test.name}'..."
|
56
|
-
|
57
|
-
issues = find_issues_by_hash(test_hash(test), state: 'opened', labels: SEARCH_LABELS)
|
58
|
-
|
59
|
-
if issues.empty?
|
60
|
-
issues << create_issue(test)
|
61
|
-
else
|
62
|
-
# Keep issues description up-to-date
|
63
|
-
update_issues(issues, test)
|
64
|
-
end
|
65
|
-
|
66
|
-
update_reports(issues, test)
|
67
|
-
collect_issues(test, issues)
|
68
|
-
end
|
35
|
+
def problem_type
|
36
|
+
'flaky'
|
69
37
|
end
|
70
38
|
|
71
39
|
def test_is_applicable?(test)
|
72
40
|
test.status == 'passed' # We only want failed tests that passed in the end
|
73
41
|
end
|
74
42
|
|
75
|
-
def
|
76
|
-
|
43
|
+
def identity_labels
|
44
|
+
IDENTITY_LABELS
|
77
45
|
end
|
78
46
|
|
79
|
-
def
|
80
|
-
|
81
|
-
puts " => Adding the flaky test to the existing issue: #{issue.web_url}"
|
82
|
-
add_report_to_issue(issue: issue, test: test, related_issues: (issues - [issue]))
|
83
|
-
end
|
47
|
+
def report_section_header
|
48
|
+
REPORT_SECTION_HEADER
|
84
49
|
end
|
85
50
|
|
86
|
-
def
|
87
|
-
|
88
|
-
new_reports_list = add_report_for_test(current_reports_content: current_reports_note&.body.to_s, test: test)
|
89
|
-
|
90
|
-
note_body = [
|
91
|
-
new_reports_list.to_s,
|
92
|
-
flakiness_status_labels_quick_action(new_reports_list.reports_count),
|
93
|
-
identity_labels_quick_action,
|
94
|
-
relate_issues_quick_actions(related_issues)
|
95
|
-
].join("\n")
|
96
|
-
|
97
|
-
if current_reports_note
|
98
|
-
gitlab.edit_issue_note(
|
99
|
-
issue_iid: issue.iid,
|
100
|
-
note_id: current_reports_note.id,
|
101
|
-
note: note_body
|
102
|
-
)
|
103
|
-
else
|
104
|
-
gitlab.create_issue_note(iid: issue.iid, note: note_body)
|
105
|
-
end
|
106
|
-
end
|
107
|
-
|
108
|
-
def existing_reports_note(issue:)
|
109
|
-
gitlab.find_issue_notes(iid: issue.iid).find do |note|
|
110
|
-
note.body.start_with?(REPORT_SECTION_HEADER)
|
111
|
-
end
|
112
|
-
end
|
113
|
-
|
114
|
-
def add_report_for_test(current_reports_content:, test:)
|
115
|
-
increment_reports(
|
116
|
-
current_reports_content: current_reports_content,
|
117
|
-
test: test,
|
118
|
-
reports_section_header: REPORT_SECTION_HEADER,
|
119
|
-
item_extra_content: found_label,
|
120
|
-
reports_extra_content: REPORTS_DOCUMENTATION
|
121
|
-
)
|
51
|
+
def reports_extra_content(_test)
|
52
|
+
REPORTS_DOCUMENTATION
|
122
53
|
end
|
123
54
|
|
124
|
-
def
|
125
|
-
|
126
|
-
FOUND_IN_MR_LABEL
|
127
|
-
else
|
128
|
-
FOUND_IN_MASTER_LABEL
|
129
|
-
end
|
130
|
-
end
|
131
|
-
|
132
|
-
# The report count is based on the percentiles of flakiness issues.
|
133
|
-
#
|
134
|
-
# See https://gitlab.com/gitlab-org/quality/engineering-productivity/team/-/blob/main/scripts/unhealthy_test_issues_statistics.rb
|
135
|
-
# to gather these statistics.
|
136
|
-
#
|
137
|
-
# x <= P90 => flakiness::4
|
138
|
-
# P90 < x <= P95 => flakiness::3
|
139
|
-
# P95 < x <= P99 => flakiness::2
|
140
|
-
# > P99 => flakiness::1
|
141
|
-
def flakiness_status_labels_quick_action(reports_count)
|
142
|
-
case reports_count
|
55
|
+
def health_problem_status_label_quick_action(reports_list)
|
56
|
+
case reports_list.reports_count
|
143
57
|
when 399..Float::INFINITY
|
144
58
|
'/label ~"flakiness::1"'
|
145
59
|
when 37..398
|
@@ -151,15 +65,8 @@ module GitlabQuality
|
|
151
65
|
end
|
152
66
|
end
|
153
67
|
|
154
|
-
def
|
155
|
-
|
156
|
-
%(/label #{labels_list})
|
157
|
-
end
|
158
|
-
|
159
|
-
def relate_issues_quick_actions(issues)
|
160
|
-
issues.map do |issue|
|
161
|
-
"/relate #{issue.web_url}"
|
162
|
-
end.join("\n")
|
68
|
+
def up_to_date_labels(test:, issue: nil, new_labels: Set.new)
|
69
|
+
(base_issue_labels + super).to_a
|
163
70
|
end
|
164
71
|
end
|
165
72
|
end
|
@@ -25,7 +25,7 @@ module GitlabQuality
|
|
25
25
|
tests = Dir.glob(files).flat_map do |path|
|
26
26
|
puts "Loading tests in #{path}"
|
27
27
|
|
28
|
-
TestResults::JsonTestResults.new(path).to_a
|
28
|
+
TestResults::JsonTestResults.new(path: path).to_a
|
29
29
|
end
|
30
30
|
|
31
31
|
tests = tests.select { |test| pipeline_stages.include? test.report["stage"] } unless pipeline_stages.empty?
|
@@ -0,0 +1,194 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GitlabQuality
|
4
|
+
module TestTooling
|
5
|
+
module Report
|
6
|
+
# Base class for specific health problems reporting.
|
7
|
+
# Uses the API to create GitLab issues for any passed test coming from JSON test reports.
|
8
|
+
# We expect the test reports to come from a new RSpec process where we retried failing specs.
|
9
|
+
#
|
10
|
+
# - Takes the JSON test reports like rspec-*.json
|
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 or create a new issue if no issue was found
|
14
|
+
# - Add a flakiness report in the "Flakiness reports" note
|
15
|
+
class HealthProblemReporter < ReportAsIssue
|
16
|
+
include Concerns::GroupAndCategoryLabels
|
17
|
+
include Concerns::IssueReports
|
18
|
+
|
19
|
+
BASE_SEARCH_LABELS = ['test'].freeze
|
20
|
+
FOUND_IN_MR_LABEL = '~"found:in MR"'
|
21
|
+
FOUND_IN_MASTER_LABEL = '~"found:master"'
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def problem_type
|
26
|
+
'unhealthy'
|
27
|
+
end
|
28
|
+
|
29
|
+
def test_is_applicable?(_test)
|
30
|
+
false
|
31
|
+
end
|
32
|
+
|
33
|
+
def report_in_discussion?
|
34
|
+
false
|
35
|
+
end
|
36
|
+
|
37
|
+
def identity_labels
|
38
|
+
[]
|
39
|
+
end
|
40
|
+
|
41
|
+
def search_labels
|
42
|
+
BASE_SEARCH_LABELS
|
43
|
+
end
|
44
|
+
|
45
|
+
def report_section_header
|
46
|
+
''
|
47
|
+
end
|
48
|
+
|
49
|
+
def find_failure_discussion_note(_issue:, _test:, _reports_discussion:)
|
50
|
+
nil
|
51
|
+
end
|
52
|
+
|
53
|
+
def reports_extra_content(_test)
|
54
|
+
''
|
55
|
+
end
|
56
|
+
|
57
|
+
def health_problem_status_label_quick_action(_reports_count)
|
58
|
+
''
|
59
|
+
end
|
60
|
+
|
61
|
+
def item_extra_content(_test)
|
62
|
+
found_label
|
63
|
+
end
|
64
|
+
|
65
|
+
def run!
|
66
|
+
puts "Reporting tests in `#{files.join(',')}` as issues in project `#{project}` via the API at `#{Runtime::Env.gitlab_api_base}`."
|
67
|
+
|
68
|
+
TestResults::Builder.new(file_glob: files, token: token, project: project).test_results_per_file do |test_results|
|
69
|
+
puts "=> Reporting #{test_results.count} tests in #{test_results.path}"
|
70
|
+
|
71
|
+
process_test_results(test_results)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def process_test_results(test_results)
|
76
|
+
test_results.each do |test|
|
77
|
+
next unless test_is_applicable?(test)
|
78
|
+
|
79
|
+
puts " => Reporting #{problem_type} test '#{test.name}'..."
|
80
|
+
|
81
|
+
issues = find_issues_by_hash(test_hash(test), state: 'opened', labels: search_labels)
|
82
|
+
|
83
|
+
if issues.empty?
|
84
|
+
issues << create_issue(test)
|
85
|
+
else
|
86
|
+
# Keep issues description up-to-date
|
87
|
+
update_issues(issues, test)
|
88
|
+
end
|
89
|
+
|
90
|
+
update_reports(issues, test)
|
91
|
+
collect_issues(test, issues)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def update_reports(issues, test)
|
96
|
+
issues.each do |issue|
|
97
|
+
puts " => Reporting #{problem_type} test to existing issue: #{issue.web_url}"
|
98
|
+
add_report_to_issue(issue: issue, test: test, related_issues: (issues - [issue]))
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def add_report_to_issue(issue:, test:, related_issues:) # rubocop:disable Metrics/AbcSize -- FIXME
|
103
|
+
current_reports_note =
|
104
|
+
if report_in_discussion?
|
105
|
+
reports_discussion = find_or_create_reports_discussion(issue: issue)
|
106
|
+
|
107
|
+
find_failure_discussion_note(issue: issue, test: test, reports_discussion: reports_discussion)
|
108
|
+
else
|
109
|
+
existing_reports_note(issue: issue)
|
110
|
+
end
|
111
|
+
|
112
|
+
new_reports_list = add_report_for_test(current_reports_content: current_reports_note&.body.to_s, test: test)
|
113
|
+
|
114
|
+
note_body = [
|
115
|
+
new_reports_list.to_s,
|
116
|
+
health_problem_status_label_quick_action(new_reports_list),
|
117
|
+
identity_labels_quick_action,
|
118
|
+
relate_issues_quick_actions(related_issues)
|
119
|
+
].join("\n")
|
120
|
+
|
121
|
+
if current_reports_note
|
122
|
+
gitlab.edit_issue_note(
|
123
|
+
issue_iid: issue.iid,
|
124
|
+
note_id: current_reports_note.id,
|
125
|
+
note: note_body
|
126
|
+
)
|
127
|
+
elsif report_in_discussion?
|
128
|
+
gitlab.add_note_to_issue_discussion_as_thread(
|
129
|
+
iid: issue.iid,
|
130
|
+
discussion_id: reports_discussion.id,
|
131
|
+
note: note_body
|
132
|
+
)
|
133
|
+
else
|
134
|
+
gitlab.create_issue_note(iid: issue.iid, note: note_body)
|
135
|
+
end
|
136
|
+
rescue MultipleNotesFound => e
|
137
|
+
warn(e.message)
|
138
|
+
end
|
139
|
+
|
140
|
+
def find_or_create_reports_discussion(issue:)
|
141
|
+
reports_discussion = existing_reports_discussion(issue: issue)
|
142
|
+
return reports_discussion if reports_discussion
|
143
|
+
|
144
|
+
gitlab.create_issue_discussion(iid: issue.iid, note: report_section_header)
|
145
|
+
end
|
146
|
+
|
147
|
+
def existing_reports_discussion(issue:)
|
148
|
+
gitlab.find_issue_discussions(iid: issue.iid).find do |discussion|
|
149
|
+
next if discussion.individual_note
|
150
|
+
next unless discussion.notes.first
|
151
|
+
|
152
|
+
discussion.notes.first.body.start_with?(report_section_header)
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
def existing_reports_note(issue:)
|
157
|
+
gitlab.find_issue_notes(iid: issue.iid).find do |note|
|
158
|
+
note.body.start_with?(report_section_header)
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
def add_report_for_test(current_reports_content:, test:)
|
163
|
+
increment_reports(
|
164
|
+
current_reports_content: current_reports_content,
|
165
|
+
test: test,
|
166
|
+
reports_section_header: report_section_header,
|
167
|
+
item_extra_content: item_extra_content(test),
|
168
|
+
reports_extra_content: reports_extra_content(test)
|
169
|
+
)
|
170
|
+
end
|
171
|
+
|
172
|
+
def found_label
|
173
|
+
if ENV.key?('CI_MERGE_REQUEST_IID')
|
174
|
+
FOUND_IN_MR_LABEL
|
175
|
+
else
|
176
|
+
FOUND_IN_MASTER_LABEL
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
def identity_labels_quick_action
|
181
|
+
return if identity_labels.empty?
|
182
|
+
|
183
|
+
%(/label #{identity_labels.map { |label| %(~"#{label}") }.join(' ')})
|
184
|
+
end
|
185
|
+
|
186
|
+
def relate_issues_quick_actions(issues)
|
187
|
+
issues.map do |issue|
|
188
|
+
"/relate #{issue.web_url}"
|
189
|
+
end.join("\n")
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
@@ -29,7 +29,7 @@ module GitlabQuality
|
|
29
29
|
def run!
|
30
30
|
puts "Reporting slow tests in MR #{merge_request_iid}"
|
31
31
|
|
32
|
-
TestResults::Builder.new(files).test_results_per_file do |test_results|
|
32
|
+
TestResults::Builder.new(file_glob: files).test_results_per_file do |test_results|
|
33
33
|
puts "=> Reporting #{test_results.count} tests in #{test_results.path}"
|
34
34
|
|
35
35
|
@slow_tests += slow_related_tests(find_slow_tests(test_results))
|
@@ -52,7 +52,7 @@ module GitlabQuality
|
|
52
52
|
def run!
|
53
53
|
puts "Reporting test failures in `#{files.join(',')}` as issues in project `#{project}` via the API at `#{Runtime::Env.gitlab_api_base}`."
|
54
54
|
|
55
|
-
TestResults::Builder.new(files).test_results_per_file do |test_results|
|
55
|
+
TestResults::Builder.new(file_glob: files).test_results_per_file do |test_results|
|
56
56
|
puts "=> Reporting #{test_results.count} tests in #{test_results.path}"
|
57
57
|
process_test_results(test_results)
|
58
58
|
end
|
@@ -10,6 +10,7 @@ module GitlabQuality
|
|
10
10
|
|
11
11
|
def initialize(token:, input_files:, related_issues_file: nil, project: nil, confidential: false, dry_run: false, **_kwargs)
|
12
12
|
@project = project
|
13
|
+
@token = token
|
13
14
|
@gitlab = (dry_run ? GitlabClient::IssuesDryClient : GitlabClient::IssuesClient).new(token: token, project: project)
|
14
15
|
@files = Array(input_files)
|
15
16
|
@confidential = confidential
|
@@ -28,7 +29,7 @@ module GitlabQuality
|
|
28
29
|
|
29
30
|
private
|
30
31
|
|
31
|
-
attr_reader :gitlab, :files, :project, :issue_type, :confidential, :issue_logger
|
32
|
+
attr_reader :gitlab, :files, :project, :issue_type, :confidential, :issue_logger, :token
|
32
33
|
|
33
34
|
def run!
|
34
35
|
raise NotImplementedError
|
@@ -120,12 +121,15 @@ module GitlabQuality
|
|
120
121
|
end
|
121
122
|
|
122
123
|
def update_issue(issue, test)
|
123
|
-
issue_attrs = {
|
124
|
-
description: new_issue_description(test)
|
125
|
-
}
|
126
|
-
return if issue.description == issue_attrs[:description]
|
124
|
+
issue_attrs = {}
|
127
125
|
|
128
|
-
|
126
|
+
new_description = new_issue_description(test)
|
127
|
+
issue_attrs[:description] = new_description unless issue.description == new_description
|
128
|
+
|
129
|
+
new_labels = up_to_date_labels(test: test, issue: issue).to_a
|
130
|
+
issue_attrs[:labels] = new_labels unless issue.labels == new_labels
|
131
|
+
|
132
|
+
gitlab.edit_issue(iid: issue.iid, options: issue_attrs) unless issue_attrs.empty?
|
129
133
|
end
|
130
134
|
|
131
135
|
def issue_labels(issue)
|
@@ -152,6 +156,8 @@ module GitlabQuality
|
|
152
156
|
labels.delete_if { |label| label.include?('quarantine') }
|
153
157
|
end
|
154
158
|
|
159
|
+
labels << 'rspec-shared-examples' if test.calls_shared_examples?
|
160
|
+
|
155
161
|
labels
|
156
162
|
end
|
157
163
|
|
@@ -36,7 +36,7 @@ module GitlabQuality
|
|
36
36
|
puts "Reporting test results in `#{files.join(',')}` as test cases in project `#{test_case_project}` " \
|
37
37
|
"and issues in project `#{results_issue_project}` via the API at `#{Runtime::Env.gitlab_api_base}`."
|
38
38
|
|
39
|
-
TestResults::Builder.new(files).test_results_per_file do |test_results|
|
39
|
+
TestResults::Builder.new(file_glob: files).test_results_per_file do |test_results|
|
40
40
|
puts "Reporting tests in #{test_results.path}"
|
41
41
|
|
42
42
|
test_results.each do |test|
|
@@ -9,15 +9,9 @@ module GitlabQuality
|
|
9
9
|
# - Takes a project where slow issues should be created
|
10
10
|
# - Find issue by title (with test description or test file)
|
11
11
|
# - Add test metadata, duration to the issue with group and category labels
|
12
|
-
class SlowTestIssue <
|
13
|
-
include Concerns::GroupAndCategoryLabels
|
14
|
-
include Concerns::IssueReports
|
15
|
-
|
12
|
+
class SlowTestIssue < HealthProblemReporter
|
16
13
|
IDENTITY_LABELS = ['test', 'rspec:slow test', 'rspec profiling', 'automation:bot-authored'].freeze
|
17
14
|
NEW_ISSUE_LABELS = Set.new(['test', 'type::maintenance', 'maintenance::performance', 'priority::3', 'severity::3']).freeze
|
18
|
-
SEARCH_LABELS = ['test'].freeze
|
19
|
-
FOUND_IN_MR_LABEL = '~"found:in MR"'
|
20
|
-
FOUND_IN_MASTER_LABEL = '~"found:master"'
|
21
15
|
REPORT_SECTION_HEADER = '### Slowness reports'
|
22
16
|
REPORTS_DOCUMENTATION = <<~DOC
|
23
17
|
Slow tests were detected, please see the [test speed best practices guide](https://docs.gitlab.com/ee/development/testing_guide/best_practices.html#test-speed)
|
@@ -28,104 +22,28 @@ module GitlabQuality
|
|
28
22
|
|
29
23
|
private
|
30
24
|
|
31
|
-
def
|
32
|
-
|
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
|
-
process_test_results(test_results)
|
38
|
-
end
|
39
|
-
end
|
40
|
-
|
41
|
-
def process_test_results(test_results)
|
42
|
-
test_results.each do |test|
|
43
|
-
next unless test.slow_test?
|
44
|
-
|
45
|
-
puts " => Reporting slowness for test '#{test.name}'..."
|
46
|
-
|
47
|
-
issues = find_issues_by_hash(test_hash(test), state: 'opened', labels: SEARCH_LABELS)
|
48
|
-
|
49
|
-
if issues.empty?
|
50
|
-
issues << create_issue(test)
|
51
|
-
else
|
52
|
-
# Keep issues description up-to-date
|
53
|
-
update_issues(issues, test)
|
54
|
-
end
|
55
|
-
|
56
|
-
update_reports(issues, test)
|
57
|
-
collect_issues(test, issues)
|
58
|
-
end
|
25
|
+
def problem_type
|
26
|
+
'slow'
|
59
27
|
end
|
60
28
|
|
61
29
|
def test_is_applicable?(test)
|
62
30
|
test.slow_test?
|
63
31
|
end
|
64
32
|
|
65
|
-
def
|
66
|
-
|
67
|
-
puts " => Adding the slow test to the existing issue: #{issue.web_url}"
|
68
|
-
add_report_to_issue(issue: issue, test: test, related_issues: (issues - [issue]))
|
69
|
-
end
|
33
|
+
def identity_labels
|
34
|
+
IDENTITY_LABELS
|
70
35
|
end
|
71
36
|
|
72
|
-
def
|
73
|
-
|
74
|
-
new_reports_list = add_report_for_test(current_reports_content: current_reports_note&.body.to_s, test: test)
|
75
|
-
|
76
|
-
note_body = [
|
77
|
-
new_reports_list.to_s,
|
78
|
-
slowness_status_labels_quick_action(new_reports_list.reports_count),
|
79
|
-
identity_labels_quick_action,
|
80
|
-
relate_issues_quick_actions(related_issues)
|
81
|
-
].join("\n")
|
82
|
-
|
83
|
-
if current_reports_note
|
84
|
-
gitlab.edit_issue_note(
|
85
|
-
issue_iid: issue.iid,
|
86
|
-
note_id: current_reports_note.id,
|
87
|
-
note: note_body
|
88
|
-
)
|
89
|
-
else
|
90
|
-
gitlab.create_issue_note(iid: issue.iid, note: note_body)
|
91
|
-
end
|
92
|
-
end
|
93
|
-
|
94
|
-
def existing_reports_note(issue:)
|
95
|
-
gitlab.find_issue_notes(iid: issue.iid).find do |note|
|
96
|
-
note.body.start_with?(REPORT_SECTION_HEADER)
|
97
|
-
end
|
37
|
+
def report_section_header
|
38
|
+
REPORT_SECTION_HEADER
|
98
39
|
end
|
99
40
|
|
100
|
-
def
|
101
|
-
|
102
|
-
current_reports_content: current_reports_content,
|
103
|
-
test: test,
|
104
|
-
reports_section_header: REPORT_SECTION_HEADER,
|
105
|
-
item_extra_content: "(#{test.run_time} seconds) #{found_label}",
|
106
|
-
reports_extra_content: REPORTS_DOCUMENTATION
|
107
|
-
)
|
41
|
+
def reports_extra_content(_test)
|
42
|
+
REPORTS_DOCUMENTATION
|
108
43
|
end
|
109
44
|
|
110
|
-
def
|
111
|
-
|
112
|
-
FOUND_IN_MR_LABEL
|
113
|
-
else
|
114
|
-
FOUND_IN_MASTER_LABEL
|
115
|
-
end
|
116
|
-
end
|
117
|
-
|
118
|
-
# The report count is based on the percentiles of slowness issues.
|
119
|
-
#
|
120
|
-
# See https://gitlab.com/gitlab-org/quality/engineering-productivity/team/-/blob/main/scripts/unhealthy_test_issues_statistics.rb
|
121
|
-
# to gather these statistics.
|
122
|
-
#
|
123
|
-
# P75 => slowness::4
|
124
|
-
# P90 => slowness::3
|
125
|
-
# P95 => slowness::2
|
126
|
-
# Above P95 => slowness::1
|
127
|
-
def slowness_status_labels_quick_action(reports_count)
|
128
|
-
case reports_count
|
45
|
+
def health_problem_status_label_quick_action(reports_list)
|
46
|
+
case reports_list.reports_count
|
129
47
|
when 40..Float::INFINITY
|
130
48
|
'/label ~"slowness::1"'
|
131
49
|
when 28..39
|
@@ -137,15 +55,8 @@ module GitlabQuality
|
|
137
55
|
end
|
138
56
|
end
|
139
57
|
|
140
|
-
def
|
141
|
-
|
142
|
-
%(/label #{labels_list})
|
143
|
-
end
|
144
|
-
|
145
|
-
def relate_issues_quick_actions(issues)
|
146
|
-
issues.map do |issue|
|
147
|
-
"/relate #{issue.web_url}"
|
148
|
-
end.join("\n")
|
58
|
+
def item_extra_content(test)
|
59
|
+
"(#{test.run_time} seconds) #{found_label}"
|
149
60
|
end
|
150
61
|
end
|
151
62
|
end
|
@@ -6,15 +6,16 @@ require 'table_print'
|
|
6
6
|
module GitlabQuality
|
7
7
|
module TestTooling
|
8
8
|
class SummaryTable
|
9
|
-
def self.create(input_files
|
10
|
-
"```\n#{TablePrint::Printer.table_print(collect_results(input_files))}```\n"
|
9
|
+
def self.create(input_files:, **options)
|
10
|
+
"```\n#{TablePrint::Printer.table_print(collect_results(input_files, **options))}```\n"
|
11
11
|
end
|
12
12
|
|
13
13
|
# rubocop:disable Metrics/AbcSize
|
14
|
-
def self.collect_results(input_files)
|
15
|
-
|
14
|
+
def self.collect_results(input_files, **options)
|
15
|
+
sort_by = options[:sort_by]
|
16
|
+
sort_direction = options[:sort_direction]
|
16
17
|
|
17
|
-
Dir.glob(input_files).
|
18
|
+
stage_wise_results = Dir.glob(input_files).each_with_object([]) do |report_file, stage_wise_results|
|
18
19
|
stage_hash = {}
|
19
20
|
stage_hash["Dev Stage"] = File.basename(report_file, ".*").capitalize
|
20
21
|
|
@@ -29,6 +30,9 @@ module GitlabQuality
|
|
29
30
|
stage_wise_results << stage_hash
|
30
31
|
end
|
31
32
|
|
33
|
+
stage_wise_results.sort_by! { |stage_hash| stage_hash[sort_by] } if sort_by
|
34
|
+
stage_wise_results.reverse! if sort_direction == :desc
|
35
|
+
|
32
36
|
stage_wise_results
|
33
37
|
end
|
34
38
|
# rubocop:enable Metrics/AbcSize
|
@@ -1,5 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'time'
|
4
|
+
|
3
5
|
module GitlabQuality
|
4
6
|
module TestTooling
|
5
7
|
module TestMetricsExporter
|
@@ -11,7 +13,7 @@ module GitlabQuality
|
|
11
13
|
return @time if defined?(@time)
|
12
14
|
|
13
15
|
created_at = Time.strptime(env('CI_PIPELINE_CREATED_AT'), '%Y-%m-%dT%H:%M:%S%z') if env('CI_PIPELINE_CREATED_AT')
|
14
|
-
@time = (created_at || Time.now).utc.strftime('%Y-%m-%
|
16
|
+
@time = Time.parse((created_at || Time.now).utc.strftime('%Y-%m-%d %H:%M:%S %z'))
|
15
17
|
end
|
16
18
|
|
17
19
|
# rubocop:disable Metrics/AbcSize
|
@@ -9,10 +9,15 @@ module GitlabQuality
|
|
9
9
|
'403 Forbidden - Your account has been blocked'
|
10
10
|
].freeze
|
11
11
|
|
12
|
+
SHARED_EXAMPLES_CALLERS = %w[include_examples it_behaves_like].freeze
|
13
|
+
|
12
14
|
attr_reader :report
|
13
15
|
|
14
|
-
def initialize(report)
|
16
|
+
def initialize(report:, token: nil, project: nil, ref: 'master')
|
15
17
|
@report = report
|
18
|
+
@token = token
|
19
|
+
@project = project
|
20
|
+
@ref = ref
|
16
21
|
end
|
17
22
|
|
18
23
|
def stage
|
@@ -27,6 +32,10 @@ module GitlabQuality
|
|
27
32
|
raise NotImplementedError
|
28
33
|
end
|
29
34
|
|
35
|
+
def line_number
|
36
|
+
raise NotImplementedError
|
37
|
+
end
|
38
|
+
|
30
39
|
def section
|
31
40
|
raise NotImplementedError
|
32
41
|
end
|
@@ -57,6 +66,26 @@ module GitlabQuality
|
|
57
66
|
return message_lines.empty? ? message : message_lines.join("\n")
|
58
67
|
end
|
59
68
|
end
|
69
|
+
|
70
|
+
def calls_shared_examples?
|
71
|
+
reported_line = files_client.file_contents_at_line(line_number)
|
72
|
+
|
73
|
+
return false unless reported_line
|
74
|
+
|
75
|
+
SHARED_EXAMPLES_CALLERS.any? { |caller_method| reported_line.strip.start_with?(caller_method) }
|
76
|
+
end
|
77
|
+
|
78
|
+
def files_client
|
79
|
+
@files_client ||= GitlabClient::RepositoryFilesClient.new(
|
80
|
+
token: token,
|
81
|
+
project: project,
|
82
|
+
file_path: file,
|
83
|
+
ref: ref)
|
84
|
+
end
|
85
|
+
|
86
|
+
private
|
87
|
+
|
88
|
+
attr_reader :token, :project, :ref
|
60
89
|
end
|
61
90
|
end
|
62
91
|
end
|
@@ -8,8 +8,11 @@ module GitlabQuality
|
|
8
8
|
|
9
9
|
attr_reader :path
|
10
10
|
|
11
|
-
def initialize(path)
|
11
|
+
def initialize(path:, token: nil, project: nil, ref: 'master')
|
12
12
|
@path = path
|
13
|
+
@token = token
|
14
|
+
@project = project
|
15
|
+
@ref = ref
|
13
16
|
@results = parse
|
14
17
|
@testcases = process
|
15
18
|
end
|
@@ -24,7 +27,7 @@ module GitlabQuality
|
|
24
27
|
|
25
28
|
private
|
26
29
|
|
27
|
-
attr_reader :results, :testcases
|
30
|
+
attr_reader :results, :testcases, :token, :project, :ref
|
28
31
|
|
29
32
|
def parse
|
30
33
|
raise NotImplementedError
|
@@ -4,8 +4,11 @@ module GitlabQuality
|
|
4
4
|
module TestTooling
|
5
5
|
module TestResults
|
6
6
|
class Builder
|
7
|
-
def initialize(file_glob)
|
7
|
+
def initialize(file_glob:, token: nil, project: nil, ref: 'master')
|
8
8
|
@file_glob = file_glob
|
9
|
+
@token = token
|
10
|
+
@project = project
|
11
|
+
@ref = ref
|
9
12
|
end
|
10
13
|
|
11
14
|
def test_results_per_file
|
@@ -15,9 +18,9 @@ module GitlabQuality
|
|
15
18
|
test_results =
|
16
19
|
case extension
|
17
20
|
when '.json'
|
18
|
-
TestResults::JsonTestResults.new(path)
|
21
|
+
TestResults::JsonTestResults.new(path: path, token: token, project: project, ref: ref)
|
19
22
|
when '.xml'
|
20
|
-
TestResults::JUnitTestResults.new(path)
|
23
|
+
TestResults::JUnitTestResults.new(path: path, token: token, project: project, ref: ref)
|
21
24
|
else
|
22
25
|
raise "Unknown extension #{extension}"
|
23
26
|
end
|
@@ -28,7 +31,7 @@ module GitlabQuality
|
|
28
31
|
|
29
32
|
private
|
30
33
|
|
31
|
-
attr_reader :file_glob
|
34
|
+
attr_reader :file_glob, :token, :project, :ref
|
32
35
|
end
|
33
36
|
end
|
34
37
|
end
|
@@ -18,7 +18,7 @@ module GitlabQuality
|
|
18
18
|
|
19
19
|
def process
|
20
20
|
results.xpath('//testcase').map do |test|
|
21
|
-
GitlabQuality::TestTooling::TestResult::JUnitTestResult.new(test)
|
21
|
+
GitlabQuality::TestTooling::TestResult::JUnitTestResult.new(report: test, project: project, token: token)
|
22
22
|
end
|
23
23
|
end
|
24
24
|
end
|
@@ -20,7 +20,7 @@ module GitlabQuality
|
|
20
20
|
|
21
21
|
def process
|
22
22
|
results['examples'].map do |test|
|
23
|
-
GitlabQuality::TestTooling::TestResult::JsonTestResult.new(test)
|
23
|
+
GitlabQuality::TestTooling::TestResult::JsonTestResult.new(report: test, project: project, token: token)
|
24
24
|
end
|
25
25
|
end
|
26
26
|
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.29.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-
|
11
|
+
date: 2024-06-18 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: climate_control
|
@@ -440,6 +440,7 @@ files:
|
|
440
440
|
- lib/gitlab_quality/test_tooling/report/failed_test_issue.rb
|
441
441
|
- lib/gitlab_quality/test_tooling/report/flaky_test_issue.rb
|
442
442
|
- lib/gitlab_quality/test_tooling/report/generate_test_session.rb
|
443
|
+
- lib/gitlab_quality/test_tooling/report/health_problem_reporter.rb
|
443
444
|
- lib/gitlab_quality/test_tooling/report/issue_logger.rb
|
444
445
|
- lib/gitlab_quality/test_tooling/report/knapsack_report_issue.rb
|
445
446
|
- lib/gitlab_quality/test_tooling/report/merge_request_slow_tests_report.rb
|