gitlab_quality-test_tooling 1.5.4 → 1.8.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: 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