gitlab_quality-test_tooling 1.12.0 → 1.14.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|