gitlab_quality-test_tooling 1.11.0 → 1.17.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.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +93 -68
  3. data/README.md +35 -2
  4. data/exe/flaky-test-issues +7 -2
  5. data/exe/knapsack-report-issues +54 -0
  6. data/exe/update-test-meta +70 -0
  7. data/lefthook.yml +13 -0
  8. data/lib/gitlab_quality/test_tooling/concerns/find_set_dri.rb +47 -0
  9. data/lib/gitlab_quality/test_tooling/gitlab_client/branches_client.rb +18 -0
  10. data/lib/gitlab_quality/test_tooling/gitlab_client/branches_dry_client.rb +15 -0
  11. data/lib/gitlab_quality/test_tooling/gitlab_client/commits_client.rb +20 -0
  12. data/lib/gitlab_quality/test_tooling/gitlab_client/commits_dry_client.rb +14 -0
  13. data/lib/gitlab_quality/test_tooling/gitlab_client/gitlab_client.rb +12 -13
  14. data/lib/gitlab_quality/test_tooling/gitlab_client/issues_client.rb +6 -6
  15. data/lib/gitlab_quality/test_tooling/gitlab_client/merge_requests_client.rb +18 -10
  16. data/lib/gitlab_quality/test_tooling/gitlab_client/merge_requests_dry_client.rb +4 -2
  17. data/lib/gitlab_quality/test_tooling/gitlab_client/repository_files_client.rb +23 -0
  18. data/lib/gitlab_quality/test_tooling/knapsack_reports/spec_run_time.rb +85 -0
  19. data/lib/gitlab_quality/test_tooling/knapsack_reports/spec_run_time_report.rb +60 -0
  20. data/lib/gitlab_quality/test_tooling/report/concerns/issue_reports.rb +41 -28
  21. data/lib/gitlab_quality/test_tooling/report/concerns/utils.rb +23 -1
  22. data/lib/gitlab_quality/test_tooling/report/flaky_test_issue.rb +89 -44
  23. data/lib/gitlab_quality/test_tooling/report/generate_test_session.rb +1 -4
  24. data/lib/gitlab_quality/test_tooling/report/knapsack_report_issue.rb +139 -0
  25. data/lib/gitlab_quality/test_tooling/report/relate_failure_issue.rb +6 -12
  26. data/lib/gitlab_quality/test_tooling/report/report_as_issue.rb +16 -5
  27. data/lib/gitlab_quality/test_tooling/report/slow_test_issue.rb +71 -80
  28. data/lib/gitlab_quality/test_tooling/runtime/env.rb +5 -1
  29. data/lib/gitlab_quality/test_tooling/slack/post_to_slack_dry.rb +14 -0
  30. data/lib/gitlab_quality/test_tooling/test_meta/processor/add_to_blocking_processor.rb +143 -0
  31. data/lib/gitlab_quality/test_tooling/test_meta/processor/add_to_quarantine_processor.rb +199 -0
  32. data/lib/gitlab_quality/test_tooling/test_meta/processor/meta_processor.rb +44 -0
  33. data/lib/gitlab_quality/test_tooling/test_meta/test_meta_updater.rb +313 -0
  34. data/lib/gitlab_quality/test_tooling/version.rb +1 -1
  35. metadata +40 -9
  36. data/lib/gitlab_quality/test_tooling/report/concerns/find_set_dri.rb +0 -49
