gitlab_quality-test_tooling 1.14.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 (25) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +73 -70
  3. data/exe/flaky-test-issues +4 -4
  4. data/lefthook.yml +13 -0
  5. data/lib/gitlab_quality/test_tooling/gitlab_client/branches_client.rb +1 -1
  6. data/lib/gitlab_quality/test_tooling/gitlab_client/commits_client.rb +6 -4
  7. data/lib/gitlab_quality/test_tooling/gitlab_client/gitlab_client.rb +12 -13
  8. data/lib/gitlab_quality/test_tooling/gitlab_client/issues_client.rb +6 -6
  9. data/lib/gitlab_quality/test_tooling/gitlab_client/merge_requests_client.rb +6 -3
  10. data/lib/gitlab_quality/test_tooling/gitlab_client/merge_requests_dry_client.rb +4 -2
  11. data/lib/gitlab_quality/test_tooling/report/concerns/issue_reports.rb +41 -28
  12. data/lib/gitlab_quality/test_tooling/report/concerns/utils.rb +1 -1
  13. data/lib/gitlab_quality/test_tooling/report/flaky_test_issue.rb +78 -43
  14. data/lib/gitlab_quality/test_tooling/report/generate_test_session.rb +1 -4
  15. data/lib/gitlab_quality/test_tooling/report/knapsack_report_issue.rb +0 -3
  16. data/lib/gitlab_quality/test_tooling/report/relate_failure_issue.rb +4 -8
  17. data/lib/gitlab_quality/test_tooling/report/report_as_issue.rb +5 -3
  18. data/lib/gitlab_quality/test_tooling/report/slow_test_issue.rb +71 -78
  19. data/lib/gitlab_quality/test_tooling/runtime/env.rb +5 -1
  20. data/lib/gitlab_quality/test_tooling/test_meta/processor/add_to_blocking_processor.rb +33 -16
  21. data/lib/gitlab_quality/test_tooling/test_meta/processor/add_to_quarantine_processor.rb +34 -19
  22. data/lib/gitlab_quality/test_tooling/test_meta/processor/meta_processor.rb +21 -0
  23. data/lib/gitlab_quality/test_tooling/test_meta/test_meta_updater.rb +70 -9
  24. data/lib/gitlab_quality/test_tooling/version.rb +1 -1
  25. metadata +4 -4
@@ -24,30 +24,34 @@ module GitlabQuality
24
24
  # @param [TestMetaUpdater] context instance of TestMetaUpdater
25
25
  def execute(spec, context) # rubocop:disable Metrics/AbcSize
26
26
  @context = context
27
-
27
+ @existing_mrs = nil
28
28
  @file_path = spec["file_path"]
29
+ testcase = spec["testcase"]
29
30
  devops_stage = spec["stage"]
31
+ product_group = spec["product_group"]
30
32
  @failure_issue_url = spec["failure_issue"]
31
33
  @example_name = spec["name"]
32
34
  @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)
35
+ @mr_title = format("%{prefix} %{example_name}", prefix: '[QUARANTINE]', example_name: example_name).truncate(72, omission: '')
34
36
  @failure_issue = context.fetch_issue(iid: issue_id)
35
37
 
36
- return unless proceed_with_merge_request?
37
-
38
38
  @file_contents = context.get_file_contents(file_path)
39
39
 
40
- new_content, changed_line_no = add_quarantine_metadata
40
+ new_content, @changed_line_no = add_quarantine_metadata
41
+
42
+ return unless proceed_with_merge_request?
41
43
 
42
44
  branch = context.create_branch("#{issue_id}-quarantine-#{SecureRandom.hex(4)}", example_name, context.ref)
43
45
 
44
46
  context.commit_changes(branch, <<~COMMIT_MESSAGE, file_path, new_content)
45
47
  Quarantine end-to-end test
46
48
 
47
- Quarantine #{example_name}
49
+ #{"Quarantine #{example_name}".truncate(72)}
48
50
  COMMIT_MESSAGE
49
51
 
50
- context.create_merge_request(mr_title, branch) do
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
51
55
  <<~MARKDOWN
52
56
  ## What does this MR do?
53
57
 
@@ -55,6 +59,10 @@ module GitlabQuality
55
59
 
56
60
  This test was identified in the reliable e2e test report: #{context.report_issue}
