gitlab_quality-test_tooling 1.5.4 → 1.7.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: acc009f4f6d5f4be05fa85cd3fffc68778d415d970ca96f22ace03c8a86f9488
4
- data.tar.gz: 5c31655a8245c632e595431fb99ec14852bfd4f9b9778d09e8b8d0ae3393dbc1
3
+ metadata.gz: 6924fc592c0dbb5e1ae0aed8710f7f7714c336bdd1d1d31b343e6edddd358ead
4
+ data.tar.gz: 0ebb5fbe0aeff0417193d4521cf52a78d849b83d116e08118b9cf87ee2655a71
5
5
  SHA512:
6
- metadata.gz: 754b2ed722bc050515ceb9f4691bc49da1e63c27c1ff44b3cd97bc597d6691f947a2390b63b41e980b4229332817671bc9cad351fee4455d424ce66f1d54cf03
7
- data.tar.gz: ff2b356903435078c53b6234e69d57e272c9f367cd01518158fda38a94372ceeb1fe1e2f2cebec16e2aaa0dd4db9c98592c36631c858119d3340cf4bcd961bda
6
+ metadata.gz: f9a64cbae7e3a34ae66450ae7fe20181560dc6860950a92e53c5aaf61ab10343372044968b5a726bef51b082a45dd9150d6f161d93a45a4ccf75d550cf981be1
7
+ data.tar.gz: a77ea69469a02b3cbd089cade6be20347abc03cada1cf93fec8380456f000d083a914296e0b35faff573e60d73e4759abd1b5d224cb04e64b38862fe720cf65c
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- gitlab_quality-test_tooling (1.5.4)
4
+ gitlab_quality-test_tooling (1.7.0)
5
5
  activesupport (>= 6.1, < 7.2)
6
6
  amatch (~> 0.4.1)
7
7
  gitlab (~> 4.19)
@@ -29,6 +29,7 @@ GEM
29
29
  ast (2.4.2)
30
30
  backport (1.2.0)
31
31
  benchmark (0.2.1)
32
+ byebug (11.1.3)
32
33
  claide (1.1.0)
33
34
  claide-plugins (0.9.2)
34
35
  cork
@@ -160,6 +161,9 @@ GEM
160
161
  pry (0.14.2)
161
162
  coderay (~> 1.1)
162
163
  method_source (~> 1.0)
164
+ pry-byebug (3.10.1)
165
+ byebug (~> 11.0)
166
+ pry (>= 0.13, < 0.15)
163
167
  public_suffix (5.0.1)
164
168
  racc (1.6.2)
165
169
  rack (3.0.7)
@@ -279,6 +283,7 @@ DEPENDENCIES
279
283
  gitlab_quality-test_tooling!
280
284
  guard-rspec (~> 4.7)
281
285
  lefthook (~> 1.3)
286
+ pry-byebug (= 3.10.1)
282
287
  rake (~> 13.0)
283
288
  rspec (~> 3.12)
284
289
  simplecov (~> 0.22)
data/README.md CHANGED
@@ -81,10 +81,14 @@ Usage: exe/relate-failure-issue [options]
81
81
  Max stacktrace diff ratio for failure issues detection
82
82
  -p, --project PROJECT Can be an integer or a group/project string
83
83
  -t, --token TOKEN A valid access token with `api` scope and Maintainer permission in PROJECT
84
+ -r RELATED_ISSUES_FILE, The file path for the related issues
85
+ --related-issues-file
84
86
  --system-log-files SYSTEM_LOG_FILES
85
87
  Include errors from system logs in failure issues
86
88
  --base-issue-labels BASE_ISSUE_LABELS
87
89
  Labels to add to new failure issues
90
+ --exclude-labels-for-search EXCLUDE_LABELS_FOR_SEARCH
91
+ Labels to exclude when searching for existing issues
88
92
  --confidential Makes created new issues confidential
