gitlab_quality-test_tooling 0.8.2 → 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.tool-versions +1 -0
- data/Gemfile.lock +1 -1
- data/README.md +12 -0
- data/exe/slow-test-issues +50 -0
- data/lib/gitlab_quality/test_tooling/report/concerns/group_and_category_labels.rb +24 -0
- data/lib/gitlab_quality/test_tooling/report/relate_failure_issue.rb +41 -47
- data/lib/gitlab_quality/test_tooling/report/report_as_issue.rb +10 -3
- data/lib/gitlab_quality/test_tooling/report/report_results.rb +1 -1
- data/lib/gitlab_quality/test_tooling/report/results_in_issues.rb +1 -1
- data/lib/gitlab_quality/test_tooling/report/slow_test_issue.rb +88 -0
- data/lib/gitlab_quality/test_tooling/test_result/base_test_result.rb +35 -0
- data/lib/gitlab_quality/test_tooling/test_result/j_unit_test_result.rb +42 -0
- data/lib/gitlab_quality/test_tooling/test_result/json_test_result.rb +138 -0
- 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 +10 -3
- data/lib/gitlab_quality/test_tooling/test_results/test_result.rb +0 -188
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e9bcb8c6202edb63313039592fdf1a6c953c0b43db4566d9e84105e31d2029d3
|
4
|
+
data.tar.gz: 27dcc6d668da50a3ec76bd13b8898d61b24bbe2835faeee6fc6dbce684945388
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: cd3003fced80af3cd8ebafa3495d799eb63756b659b4c6e002c27557ea84beb3ec4bba1bbb08784cf27fe74ab6f4d1c6eb2ed7888fdbf317209683475247310b
|
7
|
+
data.tar.gz: 579e87d66fa4107263220501f4959e3b08b0dbb887224ecb11917a69e5d6774e9cfdd2298d62a20992bc27b7b4be3f561e35574f599a4cbb165230331261e9c5
|
data/.tool-versions
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
ruby 3.0.5
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -121,6 +121,18 @@ Usage: exe/update-screenshot-paths [options]
|
|
121
121
|
-h, --help Show the usage
|
122
122
|
```
|
123
123
|
|
124
|
+
### `slow-test-issues`
|
125
|
+
|
126
|
+
```shell
|
127
|
+
Purpose: Create slow test issues from JSON RSpec report files
|
128
|
+
Usage: exe/slow-test-issue [options]
|
129
|
+
-i, --input-files INPUT_FILES JSON RSpec report files JSON
|
130
|
+
-p, --project PROJECT Can be an integer or a group/project string
|
131
|
+
-t, --token TOKEN A valid access token with `api` scope and Maintainer permission in PROJECT
|
132
|
+
--dry-run Perform a dry-run (don't create issues)
|
133
|
+
-v, --version Show the version
|
134
|
+
-h, --help Show the usage
|
135
|
+
```
|
124
136
|
## Development
|
125
137
|
|
126
138
|
### Initial setup
|
@@ -0,0 +1,50 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "bundler/setup"
|
5
|
+
require "optparse"
|
6
|
+
|
7
|
+
require_relative "../lib/gitlab_quality/test_tooling"
|
8
|
+
|
9
|
+
params = {}
|
10
|
+
|
11
|
+
options = OptionParser.new do |opts|
|
12
|
+
opts.banner = "Usage: #{$PROGRAM_NAME} [options]"
|
13
|
+
|
14
|
+
opts.on('-i', '--input-files INPUT_FILES', String, 'JSON RSpec report files JSON') do |input_files|
|
15
|
+
params[:input_files] = input_files
|
16
|
+
end
|
17
|
+
|
18
|
+
opts.on('-p', '--project PROJECT', String, 'Can be an integer or a group/project string') do |project|
|
19
|
+
params[:project] = project
|
20
|
+
end
|
21
|
+
|
22
|
+
opts.on('-t', '--token TOKEN', String, 'A valid access token with `api` scope and Maintainer permission in PROJECT') do |token|
|
23
|
+
params[:token] = token
|
24
|
+
end
|
25
|
+
|
26
|
+
opts.on('--dry-run', "Perform a dry-run (don't create issues)") do
|
27
|
+
params[:dry_run] = true
|
28
|
+
end
|
29
|
+
|
30
|
+
opts.on_tail('-v', '--version', 'Show the version') do
|
31
|
+
require_relative "../lib/gitlab_quality/test_tooling/version"
|
32
|
+
puts "#{$PROGRAM_NAME} : #{GitlabQuality::TestTooling::VERSION}"
|
33
|
+
exit
|
34
|
+
end
|
35
|
+
|
36
|
+
opts.on_tail('-h', '--help', 'Show the usage') do
|
37
|
+
puts "Purpose: Create slow test issues from JSON RSpec report files"
|
38
|
+
puts opts
|
39
|
+
exit
|
40
|
+
end
|
41
|
+
|
42
|
+
opts.parse(ARGV)
|
43
|
+
end
|
44
|
+
|
45
|
+
if params.any?
|
46
|
+
GitlabQuality::TestTooling::Report::SlowTestIssue.new(**params).invoke!
|
47
|
+
else
|
48
|
+
puts options
|
49
|
+
exit 1
|
50
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GitlabQuality
|
4
|
+
module TestTooling
|
5
|
+
module Report
|
6
|
+
module Concerns
|
7
|
+
module GroupAndCategoryLabels
|
8
|
+
def labels_inference
|
9
|
+
@labels_inference ||= GitlabQuality::TestTooling::LabelsInference.new
|
10
|
+
end
|
11
|
+
|
12
|
+
def new_issue_labels(test)
|
13
|
+
puts " => [DEBUG] product_group: #{test.product_group}; feature_category: #{test.feature_category}"
|
14
|
+
|
15
|
+
new_labels = self.class::NEW_ISSUE_LABELS +
|
16
|
+
labels_inference.infer_labels_from_product_group(test.product_group) +
|
17
|
+
labels_inference.infer_labels_from_feature_category(test.feature_category)
|
18
|
+
up_to_date_labels(test: test, new_labels: new_labels)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -16,6 +16,7 @@ module GitlabQuality
|
|
16
16
|
# - Add the failed job to the issue description, and update labels
|
17
17
|
class RelateFailureIssue < ReportAsIssue
|
18
18
|
include Concerns::FindSetDri
|
19
|
+
include Concerns::GroupAndCategoryLabels
|
19
20
|
|
20
21
|
DEFAULT_MAX_DIFF_RATIO_FOR_DETECTION = 0.15
|
21
22
|
SYSTEMIC_EXCEPTIONS_THRESHOLD = 10
|
@@ -26,7 +27,7 @@ module GitlabQuality
|
|
26
27
|
FAILED_JOB_DESCRIPTION_REGEX = /First happened in #{JOB_URL_REGEX}\./m
|
27
28
|
REPORT_ITEM_REGEX = /^1\. \d{4}-\d{2}-\d{2}: #{JOB_URL_REGEX} \((?<pipeline_url>.+)\)$/
|
28
29
|
NEW_ISSUE_LABELS = Set.new(%w[test failure::new priority::2]).freeze
|
29
|
-
|
30
|
+
IGNORED_FAILURES = [
|
30
31
|
'Net::ReadTimeout',
|
31
32
|
'403 Forbidden - Your account has been blocked'
|
32
33
|
].freeze
|
@@ -53,19 +54,19 @@ module GitlabQuality
|
|
53
54
|
TestResults::Builder.new(files).test_results_per_file do |test_results|
|
54
55
|
puts "=> Reporting #{test_results.count} tests in #{test_results.path}"
|
55
56
|
|
56
|
-
|
57
|
+
systemic_failures = systemic_failures_for_test_results(test_results)
|
57
58
|
|
58
59
|
test_results.each do |test|
|
59
|
-
relate_failure_to_issue(test) if should_report?(test,
|
60
|
+
relate_failure_to_issue(test) if should_report?(test, systemic_failures)
|
60
61
|
end
|
61
62
|
|
62
63
|
test_results.write
|
63
64
|
end
|
64
65
|
end
|
65
66
|
|
66
|
-
def
|
67
|
+
def systemic_failures_for_test_results(test_results)
|
67
68
|
test_results
|
68
|
-
.flat_map { |test| test.
|
69
|
+
.flat_map { |test| test.failures.map { |failure| failure['message'].lines.first.chomp } }
|
69
70
|
.compact
|
70
71
|
.tally
|
71
72
|
.select { |_e, count| count >= SYSTEMIC_EXCEPTIONS_THRESHOLD }
|
@@ -171,18 +172,16 @@ module GitlabQuality
|
|
171
172
|
end
|
172
173
|
|
173
174
|
def failure_issues(test)
|
174
|
-
|
175
|
-
gitlab.find_issues(options: { state: 'opened', labels: search_labels }).select do |issue|
|
176
|
-
issue_title = issue.title.strip
|
177
|
-
issue_title.include?(test.name) || issue_title.include?(partial_file_path(test.file))
|
178
|
-
end
|
175
|
+
find_issues(test, (base_issue_labels + Set.new(%w[test])).to_a)
|
179
176
|
end
|
180
177
|
|
181
178
|
def full_stacktrace(test)
|
182
|
-
|
183
|
-
|
179
|
+
first_failure = test.failures.first
|
180
|
+
|
181
|
+
if first_failure['message_lines'].empty?
|
182
|
+
first_failure['message']
|
184
183
|
else
|
185
|
-
|
184
|
+
first_failure['message_lines'].join("\n")
|
186
185
|
end
|
187
186
|
end
|
188
187
|
|
@@ -313,18 +312,6 @@ module GitlabQuality
|
|
313
312
|
"1. #{Time.new.utc.strftime('%F')}: #{test.ci_job_url} (#{ENV.fetch('CI_PIPELINE_URL', 'pipeline url is missing')})"
|
314
313
|
end
|
315
314
|
|
316
|
-
def labels_inference
|
317
|
-
@labels_inference ||= GitlabQuality::TestTooling::LabelsInference.new
|
318
|
-
end
|
319
|
-
|
320
|
-
def new_issue_labels(test)
|
321
|
-
puts " => [DEBUG] product_group: #{test.product_group}; feature_category: #{test.feature_category}"
|
322
|
-
new_labels = NEW_ISSUE_LABELS +
|
323
|
-
labels_inference.infer_labels_from_product_group(test.product_group) +
|
324
|
-
labels_inference.infer_labels_from_feature_category(test.feature_category)
|
325
|
-
up_to_date_labels(test: test, new_labels: new_labels)
|
326
|
-
end
|
327
|
-
|
328
315
|
def up_to_date_labels(test:, issue: nil, new_labels: Set.new)
|
329
316
|
(Set.new(base_issue_labels) + (super << pipeline_name_label)).to_a
|
330
317
|
end
|
@@ -382,7 +369,7 @@ module GitlabQuality
|
|
382
369
|
failure = full_stacktrace(test)
|
383
370
|
return if SCREENSHOT_IGNORED_ERRORS.any? { |e| failure.include?(e) }
|
384
371
|
|
385
|
-
relative_url = gitlab.upload_file(file_fullpath: test.
|
372
|
+
relative_url = gitlab.upload_file(file_fullpath: test.screenshot_image)
|
386
373
|
return unless relative_url
|
387
374
|
|
388
375
|
"### Screenshot\n\n#{relative_url.markdown}"
|
@@ -392,37 +379,44 @@ module GitlabQuality
|
|
392
379
|
#
|
393
380
|
# @return [TrueClass|FalseClass] false if the test was skipped or failed because of a transient error that can be ignored.
|
394
381
|
# Otherwise returns true.
|
395
|
-
def should_report?(test,
|
396
|
-
return false
|
382
|
+
def should_report?(test, systemic_failure_messages)
|
383
|
+
return false unless test.failures?
|
397
384
|
|
398
|
-
puts " => Systemic
|
399
|
-
|
385
|
+
puts " => Systemic failures detected: #{systemic_failure_messages}" if systemic_failure_messages.any?
|
386
|
+
failure_to_ignore = IGNORED_FAILURES + systemic_failure_messages
|
400
387
|
|
401
|
-
|
402
|
-
reason = ignore_failure_reason(test.report['exceptions'], exceptions_to_ignore)
|
388
|
+
reason = ignored_failure_reason(test.failures, failure_to_ignore)
|
403
389
|
|
404
|
-
|
405
|
-
|
390
|
+
if reason
|
391
|
+
puts " => Failure reporting skipped because #{reason}"
|
406
392
|
|
407
|
-
|
408
|
-
|
393
|
+
false
|
394
|
+
else
|
395
|
+
true
|
409
396
|
end
|
410
|
-
|
411
|
-
true
|
412
397
|
end
|
413
398
|
|
414
399
|
# Determine any reason to ignore a failure.
|
415
400
|
#
|
416
|
-
# @param [Array<Hash>]
|
417
|
-
# @
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
return if exception_messages.empty? || exception_messages.size < exceptions.size
|
401
|
+
# @param [Array<Hash>] failures the failures associated with the failure.
|
402
|
+
# @param [Array<String>] failure_to_ignore the failures messages that should be ignored.
|
403
|
+
# @return [String] the reason to ignore the failures, or `nil` if any failures should not be ignored.
|
404
|
+
def ignored_failure_reason(failures, failure_to_ignore)
|
405
|
+
failures_to_ignore = compute_ignored_failures(failures, failure_to_ignore)
|
406
|
+
return if failures_to_ignore.empty? || failures_to_ignore.size < failures.size
|
423
407
|
|
424
|
-
|
425
|
-
|
408
|
+
"the errors included: #{failures_to_ignore.map { |e| "`#{e}`" }.join(', ')}"
|
409
|
+
end
|
410
|
+
|
411
|
+
# Determine the failures that should be ignored based on a list of exception messages to ignore.
|
412
|
+
#
|
413
|
+
# @param [Array<Hash>] failures the failures associated with the failure.
|
414
|
+
# @param [Array<String>] failure_to_ignore the failures messages that should be ignored.
|
415
|
+
# @return [Array<String>] the exception messages to ignore, or `nil` if any failures should not be ignored.
|
416
|
+
def compute_ignored_failures(failures, failure_to_ignore)
|
417
|
+
failures
|
418
|
+
.filter_map { |e| failure_to_ignore.find { |m| e['message'].include?(m) } }
|
419
|
+
.compact
|
426
420
|
end
|
427
421
|
end
|
428
422
|
end
|
@@ -30,7 +30,7 @@ module GitlabQuality
|
|
30
30
|
raise NotImplementedError
|
31
31
|
end
|
32
32
|
|
33
|
-
def
|
33
|
+
def test_hash(test)
|
34
34
|
OpenSSL::Digest::SHA256.hexdigest(test.file + test.name)
|
35
35
|
end
|
36
36
|
|
@@ -42,7 +42,7 @@ module GitlabQuality
|
|
42
42
|
| ------ | ------ |
|
43
43
|
| File | #{test_file_link(test)} |
|
44
44
|
| Description | `#{test.name}` |
|
45
|
-
| Hash | `#{
|
45
|
+
| Hash | `#{test_hash(test)}` |
|
46
46
|
#{"| Test case | #{test.testcase} |" if test.testcase}
|
47
47
|
DESCRIPTION
|
48
48
|
end
|
@@ -50,7 +50,7 @@ module GitlabQuality
|
|
50
50
|
def test_file_link(test)
|
51
51
|
path_prefix = test.file.start_with?('qa/') ? 'qa/' : ''
|
52
52
|
|
53
|
-
"[`#{path_prefix}#{test.file}`](#{FILE_BASE_URL}#{path_prefix}#{test.file})"
|
53
|
+
"[`#{path_prefix}#{test.file}`](#{FILE_BASE_URL}#{path_prefix}#{test.file}#L#{test.line_number})"
|
54
54
|
end
|
55
55
|
|
56
56
|
def new_issue_labels(_test)
|
@@ -128,6 +128,13 @@ module GitlabQuality
|
|
128
128
|
labels
|
129
129
|
end
|
130
130
|
|
131
|
+
def find_issues(test, labels)
|
132
|
+
gitlab.find_issues(options: { state: 'opened', labels: labels.to_a }).find_all do |issue|
|
133
|
+
issue_title = issue.title.strip
|
134
|
+
issue_title.include?(test.name) || issue_title.include?(partial_file_path(test.file))
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
131
138
|
def pipeline_name_label
|
132
139
|
case pipeline
|
133
140
|
when 'production'
|
@@ -40,7 +40,7 @@ module GitlabQuality
|
|
40
40
|
puts "Reporting tests in #{test_results.path}"
|
41
41
|
|
42
42
|
test_results.each do |test|
|
43
|
-
next if test.file.include?('/features/sanity/') || test.skipped
|
43
|
+
next if test.file.include?('/features/sanity/') || test.skipped?
|
44
44
|
|
45
45
|
puts "Reporting test: #{test.file} | #{test.name}\n"
|
46
46
|
|
@@ -0,0 +1,88 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GitlabQuality
|
4
|
+
module TestTooling
|
5
|
+
module Report
|
6
|
+
# Uses the API to create GitLab issues for slow tests
|
7
|
+
#
|
8
|
+
# - Takes the JSON test reports like rspec-*.json`
|
9
|
+
# - Takes a project where slow issues should be created
|
10
|
+
# - Find issue by title (with test description or test file)
|
11
|
+
# - Add test metadata, duration to the issue with group and category labels
|
12
|
+
class SlowTestIssue < ReportAsIssue
|
13
|
+
include Concerns::FindSetDri
|
14
|
+
include Concerns::GroupAndCategoryLabels
|
15
|
+
|
16
|
+
NEW_ISSUE_LABELS = Set.new(%w[test type::maintenance maintenance::performance priority::3 severity::3]).freeze
|
17
|
+
SEARCH_LABELS = %w[test maintenance::performance].freeze
|
18
|
+
|
19
|
+
MultipleIssuesFound = Class.new(StandardError)
|
20
|
+
|
21
|
+
TestLevelSpecification = Struct.new(:regex, :max_duration)
|
22
|
+
|
23
|
+
OTHER_TESTS_MAX_DURATION = 45.40 # seconds
|
24
|
+
|
25
|
+
TEST_LEVEL_SPECIFICATIONS = [
|
26
|
+
TestLevelSpecification.new(%r{/features/}, 50.13),
|
27
|
+
TestLevelSpecification.new(%r{/controllers|requests/}, 19.20),
|
28
|
+
TestLevelSpecification.new(%r{/lib/}, 27.12)
|
29
|
+
].freeze
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def run!
|
34
|
+
puts "Reporting slow tests in `#{files.join(',')}` as issues in project `#{project}` via the API at `#{Runtime::Env.gitlab_api_base}`."
|
35
|
+
|
36
|
+
TestResults::Builder.new(files).test_results_per_file do |test_results|
|
37
|
+
puts "=> Reporting #{test_results.count} tests in #{test_results.path}"
|
38
|
+
|
39
|
+
test_results.each do |test|
|
40
|
+
create_slow_issue(test) if should_create_slow_issue?(test)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def new_issue_title(test)
|
46
|
+
"Slow test in #{super}"
|
47
|
+
end
|
48
|
+
|
49
|
+
def new_issue_description(test)
|
50
|
+
super + [
|
51
|
+
"\n### Slow test",
|
52
|
+
"Slow tests detected, see guides for more details and how to improve them:",
|
53
|
+
"- [Top slow tests](https://docs.gitlab.com/ee/development/testing_guide/best_practices.html#top-slow-tests)",
|
54
|
+
"- [Test speed](https://docs.gitlab.com/ee/development/testing_guide/best_practices.html#test-speed)",
|
55
|
+
"**Duration**: #{test.run_time} seconds"
|
56
|
+
].compact.join("\n\n")
|
57
|
+
end
|
58
|
+
|
59
|
+
def create_slow_issue(test)
|
60
|
+
puts " => Finding existing issues for slow test '#{test.name}' (run time: #{test.run_time} seconds)..."
|
61
|
+
|
62
|
+
issues = find_issues(test, SEARCH_LABELS)
|
63
|
+
|
64
|
+
issues.each do |issue|
|
65
|
+
puts " => Existing issue link #{issue['web_url']}"
|
66
|
+
end
|
67
|
+
|
68
|
+
create_issue(test) unless issues.any?
|
69
|
+
rescue MultipleIssuesFound => e
|
70
|
+
warn(e.message)
|
71
|
+
end
|
72
|
+
|
73
|
+
def should_create_slow_issue?(test)
|
74
|
+
test.run_time > max_duration_for_test(test)
|
75
|
+
end
|
76
|
+
|
77
|
+
def max_duration_for_test(test)
|
78
|
+
test_level_specification = TEST_LEVEL_SPECIFICATIONS.find do |test_level_specification|
|
79
|
+
test.example_id =~ test_level_specification.regex
|
80
|
+
end
|
81
|
+
return OTHER_TESTS_MAX_DURATION unless test_level_specification
|
82
|
+
|
83
|
+
test_level_specification.max_duration
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GitlabQuality
|
4
|
+
module TestTooling
|
5
|
+
module TestResult
|
6
|
+
class BaseTestResult
|
7
|
+
attr_reader :report
|
8
|
+
|
9
|
+
def initialize(report)
|
10
|
+
@report = report
|
11
|
+
end
|
12
|
+
|
13
|
+
def stage
|
14
|
+
@stage ||= file[%r{(?:api|browser_ui)/(?:(?:\d+_)?(\w+))}, 1]
|
15
|
+
end
|
16
|
+
|
17
|
+
def name
|
18
|
+
raise NotImplementedError
|
19
|
+
end
|
20
|
+
|
21
|
+
def file
|
22
|
+
raise NotImplementedError
|
23
|
+
end
|
24
|
+
|
25
|
+
def skipped?
|
26
|
+
raise NotImplementedError
|
27
|
+
end
|
28
|
+
|
29
|
+
def failures
|
30
|
+
raise NotImplementedError
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GitlabQuality
|
4
|
+
module TestTooling
|
5
|
+
module TestResult
|
6
|
+
class JUnitTestResult < BaseTestResult
|
7
|
+
attr_accessor :testcase # Ignore it for now
|
8
|
+
|
9
|
+
def name
|
10
|
+
report['name']
|
11
|
+
end
|
12
|
+
|
13
|
+
def file
|
14
|
+
report['file'].delete_prefix('./')
|
15
|
+
end
|
16
|
+
|
17
|
+
def skipped?
|
18
|
+
report.search('skipped').any?
|
19
|
+
end
|
20
|
+
|
21
|
+
def failures # rubocop:disable Metrics/AbcSize
|
22
|
+
failures = report.search('failure')
|
23
|
+
return [] if failures.empty?
|
24
|
+
|
25
|
+
failures.map do |exception|
|
26
|
+
trace = exception.content.split("\n").map(&:strip)
|
27
|
+
spec_file_first_index = trace.rindex do |line|
|
28
|
+
line.include?(File.basename(report['file']))
|
29
|
+
end
|
30
|
+
|
31
|
+
exception['message'].gsub!(/(private_token=)[\w-]+/, '********')
|
32
|
+
|
33
|
+
{
|
34
|
+
'message' => "#{exception['type']}: #{exception['message']}",
|
35
|
+
'stacktrace' => trace.slice(0..spec_file_first_index).join("\n")
|
36
|
+
}
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,138 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GitlabQuality
|
4
|
+
module TestTooling
|
5
|
+
module TestResult
|
6
|
+
class JsonTestResult < BaseTestResult
|
7
|
+
PRIVATE_TOKEN_REGEX = /(private_token=)[\w-]+/
|
8
|
+
|
9
|
+
def name
|
10
|
+
report.fetch('full_description')
|
11
|
+
end
|
12
|
+
|
13
|
+
def file
|
14
|
+
report.fetch('file_path').delete_prefix('./')
|
15
|
+
end
|
16
|
+
|
17
|
+
def status
|
18
|
+
report.fetch('status')
|
19
|
+
end
|
20
|
+
|
21
|
+
def skipped?
|
22
|
+
status == 'pending'
|
23
|
+
end
|
24
|
+
|
25
|
+
def ci_job_url
|
26
|
+
report.fetch('ci_job_url', '')
|
27
|
+
end
|
28
|
+
|
29
|
+
def testcase
|
30
|
+
report.fetch('testcase', '')
|
31
|
+
end
|
32
|
+
|
33
|
+
def testcase=(new_testcase)
|
34
|
+
report['testcase'] = new_testcase
|
35
|
+
end
|
36
|
+
|
37
|
+
def failure_issue
|
38
|
+
report['failure_issue']
|
39
|
+
end
|
40
|
+
|
41
|
+
def failure_issue=(new_failure_issue)
|
42
|
+
report['failure_issue'] = new_failure_issue
|
43
|
+
end
|
44
|
+
|
45
|
+
def quarantine?
|
46
|
+
# The value for 'quarantine' could be nil, a hash, a string,
|
47
|
+
# or true (if the test just has the :quarantine tag)
|
48
|
+
# But any non-nil or false value should means the test is in quarantine
|
49
|
+
!!quarantine
|
50
|
+
end
|
51
|
+
|
52
|
+
def quarantine_type
|
53
|
+
quarantine['type'] if quarantine?
|
54
|
+
end
|
55
|
+
|
56
|
+
def quarantine_issue
|
57
|
+
quarantine['issue'] if quarantine?
|
58
|
+
end
|
59
|
+
|
60
|
+
def screenshot?
|
61
|
+
!!screenshot
|
62
|
+
end
|
63
|
+
|
64
|
+
def screenshot_image
|
65
|
+
screenshot['image'] if screenshot?
|
66
|
+
end
|
67
|
+
|
68
|
+
def product_group
|
69
|
+
report['product_group'].to_s
|
70
|
+
end
|
71
|
+
|
72
|
+
def product_group?
|
73
|
+
product_group != ''
|
74
|
+
end
|
75
|
+
|
76
|
+
def feature_category
|
77
|
+
report['feature_category']
|
78
|
+
end
|
79
|
+
|
80
|
+
def run_time
|
81
|
+
report['run_time'].to_f.round(2)
|
82
|
+
end
|
83
|
+
|
84
|
+
def example_id
|
85
|
+
report['id']
|
86
|
+
end
|
87
|
+
|
88
|
+
def line_number
|
89
|
+
report['line_number']
|
90
|
+
end
|
91
|
+
|
92
|
+
def failures # rubocop:disable Metrics/AbcSize
|
93
|
+
@failures ||=
|
94
|
+
report.fetch('exceptions', []).filter_map do |exception|
|
95
|
+
backtrace = exception['backtrace']
|
96
|
+
next unless backtrace.respond_to?(:rindex)
|
97
|
+
|
98
|
+
spec_file_first_index = backtrace.rindex do |line|
|
99
|
+
line.include?(File.basename(report['file_path']))
|
100
|
+
end
|
101
|
+
|
102
|
+
message = redact_private_token(exception['message'])
|
103
|
+
message_lines = Array(exception['message_lines']).map { |line| redact_private_token(line) }
|
104
|
+
|
105
|
+
{
|
106
|
+
'message' => "#{exception['class']}: #{message}",
|
107
|
+
'message_lines' => message_lines,
|
108
|
+
'stacktrace' => "#{format_message_lines(message_lines)}\n#{backtrace.slice(0..spec_file_first_index).join("\n")}",
|
109
|
+
'correlation_id' => exception['correlation_id']
|
110
|
+
}
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def failures?
|
115
|
+
failures.any?
|
116
|
+
end
|
117
|
+
|
118
|
+
private
|
119
|
+
|
120
|
+
def quarantine
|
121
|
+
report.fetch('quarantine', nil)
|
122
|
+
end
|
123
|
+
|
124
|
+
def screenshot
|
125
|
+
report.fetch('screenshot', nil)
|
126
|
+
end
|
127
|
+
|
128
|
+
def format_message_lines(message_lines)
|
129
|
+
message_lines.is_a?(Array) ? message_lines.join("\n") : message_lines
|
130
|
+
end
|
131
|
+
|
132
|
+
def redact_private_token(text)
|
133
|
+
text.gsub(PRIVATE_TOKEN_REGEX, '********')
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
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: 0.
|
4
|
+
version: 0.9.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: 2023-
|
11
|
+
date: 2023-07-05 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: climate_control
|
@@ -323,6 +323,7 @@ executables:
|
|
323
323
|
- prepare-stage-reports
|
324
324
|
- relate-failure-issue
|
325
325
|
- report-results
|
326
|
+
- slow-test-issues
|
326
327
|
- update-screenshot-paths
|
327
328
|
extensions: []
|
328
329
|
extra_rdoc_files: []
|
@@ -331,6 +332,7 @@ files:
|
|
331
332
|
- ".rubocop.yml"
|
332
333
|
- ".rubocop_todo.yml"
|
333
334
|
- ".ruby-version"
|
335
|
+
- ".tool-versions"
|
334
336
|
- CODE_OF_CONDUCT.md
|
335
337
|
- CONTRIBUTING.md
|
336
338
|
- Dangerfile
|
@@ -345,6 +347,7 @@ files:
|
|
345
347
|
- exe/prepare-stage-reports
|
346
348
|
- exe/relate-failure-issue
|
347
349
|
- exe/report-results
|
350
|
+
- exe/slow-test-issues
|
348
351
|
- exe/update-screenshot-paths
|
349
352
|
- lefthook.yml
|
350
353
|
- lib/gitlab_quality/test_tooling.rb
|
@@ -352,6 +355,7 @@ files:
|
|
352
355
|
- lib/gitlab_quality/test_tooling/gitlab_issue_dry_client.rb
|
353
356
|
- lib/gitlab_quality/test_tooling/labels_inference.rb
|
354
357
|
- lib/gitlab_quality/test_tooling/report/concerns/find_set_dri.rb
|
358
|
+
- lib/gitlab_quality/test_tooling/report/concerns/group_and_category_labels.rb
|
355
359
|
- lib/gitlab_quality/test_tooling/report/concerns/results_reporter.rb
|
356
360
|
- lib/gitlab_quality/test_tooling/report/concerns/utils.rb
|
357
361
|
- lib/gitlab_quality/test_tooling/report/generate_test_session.rb
|
@@ -361,6 +365,7 @@ files:
|
|
361
365
|
- lib/gitlab_quality/test_tooling/report/report_results.rb
|
362
366
|
- lib/gitlab_quality/test_tooling/report/results_in_issues.rb
|
363
367
|
- lib/gitlab_quality/test_tooling/report/results_in_test_cases.rb
|
368
|
+
- lib/gitlab_quality/test_tooling/report/slow_test_issue.rb
|
364
369
|
- lib/gitlab_quality/test_tooling/report/update_screenshot_path.rb
|
365
370
|
- lib/gitlab_quality/test_tooling/runtime/env.rb
|
366
371
|
- lib/gitlab_quality/test_tooling/runtime/logger.rb
|
@@ -379,11 +384,13 @@ files:
|
|
379
384
|
- lib/gitlab_quality/test_tooling/system_logs/log_types/rails/graphql_log.rb
|
380
385
|
- lib/gitlab_quality/test_tooling/system_logs/shared_fields.rb
|
381
386
|
- lib/gitlab_quality/test_tooling/system_logs/system_logs_formatter.rb
|
387
|
+
- lib/gitlab_quality/test_tooling/test_result/base_test_result.rb
|
388
|
+
- lib/gitlab_quality/test_tooling/test_result/j_unit_test_result.rb
|
389
|
+
- lib/gitlab_quality/test_tooling/test_result/json_test_result.rb
|
382
390
|
- lib/gitlab_quality/test_tooling/test_results/base_test_results.rb
|
383
391
|
- lib/gitlab_quality/test_tooling/test_results/builder.rb
|
384
392
|
- lib/gitlab_quality/test_tooling/test_results/j_unit_test_results.rb
|
385
393
|
- lib/gitlab_quality/test_tooling/test_results/json_test_results.rb
|
386
|
-
- lib/gitlab_quality/test_tooling/test_results/test_result.rb
|
387
394
|
- lib/gitlab_quality/test_tooling/version.rb
|
388
395
|
- sig/gitlab_quality/test_tooling.rbs
|
389
396
|
homepage: https://gitlab.com/gitlab-org/ruby/gems/gitlab_quality-test_tooling
|
@@ -1,188 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'active_support/core_ext/object/blank'
|
4
|
-
|
5
|
-
module GitlabQuality
|
6
|
-
module TestTooling
|
7
|
-
module TestResults
|
8
|
-
class TestResult
|
9
|
-
def self.from_json(report)
|
10
|
-
JsonTestResult.new(report)
|
11
|
-
end
|
12
|
-
|
13
|
-
def self.from_junit(report)
|
14
|
-
JUnitTestResult.new(report)
|
15
|
-
end
|
16
|
-
|
17
|
-
attr_accessor :report, :failures
|
18
|
-
|
19
|
-
def initialize(report)
|
20
|
-
self.report = report
|
21
|
-
self.failures = failures_from_exceptions
|
22
|
-
end
|
23
|
-
|
24
|
-
def stage
|
25
|
-
@stage ||= file[%r{(?:api|browser_ui)/(?:(?:\d+_)?(\w+))}, 1]
|
26
|
-
end
|
27
|
-
|
28
|
-
def name
|
29
|
-
raise NotImplementedError
|
30
|
-
end
|
31
|
-
|
32
|
-
def file
|
33
|
-
raise NotImplementedError
|
34
|
-
end
|
35
|
-
|
36
|
-
def skipped
|
37
|
-
raise NotImplementedError
|
38
|
-
end
|
39
|
-
|
40
|
-
private
|
41
|
-
|
42
|
-
def failures_from_exceptions
|
43
|
-
raise NotImplementedError
|
44
|
-
end
|
45
|
-
|
46
|
-
class JsonTestResult < TestResult
|
47
|
-
def name
|
48
|
-
report['full_description']
|
49
|
-
end
|
50
|
-
|
51
|
-
def file
|
52
|
-
report['file_path'].delete_prefix('./')
|
53
|
-
end
|
54
|
-
|
55
|
-
def status
|
56
|
-
report['status']
|
57
|
-
end
|
58
|
-
|
59
|
-
def ci_job_url
|
60
|
-
report['ci_job_url']
|
61
|
-
end
|
62
|
-
|
63
|
-
def skipped
|
64
|
-
status == 'pending'
|
65
|
-
end
|
66
|
-
|
67
|
-
def testcase
|
68
|
-
report['testcase']
|
69
|
-
end
|
70
|
-
|
71
|
-
def testcase=(new_testcase)
|
72
|
-
report['testcase'] = new_testcase
|
73
|
-
end
|
74
|
-
|
75
|
-
def failure_issue
|
76
|
-
report['failure_issue']
|
77
|
-
end
|
78
|
-
|
79
|
-
def failure_issue=(new_failure_issue)
|
80
|
-
report['failure_issue'] = new_failure_issue
|
81
|
-
end
|
82
|
-
|
83
|
-
def quarantine?
|
84
|
-
# The value for 'quarantine' could be nil, a hash, a string,
|
85
|
-
# or true (if the test just has the :quarantine tag)
|
86
|
-
# But any non-nil or false value should means the test is in quarantine
|
87
|
-
report['quarantine'].present?
|
88
|
-
end
|
89
|
-
|
90
|
-
def quarantine_type
|
91
|
-
report['quarantine']['type'] if quarantine?
|
92
|
-
end
|
93
|
-
|
94
|
-
def quarantine_issue
|
95
|
-
report['quarantine']['issue'] if quarantine?
|
96
|
-
end
|
97
|
-
|
98
|
-
def screenshot?
|
99
|
-
report['screenshot'].present?
|
100
|
-
end
|
101
|
-
|
102
|
-
def failure_screenshot
|
103
|
-
report['screenshot']['image'] if screenshot?
|
104
|
-
end
|
105
|
-
|
106
|
-
def product_group?
|
107
|
-
report['product_group'].present?
|
108
|
-
end
|
109
|
-
|
110
|
-
def product_group
|
111
|
-
report['product_group']
|
112
|
-
end
|
113
|
-
|
114
|
-
def feature_category
|
115
|
-
report['feature_category']
|
116
|
-
end
|
117
|
-
|
118
|
-
private
|
119
|
-
|
120
|
-
# rubocop:disable Metrics/AbcSize
|
121
|
-
def failures_from_exceptions
|
122
|
-
return [] unless report.key?('exceptions')
|
123
|
-
|
124
|
-
report['exceptions'].map do |exception|
|
125
|
-
spec_file_first_index = exception['backtrace'].rindex do |line|
|
126
|
-
line.include?(File.basename(report['file_path']))
|
127
|
-
end
|
128
|
-
|
129
|
-
exception['message'].gsub!(/(private_token=)[\w-]+/, '********')
|
130
|
-
Array(exception['message_lines']).each { |line| line.gsub!(/(private_token=)([\w-]+)/, '********') }
|
131
|
-
|
132
|
-
{
|
133
|
-
'message' => "#{exception['class']}: #{exception['message']}",
|
134
|
-
'message_lines' => exception['message_lines'],
|
135
|
-
'stacktrace' => "#{format_message_lines(exception['message_lines'])}\n#{exception['backtrace'].slice(0..spec_file_first_index).join("\n")}",
|
136
|
-
'correlation_id' => exception['correlation_id']
|
137
|
-
}
|
138
|
-
end
|
139
|
-
end
|
140
|
-
|
141
|
-
def format_message_lines(message_lines)
|
142
|
-
message_lines.is_a?(Array) ? message_lines.join("\n") : message_lines
|
143
|
-
end
|
144
|
-
# rubocop:enable Metrics/AbcSize
|
145
|
-
end
|
146
|
-
|
147
|
-
class JUnitTestResult < TestResult
|
148
|
-
def name
|
149
|
-
report['name']
|
150
|
-
end
|
151
|
-
|
152
|
-
def file
|
153
|
-
report['file'].delete_prefix('./')
|
154
|
-
end
|
155
|
-
|
156
|
-
def skipped
|
157
|
-
report.search('skipped').any?
|
158
|
-
end
|
159
|
-
|
160
|
-
attr_accessor :testcase # Ignore it for now
|
161
|
-
|
162
|
-
private
|
163
|
-
|
164
|
-
# rubocop:disable Metrics/AbcSize
|
165
|
-
def failures_from_exceptions
|
166
|
-
failures = report.search('failure')
|
167
|
-
return [] if failures.empty?
|
168
|
-
|
169
|
-
failures.map do |exception|
|
170
|
-
trace = exception.content.split("\n").map(&:strip)
|
171
|
-
spec_file_first_index = trace.rindex do |line|
|
172
|
-
line.include?(File.basename(report['file']))
|
173
|
-
end
|
174
|
-
|
175
|
-
exception['message'].gsub!(/(private_token=)[\w-]+/, '********')
|
176
|
-
|
177
|
-
{
|
178
|
-
'message' => "#{exception['type']}: #{exception['message']}",
|
179
|
-
'stacktrace' => trace.slice(0..spec_file_first_index).join("\n")
|
180
|
-
}
|
181
|
-
end
|
182
|
-
end
|
183
|
-
# rubocop:enable Metrics/AbcSize
|
184
|
-
end
|
185
|
-
end
|
186
|
-
end
|
187
|
-
end
|
188
|
-
end
|