gitlab_quality-test_tooling 1.27.1 → 1.29.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.
Files changed (24) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +11 -2
  3. data/Gemfile.lock +2 -2
  4. data/exe/post-to-slack +21 -3
  5. data/lib/gitlab_quality/test_tooling/gitlab_client/repository_files_client.rb +4 -0
  6. data/lib/gitlab_quality/test_tooling/report/concerns/issue_reports.rb +61 -8
  7. data/lib/gitlab_quality/test_tooling/report/failed_test_issue.rb +16 -109
  8. data/lib/gitlab_quality/test_tooling/report/flaky_test_issue.rb +13 -106
  9. data/lib/gitlab_quality/test_tooling/report/generate_test_session.rb +1 -1
  10. data/lib/gitlab_quality/test_tooling/report/health_problem_reporter.rb +194 -0
  11. data/lib/gitlab_quality/test_tooling/report/merge_request_slow_tests_report.rb +1 -1
  12. data/lib/gitlab_quality/test_tooling/report/relate_failure_issue.rb +1 -1
  13. data/lib/gitlab_quality/test_tooling/report/report_as_issue.rb +12 -6
  14. data/lib/gitlab_quality/test_tooling/report/report_results.rb +1 -1
  15. data/lib/gitlab_quality/test_tooling/report/slow_test_issue.rb +13 -102
  16. data/lib/gitlab_quality/test_tooling/summary_table.rb +9 -5
  17. data/lib/gitlab_quality/test_tooling/test_metrics_exporter/test_metrics.rb +3 -1
  18. data/lib/gitlab_quality/test_tooling/test_result/base_test_result.rb +30 -1
  19. data/lib/gitlab_quality/test_tooling/test_results/base_test_results.rb +5 -2
  20. data/lib/gitlab_quality/test_tooling/test_results/builder.rb +7 -4
  21. data/lib/gitlab_quality/test_tooling/test_results/j_unit_test_results.rb +1 -1
  22. data/lib/gitlab_quality/test_tooling/test_results/json_test_results.rb +1 -1
  23. data/lib/gitlab_quality/test_tooling/version.rb +1 -1
  24. metadata +3 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 34edb6377d3304f4b061a5704e9ba865cd550209c7fefda3c66b6734f2b95ce5
4
- data.tar.gz: e00f10eaf9690f803168a86da44417b5bdc02f074773819f6deb6fa466bef23e
3
+ metadata.gz: ca3b15faac90436068b21df05071cbcd06dd1a71d33b3220a58d17201a893a2d
4
+ data.tar.gz: dee6c41cfe097f6af193890eee46a95a17f1fa7724377fb8529f75142931bbb1
5
5
  SHA512:
6
- metadata.gz: 3ced20afebe7dbdcc5e47fcc3c864db514bd2d2b4920d6085683c201444243e5ec023cde39c7d333d28acd6a2539f35dfe6af21ee881d2442f48e5c4335b164c
7
- data.tar.gz: 9d279b982fb6438e064db0336b532fbd0ba7e803e16b65e678576fca037120cc7a327bba21d6ab3400122e6aee18060e900eb14d1063aed77f61d57df73f8cab
6
+ metadata.gz: ab45b4d7059cfb911b103f952f0e072dd578057a24fb539ac322087b09d4cd646ecdeb0c7a528427cd378f67a056d374a663c1effb62945a2001b18fea26af8c
7
+ data.tar.gz: 387923350e8554c3fd99d17d3a1ea502768d4cfc251805a5c76a17bca950ef28e616ec161eb329c0482b678f5e3a037c3235ca6562eb93008e57c6963838a3ba
data/.rubocop.yml CHANGED
@@ -8,8 +8,8 @@ inherit_from:
8
8
  <% end %>
9
9
 
10
10
  AllCops:
11
- # Target the current Ruby version. For example, "2.7" or "3.0".
12
- TargetRubyVersion: <%= RUBY_VERSION[/^\d+\.\d+/, 0] %>
11
+ # The oldest supported Ruby version.
12
+ TargetRubyVersion: 3.1
13
13
  Exclude:
14
14
  - 'vendor/**/*'
15
15
  - 'tmp/**/*'
@@ -63,3 +63,12 @@ Layout/SpaceBeforeFirstArg:
63
63
 
64
64
  RSpec/MultipleMemoizedHelpers:
65
65
  Enabled: false
66
+
67
+ # Short-hand Hash syntax does not work prior 3.1.
68
+ # See https://gitlab.com/gitlab-org/gitlab/-/issues/435940#note_1703307479
69
+ Style/HashSyntax:
70
+ EnforcedShorthandSyntax: never
71
+
72
+ # Anonymous block forwarding does not work prior 3.1.
73
+ Naming/BlockForwarding:
74
+ Enabled: false
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- gitlab_quality-test_tooling (1.27.1)
4
+ gitlab_quality-test_tooling (1.29.0)
5
5
  activesupport (>= 7.0, < 7.2)
6
6
  amatch (~> 0.4.1)
7
7
  gitlab (~> 4.19)
@@ -330,4 +330,4 @@ DEPENDENCIES
330
330
  webmock (= 3.7.0)
331
331
 
332
332
  BUNDLED WITH
333
- 2.5.4
333
+ 2.5.6
data/exe/post-to-slack CHANGED
@@ -7,6 +7,7 @@ require "optparse"
7
7
  require_relative "../lib/gitlab_quality/test_tooling"
8
8
 
9
9
  params = {}
10
+ summary_table_opts = {}
10
11
 
11
12
  messages = []
12
13
  gitlab_api_token = nil
@@ -30,8 +31,24 @@ options = OptionParser.new do |opts|
30
31
  messages << message
31
32
  end
32
33
 
34
+ opts.on('-s', '--sort-by SORT', String,
35
+ 'Used with the `--include-summary-table` flag — An optional sort for the test summary table. This flag must be positioned before `--include-summary-table`') do |sort_by|
36
+ valid_options = ["Dev Stage", "Total", "Failures", "Errors", "Skipped", "Result"].freeze
37
+
38
+ raise ArgumentError, "Invalid sort option: #{sort_by}. Valid options are: #{valid_options.join(', ')}" unless valid_options.include?(sort_by)
39
+
40
+ summary_table_opts[:sort_by] = sort_by
41
+ end
42
+
43
+ opts.on('-d', '--sort-direction [asc|desc]', String,
44
+ 'Used with the `--sort-by` flag — Define the sort direction of the test summary table (default: `asc`)') do |sort_direction|
45
+ raise ArgumentError, "Invalid sort direction: #{sort_direction}. Valid options are: asc, desc" unless %w[asc desc].include?(sort_direction.downcase)
46
+
47
+ summary_table_opts[:sort_direction] = sort_direction.downcase.to_sym
48
+ end
49
+
33
50
  opts.on('-t', '--include-summary-table FILES', String, 'Add a test summary table based on RSpec report files (JUnit XML)') do |files|
