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
@@ -205,12 +205,9 @@ module GitlabQuality
205
205
 
206
206
  def generate_test_text(testcase, tests_with_same_testcase, passed)
207
207
  text = tests_with_same_testcase.map(&:name).uniq.join(', ')
208
- encoded_text = ERB::Util.url_encode(text)
209
208
 
210
209
  if testcase && !passed
211
- # Workaround for reducing system notes on testcase issues
212
- # The first regex extracts the link to the issues list page from a link to a single issue show page by removing the issue id.
213
- "[#{text}](#{testcase.match(%r{[\s\S]+/[^/\d]+})}?state=opened&search=#{encoded_text})"
210
+ "[#{text}](#{testcase})"
214
211
  else
215
212
  text
216
213
  end
@@ -0,0 +1,139 @@
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
+ updated_description = <<~MARKDOWN.chomp
103
+ #{issue.description}
104
+
105
+ #{run_time_detail(spec_run_time)}
106
+ MARKDOWN
107
+
108
+ issue_attrs = {
109
+ description: updated_description
110
+ }
111
+
112
+ gitlab.edit_issue(iid: issue.iid, options: issue_attrs)
113
+ puts " => Added a report in #{issue.web_url}!"
114
+ end
115
+
116
+ def run_time_detail(spec_run_time)
117
+ <<~MARKDOWN.chomp
118
+ - Reported from pipeline #{spec_run_time.ci_pipeline_url_markdown} created at `#{spec_run_time.ci_pipeline_created_at}`
119
+
120
+ | Field | Value |
121
+ | ------ | ------ |
122
+ | Job URL| #{spec_run_time.ci_job_link_markdown} |
123
+ | Job total RSpec suite run time | expected: `#{readable_duration(spec_run_time.expected_suite_duration)}`, actual: `#{readable_duration(spec_run_time.actual_suite_duration)}` |
124
+ | Spec file run time | expected: `#{readable_duration(spec_run_time.expected)}`, actual: `#{readable_duration(spec_run_time.actual)}` |
125
+ | Spec file weight | `#{spec_run_time.actual_percentage}%` of total suite run time |
126
+ MARKDOWN
127
+ end
128
+
129
+ def assert_input_files!(_files)
130
+ missing_expected_report_msg = "Missing a valid expected Knapsack report."
131
+ missing_actual_report_msg = "Missing a valid actual Knapsack report."
132
+
133
+ abort missing_expected_report_msg if expected_report_path.nil?
134
+ abort missing_actual_report_msg if actual_report_path.nil?
135
+ end
136
+ end
137
+ end
138
+ end
139
+ end
@@ -16,7 +16,7 @@ module GitlabQuality
16
16
  # - Find issue by title (with test description or test file), then further filter by stack trace, then pick the better-matching one
17
17
  # - Add the failed job to the issue description, and update labels
18
18
  class RelateFailureIssue < ReportAsIssue
19
- include Concerns::FindSetDri
19
+ include TestTooling::Concerns::FindSetDri
20
20
  include Concerns::GroupAndCategoryLabels
21
21
  include Concerns::IssueReports
22
22
  include Amatch
@@ -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
@@ -192,8 +190,8 @@ module GitlabQuality
192
190
  end
193
191
 
194
192
  def failure_issues(test)