89
93
  --dry-run Perform a dry-run (don't create or update issues)
90
94
  -v, --version Show the version
@@ -133,6 +137,21 @@ Usage: exe/slow-test-issue [options]
133
137
  -h, --help Show the usage
134
138
  ```
135
139
 
140
+ ### `exe/flaky-test-issues`
141
+
142
+ ```shell
143
+ Purpose: Purpose: Create flaky test issues for any passed test coming from rspec-retry JSON report files
144
+ Usage: exe/flaky-test-issue [options]
145
+ -i, --input-files INPUT_FILES JSON rspec-retry report files
146
+ -p, --project PROJECT Can be an integer or a group/project string
147
+ -m MERGE_REQUEST_IID, An integer merge request IID
148
+ --merge_request_iid
149
+ -t, --token TOKEN A valid access token with `api` scope and Maintainer permission in PROJECT
150
+ --dry-run Perform a dry-run (don't create note)
151
+ -v, --version Show the version
152
+ -h, --help Show the usage
153
+ ```
154
+
136
155
  ### `exe/slow-test-merge-request-report-note`
137
156
 
138
157
  ```shell
@@ -0,0 +1,54 @@
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-retry report files') 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('-m', '--merge_request_iid MERGE_REQUEST_IID', String, 'An integer merge request IID') do |merge_request_iid|
23
+ params[:merge_request_iid] = merge_request_iid
24
+ end
25
+
26
+ opts.on('-t', '--token TOKEN', String, 'A valid access token with `api` scope and Maintainer permission in PROJECT') do |token|
27
+ params[:token] = token
28
+ end
29
+
30
+ opts.on('--dry-run', "Perform a dry-run (don't create issues)") do
31
+ params[:dry_run] = true
32
+ end
33
+
34
+ opts.on_tail('-v', '--version', 'Show the version') do
35
+ require_relative "../lib/gitlab_quality/test_tooling/version"
36
+ puts "#{$PROGRAM_NAME} : #{GitlabQuality::TestTooling::VERSION}"
37
+ exit
38
+ end
39
+
40
+ opts.on_tail('-h', '--help', 'Show the usage') do
41
+ puts "Purpose: Create flaky test issues for any passed test coming from rspec-retry JSON report files."
42
+ puts opts
43
+ exit
44
+ end
45
+
46
+ opts.parse(ARGV)
47
+ end
48
+
49
+ if params.any?
50
+ GitlabQuality::TestTooling::Report::FlakyTestIssue.new(**params).invoke!
51
+ else
52
+ puts options
53
+ exit 1
54
+ end
@@ -41,6 +41,11 @@ options = OptionParser.new do |opts|
41
41
  params[:base_issue_labels] = base_issue_labels.split(',')
42
42
  end
43
43
 
44
+ opts.on('--exclude-labels-for-search EXCLUDE_LABELS_FOR_SEARCH', String,
45
+ 'Labels to exclude when searching for existing issues') do |exclude_labels_for_search|
46
+ params[:exclude_labels_for_search] = exclude_labels_for_search.split(',')
47
+ end
48
+
44
49
  opts.on('--confidential', "Makes created new issues confidential") do
45
50
  params[:confidential] = true
46
51
  end
@@ -55,6 +55,16 @@ module GitlabQuality
55
55
  end
56
56
  end
57
57
 
58
+ def find_issues_by_hash(test_hash, &select)
59
+ select ||= :itself
60
+
61
+ handle_gitlab_client_exceptions do
62
+ client.search_in_project(project, 'issues', test_hash)
63
+ .auto_paginate
64
+ .select(&select)
65
+ end
66
+ end
67
+
58
68
  def find_issue_discussions(iid:)
59
69
  handle_gitlab_client_exceptions do
60
70
  client.issue_discussions(project, iid, order_by: 'created_at', sort: 'asc').auto_paginate
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GitlabQuality
4
+ module TestTooling
5
+ module Report
6
+ module Concerns
7
+ module IssueReports
8
+ JOB_URL_REGEX = %r{(?<job_url>https://(?<host>[\w.]+)/(?<project_path>[\w\-./]+)/-/jobs/\d+)}
9
+ FAILED_JOB_DESCRIPTION_REGEX = /First happened in #{JOB_URL_REGEX}\./m
10
+ REPORT_ITEM_REGEX = /^1\. \d{4}-\d{2}-\d{2}: #{JOB_URL_REGEX} \((?<pipeline_url>.+)\)$/
11
+
12
+ def initial_reports_section(test)
13
+ <<~REPORTS
14
+ ### Reports (1)
15
+
16
+ #{report_list_item(test)}
17
+ REPORTS
18
+ end
19
+
20
+ def add_report_to_issue_description(issue, test)
21
+ issue_description = issue.description
22
+
23
+ # We include the number of reports in the header, for visibility.
24
+ new_issue_description =
25
+ if issue_description.include?('### Reports')
26
+ # We count the number of existing reports.
27
+ reports_count = issue_description
28
+ .scan(REPORT_ITEM_REGEX)
29
+ .size.to_i + 1
30
+ issue_description.sub(/^### Reports.*$/, "### Reports (#{reports_count})")
31
+ else # For issue with the legacy format, we add the Reports section
32
+ update_legacy_issue_description(issue_description)
33
+ end
34
+
35
+ [new_issue_description, report_list_item(test)].join("\n")
36
+ end
37
+
38
+ def failed_issue_job_url(issue)
39
+ job_urls_from_description(issue.description, REPORT_ITEM_REGEX).last ||
40
+ # Legacy format
41
+ job_urls_from_description(issue.description, FAILED_JOB_DESCRIPTION_REGEX).last
42
+ end
43
+
44
+ def failed_issue_job_urls(issue)
45
+ job_urls_from_description(issue.description, REPORT_ITEM_REGEX) +
46
+ # Legacy format
47
+ job_urls_from_description(issue.description, FAILED_JOB_DESCRIPTION_REGEX)
48
+ end
49
+
50
+ private
51
+
52
+ def report_list_item(test)
53
+ "1. #{Time.new.utc.strftime('%F')}: #{test.ci_job_url} (#{ENV.fetch('CI_PIPELINE_URL', 'pipeline url is missing')})"
54
+ end
55
+
56
+ def job_urls_from_description(issue_description, regex)
57
+ issue_description.lines.filter_map do |line|
58
+ match = line.match(regex)
59
+ match[:job_url] if match
60
+ end
61
+ end
62
+
63
+ def update_legacy_issue_description(issue_description)
64
+ test_captures = issue_description.scan(JOB_URL_REGEX)
65
+ reports_count = test_captures.size.to_i + 1
66
+
67
+ updated_description = "#{issue_description}\n\n### Reports (#{reports_count})\n"
68
+ updated_description = [updated_description, *test_captures_to_report_items(test_captures)].join("\n") unless test_captures.empty?
69
+ updated_description
70
+ end
71
+
72
+ def test_captures_to_report_items(test_captures)
73
+ test_captures.map do |ci_job_url, _, _|
74
+ report_list_item(GitlabQuality::TestTooling::TestResult::JsonTestResult.new(
75
+ 'ci_job_url' => ci_job_url
76
+ ))
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GitlabQuality
4
+ module TestTooling
5
+ module Report
6
+ # Uses the API to create GitLab issues for any passed test coming from JSON test reports.
7
+ # We expect the test reports to come from rspec-retry, or from a new RSpec process where
8
+ # we retried failing specs.
9
+ #
10
+ # - Takes the JSON test reports like rspec-*.json (typically from rspec-retry gem)`
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
14
+ # - Reopen issue if it already exists, but is closed
15
+ class FlakyTestIssue < ReportAsIssue
16
+ include Concerns::IssueReports
17
+
18
+ NEW_ISSUE_LABELS = Set.new(['type::maintenance', 'priority::3', 'severity::3', 'failure::flaky-test']).freeze
19
+ FOUND_IN_MR_LABEL = 'found:in MR'
20
+ FOUND_IN_MASTER_LABEL = 'found:master'
21
+
22
+ def initialize(token:, input_files:, project: nil, merge_request_iid: nil, confidential: false, dry_run: false, **_kwargs)
23
+ super(token: token, input_files: input_files, project: project, confidential: confidential, dry_run: dry_run)
24
+ @merge_request_iid = merge_request_iid
25
+ end
26
+
27
+ private
28
+
29
+ attr_reader :merge_request_iid
30
+
31
+ def run!
32
+ puts "Reporting flaky 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
+ test_results.each do |test|
38
+ next if test.status != 'passed' # We only want failed tests that passed in the end
39
+
40
+ create_flaky_issue(test)
41
+ end
42
+ end
43
+ end
44
+
45
+ def new_issue_title(test)
46
+ "Flaky test in #{super}"
47
+ end
48
+
49
+ def new_issue_labels(_test)
50
+ found_label =
51
+ if !merge_request_iid || merge_request_iid.empty?
52
+ FOUND_IN_MASTER_LABEL
53
+ else
54
+ FOUND_IN_MR_LABEL
55
+ end
56
+
57
+ NEW_ISSUE_LABELS + [found_label]
58
+ end
59
+
60
+ def new_issue_description(test)
61
+ super + [
62
+ "Flaky tests were detected, please refer to the [Flaky tests documentation](https://docs.gitlab.com/ee/development/testing_guide/flaky_tests.html) " \
63
+ "to learn more about how to reproduce them.",
64
+ initial_reports_section(test)
65
+ ].compact.join("\n\n")
66
+ end
67
+
68
+ def create_flaky_issue(test)
69
+ puts " => Finding existing issues for flaky test '#{test.name}' (run time: #{test.run_time} seconds)..."
70
+
71
+ issues = find_issues_by_hash(test_hash(test))
72
+ issues.each do |issue|
73
+ puts " => Existing issue link #{issue.web_url}."
74
+
75
+ puts " => Adding the flaky test to the existing issue..."
76
+ add_report_to_issue(issue, test)
77
+
78
+ if issue.state == 'closed'
79
+ puts " => Issue is closed. Reopening it."
80
+ reopen_issue(issue)
81
+ end
82
+ end
83
+
84
+ create_issue(test) unless issues.any?
85
+ end
86
+
87
+ def add_report_to_issue(issue, test)
88
+ gitlab.edit_issue(iid: issue.iid, options: { description: add_report_to_issue_description(issue, test) })
89
+ end
90
+
91
+ def reopen_issue(issue)
92
+ gitlab.edit_issue(iid: issue.iid, options: { state_event: 'reopen' })
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
@@ -7,7 +7,6 @@ module GitlabQuality
7
7
  SLOW_TEST_MESSAGE = '<!-- slow-test -->'
8
8
  SLOW_TEST_LABEL = '/label ~"rspec:slow test detected"'
9
9
  SLOW_TEST_NOTE_SOURCE_CODE = 'Generated by [`gitlab_quality-test_tooling`](https://gitlab.com/gitlab-org/ruby/gems/gitlab_quality-test_tooling/-/blob/main/lib/gitlab_quality/test_tooling/report/merge_request_slow_tests_report.rb).'
10
- SLOW_TEST_NOTE_FEEDBACK = 'Please [share your feedback and suggestions](https://gitlab.com/gitlab-org/quality/engineering-productivity/team/-/issues/289).'
11
10
 
12
11
  def initialize(token:, input_files:, merge_request_iid:, project: nil, dry_run: false, **_kwargs)
13
12
  @project = project
@@ -70,7 +69,6 @@ module GitlabQuality
70
69
  SLOW_TEST_MESSAGE,
71
70
  SLOW_TEST_LABEL,
72
71
  ":tools: #{SLOW_TEST_NOTE_SOURCE_CODE}\n",
73
- ":recycle: #{SLOW_TEST_NOTE_FEEDBACK}\n",
74
72
  "---\n",
75
73
  ":snail: Slow tests detected in this merge request. These slow tests might be related to this merge request's changes.",
76
74
  "<details><summary>Click to expand</summary>\n",
@@ -18,6 +18,7 @@ module GitlabQuality
18
18
  class RelateFailureIssue < ReportAsIssue
19
19
  include Concerns::FindSetDri
20
20
  include Concerns::GroupAndCategoryLabels
21
+ include Concerns::IssueReports
21
22
  include Amatch
22
23
 
23
24
  DEFAULT_MAX_DIFF_RATIO_FOR_DETECTION = 0.15
@@ -25,9 +26,7 @@ module GitlabQuality
25
26
  SPAM_THRESHOLD_FOR_FAILURE_ISSUES = 3
26
27
  FAILURE_STACKTRACE_REGEX = %r{(?:(?:.*Failure/Error:(?<stacktrace>.+))|(?<stacktrace>.+))}m
27
28
  ISSUE_STACKTRACE_REGEX = /### Stack trace\s*(```)#{FAILURE_STACKTRACE_REGEX}(```)/m
28
- JOB_URL_REGEX = %r{(?<job_url>https://(?<host>[\w.]+)/(?<project_path>[\w\-./]+)/-/jobs/\d+)}
29
- FAILED_JOB_DESCRIPTION_REGEX = /First happened in #{JOB_URL_REGEX}\./m
30
- REPORT_ITEM_REGEX = /^1\. \d{4}-\d{2}-\d{2}: #{JOB_URL_REGEX} \((?<pipeline_url>.+)\)$/
29
+
31
30
  NEW_ISSUE_LABELS = Set.new(%w[test failure::new priority::2]).freeze
32
31
  IGNORED_FAILURES = [
33
32
  'Net::ReadTimeout',
@@ -37,18 +36,24 @@ module GitlabQuality
37
36
 
38
37
  MultipleIssuesFound = Class.new(StandardError)
39
38
 
40
- def initialize(max_diff_ratio: DEFAULT_MAX_DIFF_RATIO_FOR_DETECTION, system_logs: [], base_issue_labels: Set.new, **kwargs)
39
+ def initialize(
40
+ max_diff_ratio: DEFAULT_MAX_DIFF_RATIO_FOR_DETECTION,
41
+ system_logs: [],
42
+ base_issue_labels: nil,
43
+ exclude_labels_for_search: nil,
44
+ **kwargs)
41
45
  super
42
46
  @max_diff_ratio = max_diff_ratio.to_f
43
47
  @system_logs = Dir.glob(system_logs)
44
48
  @base_issue_labels = Set.new(base_issue_labels)
49
+ @exclude_labels_for_search = Set.new(exclude_labels_for_search)
45
50
  @issue_type = 'issue'
46
51
  @commented_issue_list = Set.new
47
52
  end
48
53
 
49
54
  private
50
55
 
51
- attr_reader :max_diff_ratio, :system_logs, :base_issue_labels
56
+ attr_reader :max_diff_ratio, :system_logs, :base_issue_labels, :exclude_labels_for_search
52
57
 
53
58
  def run!
54
59
  puts "Reporting test failures in `#{files.join(',')}` as issues in project `#{project}` via the API at `#{Runtime::Env.gitlab_api_base}`."
@@ -136,35 +141,15 @@ module GitlabQuality
136
141
 
137
142
  def pipeline_issues_with_similar_stacktrace(test)
138
143
  search_labels = (base_issue_labels + Set.new(%w[test failure::new])).to_a
144
+ not_labels = exclude_labels_for_search.to_a
139
145
  gitlab.find_issues(options: { labels: search_labels,
146
+ not: { labels: not_labels },
140
147
  created_after: past_timestamp(2) }).select do |issue|
141
148
  job_url_from_issue = failed_issue_job_url(issue)
142
149
 
143
150
  next if pipeline != pipeline_env_from_job_url(job_url_from_issue)
144
151
 
145
- stack_trace_from_issue = cleaned_stack_trace_from_issue(issue)
146
- stack_trace_from_test = cleaned_stacktrace_from_test(test)
147
- diff_ratio = compare_stack_traces(stack_trace_from_test, stack_trace_from_issue)
148
- diff_ratio < max_diff_ratio
149
- end
150
- end
151
-
152
- def failed_issue_job_url(issue)
153
- job_urls_from_description(issue.description, REPORT_ITEM_REGEX).last ||
154
- # Legacy format
155
- job_urls_from_description(issue.description, FAILED_JOB_DESCRIPTION_REGEX).last
156
- end
157
-
158
- def failed_issue_job_urls(issue)
159
- job_urls_from_description(issue.description, REPORT_ITEM_REGEX) +
160
- # Legacy format
161
- job_urls_from_description(issue.description, FAILED_JOB_DESCRIPTION_REGEX)
162
- end
163
-
164
- def job_urls_from_description(issue_description, regex)
165
- issue_description.lines.filter_map do |line|
166
- match = line.match(regex)
167
- match[:job_url] if match
152
+ compare_stack_traces(cleaned_stack_trace_from_test(test), cleaned_stack_trace_from_issue(issue)) < max_diff_ratio
168
153
  end
169
154
  end
170
155
 
@@ -184,7 +169,11 @@ module GitlabQuality
184
169
  end
185
170
 
186
171
  def failure_issues(test)
187
- find_issues(test, (base_issue_labels + Set.new(%w[test])).to_a) # Search for opened and closed issues
172
+ find_issues_for_test(
173
+ test,
174
+ labels: base_issue_labels + Set.new(%w[test]),
175
+ not_labels: exclude_labels_for_search
176
+ )
188
177
  end
189
178
 
190
179
  def full_stacktrace(test)
@@ -205,7 +194,7 @@ module GitlabQuality
205
194
  remove_unique_resource_names(relevant_issue_stacktrace)
206
195
  end
207
196
 
208
- def cleaned_stacktrace_from_test(test)
197
+ def cleaned_stack_trace_from_test(test)
209
198
  test_failure_stacktrace = sanitize_stacktrace(full_stacktrace(test),
210
199
  FAILURE_STACKTRACE_REGEX) || full_stacktrace(test)
211
200
  remove_unique_resource_names(test_failure_stacktrace)
@@ -221,7 +210,7 @@ module GitlabQuality
221
210
  end
222
211
 
223
212
  def find_relevant_failure_issues(test) # rubocop:disable Metrics/AbcSize
224
- clean_first_test_failure_stacktrace = cleaned_stacktrace_from_test(test)
213
+ clean_first_test_failure_stacktrace = cleaned_stack_trace_from_test(test)
225
214
  # Search with the `search` param returns 500 errors, so we filter by `base_issue_labels` and then filter further in Ruby
226
215
  failure_issues(test).each_with_object({}) do |issue, memo|
227
216
  clean_relevant_issue_stacktrace = cleaned_stack_trace_from_issue(issue)
@@ -289,7 +278,7 @@ module GitlabQuality
289
278
  "```\n#{full_stacktrace(test)}\n```",
290
279
  screenshot_section(test),
291
280
  system_log_errors_section(test),
292
- reports_section(test)
281
+ initial_reports_section(test)
293
282
  ].compact.join("\n\n")
294
283
  end
295
284
 
@@ -312,18 +301,6 @@ module GitlabQuality
312
301
  section
313
302
  end
314
303
 
315
- def reports_section(test)
316
- <<~REPORTS
317
- ### Reports (1)
318
-
319
- #{report_list_item(test)}
320
- REPORTS
321
- end
322
-
323
- def report_list_item(test)
324
- "1. #{Time.new.utc.strftime('%F')}: #{test.ci_job_url} (#{ENV.fetch('CI_PIPELINE_URL', 'pipeline url is missing')})"
325
- end
326
-
327
304
  def up_to_date_labels(test:, issue: nil, new_labels: Set.new)
328
305
  (Set.new(base_issue_labels) + (super << pipeline_name_label)).to_a
329
306
  end
@@ -348,7 +325,7 @@ module GitlabQuality
348
325
  state_event = issue.state == 'closed' ? 'reopen' : nil
349
326
 
350
327
  issue_attrs = {
351
- description: up_to_date_issue_description(issue.description, test),
328
+ description: add_report_to_issue_description(issue, test),
352
329
  labels: up_to_date_labels(test: test, issue: issue)
353
330
  }
354
331
  issue_attrs[:state_event] = state_event if state_event
@@ -357,26 +334,6 @@ module GitlabQuality
357
334
  puts " => Added a report in '#{issue.title}': #{issue.web_url}!"
358
335
  end
359
336
 
360
- def up_to_date_issue_description(issue_description, test)
361
- # We include the number of reports in the header, for visibility.
362
- new_issue_description =
363
- if issue_description.include?('### Reports')
364
- # We count the number of existing reports.
365
- reports_count = issue_description
366
- .scan(REPORT_ITEM_REGEX)
367
- .size.to_i + 1
368
- issue_description.sub(/^### Reports.*$/, "### Reports (#{reports_count})")
369
- else # For issue with the legacy format, we add the Reports section
370
- reports_count = issue_description
371
- .scan(JOB_URL_REGEX)
372
- .size.to_i + 1
373
-
374
- "#{issue_description}\n\n### Reports (#{reports_count})"
375
- end
376
-
377
- "#{new_issue_description}\n#{report_list_item(test)}"
378
- end
379
-
380
337
  def new_issue_title(test)
381
338
  "Failure in #{super}"
382
339
  end
@@ -39,7 +39,10 @@ module GitlabQuality
39
39
  end
40
40
 
41
41
  def test_hash(test)
42
- OpenSSL::Digest.hexdigest('SHA256', "#{test.file}#{test.name}")
42
+ # Should not be more than 50 characters if we want it indexed.
43
+ #
44
+ # See https://gitlab.com/gitlab-org/ruby/gems/gitlab_quality-test_tooling/-/issues/27#note_1607276486
45
+ OpenSSL::Digest.hexdigest('SHA256', "#{test.file}#{test.name}")[..40]
43
46
  end
44
47
 
45
48
  def new_issue_description(test)
@@ -48,7 +51,8 @@ module GitlabQuality
48
51
 
49
52
  | Field | Value |
50
53
  | ------ | ------ |
51
- | File | #{test.test_file_link} |
54
+ | File URL | #{test.test_file_link} |
55
+ | Filename | `#{test.file}` |
52
56
  | Description | `#{test.name}` |
53
57
  | Test level | #{test.level} |
54
58
  | Hash | `#{test_hash(test)}` |
@@ -134,15 +138,21 @@ module GitlabQuality
134
138
  labels
135
139
  end
136
140
 
137
- def find_issues(test, labels, state: nil)
138
- search_options = { labels: labels.to_a }
141
+ def find_issues_by_hash(test_hash)
142
+ gitlab.find_issues_by_hash(test_hash)
143
+ end
144
+
145
+ def find_issues_for_test(test, labels:, not_labels: Set.new, state: nil)
146
+ search_options = { labels: labels.to_a, not: { labels: not_labels.to_a } }
139
147
  search_options[:state] = state if state
140
148
 
141
- gitlab.find_issues(options: search_options).find_all do |issue|
142
- issue_title = issue.title.strip
143
- test_file_path_found = !test.file.to_s.empty? && issue_title.include?(partial_file_path(test.file))
144
- issue_title.include?(test.name) || test_file_path_found
145
- end
149
+ gitlab.find_issues(options: search_options).find_all { |issue| issue_match_test?(issue, test) }
150
+ end
151
+
152
+ def issue_match_test?(issue, test)
153
+ issue_title = issue.title.strip
154
+ test_file_path_found = !test.file.to_s.empty? && issue_title.include?(partial_file_path(test.file))
155
+ issue_title.include?(test.name) || test_file_path_found
146
156
  end
147
157
 
148
158
  def pipeline_name_label
@@ -68,7 +68,7 @@ module GitlabQuality
68
68
  def create_slow_issue(test)
69
69
  puts " => Finding existing issues for slow test '#{test.name}' (run time: #{test.run_time} seconds)..."
70
70
 
71
- issues = find_issues(test, SEARCH_LABELS, state: 'opened')
71
+ issues = find_issues_for_test(test, labels: SEARCH_LABELS, state: 'opened')
72
72
 
73
73
  if issues.blank?
74
74
  issues << create_issue(test)
@@ -18,7 +18,25 @@ module GitlabQuality
18
18
  ].freeze
19
19
 
20
20
  def name
21
- report.fetch('full_description')
21
+ # If we see a string representation of an object in a test full_description, we discard it.
22
+ #
23
+ # This is to ensure that tests would have a reproducible name, in case they don't have a name.
24
+ #
25
+ # Test example:
26
+ #
27
+ # it { is_expected.to eq(secondary_node) }
28
+ #
29
+ # Would have its full_description as follows:
30
+ #
31
+ # Gitlab::Geo.proxied_site on a primary for a proxied request with a proxy extra data header
32
+ # for an existing site is expected to eq #<GeoNode id: 116, primary: false, oauth_application_id: 97
33
+ # , enabled: true, access_key: [FILTERED], e...pdated_at: "2023-10-10 08:49:49.797128469 +0000",
34
+ # sync_object_storage: true, secret_access_key: nil>
35
+ #
36
+ # Which would change for every test run due to the timestamps.
37
+ #
38
+ # See https://gitlab.com/gitlab-org/ruby/gems/gitlab_quality-test_tooling/-/merge_requests/77#note_1608793804
39
+ report.fetch('full_description').split('#<').first
22
40
  end
23
41
 
24
42
  def file
@@ -2,6 +2,6 @@
2
2
 
3
3
  module GitlabQuality
4
4
  module TestTooling
5
- VERSION = "1.5.4"
5
+ VERSION = "1.7.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.5.4
4
+ version: 1.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - GitLab Quality
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-11-22 00:00:00.000000000 Z
11
+ date: 2023-11-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: climate_control
@@ -80,6 +80,20 @@ dependencies:
80
80
  - - "~>"
81
81
  - !ruby/object:Gem::Version
82
82
  version: '1.3'
83
+ - !ruby/object:Gem::Dependency
84
+ name: pry-byebug
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - '='
88
+ - !ruby/object:Gem::Version
89
+ version: 3.10.1
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - '='
95
+ - !ruby/object:Gem::Version
96
+ version: 3.10.1
83
97
  - !ruby/object:Gem::Dependency
84
98
  name: rake
85
99
  requirement: !ruby/object:Gem::Requirement
@@ -332,6 +346,7 @@ description: A collection of test-related tools.
332
346
  email:
333
347
  - quality@gitlab.com
334
348
  executables:
349
+ - flaky-test-issues
335
350
  - generate-test-session
336
351
  - post-to-slack
337
352
  - prepare-stage-reports
@@ -357,6 +372,7 @@ files:
357
372
  - LICENSE.txt
358
373
  - README.md
359
374
  - Rakefile
375
+ - exe/flaky-test-issues
360
376
  - exe/generate-test-session
361
377
  - exe/post-to-slack
362
378
  - exe/prepare-stage-reports
@@ -374,8 +390,10 @@ files:
374
390
  - lib/gitlab_quality/test_tooling/labels_inference.rb
375
391
  - lib/gitlab_quality/test_tooling/report/concerns/find_set_dri.rb
376
392
  - lib/gitlab_quality/test_tooling/report/concerns/group_and_category_labels.rb
393
+ - lib/gitlab_quality/test_tooling/report/concerns/issue_reports.rb
377
394
  - lib/gitlab_quality/test_tooling/report/concerns/results_reporter.rb
378
395
  - lib/gitlab_quality/test_tooling/report/concerns/utils.rb
396
+ - lib/gitlab_quality/test_tooling/report/flaky_test_issue.rb
379
397
  - lib/gitlab_quality/test_tooling/report/generate_test_session.rb
380
398
  - lib/gitlab_quality/test_tooling/report/issue_logger.rb
381
399
  - lib/gitlab_quality/test_tooling/report/merge_request_slow_tests_report.rb