34
- messages << GitlabQuality::TestTooling::SummaryTable.create(input_files: files)
51
+ params[:summary_table_files] = files
35
52
  end
36
53
 
37
54
  opts.on('-j', '--include-failed-jobs-table', 'Add a list of failed jobs in the pipeline') do
@@ -71,9 +88,10 @@ options = OptionParser.new do |opts|
71
88
  opts.parse(ARGV)
72
89
  end
73
90
 
74
- params[:message] = messages.join("\n")
75
-
76
91
  if params.any?
92
+ messages << GitlabQuality::TestTooling::SummaryTable.create(input_files: params.delete(:summary_table_files), **summary_table_opts) if params.key?(:summary_table_files)
93
+ params[:message] = messages.join("\n")
94
+
77
95
  GitlabQuality::TestTooling::Slack::PostToSlack.new(**params).invoke!
78
96
  else
79
97
  puts options
@@ -18,6 +18,10 @@ module GitlabQuality
18
18
  client.file_contents(project, file_path.gsub(%r{^/}, ""), ref)
19
19
  end
20
20
  end
21
+
22
+ def file_contents_at_line(line_number)
23
+ file_contents.lines(chomp: true)[line_number - 1]
24
+ end
21
25
  end
22
26
  end
23
27
  end
@@ -9,8 +9,9 @@ module GitlabQuality
9
9
  module IssueReports
