gitlab_quality-test_tooling 1.13.0 → 1.14.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0b5576ca28a8e8cbf76564017a53418fd059747789bbbb8146835285d8f5b37c
4
- data.tar.gz: b1d7c6dd581dccabe91f64687b420a83f5561aa407e1e4583e7a5146183890f4
3
+ metadata.gz: 970354551e0118a942865386f3e844762cf1fdfb06cfc02aa7bca602e55abc8f
4
+ data.tar.gz: f512f45f80e5a3949b07a237c1f29c6bc73fcd8e320a92b6b3fcd0a23ccc311f
5
5
  SHA512:
6
- metadata.gz: d245b9be5a9a127630d934096277c339e649e9e719727f871d8b794245b385e6f4dbfba3175c546f44b2ee833099ef5398738dcaa631bab39b44eb9734fe338f
7
- data.tar.gz: 33a391d891853d45e75198462d504641d090e0f619e31a9fbf5c613b4fe5cef6443d6b465e9e2ffc27d643a3d8d8426030bb10c872a83ca0a2def1b0232ae0ae
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.13.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
@@ -188,6 +188,22 @@ Usage: exe/slow-test-merge-request-report-note [options]
188
188
  -h, --help Show the usage
189
189
  ```
190
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
+
191
207
  ## Development
192
208
 
193
209
  ### Initial setup
@@ -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,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.13.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.13.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-19 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,6 +342,20 @@ 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
@@ -356,6 +370,7 @@ executables:
356
370
  - slow-test-issues
357
371
  - slow-test-merge-request-report-note
358
372
  - update-screenshot-paths
373
+ - update-test-meta
359
374
  extensions: []
360
375
  extra_rdoc_files: []
361
376
  files:
@@ -383,6 +398,7 @@ files:
383
398
  - exe/slow-test-issues
384
399
  - exe/slow-test-merge-request-report-note
385
400
  - exe/update-screenshot-paths
401
+ - exe/update-test-meta
386
402
  - lefthook.yml
387
403
  - lib/gitlab_quality/test_tooling.rb
388
404
  - lib/gitlab_quality/test_tooling/concerns/find_set_dri.rb
@@ -436,6 +452,10 @@ files:
436
452
  - lib/gitlab_quality/test_tooling/system_logs/log_types/rails/graphql_log.rb
437
453
  - lib/gitlab_quality/test_tooling/system_logs/shared_fields.rb
438
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
439
459
  - lib/gitlab_quality/test_tooling/test_metric/json_test_metric.rb
440
460
  - lib/gitlab_quality/test_tooling/test_metrics/json_test_metric_collection.rb
441
461
  - lib/gitlab_quality/test_tooling/test_result/base_test_result.rb