gitlab_quality-test_tooling 1.12.0 → 1.14.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 +4 -4
- data/Gemfile.lock +23 -1
- data/README.md +31 -0
- data/exe/knapsack-report-issues +54 -0
- data/exe/update-test-meta +70 -0
- data/lib/gitlab_quality/test_tooling/knapsack_reports/spec_run_time.rb +85 -0
- data/lib/gitlab_quality/test_tooling/knapsack_reports/spec_run_time_report.rb +60 -0
- data/lib/gitlab_quality/test_tooling/report/concerns/utils.rb +22 -0
- data/lib/gitlab_quality/test_tooling/report/knapsack_report_issue.rb +142 -0
- data/lib/gitlab_quality/test_tooling/report/relate_failure_issue.rb +0 -2
- data/lib/gitlab_quality/test_tooling/report/report_as_issue.rb +11 -2
- data/lib/gitlab_quality/test_tooling/report/slow_test_issue.rb +0 -2
- data/lib/gitlab_quality/test_tooling/test_meta/processor/add_to_blocking_processor.rb +126 -0
- data/lib/gitlab_quality/test_tooling/test_meta/processor/add_to_quarantine_processor.rb +184 -0
- data/lib/gitlab_quality/test_tooling/test_meta/processor/meta_processor.rb +23 -0
- data/lib/gitlab_quality/test_tooling/test_meta/test_meta_updater.rb +252 -0
- data/lib/gitlab_quality/test_tooling/version.rb +1 -1
- metadata +27 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 970354551e0118a942865386f3e844762cf1fdfb06cfc02aa7bca602e55abc8f
|
|
4
|
+
data.tar.gz: f512f45f80e5a3949b07a237c1f29c6bc73fcd8e320a92b6b3fcd0a23ccc311f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 639c54db934d3fb5ee5cc57264957a6ffff2714fff9dce6517701b7ff4e5ce74fca87a518f35b1e8d52852952d875f6b9aa45e79a506fbb9416d9712bba2ab95
|
|
7
|
+
data.tar.gz: 1dbfcfab9f4161827ae2775a69f6203008cc4ea07dc23424a971c5451a354ebb28d7a698e922aecf8ca321a9612a05e5adf966c1bc0e38686c249aaa87ce6446
|
data/Gemfile.lock
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
gitlab_quality-test_tooling (1.
|
|
4
|
+
gitlab_quality-test_tooling (1.14.0)
|
|
5
5
|
activesupport (>= 6.1, < 7.2)
|
|
6
6
|
amatch (~> 0.4.1)
|
|
7
7
|
gitlab (~> 4.19)
|
|
@@ -9,6 +9,7 @@ PATH
|
|
|
9
9
|
nokogiri (~> 1.10)
|
|
10
10
|
parallel (>= 1, < 2)
|
|
11
11
|
rainbow (>= 3, < 4)
|
|
12
|
+
rspec-parameterized (~> 1.0.0)
|
|
12
13
|
table_print (= 1.5.7)
|
|
13
14
|
zeitwerk (>= 2, < 3)
|
|
14
15
|
|
|
@@ -29,6 +30,8 @@ GEM
|
|
|
29
30
|
ast (2.4.2)
|
|
30
31
|
backport (1.2.0)
|
|
31
32
|
benchmark (0.2.1)
|
|
33
|
+
binding_of_caller (1.0.0)
|
|
34
|
+
debug_inspector (>= 0.0.1)
|
|
32
35
|
byebug (11.1.3)
|
|
33
36
|
claide (1.1.0)
|
|
34
37
|
claide-plugins (0.9.2)
|
|
@@ -59,6 +62,7 @@ GEM
|
|
|
59
62
|
danger-gitlab (8.0.0)
|
|
60
63
|
danger
|
|
61
64
|
gitlab (~> 4.2, >= 4.2.0)
|
|
65
|
+
debug_inspector (1.2.0)
|
|
62
66
|
diff-lcs (1.5.0)
|
|
63
67
|
docile (1.4.0)
|
|
64
68
|
domain_name (0.5.20190701)
|
|
@@ -156,6 +160,10 @@ GEM
|
|
|
156
160
|
parallel (1.23.0)
|
|
157
161
|
parser (3.2.2.1)
|
|
158
162
|
ast (~> 2.4.1)
|
|
163
|
+
proc_to_ast (0.1.0)
|
|
164
|
+
coderay
|
|
165
|
+
parser
|
|
166
|
+
unparser
|
|
159
167
|
protocol (2.0.0)
|
|
160
168
|
ruby_parser (~> 3.0)
|
|
161
169
|
pry (0.14.2)
|
|
@@ -190,6 +198,17 @@ GEM
|
|
|
190
198
|
rspec-mocks (3.12.5)
|
|
191
199
|
diff-lcs (>= 1.2.0, < 2.0)
|
|
192
200
|
rspec-support (~> 3.12.0)
|
|
201
|
+
rspec-parameterized (1.0.0)
|
|
202
|
+
rspec-parameterized-core (< 2)
|
|
203
|
+
rspec-parameterized-table_syntax (< 2)
|
|
204
|
+
rspec-parameterized-core (1.0.0)
|
|
205
|
+
parser
|
|
206
|
+
proc_to_ast
|
|
207
|
+
rspec (>= 2.13, < 4)
|
|
208
|
+
unparser
|
|
209
|
+
rspec-parameterized-table_syntax (1.0.1)
|
|
210
|
+
binding_of_caller
|
|
211
|
+
rspec-parameterized-core (< 2)
|
|
193
212
|
rspec-support (3.12.0)
|
|
194
213
|
rubocop (1.43.0)
|
|
195
214
|
json (~> 2.3)
|
|
@@ -266,6 +285,9 @@ GEM
|
|
|
266
285
|
unf_ext
|
|
267
286
|
unf_ext (0.0.8.2)
|
|
268
287
|
unicode-display_width (2.4.2)
|
|
288
|
+
unparser (0.6.8)
|
|
289
|
+
diff-lcs (~> 1.3)
|
|
290
|
+
parser (>= 3.2.0)
|
|
269
291
|
webmock (3.7.0)
|
|
270
292
|
addressable (>= 2.3.6)
|
|
271
293
|
crack (>= 0.3.2)
|
data/README.md
CHANGED
|
@@ -141,6 +141,21 @@ Usage: exe/slow-test-issues [options]
|
|
|
141
141
|
-h, --help Show the usage
|
|
142
142
|
```
|
|
143
143
|
|
|
144
|
+
### `exe/knapsack-report-issues`
|
|
145
|
+
|
|
146
|
+
```shell
|
|
147
|
+
Purpose: Create spec run time issue when a spec file almost caused job timeout because it ran significantly longer than what Knapsack expected.
|
|
148
|
+
Usage: exe/knapsack-report-issues [options]
|
|
149
|
+
-i, --input-file INPUT_FILE Knapsack actual run time report file path glob
|
|
150
|
+
-e EXPECTED_REPORT, Knapsack expected report file path
|
|
151
|
+
--expected-report
|
|
152
|
+
-p, --project PROJECT Can be an integer or a group/project string
|
|
153
|
+
-t, --token TOKEN A valid access token with `api` scope and Maintainer permission in PROJECT
|
|
154
|
+
--dry-run Perform a dry-run (don't create issues)
|
|
155
|
+
-v, --version Show the version
|
|
156
|
+
-h, --help Show the usage
|
|
157
|
+
```
|
|
158
|
+
|
|
144
159
|
### `exe/flaky-test-issues`
|
|
145
160
|
|
|
146
161
|
```shell
|
|
@@ -173,6 +188,22 @@ Usage: exe/slow-test-merge-request-report-note [options]
|
|
|
173
188
|
-h, --help Show the usage
|
|
174
189
|
```
|
|
175
190
|
|
|
191
|
+
### `exe/update-test-meta`
|
|
192
|
+
|
|
193
|
+
```shell
|
|
194
|
+
Purpose: Add quarantine or reliable meta to specs
|
|
195
|
+
Usage: exe/update-test-meta [options]
|
|
196
|
+
-u INPUT_FILES, File with list of unstable specs (JSON) to quarantine
|
|
197
|
+
--unstable-specs-file
|
|
198
|
+
-s INPUT_FILES, File with list of stable specs (JSON) to add :reliable meta
|
|
199
|
+
--stable-specs-file
|
|
200
|
+
-p, --project PROJECT Can be an integer or a group/project string
|
|
201
|
+
-t, --token TOKEN A valid access token with `api` scope and Maintainer permission in PROJECT
|
|
202
|
+
--dry-run Perform a dry-run (don't create branches, commits or MRs)
|
|
203
|
+
-v, --version Show the version
|
|
204
|
+
-h, --help Show the usage
|
|
205
|
+
```
|
|
206
|
+
|
|
176
207
|
## Development
|
|
177
208
|
|
|
178
209
|
### Initial setup
|
|
@@ -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-file INPUT_FILE", String, "Knapsack actual run time report file path glob") do |input_file|
|
|
15
|
+
params[:input_files] = input_file
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
opts.on("-e", "--expected-report EXPECTED_REPORT", String, "Knapsack expected report file path") do |report_path|
|
|
19
|
+
params[:expected_report] = report_path.strip
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
opts.on('-p', '--project PROJECT', String, 'Can be an integer or a group/project string') do |project|
|
|
23
|
+
params[:project] = project
|
|
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 spec run time issue when a spec file almost caused job timeout because it ran significantly longer than what Knapsack expected."
|
|
42
|
+
puts opts
|
|
43
|
+
exit
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
opts.parse(ARGV)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
if params.any?
|
|
50
|
+
GitlabQuality::TestTooling::Report::KnapsackReportIssue.new(**params).invoke!
|
|
51
|
+
else
|
|
52
|
+
puts options
|
|
53
|
+
exit 1
|
|
54
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
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('-u', '--unstable-specs-file INPUT_FILES', String, 'File with list of unstable specs (JSON) to quarantine') do |unstable_specs_file|
|
|
15
|
+
params[:unstable_specs_file] = unstable_specs_file
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
opts.on('-s', '--stable-specs-file INPUT_FILES', String, 'File with list of stable specs (JSON) to add :blocking meta') do |stable_specs_file|
|
|
19
|
+
params[:stable_specs_file] = stable_specs_file
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
opts.on('-p', '--project PROJECT', String, 'Can be an integer or a group/project string') do |project|
|
|
23
|
+
params[:project] = project
|
|
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 branches, commits or MRs)") 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: Add quarantine or blocking meta to specs"
|
|
42
|
+
puts opts
|
|
43
|
+
exit
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
opts.parse(ARGV)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
if params.any?
|
|
50
|
+
if params[:unstable_specs_file] && params[:stable_specs_file]
|
|
51
|
+
puts "Please provide only one of one of -u and -s"
|
|
52
|
+
exit 1
|
|
53
|
+
elsif !params[:unstable_specs_file] && !params[:stable_specs_file]
|
|
54
|
+
puts "Please provide at least one of one of -u and -s"
|
|
55
|
+
exit 1
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
if params[:unstable_specs_file]
|
|
59
|
+
params[:specs_file] = params.delete(:unstable_specs_file)
|
|
60
|
+
params[:processor] = GitlabQuality::TestTooling::TestMeta::Processor::AddToQuarantineProcessor
|
|
61
|
+
else
|
|
62
|
+
params[:specs_file] = params.delete(:stable_specs_file)
|
|
63
|
+
params[:processor] = GitlabQuality::TestTooling::TestMeta::Processor::AddToBlockingProcessor
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
GitlabQuality::TestTooling::TestMeta::TestMetaUpdater.new(**params).invoke!
|
|
67
|
+
else
|
|
68
|
+
puts options
|
|
69
|
+
exit 1
|
|
70
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GitlabQuality
|
|
4
|
+
module TestTooling
|
|
5
|
+
module KnapsackReports
|
|
6
|
+
class SpecRunTime
|
|
7
|
+
attr_reader :file, :expected, :actual, :expected_suite_duration, :actual_suite_duration
|
|
8
|
+
|
|
9
|
+
ACTUAL_TO_EXPECTED_SPEC_RUN_TIME_RATIO_THRESHOLD = 1.5 # actual run time is longer than expected by 50% +
|
|
10
|
+
SPEC_WEIGHT_PERCENTAGE_TRESHOLD = 15 # a spec file takes 15%+ of the total test suite run time
|
|
11
|
+
SUITE_DURATION_THRESHOLD = 70 * 60 # if test suite takes more than 70 minutes, job risks timing out
|
|
12
|
+
|
|
13
|
+
def initialize(file:, expected:, actual:, expected_suite_duration:, actual_suite_duration:)
|
|
14
|
+
@file = file
|
|
15
|
+
@expected = expected.to_f
|
|
16
|
+
@actual = actual.to_f
|
|
17
|
+
@expected_suite_duration = expected_suite_duration.to_f
|
|
18
|
+
@actual_suite_duration = actual_suite_duration.to_f
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def should_report?
|
|
22
|
+
# guideline proposed in https://gitlab.com/gitlab-org/quality/engineering-productivity/team/-/issues/354
|
|
23
|
+
exceed_actual_to_expected_ratio_threshold? && test_suite_bottleneck?
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def ci_pipeline_url_markdown
|
|
27
|
+
"[#{ci_pipeline_id}](#{ci_pipeline_url})"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def ci_pipeline_created_at
|
|
31
|
+
ENV.fetch('CI_PIPELINE_CREATED_AT', nil)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def ci_job_link_markdown
|
|
35
|
+
"[#{ci_job_name}](#{ci_job_url})"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def file_link_markdown
|
|
39
|
+
"[#{file}](#{file_link})"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def actual_percentage
|
|
43
|
+
(actual / actual_suite_duration * 100).round(2)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def name
|
|
47
|
+
nil
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def exceed_actual_to_expected_ratio_threshold?
|
|
53
|
+
actual / expected >= ACTUAL_TO_EXPECTED_SPEC_RUN_TIME_RATIO_THRESHOLD
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def test_suite_bottleneck?
|
|
57
|
+
# now we only report bottlenecks when they risk causing job timeouts
|
|
58
|
+
return unless actual_suite_duration > SUITE_DURATION_THRESHOLD
|
|
59
|
+
|
|
60
|
+
actual_percentage > SPEC_WEIGHT_PERCENTAGE_TRESHOLD
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def ci_job_url
|
|
64
|
+
ENV.fetch('CI_JOB_URL', nil)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def ci_job_name
|
|
68
|
+
ENV.fetch('CI_JOB_NAME_SLUG', nil)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def ci_pipeline_id
|
|
72
|
+
ENV.fetch('CI_PIPELINE_IID', nil)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def ci_pipeline_url
|
|
76
|
+
ENV.fetch('CI_PIPELINE_URL', nil)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def file_link
|
|
80
|
+
"#{Runtime::Env.file_base_url}#{file}"
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
|
|
5
|
+
module GitlabQuality
|
|
6
|
+
module TestTooling
|
|
7
|
+
module KnapsackReports
|
|
8
|
+
class SpecRunTimeReport
|
|
9
|
+
attr_reader :expected_report, :actual_report
|
|
10
|
+
|
|
11
|
+
def initialize(expected_report_path:, actual_report_path:)
|
|
12
|
+
@expected_report = parse(expected_report_path)
|
|
13
|
+
@actual_report = parse(actual_report_path)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def filtered_report
|
|
17
|
+
@filtered_report = actual_report.keys.filter_map do |spec_file|
|
|
18
|
+
expected_run_time = expected_report[spec_file]
|
|
19
|
+
actual_run_time = actual_report[spec_file]
|
|
20
|
+
|
|
21
|
+
if expected_run_time.nil?
|
|
22
|
+
puts "#{spec_file} missing from the expected Knapsack report, skipping."
|
|
23
|
+
next
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
spec_run_time = SpecRunTime.new(
|
|
27
|
+
file: spec_file,
|
|
28
|
+
expected: expected_run_time,
|
|
29
|
+
actual: actual_run_time,
|
|
30
|
+
expected_suite_duration: expected_test_suite_run_time_total,
|
|
31
|
+
actual_suite_duration: actual_test_suite_run_time_total
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
spec_run_time if spec_run_time.should_report?
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def parse(report_path)
|
|
41
|
+
JSON.parse(File.read(report_path))
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def expected_test_suite_run_time_total
|
|
45
|
+
@expected_test_suite_run_time_total ||=
|
|
46
|
+
expected_report.reduce(0) do |total_run_time, (_spec_file, run_time)|
|
|
47
|
+
total_run_time + run_time
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def actual_test_suite_run_time_total
|
|
52
|
+
@actual_test_suite_run_time_total ||=
|
|
53
|
+
actual_report.reduce(0) do |total_run_time, (_spec_file, run_time)|
|
|
54
|
+
total_run_time + run_time
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -45,6 +45,28 @@ module GitlabQuality
|
|
|
45
45
|
|
|
46
46
|
@pipeline ||= Runtime::Env.pipeline_from_project_name
|
|
47
47
|
end
|
|
48
|
+
|
|
49
|
+
def readable_duration(duration_in_seconds)
|
|
50
|
+
minutes = (duration_in_seconds / 60).to_i
|
|
51
|
+
seconds = (duration_in_seconds % 60).round(2)
|
|
52
|
+
|
|
53
|
+
min_output = normalize_duration_output(minutes, 'minute')
|
|
54
|
+
sec_output = normalize_duration_output(seconds, 'second')
|
|
55
|
+
|
|
56
|
+
"#{min_output} #{sec_output}".strip
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
def normalize_duration_output(number, unit)
|
|
62
|
+
if number <= 0
|
|
63
|
+
""
|
|
64
|
+
elsif number <= 1
|
|
65
|
+
"#{number} #{unit}"
|
|
66
|
+
else
|
|
67
|
+
"#{number} #{unit}s"
|
|
68
|
+
end
|
|
69
|
+
end
|
|
48
70
|
end
|
|
49
71
|
end
|
|
50
72
|
end
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GitlabQuality
|
|
4
|
+
module TestTooling
|
|
5
|
+
module Report
|
|
6
|
+
# Uses the API to create GitLab issues for spec run time exceeding Knapsack expectation
|
|
7
|
+
#
|
|
8
|
+
# - Takes the expected and actual Knapsack JSON reports from the knapsack output
|
|
9
|
+
# - Takes a project where issues should be created
|
|
10
|
+
# - For every test file reported with unexpectedly long run time:
|
|
11
|
+
# - Find issue by test file name, and if found:
|
|
12
|
+
# - Reopen issue if it already exists, but is closed
|
|
13
|
+
# - Update the issue with the new run time data
|
|
14
|
+
# - If not found:
|
|
15
|
+
# - Create a new issue with the run time data
|
|
16
|
+
class KnapsackReportIssue < ReportAsIssue
|
|
17
|
+
NEW_ISSUE_LABELS = Set.new(['test', 'type::maintenance', 'maintenance::performance', 'priority::3', 'severity::3', 'knapsack_report']).freeze
|
|
18
|
+
SEARCH_LABELS = %w[test maintenance::performance knapsack_report].freeze
|
|
19
|
+
JOB_TIMEOUT_EPIC_URL = 'https://gitlab.com/groups/gitlab-org/quality/engineering-productivity/-/epics/19'
|
|
20
|
+
|
|
21
|
+
def initialize(token:, input_files:, expected_report:, project: nil, dry_run: false)
|
|
22
|
+
super
|
|
23
|
+
|
|
24
|
+
@expected_report = expected_report
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
attr_reader :expected_report
|
|
30
|
+
|
|
31
|
+
def run!
|
|
32
|
+
puts "Reporting spec file exceeding Knapsack expectaton issues in project `#{project}` via the API at `#{Runtime::Env.gitlab_api_base}`."
|
|
33
|
+
|
|
34
|
+
search_and_create_issue
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def search_and_create_issue
|
|
38
|
+
filtered_report = KnapsackReports::SpecRunTimeReport.new(
|
|
39
|
+
expected_report_path: expected_report_path,
|
|
40
|
+
actual_report_path: actual_report_path
|
|
41
|
+
).filtered_report
|
|
42
|
+
|
|
43
|
+
puts "=> Reporting #{filtered_report.count} spec files exceeding Knapsack expectation."
|
|
44
|
+
|
|
45
|
+
filtered_report.each do |spec_with_run_time|
|
|
46
|
+
existing_issues = find_issues_for_test(spec_with_run_time, labels: SEARCH_LABELS)
|
|
47
|
+
|
|
48
|
+
if existing_issues.empty?
|
|
49
|
+
puts "Creating issue for #{spec_with_run_time.file}"
|
|
50
|
+
create_issue(spec_with_run_time)
|
|
51
|
+
else
|
|
52
|
+
update_issue(issue: existing_issues.last, spec_run_time: spec_with_run_time)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def expected_report_path
|
|
58
|
+
return if expected_report.nil? || !File.exist?(expected_report)
|
|
59
|
+
|
|
60
|
+
expected_report
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def actual_report_path
|
|
64
|
+
return if files.nil? || !File.exist?(files.first)
|
|
65
|
+
|
|
66
|
+
files.first
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def new_issue_labels(_spec_run_time)
|
|
70
|
+
NEW_ISSUE_LABELS
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def new_issue_title(spec_run_time)
|
|
74
|
+
"Job timeout risk: #{spec_run_time.file} ran much longer than expected"
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def new_issue_description(spec_run_time)
|
|
78
|
+
<<~MARKDOWN.chomp
|
|
79
|
+
/epic #{JOB_TIMEOUT_EPIC_URL}
|
|
80
|
+
|
|
81
|
+
### Why was this issue created?
|
|
82
|
+
|
|
83
|
+
#{spec_run_time.file_link_markdown} was reported to have:
|
|
84
|
+
|
|
85
|
+
1. exceeded Knapsack's expected runtime by at least 50%, and
|
|
86
|
+
2. been identified as a notable pipeline bottleneck and a job timeout risk
|
|
87
|
+
|
|
88
|
+
### Suggested steps for investigation
|
|
89
|
+
|
|
90
|
+
1. To reproduce in CI by running test files in the same order, you can follow the steps listed [here](https://docs.gitlab.com/ee/development/testing_guide/flaky_tests.html#recreate-job-failure-in-ci-by-forcing-the-job-to-run-the-same-set-of-test-files).
|
|
91
|
+
1. Identify if a specific test case is stalling the run time. Hint: You can search the job's log for `Starting example group #{spec_run_time.file}` and view the elapsed time after each test case in the proceeding lines starting with `[RSpecRunTime]`.
|
|
92
|
+
1. If the test file is large, consider refactoring it into multiple files to allow better test parallelization across runners.
|
|
93
|
+
1. If the run time cannot be fixed in time, consider quarantine the spec(s) to restore performance.
|
|
94
|
+
|
|
95
|
+
### Run time details
|
|
96
|
+
|
|
97
|
+
#{run_time_detail(spec_run_time)}
|
|
98
|
+
MARKDOWN
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def update_issue(issue:, spec_run_time:)
|
|
102
|
+
state_event = issue.state == 'closed' ? 'reopen' : nil
|
|
103
|
+
updated_description = <<~MARKDOWN.chomp
|
|
104
|
+
#{issue.description}
|
|
105
|
+
|
|
106
|
+
#{run_time_detail(spec_run_time)}
|
|
107
|
+
MARKDOWN
|
|
108
|
+
|
|
109
|
+
issue_attrs = {
|
|
110
|
+
description: updated_description
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
issue_attrs[:state_event] = state_event if state_event
|
|
114
|
+
|
|
115
|
+
gitlab.edit_issue(iid: issue.iid, options: issue_attrs)
|
|
116
|
+
puts " => Added a report in #{issue.web_url}!"
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def run_time_detail(spec_run_time)
|
|
120
|
+
<<~MARKDOWN.chomp
|
|
121
|
+
- Reported from pipeline #{spec_run_time.ci_pipeline_url_markdown} created at `#{spec_run_time.ci_pipeline_created_at}`
|
|
122
|
+
|
|
123
|
+
| Field | Value |
|
|
124
|
+
| ------ | ------ |
|
|
125
|
+
| Job URL| #{spec_run_time.ci_job_link_markdown} |
|
|
126
|
+
| Job total RSpec suite run time | expected: `#{readable_duration(spec_run_time.expected_suite_duration)}`, actual: `#{readable_duration(spec_run_time.actual_suite_duration)}` |
|
|
127
|
+
| Spec file run time | expected: `#{readable_duration(spec_run_time.expected)}`, actual: `#{readable_duration(spec_run_time.actual)}` |
|
|
128
|
+
| Spec file weight | `#{spec_run_time.actual_percentage}%` of total suite run time |
|
|
129
|
+
MARKDOWN
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def assert_input_files!(_files)
|
|
133
|
+
missing_expected_report_msg = "Missing a valid expected Knapsack report."
|
|
134
|
+
missing_actual_report_msg = "Missing a valid actual Knapsack report."
|
|
135
|
+
|
|
136
|
+
abort missing_expected_report_msg if expected_report_path.nil?
|
|
137
|
+
abort missing_actual_report_msg if actual_report_path.nil?
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
@@ -19,7 +19,11 @@ module GitlabQuality
|
|
|
19
19
|
def invoke!
|
|
20
20
|
validate_input!
|
|
21
21
|
|
|
22
|
-
run!
|
|
22
|
+
issue_url = run!
|
|
23
|
+
|
|
24
|
+
write_issues_log_file
|
|
25
|
+
|
|
26
|
+
issue_url
|
|
23
27
|
end
|
|
24
28
|
|
|
25
29
|
private
|
|
@@ -162,7 +166,12 @@ module GitlabQuality
|
|
|
162
166
|
def issue_match_test?(issue, test)
|
|
163
167
|
issue_title = issue.title.strip
|
|
164
168
|
test_file_path_found = !test.file.to_s.empty? && issue_title.include?(partial_file_path(test.file))
|
|
165
|
-
|
|
169
|
+
|
|
170
|
+
if test.name
|
|
171
|
+
issue_title.include?(test.name) || test_file_path_found
|
|
172
|
+
else
|
|
173
|
+
test_file_path_found
|
|
174
|
+
end
|
|
166
175
|
end
|
|
167
176
|
|
|
168
177
|
def pipeline_name_label
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GitlabQuality
|
|
4
|
+
module TestTooling
|
|
5
|
+
module TestMeta
|
|
6
|
+
module Processor
|
|
7
|
+
class AddToBlockingProcessor < MetaProcessor
|
|
8
|
+
BLOCKING_METADATA = ", :blocking%{suffix}"
|
|
9
|
+
|
|
10
|
+
class << self
|
|
11
|
+
# Execute the processor
|
|
12
|
+
#
|
|
13
|
+
# @param [Hash] spec the spec to update
|
|
14
|
+
# @param [TestMetaUpdater] context instance of TestMetaUpdater
|
|
15
|
+
def execute(spec, context) # rubocop:disable Metrics/AbcSize
|
|
16
|
+
@context = context
|
|
17
|
+
|
|
18
|
+
@file_path = spec["file_path"]
|
|
19
|
+
devops_stage = spec["stage"]
|
|
20
|
+
product_group = spec["product_group"]
|
|
21
|
+
@example_name = spec["name"]
|
|
22
|
+
@mr_title = format("%{prefix} %{example_name}", prefix: '[PROMOTE TO BLOCKING]', example_name: example_name)
|
|
23
|
+
|
|
24
|
+
return unless proceed_with_merge_request?
|
|
25
|
+
|
|
26
|
+
@file_contents = context.get_file_contents(file_path)
|
|
27
|
+
|
|
28
|
+
new_content, changed_line_no = add_blocking_metadata
|
|
29
|
+
|
|
30
|
+
return if changed_line_no.negative?
|
|
31
|
+
|
|
32
|
+
branch = context.create_branch("blocking-promotion-#{SecureRandom.hex(4)}", example_name, context.ref)
|
|
33
|
+
|
|
34
|
+
context.commit_changes(branch, <<~COMMIT_MESSAGE, file_path, new_content)
|
|
35
|
+
Promote end-to-end test to blocking
|
|
36
|
+
|
|
37
|
+
Promote to blocking: #{example_name}
|
|
38
|
+
COMMIT_MESSAGE
|
|
39
|
+
|
|
40
|
+
assignee_id, assignee_handle = context.fetch_dri_id(product_group, devops_stage)
|
|
41
|
+
|
|
42
|
+
merge_request = context.create_merge_request(mr_title, branch, assignee_id) do
|
|
43
|
+
<<~MARKDOWN
|
|
44
|
+
## What does this MR do?
|
|
45
|
+
|
|
46
|
+
Promotes the test [`#{example_name}`](https://gitlab.com/#{context.project}/-/blob/#{context.ref}/#{file_path}#L#{changed_line_no + 1})
|
|
47
|
+
to the blocking bucket
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
/label ~"Quality" ~"QA" ~"type::maintenance"
|
|
51
|
+
/label ~"devops::#{devops_stage}"
|
|
52
|
+
|
|
53
|
+
<div align="center">
|
|
54
|
+
(This MR was automatically generated by [`gitlab_quality-test_tooling`](https://gitlab.com/gitlab-org/ruby/gems/gitlab_quality-test_tooling) at #{Time.now.utc})
|
|
55
|
+
</div>
|
|
56
|
+
MARKDOWN
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
context.post_note_on_merge_request(<<~MARKDOWN, merge_request.iid)
|
|
60
|
+
@#{assignee_handle} Please review this MR, approve and assign it to a maintainer.
|
|
61
|
+
|
|
62
|
+
If you think this MR should not be merged, please close it and add a note of the reason to the blocking report: #{context.report_issue}
|
|
63
|
+
MARKDOWN
|
|
64
|
+
|
|
65
|
+
merge_request
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Performs post processing. Takes a list of MRs and posts them in a note on report_issue
|
|
69
|
+
#
|
|
70
|
+
# @param [Gitlab::ObjectifiedHash] merge_requests
|
|
71
|
+
def post_process(merge_requests)
|
|
72
|
+
web_urls = merge_requests.compact.map { |mr| "- #{mr.web_url}\n" }.join
|
|
73
|
+
|
|
74
|
+
return if web_urls.empty?
|
|
75
|
+
|
|
76
|
+
context.post_note_on_report_issue(<<~ISSUE_NOTE)
|
|
77
|
+
The following merge requests have been created to promote stable specs to blocking:
|
|
78
|
+
|
|
79
|
+
#{web_urls}
|
|
80
|
+
ISSUE_NOTE
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
private
|
|
84
|
+
|
|
85
|
+
attr_reader :context, :file_path, :file_contents, :example_name, :mr_title
|
|
86
|
+
|
|
87
|
+
# Checks if there is already an MR open
|
|
88
|
+
#
|
|
89
|
+
# @return [Boolean]
|
|
90
|
+
def proceed_with_merge_request?
|
|
91
|
+
open_mrs = context.existing_merge_requests(title: mr_title)
|
|
92
|
+
if open_mrs.any?
|
|
93
|
+
puts " An open MR already exists for '#{example_name}': #{open_mrs.first['web_url']}. Will not proceed with creating MR."
|
|
94
|
+
return false
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
true
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Add blocking metadata to the file content and replace it
|
|
101
|
+
#
|
|
102
|
+
# @return [Array<String, Integer>] first value holds the new content, the second value holds the line number of the test
|
|
103
|
+
def add_blocking_metadata # rubocop:disable Metrics/AbcSize
|
|
104
|
+
matched_lines = context.find_example_match_lines(file_contents, example_name)
|
|
105
|
+
|
|
106
|
+
if matched_lines.any? { |line| line[0].include?(':blocking') }
|
|
107
|
+
puts "Example '#{example_name}' is already blocking"
|
|
108
|
+
return [file_contents, -1]
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
context.update_matched_line(matched_lines.last, file_contents.dup) do |line|
|
|
112
|
+
if line.include?(',')
|
|
113
|
+
line[line.index(',')] = format(BLOCKING_METADATA, suffix: ',')
|
|
114
|
+
else
|
|
115
|
+
line[line.rindex(' ')] = format(BLOCKING_METADATA, suffix: ' ')
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
line
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GitlabQuality
|
|
4
|
+
module TestTooling
|
|
5
|
+
module TestMeta
|
|
6
|
+
module Processor
|
|
7
|
+
class AddToQuarantineProcessor < MetaProcessor
|
|
8
|
+
QUARANTINE_METADATA = <<~META
|
|
9
|
+
,
|
|
10
|
+
%{indentation}quarantine: {
|
|
11
|
+
%{indentation} issue: '%{issue_url}',
|
|
12
|
+
%{indentation} type: %{quarantine_type}
|
|
13
|
+
%{indentation}}%{suffix}
|
|
14
|
+
META
|
|
15
|
+
|
|
16
|
+
class << self
|
|
17
|
+
# Execute the processor
|
|
18
|
+
#
|
|
19
|
+
# @param [Hash<String,String>] spec the spec to update
|
|
20
|
+
# @option spec [String] :file_path the path to the spec file
|
|
21
|
+
# @option spec [String] :stage the stage of the test
|
|
22
|
+
# @option spec [String] :failure_issue the issue url of the failure
|
|
23
|
+
# @option spec [String] :name the name of the example
|
|
24
|
+
# @param [TestMetaUpdater] context instance of TestMetaUpdater
|
|
25
|
+
def execute(spec, context) # rubocop:disable Metrics/AbcSize
|
|
26
|
+
@context = context
|
|
27
|
+
|
|
28
|
+
@file_path = spec["file_path"]
|
|
29
|
+
devops_stage = spec["stage"]
|
|
30
|
+
@failure_issue_url = spec["failure_issue"]
|
|
31
|
+
@example_name = spec["name"]
|
|
32
|
+
@issue_id = failure_issue_url.split('/').last # split url segment, last segment of path is the issue id
|
|
33
|
+
@mr_title = format("%{prefix} %{example_name}", prefix: '[QUARANTINE]', example_name: example_name)
|
|
34
|
+
@failure_issue = context.fetch_issue(iid: issue_id)
|
|
35
|
+
|
|
36
|
+
return unless proceed_with_merge_request?
|
|
37
|
+
|
|
38
|
+
@file_contents = context.get_file_contents(file_path)
|
|
39
|
+
|
|
40
|
+
new_content, changed_line_no = add_quarantine_metadata
|
|
41
|
+
|
|
42
|
+
branch = context.create_branch("#{issue_id}-quarantine-#{SecureRandom.hex(4)}", example_name, context.ref)
|
|
43
|
+
|
|
44
|
+
context.commit_changes(branch, <<~COMMIT_MESSAGE, file_path, new_content)
|
|
45
|
+
Quarantine end-to-end test
|
|
46
|
+
|
|
47
|
+
Quarantine #{example_name}
|
|
48
|
+
COMMIT_MESSAGE
|
|
49
|
+
|
|
50
|
+
context.create_merge_request(mr_title, branch) do
|
|
51
|
+
<<~MARKDOWN
|
|
52
|
+
## What does this MR do?
|
|
53
|
+
|
|
54
|
+
Quarantines the test [`#{example_name}`](https://gitlab.com/#{context.project}/-/blob/#{context.ref}/#{file_path}#L#{changed_line_no + 1})
|
|
55
|
+
|
|
56
|
+
This test was identified in the reliable e2e test report: #{context.report_issue}
|
|
57
|
+
|
|
58
|
+
### E2E Test Failure issue(s)
|
|
59
|
+
|
|
60
|
+
#{failure_issue_url}
|
|
61
|
+
|
|
62
|
+
### Check-list
|
|
63
|
+
|
|
64
|
+
- [ ] General code guidelines check-list
|
|
65
|
+
- [ ] [Code review guidelines](https://docs.gitlab.com/ee/development/code_review.html)
|
|
66
|
+
- [ ] [Style guides](https://docs.gitlab.com/ee/development/contributing/style_guides.html)
|
|
67
|
+
- [ ] Quarantine test check-list
|
|
68
|
+
- [ ] Follow the [Quarantining Tests guide](https://about.gitlab.com/handbook/engineering/infrastructure/test-platform/debugging-qa-test-failures/#quarantining-tests).
|
|
69
|
+
- [ ] Confirm the test has a [`quarantine:` tag with the specified quarantine type](https://about.gitlab.com/handbook/engineering/infrastructure/test-platform/debugging-qa-test-failures/#quarantined-test-types).
|
|
70
|
+
- [ ] Note if the test should be [quarantined for a specific environment](https://docs.gitlab.com/ee/development/testing_guide/end_to_end/execution_context_selection.html#quarantine-a-test-for-a-specific-environment).
|
|
71
|
+
- [ ] (Optionally) In case of an emergency (e.g. blocked deployments), consider adding labels to pick into auto-deploy (~"Pick into auto-deploy" ~"priority::1" ~"severity::1").
|
|
72
|
+
- [ ] To ensure a faster turnaround, ask in the `#quality_maintainers` Slack channel for someone to review and merge the merge request, rather than assigning it directly.
|
|
73
|
+
|
|
74
|
+
<!-- Base labels. -->
|
|
75
|
+
/label ~"Quality" ~"QA" ~"type::maintenance" ~"maintenance::pipelines"
|
|
76
|
+
|
|
77
|
+
<!--
|
|
78
|
+
Choose the stage that appears in the test path, e.g. ~"devops::create" for
|
|
79
|
+
`qa/specs/features/browser_ui/3_create/web_ide/add_file_template_spec.rb`.
|
|
80
|
+
-->
|
|
81
|
+
/label ~"devops::#{devops_stage}"
|
|
82
|
+
|
|
83
|
+
<div align="center">
|
|
84
|
+
(This MR was automatically generated by [`gitlab_quality-test_tooling`](https://gitlab.com/gitlab-org/ruby/gems/gitlab_quality-test_tooling) at #{Time.now.utc})
|
|
85
|
+
</div>
|
|
86
|
+
MARKDOWN
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Performs post processing. Takes a list of MRs and posts them in a note on report_issue and Slack
|
|
91
|
+
#
|
|
92
|
+
# @param [Gitlab::ObjectifiedHash] merge_requests
|
|
93
|
+
def post_process(merge_requests)
|
|
94
|
+
web_urls = merge_requests.compact.map { |mr| "- #{mr.web_url}\n" }.join
|
|
95
|
+
|
|
96
|
+
return if web_urls.empty?
|
|
97
|
+
|
|
98
|
+
context.post_note_on_report_issue(<<~ISSUE_NOTE)
|
|
99
|
+
|
|
100
|
+
The following merge requests have been created to quarantine the unstable tests:
|
|
101
|
+
|
|
102
|
+
#{web_urls}
|
|
103
|
+
ISSUE_NOTE
|
|
104
|
+
|
|
105
|
+
context.post_message_on_slack(<<~MSG)
|
|
106
|
+
*Action Required!* The following merge requests have been created to quarantine the unstable tests identified
|
|
107
|
+
in the reliable test report: #{context.report_issue}
|
|
108
|
+
|
|
109
|
+
#{web_urls}
|
|
110
|
+
|
|
111
|
+
Maintainers are requested to review and merge. Thank you.
|
|
112
|
+
MSG
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
private
|
|
116
|
+
|
|
117
|
+
attr_reader :context, :file_path, :file_contents, :failure_issue_url, :example_name, :issue_id, :mr_title, :failure_issue
|
|
118
|
+
|
|
119
|
+
# Checks if the failure issue is closed or if there is already an MR open
|
|
120
|
+
#
|
|
121
|
+
# @return [Boolean]
|
|
122
|
+
def proceed_with_merge_request?
|
|
123
|
+
if context.issue_is_closed?(failure_issue)
|
|
124
|
+
puts " Failure issue '#{failure_issue_url}' is closed. Will not proceed with creating MR."
|
|
125
|
+
return false
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
open_mrs = context.existing_merge_requests(title: mr_title)
|
|
129
|
+
if open_mrs.any?
|
|
130
|
+
puts " An open MR already exists for '#{example_name}': #{open_mrs.first['web_url']}. Will not proceed with creating MR."
|
|
131
|
+
return false
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
true
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Add quarantine metadata to the file content and replace it
|
|
138
|
+
#
|
|
139
|
+
# @return [Array<String, Integer>] first value holds the new content, the second value holds the line number of the test
|
|
140
|
+
def add_quarantine_metadata # rubocop:disable Metrics/AbcSize
|
|
141
|
+
matched_lines = context.find_example_match_lines(file_contents, example_name)
|
|
142
|
+
|
|
143
|
+
context.update_matched_line(matched_lines.last, file_contents.dup) do |line|
|
|
144
|
+
indentation = context.indentation(line)
|
|
145
|
+
|
|
146
|
+
if line.include?(',') && line.split.last != 'do'
|
|
147
|
+
line[line.index(',')] = format(QUARANTINE_METADATA.rstrip, issue_url: failure_issue_url, indentation: indentation, suffix: ',', quarantine_type: quarantine_type)
|
|
148
|
+
else
|
|
149
|
+
line[line.rindex(' ')] = format(QUARANTINE_METADATA.rstrip, issue_url: failure_issue_url, indentation: indentation, suffix: ' ', quarantine_type: quarantine_type)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
line
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Returns the quarantine type based on the failure scoped label
|
|
157
|
+
#
|
|
158
|
+
# @return [String]
|
|
159
|
+
def quarantine_type
|
|
160
|
+
case context.issue_scoped_label(failure_issue, 'failure')&.split('::')&.last
|
|
161
|
+
when 'new', 'investigating'
|
|
162
|
+
':investigating'
|
|
163
|
+
when 'external-dependency'
|
|
164
|
+
':external_dependency'
|
|
165
|
+
when 'broken-test'
|
|
166
|
+
':broken'
|
|
167
|
+
when 'bug'
|
|
168
|
+
':bug'
|
|
169
|
+
when 'flaky-test'
|
|
170
|
+
':flaky'
|
|
171
|
+
when 'stale-test'
|
|
172
|
+
':stale'
|
|
173
|
+
when 'test-environment'
|
|
174
|
+
':test_environment'
|
|
175
|
+
else
|
|
176
|
+
':investigating'
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GitlabQuality
|
|
4
|
+
module TestTooling
|
|
5
|
+
module TestMeta
|
|
6
|
+
module Processor
|
|
7
|
+
class MetaProcessor
|
|
8
|
+
class << self
|
|
9
|
+
def execute
|
|
10
|
+
raise 'method not implemented'
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def post_process
|
|
14
|
+
raise 'method not implemented'
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private_class_method :new
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
|
|
5
|
+
module GitlabQuality
|
|
6
|
+
module TestTooling
|
|
7
|
+
module TestMeta
|
|
8
|
+
class TestMetaUpdater
|
|
9
|
+
include TestTooling::Concerns::FindSetDri
|
|
10
|
+
|
|
11
|
+
attr_reader :project, :ref, :report_issue
|
|
12
|
+
|
|
13
|
+
TEST_PLATFORM_MAINTAINERS_SLACK_CHANNEL_ID = 'C0437FV9KBN' # test-platform-maintainers
|
|
14
|
+
|
|
15
|
+
def initialize(token:, project:, specs_file:, processor:, ref: 'master', dry_run: false)
|
|
16
|
+
@specs_file = specs_file
|
|
17
|
+
@token = token
|
|
18
|
+
@project = project
|
|
19
|
+
@ref = ref
|
|
20
|
+
@dry_run = dry_run
|
|
21
|
+
@processor = processor
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def invoke!
|
|
25
|
+
JSON.parse(File.read(specs_file)).tap do |contents|
|
|
26
|
+
@report_issue = contents['report_issue']
|
|
27
|
+
|
|
28
|
+
results = []
|
|
29
|
+
contents['specs'].each do |spec|
|
|
30
|
+
results << processor.execute(spec, self)
|
|
31
|
+
end
|
|
32
|
+
processor.post_process(results)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Fetch contents of file from the repository
|
|
37
|
+
#
|
|
38
|
+
# [String] file_path path to the file
|
|
39
|
+
# [String] contents of the file
|
|
40
|
+
def get_file_contents(file_path)
|
|
41
|
+
repository_files = GitlabClient::RepositoryFilesClient.new(token: token, project: project, file_path: file_path)
|
|
42
|
+
repository_files.file_contents
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Find all lines that contain any part of the example name
|
|
46
|
+
#
|
|
47
|
+
# @param [String] content the content of the spec file
|
|
48
|
+
# @param [String] example_name the name of example to find
|
|
49
|
+
# @return [Array<String, Integer>] first value holds the matched line, the second value holds the line number of matched line
|
|
50
|
+
def find_example_match_lines(content, example_name)
|
|
51
|
+
lines = content.split("\n")
|
|
52
|
+
|
|
53
|
+
matched_lines = []
|
|
54
|
+
|
|
55
|
+
lines.each_with_index do |line, line_index|
|
|
56
|
+
string_within_quotes = spec_desc_string_within_quotes(line)
|
|
57
|
+
|
|
58
|
+
matched_lines << [line, line_index] if string_within_quotes && example_name.include?(string_within_quotes)
|
|
59
|
+
rescue StandardError => e
|
|
60
|
+
puts "Error: #{e}"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
matched_lines
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Update the provided matched_line with content from the block if given
|
|
67
|
+
#
|
|
68
|
+
# @param [Array<String, Integer>] matched_line first value holds the line content, the second value holds the line number
|
|
69
|
+
# @param [String] content full orignal content of the spec file
|
|
70
|
+
# @return [Array<String, Integer>] first value holds the new content, the second value holds the line number of the test
|
|
71
|
+
def update_matched_line(matched_line, content)
|
|
72
|
+
lines = content.split("\n")
|
|
73
|
+
|
|
74
|
+
begin
|
|
75
|
+
resulting_line = block_given? ? yield(matched_line[0]) : matched_line[0]
|
|
76
|
+
lines[matched_line[1]] = resulting_line
|
|
77
|
+
rescue StandardError => e
|
|
78
|
+
puts "Error: #{e}"
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
[lines.join("\n") << "\n", matched_line[1]]
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Create a branch from the ref
|
|
85
|
+
#
|
|
86
|
+
# @param [String] name_prefix the prefix to attach to the branch name
|
|
87
|
+
# @param [String] example_name the example
|
|
88
|
+
# @return [Gitlab::ObjectifiedHash] the new branch
|
|
89
|
+
def create_branch(name_prefix, example_name, ref)
|
|
90
|
+
branch_name = [name_prefix, example_name.gsub(/\W/, '-')]
|
|
91
|
+
@branches_client ||= (dry_run ? GitlabClient::BranchesDryClient : GitlabClient::BranchesClient).new(token: token, project: project)
|
|
92
|
+
@branches_client.create(branch_name.join('-'), ref)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Commit changes to a branch
|
|
96
|
+
#
|
|
97
|
+
# @param [Gitlab::ObjectifiedHash] branch the branch to commit to
|
|
98
|
+
# @param [String] message the message to commit
|
|
99
|
+
# @param [String] new_content the new content to commit
|
|
100
|
+
# @return [Gitlab::ObjectifiedHash] the commit
|
|
101
|
+
def commit_changes(branch, message, file_path, new_content)
|
|
102
|
+
@commits_client ||= (dry_run ? GitlabClient::CommitsDryClient : GitlabClient::CommitsClient)
|
|
103
|
+
.new(token: token, project: project)
|
|
104
|
+
@commits_client.create(branch['name'], file_path, new_content, message)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Create a Merge Request with a given branch
|
|
108
|
+
#
|
|
109
|
+
# @param [String] title_prefix the prefix of the title
|
|
110
|
+
# @param [String] example_name the example
|
|
111
|
+
# @param [Gitlab::ObjectifiedHash] branch the branch
|
|
112
|
+
# @param [Integer] assignee_id
|
|
113
|
+
# @return [Gitlab::ObjectifiedHash] the created merge request
|
|
114
|
+
def create_merge_request(title, branch, assignee_id = nil, labels = '')
|
|
115
|
+
description = yield
|
|
116
|
+
|
|
117
|
+
merge_request_client.create_merge_request(
|
|
118
|
+
title: title,
|
|
119
|
+
source_branch: branch['name'],
|
|
120
|
+
target_branch: ref,
|
|
121
|
+
description: description,
|
|
122
|
+
labels: labels,
|
|
123
|
+
assignee_id: assignee_id)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Check if issue is closed
|
|
127
|
+
#
|
|
128
|
+
# @param [Gitlab::ObjectifiedHash] issue the issue
|
|
129
|
+
# @return [Boolean] True or False
|
|
130
|
+
def issue_is_closed?(issue)
|
|
131
|
+
issue['state'] == 'closed'
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Get scoped label from issue
|
|
135
|
+
#
|
|
136
|
+
# @param [Gitlab::ObjectifiedHash] issue the issue
|
|
137
|
+
# @param [String] scope
|
|
138
|
+
# @return [String] scoped label
|
|
139
|
+
def issue_scoped_label(issue, scope)
|
|
140
|
+
issue['labels'].detect { |label| label.match(/#{scope}::/) }
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Fetch an issue
|
|
144
|
+
#
|
|
145
|
+
# @param [String] iid: The iid of the issue
|
|
146
|
+
# @return [Gitlab::ObjectifiedHash]
|
|
147
|
+
def fetch_issue(iid:)
|
|
148
|
+
issue_client.find_issues(iid: iid).first
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Post note on report_issue
|
|
152
|
+
#
|
|
153
|
+
# @param [String] note the note to post
|
|
154
|
+
# @return [Gitlab::ObjectifiedHash]
|
|
155
|
+
def post_note_on_report_issue(note)
|
|
156
|
+
iid = report_issue.split('/').last # split url segment, last segment of path is the issue id
|
|
157
|
+
issue_client.create_issue_note(iid: iid, note: note)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Post a note of merge reqest
|
|
161
|
+
#
|
|
162
|
+
# @param [String] note
|
|
163
|
+
# @param [Integer] merge_request_iid
|
|
164
|
+
# @return [Gitlab::ObjectifiedHash]
|
|
165
|
+
def post_note_on_merge_request(note, merge_request_iid)
|
|
166
|
+
merge_request_client.create_note(note: note, merge_request_iid: merge_request_iid)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Fetch the id for the dri of the product group and stage
|
|
170
|
+
# The first item returned is the id of the assignee and the second item is the handle
|
|
171
|
+
#
|
|
172
|
+
# @param [String] product_group
|
|
173
|
+
# @param [String] devops_stage
|
|
174
|
+
# @return [Array<Integer, String>]
|
|
175
|
+
def fetch_dri_id(product_group, devops_stage)
|
|
176
|
+
assignee_handle = ENV.fetch('QA_TEST_DRI_HANDLE', nil) || set_dri_via_group(product_group, devops_stage)
|
|
177
|
+
|
|
178
|
+
[issue_client.find_user_id(username: assignee_handle), assignee_handle]
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Post a message on Slack
|
|
182
|
+
#
|
|
183
|
+
# @param [String] message the message to post
|
|
184
|
+
# @return [HTTP::Response]
|
|
185
|
+
def post_message_on_slack(message)
|
|
186
|
+
channel = ENV.fetch('SLACK_QA_CHANNEL', nil) || TEST_PLATFORM_MAINTAINERS_SLACK_CHANNEL_ID
|
|
187
|
+
slack_options = {
|
|
188
|
+
slack_webhook_url: ENV.fetch('CI_SLACK_WEBHOOK_URL', nil),
|
|
189
|
+
channel: channel,
|
|
190
|
+
username: "GitLab Quality Test Tooling",
|
|
191
|
+
icon_emoji: ':warning:',
|
|
192
|
+
message: message
|
|
193
|
+
}
|
|
194
|
+
puts "Posting Slack message to channel: #{channel}"
|
|
195
|
+
|
|
196
|
+
(dry_run ? GitlabQuality::TestTooling::Slack::PostToSlackDry : GitlabQuality::TestTooling::Slack::PostToSlack).new(**slack_options).invoke!
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Provide indentaiton based on the given line
|
|
200
|
+
#
|
|
201
|
+
# @param[String] line the line to use for indentation
|
|
202
|
+
# @return[String] indentation
|
|
203
|
+
def indentation(line)
|
|
204
|
+
# Indent the same number of spaces as the current line
|
|
205
|
+
no_of_spaces = line[/\A */].size
|
|
206
|
+
# If the first char on current line is not a quote, add two more spaces
|
|
207
|
+
no_of_spaces += /['"]/.match?(line.lstrip[0]) ? 0 : 2
|
|
208
|
+
|
|
209
|
+
" " * no_of_spaces
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# Returns and existing merge request with the given title
|
|
213
|
+
#
|
|
214
|
+
# @param [String] title: Title of the merge request
|
|
215
|
+
# @return [Array<Gitlab::ObjectifiedHash>] Merge requests
|
|
216
|
+
def existing_merge_requests(title:)
|
|
217
|
+
merge_request_client.find(options: { search: title, in: 'title', state: 'opened' })
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
private
|
|
221
|
+
|
|
222
|
+
attr_reader :token, :specs_file, :dry_run, :processor
|
|
223
|
+
|
|
224
|
+
# Returns any test description string within single or double quotes
|
|
225
|
+
#
|
|
226
|
+
# @param [String] line the line to check for any quoted string
|
|
227
|
+
# @return [String] the match or nil if no match
|
|
228
|
+
def spec_desc_string_within_quotes(line)
|
|
229
|
+
match = line.match(/(?:it|describe|context|\s)+ ['"]([^'"]*)['"]/)
|
|
230
|
+
match ? match[1] : nil
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# Returns the GitlabIssueClient or GitlabIssueDryClient based on the value of dry_run
|
|
234
|
+
#
|
|
235
|
+
# @return [GitlabIssueDryClient | GitlabIssueClient]
|
|
236
|
+
def issue_client
|
|
237
|
+
@issue_client ||= (dry_run ? GitlabClient::IssuesDryClient : GitlabClient::IssuesClient).new(token: token, project: "gitlab-org/gitlab")
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
# Returns the MergeRequestDryClient or MergeRequest based on the value of dry_run
|
|
241
|
+
#
|
|
242
|
+
# @return [MergeRequestDryClient | MergeRequest]
|
|
243
|
+
def merge_request_client
|
|
244
|
+
@merge_request_client ||= (dry_run ? GitlabClient::MergeRequestsDryClient : GitlabClient::MergeRequestsClient).new(
|
|
245
|
+
token: token,
|
|
246
|
+
project: project
|
|
247
|
+
)
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
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.
|
|
4
|
+
version: 1.14.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-01-
|
|
11
|
+
date: 2024-01-24 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: climate_control
|
|
@@ -342,12 +342,27 @@ dependencies:
|
|
|
342
342
|
- - "<"
|
|
343
343
|
- !ruby/object:Gem::Version
|
|
344
344
|
version: '3'
|
|
345
|
+
- !ruby/object:Gem::Dependency
|
|
346
|
+
name: rspec-parameterized
|
|
347
|
+
requirement: !ruby/object:Gem::Requirement
|
|
348
|
+
requirements:
|
|
349
|
+
- - "~>"
|
|
350
|
+
- !ruby/object:Gem::Version
|
|
351
|
+
version: 1.0.0
|
|
352
|
+
type: :runtime
|
|
353
|
+
prerelease: false
|
|
354
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
355
|
+
requirements:
|
|
356
|
+
- - "~>"
|
|
357
|
+
- !ruby/object:Gem::Version
|
|
358
|
+
version: 1.0.0
|
|
345
359
|
description: A collection of test-related tools.
|
|
346
360
|
email:
|
|
347
361
|
- quality@gitlab.com
|
|
348
362
|
executables:
|
|
349
363
|
- flaky-test-issues
|
|
350
364
|
- generate-test-session
|
|
365
|
+
- knapsack-report-issues
|
|
351
366
|
- post-to-slack
|
|
352
367
|
- prepare-stage-reports
|
|
353
368
|
- relate-failure-issue
|
|
@@ -355,6 +370,7 @@ executables:
|
|
|
355
370
|
- slow-test-issues
|
|
356
371
|
- slow-test-merge-request-report-note
|
|
357
372
|
- update-screenshot-paths
|
|
373
|
+
- update-test-meta
|
|
358
374
|
extensions: []
|
|
359
375
|
extra_rdoc_files: []
|
|
360
376
|
files:
|
|
@@ -374,6 +390,7 @@ files:
|
|
|
374
390
|
- Rakefile
|
|
375
391
|
- exe/flaky-test-issues
|
|
376
392
|
- exe/generate-test-session
|
|
393
|
+
- exe/knapsack-report-issues
|
|
377
394
|
- exe/post-to-slack
|
|
378
395
|
- exe/prepare-stage-reports
|
|
379
396
|
- exe/relate-failure-issue
|
|
@@ -381,6 +398,7 @@ files:
|
|
|
381
398
|
- exe/slow-test-issues
|
|
382
399
|
- exe/slow-test-merge-request-report-note
|
|
383
400
|
- exe/update-screenshot-paths
|
|
401
|
+
- exe/update-test-meta
|
|
384
402
|
- lefthook.yml
|
|
385
403
|
- lib/gitlab_quality/test_tooling.rb
|
|
386
404
|
- lib/gitlab_quality/test_tooling/concerns/find_set_dri.rb
|
|
@@ -396,6 +414,8 @@ files:
|
|
|
396
414
|
- lib/gitlab_quality/test_tooling/gitlab_client/merge_requests_client.rb
|
|
397
415
|
- lib/gitlab_quality/test_tooling/gitlab_client/merge_requests_dry_client.rb
|
|
398
416
|
- lib/gitlab_quality/test_tooling/gitlab_client/repository_files_client.rb
|
|
417
|
+
- lib/gitlab_quality/test_tooling/knapsack_reports/spec_run_time.rb
|
|
418
|
+
- lib/gitlab_quality/test_tooling/knapsack_reports/spec_run_time_report.rb
|
|
399
419
|
- lib/gitlab_quality/test_tooling/labels_inference.rb
|
|
400
420
|
- lib/gitlab_quality/test_tooling/report/concerns/group_and_category_labels.rb
|
|
401
421
|
- lib/gitlab_quality/test_tooling/report/concerns/issue_reports.rb
|
|
@@ -404,6 +424,7 @@ files:
|
|
|
404
424
|
- lib/gitlab_quality/test_tooling/report/flaky_test_issue.rb
|
|
405
425
|
- lib/gitlab_quality/test_tooling/report/generate_test_session.rb
|
|
406
426
|
- lib/gitlab_quality/test_tooling/report/issue_logger.rb
|
|
427
|
+
- lib/gitlab_quality/test_tooling/report/knapsack_report_issue.rb
|
|
407
428
|
- lib/gitlab_quality/test_tooling/report/merge_request_slow_tests_report.rb
|
|
408
429
|
- lib/gitlab_quality/test_tooling/report/prepare_stage_reports.rb
|
|
409
430
|
- lib/gitlab_quality/test_tooling/report/relate_failure_issue.rb
|
|
@@ -431,6 +452,10 @@ files:
|
|
|
431
452
|
- lib/gitlab_quality/test_tooling/system_logs/log_types/rails/graphql_log.rb
|
|
432
453
|
- lib/gitlab_quality/test_tooling/system_logs/shared_fields.rb
|
|
433
454
|
- lib/gitlab_quality/test_tooling/system_logs/system_logs_formatter.rb
|
|
455
|
+
- lib/gitlab_quality/test_tooling/test_meta/processor/add_to_blocking_processor.rb
|
|
456
|
+
- lib/gitlab_quality/test_tooling/test_meta/processor/add_to_quarantine_processor.rb
|
|
457
|
+
- lib/gitlab_quality/test_tooling/test_meta/processor/meta_processor.rb
|
|
458
|
+
- lib/gitlab_quality/test_tooling/test_meta/test_meta_updater.rb
|
|
434
459
|
- lib/gitlab_quality/test_tooling/test_metric/json_test_metric.rb
|
|
435
460
|
- lib/gitlab_quality/test_tooling/test_metrics/json_test_metric_collection.rb
|
|
436
461
|
- lib/gitlab_quality/test_tooling/test_result/base_test_result.rb
|