10
10
  JOB_URL_REGEX = %r{(?<job_url>https://(?<host>[\w.]+)/(?<project_path>[\w\-./]+)/-/jobs/\d+)}
11
11
  FAILED_JOB_DESCRIPTION_REGEX = /First happened in #{JOB_URL_REGEX}\./m
12
- REPORT_ITEM_REGEX = /^1\. \d{4}-\d{2}-\d{2}: #{JOB_URL_REGEX} \((?<pipeline_url>\S+)\)/
12
+ REPORT_ITEM_REGEX = /^1\. (?<report_date>\d{4}-\d{2}-\d{2}): #{JOB_URL_REGEX} \((?<pipeline_url>\S+)\) ?(?<extra_content>.*)$/
13
13
  LATEST_REPORTS_TO_SHOW = 10
14
+ DAILY_REPORTS_THRESHOLDS = 10
14
15
 
15
16
  class ReportsList
16
17
  def initialize(preserved_content:, section_header:, reports:, extra_content:)
@@ -21,7 +22,12 @@ module GitlabQuality
21
22
  end
22
23
 
23
24
  def self.report_list_item(test, item_extra_content: nil)
24
- "1. #{Time.new.utc.strftime('%F')}: #{test.ci_job_url} (#{ENV.fetch('CI_PIPELINE_URL', 'pipeline url is missing')}) #{item_extra_content}".strip
25
+ ReportListItem.new(job_url: test.ci_job_url, extra_content: item_extra_content)
26
+ end
27
+
28
+ def self.parse_report_date_from_string(date_string)
29
+ parsed_time = Time.strptime(date_string, '%F')
30
+ Time.utc(parsed_time.year, parsed_time.month, parsed_time.day)
25
31
  end
26
32
 
27
33
  def reports_count
@@ -32,21 +38,27 @@ module GitlabQuality
32
38
  [
33
39
  preserved_content,
34
40
  "#{section_header} (#{reports_count})",
35
- reports_list(reports),
41
+ reports_list,
36
42
  extra_content
37
43
  ].reject(&:blank?).compact.join("\n\n")
38
44
  end
39
45
 
46
+ def spiked_in_short_period?
47
+ latest_report = sorted_reports.first
48
+
49
+ reports_for_latest_report_day = sorted_reports.count { |report| report.report_date == latest_report.report_date }
50
+
51
+ reports_for_latest_report_day >= DAILY_REPORTS_THRESHOLDS
52
+ end
53
+
40
54
  private
41
55
 
42
56
  attr_reader :preserved_content, :section_header, :reports, :extra_content
43
57
 
44
- def reports_list(reports)
45
- sorted_reports = reports.sort.reverse
46
-
58
+ def reports_list
47
59
  if sorted_reports.size > LATEST_REPORTS_TO_SHOW
48
60
  [
49
- "Last 10 reports:",
61
+ "Last #{LATEST_REPORTS_TO_SHOW} reports:",
50
62
  sorted_reports[...LATEST_REPORTS_TO_SHOW].join("\n"),
51
63
  "<details><summary>See #{sorted_reports.size - LATEST_REPORTS_TO_SHOW} more reports</summary>",
52
64
  sorted_reports[LATEST_REPORTS_TO_SHOW..].join("\n"),
@@ -56,6 +68,41 @@ module GitlabQuality
56
68
  sorted_reports.join("\n")
57
69
  end
58
70
  end
71
+
72
+ def sorted_reports
73
+ @sorted_reports ||= reports.sort.reverse
74
+ end
75
+ end
76
+
77
+ class ReportListItem
78
+ attr_reader :report_date
79
+
80
+ def initialize(job_url:, report_date: now, pipeline_url: default_pipeline_url, extra_content: '')
81
+ @job_url = job_url
82
+ @report_date = report_date
83
+ @pipeline_url = pipeline_url
84
+ @extra_content = extra_content
85
+ end
86
+
87
+ def to_s
88
+ "1. #{report_date}: #{job_url} (#{pipeline_url}) #{extra_content}".strip
89
+ end
90
+
91
+ def <=>(other)
92
+ to_s <=> other.to_s
93
+ end
94
+
95
+ private
96
+
97
+ attr_reader :job_url, :pipeline_url, :extra_content
98
+
99
+ def default_pipeline_url
100
+ ENV.fetch('CI_PIPELINE_URL', 'pipeline url is missing')
101
+ end
102
+
103
+ def now
104
+ Time.new.utc.strftime('%F')
105
+ end
59
106
  end
60
107
 
61
108
  def initial_reports_section(test)
@@ -98,7 +145,13 @@ module GitlabQuality
98
145
  private
99
146
 
100
147
  def report_lines(content)
101
- content.lines.grep(REPORT_ITEM_REGEX).map(&:strip)
148
+ content.lines.filter_map do |line|
149
+ match = line.match(REPORT_ITEM_REGEX)
150
+ next unless match
151
+
152
+ match_data_hash = match.named_captures.transform_keys(&:to_sym)
153
+ ReportListItem.new(**match_data_hash.slice(:job_url, :report_date, :pipeline_url, :extra_content))
154
+ end
102
155
  end
103
156
 
104
157
  def job_urls_from_description(issue_description, regex)
@@ -10,16 +10,9 @@ module GitlabQuality
10
10
  # - For every passed test in the report:
11
11
  # - Find issue by test hash or create a new issue if no issue was found
12
12
  # - Add a failure report in the "Failure reports" note
13
- class FailedTestIssue < ReportAsIssue
14
- include Concerns::GroupAndCategoryLabels
15
- include Concerns::IssueReports
16
-
13
+ class FailedTestIssue < HealthProblemReporter
17
14
  IDENTITY_LABELS = ['test', 'automation:bot-authored'].freeze
18
15
  NEW_ISSUE_LABELS = Set.new(['type::maintenance', 'failure::new', 'priority::3', 'severity::3', *IDENTITY_LABELS]).freeze
19
- SEARCH_LABELS = ['test'].freeze
20
- FOUND_IN_MR_LABEL = '~"found:in MR"'
21
- FOUND_IN_MASTER_LABEL = '~"found:master"'
22
- REPORTS_DISCUSSION_HEADER = '### Failure reports'
23
16
  REPORT_SECTION_HEADER = '#### Failure reports'
24
17
 
25
18
  FAILURE_STACKTRACE_REGEX = %r{(?:(?:.*Failure/Error:(?<stacktrace>.+))|(?<stacktrace>.+))}m
@@ -42,93 +35,36 @@ module GitlabQuality
42
35
 
43
36
  attr_reader :base_issue_labels, :max_diff_ratio
44
37
 
45
- def run!
46
- puts "Reporting failed tests in `#{files.join(',')}` as issues in project `#{project}` via the API at `#{Runtime::Env.gitlab_api_base}`."
47
-
48
- TestResults::Builder.new(files).test_results_per_file do |test_results|
49
- puts "=> Reporting #{test_results.count} tests in #{test_results.path}"
50
-
51
- process_test_results(test_results)
52
- end
53
- end
54
-
55
- def process_test_results(test_results)
56
- test_results.each do |test|
57
- next unless test_is_applicable?(test)
58
-
59
- puts " => Reporting failure for test '#{test.name}'..."
60
-
61
- issues = find_issues_by_hash(test_hash(test), state: 'opened', labels: SEARCH_LABELS)
62
-
63
- if issues.empty?
64
- issues << create_issue(test)
65
- else
66
- # Keep issues description up-to-date
67
- update_issues(issues, test)
68
- end
69
-
70
- update_reports(issues, test)
71
- collect_issues(test, issues)
72
- end
38
+ def problem_type
39
+ 'failed'
73
40
  end
74
41
 
75
42
  def test_is_applicable?(test)
76
43
  test.status == 'failed'
77
44
  end
78
45
 
79
- def up_to_date_labels(test:, issue: nil, new_labels: Set.new)
80
- (base_issue_labels + super).to_a
46
+ def report_in_discussion?
47
+ true
81
48
  end
82
49
 
83
- def update_reports(issues, test)
84
- issues.each do |issue|
85
- puts " => Adding the failed test to the existing issue: #{issue.web_url}"
86
- add_report_to_issue(issue: issue, test: test, related_issues: (issues - [issue]))
87
- end
50
+ def identity_labels
51
+ IDENTITY_LABELS
88
52
  end
89
53
 
90
- def add_report_to_issue(issue:, test:, related_issues:) # rubocop:disable Metrics/AbcSize:
91
- reports_discussion = find_or_create_reports_discussion(issue: issue)
92
- current_reports_note = find_failure_discussion_note(issue: issue, test: test, reports_discussion: reports_discussion)
93
- new_reports_list = add_report_for_test(current_reports_content: current_reports_note&.body.to_s, test: test)
94
-
95
- note_body = [
96
- new_reports_list.to_s,
97
- identity_labels_quick_action,
98
- relate_issues_quick_actions(related_issues)
99
- ].join("\n")
100
-
101
- if current_reports_note
102
- gitlab.edit_issue_note(
103
- issue_iid: issue.iid,
104
- note_id: current_reports_note.id,
105
- note: note_body
106
- )
107
- else
108
- gitlab.add_note_to_issue_discussion_as_thread(
109
- iid: issue.iid,
110
- discussion_id: reports_discussion.id,
111
- note: note_body
112
- )
113
- end
114
- rescue MultipleNotesFound => e
115
- warn(e.message)
54
+ def report_section_header
55
+ REPORT_SECTION_HEADER
116
56
  end
117
57
 
118
- def find_or_create_reports_discussion(issue:)
119
- reports_discussion = existing_reports_discussion(issue: issue)
120
- return reports_discussion if reports_discussion
121
-
122
- gitlab.create_issue_discussion(iid: issue.iid, note: REPORTS_DISCUSSION_HEADER)
58
+ def reports_extra_content(test)
59
+ "##### Stack trace\n\n```\n#{test.full_stacktrace}\n```"
123
60
  end
124
61
 
125
- def existing_reports_discussion(issue:)
126
- gitlab.find_issue_discussions(iid: issue.iid).find do |discussion|
127
- next if discussion.individual_note
128
- next unless discussion.notes.first
62
+ def health_problem_status_label_quick_action(reports_list)
63
+ '/label ~"severity::1"' if reports_list.spiked_in_short_period?
64
+ end
129
65
 
130
- discussion.notes.first.body.start_with?(REPORTS_DISCUSSION_HEADER)
131
- end
66
+ def up_to_date_labels(test:, issue: nil, new_labels: Set.new)
67
+ (base_issue_labels + super).to_a
132
68
  end
133
69
 
134
70
  def find_failure_discussion_note(issue:, test:, reports_discussion:)
@@ -208,35 +144,6 @@ module GitlabQuality
208
144
  puts " => [DEBUG] Failure stacktrace:\n----------------\n#{test_stacktrace}\n----------------\n"
209
145
  end
210
146
  end
211
-
212
- def add_report_for_test(current_reports_content:, test:)
213
- increment_reports(
214
- current_reports_content: current_reports_content,
215
- test: test,
216
- reports_section_header: REPORT_SECTION_HEADER,
217
- item_extra_content: found_label,
218
- reports_extra_content: "##### Stack trace\n\n```\n#{test.full_stacktrace}\n```"
219
- )
220
- end
221
-
222
- def found_label
223
- if ENV.key?('CI_MERGE_REQUEST_IID')
224
- FOUND_IN_MR_LABEL
225
- else
226
- FOUND_IN_MASTER_LABEL
227
- end
228
- end
229
-
230
- def identity_labels_quick_action
231
- labels_list = IDENTITY_LABELS.map { |label| %(~"#{label}") }.join(' ')
232
- %(/label #{labels_list})
233
- end
234
-
235
- def relate_issues_quick_actions(issues)
236
- issues.map do |issue|
237
- "/relate #{issue.web_url}"
238
- end.join("\n")
239
- end
240
147
  end
241
148
  end
242
149
  end
@@ -11,15 +11,9 @@ module GitlabQuality
11
11
  # - For every passed test in the report:
12
12
  # - Find issue by test hash or create a new issue if no issue was found
13
13
  # - Add a flakiness report in the "Flakiness reports" note
14
- class FlakyTestIssue < ReportAsIssue
15
- include Concerns::GroupAndCategoryLabels
16
- include Concerns::IssueReports
17
-
14
+ class FlakyTestIssue < HealthProblemReporter
18
15
  IDENTITY_LABELS = ['test', 'failure::flaky-test', 'automation:bot-authored'].freeze
19
16
  NEW_ISSUE_LABELS = Set.new(['type::maintenance', 'priority::3', 'severity::3', *IDENTITY_LABELS]).freeze
20
- SEARCH_LABELS = ['test'].freeze
21
- FOUND_IN_MR_LABEL = '~"found:in MR"'
22
- FOUND_IN_MASTER_LABEL = '~"found:master"'
23
17
  REPORT_SECTION_HEADER = '### Flakiness reports'
24
18
  REPORTS_DOCUMENTATION = <<~DOC
25
19
  Flaky tests were detected. Please refer to the [Flaky tests reproducibility instructions](https://docs.gitlab.com/ee/development/testing_guide/flaky_tests.html#how-to-reproduce-a-flaky-test-locally)
@@ -38,108 +32,28 @@ module GitlabQuality
38
32
 
39
33
  attr_reader :base_issue_labels
40
34
 
41
- def run!
42
- puts "Reporting flaky tests in `#{files.join(',')}` as issues in project `#{project}` via the API at `#{Runtime::Env.gitlab_api_base}`."
43
-
44
- TestResults::Builder.new(files).test_results_per_file do |test_results|
45
- puts "=> Reporting #{test_results.count} tests in #{test_results.path}"
46
-
47
- process_test_results(test_results)
48
- end
49
- end
50
-
51
- def process_test_results(test_results)
52
- test_results.each do |test|
53
- next unless test_is_applicable?(test)
54
-
55
- puts " => Reporting flakiness for test '#{test.name}'..."
56
-
57
- issues = find_issues_by_hash(test_hash(test), state: 'opened', labels: SEARCH_LABELS)
58
-
59
- if issues.empty?
60
- issues << create_issue(test)
61
- else
62
- # Keep issues description up-to-date
63
- update_issues(issues, test)
64
- end
65
-
66
- update_reports(issues, test)
67
- collect_issues(test, issues)
68
- end
35
+ def problem_type
36
+ 'flaky'
69
37
  end
70
38
 
71
39
  def test_is_applicable?(test)
72
40
  test.status == 'passed' # We only want failed tests that passed in the end
73
41
  end
74
42
 
75
- def up_to_date_labels(test:, issue: nil, new_labels: Set.new)
76
- (base_issue_labels + super).to_a
43
+ def identity_labels
44
+ IDENTITY_LABELS
77
45
  end
78
46
 
79
- def update_reports(issues, test)
80
- issues.each do |issue|
81
- puts " => Adding the flaky test to the existing issue: #{issue.web_url}"
82
- add_report_to_issue(issue: issue, test: test, related_issues: (issues - [issue]))
83
- end
47
+ def report_section_header
48
+ REPORT_SECTION_HEADER
84
49
  end
85
50
 
86
- def add_report_to_issue(issue:, test:, related_issues:)
87
- current_reports_note = existing_reports_note(issue: issue)
88
- new_reports_list = add_report_for_test(current_reports_content: current_reports_note&.body.to_s, test: test)
89
-
90
- note_body = [
91
- new_reports_list.to_s,
92
- flakiness_status_labels_quick_action(new_reports_list.reports_count),
93
- identity_labels_quick_action,
94
- relate_issues_quick_actions(related_issues)
95
- ].join("\n")
96
-
97
- if current_reports_note
98
- gitlab.edit_issue_note(
99
- issue_iid: issue.iid,
100
- note_id: current_reports_note.id,
101
- note: note_body
102
- )
103
- else
104
- gitlab.create_issue_note(iid: issue.iid, note: note_body)
105
- end
106
- end
107
-
108
- def existing_reports_note(issue:)
109
- gitlab.find_issue_notes(iid: issue.iid).find do |note|
110
- note.body.start_with?(REPORT_SECTION_HEADER)
111
- end
112
- end
113
-
114
- def add_report_for_test(current_reports_content:, test:)
115
- increment_reports(
116
- current_reports_content: current_reports_content,
117
- test: test,
118
- reports_section_header: REPORT_SECTION_HEADER,
119
- item_extra_content: found_label,
120
- reports_extra_content: REPORTS_DOCUMENTATION
121
- )
51
+ def reports_extra_content(_test)
52
+ REPORTS_DOCUMENTATION
122
53
  end
123
54
 
124
- def found_label
125
- if ENV.key?('CI_MERGE_REQUEST_IID')
126
- FOUND_IN_MR_LABEL
127
- else
128
- FOUND_IN_MASTER_LABEL
129
- end
130
- end
131
-
132
- # The report count is based on the percentiles of flakiness issues.
133
- #
134
- # See https://gitlab.com/gitlab-org/quality/engineering-productivity/team/-/blob/main/scripts/unhealthy_test_issues_statistics.rb
135
- # to gather these statistics.
136
- #
137
- # x <= P90 => flakiness::4
138
- # P90 < x <= P95 => flakiness::3
139
- # P95 < x <= P99 => flakiness::2
140
- # > P99 => flakiness::1
141
- def flakiness_status_labels_quick_action(reports_count)
142
- case reports_count
55
+ def health_problem_status_label_quick_action(reports_list)
56
+ case reports_list.reports_count
143
57
  when 399..Float::INFINITY
144
58
  '/label ~"flakiness::1"'
145
59
  when 37..398
@@ -151,15 +65,8 @@ module GitlabQuality
151
65
  end
152
66
  end
153
67
 
154
- def identity_labels_quick_action
155
- labels_list = IDENTITY_LABELS.map { |label| %(~"#{label}") }.join(' ')
156
- %(/label #{labels_list})
157
- end
158
-
159
- def relate_issues_quick_actions(issues)
160
- issues.map do |issue|
161
- "/relate #{issue.web_url}"
162
- end.join("\n")
68
+ def up_to_date_labels(test:, issue: nil, new_labels: Set.new)
69
+ (base_issue_labels + super).to_a
163
70
  end
164
71
  end
165
72
  end
@@ -25,7 +25,7 @@ module GitlabQuality
25
25
  tests = Dir.glob(files).flat_map do |path|
26
26
  puts "Loading tests in #{path}"
27
27
 
28
- TestResults::JsonTestResults.new(path).to_a
28
+ TestResults::JsonTestResults.new(path: path).to_a
29
29
  end
30
30
 
31
31
  tests = tests.select { |test| pipeline_stages.include? test.report["stage"] } unless pipeline_stages.empty?
@@ -0,0 +1,194 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GitlabQuality
4
+ module TestTooling
5
+ module Report
6
+ # Base class for specific health problems reporting.
7
+ # Uses the API to create GitLab issues for any passed test coming from JSON test reports.
8
+ # We expect the test reports to come from a new RSpec process where we retried failing specs.
9
+ #
10
+ # - Takes the JSON test reports like rspec-*.json
11
+ # - Takes a project where flaky test issues should be created
12
+ # - For every passed test in the report:
13
+ # - Find issue by test hash or create a new issue if no issue was found
14
+ # - Add a flakiness report in the "Flakiness reports" note
15
+ class HealthProblemReporter < ReportAsIssue
16
+ include Concerns::GroupAndCategoryLabels
17
+ include Concerns::IssueReports
18
+
19
+ BASE_SEARCH_LABELS = ['test'].freeze
20
+ FOUND_IN_MR_LABEL = '~"found:in MR"'
21
+ FOUND_IN_MASTER_LABEL = '~"found:master"'
22
+
23
+ private
24
+
25
+ def problem_type
26
+ 'unhealthy'
27
+ end
28
+
29
+ def test_is_applicable?(_test)
30
+ false
31
+ end
32
+
33
+ def report_in_discussion?
34
+ false
35
+ end
36
+
37
+ def identity_labels
38
+ []
39
+ end
40
+
41
+ def search_labels
42
+ BASE_SEARCH_LABELS
43
+ end
44
+
45
+ def report_section_header
46
+ ''
47
+ end
48
+
49
+ def find_failure_discussion_note(_issue:, _test:, _reports_discussion:)
50
+ nil
51
+ end
52
+
53
+ def reports_extra_content(_test)
54
+ ''
55
+ end
56
+
57
+ def health_problem_status_label_quick_action(_reports_count)
58
+ ''
59
+ end
60
+
61
+ def item_extra_content(_test)
62
+ found_label
63
+ end
64
+
65
+ def run!
66
+ puts "Reporting tests in `#{files.join(',')}` as issues in project `#{project}` via the API at `#{Runtime::Env.gitlab_api_base}`."
67
+
68
+ TestResults::Builder.new(file_glob: files, token: token, project: project).test_results_per_file do |test_results|
69
+ puts "=> Reporting #{test_results.count} tests in #{test_results.path}"
70
+
71
+ process_test_results(test_results)
72
+ end
73
+ end
74
+
75
+ def process_test_results(test_results)
76
+ test_results.each do |test|
77
+ next unless test_is_applicable?(test)
78
+
79
+ puts " => Reporting #{problem_type} test '#{test.name}'..."
80
+
81
+ issues = find_issues_by_hash(test_hash(test), state: 'opened', labels: search_labels)
82
+
83
+ if issues.empty?
84
+ issues << create_issue(test)
85
+ else
86
+ # Keep issues description up-to-date
87
+ update_issues(issues, test)
88
+ end
89
+
90
+ update_reports(issues, test)
91
+ collect_issues(test, issues)
92
+ end
93
+ end
94
+
95
+ def update_reports(issues, test)
96
+ issues.each do |issue|
97
+ puts " => Reporting #{problem_type} test to existing issue: #{issue.web_url}"
98
+ add_report_to_issue(issue: issue, test: test, related_issues: (issues - [issue]))
99
+ end
100
+ end
101
+
102
+ def add_report_to_issue(issue:, test:, related_issues:) # rubocop:disable Metrics/AbcSize -- FIXME
103
+ current_reports_note =
104
+ if report_in_discussion?
105
+ reports_discussion = find_or_create_reports_discussion(issue: issue)
106
+
107
+ find_failure_discussion_note(issue: issue, test: test, reports_discussion: reports_discussion)
108
+ else
109
+ existing_reports_note(issue: issue)
110
+ end
111
+
112
+ new_reports_list = add_report_for_test(current_reports_content: current_reports_note&.body.to_s, test: test)
113
+
114
+ note_body = [
115
+ new_reports_list.to_s,
116
+ health_problem_status_label_quick_action(new_reports_list),
117
+ identity_labels_quick_action,
118
+ relate_issues_quick_actions(related_issues)
119
+ ].join("\n")
120
+
121
+ if current_reports_note
122
+ gitlab.edit_issue_note(
123
+ issue_iid: issue.iid,
124
+ note_id: current_reports_note.id,
125
+ note: note_body
126
+ )
127
+ elsif report_in_discussion?
128
+ gitlab.add_note_to_issue_discussion_as_thread(
129
+ iid: issue.iid,
130
+ discussion_id: reports_discussion.id,
131
+ note: note_body
132
+ )
133
+ else
134
+ gitlab.create_issue_note(iid: issue.iid, note: note_body)
135
+ end
136
+ rescue MultipleNotesFound => e
137
+ warn(e.message)
138
+ end
139
+
140
+ def find_or_create_reports_discussion(issue:)
141
+ reports_discussion = existing_reports_discussion(issue: issue)
142
+ return reports_discussion if reports_discussion
143
+
144
+ gitlab.create_issue_discussion(iid: issue.iid, note: report_section_header)
145
+ end
146
+
147
+ def existing_reports_discussion(issue:)
148
+ gitlab.find_issue_discussions(iid: issue.iid).find do |discussion|
149
+ next if discussion.individual_note
150
+ next unless discussion.notes.first
151
+
152
+ discussion.notes.first.body.start_with?(report_section_header)
153
+ end
154
+ end
155
+
156
+ def existing_reports_note(issue:)
157
+ gitlab.find_issue_notes(iid: issue.iid).find do |note|
158
+ note.body.start_with?(report_section_header)
159
+ end
160
+ end
161
+
162
+ def add_report_for_test(current_reports_content:, test:)
163
+ increment_reports(
164
+ current_reports_content: current_reports_content,
165
+ test: test,
166
+ reports_section_header: report_section_header,
167
+ item_extra_content: item_extra_content(test),
168
+ reports_extra_content: reports_extra_content(test)
169
+ )
170
+ end
171
+
172
+ def found_label
173
+ if ENV.key?('CI_MERGE_REQUEST_IID')
174
+ FOUND_IN_MR_LABEL
175
+ else
176
+ FOUND_IN_MASTER_LABEL
177
+ end
178
+ end
179
+
180
+ def identity_labels_quick_action
181
+ return if identity_labels.empty?
182
+
183
+ %(/label #{identity_labels.map { |label| %(~"#{label}") }.join(' ')})
184
+ end
185
+
186
+ def relate_issues_quick_actions(issues)
187
+ issues.map do |issue|
188
+ "/relate #{issue.web_url}"
189
+ end.join("\n")
190
+ end
191
+ end
192
+ end
193
+ end
194
+ end
@@ -29,7 +29,7 @@ module GitlabQuality
29
29
  def run!
30
30
  puts "Reporting slow tests in MR #{merge_request_iid}"
31
31
 
32
- TestResults::Builder.new(files).test_results_per_file do |test_results|
32
+ TestResults::Builder.new(file_glob: files).test_results_per_file do |test_results|
33
33
  puts "=> Reporting #{test_results.count} tests in #{test_results.path}"
34
34
 
35
35
  @slow_tests += slow_related_tests(find_slow_tests(test_results))
@@ -52,7 +52,7 @@ module GitlabQuality
52
52
  def run!
53
53
  puts "Reporting test failures in `#{files.join(',')}` as issues in project `#{project}` via the API at `#{Runtime::Env.gitlab_api_base}`."
54
54
 
55
- TestResults::Builder.new(files).test_results_per_file do |test_results|
55
+ TestResults::Builder.new(file_glob: files).test_results_per_file do |test_results|
56
56
  puts "=> Reporting #{test_results.count} tests in #{test_results.path}"
57
57
  process_test_results(test_results)
58
58
  end
@@ -10,6 +10,7 @@ module GitlabQuality
10
10
 
11
11
  def initialize(token:, input_files:, related_issues_file: nil, project: nil, confidential: false, dry_run: false, **_kwargs)
12
12
  @project = project
13
+ @token = token
13
14
  @gitlab = (dry_run ? GitlabClient::IssuesDryClient : GitlabClient::IssuesClient).new(token: token, project: project)
14
15
  @files = Array(input_files)
15
16
  @confidential = confidential
@@ -28,7 +29,7 @@ module GitlabQuality
28
29
 
29
30
  private
30
31
 
31
- attr_reader :gitlab, :files, :project, :issue_type, :confidential, :issue_logger
32
+ attr_reader :gitlab, :files, :project, :issue_type, :confidential, :issue_logger, :token
32
33
 
33
34
  def run!
34
35
  raise NotImplementedError
@@ -120,12 +121,15 @@ module GitlabQuality
120
121
  end
121
122
 
122
123
  def update_issue(issue, test)
123
- issue_attrs = {
124
- description: new_issue_description(test)
125
- }
126
- return if issue.description == issue_attrs[:description]
124
+ issue_attrs = {}
127
125
 
128
- gitlab.edit_issue(iid: issue.iid, options: issue_attrs)
126
+ new_description = new_issue_description(test)
127
+ issue_attrs[:description] = new_description unless issue.description == new_description
128
+
129
+ new_labels = up_to_date_labels(test: test, issue: issue).to_a
130
+ issue_attrs[:labels] = new_labels unless issue.labels == new_labels
131
+
132
+ gitlab.edit_issue(iid: issue.iid, options: issue_attrs) unless issue_attrs.empty?
129
133
  end
130
134
 
131
135
  def issue_labels(issue)
@@ -152,6 +156,8 @@ module GitlabQuality
152
156
  labels.delete_if { |label| label.include?('quarantine') }
153
157
  end
154
158
 
159
+ labels << 'rspec-shared-examples' if test.calls_shared_examples?
160
+
155
161
  labels
156
162
  end
157
163
 
@@ -36,7 +36,7 @@ module GitlabQuality
36
36
  puts "Reporting test results in `#{files.join(',')}` as test cases in project `#{test_case_project}` " \
37
37
  "and issues in project `#{results_issue_project}` via the API at `#{Runtime::Env.gitlab_api_base}`."
38
38
 
39
- TestResults::Builder.new(files).test_results_per_file do |test_results|
39
+ TestResults::Builder.new(file_glob: files).test_results_per_file do |test_results|
40
40
  puts "Reporting tests in #{test_results.path}"
41
41
 
42
42
  test_results.each do |test|
@@ -9,15 +9,9 @@ module GitlabQuality
9
9
  # - Takes a project where slow issues should be created
10
10
  # - Find issue by title (with test description or test file)
11
11
  # - Add test metadata, duration to the issue with group and category labels
12
- class SlowTestIssue < ReportAsIssue
13
- include Concerns::GroupAndCategoryLabels
14
- include Concerns::IssueReports
15
-
12
+ class SlowTestIssue < HealthProblemReporter
16
13
  IDENTITY_LABELS = ['test', 'rspec:slow test', 'rspec profiling', 'automation:bot-authored'].freeze
17
14
  NEW_ISSUE_LABELS = Set.new(['test', 'type::maintenance', 'maintenance::performance', 'priority::3', 'severity::3']).freeze
18
- SEARCH_LABELS = ['test'].freeze
19
- FOUND_IN_MR_LABEL = '~"found:in MR"'
20
- FOUND_IN_MASTER_LABEL = '~"found:master"'
21
15
  REPORT_SECTION_HEADER = '### Slowness reports'
22
16
  REPORTS_DOCUMENTATION = <<~DOC
23
17
  Slow tests were detected, please see the [test speed best practices guide](https://docs.gitlab.com/ee/development/testing_guide/best_practices.html#test-speed)
@@ -28,104 +22,28 @@ module GitlabQuality
28
22
 
29
23
  private
30
24
 
31
- def run!
32
- puts "Reporting slow tests in `#{files.join(',')}` as issues in project `#{project}` via the API at `#{Runtime::Env.gitlab_api_base}`."
33
-
34
- TestResults::Builder.new(files).test_results_per_file do |test_results|
35
- puts "=> Reporting #{test_results.count} tests in #{test_results.path}"
36
-
37
- process_test_results(test_results)
38
- end
39
- end
40
-
41
- def process_test_results(test_results)
42
- test_results.each do |test|
43
- next unless test.slow_test?
44
-
45
- puts " => Reporting slowness for test '#{test.name}'..."
46
-
47
- issues = find_issues_by_hash(test_hash(test), state: 'opened', labels: SEARCH_LABELS)
48
-
49
- if issues.empty?
50
- issues << create_issue(test)
51
- else
52
- # Keep issues description up-to-date
53
- update_issues(issues, test)
54
- end
55
-
56
- update_reports(issues, test)
57
- collect_issues(test, issues)
58
- end
25
+ def problem_type
26
+ 'slow'
59
27
  end
60
28
 
61
29
  def test_is_applicable?(test)
62
30
  test.slow_test?
63
31
  end
64
32
 
65
- def update_reports(issues, test)
66
- issues.each do |issue|
67
- puts " => Adding the slow test to the existing issue: #{issue.web_url}"
68
- add_report_to_issue(issue: issue, test: test, related_issues: (issues - [issue]))
69
- end
33
+ def identity_labels
34
+ IDENTITY_LABELS
70
35
  end
71
36
 
72
- def add_report_to_issue(issue:, test:, related_issues:)
73
- current_reports_note = existing_reports_note(issue: issue)
74
- new_reports_list = add_report_for_test(current_reports_content: current_reports_note&.body.to_s, test: test)
75
-
76
- note_body = [
77
- new_reports_list.to_s,
78
- slowness_status_labels_quick_action(new_reports_list.reports_count),
79
- identity_labels_quick_action,
80
- relate_issues_quick_actions(related_issues)
81
- ].join("\n")
82
-
83
- if current_reports_note
84
- gitlab.edit_issue_note(
85
- issue_iid: issue.iid,
86
- note_id: current_reports_note.id,
87
- note: note_body
88
- )
89
- else
90
- gitlab.create_issue_note(iid: issue.iid, note: note_body)
91
- end
92
- end
93
-
94
- def existing_reports_note(issue:)
95
- gitlab.find_issue_notes(iid: issue.iid).find do |note|
96
- note.body.start_with?(REPORT_SECTION_HEADER)
97
- end
37
+ def report_section_header
38
+ REPORT_SECTION_HEADER
98
39
  end
99
40
 
100
- def add_report_for_test(current_reports_content:, test:)
101
- increment_reports(
102
- current_reports_content: current_reports_content,
103
- test: test,
104
- reports_section_header: REPORT_SECTION_HEADER,
105
- item_extra_content: "(#{test.run_time} seconds) #{found_label}",
106
- reports_extra_content: REPORTS_DOCUMENTATION
107
- )
41
+ def reports_extra_content(_test)
42
+ REPORTS_DOCUMENTATION
108
43
  end
109
44
 
110
- def found_label
111
- if ENV.key?('CI_MERGE_REQUEST_IID')
112
- FOUND_IN_MR_LABEL
113
- else
114
- FOUND_IN_MASTER_LABEL
115
- end
116
- end
117
-
118
- # The report count is based on the percentiles of slowness issues.
119
- #
120
- # See https://gitlab.com/gitlab-org/quality/engineering-productivity/team/-/blob/main/scripts/unhealthy_test_issues_statistics.rb
121
- # to gather these statistics.
122
- #
123
- # P75 => slowness::4
124
- # P90 => slowness::3
125
- # P95 => slowness::2
126
- # Above P95 => slowness::1
127
- def slowness_status_labels_quick_action(reports_count)
128
- case reports_count
45
+ def health_problem_status_label_quick_action(reports_list)
46
+ case reports_list.reports_count
129
47
  when 40..Float::INFINITY
130
48
  '/label ~"slowness::1"'
131
49
  when 28..39
@@ -137,15 +55,8 @@ module GitlabQuality
137
55
  end
138
56
  end
139
57
 
140
- def identity_labels_quick_action
141
- labels_list = IDENTITY_LABELS.map { |label| %(~"#{label}") }.join(' ')
142
- %(/label #{labels_list})
143
- end
144
-
145
- def relate_issues_quick_actions(issues)
146
- issues.map do |issue|
147
- "/relate #{issue.web_url}"
148
- end.join("\n")
58
+ def item_extra_content(test)
59
+ "(#{test.run_time} seconds) #{found_label}"
149
60
  end
150
61
  end
151
62
  end
@@ -6,15 +6,16 @@ require 'table_print'
6
6
  module GitlabQuality
7
7
  module TestTooling
8
8
  class SummaryTable
9
- def self.create(input_files:)
10
- "```\n#{TablePrint::Printer.table_print(collect_results(input_files))}```\n"
9
+ def self.create(input_files:, **options)
10
+ "```\n#{TablePrint::Printer.table_print(collect_results(input_files, **options))}```\n"
11
11
  end
12
12
 
13
13
  # rubocop:disable Metrics/AbcSize
14
- def self.collect_results(input_files)
15
- stage_wise_results = []
14
+ def self.collect_results(input_files, **options)
15
+ sort_by = options[:sort_by]
16
+ sort_direction = options[:sort_direction]
16
17
 
17
- Dir.glob(input_files).each do |report_file|
18
+ stage_wise_results = Dir.glob(input_files).each_with_object([]) do |report_file, stage_wise_results|
18
19
  stage_hash = {}
19
20
  stage_hash["Dev Stage"] = File.basename(report_file, ".*").capitalize
20
21
 
@@ -29,6 +30,9 @@ module GitlabQuality
29
30
  stage_wise_results << stage_hash
30
31
  end
31
32
 
33
+ stage_wise_results.sort_by! { |stage_hash| stage_hash[sort_by] } if sort_by
34
+ stage_wise_results.reverse! if sort_direction == :desc
35
+
32
36
  stage_wise_results
33
37
  end
34
38
  # rubocop:enable Metrics/AbcSize
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'time'
4
+
3
5
  module GitlabQuality
4
6
  module TestTooling
5
7
  module TestMetricsExporter
@@ -11,7 +13,7 @@ module GitlabQuality
11
13
  return @time if defined?(@time)
12
14
 
13
15
  created_at = Time.strptime(env('CI_PIPELINE_CREATED_AT'), '%Y-%m-%dT%H:%M:%S%z') if env('CI_PIPELINE_CREATED_AT')
14
- @time = (created_at || Time.now).utc.strftime('%Y-%m-%dT%H:%M:%S%z')
16
+ @time = Time.parse((created_at || Time.now).utc.strftime('%Y-%m-%d %H:%M:%S %z'))
15
17
  end
16
18
 
17
19
  # rubocop:disable Metrics/AbcSize
@@ -9,10 +9,15 @@ module GitlabQuality
9
9
  '403 Forbidden - Your account has been blocked'
10
10
  ].freeze
11
11
 
12
+ SHARED_EXAMPLES_CALLERS = %w[include_examples it_behaves_like].freeze
13
+
12
14
  attr_reader :report
13
15
 
14
- def initialize(report)
16
+ def initialize(report:, token: nil, project: nil, ref: 'master')
15
17
  @report = report
18
+ @token = token
19
+ @project = project
20
+ @ref = ref
16
21
  end
17
22
 
18
23
  def stage
@@ -27,6 +32,10 @@ module GitlabQuality
27
32
  raise NotImplementedError
28
33
  end
29
34
 
35
+ def line_number
36
+ raise NotImplementedError
37
+ end
38
+
30
39
  def section
31
40
  raise NotImplementedError
32
41
  end
@@ -57,6 +66,26 @@ module GitlabQuality
57
66
  return message_lines.empty? ? message : message_lines.join("\n")
58
67
  end
59
68
  end
69
+
70
+ def calls_shared_examples?
71
+ reported_line = files_client.file_contents_at_line(line_number)
72
+
73
+ return false unless reported_line
74
+
75
+ SHARED_EXAMPLES_CALLERS.any? { |caller_method| reported_line.strip.start_with?(caller_method) }
76
+ end
77
+
78
+ def files_client
79
+ @files_client ||= GitlabClient::RepositoryFilesClient.new(
80
+ token: token,
81
+ project: project,
82
+ file_path: file,
83
+ ref: ref)
84
+ end
85
+
86
+ private
87
+
88
+ attr_reader :token, :project, :ref
60
89
  end
61
90
  end
62
91
  end
@@ -8,8 +8,11 @@ module GitlabQuality
8
8
 
9
9
  attr_reader :path
10
10
 
11
- def initialize(path)
11
+ def initialize(path:, token: nil, project: nil, ref: 'master')
12
12
  @path = path
13
+ @token = token
14
+ @project = project
15
+ @ref = ref
13
16
  @results = parse
14
17
  @testcases = process
15
18
  end
@@ -24,7 +27,7 @@ module GitlabQuality
24
27
 
25
28
  private
26
29
 
27
- attr_reader :results, :testcases
30
+ attr_reader :results, :testcases, :token, :project, :ref
28
31
 
29
32
  def parse
30
33
  raise NotImplementedError
@@ -4,8 +4,11 @@ module GitlabQuality
4
4
  module TestTooling
5
5
  module TestResults
6
6
  class Builder
7
- def initialize(file_glob)
7
+ def initialize(file_glob:, token: nil, project: nil, ref: 'master')
8
8
  @file_glob = file_glob
9
+ @token = token
10
+ @project = project
11
+ @ref = ref
9
12
  end
10
13
 
11
14
  def test_results_per_file
@@ -15,9 +18,9 @@ module GitlabQuality
15
18
  test_results =
16
19
  case extension
17
20
  when '.json'
18
- TestResults::JsonTestResults.new(path)
21
+ TestResults::JsonTestResults.new(path: path, token: token, project: project, ref: ref)
19
22
  when '.xml'
20
- TestResults::JUnitTestResults.new(path)
23
+ TestResults::JUnitTestResults.new(path: path, token: token, project: project, ref: ref)
21
24
  else
22
25
  raise "Unknown extension #{extension}"
23
26
  end
@@ -28,7 +31,7 @@ module GitlabQuality
28
31
 
29
32
  private
30
33
 
31
- attr_reader :file_glob
34
+ attr_reader :file_glob, :token, :project, :ref
32
35
  end
33
36
  end
34
37
  end
@@ -18,7 +18,7 @@ module GitlabQuality
18
18
 
19
19
  def process
20
20
  results.xpath('//testcase').map do |test|
21
- GitlabQuality::TestTooling::TestResult::JUnitTestResult.new(test)
21
+ GitlabQuality::TestTooling::TestResult::JUnitTestResult.new(report: test, project: project, token: token)
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
- GitlabQuality::TestTooling::TestResult::JsonTestResult.new(test)
23
+ GitlabQuality::TestTooling::TestResult::JsonTestResult.new(report: test, project: project, token: token)
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 = "1.27.1"
5
+ VERSION = "1.29.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: 1.27.1
4
+ version: 1.29.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: 2024-05-28 00:00:00.000000000 Z
11
+ date: 2024-06-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: climate_control
@@ -440,6 +440,7 @@ files:
440
440
  - lib/gitlab_quality/test_tooling/report/failed_test_issue.rb
441
441
  - lib/gitlab_quality/test_tooling/report/flaky_test_issue.rb
442
442
  - lib/gitlab_quality/test_tooling/report/generate_test_session.rb
443
+ - lib/gitlab_quality/test_tooling/report/health_problem_reporter.rb
443
444
  - lib/gitlab_quality/test_tooling/report/issue_logger.rb
444
445
  - lib/gitlab_quality/test_tooling/report/knapsack_report_issue.rb
445
446
  - lib/gitlab_quality/test_tooling/report/merge_request_slow_tests_report.rb