@@ -0,0 +1,199 @@
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
+ @existing_mrs = nil
28
+ @file_path = spec["file_path"]
29
+ testcase = spec["testcase"]
30
+ devops_stage = spec["stage"]
31
+ product_group = spec["product_group"]
32
+ @failure_issue_url = spec["failure_issue"]
33
+ @example_name = spec["name"]
34
+ @issue_id = failure_issue_url.split('/').last # split url segment, last segment of path is the issue id
35
+ @mr_title = format("%{prefix} %{example_name}", prefix: '[QUARANTINE]', example_name: example_name).truncate(72, omission: '')
36
+ @failure_issue = context.fetch_issue(iid: issue_id)
37
+
38
+ @file_contents = context.get_file_contents(file_path)
39
+
40
+ new_content, @changed_line_no = add_quarantine_metadata
41
+
42
+ return unless proceed_with_merge_request?
43
+
44
+ branch = context.create_branch("#{issue_id}-quarantine-#{SecureRandom.hex(4)}", example_name, context.ref)
45
+
46
+ context.commit_changes(branch, <<~COMMIT_MESSAGE, file_path, new_content)
47
+ Quarantine end-to-end test
48
+
49
+ #{"Quarantine #{example_name}".truncate(72)}
50
+ COMMIT_MESSAGE
51
+
52
+ gitlab_bot_user_id = context.user_id_for_username(Runtime::Env.gitlab_bot_username)
53
+
54
+ merge_request = context.create_merge_request(mr_title, branch, gitlab_bot_user_id) do
55
+ <<~MARKDOWN
56
+ ## What does this MR do?
57
+
58
+ Quarantines the test [`#{example_name}`](https://gitlab.com/#{context.project}/-/blob/#{context.ref}/#{file_path}#L#{changed_line_no + 1})
59
+
60
+ This test was identified in the reliable e2e test report: #{context.report_issue}
61
+
62
+ [Testcase link](#{testcase})
63
+
64
+ [Spec metrics link](#{context.single_spec_metrics_link(example_name)})
65
+
66
+ ### E2E Test Failure issue(s)
67
+
68
+ #{failure_issue_url}
69
+
70
+ ### Check-list
71
+
72
+ - [ ] General code guidelines check-list
73
+ - [ ] [Code review guidelines](https://docs.gitlab.com/ee/development/code_review.html)
74
+ - [ ] [Style guides](https://docs.gitlab.com/ee/development/contributing/style_guides.html)
75
+ - [ ] Quarantine test check-list
76
+ - [ ] Follow the [Quarantining Tests guide](https://about.gitlab.com/handbook/engineering/infrastructure/test-platform/debugging-qa-test-failures/#quarantining-tests).
77
+ - [ ] 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).
78
+ - [ ] 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).
79
+ - [ ] (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").
80
+ - [ ] 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.
81
+
82
+ <!-- Base labels. -->
83
+ /label ~"Quality" ~"QA" ~"type::maintenance" ~"maintenance::pipelines"
84
+
85
+ <!--
86
+ Choose the stage that appears in the test path, e.g. ~"devops::create" for
87
+ `qa/specs/features/browser_ui/3_create/web_ide/add_file_template_spec.rb`.
88
+ -->
89
+ /label ~"devops::#{devops_stage}"
90
+ #{context.label_from_product_group(product_group)}
91
+
92
+ <div align="center">
93
+ (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})
94
+ </div>
95
+ MARKDOWN
96
+ end
97
+
98
+ if merge_request
99
+ context.add_processed_record({ file_path => changed_line_no })
100
+ Runtime::Logger.info(" Created MR for quarantine: #{merge_request.web_url}")
101
+ end
102
+
103
+ merge_request
104
+ end
105
+
106
+ # Performs post processing. Takes a list of MRs and posts them in a note on report_issue and Slack
107
+ #
108
+ # @param [Gitlab::ObjectifiedHash] merge_requests
109
+ def post_process(merge_requests)
110
+ web_urls = merge_requests.compact.map { |mr| "- #{mr.web_url}\n" }.join
111
+
112
+ return if web_urls.empty?
113
+
114
+ context.post_note_on_report_issue(<<~ISSUE_NOTE)
115
+
116
+ The following merge requests have been created to quarantine the unstable tests:
117
+
118
+ #{web_urls}
119
+ ISSUE_NOTE
120
+
121
+ context.post_message_on_slack(<<~MSG)
122
+ *Action Required!* The following merge requests have been created to quarantine the unstable tests identified
123
+ in the reliable test report: #{context.report_issue}
124
+
125
+ #{web_urls}
126
+
127
+ Maintainers are requested to review and merge. Thank you.
128
+ MSG
129
+ end
130
+
131
+ private
132
+
133
+ attr_reader :context, :file_path, :file_contents, :failure_issue_url, :example_name,
134
+ :issue_id, :mr_title, :failure_issue, :changed_line_no
135
+
136
+ # Checks if the failure issue is closed or if there is already an MR open
137
+ #
138
+ # @return [Boolean]
139
+ def proceed_with_merge_request? # rubocop:disable Metrics/AbcSize
140
+ if context.issue_is_closed?(failure_issue)
141
+ Runtime::Logger.info(" Failure issue '#{failure_issue_url}' is closed. Will not proceed with creating MR.")
142
+ return false
143
+ elsif context.record_processed?(file_path, changed_line_no)
144
+ Runtime::Logger.info(" Record already processed for #{file_path}:#{changed_line_no}. Will not proceed with creating MR.")
145
+ return false
146
+ elsif existing_mrs&.any?
147
+ Runtime::Logger.info(" An open MR already exists for '#{example_name}': #{existing_mrs.first['web_url']}. Will not proceed with creating MR.")
148
+ return false
149
+ end
150
+
151
+ true
152
+ end
153
+
154
+ # Add quarantine metadata to the file content and replace it
155
+ #
156
+ # @return [Array<String, Integer>] first value holds the new content, the second value holds the line number of the test
157
+ def add_quarantine_metadata # rubocop:disable Metrics/AbcSize
158
+ matched_lines = context.find_example_match_lines(file_contents, example_name)
159
+
160
+ context.update_matched_line(matched_lines.last, file_contents.dup) do |line|
161
+ indentation = context.indentation(line)
162
+
163
+ if line.sub(DESCRIPTION_REGEX, '').include?(',') && line.split.last != 'do'
164
+ line[line.rindex(',')] = format(QUARANTINE_METADATA.rstrip, issue_url: failure_issue_url, indentation: indentation, suffix: ',', quarantine_type: quarantine_type)
165
+ else
166
+ line[line.rindex(' ')] = format(QUARANTINE_METADATA.rstrip, issue_url: failure_issue_url, indentation: indentation, suffix: ' ', quarantine_type: quarantine_type)
167
+ end
168
+
169
+ line
170
+ end
171
+ end
172
+
173
+ # Returns the quarantine type based on the failure scoped label
174
+ #
175
+ # @return [String]
176
+ def quarantine_type
177
+ case context.issue_scoped_label(failure_issue, 'failure')&.split('::')&.last
178
+ when 'new', 'investigating'
179
+ ':investigating'
180
+ when 'broken-test'
181
+ ':broken'
182
+ when 'bug'
183
+ ':bug'
184
+ when 'flaky-test'
185
+ ':flaky'
186
+ when 'stale-test'
187
+ ':stale'
188
+ when 'test-environment'
189
+ ':test_environment'
190
+ else
191
+ ':investigating'
192
+ end
193
+ end
194
+ end
195
+ end
196
+ end
197
+ end
198
+ end
199
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/core_ext/string/filters'
4
+
5
+ module GitlabQuality
6
+ module TestTooling
7
+ module TestMeta
8
+ module Processor
9
+ class MetaProcessor
10
+ class << self
11
+ DESCRIPTION_REGEX = /('.*?')|(".*?")/
12
+
13
+ def execute
14
+ raise 'method not implemented'
15
+ end
16
+
17
+ def post_process
18
+ raise 'method not implemented'
19
+ end
20
+
21
+ private_class_method :new
22
+
23
+ # Fetch existing MRs for given mr title
24
+ #
25
+ # @return [Array<Gitlab::ObjectifiedHash>]
26
+ def existing_mrs
27
+ @existing_mrs ||= context.existing_merge_requests(title: mr_title)
28
+ end
29
+
30
+ # Returns the index of the end of test description
31
+ #
32
+ # @param [String] line The line containing the test description
33
+ # @return [Integer]
34
+ def end_of_description_index(line)
35
+ description_length = line.match(DESCRIPTION_REGEX)[0].length
36
+ description_start_index = line.index(DESCRIPTION_REGEX)
37
+ description_start_index + description_length
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,313 @@
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, :processed_records
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
+ @processed_records = {}
23
+ end
24
+
25
+ def invoke!
26
+ JSON.parse(File.read(specs_file)).tap do |contents|
27
+ @report_issue = contents['report_issue']
28
+
29
+ results = []
30
+ contents['specs'].each do |spec|
31
+ results << processor.execute(spec, self)
32
+ end
33
+ processor.post_process(results)
34
+ end
35
+ end
36
+
37
+ # Add processed records
38
+ #
39
+ # @param [Hash<String,Integer>] record the processed record
40
+ # @option record [String] :file_path the path to the spec file
41
+ # @option spec [Intenger] :changed_line_no the line number change in file_path
42
+ # @return [Hash<String,Integer>] processed_records
43
+ def add_processed_record(record)
44
+ @processed_records.merge!(record)
45
+ end
46
+
47
+ # Fetch contents of file from the repository
48
+ #
49
+ # @param [String] file_path path to the file
50
+ # @return [String] contents of the file
51
+ def get_file_contents(file_path)
52
+ repository_files = GitlabClient::RepositoryFilesClient.new(token: token, project: project, file_path: file_path)
53
+ repository_files.file_contents
54
+ end
55
+
56
+ # Find all lines that contain any part of the example name
57
+ #
58
+ # @param [String] content the content of the spec file
59
+ # @param [String] example_name the name of example to find
60
+ # @return [Array<String, Integer>] first value holds the matched line, the second value holds the line number of matched line
61
+ def find_example_match_lines(content, example_name)
62
+ lines = content.split("\n")
63
+
64
+ matched_lines = []
65
+
66
+ lines.each_with_index do |line, line_index|
67
+ string_within_quotes = spec_desc_string_within_quotes(line)
68
+
69
+ matched_lines << [line, line_index] if string_within_quotes && example_name.include?(string_within_quotes)
70
+ rescue StandardError => e
71
+ puts "Error: #{e}"
72
+ end
73
+
74
+ matched_lines
75
+ end
76
+
77
+ # Update the provided matched_line with content from the block if given
78
+ #
79
+ # @param [Array<String, Integer>] matched_line first value holds the line content, the second value holds the line number
80
+ # @param [String] content full orignal content of the spec file
81
+ # @return [Array<String, Integer>] first value holds the new content, the second value holds the line number of the test
82
+ def update_matched_line(matched_line, content)
83
+ lines = content.split("\n")
84
+
85
+ begin
86
+ resulting_line = block_given? ? yield(matched_line[0]) : matched_line[0]
87
+ lines[matched_line[1]] = resulting_line
88
+ rescue StandardError => e
89
+ puts "Error: #{e}"
90
+ end
91
+
92
+ [lines.join("\n") << "\n", matched_line[1]]
93
+ end
94
+
95
+ # Create a branch from the ref
96
+ #
97
+ # @param [String] name_prefix the prefix to attach to the branch name
98
+ # @param [String] example_name the example
99
+ # @return [Gitlab::ObjectifiedHash] the new branch
100
+ def create_branch(name_prefix, example_name, ref)
101
+ branch_name = [name_prefix, example_name.gsub(/\W/, '-')]
102
+ @branches_client ||= (dry_run ? GitlabClient::BranchesDryClient : GitlabClient::BranchesClient).new(token: token, project: project)
103
+ @branches_client.create(branch_name.join('-'), ref)
104
+ end
105
+
106
+ # Commit changes to a branch
107
+ #
108
+ # @param [Gitlab::ObjectifiedHash] branch the branch to commit to
109
+ # @param [String] message the message to commit
110
+ # @param [String] new_content the new content to commit
111
+ # @return [Gitlab::ObjectifiedHash] the commit
112
+ def commit_changes(branch, message, file_path, new_content)
113
+ @commits_client ||= (dry_run ? GitlabClient::CommitsDryClient : GitlabClient::CommitsClient)
114
+ .new(token: token, project: project)
115
+ @commits_client.create(branch['name'], file_path, new_content, message)
116
+ end
117
+
118
+ # Create a Merge Request with a given branch
119
+ #
120
+ # @param [String] title_prefix the prefix of the title
121
+ # @param [String] example_name the example
122
+ # @param [Gitlab::ObjectifiedHash] branch the branch
123
+ # @param [Integer] assignee_id
124
+ # @param [Array<Integer>] reviewer_ids
125
+ # @param [String] labels comma seperated list of labels
126
+ # @return [Gitlab::ObjectifiedHash] the created merge request
127
+ def create_merge_request(title, branch, assignee_id = nil, reviewer_ids = [], labels = '')
128
+ description = yield
129
+
130
+ merge_request_client.create_merge_request(
131
+ title: title,
132
+ source_branch: branch['name'],
133
+ target_branch: ref,
134
+ description: description,
135
+ labels: labels,
136
+ assignee_id: assignee_id,
137
+ reviewer_ids: reviewer_ids)
138
+ end
139
+
140
+ # Check if issue is closed
141
+ #
142
+ # @param [Gitlab::ObjectifiedHash] issue the issue
143
+ # @return [Boolean] True or False
144
+ def issue_is_closed?(issue)
145
+ issue['state'] == 'closed'
146
+ end
147
+
148
+ # Get scoped label from issue
149
+ #
150
+ # @param [Gitlab::ObjectifiedHash] issue the issue
151
+ # @param [String] scope
152
+ # @return [String] scoped label
153
+ def issue_scoped_label(issue, scope)
154
+ issue['labels'].detect { |label| label.match(/#{scope}::/) }
155
+ end
156
+
157
+ # Fetch an issue
158
+ #
159
+ # @param [String] iid: The iid of the issue
160
+ # @return [Gitlab::ObjectifiedHash]
161
+ def fetch_issue(iid:)
162
+ issue_client.find_issues(iid: iid).first
163
+ end
164
+
165
+ # Post note on report_issue
166
+ #
167
+ # @param [String] note the note to post
168
+ # @return [Gitlab::ObjectifiedHash]
169
+ def post_note_on_report_issue(note)
170
+ iid = report_issue&.split('/')&.last # split url segment, last segment of path is the issue id
171
+ if iid
172
+ issue_client.create_issue_note(iid: iid, note: note)
173
+ else
174
+ Runtime::Logger.info("#{self.class.name}##{__method__} Note was NOT posted on report issue: #{report_issue}")
175
+ end
176
+ end
177
+
178
+ # Post a note of merge reqest
179
+ #
180
+ # @param [String] note
181
+ # @param [Integer] merge_request_iid
182
+ # @return [Gitlab::ObjectifiedHash]
183
+ def post_note_on_merge_request(note, merge_request_iid)
184
+ merge_request_client.create_note(note: note, merge_request_iid: merge_request_iid)
185
+ end
186
+
187
+ # Fetch the id for the dri of the product group and stage
188
+ # The first item returned is the id of the assignee and the second item is the handle
189
+ #
190
+ # @param [String] product_group
191
+ # @param [String] devops_stage
192
+ # @return [Array<Integer, String>]
193
+ def fetch_dri_id(product_group, devops_stage)
194
+ assignee_handle = ENV.fetch('QA_TEST_DRI_HANDLE', nil) || set_dri_via_group(product_group, devops_stage)
195
+
196
+ [user_id_for_username(assignee_handle), assignee_handle]
197
+ end
198
+
199
+ # Fetch id for the given GitLab username/handle
200
+ #
201
+ # @param [String] username
202
+ # @return [Integer]
203
+ def user_id_for_username(username)
204
+ issue_client.find_user_id(username: username)
205
+ end
206
+
207
+ # Post a message on Slack
208
+ #
209
+ # @param [String] message the message to post
210
+ # @return [HTTP::Response]
211
+ def post_message_on_slack(message)
212
+ channel = ENV.fetch('SLACK_QA_CHANNEL', nil) || TEST_PLATFORM_MAINTAINERS_SLACK_CHANNEL_ID
213
+ slack_options = {
214
+ slack_webhook_url: ENV.fetch('CI_SLACK_WEBHOOK_URL', nil),
215
+ channel: channel,
216
+ username: "GitLab Quality Test Tooling",
217
+ icon_emoji: ':warning:',
218
+ message: message
219
+ }
220
+ puts "Posting Slack message to channel: #{channel}"
221
+
222
+ (dry_run ? GitlabQuality::TestTooling::Slack::PostToSlackDry : GitlabQuality::TestTooling::Slack::PostToSlack).new(**slack_options).invoke!
223
+ end
224
+
225
+ # Provide indentaiton based on the given line
226
+ #
227
+ # @param[String] line the line to use for indentation
228
+ # @return[String] indentation
229
+ def indentation(line)
230
+ # Indent the same number of spaces as the current line
231
+ no_of_spaces = line[/\A */].size
232
+ # If the first char on current line is not a quote, add two more spaces
233
+ no_of_spaces += /['"]/.match?(line.lstrip[0]) ? 0 : 2
234
+
235
+ " " * no_of_spaces
236
+ end
237
+
238
+ # Returns and existing merge request with the given title
239
+ #
240
+ # @param [String] title: Title of the merge request
241
+ # @return [Array<Gitlab::ObjectifiedHash>] Merge requests
242
+ def existing_merge_requests(title:)
243
+ merge_request_client.find(options: { search: title, in: 'title', state: 'opened' })
244
+ end
245
+
246
+ # Checks if changes has already been made to given file and line number
247
+ #
248
+ # @param [String] file_path path to the file
249
+ # @param [Integer] changed_line_no updated line number
250
+ # @return [Boolean]
251
+ def record_processed?(file_path, changed_line_no)
252
+ processed_records[file_path] && processed_records[file_path] == changed_line_no
253
+ end
254
+
255
+ # Infers product group label from the provided product group
256
+ #
257
+ # @param [String] product_group product group
258
+ # @return [String]
259
+ def label_from_product_group(product_group)
260
+ label = labels_inference.infer_labels_from_product_group(product_group).to_a.first
261
+
262
+ label ? %(/label ~"#{label}") : ''
263
+ end
264
+
265
+ # Returns the link to the Grafana dashboard for single spec metrics
266
+ #
267
+ # @param [String] example_name the full example name
268
+ # @return [String]
269
+ def single_spec_metrics_link(example_name)
270
+ base_url = "https://dashboards.quality.gitlab.net/d/cW0UMgv7k/single-spec-metrics?orgId=1&var-run_type=All&var-name="
271
+ base_url + CGI.escape(example_name)
272
+ end
273
+
274
+ private
275
+
276
+ attr_reader :token, :specs_file, :dry_run, :processor
277
+
278
+ # Returns any test description string within single or double quotes
279
+ #
280
+ # @param [String] line the line to check for any quoted string
281
+ # @return [String] the match or nil if no match
282
+ def spec_desc_string_within_quotes(line)
283
+ match = line.match(/(?:it|describe|context|\s)+ ['"]([^'"]*)['"]/)
284
+ match ? match[1] : nil
285
+ end
286
+
287
+ # Returns the GitlabIssueClient or GitlabIssueDryClient based on the value of dry_run
288
+ #
289
+ # @return [GitlabIssueDryClient | GitlabIssueClient]
290
+ def issue_client
291
+ @issue_client ||= (dry_run ? GitlabClient::IssuesDryClient : GitlabClient::IssuesClient).new(token: token, project: project)
292
+ end
293
+
294
+ # Returns the MergeRequestDryClient or MergeRequest based on the value of dry_run
295
+ #
296
+ # @return [MergeRequestDryClient | MergeRequest]
297
+ def merge_request_client
298
+ @merge_request_client ||= (dry_run ? GitlabClient::MergeRequestsDryClient : GitlabClient::MergeRequestsClient).new(
299
+ token: token,
300
+ project: project
301
+ )
302
+ end
303
+
304
+ # Returns a cached instance of GitlabQuality::TestTooling::LabelsInference
305
+ #
306
+ # @return [GitlabQuality::TestTooling::LabelsInference]
307
+ def labels_inference
308
+ @labels_inference ||= GitlabQuality::TestTooling::LabelsInference.new
309
+ end
310
+ end
311
+ end
312
+ end
313
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module GitlabQuality
4
4
  module TestTooling
5
- VERSION = "1.11.0"
5
+ VERSION = "1.17.0"
6
6
  end
7
7
  end