gitlab_quality-test_tooling 0.1.0 → 0.2.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 +9 -4
- data/Gemfile.lock +1 -1
- data/Guardfile +0 -22
- data/exe/generate-test-session +49 -0
- data/exe/post-to-slack +57 -0
- data/exe/prepare-stage-reports +38 -0
- data/exe/relate-failure-issue +59 -0
- data/exe/report-results +56 -0
- data/exe/update-screenshot-paths +38 -0
- data/lib/gitlab_quality/test_tooling/gitlab_issue_client.rb +194 -0
- data/lib/gitlab_quality/test_tooling/gitlab_issue_dry_client.rb +26 -0
- data/lib/gitlab_quality/test_tooling/report/concerns/find_set_dri.rb +51 -0
- data/lib/gitlab_quality/test_tooling/report/concerns/results_reporter.rb +75 -0
- data/lib/gitlab_quality/test_tooling/report/concerns/utils.rb +49 -0
- data/lib/gitlab_quality/test_tooling/report/generate_test_session.rb +275 -0
- data/lib/gitlab_quality/test_tooling/report/prepare_stage_reports.rb +79 -0
- data/lib/gitlab_quality/test_tooling/report/relate_failure_issue.rb +377 -0
- data/lib/gitlab_quality/test_tooling/report/report_as_issue.rb +134 -0
- data/lib/gitlab_quality/test_tooling/report/report_results.rb +83 -0
- data/lib/gitlab_quality/test_tooling/report/results_in_issues.rb +130 -0
- data/lib/gitlab_quality/test_tooling/report/results_in_testcases.rb +113 -0
- data/lib/gitlab_quality/test_tooling/report/update_screenshot_path.rb +81 -0
- data/lib/gitlab_quality/test_tooling/runtime/env.rb +113 -0
- data/lib/gitlab_quality/test_tooling/runtime/logger.rb +92 -0
- data/lib/gitlab_quality/test_tooling/runtime/token_finder.rb +44 -0
- data/lib/gitlab_quality/test_tooling/slack/post_to_slack.rb +36 -0
- data/lib/gitlab_quality/test_tooling/summary_table.rb +41 -0
- data/lib/gitlab_quality/test_tooling/support/http_request.rb +34 -0
- data/lib/gitlab_quality/test_tooling/system_logs/finders/json_log_finder.rb +65 -0
- data/lib/gitlab_quality/test_tooling/system_logs/finders/rails/api_log_finder.rb +21 -0
- data/lib/gitlab_quality/test_tooling/system_logs/finders/rails/application_log_finder.rb +21 -0
- data/lib/gitlab_quality/test_tooling/system_logs/finders/rails/exception_log_finder.rb +21 -0
- data/lib/gitlab_quality/test_tooling/system_logs/finders/rails/graphql_log_finder.rb +21 -0
- data/lib/gitlab_quality/test_tooling/system_logs/log_types/log.rb +38 -0
- data/lib/gitlab_quality/test_tooling/system_logs/log_types/rails/api_log.rb +34 -0
- data/lib/gitlab_quality/test_tooling/system_logs/log_types/rails/application_log.rb +27 -0
- data/lib/gitlab_quality/test_tooling/system_logs/log_types/rails/exception_log.rb +23 -0
- data/lib/gitlab_quality/test_tooling/system_logs/log_types/rails/graphql_log.rb +30 -0
- data/lib/gitlab_quality/test_tooling/system_logs/shared_fields.rb +29 -0
- data/lib/gitlab_quality/test_tooling/system_logs/system_logs_formatter.rb +65 -0
- data/lib/gitlab_quality/test_tooling/test_results/base_test_results.rb +39 -0
- data/lib/gitlab_quality/test_tooling/test_results/builder.rb +35 -0
- data/lib/gitlab_quality/test_tooling/test_results/j_unit_test_results.rb +27 -0
- data/lib/gitlab_quality/test_tooling/test_results/json_test_results.rb +29 -0
- data/lib/gitlab_quality/test_tooling/test_results/test_result.rb +184 -0
- data/lib/gitlab_quality/test_tooling/version.rb +1 -1
- data/lib/gitlab_quality/test_tooling.rb +11 -2
- metadata +51 -3
@@ -0,0 +1,75 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_support/core_ext/enumerable'
|
4
|
+
|
5
|
+
module GitlabQuality
|
6
|
+
module TestTooling
|
7
|
+
module Report
|
8
|
+
module Concerns
|
9
|
+
module ResultsReporter
|
10
|
+
include Concerns::Utils
|
11
|
+
|
12
|
+
TEST_CASE_RESULTS_SECTION_TEMPLATE = "\n\n### DO NOT EDIT BELOW THIS LINE\n\nActive and historical test results:"
|
13
|
+
|
14
|
+
def find_issue(test)
|
15
|
+
issues = search_for_issues(test)
|
16
|
+
|
17
|
+
warn(%(Too many #{issue_type}s found with the file path "#{test.file}" and name "#{test.name}")) if issues.many?
|
18
|
+
|
19
|
+
puts "Found existing #{issue_type}: #{issues.first.web_url}" unless issues.empty?
|
20
|
+
|
21
|
+
issues.first
|
22
|
+
end
|
23
|
+
|
24
|
+
def find_issue_by_iid(iid)
|
25
|
+
issues = gitlab.find_issues(iid: iid) do |issue|
|
26
|
+
issue.state == 'opened' && issue.issue_type == issue_type
|
27
|
+
end
|
28
|
+
|
29
|
+
warn(%(#{issue_type} iid "#{iid}" not valid)) if issues.empty?
|
30
|
+
|
31
|
+
issues.first
|
32
|
+
end
|
33
|
+
|
34
|
+
def issue_title_needs_updating?(issue, test)
|
35
|
+
issue.title.strip != title_from_test(test) && !%w[canary production preprod release].include?(pipeline)
|
36
|
+
end
|
37
|
+
|
38
|
+
def new_issue_labels(_test)
|
39
|
+
%w[Quality status::automated]
|
40
|
+
end
|
41
|
+
|
42
|
+
def up_to_date_labels(test:, issue: nil, new_labels: Set.new)
|
43
|
+
labels = super
|
44
|
+
labels |= new_issue_labels(test).to_set
|
45
|
+
labels.delete_if { |label| label.start_with?("#{pipeline}::") }
|
46
|
+
labels << (test.failures.empty? ? "#{pipeline}::passed" : "#{pipeline}::failed")
|
47
|
+
end
|
48
|
+
|
49
|
+
def update_issue_title(issue, test)
|
50
|
+
old_title = issue.title.strip
|
51
|
+
new_title = title_from_test(test)
|
52
|
+
|
53
|
+
warn(%(#{issue_type} title needs to be updated from '#{old_title}' to '#{new_title}'))
|
54
|
+
|
55
|
+
new_description = updated_description(issue, test)
|
56
|
+
|
57
|
+
gitlab.edit_issue(iid: issue.iid, options: { title: new_title, description: new_description })
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
def search_term(test)
|
63
|
+
%("#{partial_file_path(test.file)}" "#{search_safe(test.name)}")
|
64
|
+
end
|
65
|
+
|
66
|
+
def search_for_issues(test)
|
67
|
+
gitlab.find_issues(options: { search: search_term(test) }) do |issue|
|
68
|
+
issue.state == 'opened' && issue.issue_type == issue_type && issue.title.strip == title_from_test(test)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GitlabQuality
|
4
|
+
module TestTooling
|
5
|
+
module Report
|
6
|
+
module Concerns
|
7
|
+
module Utils
|
8
|
+
MAX_TITLE_LENGTH = 255
|
9
|
+
|
10
|
+
def partial_file_path(path)
|
11
|
+
path.match(/((ee|api|browser_ui).*)/i)[1]
|
12
|
+
end
|
13
|
+
|
14
|
+
def new_issue_title(test)
|
15
|
+
"#{partial_file_path(test.file)} | #{search_safe(test.name)}".strip
|
16
|
+
end
|
17
|
+
|
18
|
+
def title_from_test(test)
|
19
|
+
title = new_issue_title(test)
|
20
|
+
|
21
|
+
return title unless title.length > MAX_TITLE_LENGTH
|
22
|
+
|
23
|
+
"#{title[0...MAX_TITLE_LENGTH - 3]}..."
|
24
|
+
end
|
25
|
+
|
26
|
+
def search_safe(value)
|
27
|
+
value.delete('"')
|
28
|
+
end
|
29
|
+
|
30
|
+
def pipeline
|
31
|
+
# Gets the name of the pipeline the test was run in, to be used as the key of a scoped label
|
32
|
+
#
|
33
|
+
# Tests can be run in several pipelines:
|
34
|
+
# gitlab, nightly, staging, canary, production, preprod, MRs, and the default branch (master/main)
|
35
|
+
#
|
36
|
+
# Some of those run in their own project, so CI_PROJECT_NAME is the name we need. Those are:
|
37
|
+
# nightly, staging, canary, production, and preprod
|
38
|
+
#
|
39
|
+
# MR, master/main, and gitlab tests run in gitlab-qa, but we only want to report tests run on
|
40
|
+
# master/main because the other pipelines will be monitored by the author of the MR that triggered them.
|
41
|
+
# So we assume that we're reporting a master/main pipeline if the project name is 'gitlab'.
|
42
|
+
|
43
|
+
@pipeline ||= Runtime::Env.pipeline_from_project_name
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,275 @@
|
|
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(**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
|
+
TestResults::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
|
@@ -0,0 +1,79 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'nokogiri'
|
4
|
+
|
5
|
+
module GitlabQuality
|
6
|
+
module TestTooling
|
7
|
+
module Report
|
8
|
+
# Create a new JUnit report file for each Stage, containing tests from that Stage alone
|
9
|
+
class PrepareStageReports
|
10
|
+
EXTRACT_STAGE_FROM_TEST_FILE_REGEX = %r{(?:api|browser_ui)/(?:[0-9]+_)?(?<stage>[_\w]+)/}i
|
11
|
+
|
12
|
+
def initialize(junit_files:)
|
13
|
+
@junit_files = junit_files
|
14
|
+
end
|
15
|
+
|
16
|
+
def invoke!
|
17
|
+
collate_test_cases.each do |stage, tests|
|
18
|
+
filename = "#{stage}.xml"
|
19
|
+
|
20
|
+
File.write(filename, junit_report(tests).to_s)
|
21
|
+
|
22
|
+
puts "Saved #{filename}"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
attr_reader :junit_files
|
29
|
+
|
30
|
+
# Collect the test cases from the original reports and group them by Stage
|
31
|
+
def collate_test_cases
|
32
|
+
Dir.glob(junit_files)
|
33
|
+
.each_with_object(Hash.new { |h, k| h[k] = [] }) do |junit_file, test_cases|
|
34
|
+
report = Nokogiri::XML(File.open(junit_file))
|
35
|
+
report.xpath('//testcase').each do |test_case|
|
36
|
+
# The test file paths could start with any of
|
37
|
+
# /qa/specs/features/api/<stage>
|
38
|
+
# /qa/specs/features/browser_ui/<stage>
|
39
|
+
# /qa/specs/features/ee/api/<stage>
|
40
|
+
# /qa/specs/features/ee/browser_ui/<stage>
|
41
|
+
# For now we assume the Stage is whatever follows api/ or browser_ui/
|
42
|
+
test_file_match = test_case['file'].match(EXTRACT_STAGE_FROM_TEST_FILE_REGEX)
|
43
|
+
next unless test_file_match
|
44
|
+
|
45
|
+
stage = test_file_match[:stage]
|
46
|
+
test_cases[stage] << test_case
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def junit_report(test_cases)
|
52
|
+
Nokogiri::XML::Document.new.tap do |report|
|
53
|
+
test_suite_node = report.create_element('testsuite', name: 'rspec', **collect_stats(test_cases))
|
54
|
+
report.root = test_suite_node
|
55
|
+
|
56
|
+
test_cases.each do |test_case|
|
57
|
+
test_suite_node.add_child(test_case)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def collect_stats(test_cases)
|
63
|
+
stats = {
|
64
|
+
tests: test_cases.size,
|
65
|
+
failures: 0,
|
66
|
+
errors: 0,
|
67
|
+
skipped: 0
|
68
|
+
}
|
69
|
+
|
70
|
+
test_cases.each_with_object(stats) do |test_case, memo|
|
71
|
+
memo[:failures] += 1 unless test_case.search('failure').empty?
|
72
|
+
memo[:errors] += 1 unless test_case.search('error').empty?
|
73
|
+
memo[:skipped] += 1 unless test_case.search('skipped').empty?
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|