gitlab_quality-test_tooling 3.0.0 → 3.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Gemfile.lock +2 -4
- data/README.md +47 -14
- data/exe/sync-category-owners +95 -0
- data/exe/test-coverage +59 -15
- data/lib/gitlab_quality/test_tooling/code_coverage/README.md +162 -0
- data/lib/gitlab_quality/test_tooling/code_coverage/artifacts.rb +5 -2
- data/lib/gitlab_quality/test_tooling/code_coverage/category_owners.rb +32 -28
- data/lib/gitlab_quality/test_tooling/code_coverage/click_house/category_owners_table.rb +102 -35
- data/lib/gitlab_quality/test_tooling/code_coverage/click_house/coverage_metrics_table.rb +44 -37
- data/lib/gitlab_quality/test_tooling/code_coverage/click_house/table.rb +17 -0
- data/lib/gitlab_quality/test_tooling/code_coverage/click_house/test_file_mappings_table.rb +52 -0
- data/lib/gitlab_quality/test_tooling/code_coverage/coverage_data.rb +77 -34
- data/lib/gitlab_quality/test_tooling/code_coverage/lcov_file.rb +11 -1
- data/lib/gitlab_quality/test_tooling/code_coverage/responsibility_classifier.rb +47 -0
- data/lib/gitlab_quality/test_tooling/code_coverage/responsibility_patterns_config.rb +46 -0
- data/lib/gitlab_quality/test_tooling/code_coverage/test_file_mapping_data.rb +46 -0
- data/lib/gitlab_quality/test_tooling/report/results_in_test_cases.rb +2 -4
- data/lib/gitlab_quality/test_tooling/runtime/env.rb +4 -0
- data/lib/gitlab_quality/test_tooling/test_meta/processor/meta_processor.rb +1 -1
- data/lib/gitlab_quality/test_tooling/test_meta/test_meta_updater.rb +4 -4
- data/lib/gitlab_quality/test_tooling/test_metrics_exporter/config.rb +8 -0
- data/lib/gitlab_quality/test_tooling/test_metrics_exporter/test_metrics.rb +36 -10
- data/lib/gitlab_quality/test_tooling/test_metrics_exporter/utils.rb +2 -0
- data/lib/gitlab_quality/test_tooling/test_quarantine/quarantine_formatter.rb +38 -0
- data/lib/gitlab_quality/test_tooling/test_quarantine/quarantine_helper.rb +76 -0
- data/lib/gitlab_quality/test_tooling/test_result/base_test_result.rb +15 -1
- data/lib/gitlab_quality/test_tooling/version.rb +1 -1
- metadata +11 -28
- data/exe/existing-test-health-issue +0 -59
- data/exe/generate-test-session +0 -70
- data/lib/gitlab_quality/test_tooling/report/generate_test_session.rb +0 -288
- data/lib/gitlab_quality/test_tooling/report/test_health_issue_finder.rb +0 -79
|
@@ -1,288 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'erb'
|
|
4
|
-
require 'date'
|
|
5
|
-
|
|
6
|
-
module GitlabQuality
|
|
7
|
-
module TestTooling
|
|
8
|
-
module Report
|
|
9
|
-
class GenerateTestSession < ReportAsIssue
|
|
10
|
-
def initialize(ci_project_token:, pipeline_stages: nil, **kwargs)
|
|
11
|
-
super
|
|
12
|
-
@ci_project_token = ci_project_token
|
|
13
|
-
@pipeline_stages = Set.new(pipeline_stages)
|
|
14
|
-
@issue_type = 'issue'
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
private
|
|
18
|
-
|
|
19
|
-
attr_reader :ci_project_token, :pipeline_stages
|
|
20
|
-
|
|
21
|
-
# rubocop:disable Metrics/AbcSize
|
|
22
|
-
def run!
|
|
23
|
-
puts "Generating test results in `#{files.join(',')}` as issues in project `#{project}` via the API at `#{Runtime::Env.gitlab_api_base}`."
|
|
24
|
-
|
|
25
|
-
tests = Dir.glob(files).flat_map do |path|
|
|
26
|
-
puts "Loading tests in #{path}"
|
|
27
|
-
|
|
28
|
-
TestResults::JsonTestResults.new(path: path).to_a
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
tests = tests.select { |test| pipeline_stages.include? test.report["stage"] } unless pipeline_stages.empty?
|
|
32
|
-
|
|
33
|
-
issue = gitlab.create_issue(
|
|
34
|
-
title: "#{Time.now.to_date.iso8601} Test session report | #{Runtime::Env.qa_run_type}",
|
|
35
|
-
description: generate_description(tests),
|
|
36
|
-
labels: ['automation:bot-authored', 'E2E', 'triage report', pipeline_name_label, 'suppress-contributor-links'],
|
|
37
|
-
confidential: confidential
|
|
38
|
-
)
|
|
39
|
-
|
|
40
|
-
# Workaround for https://gitlab.com/gitlab-org/gitlab/-/issues/295493
|
|
41
|
-
unless Runtime::Env.qa_issue_url.to_s.empty?
|
|
42
|
-
gitlab.create_issue_note(
|
|
43
|
-
iid: issue.iid,
|
|
44
|
-
note: "/relate #{Runtime::Env.qa_issue_url}")
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
issue&.web_url # Issue isn't created in dry-run mode
|
|
48
|
-
end
|
|
49
|
-
# rubocop:enable Metrics/AbcSize
|
|
50
|
-
|
|
51
|
-
def generate_description(tests)
|
|
52
|
-
<<~MARKDOWN.rstrip
|
|
53
|
-
## Session summary
|
|
54
|
-
|
|
55
|
-
* Deploy version: #{Runtime::Env.deploy_version}
|
|
56
|
-
* Deploy environment: #{Runtime::Env.deploy_environment}
|
|
57
|
-
* Pipeline: #{Runtime::Env.pipeline_from_project_name} [#{Runtime::Env.ci_pipeline_id}](#{Runtime::Env.ci_pipeline_url})
|
|
58
|
-
#{generate_summary(tests: tests)}
|
|
59
|
-
|
|
60
|
-
#{generate_failed_jobs_listing}
|
|
61
|
-
|
|
62
|
-
#{generate_stages_listing(tests)}
|
|
63
|
-
|
|
64
|
-
#{generate_qa_issue_relation}
|
|
65
|
-
|
|
66
|
-
#{generate_link_to_dashboard}
|
|
67
|
-
MARKDOWN
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
def generate_summary(tests:, tests_by_status: nil)
|
|
71
|
-
tests_by_status ||= tests.group_by(&:status)
|
|
72
|
-
total = tests.size
|
|
73
|
-
passed = tests_by_status['passed']&.size || 0
|
|
74
|
-
failed = tests_by_status['failed']&.size || 0
|
|
75
|
-
others = total - passed - failed
|
|
76
|
-
|
|
77
|
-
<<~MARKDOWN.chomp
|
|
78
|
-
* Total #{total} tests
|
|
79
|
-
* Passed #{passed} tests
|
|
80
|
-
* Failed #{failed} tests
|
|
81
|
-
* #{others} other tests (usually skipped)
|
|
82
|
-
MARKDOWN
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
def generate_failed_jobs_listing
|
|
86
|
-
failed_jobs = fetch_pipeline_failed_jobs
|
|
87
|
-
listings = failed_jobs.filter_map do |job|
|
|
88
|
-
next if pipeline_stages.any? && !pipeline_stages.include?(job.stage)
|
|
89
|
-
|
|
90
|
-
allowed_to_fail = ' (allowed to fail)' if job.allow_failure
|
|
91
|
-
|
|
92
|
-
"* [#{job.name}](#{job.web_url})#{allowed_to_fail}"
|
|
93
|
-
end.join("\n")
|
|
94
|
-
|
|
95
|
-
<<~MARKDOWN.chomp if failed_jobs.any?
|
|
96
|
-
## Failed jobs
|
|
97
|
-
|
|
98
|
-
#{listings}
|
|
99
|
-
MARKDOWN
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
def generate_stages_listing(tests)
|
|
103
|
-
generate_tests_by_stage(tests).map do |stage, tests_for_stage|
|
|
104
|
-
tests_by_status = tests_for_stage.group_by(&:status)
|
|
105
|
-
|
|
106
|
-
<<~MARKDOWN.chomp
|
|
107
|
-
### #{stage&.capitalize || 'Unknown'}
|
|
108
|
-
|
|
109
|
-
#{generate_summary(
|
|
110
|
-
tests: tests_for_stage, tests_by_status: tests_by_status)}
|
|
111
|
-
|
|
112
|
-
#{generate_testcase_listing_by_status(
|
|
113
|
-
tests: tests_for_stage, tests_by_status: tests_by_status)}
|
|
114
|
-
MARKDOWN
|
|
115
|
-
end.join("\n\n")
|
|
116
|
-
end
|
|
117
|
-
|
|
118
|
-
def generate_tests_by_stage(tests)
|
|
119
|
-
# https://about.gitlab.com/handbook/product/product-categories/#devops-stages
|
|
120
|
-
ordering = %w[
|
|
121
|
-
manage
|
|
122
|
-
plan
|
|
123
|
-
create
|
|
124
|
-
verify
|
|
125
|
-
package
|
|
126
|
-
release
|
|
127
|
-
configure
|
|
128
|
-
monitor
|
|
129
|
-
secure
|
|
130
|
-
defend
|
|
131
|
-
growth
|
|
132
|
-
fulfillment
|
|
133
|
-
enablement
|
|
134
|
-
self-managed
|
|
135
|
-
saas
|
|
136
|
-
]
|
|
137
|
-
|
|
138
|
-
tests.sort_by do |test|
|
|
139
|
-
ordering.index(test.stage) || ordering.size
|
|
140
|
-
end.group_by(&:stage)
|
|
141
|
-
end
|
|
142
|
-
|
|
143
|
-
def generate_testcase_listing_by_status(tests:, tests_by_status:)
|
|
144
|
-
failed_tests = tests_by_status['failed']
|
|
145
|
-
passed_tests = tests_by_status['passed']
|
|
146
|
-
other_tests = tests.reject do |test|
|
|
147
|
-
test.status == 'failed' || test.status == 'passed'
|
|
148
|
-
end
|
|
149
|
-
|
|
150
|
-
[
|
|
151
|
-
(failed_listings(failed_tests) if failed_tests),
|
|
152
|
-
(passed_listings(passed_tests) if passed_tests),
|
|
153
|
-
(other_listings(other_tests) if other_tests.any?)
|
|
154
|
-
].compact.join("\n\n")
|
|
155
|
-
end
|
|
156
|
-
|
|
157
|
-
def failed_listings(failed_tests)
|
|
158
|
-
generate_testcase_listing(failed_tests)
|
|
159
|
-
end
|
|
160
|
-
|
|
161
|
-
def passed_listings(passed_tests)
|
|
162
|
-
<<~MARKDOWN.chomp
|
|
163
|
-
<details><summary>Passed tests:</summary>
|
|
164
|
-
|
|
165
|
-
#{generate_testcase_listing(passed_tests, passed: true)}
|
|
166
|
-
|
|
167
|
-
</details>
|
|
168
|
-
MARKDOWN
|
|
169
|
-
end
|
|
170
|
-
|
|
171
|
-
def other_listings(other_tests)
|
|
172
|
-
<<~MARKDOWN.chomp
|
|
173
|
-
<details><summary>Other tests:</summary>
|
|
174
|
-
|
|
175
|
-
#{generate_testcase_listing(other_tests)}
|
|
176
|
-
|
|
177
|
-
</details>
|
|
178
|
-
MARKDOWN
|
|
179
|
-
end
|
|
180
|
-
|
|
181
|
-
def generate_testcase_listing(tests, passed: false)
|
|
182
|
-
body = tests.group_by(&:testcase).map do |testcase, tests_with_same_testcase|
|
|
183
|
-
tests_with_same_testcase.sort_by!(&:name)
|
|
184
|
-
[
|
|
185
|
-
generate_test_text(testcase, tests_with_same_testcase, passed),
|
|
186
|
-
generate_test_job(tests_with_same_testcase),
|
|
187
|
-
generate_test_status(tests_with_same_testcase),
|
|
188
|
-
generate_test_actions(tests_with_same_testcase)
|
|
189
|
-
].join(' | ')
|
|
190
|
-
end.join("\n")
|
|
191
|
-
|
|
192
|
-
<<~MARKDOWN.chomp
|
|
193
|
-
| Test | Job | Status | Action |
|
|
194
|
-
| - | - | - | - |
|
|
195
|
-
#{body}
|
|
196
|
-
MARKDOWN
|
|
197
|
-
end
|
|
198
|
-
|
|
199
|
-
def generate_test_text(testcase, tests_with_same_testcase, passed)
|
|
200
|
-
text = tests_with_same_testcase.map(&:name).uniq.join(', ')
|
|
201
|
-
|
|
202
|
-
if testcase && !passed
|
|
203
|
-
"[#{text}](#{testcase})"
|
|
204
|
-
else
|
|
205
|
-
text
|
|
206
|
-
end
|
|
207
|
-
end
|
|
208
|
-
|
|
209
|
-
def generate_test_job(tests_with_same_testcase)
|
|
210
|
-
tests_with_same_testcase.map do |test|
|
|
211
|
-
ci_job_id = test.ci_job_url[/\d+\z/]
|
|
212
|
-
|
|
213
|
-
"[#{ci_job_id}](#{test.ci_job_url})#{' ~"quarantine"' if test.quarantine?}"
|
|
214
|
-
end.uniq.join(', ')
|
|
215
|
-
end
|
|
216
|
-
|
|
217
|
-
def generate_test_status(tests_with_same_testcase)
|
|
218
|
-
tests_with_same_testcase.map(&:status).uniq.map do |status|
|
|
219
|
-
%(~"#{status}")
|
|
220
|
-
end.join(', ')
|
|
221
|
-
end
|
|
222
|
-
|
|
223
|
-
def generate_test_actions(tests_with_same_testcase)
|
|
224
|
-
# All failed tests would be grouped together, meaning that
|
|
225
|
-
# if one failed, all the tests here would be failed too.
|
|
226
|
-
# So this check is safe. Same applies to 'passed'.
|
|
227
|
-
# But all other status might be mixing together,
|
|
228
|
-
# we cannot assume other statuses.
|
|
229
|
-
if tests_with_same_testcase.first.status == 'failed'
|
|
230
|
-
tests_having_failure_issue =
|
|
231
|
-
tests_with_same_testcase.select(&:failure_issue)
|
|
232
|
-
|
|
233
|
-
if tests_having_failure_issue.any?
|
|
234
|
-
items = tests_having_failure_issue.uniq(&:failure_issue).map do |test|
|
|
235
|
-
"<li>[ ] [failure issue](#{test.failure_issue})</li>"
|
|
236
|
-
end.join(' ')
|
|
237
|
-
|
|
238
|
-
"<ul>#{items}</ul>"
|
|
239
|
-
else
|
|
240
|
-
'<ul><li>[ ] failure issue exists or was created</li></ul>'
|
|
241
|
-
end
|
|
242
|
-
else
|
|
243
|
-
'-'
|
|
244
|
-
end
|
|
245
|
-
end
|
|
246
|
-
|
|
247
|
-
def generate_qa_issue_relation
|
|
248
|
-
return unless Runtime::Env.qa_issue_url
|
|
249
|
-
|
|
250
|
-
<<~MARKDOWN.chomp
|
|
251
|
-
## Release QA issue
|
|
252
|
-
|
|
253
|
-
* #{Runtime::Env.qa_issue_url}
|
|
254
|
-
|
|
255
|
-
/relate #{Runtime::Env.qa_issue_url}
|
|
256
|
-
MARKDOWN
|
|
257
|
-
end
|
|
258
|
-
|
|
259
|
-
def generate_link_to_dashboard
|
|
260
|
-
return unless Runtime::Env.qa_run_type
|
|
261
|
-
|
|
262
|
-
<<~MARKDOWN.chomp
|
|
263
|
-
## Link to Grafana dashboard for run-type of #{Runtime::Env.qa_run_type}
|
|
264
|
-
|
|
265
|
-
* https://dashboards.quality.gitlab.net/d/tR_SmBDVk/main-runs?orgId=1&refresh=1m&var-run_type=#{Runtime::Env.qa_run_type}
|
|
266
|
-
MARKDOWN
|
|
267
|
-
end
|
|
268
|
-
|
|
269
|
-
def fetch_pipeline_failed_jobs
|
|
270
|
-
failed_jobs = []
|
|
271
|
-
|
|
272
|
-
ci_project_client = Gitlab.client(
|
|
273
|
-
endpoint: Runtime::Env.ci_api_v4_url,
|
|
274
|
-
private_token: ci_project_token)
|
|
275
|
-
|
|
276
|
-
gitlab.handle_gitlab_client_exceptions do
|
|
277
|
-
failed_jobs = ci_project_client.pipeline_jobs(
|
|
278
|
-
Runtime::Env.ci_project_id,
|
|
279
|
-
Runtime::Env.ci_pipeline_id,
|
|
280
|
-
scope: 'failed')
|
|
281
|
-
end
|
|
282
|
-
|
|
283
|
-
failed_jobs
|
|
284
|
-
end
|
|
285
|
-
end
|
|
286
|
-
end
|
|
287
|
-
end
|
|
288
|
-
end
|
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'http'
|
|
4
|
-
require 'json'
|
|
5
|
-
|
|
6
|
-
module GitlabQuality
|
|
7
|
-
module TestTooling
|
|
8
|
-
module Report
|
|
9
|
-
class TestHealthIssueFinder < ReportAsIssue
|
|
10
|
-
HEALTH_PROBLEM_TYPE_TO_LABEL = {
|
|
11
|
-
'pass-after-retry' => 'test-health:pass-after-retry',
|
|
12
|
-
'slow' => 'test-health:slow',
|
|
13
|
-
'failures' => 'test-health:failures'
|
|
14
|
-
}.freeze
|
|
15
|
-
|
|
16
|
-
def initialize(health_problem_type: [], **kwargs)
|
|
17
|
-
super(**kwargs)
|
|
18
|
-
|
|
19
|
-
@health_problem_type = health_problem_type
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
def found_existing_unhealthy_test_issue?
|
|
23
|
-
issue_url = invoke!
|
|
24
|
-
|
|
25
|
-
!issue_url.nil? && !issue_url.empty?
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
def run!
|
|
29
|
-
existing_issue_found = nil
|
|
30
|
-
|
|
31
|
-
applicable_tests.each do |test|
|
|
32
|
-
issues = find_issues_by_hash(test_hash(test), state: 'opened', labels: search_labels)
|
|
33
|
-
next if issues.empty?
|
|
34
|
-
|
|
35
|
-
existing_issue_found = issues.first.web_url
|
|
36
|
-
puts "Found an existing test health issue of type #{health_problem_type} for test #{test.file}:#{test.line_number}: #{existing_issue_found}."
|
|
37
|
-
break
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
puts "Did not find an existing test health issue of type #{health_problem_type}." unless existing_issue_found
|
|
41
|
-
|
|
42
|
-
existing_issue_found
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
def applicable_tests
|
|
46
|
-
applicable_tests = []
|
|
47
|
-
|
|
48
|
-
TestResults::Builder.new(file_glob: files, token: token, project: project).test_results_per_file do |test_results|
|
|
49
|
-
applicable_tests = test_results.select { |test| test_is_applicable?(test) }
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
applicable_tests
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
private
|
|
56
|
-
|
|
57
|
-
attr_reader :health_problem_type
|
|
58
|
-
|
|
59
|
-
# Be mindful about the number of tests this method would return,
|
|
60
|
-
# as we will make at least one API request per test.
|
|
61
|
-
def test_is_applicable?(test)
|
|
62
|
-
expected_test_status =
|
|
63
|
-
case health_problem_type
|
|
64
|
-
when 'failures'
|
|
65
|
-
'failed'
|
|
66
|
-
else
|
|
67
|
-
'passed'
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
test.status == expected_test_status
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
def search_labels
|
|
74
|
-
['test', HEALTH_PROBLEM_TYPE_TO_LABEL[health_problem_type]]
|
|
75
|
-
end
|
|
76
|
-
end
|
|
77
|
-
end
|
|
78
|
-
end
|
|
79
|
-
end
|