gitlab_quality-test_tooling 1.5.4 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: acc009f4f6d5f4be05fa85cd3fffc68778d415d970ca96f22ace03c8a86f9488
4
- data.tar.gz: 5c31655a8245c632e595431fb99ec14852bfd4f9b9778d09e8b8d0ae3393dbc1
3
+ metadata.gz: 4ee3407afd5274be75c1bb63c7f7aeeb0d2353cf62f0fb75c1393bd2103c8c1e
4
+ data.tar.gz: a4965dd9d979a58c4202c6f25315d60e08d597d51322db491a077a0ae797cf34
5
5
  SHA512:
6
- metadata.gz: 754b2ed722bc050515ceb9f4691bc49da1e63c27c1ff44b3cd97bc597d6691f947a2390b63b41e980b4229332817671bc9cad351fee4455d424ce66f1d54cf03
7
- data.tar.gz: ff2b356903435078c53b6234e69d57e272c9f367cd01518158fda38a94372ceeb1fe1e2f2cebec16e2aaa0dd4db9c98592c36631c858119d3340cf4bcd961bda
6
+ metadata.gz: 1619807ec2456f02c9d4a1c09dfb0aeba598de7d51fe7a0377c1e5a9f3f9fc82cf607fa34387bb5ee1691ad28b5637e75c18c38ce068269c3bba76e3c902df22
7
+ data.tar.gz: '0940057d402dfb8fbcd3a6d2d09f7ff698e26d140ebdda659fcc5347445574fa7f398b2bd91e8a39c872b0f8a9f368fa1f734e59e10262790ed2d507fb97e514'
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.8.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
@@ -77,14 +77,20 @@ Usage: exe/prepare-stage-reports [options]
77
77
  Purpose: Relate test failures to failure issues from RSpec report files (JSON or JUnit XML)
78
78
  Usage: exe/relate-failure-issue [options]
79
79
  -i, --input-files INPUT_FILES RSpec report files (JSON or JUnit XML)
80
+ -m METRICS_FILES, Test metrics files (JSON)
81
+ --metrics-files
80
82
  --max-diff-ratio MAX_DIFF_RATO
81
83
  Max stacktrace diff ratio for failure issues detection
82
84
  -p, --project PROJECT Can be an integer or a group/project string
83
85
  -t, --token TOKEN A valid access token with `api` scope and Maintainer permission in PROJECT
86
+ -r RELATED_ISSUES_FILE, The file path for the related issues
87
+ --related-issues-file
84
88
  --system-log-files SYSTEM_LOG_FILES
85
89
  Include errors from system logs in failure issues
86
90
  --base-issue-labels BASE_ISSUE_LABELS
87
91
  Labels to add to new failure issues
92
+ --exclude-labels-for-search EXCLUDE_LABELS_FOR_SEARCH
93
+ Labels to exclude when searching for existing issues
88
94
  --confidential Makes created new issues confidential
