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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b88e790bd74462ff96779cb28d8f8f2cdadd99356e3889f50ec56ed7f5ea691d
4
- data.tar.gz: 865098baec02cb4f7cf6634fc7e7cb45a0394261957d9b0cdc33b9c0efd74c29
3
+ metadata.gz: 970354551e0118a942865386f3e844762cf1fdfb06cfc02aa7bca602e55abc8f
4
+ data.tar.gz: f512f45f80e5a3949b07a237c1f29c6bc73fcd8e320a92b6b3fcd0a23ccc311f
5
5
  SHA512:
6
- metadata.gz: 1bdaa39d9a6e2ea9000d3318d54e1c3385c552fa967b86780f697ee10becf7e6f0cc3c8edf19a7ac7d29988babaf6ad735a1191d7132ed7f635939cde3575fa6
7
- data.tar.gz: 3edfd0209db270145f48338dbf0ebc1785176f8fd158fef8880b13b3280e8a569437d30f26234203bb58bbb6b111992265209882df4c783b83342b942d2d7d00
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.12.0)
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
@@ -64,8 +64,6 @@ module GitlabQuality
64
64
  puts "=> Reporting #{test_results.count} tests in #{test_results.path}"
65
65
  process_test_results(test_results)
66
66
  end
67
-
68
- write_issues_log_file
69
67
  end
70
68
 
71
69
  def test_metric_collections
@@ -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
- issue_title.include?(test.name) || test_file_path_found
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
@@ -33,8 +33,6 @@ module GitlabQuality
33
33
  create_slow_issue(test) if test.slow_test?
34
34
  end
35
35
  end
36
-
37
- write_issues_log_file
38
36
  end
39
37
 
40
38
  def new_issue_title(test)
@@ -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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module GitlabQuality
4
4
  module TestTooling
5
- VERSION = "1.12.0"
5
+ VERSION = "1.14.0"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gitlab_quality-test_tooling
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.12.0
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 00:00:00.000000000 Z
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