gitlab-qa 10.3.0 → 12.2.1
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/.gitignore +1 -1
- data/.gitlab/changelog_config.yml +13 -0
- data/.gitlab/merge_request_templates/Release.md +13 -34
- data/.gitlab-ci.yml +3 -1
- data/.rubocop.yml +13 -2
- data/.rubocop_todo.yml +57 -97
- data/Dangerfile +1 -5
- data/Gemfile.lock +47 -39
- data/README.md +1 -2
- data/docs/release_process.md +1 -1
- data/docs/running_against_remote_grid.md +42 -3
- data/docs/what_tests_can_be_run.md +5 -2
- data/gitlab-qa.gemspec +5 -3
- data/lib/gitlab/qa/component/base.rb +9 -9
- data/lib/gitlab/qa/component/gitaly.rb +7 -5
- data/lib/gitlab/qa/component/gitaly_cluster.rb +15 -9
- data/lib/gitlab/qa/component/gitlab.rb +48 -21
- data/lib/gitlab/qa/component/mail_hog.rb +1 -0
- data/lib/gitlab/qa/component/praefect.rb +41 -31
- data/lib/gitlab/qa/component/selenoid.rb +14 -7
- data/lib/gitlab/qa/component/specs.rb +11 -5
- data/lib/gitlab/qa/component/staging.rb +4 -4
- data/lib/gitlab/qa/component/telegraf.rb +2 -1
- data/lib/gitlab/qa/docker/engine.rb +6 -3
- data/lib/gitlab/qa/docker/volumes.rb +1 -1
- data/lib/gitlab/qa/release.rb +4 -4
- data/lib/gitlab/qa/runner.rb +10 -3
- data/lib/gitlab/qa/runtime/env.rb +47 -62
- data/lib/gitlab/qa/runtime/logger.rb +1 -1
- data/lib/gitlab/qa/runtime/omnibus_configuration.rb +1 -0
- data/lib/gitlab/qa/runtime/omnibus_configurations/decomposition_single_db.rb +1 -2
- data/lib/gitlab/qa/runtime/omnibus_configurations/object_storage_gcs.rb +2 -1
- data/lib/gitlab/qa/runtime/scenario.rb +1 -5
- data/lib/gitlab/qa/scenario/actable.rb +4 -4
- data/lib/gitlab/qa/scenario/test/instance/airgapped.rb +2 -4
- data/lib/gitlab/qa/scenario/test/instance/deployment_base.rb +2 -1
- data/lib/gitlab/qa/scenario/test/integration/gitaly_cluster.rb +0 -2
- data/lib/gitlab/qa/scenario/test/integration/group_saml.rb +1 -1
- data/lib/gitlab/qa/scenario/test/integration/ldap.rb +5 -6
- data/lib/gitlab/qa/scenario/test/integration/mtls.rb +20 -6
- data/lib/gitlab/qa/scenario/test/integration/oauth.rb +13 -4
- data/lib/gitlab/qa/scenario/test/integration/praefect.rb +16 -10
- data/lib/gitlab/qa/scenario/test/integration/registry_with_cdn.rb +5 -2
- data/lib/gitlab/qa/scenario/test/omnibus/update_from_previous.rb +1 -1
- data/lib/gitlab/qa/scenario/test/omnibus/upgrade.rb +1 -3
- data/lib/gitlab/qa/scenario/test/sanity/version.rb +1 -1
- data/lib/gitlab/qa/support/config_scripts.rb +1 -1
- data/lib/gitlab/qa/support/gitlab_version_info.rb +30 -10
- data/lib/gitlab/qa/support/shell_command.rb +1 -0
- data/lib/gitlab/qa/test_logger.rb +2 -2
- data/lib/gitlab/qa/version.rb +1 -1
- data/lib/gitlab/qa.rb +0 -1
- data/support/data/admin_access_token_seed.rb +5 -1
- data/support/data/license_usage_seed.rb +3 -1
- metadata +16 -45
- data/bin/slack +0 -14
- data/exe/gitlab-qa-report +0 -10
- data/lib/gitlab/qa/report/base_test_results.rb +0 -39
- data/lib/gitlab/qa/report/find_set_dri.rb +0 -43
- data/lib/gitlab/qa/report/generate_test_session.rb +0 -275
- data/lib/gitlab/qa/report/gitlab_issue_client.rb +0 -190
- data/lib/gitlab/qa/report/gitlab_issue_dry_client.rb +0 -28
- data/lib/gitlab/qa/report/j_unit_test_results.rb +0 -27
- data/lib/gitlab/qa/report/json_test_results.rb +0 -29
- data/lib/gitlab/qa/report/prepare_stage_reports.rb +0 -86
- data/lib/gitlab/qa/report/relate_failure_issue.rb +0 -374
- data/lib/gitlab/qa/report/report_as_issue.rb +0 -176
- data/lib/gitlab/qa/report/report_results.rb +0 -64
- data/lib/gitlab/qa/report/results_in_issues.rb +0 -126
- data/lib/gitlab/qa/report/results_in_testcases.rb +0 -111
- data/lib/gitlab/qa/report/results_reporter_shared.rb +0 -70
- data/lib/gitlab/qa/report/summary_table.rb +0 -43
- data/lib/gitlab/qa/report/test_result.rb +0 -184
- data/lib/gitlab/qa/report/update_screenshot_path.rb +0 -63
- data/lib/gitlab/qa/reporter.rb +0 -131
- data/lib/gitlab/qa/runtime/omnibus_configurations/packages.rb +0 -17
- data/lib/gitlab/qa/runtime/token_finder.rb +0 -44
- data/lib/gitlab/qa/slack/post_to_slack.rb +0 -30
- data/lib/gitlab/qa/system_logs/finders/json_log_finder.rb +0 -65
- data/lib/gitlab/qa/system_logs/finders/rails/api_log_finder.rb +0 -21
- data/lib/gitlab/qa/system_logs/finders/rails/application_log_finder.rb +0 -21
- data/lib/gitlab/qa/system_logs/finders/rails/exception_log_finder.rb +0 -21
- data/lib/gitlab/qa/system_logs/finders/rails/graphql_log_finder.rb +0 -21
- data/lib/gitlab/qa/system_logs/log_types/log.rb +0 -38
- data/lib/gitlab/qa/system_logs/log_types/rails/api_log.rb +0 -34
- data/lib/gitlab/qa/system_logs/log_types/rails/application_log.rb +0 -27
- data/lib/gitlab/qa/system_logs/log_types/rails/exception_log.rb +0 -23
- data/lib/gitlab/qa/system_logs/log_types/rails/graphql_log.rb +0 -30
- data/lib/gitlab/qa/system_logs/shared_fields.rb +0 -29
- data/lib/gitlab/qa/system_logs/system_logs_formatter.rb +0 -65
|
@@ -1,275 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'erb'
|
|
4
|
-
require 'date'
|
|
5
|
-
|
|
6
|
-
module Gitlab
|
|
7
|
-
module QA
|
|
8
|
-
module Report
|
|
9
|
-
class GenerateTestSession < ReportAsIssue
|
|
10
|
-
def initialize(**kwargs)
|
|
11
|
-
super
|
|
12
|
-
@issue_type = 'issue'
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
private
|
|
16
|
-
|
|
17
|
-
# rubocop:disable Metrics/AbcSize
|
|
18
|
-
def run!
|
|
19
|
-
puts "Generating test results in `#{files.join(',')}` as issues in project `#{project}` via the API at `#{Runtime::Env.gitlab_api_base}`."
|
|
20
|
-
|
|
21
|
-
tests = Dir.glob(files).flat_map do |path|
|
|
22
|
-
puts "Loading tests in #{path}"
|
|
23
|
-
|
|
24
|
-
Report::JsonTestResults.new(path).to_a
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
issue = gitlab.create_issue(
|
|
28
|
-
title: "#{Time.now.strftime('%Y-%m-%d')} Test session report | #{Runtime::Env.qa_run_type}",
|
|
29
|
-
description: generate_description(tests),
|
|
30
|
-
labels: ['Quality', 'QA', 'triage report', pipeline_name_label]
|
|
31
|
-
)
|
|
32
|
-
|
|
33
|
-
# Workaround for https://gitlab.com/gitlab-org/gitlab/-/issues/295493
|
|
34
|
-
unless Runtime::Env.qa_issue_url.to_s.empty?
|
|
35
|
-
gitlab.create_issue_note(
|
|
36
|
-
iid: issue.iid,
|
|
37
|
-
note: "/relate #{Runtime::Env.qa_issue_url}")
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
File.write('REPORT_ISSUE_URL', issue.web_url)
|
|
41
|
-
end
|
|
42
|
-
# rubocop:enable Metrics/AbcSize
|
|
43
|
-
|
|
44
|
-
def generate_description(tests)
|
|
45
|
-
<<~MARKDOWN.rstrip
|
|
46
|
-
## Session summary
|
|
47
|
-
|
|
48
|
-
* Deploy version: #{Runtime::Env.deploy_version}
|
|
49
|
-
* Deploy environment: #{Runtime::Env.deploy_environment}
|
|
50
|
-
* Pipeline: #{Runtime::Env.pipeline_from_project_name} [#{Runtime::Env.ci_pipeline_id}](#{Runtime::Env.ci_pipeline_url})
|
|
51
|
-
#{generate_summary(tests: tests)}
|
|
52
|
-
|
|
53
|
-
#{generate_failed_jobs_listing}
|
|
54
|
-
|
|
55
|
-
#{generate_stages_listing(tests)}
|
|
56
|
-
|
|
57
|
-
#{generate_qa_issue_relation}
|
|
58
|
-
|
|
59
|
-
#{generate_link_to_dashboard}
|
|
60
|
-
MARKDOWN
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
def generate_summary(tests:, tests_by_status: nil)
|
|
64
|
-
tests_by_status ||= tests.group_by(&:status)
|
|
65
|
-
total = tests.size
|
|
66
|
-
passed = tests_by_status['passed']&.size || 0
|
|
67
|
-
failed = tests_by_status['failed']&.size || 0
|
|
68
|
-
others = total - passed - failed
|
|
69
|
-
|
|
70
|
-
<<~MARKDOWN.chomp
|
|
71
|
-
* Total #{total} tests
|
|
72
|
-
* Passed #{passed} tests
|
|
73
|
-
* Failed #{failed} tests
|
|
74
|
-
* #{others} other tests (usually skipped)
|
|
75
|
-
MARKDOWN
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
def generate_failed_jobs_listing
|
|
79
|
-
failed_jobs = []
|
|
80
|
-
|
|
81
|
-
client = Gitlab.client(
|
|
82
|
-
endpoint: Runtime::Env.ci_api_v4_url,
|
|
83
|
-
private_token: Runtime::Env.gitlab_ci_api_token)
|
|
84
|
-
|
|
85
|
-
gitlab.handle_gitlab_client_exceptions do
|
|
86
|
-
failed_jobs = client.pipeline_jobs(
|
|
87
|
-
Runtime::Env.ci_project_id,
|
|
88
|
-
Runtime::Env.ci_pipeline_id,
|
|
89
|
-
scope: 'failed')
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
listings = failed_jobs.map do |job|
|
|
93
|
-
allowed_to_fail = ' (allowed to fail)' if job.allow_failure
|
|
94
|
-
|
|
95
|
-
"* [#{job.name}](#{job.web_url})#{allowed_to_fail}"
|
|
96
|
-
end.join("\n")
|
|
97
|
-
|
|
98
|
-
<<~MARKDOWN.chomp if failed_jobs.any?
|
|
99
|
-
## Failed jobs
|
|
100
|
-
|
|
101
|
-
#{listings}
|
|
102
|
-
MARKDOWN
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
def generate_stages_listing(tests)
|
|
106
|
-
generate_tests_by_stage(tests).map do |stage, tests_for_stage|
|
|
107
|
-
tests_by_status = tests_for_stage.group_by(&:status)
|
|
108
|
-
|
|
109
|
-
<<~MARKDOWN.chomp
|
|
110
|
-
### #{stage&.capitalize || 'Unknown'}
|
|
111
|
-
|
|
112
|
-
#{generate_summary(
|
|
113
|
-
tests: tests_for_stage, tests_by_status: tests_by_status)}
|
|
114
|
-
|
|
115
|
-
#{generate_testcase_listing_by_status(
|
|
116
|
-
tests: tests_for_stage, tests_by_status: tests_by_status)}
|
|
117
|
-
MARKDOWN
|
|
118
|
-
end.join("\n\n")
|
|
119
|
-
end
|
|
120
|
-
|
|
121
|
-
def generate_tests_by_stage(tests)
|
|
122
|
-
# https://about.gitlab.com/handbook/product/product-categories/#devops-stages
|
|
123
|
-
ordering = %w[
|
|
124
|
-
manage
|
|
125
|
-
plan
|
|
126
|
-
create
|
|
127
|
-
verify
|
|
128
|
-
package
|
|
129
|
-
release
|
|
130
|
-
configure
|
|
131
|
-
monitor
|
|
132
|
-
secure
|
|
133
|
-
defend
|
|
134
|
-
growth
|
|
135
|
-
fulfillment
|
|
136
|
-
enablement
|
|
137
|
-
]
|
|
138
|
-
|
|
139
|
-
tests.sort_by do |test|
|
|
140
|
-
ordering.index(test.stage) || ordering.size
|
|
141
|
-
end.group_by(&:stage)
|
|
142
|
-
end
|
|
143
|
-
|
|
144
|
-
def generate_testcase_listing_by_status(tests:, tests_by_status:)
|
|
145
|
-
failed_tests = tests_by_status['failed']
|
|
146
|
-
passed_tests = tests_by_status['passed']
|
|
147
|
-
other_tests = tests.reject do |test|
|
|
148
|
-
test.status == 'failed' || test.status == 'passed'
|
|
149
|
-
end
|
|
150
|
-
|
|
151
|
-
[
|
|
152
|
-
(failed_listings(failed_tests) if failed_tests),
|
|
153
|
-
(passed_listings(passed_tests) if passed_tests),
|
|
154
|
-
(other_listings(other_tests) if other_tests.any?)
|
|
155
|
-
].compact.join("\n\n")
|
|
156
|
-
end
|
|
157
|
-
|
|
158
|
-
def failed_listings(failed_tests)
|
|
159
|
-
generate_testcase_listing(failed_tests)
|
|
160
|
-
end
|
|
161
|
-
|
|
162
|
-
def passed_listings(passed_tests)
|
|
163
|
-
<<~MARKDOWN.chomp
|
|
164
|
-
<details><summary>Passed tests:</summary>
|
|
165
|
-
|
|
166
|
-
#{generate_testcase_listing(passed_tests, passed: true)}
|
|
167
|
-
|
|
168
|
-
</details>
|
|
169
|
-
MARKDOWN
|
|
170
|
-
end
|
|
171
|
-
|
|
172
|
-
def other_listings(other_tests)
|
|
173
|
-
<<~MARKDOWN.chomp
|
|
174
|
-
<details><summary>Other tests:</summary>
|
|
175
|
-
|
|
176
|
-
#{generate_testcase_listing(other_tests)}
|
|
177
|
-
|
|
178
|
-
</details>
|
|
179
|
-
MARKDOWN
|
|
180
|
-
end
|
|
181
|
-
|
|
182
|
-
def generate_testcase_listing(tests, passed: false)
|
|
183
|
-
body = tests.group_by(&:testcase).map do |testcase, tests_with_same_testcase|
|
|
184
|
-
tests_with_same_testcase.sort_by!(&:name)
|
|
185
|
-
[
|
|
186
|
-
generate_test_text(testcase, tests_with_same_testcase, passed),
|
|
187
|
-
generate_test_job(tests_with_same_testcase),
|
|
188
|
-
generate_test_status(tests_with_same_testcase),
|
|
189
|
-
generate_test_actions(tests_with_same_testcase)
|
|
190
|
-
].join(' | ')
|
|
191
|
-
end.join("\n")
|
|
192
|
-
|
|
193
|
-
<<~MARKDOWN.chomp
|
|
194
|
-
| Test | Job | Status | Action |
|
|
195
|
-
| - | - | - | - |
|
|
196
|
-
#{body}
|
|
197
|
-
MARKDOWN
|
|
198
|
-
end
|
|
199
|
-
|
|
200
|
-
def generate_test_text(testcase, tests_with_same_testcase, passed)
|
|
201
|
-
text = tests_with_same_testcase.map(&:name).uniq.join(', ')
|
|
202
|
-
encoded_text = ERB::Util.url_encode(text)
|
|
203
|
-
|
|
204
|
-
if testcase && !passed
|
|
205
|
-
# Workaround for reducing system notes on testcase issues
|
|
206
|
-
# The first regex extracts the link to the issues list page from a link to a single issue show page by removing the issue id.
|
|
207
|
-
"[#{text}](#{testcase.match(%r{[\s\S]+\/[^\/\d]+})}?state=opened&search=#{encoded_text})"
|
|
208
|
-
else
|
|
209
|
-
text
|
|
210
|
-
end
|
|
211
|
-
end
|
|
212
|
-
|
|
213
|
-
def generate_test_job(tests_with_same_testcase)
|
|
214
|
-
tests_with_same_testcase.map do |test|
|
|
215
|
-
ci_job_id = test.ci_job_url[/\d+\z/]
|
|
216
|
-
|
|
217
|
-
"[#{ci_job_id}](#{test.ci_job_url})#{' ~"quarantine"' if test.quarantine?}"
|
|
218
|
-
end.uniq.join(', ')
|
|
219
|
-
end
|
|
220
|
-
|
|
221
|
-
def generate_test_status(tests_with_same_testcase)
|
|
222
|
-
tests_with_same_testcase.map(&:status).uniq.map do |status|
|
|
223
|
-
%(~"#{status}")
|
|
224
|
-
end.join(', ')
|
|
225
|
-
end
|
|
226
|
-
|
|
227
|
-
def generate_test_actions(tests_with_same_testcase)
|
|
228
|
-
# All failed tests would be grouped together, meaning that
|
|
229
|
-
# if one failed, all the tests here would be failed too.
|
|
230
|
-
# So this check is safe. Same applies to 'passed'.
|
|
231
|
-
# But all other status might be mixing together,
|
|
232
|
-
# we cannot assume other statuses.
|
|
233
|
-
if tests_with_same_testcase.first.status == 'failed'
|
|
234
|
-
tests_having_failure_issue =
|
|
235
|
-
tests_with_same_testcase.select(&:failure_issue)
|
|
236
|
-
|
|
237
|
-
if tests_having_failure_issue.any?
|
|
238
|
-
items = tests_having_failure_issue.uniq(&:failure_issue).map do |test|
|
|
239
|
-
"<li>[ ] [failure issue](#{test.failure_issue})</li>"
|
|
240
|
-
end.join(' ')
|
|
241
|
-
|
|
242
|
-
"<ul>#{items}</ul>"
|
|
243
|
-
else
|
|
244
|
-
'<ul><li>[ ] failure issue exists or was created</li></ul>'
|
|
245
|
-
end
|
|
246
|
-
else
|
|
247
|
-
'-'
|
|
248
|
-
end
|
|
249
|
-
end
|
|
250
|
-
|
|
251
|
-
def generate_qa_issue_relation
|
|
252
|
-
return unless Runtime::Env.qa_issue_url
|
|
253
|
-
|
|
254
|
-
<<~MARKDOWN.chomp
|
|
255
|
-
## Release QA issue
|
|
256
|
-
|
|
257
|
-
* #{Runtime::Env.qa_issue_url}
|
|
258
|
-
|
|
259
|
-
/relate #{Runtime::Env.qa_issue_url}
|
|
260
|
-
MARKDOWN
|
|
261
|
-
end
|
|
262
|
-
|
|
263
|
-
def generate_link_to_dashboard
|
|
264
|
-
return unless Runtime::Env.qa_run_type
|
|
265
|
-
|
|
266
|
-
<<~MARKDOWN.chomp
|
|
267
|
-
## Link to Grafana dashboard for run-type of #{Runtime::Env.qa_run_type}
|
|
268
|
-
|
|
269
|
-
* https://dashboards.quality.gitlab.net/d/kuNYMgDnz/test-run-metrics?orgId=1&refresh=1m&var-run_type=#{Runtime::Env.qa_run_type}
|
|
270
|
-
MARKDOWN
|
|
271
|
-
end
|
|
272
|
-
end
|
|
273
|
-
end
|
|
274
|
-
end
|
|
275
|
-
end
|
|
@@ -1,190 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'gitlab'
|
|
4
|
-
|
|
5
|
-
module Gitlab
|
|
6
|
-
# Monkey patch the Gitlab client to use the correct API path and add required methods
|
|
7
|
-
class Client
|
|
8
|
-
def team_member(project, id)
|
|
9
|
-
get("/projects/#{url_encode(project)}/members/all/#{id}")
|
|
10
|
-
end
|
|
11
|
-
|
|
12
|
-
def issue_discussions(project, issue_id, options = {})
|
|
13
|
-
get("/projects/#{url_encode(project)}/issues/#{issue_id}/discussions", query: options)
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
def add_note_to_issue_discussion_as_thread(project, issue_id, discussion_id, options = {})
|
|
17
|
-
post("/projects/#{url_encode(project)}/issues/#{issue_id}/discussions/#{discussion_id}/notes", query: options)
|
|
18
|
-
end
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
module QA
|
|
22
|
-
module Report
|
|
23
|
-
# The GitLab client is used for API access: https://github.com/NARKOZ/gitlab
|
|
24
|
-
class GitlabIssueClient
|
|
25
|
-
MAINTAINER_ACCESS_LEVEL = 40
|
|
26
|
-
RETRY_BACK_OFF_DELAY = 60
|
|
27
|
-
MAX_RETRY_ATTEMPTS = 3
|
|
28
|
-
|
|
29
|
-
def initialize(token:, project:)
|
|
30
|
-
@token = token
|
|
31
|
-
@project = project
|
|
32
|
-
@retry_backoff = 0
|
|
33
|
-
|
|
34
|
-
configure_gitlab_client
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
def assert_user_permission!
|
|
38
|
-
handle_gitlab_client_exceptions do
|
|
39
|
-
user = Gitlab.user
|
|
40
|
-
member = Gitlab.team_member(project, user.id)
|
|
41
|
-
|
|
42
|
-
abort_not_permitted if member.access_level < MAINTAINER_ACCESS_LEVEL
|
|
43
|
-
end
|
|
44
|
-
rescue Gitlab::Error::NotFound
|
|
45
|
-
abort_not_permitted
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
def find_issues(iid: nil, options: {}, &select)
|
|
49
|
-
select ||= :itself
|
|
50
|
-
|
|
51
|
-
handle_gitlab_client_exceptions do
|
|
52
|
-
return [Gitlab.issue(project, iid)].select(&select) if iid
|
|
53
|
-
|
|
54
|
-
Gitlab.issues(project, options)
|
|
55
|
-
.auto_paginate
|
|
56
|
-
.select(&select)
|
|
57
|
-
end
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
def find_issue_discussions(iid:)
|
|
61
|
-
handle_gitlab_client_exceptions do
|
|
62
|
-
Gitlab.issue_discussions(project, iid, order_by: 'created_at', sort: 'asc').auto_paginate
|
|
63
|
-
end
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
def create_issue(title:, description:, labels:, issue_type: 'issue')
|
|
67
|
-
attrs = { issue_type: issue_type, description: description, labels: labels }
|
|
68
|
-
|
|
69
|
-
handle_gitlab_client_exceptions do
|
|
70
|
-
Gitlab.create_issue(project, title, attrs)
|
|
71
|
-
end
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
def edit_issue(iid:, options: {})
|
|
75
|
-
handle_gitlab_client_exceptions do
|
|
76
|
-
Gitlab.edit_issue(project, iid, options)
|
|
77
|
-
end
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
def find_issue_notes(iid:)
|
|
81
|
-
handle_gitlab_client_exceptions do
|
|
82
|
-
Gitlab.issue_notes(project, iid, order_by: 'created_at', sort: 'asc')&.auto_paginate
|
|
83
|
-
end
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
def create_issue_note(iid:, note:)
|
|
87
|
-
handle_gitlab_client_exceptions do
|
|
88
|
-
Gitlab.create_issue_note(project, iid, note)
|
|
89
|
-
end
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
def edit_issue_note(issue_iid:, note_id:, note:)
|
|
93
|
-
handle_gitlab_client_exceptions do
|
|
94
|
-
Gitlab.edit_issue_note(project, issue_iid, note_id, note)
|
|
95
|
-
end
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
def add_note_to_issue_discussion_as_thread(iid:, discussion_id:, body:)
|
|
99
|
-
handle_gitlab_client_exceptions do
|
|
100
|
-
Gitlab.add_note_to_issue_discussion_as_thread(project, iid, discussion_id, body: body)
|
|
101
|
-
end
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
def find_user_id(username:)
|
|
105
|
-
handle_gitlab_client_exceptions do
|
|
106
|
-
user = Gitlab.users(username: username)&.first
|
|
107
|
-
user['id'] unless user.nil?
|
|
108
|
-
end
|
|
109
|
-
end
|
|
110
|
-
|
|
111
|
-
def upload_file(file_fullpath:)
|
|
112
|
-
ignore_gitlab_client_exceptions do
|
|
113
|
-
Gitlab.upload_file(project, file_fullpath)
|
|
114
|
-
end
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
def ignore_gitlab_client_exceptions
|
|
118
|
-
yield
|
|
119
|
-
rescue StandardError, SystemCallError, OpenSSL::SSL::SSLError, Net::OpenTimeout, Net::ReadTimeout, Gitlab::Error::Error => e
|
|
120
|
-
puts "Ignoring the following error: #{e}"
|
|
121
|
-
end
|
|
122
|
-
|
|
123
|
-
def handle_gitlab_client_exceptions
|
|
124
|
-
yield
|
|
125
|
-
rescue Gitlab::Error::NotFound
|
|
126
|
-
# This error could be raised in assert_user_permission!
|
|
127
|
-
# If so, we want it to terminate at that point
|
|
128
|
-
raise
|
|
129
|
-
rescue SystemCallError, OpenSSL::SSL::SSLError, Net::OpenTimeout, Net::ReadTimeout, Gitlab::Error::InternalServerError, Gitlab::Error::Parsing => e
|
|
130
|
-
@retry_backoff += RETRY_BACK_OFF_DELAY
|
|
131
|
-
|
|
132
|
-
raise if @retry_backoff > RETRY_BACK_OFF_DELAY * MAX_RETRY_ATTEMPTS
|
|
133
|
-
|
|
134
|
-
warn_exception(e)
|
|
135
|
-
warn("Sleeping for #{@retry_backoff} seconds before retrying...")
|
|
136
|
-
sleep @retry_backoff
|
|
137
|
-
|
|
138
|
-
retry
|
|
139
|
-
rescue StandardError => e
|
|
140
|
-
pipeline = QA::Runtime::Env.pipeline_from_project_name
|
|
141
|
-
channel = case pipeline
|
|
142
|
-
when "canary"
|
|
143
|
-
"qa-production"
|
|
144
|
-
when "staging-canary"
|
|
145
|
-
"qa-staging"
|
|
146
|
-
else
|
|
147
|
-
"qa-#{pipeline}"
|
|
148
|
-
end
|
|
149
|
-
error_msg = warn_exception(e)
|
|
150
|
-
|
|
151
|
-
return unless QA::Runtime::Env.ci_commit_ref_name == QA::Runtime::Env.default_branch
|
|
152
|
-
|
|
153
|
-
slack_options = {
|
|
154
|
-
channel: channel,
|
|
155
|
-
icon_emoji: ':ci_failing:',
|
|
156
|
-
message: <<~MSG
|
|
157
|
-
An unexpected error occurred while reporting test results in issues.
|
|
158
|
-
The error occurred in job: #{QA::Runtime::Env.ci_job_url}
|
|
159
|
-
`#{error_msg}`
|
|
160
|
-
MSG
|
|
161
|
-
}
|
|
162
|
-
puts "Posting Slack message to channel: #{channel}"
|
|
163
|
-
|
|
164
|
-
Gitlab::QA::Slack::PostToSlack.new(**slack_options).invoke!
|
|
165
|
-
end
|
|
166
|
-
|
|
167
|
-
private
|
|
168
|
-
|
|
169
|
-
attr_reader :token, :project
|
|
170
|
-
|
|
171
|
-
def configure_gitlab_client
|
|
172
|
-
Gitlab.configure do |config|
|
|
173
|
-
config.endpoint = Runtime::Env.gitlab_api_base
|
|
174
|
-
config.private_token = token
|
|
175
|
-
end
|
|
176
|
-
end
|
|
177
|
-
|
|
178
|
-
def abort_not_permitted
|
|
179
|
-
abort "You must have at least Maintainer access to the project to use this feature."
|
|
180
|
-
end
|
|
181
|
-
|
|
182
|
-
def warn_exception(error)
|
|
183
|
-
error_msg = "#{error.class.name} #{error.message}"
|
|
184
|
-
warn(error_msg)
|
|
185
|
-
error_msg
|
|
186
|
-
end
|
|
187
|
-
end
|
|
188
|
-
end
|
|
189
|
-
end
|
|
190
|
-
end
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Gitlab
|
|
4
|
-
module QA
|
|
5
|
-
module Report
|
|
6
|
-
class GitlabIssueDryClient < GitlabIssueClient
|
|
7
|
-
def create_issue(title:, description:, labels:, issue_type: 'issue')
|
|
8
|
-
attrs = { description: description, labels: labels }
|
|
9
|
-
|
|
10
|
-
puts "The following #{issue_type} would have been created:"
|
|
11
|
-
puts "project: #{project}, title: #{title}, attrs: #{attrs}"
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
def edit_issue(iid:, options: {})
|
|
15
|
-
puts "The #{project}##{iid} issue would have been updated with: #{options}"
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
def create_issue_note(iid:, note:)
|
|
19
|
-
puts "The following note would have been posted on #{project}##{iid} issue: #{note}"
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
def add_note_to_issue_discussion_as_thread(iid:, discussion_id:, body:)
|
|
23
|
-
puts "The following discussion note would have been posted on #{project}##{iid} (discussion #{discussion_id}) issue: #{body}"
|
|
24
|
-
end
|
|
25
|
-
end
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
end
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'nokogiri'
|
|
4
|
-
|
|
5
|
-
module Gitlab
|
|
6
|
-
module QA
|
|
7
|
-
module Report
|
|
8
|
-
class JUnitTestResults < BaseTestResults
|
|
9
|
-
def write
|
|
10
|
-
# Ignore it for now
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
private
|
|
14
|
-
|
|
15
|
-
def parse
|
|
16
|
-
Nokogiri::XML.parse(File.read(path))
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
def process
|
|
20
|
-
results.xpath('//testcase').map do |test|
|
|
21
|
-
TestResult.from_junit(test)
|
|
22
|
-
end
|
|
23
|
-
end
|
|
24
|
-
end
|
|
25
|
-
end
|
|
26
|
-
end
|
|
27
|
-
end
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'json'
|
|
4
|
-
|
|
5
|
-
module Gitlab
|
|
6
|
-
module QA
|
|
7
|
-
module Report
|
|
8
|
-
class JsonTestResults < BaseTestResults
|
|
9
|
-
def write
|
|
10
|
-
json = results.merge('examples' => testcases.map(&:report))
|
|
11
|
-
|
|
12
|
-
File.write(path, JSON.pretty_generate(json))
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
private
|
|
16
|
-
|
|
17
|
-
def parse
|
|
18
|
-
JSON.parse(File.read(path))
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
def process
|
|
22
|
-
results['examples'].map do |test|
|
|
23
|
-
TestResult.from_json(test)
|
|
24
|
-
end
|
|
25
|
-
end
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
end
|
|
29
|
-
end
|
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'nokogiri'
|
|
4
|
-
|
|
5
|
-
module Gitlab
|
|
6
|
-
module QA
|
|
7
|
-
module Report
|
|
8
|
-
class PrepareStageReports
|
|
9
|
-
def initialize(input_files:)
|
|
10
|
-
@input_files = input_files
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
# Create a new JUnit report file for each Stage, containing tests from that Stage alone
|
|
14
|
-
def invoke!
|
|
15
|
-
collate_test_cases(@input_files).each do |stage, tests|
|
|
16
|
-
filename = "#{stage}.xml"
|
|
17
|
-
|
|
18
|
-
File.write(filename, new_junit_report(tests))
|
|
19
|
-
|
|
20
|
-
puts "Saved #{filename}"
|
|
21
|
-
end
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
private
|
|
25
|
-
|
|
26
|
-
def collate_test_cases(input_files)
|
|
27
|
-
# Collect the test cases from the original reports and group them by Stage
|
|
28
|
-
testcases = {}
|
|
29
|
-
|
|
30
|
-
Dir.glob(input_files).each do |rspec_report_file|
|
|
31
|
-
report = Nokogiri::XML(File.open(rspec_report_file))
|
|
32
|
-
report.xpath('//testcase').each do |test|
|
|
33
|
-
# The test file paths could start with any of
|
|
34
|
-
# /qa/specs/features/api/<stage>
|
|
35
|
-
# /qa/specs/features/browser_ui/<stage>
|
|
36
|
-
# /qa/specs/features/ee/api/<stage>
|
|
37
|
-
# /qa/specs/features/ee/browser_ui/<stage>
|
|
38
|
-
# For now we assume the Stage is whatever follows api/ or browser_ui/
|
|
39
|
-
test_path_match = test['file'].match(%r{(api|browser_ui)/([a-z0-9_]+)}i)
|
|
40
|
-
next unless test_path_match
|
|
41
|
-
|
|
42
|
-
stage = strip_number_prefix(test_path_match[2])
|
|
43
|
-
testcases[stage] = [] unless testcases.key?(stage)
|
|
44
|
-
testcases[stage] << test
|
|
45
|
-
end
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
testcases
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
def strip_number_prefix(stage)
|
|
52
|
-
stage.sub(/^\d+_/, '')
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
def new_junit_report(testcases)
|
|
56
|
-
report = Nokogiri::XML::Document.new
|
|
57
|
-
testsuite_node = report.create_element('testsuite', name: 'rspec', **collect_stats(testcases))
|
|
58
|
-
report.root = testsuite_node
|
|
59
|
-
|
|
60
|
-
testcases.each do |test|
|
|
61
|
-
testsuite_node.add_child(test)
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
report.to_s
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
def collect_stats(testcases)
|
|
68
|
-
stats = {
|
|
69
|
-
tests: testcases.size,
|
|
70
|
-
failures: 0,
|
|
71
|
-
errors: 0,
|
|
72
|
-
skipped: 0
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
testcases.each do |test|
|
|
76
|
-
stats[:failures] += 1 unless test.search('failure').empty?
|
|
77
|
-
stats[:errors] += 1 unless test.search('error').empty?
|
|
78
|
-
stats[:skipped] += 1 unless test.search('skipped').empty?
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
stats
|
|
82
|
-
end
|
|
83
|
-
end
|
|
84
|
-
end
|
|
85
|
-
end
|
|
86
|
-
end
|