89
95
  --dry-run Perform a dry-run (don't create or update issues)
90
96
  -v, --version Show the version
@@ -133,6 +139,21 @@ Usage: exe/slow-test-issue [options]
133
139
  -h, --help Show the usage
134
140
  ```
135
141
 
142
+ ### `exe/flaky-test-issues`
143
+
144
+ ```shell
145
+ Purpose: Purpose: Create flaky test issues for any passed test coming from rspec-retry JSON report files
146
+ Usage: exe/flaky-test-issue [options]
147
+ -i, --input-files INPUT_FILES JSON rspec-retry report files
148
+ -p, --project PROJECT Can be an integer or a group/project string
149
+ -m MERGE_REQUEST_IID, An integer merge request IID
150
+ --merge_request_iid
151
+ -t, --token TOKEN A valid access token with `api` scope and Maintainer permission in PROJECT
152
+ --dry-run Perform a dry-run (don't create note)
153
+ -v, --version Show the version
154
+ -h, --help Show the usage
155
+ ```
156
+
136
157
  ### `exe/slow-test-merge-request-report-note`
137
158
 
138
159
  ```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
@@ -15,6 +15,10 @@ options = OptionParser.new do |opts|
15
15
  params[:input_files] = input_files
16
16
  end
17
17
 
18
+ opts.on('-m', '--metrics-files METRICS_FILES', String, 'Test metrics files (JSON)') do |metrics_files|
19
+ params[:metrics_files] = metrics_files
20
+ end
21
+
18
22
  opts.on('--max-diff-ratio MAX_DIFF_RATO', Float, 'Max stacktrace diff ratio for failure issues detection') do |max_diff_ratio|
19
23
  params[:max_diff_ratio] = max_diff_ratio
20
24
  end
@@ -41,6 +45,11 @@ options = OptionParser.new do |opts|
41
45
  params[:base_issue_labels] = base_issue_labels.split(',')
42
46
  end
43
47
 
48
+ opts.on('--exclude-labels-for-search EXCLUDE_LABELS_FOR_SEARCH', String,
49
+ 'Labels to exclude when searching for existing issues') do |exclude_labels_for_search|
50
+ params[:exclude_labels_for_search] = exclude_labels_for_search.split(',')
51
+ end
52
+
44
53
  opts.on('--confidential', "Makes created new issues confidential") do
45
54
  params[:confidential] = true
46
55
  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,26 @@ 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
+ metrics_files: [],
45
+ **kwargs)
41
46
  super
42
47
  @max_diff_ratio = max_diff_ratio.to_f
43
48
  @system_logs = Dir.glob(system_logs)
44
49
  @base_issue_labels = Set.new(base_issue_labels)
50
+ @exclude_labels_for_search = Set.new(exclude_labels_for_search)
45
51
  @issue_type = 'issue'
46
52
  @commented_issue_list = Set.new
53
+ @metrics_files = Array(metrics_files)
47
54
  end
48
55
 
49
56
  private
50
57
 
51
- attr_reader :max_diff_ratio, :system_logs, :base_issue_labels
58
+ attr_reader :max_diff_ratio, :system_logs, :base_issue_labels, :exclude_labels_for_search, :metrics_files
52
59
 
53
60
  def run!
54
61
  puts "Reporting test failures in `#{files.join(',')}` as issues in project `#{project}` via the API at `#{Runtime::Env.gitlab_api_base}`."
@@ -61,16 +68,39 @@ module GitlabQuality
61
68
  write_issues_log_file
62
69
  end
63
70
 
71
+ def test_metric_collections
72
+ @test_metric_collections ||= Dir.glob(metrics_files).map do |path|
73
+ TestMetrics::JsonTestMetricCollection.new(path)
74
+ end
75
+ end
76
+
64
77
  def process_test_results(test_results)
65
78
  systemic_failures = systemic_failures_for_test_results(test_results)
66
79
 
67
80
  test_results.each do |test|
68
81
  collect_issues(test, relate_failure_to_issue(test)) if should_report?(test, systemic_failures)
82
+
83
+ copy_failure_issue_to_test_metrics(test) if metrics_files.any?
69
84
  end
70
85
 
71
86
  test_results.write
72
87
  end
73
88
 
89
+ def copy_failure_issue_to_test_metrics(test)
90
+ failure_issue = test.failure_issue
91
+
92
+ return unless failure_issue
93
+
94
+ test_metric_collections.find do |test_metric_collection|
95
+ test_metric = test_metric_collection.metric_for_test_id(test.example_id)
96
+
97
+ if test_metric
98
+ test_metric.fields['failure_issue'] = failure_issue
99
+ test_metric_collection.write
100
+ end
101
+ end
102
+ end
103
+
74
104
  def systemic_failures_for_test_results(test_results)
75
105
  test_results
76
106
  .flat_map { |test| test.failures.map { |failure| failure['message'].lines.first.chomp } }
@@ -136,35 +166,15 @@ module GitlabQuality
136
166
 
137
167
  def pipeline_issues_with_similar_stacktrace(test)
138
168
  search_labels = (base_issue_labels + Set.new(%w[test failure::new])).to_a
169
+ not_labels = exclude_labels_for_search.to_a
139
170
  gitlab.find_issues(options: { labels: search_labels,
171
+ not: { labels: not_labels },
140
172
  created_after: past_timestamp(2) }).select do |issue|
141
173
  job_url_from_issue = failed_issue_job_url(issue)
142
174
 
143
175
  next if pipeline != pipeline_env_from_job_url(job_url_from_issue)
144
176
 
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
177
+ compare_stack_traces(cleaned_stack_trace_from_test(test), cleaned_stack_trace_from_issue(issue)) < max_diff_ratio
168
178
  end
169
179
  end
170
180
 
@@ -184,7 +194,11 @@ module GitlabQuality
184
194
  end
185
195
 
186
196
  def failure_issues(test)
187
- find_issues(test, (base_issue_labels + Set.new(%w[test])).to_a) # Search for opened and closed issues
197
+ find_issues_for_test(
198
+ test,
199
+ labels: base_issue_labels + Set.new(%w[test]),
200
+ not_labels: exclude_labels_for_search
201
+ )
188
202
  end
189
203
 
190
204
  def full_stacktrace(test)
@@ -205,7 +219,7 @@ module GitlabQuality
205
219
  remove_unique_resource_names(relevant_issue_stacktrace)
206
220
  end
207
221
 
208
- def cleaned_stacktrace_from_test(test)
222
+ def cleaned_stack_trace_from_test(test)
209
223
  test_failure_stacktrace = sanitize_stacktrace(full_stacktrace(test),
210
224
  FAILURE_STACKTRACE_REGEX) || full_stacktrace(test)
211
225
  remove_unique_resource_names(test_failure_stacktrace)
@@ -221,7 +235,7 @@ module GitlabQuality
221
235
  end
222
236
 
223
237
  def find_relevant_failure_issues(test) # rubocop:disable Metrics/AbcSize
224
- clean_first_test_failure_stacktrace = cleaned_stacktrace_from_test(test)
238
+ clean_first_test_failure_stacktrace = cleaned_stack_trace_from_test(test)
225
239
  # Search with the `search` param returns 500 errors, so we filter by `base_issue_labels` and then filter further in Ruby
226
240
  failure_issues(test).each_with_object({}) do |issue, memo|
227
241
  clean_relevant_issue_stacktrace = cleaned_stack_trace_from_issue(issue)
@@ -289,7 +303,7 @@ module GitlabQuality
289
303
  "```\n#{full_stacktrace(test)}\n```",
290
304
  screenshot_section(test),
291
305
  system_log_errors_section(test),
292
- reports_section(test)
306
+ initial_reports_section(test)
293
307
  ].compact.join("\n\n")
