gitlab_quality-test_tooling 0.8.2 → 0.9.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/.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
|