57
61
 
62
+ [Testcase link](#{testcase})
63
+
64
+ [Spec metrics link](#{context.single_spec_metrics_link(example_name)})
65
+
58
66
  ### E2E Test Failure issue(s)
59
67
 
60
68
  #{failure_issue_url}
@@ -79,12 +87,20 @@ module GitlabQuality
79
87
  `qa/specs/features/browser_ui/3_create/web_ide/add_file_template_spec.rb`.
80
88
  -->
81
89
  /label ~"devops::#{devops_stage}"
90
+ #{context.label_from_product_group(product_group)}
82
91
 
83
92
  <div align="center">
84
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})
85
94
  </div>
86
95
  MARKDOWN
87
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
88
104
  end
89
105
 
90
106
  # Performs post processing. Takes a list of MRs and posts them in a note on report_issue and Slack
@@ -114,20 +130,21 @@ module GitlabQuality
114
130
 
115
131
  private
116
132
 
117
- attr_reader :context, :file_path, :file_contents, :failure_issue_url, :example_name, :issue_id, :mr_title, :failure_issue
133
+ attr_reader :context, :file_path, :file_contents, :failure_issue_url, :example_name,
134
+ :issue_id, :mr_title, :failure_issue, :changed_line_no
118
135
 
119
136
  # Checks if the failure issue is closed or if there is already an MR open
120
137
  #
121
138
  # @return [Boolean]
122
- def proceed_with_merge_request?
139
+ def proceed_with_merge_request? # rubocop:disable Metrics/AbcSize
123
140
  if context.issue_is_closed?(failure_issue)
124
- puts " Failure issue '#{failure_issue_url}' is closed. Will not proceed with creating MR."
141
+ Runtime::Logger.info(" Failure issue '#{failure_issue_url}' is closed. Will not proceed with creating MR.")
125
142
  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."
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.")
131
148
  return false
132
149
  end
133
150
 
@@ -143,8 +160,8 @@ module GitlabQuality
143
160
  context.update_matched_line(matched_lines.last, file_contents.dup) do |line|
144
161
  indentation = context.indentation(line)
145
162
 
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)
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)
148
165
  else
149
166
  line[line.rindex(' ')] = format(QUARANTINE_METADATA.rstrip, issue_url: failure_issue_url, indentation: indentation, suffix: ' ', quarantine_type: quarantine_type)
150
167
  end
@@ -160,8 +177,6 @@ module GitlabQuality
160
177
  case context.issue_scoped_label(failure_issue, 'failure')&.split('::')&.last
161
178
  when 'new', 'investigating'
162
179
  ':investigating'
163
- when 'external-dependency'
164
- ':external_dependency'
165
180
  when 'broken-test'
166
181
  ':broken'
167
182
  when 'bug'
@@ -1,11 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'active_support/core_ext/string/filters'
4
+
3
5
  module GitlabQuality
4
6
  module TestTooling
5
7
  module TestMeta
6
8
  module Processor
7
9
  class MetaProcessor
8
10
  class << self
11
+ DESCRIPTION_REGEX = /('.*?')|(".*?")/
12
+
9
13
  def execute
10
14
  raise 'method not implemented'
11
15
  end
@@ -15,6 +19,23 @@ module GitlabQuality
15
19
  end
16
20
 
17
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
18
39
  end
19
40
  end
20
41
  end
@@ -8,7 +8,7 @@ module GitlabQuality
8
8
  class TestMetaUpdater
9
9
  include TestTooling::Concerns::FindSetDri
10
10
 
11
- attr_reader :project, :ref, :report_issue
11
+ attr_reader :project, :ref, :report_issue, :processed_records
12
12
 
13
13
  TEST_PLATFORM_MAINTAINERS_SLACK_CHANNEL_ID = 'C0437FV9KBN' # test-platform-maintainers
14
14
 
@@ -19,6 +19,7 @@ module GitlabQuality
19
19
  @ref = ref
20
20
  @dry_run = dry_run
21
21
  @processor = processor
22
+ @processed_records = {}
22
23
  end
23
24
 
24
25
  def invoke!
@@ -33,10 +34,20 @@ module GitlabQuality
33
34
  end
34
35
  end
35
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
+
36
47
  # Fetch contents of file from the repository