294
308
  end
295
309
 
@@ -312,18 +326,6 @@ module GitlabQuality
312
326
  section
313
327
  end
314
328
 
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
329
  def up_to_date_labels(test:, issue: nil, new_labels: Set.new)
328
330
  (Set.new(base_issue_labels) + (super << pipeline_name_label)).to_a
329
331
  end
@@ -348,7 +350,7 @@ module GitlabQuality
348
350
  state_event = issue.state == 'closed' ? 'reopen' : nil
349
351
 
350
352
  issue_attrs = {
351
- description: up_to_date_issue_description(issue.description, test),
353
+ description: add_report_to_issue_description(issue, test),
352
354
  labels: up_to_date_labels(test: test, issue: issue)
353
355
  }
354
356
  issue_attrs[:state_event] = state_event if state_event
@@ -357,26 +359,6 @@ module GitlabQuality
357
359
  puts " => Added a report in '#{issue.title}': #{issue.web_url}!"
358
360
  end
359
361
 
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
362
  def new_issue_title(test)
381
363
  "Failure in #{super}"
382
364
  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)
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GitlabQuality
4
+ module TestTooling
5
+ module TestMetric
6
+ class JsonTestMetric
7
+ attr_reader :metric
8
+
9
+ def initialize(metric)
10
+ @metric = metric
11
+ end
12
+
13
+ def name
14
+ metric.fetch('name')
15
+ end
16
+
17
+ def time
18
+ metric.fetch('time')
19
+ end
20
+
21
+ def tags
22
+ @tags ||= metric.fetch('tags')
23
+ end
24
+
25
+ def fields
26
+ @fields ||= metric.fetch('fields')
27
+ end
28
+
29
+ def to_json(*options)
30
+ as_json.to_json(*options)
31
+ end
32
+
33
+ private
34
+
35
+ def as_json
36
+ {
37
+ name: name,
38
+ time: time,
39
+ tags: tags,
40
+ fields: fields
41
+ }
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module GitlabQuality
6
+ module TestTooling
7
+ module TestMetrics
8
+ class JsonTestMetricCollection
9
+ include Enumerable
10
+
11
+ attr_reader :path, :metrics
12
+
13
+ def initialize(path)
14
+ @path = path
15
+ @metrics = process
16
+ end
17
+
18
+ def metric_for_test_id(test_id)
19
+ metrics.find do |metric|
20
+ metric.fields['id'] == test_id
21
+ end
22
+ end
23
+
24
+ def write
25
+ File.write(path, JSON.pretty_generate(metrics))
26
+ end
27
+
28
+ private
29
+
30
+ def parse
31
+ JSON.parse(File.read(path))
32
+ rescue JSON::ParserError
33
+ Runtime::Logger.debug("#{self.class.name}##{__method__} attempted to parse invalid JSON at path: #{path}")
34
+ {}
35
+ end
36
+
37
+ def process
38
+ parse.map do |test|
39
+ GitlabQuality::TestTooling::TestMetric::JsonTestMetric.new(test)
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -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.8.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.8.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-30 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
@@ -404,6 +422,8 @@ files:
404
422
  - lib/gitlab_quality/test_tooling/system_logs/log_types/rails/graphql_log.rb
405
423
  - lib/gitlab_quality/test_tooling/system_logs/shared_fields.rb
406
424
  - lib/gitlab_quality/test_tooling/system_logs/system_logs_formatter.rb
425
+ - lib/gitlab_quality/test_tooling/test_metric/json_test_metric.rb
426
+ - lib/gitlab_quality/test_tooling/test_metrics/json_test_metric_collection.rb
407
427
  - lib/gitlab_quality/test_tooling/test_result/base_test_result.rb
408
428
  - lib/gitlab_quality/test_tooling/test_result/j_unit_test_result.rb
409
429
  - lib/gitlab_quality/test_tooling/test_result/json_test_result.rb