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.
- checksums.yaml +4 -4
- data/Gemfile.lock +93 -68
- data/README.md +35 -2
- data/exe/flaky-test-issues +7 -2
- data/exe/knapsack-report-issues +54 -0
- data/exe/update-test-meta +70 -0
- data/lefthook.yml +13 -0
- data/lib/gitlab_quality/test_tooling/concerns/find_set_dri.rb +47 -0
- data/lib/gitlab_quality/test_tooling/gitlab_client/branches_client.rb +18 -0
- data/lib/gitlab_quality/test_tooling/gitlab_client/branches_dry_client.rb +15 -0
- data/lib/gitlab_quality/test_tooling/gitlab_client/commits_client.rb +20 -0
- data/lib/gitlab_quality/test_tooling/gitlab_client/commits_dry_client.rb +14 -0
- data/lib/gitlab_quality/test_tooling/gitlab_client/gitlab_client.rb +12 -13
- data/lib/gitlab_quality/test_tooling/gitlab_client/issues_client.rb +6 -6
- data/lib/gitlab_quality/test_tooling/gitlab_client/merge_requests_client.rb +18 -10
- data/lib/gitlab_quality/test_tooling/gitlab_client/merge_requests_dry_client.rb +4 -2
- data/lib/gitlab_quality/test_tooling/gitlab_client/repository_files_client.rb +23 -0
- data/lib/gitlab_quality/test_tooling/knapsack_reports/spec_run_time.rb +85 -0
- data/lib/gitlab_quality/test_tooling/knapsack_reports/spec_run_time_report.rb +60 -0
- data/lib/gitlab_quality/test_tooling/report/concerns/issue_reports.rb +41 -28
- data/lib/gitlab_quality/test_tooling/report/concerns/utils.rb +23 -1
- data/lib/gitlab_quality/test_tooling/report/flaky_test_issue.rb +89 -44
- data/lib/gitlab_quality/test_tooling/report/generate_test_session.rb +1 -4
- data/lib/gitlab_quality/test_tooling/report/knapsack_report_issue.rb +139 -0
- data/lib/gitlab_quality/test_tooling/report/relate_failure_issue.rb +6 -12
- data/lib/gitlab_quality/test_tooling/report/report_as_issue.rb +16 -5
- data/lib/gitlab_quality/test_tooling/report/slow_test_issue.rb +71 -80
- data/lib/gitlab_quality/test_tooling/runtime/env.rb +5 -1
- data/lib/gitlab_quality/test_tooling/slack/post_to_slack_dry.rb +14 -0
- data/lib/gitlab_quality/test_tooling/test_meta/processor/add_to_blocking_processor.rb +143 -0
- data/lib/gitlab_quality/test_tooling/test_meta/processor/add_to_quarantine_processor.rb +199 -0
- data/lib/gitlab_quality/test_tooling/test_meta/processor/meta_processor.rb +44 -0
- data/lib/gitlab_quality/test_tooling/test_meta/test_meta_updater.rb +313 -0
- data/lib/gitlab_quality/test_tooling/version.rb +1 -1
- metadata +40 -9
- 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
|
-
#
|
|
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
|
-
|
|
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})
|
|
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:
|
|
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
|
-
|
|
|
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
|
-
|
|
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
|
-
|
|
17
|
-
|
|
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
|
-
|
|
20
|
-
|
|
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
|
|
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
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
def process_test_results(test_results)
|
|
42
|
+
test_results.each do |test|
|
|
43
|
+
next unless test.slow_test?
|
|
43
44
|
|
|
44
|
-
|
|
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
|
-
|
|
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
|
-
|
|
53
|
-
|
|
50
|
+
update_reports(issues, test)
|
|
51
|
+
collect_issues(test, issues)
|
|
52
|
+
end
|
|
54
53
|
end
|
|
55
54
|
|
|
56
|
-
def
|
|
57
|
-
|
|
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
|
|
65
|
-
|
|
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
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
|
77
|
-
|
|
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
|
-
|
|
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
|
-
|
|
82
|
-
|
|
101
|
+
def found_label
|
|
102
|
+
if ENV.key?('CI_MERGE_REQUEST_IID')
|
|
103
|
+
FOUND_IN_MR_LABEL
|
|
83
104
|
else
|
|
84
|
-
|
|
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
|
|
97
|
-
|
|
98
|
-
|
|
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
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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,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
|