37
48
  #
38
- # [String] file_path path to the file
39
- # [String] contents of the file
49
+ # @param [String] file_path path to the file
50
+ # @return [String] contents of the file
40
51
  def get_file_contents(file_path)
41
52
  repository_files = GitlabClient::RepositoryFilesClient.new(token: token, project: project, file_path: file_path)
42
53
  repository_files.file_contents
@@ -110,8 +121,10 @@ module GitlabQuality
110
121
  # @param [String] example_name the example
111
122
  # @param [Gitlab::ObjectifiedHash] branch the branch
112
123
  # @param [Integer] assignee_id
124
+ # @param [Array<Integer>] reviewer_ids
125
+ # @param [String] labels comma seperated list of labels
113
126
  # @return [Gitlab::ObjectifiedHash] the created merge request
114
- def create_merge_request(title, branch, assignee_id = nil, labels = '')
127
+ def create_merge_request(title, branch, assignee_id = nil, reviewer_ids = [], labels = '')
115
128
  description = yield
116
129
 
117
130
  merge_request_client.create_merge_request(
@@ -120,7 +133,8 @@ module GitlabQuality
120
133
  target_branch: ref,
121
134
  description: description,
122
135
  labels: labels,
123
- assignee_id: assignee_id)
136
+ assignee_id: assignee_id,
137
+ reviewer_ids: reviewer_ids)
124
138
  end
125
139
 
126
140
  # Check if issue is closed
@@ -153,8 +167,12 @@ module GitlabQuality
153
167
  # @param [String] note the note to post
154
168
  # @return [Gitlab::ObjectifiedHash]
155
169
  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)
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
158
176
  end
159
177
 
160
178
  # Post a note of merge reqest
@@ -175,7 +193,15 @@ module GitlabQuality
175
193
  def fetch_dri_id(product_group, devops_stage)
176
194
  assignee_handle = ENV.fetch('QA_TEST_DRI_HANDLE', nil) || set_dri_via_group(product_group, devops_stage)
177
195
 
178
- [issue_client.find_user_id(username: assignee_handle), assignee_handle]
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)
179
205
  end
180
206
 
181
207
  # Post a message on Slack
@@ -217,6 +243,34 @@ module GitlabQuality
217
243
  merge_request_client.find(options: { search: title, in: 'title', state: 'opened' })
218
244
  end
219
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
+
220
274
  private
221
275
 
222
276
  attr_reader :token, :specs_file, :dry_run, :processor
@@ -234,7 +288,7 @@ module GitlabQuality
234
288
  #
235
289
  # @return [GitlabIssueDryClient | GitlabIssueClient]
236
290
  def issue_client
237
- @issue_client ||= (dry_run ? GitlabClient::IssuesDryClient : GitlabClient::IssuesClient).new(token: token, project: "gitlab-org/gitlab")
291
+ @issue_client ||= (dry_run ? GitlabClient::IssuesDryClient : GitlabClient::IssuesClient).new(token: token, project: project)
238
292
  end
239
293
 
240
294
  # Returns the MergeRequestDryClient or MergeRequest based on the value of dry_run
@@ -246,6 +300,13 @@ module GitlabQuality
246
300
  project: project
247
301
  )
248
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
249
310
  end
250
311
  end
251
312
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module GitlabQuality
4
4
  module TestTooling
5
- VERSION = "1.14.0"
5
+ VERSION = "1.17.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.14.0
4
+ version: 1.17.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-24 00:00:00.000000000 Z
11
+ date: 2024-03-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: climate_control
@@ -201,7 +201,7 @@ dependencies:
201
201
  version: '6.1'
202
202
  - - "<"
203
203
  - !ruby/object:Gem::Version
204
- version: '7.2'
204
+ version: '7.1'
205
205
  type: :runtime
206
206
  prerelease: false
207
207
  version_requirements: !ruby/object:Gem::Requirement
@@ -211,7 +211,7 @@ dependencies:
211
211
  version: '6.1'
212
212
  - - "<"
213
213
  - !ruby/object:Gem::Version
214
- version: '7.2'
214
+ version: '7.1'
215
215
  - !ruby/object:Gem::Dependency
216
216
  name: amatch
217
217
  requirement: !ruby/object:Gem::Requirement