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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d201b5e75d11490d7b8a3f7b3abdc883442487d07d19f9c5c7cf02577a867116
4
- data.tar.gz: 0b50d503726912d587bb2b3d4bd1e1c2194b8f0773d2a37905afa0c66dfbbaee
3
+ metadata.gz: e9bcb8c6202edb63313039592fdf1a6c953c0b43db4566d9e84105e31d2029d3
4
+ data.tar.gz: 27dcc6d668da50a3ec76bd13b8898d61b24bbe2835faeee6fc6dbce684945388
5
5
  SHA512:
6
- metadata.gz: e5b8c615d62f29710c55a3d39373efb71cf1b7895d3b7ca08d6a4e0077e47b7a0c109f111716d49d7d5adcac4b3ffc8668c20fcb95fc01bfb2244c38bfafba62
7
- data.tar.gz: 30b05b1eac9f9b6400e089b1de4e229fcd5769aae2a41066bc43e79e08f6a6c37db8e4b7cbbfd46933021eabafa098be601a546c5cc05d7a93fa4c01d5c14021
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
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- gitlab_quality-test_tooling (0.8.2)
4
+ gitlab_quality-test_tooling (0.9.0)
5
5
  activesupport (>= 6.1, < 7.1)
6
6
  gitlab (~> 4.19)
7
7
  http (~> 5.0)
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
- IGNORE_EXCEPTIONS = [
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
- systemic_exceptions = systemic_exceptions_for_test_results(test_results)
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, systemic_exceptions)
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 systemic_exceptions_for_test_results(test_results)
67
+ def systemic_failures_for_test_results(test_results)
67
68
  test_results
68
- .flat_map { |test| test.report['exceptions']&.map { |exception| exception['message'] } }
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
- search_labels = (base_issue_labels + Set.new(%w[test])).to_a
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
- if test.failures.first['message_lines'].empty? || test.failures.first['message_lines'].instance_of?(String)
183
- test.failures.first['message']
179
+ first_failure = test.failures.first
180
+
181
+ if first_failure['message_lines'].empty?
182
+ first_failure['message']
184
183
  else
185
- test.failures.first['message_lines'].join("\n")
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.failure_screenshot)
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, systemic_exceptions)
396
- return false if test.failures.empty?
382
+ def should_report?(test, systemic_failure_messages)
383
+ return false unless test.failures?
397
384
 
398
- puts " => Systemic exceptions detected: #{systemic_exceptions}" if systemic_exceptions.any?
399
- exceptions_to_ignore = IGNORE_EXCEPTIONS + systemic_exceptions
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
- if test.report.key?('exceptions')
402
- reason = ignore_failure_reason(test.report['exceptions'], exceptions_to_ignore)
388
+ reason = ignored_failure_reason(test.failures, failure_to_ignore)
403
389
 
404
- if reason
405
- puts " => Failure reporting skipped because #{reason}"
390
+ if reason
391
+ puts " => Failure reporting skipped because #{reason}"
406
392
 
407
- return false
408
- end
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>] exceptions the exceptions associated with the failure.
417
- # @return [String] the reason to ignore the exceptions, or `nil` if any exceptions should not be ignored.
418
- def ignore_failure_reason(exceptions, ignored_exceptions)
419
- exception_messages = exceptions
420
- .filter_map { |exception| exception['message'] if ignored_exceptions.any? { |e| exception['message'].include?(e) } }
421
- .compact
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
- msg = exception_messages.many? ? 'the errors were' : 'the error was'
425
- "#{msg} #{exception_messages.join(', ')}"
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 failed_test_hash(test)
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 | `#{failed_test_hash(test)}` |
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
 
@@ -35,7 +35,7 @@ module GitlabQuality
35
35
  end
36
36
 
37
37
  def post_note(issue, test)
38
- return false if test.skipped
38
+ return false if test.skipped?
39
39
  return false if test.failures.empty?
40
40
 
41
41
  note = note_content(test)
@@ -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
@@ -18,7 +18,7 @@ module GitlabQuality
18
18
 
19
19
  def process
20
20
  results.xpath('//testcase').map do |test|
21
- TestResult.from_junit(test)
21
+ GitlabQuality::TestTooling::TestResult::JUnitTestResult.new(test)
22
22
  end
23
23
  end
24
24
  end
@@ -20,7 +20,7 @@ module GitlabQuality
20
20
 
21
21
  def process
22
22
  results['examples'].map do |test|
23
- TestResult.from_json(test)
23
+ GitlabQuality::TestTooling::TestResult::JsonTestResult.new(test)
24
24
  end
25
25
  end
26
26
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module GitlabQuality
4
4
  module TestTooling
5
- VERSION = "0.8.2"
5
+ VERSION = "0.9.0"
6
6
  end
7
7
  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.8.2
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-06-19 00:00:00.000000000 Z
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