195
- find_issues_for_test(
196
- test,
193
+ find_issues_by_hash(
194
+ test_hash(test),
197
195
  state: 'opened',
198
196
  labels: base_issue_labels + Set.new(%w[test]),
199
197
  not_labels: exclude_labels_for_search
@@ -270,7 +268,7 @@ module GitlabQuality
270
268
  if stacktrace_match
271
269
  stacktrace_match[:stacktrace].gsub(/^\s*#.*$/, '').gsub(/^[[:space:]]+/, '').strip
272
270
  else
273
- puts " => [DEBUG] Stacktrace doesn't match the regex (#{regex}):\n----------------\n#{stacktrace}\n----------------\n"
271
+ puts " => [DEBUG] Stacktrace doesn't match the regex (#{regex})!"
274
272
  end
275
273
  end
276
274
 
@@ -332,7 +330,7 @@ module GitlabQuality
332
330
  def new_issue_assignee_id(test)
333
331
  return unless test.product_group?
334
332
 
335
- dri = set_dri_via_group(test.product_group, test)
333
+ dri = set_dri_via_group(test.product_group, test.stage)
336
334
  puts " => Assigning #{dri} as DRI for the issue."
337
335
 
338
336
  gitlab.find_user_id(username: dri)
@@ -349,7 +347,7 @@ module GitlabQuality
349
347
  state_event = issue.state == 'closed' ? 'reopen' : nil
350
348
 
351
349
  issue_attrs = {
352
- description: add_report_to_issue_description(issue, test),
350
+ description: increment_reports(current_reports_content: issue.description, test: test),
353
351
  labels: up_to_date_labels(test: test, issue: issue)
354
352
  }
355
353
  issue_attrs[:state_event] = state_event if state_event
@@ -358,10 +356,6 @@ module GitlabQuality
358
356
  puts " => Added a report in '#{issue.title}': #{issue.web_url}!"
359
357
  end
360
358
 
361
- def new_issue_title(test)
362
- "Failure in #{super}"
363
- end
364
-
365
359
  def screenshot_section(test)
366
360
  return unless test.screenshot?
367
361
 
@@ -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
@@ -56,7 +60,7 @@ module GitlabQuality
56
60
  | Description | `#{test.name}` |
57
61
  | Test level | #{test.level} |
58
62
  | Hash | `#{test_hash(test)}` |
59
- | Duration | #{test.run_time} seconds |
63
+ | Reference duration | #{test.run_time} seconds |
60
64
  | Expected duration | < #{test.max_duration_for_test} seconds |
61
65
  #{"| Test case | #{test.testcase} |" if test.testcase}
62
66
  DESCRIPTION
@@ -138,8 +142,10 @@ module GitlabQuality
138
142
  labels
139
143
  end
140
144
 
141
- def find_issues_by_hash(test_hash)
142
- search_options = { search: test_hash }
145
+ def find_issues_by_hash(test_hash, labels: Set.new, not_labels: Set.new, state: nil)
146
+ search_options = { search: test_hash, labels: labels.to_a, not: { labels: not_labels.to_a } }
147
+ search_options[:state] = state if state
148
+ search_options[:in] = 'description'
143
149
  gitlab.find_issues(options: search_options)
144
150
  end
145
151
 
@@ -162,7 +168,12 @@ module GitlabQuality
162
168
  def issue_match_test?(issue, test)
163
169
  issue_title = issue.title.strip
164
170
  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
171
+
172
+ if test.name
173
+ issue_title.include?(test.name) || test_file_path_found
174
+ else
175
+ test_file_path_found
176
+ end
166
177
  end
167
178
 
168
179
  def pipeline_name_label
@@ -10,16 +10,21 @@ module GitlabQuality
10
10
  # - Find issue by title (with test description or test file)
11
11
  # - Add test metadata, duration to the issue with group and category labels
12
12
  class SlowTestIssue < ReportAsIssue
13
- include Concerns::FindSetDri
14
13
  include Concerns::GroupAndCategoryLabels
14
+ include Concerns::IssueReports
15
15
 
16
- NEW_ISSUE_LABELS = Set.new(['test', 'type::maintenance', 'maintenance::performance', 'priority::3', 'severity::3', 'rspec profiling', 'rspec:slow test']).freeze
17
- SEARCH_LABELS = %w[test maintenance::performance].freeze
16
+ IDENTITY_LABELS = ['test', 'rspec:slow test', 'rspec profiling', 'automation:bot-authored'].freeze
17
+ NEW_ISSUE_LABELS = Set.new(['test', 'type::maintenance', 'maintenance::performance', 'priority::3', 'severity::3']).freeze
18
+ SEARCH_LABELS = ['test'].freeze
19
+ FOUND_IN_MR_LABEL = '~"found:in MR"'
20
+ FOUND_IN_MASTER_LABEL = '~"found:master"'
21
+ REPORT_SECTION_HEADER = '### Slowness reports'
22
+ REPORTS_DOCUMENTATION = <<~DOC
23
+ Slow tests were detected, please see the [test speed best practices guide](https://docs.gitlab.com/ee/development/testing_guide/best_practices.html#test-speed)
24
+ to improve them. More context available about this issue in the [top slow tests guide](https://docs.gitlab.com/ee/development/testing_guide/best_practices.html#top-slow-tests).
18
25
 
19
- JOB_URL_REGEX = %r{(?<job_url>https://(?<host>[\w.]+)/(?<project_path>[\w\-./]+)/-/jobs/\d+)}
20
- REPORT_ITEM_REGEX = /^1\. \d{4}-\d{2}-\d{2}: #{JOB_URL_REGEX} \((?<pipeline_url>.+)\)$/
21
-
22
- MultipleIssuesFound = Class.new(StandardError)
26
+ Add `allowed_to_be_slow: true` to the RSpec test if this is a legit slow test and close the issue.
27
+ DOC
23
28
 
24
29
  private
25
30
 
@@ -29,101 +34,87 @@ module GitlabQuality
29
34
  TestResults::Builder.new(files).test_results_per_file do |test_results|
30
35
  puts "=> Reporting #{test_results.count} tests in #{test_results.path}"
31
36
 
32
- test_results.each do |test|
33
- create_slow_issue(test) if test.slow_test?
34
- end
37
+ process_test_results(test_results)
35
38
  end
36
-
37
- write_issues_log_file
38
39
  end
39
40
 
40
- def new_issue_title(test)
41
- "Slow test in #{super}"
42
- end
41
+ def process_test_results(test_results)
42
+ test_results.each do |test|
43
+ next unless test.slow_test?
43
44
 
44
- def new_issue_description(test)
45
- super +
46
- <<~DESCRIPTION.chomp
47
- Slow tests were detected, please see the [test speed best practices guide](https://docs.gitlab.com/ee/development/testing_guide/best_practices.html#test-speed)
48
- to improve them. More context available about this issue in the [top slow tests guide](https://docs.gitlab.com/ee/development/testing_guide/best_practices.html#top-slow-tests).
45
+ puts " => Reporting slowness for test '#{test.name}'..."
49
46
 
50
- Add `allowed_to_be_slow: true` to the RSpec test if this is a legit slow test and close the issue.
47
+ issues = find_issues_by_hash(test_hash(test), state: 'opened', labels: SEARCH_LABELS)
48
+ issues << create_issue(test) if issues.empty?
51
49
 
52
- #{reports_section(test)}
53
- DESCRIPTION
50
+ update_reports(issues, test)
51
+ collect_issues(test, issues)
52
+ end
54
53
  end
55
54
 
56
- def reports_section(test)
57
- <<~REPORTS
58
- ### Reports (1)
59
-
60
- #{report_list_item(test)}
61
- REPORTS
55
+ def test_is_applicable?(test)
56
+ test.slow_test?
62
57
  end
63
58
 
64
- def report_list_item(test)
65
- "1. #{Time.new.utc.strftime('%F')}: #{test.ci_job_url} (#{ENV.fetch('CI_PIPELINE_URL', 'pipeline url is missing')})"
59
+ def update_reports(issues, test)
60
+ issues.each do |issue|
61
+ puts " => Adding the slow test to the existing issue: #{issue.web_url}"
62
+ add_report_to_issue(issue: issue, test: test, related_issues: (issues - [issue]))
63
+ end
66
64
  end
67
65
 
68
- def slow_test_issues(test)
69
- find_issues_for_test(
70
- test,
71
- state: 'opened',
72
- labels: SEARCH_LABELS
73
- )
66
+ def add_report_to_issue(issue:, test:, related_issues:)
67
+ reports_note = existing_reports_note(issue: issue)
68
+ note_body = [
69
+ report_body(reports_note: reports_note, test: test),
70
+ identity_labels_quick_action,
71
+ relate_issues_quick_actions(related_issues)
72
+ ].join("\n")
73
+
74
+ if reports_note
75
+ gitlab.edit_issue_note(
76
+ issue_iid: issue.iid,
77
+ note_id: reports_note.id,
78
+ note: note_body
79
+ )
80
+ else
81
+ gitlab.create_issue_note(iid: issue.iid, note: note_body)
82
+ end
74
83
  end
75
84
 
76
- def create_slow_issue(test)
77
- puts " => Finding existing issues for slow test '#{test.name}' (run time: #{test.run_time} seconds)..."
85
+ def existing_reports_note(issue:)
86
+ gitlab.find_issue_notes(iid: issue.iid).find do |note|
87
+ note.body.start_with?(REPORT_SECTION_HEADER)
88
+ end
89
+ end
78
90
 
79
- issues = slow_test_issues(test)
91
+ def report_body(reports_note:, test:)
92
+ increment_reports(
93
+ current_reports_content: reports_note&.body.to_s,
94
+ test: test,
95
+ reports_section_header: REPORT_SECTION_HEADER,
96
+ item_extra_content: found_label,
97
+ reports_extra_content: REPORTS_DOCUMENTATION
98
+ )
99
+ end
80
100
 
81
- if issues.blank?
82
- issues << create_issue(test)
101
+ def found_label
102
+ if ENV.key?('CI_MERGE_REQUEST_IID')
103
+ FOUND_IN_MR_LABEL
83
104
  else
84
- issues.each do |issue|
85
- puts " => Existing issue link #{issue['web_url']}"
86
-
87
- update_reports(issue, test)
88
- end
105
+ FOUND_IN_MASTER_LABEL
89
106
  end
90
-
91
- collect_issues(test, issues)
92
- rescue MultipleIssuesFound => e
93
- warn(e.message)
94
107
  end
95
108
 
96
- def update_reports(issue, test)
97
- # We reopen closed issues to not lose any history
98
- state_event = issue.state == 'closed' ? 'reopen' : nil
99
-
100
- issue_attrs = {
101
- description: up_to_date_issue_description(issue.description, test)
102
- }
103
-
104
- issue_attrs[:state_event] = state_event if state_event
105
-
106
- gitlab.edit_issue(iid: issue.iid, options: issue_attrs)
107
- puts " => Added a report in '#{issue.title}': #{issue.web_url}!"
109
+ def identity_labels_quick_action
110
+ labels_list = IDENTITY_LABELS.map { |label| %(~"#{label}") }.join(' ')
111
+ %(/label #{labels_list})
108
112
  end
109
113
 
110
- def up_to_date_issue_description(issue_description, test)
111
- new_issue_description =
112
- if issue_description.include?('### Reports')
113
- # We count the number of existing reports.
114
- reports_count = issue_description
115
- .scan(REPORT_ITEM_REGEX)
116
- .size.to_i + 1
117
- issue_description.sub(/^### Reports.*$/, "### Reports (#{reports_count})")
118
- else # For issue with the legacy format, we add the Reports section
119
- reports_count = issue_description
120
- .scan(JOB_URL_REGEX)
121
- .size.to_i + 1
122
-
123
- "#{issue_description}\n\n### Reports (#{reports_count})"
124
- end
125
-
126
- "#{new_issue_description}\n#{report_list_item(test)}"
114
+ def relate_issues_quick_actions(issues)
115
+ issues.map do |issue|
116
+ "/relate #{issue.web_url}"
117
+ end.join("\n")
127
118
  end
128
119
  end
129
120
  end
@@ -25,7 +25,7 @@ module GitlabQuality
25
25
 
26
26
  ENV_VARIABLES.each do |env_name, method_name|
27
27
  define_method(method_name) do
28
- env_var_value_if_defined(env_name) || (instance_variable_get("@#{method_name}") if instance_variable_defined?("@#{method_name}"))
28
+ env_var_value_if_defined(env_name) || (instance_variable_get(:"@#{method_name}") if instance_variable_defined?(:"@#{method_name}"))
29
29
  end
30
30
  end
31
31
 
@@ -33,6 +33,10 @@ module GitlabQuality
33
33
  env_var_value_if_defined('QA_LOG_LEVEL')&.upcase || 'INFO'
34
34
  end
35
35
 
36
+ def gitlab_bot_username
37
+ env_var_value_if_defined('GITLAB_BOT_USERNAME') || 'gitlab-bot'
38
+ end
39
+
36
40
  def log_path
37
41
  env_var_value_if_defined('QA_LOG_PATH') || host_artifacts_dir
38
42
  end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GitlabQuality
4
+ module TestTooling
5
+ module Slack
6
+ class PostToSlackDry < PostToSlack
7
+ def invoke!
8
+ puts "The following message would have posted to Slack:"
9
+ puts message
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,143 @@
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
+ @existing_mrs = nil
18
+ @file_path = spec["file_path"]
19
+ testcase = spec["testcase"]
20
+ devops_stage = spec["stage"]
21
+ product_group = spec["product_group"]
22
+ @example_name = spec["name"]
23
+ @mr_title = format("%{prefix} %{example_name}", prefix: '[PROMOTE TO BLOCKING]', example_name: example_name).truncate(72, omission: '')
24
+
25
+ @file_contents = context.get_file_contents(file_path)
26
+
27
+ new_content, @changed_line_no = add_blocking_metadata
28
+
29
+ return unless proceed_with_merge_request?
30
+
31
+ branch = context.create_branch("blocking-promotion-#{SecureRandom.hex(4)}", example_name, context.ref)
32
+
33
+ context.commit_changes(branch, <<~COMMIT_MESSAGE, file_path, new_content)
34
+ Promote end-to-end test to blocking
35
+
36
+ #{"Promote to blocking: #{example_name}".truncate(72)}
37
+ COMMIT_MESSAGE
38
+
39
+ reviewer_id, assignee_handle = context.fetch_dri_id(product_group, devops_stage)
40
+
41
+ gitlab_bot_user_id = context.user_id_for_username(Runtime::Env.gitlab_bot_username)
42
+
43
+ merge_request = context.create_merge_request(mr_title, branch, gitlab_bot_user_id, [reviewer_id]) do
44
+ <<~MARKDOWN
45
+ ## What does this MR do?
46
+
47
+ Promotes the test [`#{example_name}`](https://gitlab.com/#{context.project}/-/blob/#{context.ref}/#{file_path}#L#{changed_line_no + 1})
48
+ to the blocking bucket
49
+
50
+ This test was identified in the reliable e2e test report: #{context.report_issue}
51
+
52
+ [Testcase link](#{testcase})
53
+
54
+ [Spec metrics link](#{context.single_spec_metrics_link(example_name)})
55
+
56
+ /label ~"Quality" ~"QA" ~"type::maintenance"
57
+ /label ~"devops::#{devops_stage}"
58
+ #{context.label_from_product_group(product_group)}
59
+
60
+ <div align="center">
61
+ (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})
62
+ </div>
63
+ MARKDOWN
64
+ end
65
+
66
+ context.post_note_on_merge_request(<<~MARKDOWN, merge_request.iid)
67
+ @#{assignee_handle} Please review this MR, approve and assign it to a maintainer.
68
+
69
+ 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}
70
+ MARKDOWN
71
+
72
+ if merge_request
73
+ context.add_processed_record({ file_path => changed_line_no })
74
+ Runtime::Logger.info(" Created MR for promotion to blocking: #{merge_request.web_url}")
75
+ end
76
+
77
+ merge_request
78
+ end
79
+
80
+ # Performs post processing. Takes a list of MRs and posts them in a note on report_issue
81
+ #
82
+ # @param [Gitlab::ObjectifiedHash] merge_requests
83
+ def post_process(merge_requests)
84
+ web_urls = merge_requests.compact.map { |mr| "- #{mr.web_url}\n" }.join
85
+
86
+ return if web_urls.empty?
87
+
88
+ context.post_note_on_report_issue(<<~ISSUE_NOTE)
89
+ The following merge requests have been created to promote stable specs to blocking:
90
+
91
+ #{web_urls}
92
+ ISSUE_NOTE
93
+ end
94
+
95
+ private
96
+
97
+ attr_reader :context, :file_path, :file_contents, :example_name, :mr_title, :changed_line_no
98
+
99
+ # Checks if there is already an MR open
100
+ #
101
+ # @return [Boolean]
102
+ def proceed_with_merge_request? # rubocop:disable Metrics/AbcSize
103
+ if changed_line_no.negative?
104
+ Runtime::Logger.info(" No lines were changed in #{file_path}. Will not proceed with creating MR.")
105
+ return false
106
+ elsif context.record_processed?(file_path, changed_line_no)
107
+ Runtime::Logger.info(" Record already processed for #{file_path}:#{changed_line_no}. Will not proceed with creating MR.")
108
+ return false
109
+ elsif existing_mrs&.any?
110
+ Runtime::Logger.info(" An open MR already exists for '#{example_name}': #{existing_mrs.first['web_url']}. Will not proceed with creating MR.")
111
+ return false
112
+ end
113
+
114
+ true
115
+ end
116
+
117
+ # Add blocking metadata to the file content and replace it
118
+ #
119
+ # @return [Array<String, Integer>] first value holds the new content, the second value holds the line number of the test
120
+ def add_blocking_metadata # rubocop:disable Metrics/AbcSize
121
+ matched_lines = context.find_example_match_lines(file_contents, example_name)
122
+
123
+ if matched_lines.any? { |line| line[0].include?(':blocking') }
124
+ puts "Example '#{example_name}' is already blocking"
125
+ return [file_contents, -1]
126
+ end
127
+
128
+ context.update_matched_line(matched_lines.last, file_contents.dup) do |line|
129
+ if line.sub(DESCRIPTION_REGEX, '').include?(',')
130
+ line[line.index(',', end_of_description_index(line))] = format(BLOCKING_METADATA, suffix: ',')
131
+ else
132
+ line[line.rindex(' ')] = format(BLOCKING_METADATA, suffix: ' ')
133
+ end
134
+
135
+ line
136
+ end
137
+ end
138
+ end
139
+ end
140
+ end
141
+ end
142
+ end
143
+ end