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 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