gitlab_quality-test_tooling 1.14.2 → 1.17.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile.lock +2 -2
- data/exe/flaky-test-issues +4 -4
- data/lefthook.yml +13 -0
- data/lib/gitlab_quality/test_tooling/gitlab_client/gitlab_client.rb +11 -12
- 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 +3 -2
- data/lib/gitlab_quality/test_tooling/gitlab_client/merge_requests_dry_client.rb +3 -2
- data/lib/gitlab_quality/test_tooling/report/concerns/issue_reports.rb +41 -28
- data/lib/gitlab_quality/test_tooling/report/concerns/utils.rb +1 -1
- data/lib/gitlab_quality/test_tooling/report/flaky_test_issue.rb +78 -43
- data/lib/gitlab_quality/test_tooling/report/generate_test_session.rb +1 -4
- data/lib/gitlab_quality/test_tooling/report/knapsack_report_issue.rb +0 -3
- data/lib/gitlab_quality/test_tooling/report/relate_failure_issue.rb +4 -8
- data/lib/gitlab_quality/test_tooling/report/report_as_issue.rb +5 -3
- data/lib/gitlab_quality/test_tooling/report/slow_test_issue.rb +71 -78
- data/lib/gitlab_quality/test_tooling/runtime/env.rb +4 -0
- data/lib/gitlab_quality/test_tooling/test_meta/processor/add_to_blocking_processor.rb +33 -16
- data/lib/gitlab_quality/test_tooling/test_meta/processor/add_to_quarantine_processor.rb +34 -19
- data/lib/gitlab_quality/test_tooling/test_meta/processor/meta_processor.rb +21 -0
- data/lib/gitlab_quality/test_tooling/test_meta/test_meta_updater.rb +63 -6
- data/lib/gitlab_quality/test_tooling/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b8709476aafe6dc96d2b49d1d74b18b2e3dbf6c37cb8ebb0acd631d05dbfe2ac
|
4
|
+
data.tar.gz: 26d4d54613a522014b5f3b93f38c0cb62027c2e209573727265a5c2b630a6d9d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 194294ffed245e88a96e35480485f98c1748a8417766cdca1cf737ece2b090a4e29efc444fa449f5f569e292333ba50954f7c07a72fbb2f9225c9195c4b6f411
|
7
|
+
data.tar.gz: 30cc7f3a5da1c15b5c3508e0850c007bddf9f22a85c9ec1361821be7bc330afd3869cd9fe74d3b424afc2d0ad2d180c4c0649b5f502f4740ca68f1b2c8119933
|
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
gitlab_quality-test_tooling (1.
|
4
|
+
gitlab_quality-test_tooling (1.17.0)
|
5
5
|
activesupport (>= 6.1, < 7.1)
|
6
6
|
amatch (~> 0.4.1)
|
7
7
|
gitlab (~> 4.19)
|
@@ -318,4 +318,4 @@ DEPENDENCIES
|
|
318
318
|
webmock (= 3.7.0)
|
319
319
|
|
320
320
|
BUNDLED WITH
|
321
|
-
2.4
|
321
|
+
2.5.4
|
data/exe/flaky-test-issues
CHANGED
@@ -19,10 +19,6 @@ options = OptionParser.new do |opts|
|
|
19
19
|
params[:project] = project
|
20
20
|
end
|
21
21
|
|
22
|
-
opts.on('-m', '--merge_request_iid MERGE_REQUEST_IID', String, 'An integer merge request IID') do |merge_request_iid|
|
23
|
-
params[:merge_request_iid] = merge_request_iid
|
24
|
-
end
|
25
|
-
|
26
22
|
opts.on('--base-issue-labels BASE_ISSUE_LABELS', String,
|
27
23
|
'Comma-separated labels (without tilde) to add to new flaky test issues') do |base_issue_labels|
|
28
24
|
params[:base_issue_labels] = base_issue_labels.split(',')
|
@@ -32,6 +28,10 @@ options = OptionParser.new do |opts|
|
|
32
28
|
params[:token] = token
|
33
29
|
end
|
34
30
|
|
31
|
+
opts.on('-r', '--related-issues-file RELATED_ISSUES_FILE', String, 'The file path for the related issues') do |related_issues_file|
|
32
|
+
params[:related_issues_file] = related_issues_file
|
33
|
+
end
|
34
|
+
|
35
35
|
opts.on('--dry-run', "Perform a dry-run (don't create issues)") do
|
36
36
|
params[:dry_run] = true
|
37
37
|
end
|
data/lefthook.yml
CHANGED
@@ -13,3 +13,16 @@ pre-push:
|
|
13
13
|
rubocop:
|
14
14
|
run: bundle exec rubocop
|
15
15
|
glob: '*.rb'
|
16
|
+
|
17
|
+
# Changelog git trailer for the first commit of the branch
|
18
|
+
changelog-on-first-commit:
|
19
|
+
run: |
|
20
|
+
first_commit_message=$(git log --format=%B -n 1 $(git log main..HEAD --pretty=format:"%h" | tail -1))
|
21
|
+
if ! echo ${first_commit_message} | grep "Changelog:"; then
|
22
|
+
echo Could not find a Changelog: git trailer on the first commit for this branch.
|
23
|
+
echo
|
24
|
+
echo Please add a trailer by amending the git commit message.
|
25
|
+
echo
|
26
|
+
echo See https://docs.gitlab.com/ee/development/changelog.html#overview for more info.
|
27
|
+
exit 1
|
28
|
+
fi
|
@@ -15,7 +15,7 @@ module GitlabQuality
|
|
15
15
|
@retry_backoff = 0
|
16
16
|
end
|
17
17
|
|
18
|
-
def handle_gitlab_client_exceptions
|
18
|
+
def handle_gitlab_client_exceptions
|
19
19
|
yield
|
20
20
|
rescue Gitlab::Error::NotFound
|
21
21
|
# This error could be raised in assert_user_permission!
|
@@ -27,12 +27,20 @@ module GitlabQuality
|
|
27
27
|
|
28
28
|
raise if @retry_backoff > RETRY_BACK_OFF_DELAY * MAX_RETRY_ATTEMPTS
|
29
29
|
|
30
|
-
|
30
|
+
warn("#{error.class.name} #{error.message}")
|
31
31
|
warn("Sleeping for #{@retry_backoff} seconds before retrying...")
|
32
32
|
sleep @retry_backoff
|
33
33
|
|
34
34
|
retry
|
35
35
|
rescue StandardError => e
|
36
|
+
post_exception_to_slack(e) if Runtime::Env.ci_commit_ref_name == Runtime::Env.default_branch
|
37
|
+
|
38
|
+
raise e
|
39
|
+
end
|
40
|
+
|
41
|
+
def post_exception_to_slack(error)
|
42
|
+
return unless ENV['CI_SLACK_WEBHOOK_URL']
|
43
|
+
|
36
44
|
pipeline = Runtime::Env.pipeline_from_project_name
|
37
45
|
channel = case pipeline
|
38
46
|
when "canary"
|
@@ -42,9 +50,6 @@ module GitlabQuality
|
|
42
50
|
else
|
43
51
|
"qa-#{pipeline}"
|
44
52
|
end
|
45
|
-
error_msg = warn_exception(e)
|
46
|
-
|
47
|
-
return unless Runtime::Env.ci_commit_ref_name == Runtime::Env.default_branch
|
48
53
|
|
49
54
|
slack_options = {
|
50
55
|
slack_webhook_url: ENV.fetch('CI_SLACK_WEBHOOK_URL', nil),
|
@@ -54,7 +59,7 @@ module GitlabQuality
|
|
54
59
|
message: <<~MSG
|
55
60
|
An unexpected error occurred while reporting test results in issues.
|
56
61
|
The error occurred in job: #{Runtime::Env.ci_job_url}
|
57
|
-
`#{
|
62
|
+
`#{error.class.name} #{error.message}`
|
58
63
|
MSG
|
59
64
|
}
|
60
65
|
puts "Posting Slack message to channel: #{channel}"
|
@@ -79,12 +84,6 @@ module GitlabQuality
|
|
79
84
|
private_token: token
|
80
85
|
)
|
81
86
|
end
|
82
|
-
|
83
|
-
def warn_exception(error)
|
84
|
-
error_msg = "#{error.class.name} #{error.message}"
|
85
|
-
warn(error_msg)
|
86
|
-
error_msg
|
87
|
-
end
|
88
87
|
end
|
89
88
|
end
|
90
89
|
end
|
@@ -48,6 +48,12 @@ module GitlabQuality
|
|
48
48
|
end
|
49
49
|
end
|
50
50
|
|
51
|
+
def find_issue_notes(iid:)
|
52
|
+
handle_gitlab_client_exceptions do
|
53
|
+
client.issue_notes(project, iid, order_by: 'created_at', sort: 'asc').auto_paginate
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
51
57
|
def find_issue_discussions(iid:)
|
52
58
|
handle_gitlab_client_exceptions do
|
53
59
|
client.issue_discussions(project, iid, order_by: 'created_at', sort: 'asc').auto_paginate
|
@@ -75,12 +81,6 @@ module GitlabQuality
|
|
75
81
|
end
|
76
82
|
end
|
77
83
|
|
78
|
-
def find_issue_notes(iid:)
|
79
|
-
handle_gitlab_client_exceptions do
|
80
|
-
client.issue_notes(project, iid, order_by: 'created_at', sort: 'asc')&.auto_paginate
|
81
|
-
end
|
82
|
-
end
|
83
|
-
|
84
84
|
def create_issue_note(iid:, note:)
|
85
85
|
handle_gitlab_client_exceptions do
|
86
86
|
client.create_issue_note(project, iid, note)
|
@@ -10,7 +10,7 @@ module GitlabQuality
|
|
10
10
|
end
|
11
11
|
end
|
12
12
|
|
13
|
-
def create_merge_request(title:, source_branch:, target_branch:, description:, labels:, assignee_id: nil)
|
13
|
+
def create_merge_request(title:, source_branch:, target_branch:, description:, labels:, assignee_id: nil, reviewer_ids: [])
|
14
14
|
attrs = {
|
15
15
|
source_branch: source_branch,
|
16
16
|
target_branch: target_branch,
|
@@ -18,7 +18,8 @@ module GitlabQuality
|
|
18
18
|
labels: labels,
|
19
19
|
assignee_id: assignee_id,
|
20
20
|
squash: true,
|
21
|
-
remove_source_branch: true
|
21
|
+
remove_source_branch: true,
|
22
|
+
reviewer_ids: reviewer_ids
|
22
23
|
}.compact
|
23
24
|
|
24
25
|
merge_request = handle_gitlab_client_exceptions do
|
@@ -26,10 +26,11 @@ module GitlabQuality
|
|
26
26
|
puts "The following note would have been updated id: #{id} with body: #{note} for mr_iid: #{merge_request_iid}"
|
27
27
|
end
|
28
28
|
|
29
|
-
def create_merge_request(title:, source_branch:, target_branch:, description:, labels:, assignee_id:)
|
29
|
+
def create_merge_request(title:, source_branch:, target_branch:, description:, labels:, assignee_id:, reviewer_ids:)
|
30
30
|
puts "A merge request would be created with title: #{title} " \
|
31
31
|
"source_branch: #{source_branch} target_branch: #{target_branch} " \
|
32
|
-
"description: #{description} labels: #{labels}, assignee_id: #{assignee_id}"
|
32
|
+
"description: #{description} labels: #{labels}, assignee_id: #{assignee_id}" \
|
33
|
+
"reviewer_ids: #{reviewer_ids}"
|
33
34
|
end
|
34
35
|
end
|
35
36
|
end
|
@@ -1,13 +1,16 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'active_support/core_ext/object/blank'
|
4
|
+
|
3
5
|
module GitlabQuality
|
4
6
|
module TestTooling
|
5
7
|
module Report
|
6
8
|
module Concerns
|
7
9
|
module IssueReports
|
8
|
-
JOB_URL_REGEX
|
10
|
+
JOB_URL_REGEX = %r{(?<job_url>https://(?<host>[\w.]+)/(?<project_path>[\w\-./]+)/-/jobs/\d+)}
|
9
11
|
FAILED_JOB_DESCRIPTION_REGEX = /First happened in #{JOB_URL_REGEX}\./m
|
10
|
-
REPORT_ITEM_REGEX
|
12
|
+
REPORT_ITEM_REGEX = /^1\. \d{4}-\d{2}-\d{2}: #{JOB_URL_REGEX} \((?<pipeline_url>\S+)\)/
|
13
|
+
LATEST_REPORTS_TO_SHOW = 10
|
11
14
|
|
12
15
|
def initial_reports_section(test)
|
13
16
|
<<~REPORTS
|
@@ -17,22 +20,21 @@ module GitlabQuality
|
|
17
20
|
REPORTS
|
18
21
|
end
|
19
22
|
|
20
|
-
def
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
.scan(REPORT_ITEM_REGEX)
|
29
|
-
.size.to_i + 1
|
30
|
-
issue_description.sub(/^### Reports.*$/, "### Reports (#{reports_count})")
|
31
|
-
else # For issue with the legacy format, we add the Reports section
|
32
|
-
update_legacy_issue_description(issue_description)
|
33
|
-
end
|
23
|
+
def increment_reports(
|
24
|
+
current_reports_content:,
|
25
|
+
test:,
|
26
|
+
reports_section_header: '### Reports',
|
27
|
+
item_extra_content: nil,
|
28
|
+
reports_extra_content: nil)
|
29
|
+
preserved_content = current_reports_content.split(reports_section_header).first&.strip
|
30
|
+
reports = report_lines(current_reports_content) + [report_list_item(test, item_extra_content: item_extra_content)]
|
34
31
|
|
35
|
-
[
|
32
|
+
[
|
33
|
+
preserved_content,
|
34
|
+
"#{reports_section_header} (#{reports.size})",
|
35
|
+
reports_list(reports),
|
36
|
+
reports_extra_content
|
37
|
+
].reject(&:blank?).compact.join("\n\n")
|
36
38
|
end
|
37
39
|
|
38
40
|
def failed_issue_job_url(issue)
|
@@ -49,8 +51,28 @@ module GitlabQuality
|
|
49
51
|
|
50
52
|
private
|
51
53
|
|
52
|
-
def
|
53
|
-
|
54
|
+
def report_lines(content)
|
55
|
+
content.lines.grep(REPORT_ITEM_REGEX).map(&:strip)
|
56
|
+
end
|
57
|
+
|
58
|
+
def reports_list(reports)
|
59
|
+
sorted_reports = reports.sort.reverse
|
60
|
+
|
61
|
+
if sorted_reports.size > LATEST_REPORTS_TO_SHOW
|
62
|
+
[
|
63
|
+
"Last 10 reports:",
|
64
|
+
sorted_reports[...LATEST_REPORTS_TO_SHOW].join("\n"),
|
65
|
+
"<details><summary>See #{sorted_reports.size - LATEST_REPORTS_TO_SHOW} more reports</summary>",
|
66
|
+
sorted_reports[LATEST_REPORTS_TO_SHOW..].join("\n"),
|
67
|
+
"</details>"
|
68
|
+
].join("\n\n")
|
69
|
+
else
|
70
|
+
sorted_reports.join("\n")
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def report_list_item(test, item_extra_content: nil)
|
75
|
+
"1. #{Time.new.utc.strftime('%F')}: #{test.ci_job_url} (#{ENV.fetch('CI_PIPELINE_URL', 'pipeline url is missing')}) #{item_extra_content}".strip
|
54
76
|
end
|
55
77
|
|
56
78
|
def job_urls_from_description(issue_description, regex)
|
@@ -60,15 +82,6 @@ module GitlabQuality
|
|
60
82
|
end
|
61
83
|
end
|
62
84
|
|
63
|
-
def update_legacy_issue_description(issue_description)
|
64
|
-
test_captures = issue_description.scan(JOB_URL_REGEX)
|
65
|
-
reports_count = test_captures.size.to_i + 1
|
66
|
-
|
67
|
-
updated_description = "#{issue_description}\n\n### Reports (#{reports_count})\n"
|
68
|
-
updated_description = [updated_description, *test_captures_to_report_items(test_captures)].join("\n") unless test_captures.empty?
|
69
|
-
updated_description
|
70
|
-
end
|
71
|
-
|
72
85
|
def test_captures_to_report_items(test_captures)
|
73
86
|
test_captures.map do |ci_job_url, _, _|
|
74
87
|
report_list_item(GitlabQuality::TestTooling::TestResult::JsonTestResult.new(
|
@@ -13,11 +13,19 @@ module GitlabQuality
|
|
13
13
|
# - Find issue by test hash
|
14
14
|
# - Reopen issue if it already exists, but is closed
|
15
15
|
class FlakyTestIssue < ReportAsIssue
|
16
|
+
include Concerns::GroupAndCategoryLabels
|
16
17
|
include Concerns::IssueReports
|
17
18
|
|
18
|
-
|
19
|
-
|
20
|
-
|
19
|
+
IDENTITY_LABELS = ['test', 'failure::flaky-test', 'automation:bot-authored'].freeze
|
20
|
+
NEW_ISSUE_LABELS = Set.new(['type::maintenance', 'priority::3', 'severity::3', *IDENTITY_LABELS]).freeze
|
21
|
+
SEARCH_LABELS = ['test'].freeze
|
22
|
+
FOUND_IN_MR_LABEL = '~"found:in MR"'
|
23
|
+
FOUND_IN_MASTER_LABEL = '~"found:master"'
|
24
|
+
REPORT_SECTION_HEADER = '### Flakiness reports'
|
25
|
+
REPORTS_DOCUMENTATION = <<~DOC
|
26
|
+
Flaky tests were detected, please refer to the [Flaky tests documentation](https://docs.gitlab.com/ee/development/testing_guide/flaky_tests.html)
|
27
|
+
to learn more about how to reproduce them.
|
28
|
+
DOC
|
21
29
|
|
22
30
|
def initialize(
|
23
31
|
token:,
|
@@ -25,18 +33,16 @@ module GitlabQuality
|
|
25
33
|
base_issue_labels: nil,
|
26
34
|
confidential: false,
|
27
35
|
dry_run: false,
|
28
|
-
merge_request_iid: nil,
|
29
36
|
project: nil,
|
30
37
|
**_kwargs)
|
31
38
|
super(token: token, input_files: input_files, project: project, confidential: confidential, dry_run: dry_run)
|
32
39
|
|
33
40
|
@base_issue_labels = Set.new(base_issue_labels)
|
34
|
-
@merge_request_iid = merge_request_iid
|
35
41
|
end
|
36
42
|
|
37
43
|
private
|
38
44
|
|
39
|
-
attr_reader :base_issue_labels
|
45
|
+
attr_reader :base_issue_labels
|
40
46
|
|
41
47
|
def run!
|
42
48
|
puts "Reporting flaky tests in `#{files.join(',')}` as issues in project `#{project}` via the API at `#{Runtime::Env.gitlab_api_base}`."
|
@@ -44,62 +50,91 @@ module GitlabQuality
|
|
44
50
|
TestResults::Builder.new(files).test_results_per_file do |test_results|
|
45
51
|
puts "=> Reporting #{test_results.count} tests in #{test_results.path}"
|
46
52
|
|
47
|
-
test_results
|
48
|
-
next if test.status != 'passed' # We only want failed tests that passed in the end
|
49
|
-
|
50
|
-
create_flaky_issue(test)
|
51
|
-
end
|
53
|
+
process_test_results(test_results)
|
52
54
|
end
|
53
55
|
end
|
54
56
|
|
55
|
-
def
|
56
|
-
|
57
|
-
|
57
|
+
def process_test_results(test_results)
|
58
|
+
test_results.each do |test|
|
59
|
+
next unless test_is_applicable?(test)
|
58
60
|
|
59
|
-
|
60
|
-
found_label =
|
61
|
-
if !merge_request_iid || merge_request_iid.empty?
|
62
|
-
FOUND_IN_MASTER_LABEL
|
63
|
-
else
|
64
|
-
FOUND_IN_MR_LABEL
|
65
|
-
end
|
61
|
+
puts " => Reporting flakiness for test '#{test.name}'..."
|
66
62
|
|
67
|
-
|
63
|
+
issues = find_issues_by_hash(test_hash(test), state: 'opened', labels: SEARCH_LABELS)
|
64
|
+
issues << create_issue(test) if issues.empty?
|
65
|
+
|
66
|
+
update_reports(issues, test)
|
67
|
+
collect_issues(test, issues)
|
68
|
+
end
|
68
69
|
end
|
69
70
|
|
70
|
-
def
|
71
|
-
|
72
|
-
"Flaky tests were detected, please refer to the [Flaky tests documentation](https://docs.gitlab.com/ee/development/testing_guide/flaky_tests.html) " \
|
73
|
-
"to learn more about how to reproduce them.",
|
74
|
-
initial_reports_section(test)
|
75
|
-
].compact.join("\n\n")
|
71
|
+
def test_is_applicable?(test)
|
72
|
+
test.status == 'passed' # We only want failed tests that passed in the end
|
76
73
|
end
|
77
74
|
|
78
|
-
def
|
79
|
-
|
75
|
+
def up_to_date_labels(test:, issue: nil, new_labels: Set.new)
|
76
|
+
(base_issue_labels + super).to_a
|
77
|
+
end
|
80
78
|
|
81
|
-
|
79
|
+
def update_reports(issues, test)
|
82
80
|
issues.each do |issue|
|
83
|
-
puts " =>
|
81
|
+
puts " => Adding the flaky test to the existing issue: #{issue.web_url}"
|
82
|
+
add_report_to_issue(issue: issue, test: test, related_issues: (issues - [issue]))
|
83
|
+
end
|
84
|
+
end
|
84
85
|
|
85
|
-
|
86
|
-
|
86
|
+
def add_report_to_issue(issue:, test:, related_issues:)
|
87
|
+
reports_note = existing_reports_note(issue: issue)
|
88
|
+
note_body = [
|
89
|
+
report_body(reports_note: reports_note, test: test),
|
90
|
+
identity_labels_quick_action,
|
91
|
+
relate_issues_quick_actions(related_issues)
|
92
|
+
].join("\n")
|
93
|
+
|
94
|
+
if reports_note
|
95
|
+
gitlab.edit_issue_note(
|
96
|
+
issue_iid: issue.iid,
|
97
|
+
note_id: reports_note.id,
|
98
|
+
note: note_body
|
99
|
+
)
|
100
|
+
else
|
101
|
+
gitlab.create_issue_note(iid: issue.iid, note: note_body)
|
102
|
+
end
|
103
|
+
end
|
87
104
|
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
end
|
105
|
+
def existing_reports_note(issue:)
|
106
|
+
gitlab.find_issue_notes(iid: issue.iid).find do |note|
|
107
|
+
note.body.start_with?(REPORT_SECTION_HEADER)
|
92
108
|
end
|
109
|
+
end
|
93
110
|
|
94
|
-
|
111
|
+
def report_body(reports_note:, test:)
|
112
|
+
increment_reports(
|
113
|
+
current_reports_content: reports_note&.body.to_s,
|
114
|
+
test: test,
|
115
|
+
reports_section_header: REPORT_SECTION_HEADER,
|
116
|
+
item_extra_content: found_label,
|
117
|
+
reports_extra_content: REPORTS_DOCUMENTATION
|
118
|
+
)
|
119
|
+
end
|
120
|
+
|
121
|
+
def found_label
|
122
|
+
if ENV.key?('CI_MERGE_REQUEST_IID')
|
123
|
+
FOUND_IN_MR_LABEL
|
124
|
+
else
|
125
|
+
FOUND_IN_MASTER_LABEL
|
126
|
+
end
|
95
127
|
end
|
96
128
|
|
97
|
-
def
|
98
|
-
|
129
|
+
def identity_labels_quick_action
|
130
|
+
labels_list = IDENTITY_LABELS.map { |label| %(~"#{label}") }.join(' ')
|
131
|
+
%(/label #{labels_list})
|
99
132
|
end
|
100
133
|
|
101
|
-
def
|
102
|
-
|
134
|
+
def relate_issues_quick_actions(issues)
|
135
|
+
issues.map do |issue|
|
136
|
+
"/relate #{issue.web_url}"
|
137
|
+
end.join("\n")
|
103
138
|
end
|
104
139
|
end
|
105
140
|
end
|
@@ -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
|
@@ -99,7 +99,6 @@ module GitlabQuality
|
|
99
99
|
end
|
100
100
|
|
101
101
|
def update_issue(issue:, spec_run_time:)
|
102
|
-
state_event = issue.state == 'closed' ? 'reopen' : nil
|
103
102
|
updated_description = <<~MARKDOWN.chomp
|
104
103
|
#{issue.description}
|
105
104
|
|
@@ -110,8 +109,6 @@ module GitlabQuality
|
|
110
109
|
description: updated_description
|
111
110
|
}
|
112
111
|
|
113
|
-
issue_attrs[:state_event] = state_event if state_event
|
114
|
-
|
115
112
|
gitlab.edit_issue(iid: issue.iid, options: issue_attrs)
|
116
113
|
puts " => Added a report in #{issue.web_url}!"
|
117
114
|
end
|
@@ -190,8 +190,8 @@ module GitlabQuality
|
|
190
190
|
end
|
191
191
|
|
192
192
|
def failure_issues(test)
|
193
|
-
|
194
|
-
test,
|
193
|
+
find_issues_by_hash(
|
194
|
+
test_hash(test),
|
195
195
|
state: 'opened',
|
196
196
|
labels: base_issue_labels + Set.new(%w[test]),
|
197
197
|
not_labels: exclude_labels_for_search
|
@@ -268,7 +268,7 @@ module GitlabQuality
|
|
268
268
|
if stacktrace_match
|
269
269
|
stacktrace_match[:stacktrace].gsub(/^\s*#.*$/, '').gsub(/^[[:space:]]+/, '').strip
|
270
270
|
else
|
271
|
-
puts " => [DEBUG] Stacktrace doesn't match the regex (#{regex})
|
271
|
+
puts " => [DEBUG] Stacktrace doesn't match the regex (#{regex})!"
|
272
272
|
end
|
273
273
|
end
|
274
274
|
|
@@ -347,7 +347,7 @@ module GitlabQuality
|
|
347
347
|
state_event = issue.state == 'closed' ? 'reopen' : nil
|
348
348
|
|
349
349
|
issue_attrs = {
|
350
|
-
description:
|
350
|
+
description: increment_reports(current_reports_content: issue.description, test: test),
|
351
351
|
labels: up_to_date_labels(test: test, issue: issue)
|
352
352
|
}
|
353
353
|
issue_attrs[:state_event] = state_event if state_event
|
@@ -356,10 +356,6 @@ module GitlabQuality
|
|
356
356
|
puts " => Added a report in '#{issue.title}': #{issue.web_url}!"
|
357
357
|
end
|
358
358
|
|
359
|
-
def new_issue_title(test)
|
360
|
-
"Failure in #{super}"
|
361
|
-
end
|
362
|
-
|
363
359
|
def screenshot_section(test)
|
364
360
|
return unless test.screenshot?
|
365
361
|
|
@@ -60,7 +60,7 @@ module GitlabQuality
|
|
60
60
|
| Description | `#{test.name}` |
|
61
61
|
| Test level | #{test.level} |
|
62
62
|
| Hash | `#{test_hash(test)}` |
|
63
|
-
|
|
63
|
+
| Reference duration | #{test.run_time} seconds |
|
64
64
|
| Expected duration | < #{test.max_duration_for_test} seconds |
|
65
65
|
#{"| Test case | #{test.testcase} |" if test.testcase}
|
66
66
|
DESCRIPTION
|
@@ -142,8 +142,10 @@ module GitlabQuality
|
|
142
142
|
labels
|
143
143
|
end
|
144
144
|
|
145
|
-
def find_issues_by_hash(test_hash)
|
146
|
-
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'
|
147
149
|
gitlab.find_issues(options: search_options)
|
148
150
|
end
|
149
151
|
|
@@ -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 TestTooling::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,99 +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
39
|
end
|
37
40
|
|
38
|
-
def
|
39
|
-
|
40
|
-
|
41
|
+
def process_test_results(test_results)
|
42
|
+
test_results.each do |test|
|
43
|
+
next unless test.slow_test?
|
41
44
|
|
42
|
-
|
43
|
-
super +
|
44
|
-
<<~DESCRIPTION.chomp
|
45
|
-
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)
|
46
|
-
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}'..."
|
47
46
|
|
48
|
-
|
47
|
+
issues = find_issues_by_hash(test_hash(test), state: 'opened', labels: SEARCH_LABELS)
|
48
|
+
issues << create_issue(test) if issues.empty?
|
49
49
|
|
50
|
-
|
51
|
-
|
50
|
+
update_reports(issues, test)
|
51
|
+
collect_issues(test, issues)
|
52
|
+
end
|
52
53
|
end
|
53
54
|
|
54
|
-
def
|
55
|
-
|
56
|
-
### Reports (1)
|
57
|
-
|
58
|
-
#{report_list_item(test)}
|
59
|
-
REPORTS
|
55
|
+
def test_is_applicable?(test)
|
56
|
+
test.slow_test?
|
60
57
|
end
|
61
58
|
|
62
|
-
def
|
63
|
-
|
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
|
64
64
|
end
|
65
65
|
|
66
|
-
def
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
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
|
72
83
|
end
|
73
84
|
|
74
|
-
def
|
75
|
-
|
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
|
76
90
|
|
77
|
-
|
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
|
78
100
|
|
79
|
-
|
80
|
-
|
101
|
+
def found_label
|
102
|
+
if ENV.key?('CI_MERGE_REQUEST_IID')
|
103
|
+
FOUND_IN_MR_LABEL
|
81
104
|
else
|
82
|
-
|
83
|
-
puts " => Existing issue link #{issue['web_url']}"
|
84
|
-
|
85
|
-
update_reports(issue, test)
|
86
|
-
end
|
105
|
+
FOUND_IN_MASTER_LABEL
|
87
106
|
end
|
88
|
-
|
89
|
-
collect_issues(test, issues)
|
90
|
-
rescue MultipleIssuesFound => e
|
91
|
-
warn(e.message)
|
92
107
|
end
|
93
108
|
|
94
|
-
def
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
issue_attrs = {
|
99
|
-
description: up_to_date_issue_description(issue.description, test)
|
100
|
-
}
|
101
|
-
|
102
|
-
issue_attrs[:state_event] = state_event if state_event
|
103
|
-
|
104
|
-
gitlab.edit_issue(iid: issue.iid, options: issue_attrs)
|
105
|
-
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})
|
106
112
|
end
|
107
113
|
|
108
|
-
def
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
reports_count = issue_description
|
113
|
-
.scan(REPORT_ITEM_REGEX)
|
114
|
-
.size.to_i + 1
|
115
|
-
issue_description.sub(/^### Reports.*$/, "### Reports (#{reports_count})")
|
116
|
-
else # For issue with the legacy format, we add the Reports section
|
117
|
-
reports_count = issue_description
|
118
|
-
.scan(JOB_URL_REGEX)
|
119
|
-
.size.to_i + 1
|
120
|
-
|
121
|
-
"#{issue_description}\n\n### Reports (#{reports_count})"
|
122
|
-
end
|
123
|
-
|
124
|
-
"#{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")
|
125
118
|
end
|
126
119
|
end
|
127
120
|
end
|
@@ -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
|
@@ -14,41 +14,48 @@ module GitlabQuality
|
|
14
14
|
# @param [TestMetaUpdater] context instance of TestMetaUpdater
|
15
15
|
def execute(spec, context) # rubocop:disable Metrics/AbcSize
|
16
16
|
@context = context
|
17
|
-
|
17
|
+
@existing_mrs = nil
|
18
18
|
@file_path = spec["file_path"]
|
19
|
+
testcase = spec["testcase"]
|
19
20
|
devops_stage = spec["stage"]
|
20
21
|
product_group = spec["product_group"]
|
21
22
|
@example_name = spec["name"]
|
22
|
-
@mr_title = format("%{prefix} %{example_name}", prefix: '[PROMOTE TO BLOCKING]', example_name: example_name)
|
23
|
-
|
24
|
-
return unless proceed_with_merge_request?
|
23
|
+
@mr_title = format("%{prefix} %{example_name}", prefix: '[PROMOTE TO BLOCKING]', example_name: example_name).truncate(72, omission: '')
|
25
24
|
|
26
25
|
@file_contents = context.get_file_contents(file_path)
|
27
26
|
|
28
|
-
new_content, changed_line_no = add_blocking_metadata
|
27
|
+
new_content, @changed_line_no = add_blocking_metadata
|
29
28
|
|
30
|
-
return
|
29
|
+
return unless proceed_with_merge_request?
|
31
30
|
|
32
31
|
branch = context.create_branch("blocking-promotion-#{SecureRandom.hex(4)}", example_name, context.ref)
|
33
32
|
|
34
33
|
context.commit_changes(branch, <<~COMMIT_MESSAGE, file_path, new_content)
|
35
34
|
Promote end-to-end test to blocking
|
36
35
|
|
37
|
-
Promote to blocking: #{example_name}
|
36
|
+
#{"Promote to blocking: #{example_name}".truncate(72)}
|
38
37
|
COMMIT_MESSAGE
|
39
38
|
|
40
|
-
|
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)
|
41
42
|
|
42
|
-
merge_request = context.create_merge_request(mr_title, branch,
|
43
|
+
merge_request = context.create_merge_request(mr_title, branch, gitlab_bot_user_id, [reviewer_id]) do
|
43
44
|
<<~MARKDOWN
|
44
45
|
## What does this MR do?
|
45
46
|
|
46
47
|
Promotes the test [`#{example_name}`](https://gitlab.com/#{context.project}/-/blob/#{context.ref}/#{file_path}#L#{changed_line_no + 1})
|
47
48
|
to the blocking bucket
|
48
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)})
|
49
55
|
|
50
56
|
/label ~"Quality" ~"QA" ~"type::maintenance"
|
51
57
|
/label ~"devops::#{devops_stage}"
|
58
|
+
#{context.label_from_product_group(product_group)}
|
52
59
|
|
53
60
|
<div align="center">
|
54
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,6 +69,11 @@ module GitlabQuality
|
|
62
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}
|
63
70
|
MARKDOWN
|
64
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
|
+
|
65
77
|
merge_request
|
66
78
|
end
|
67
79
|
|
@@ -82,15 +94,20 @@ module GitlabQuality
|
|
82
94
|
|
83
95
|
private
|
84
96
|
|
85
|
-
attr_reader :context, :file_path, :file_contents, :example_name, :mr_title
|
97
|
+
attr_reader :context, :file_path, :file_contents, :example_name, :mr_title, :changed_line_no
|
86
98
|
|
87
99
|
# Checks if there is already an MR open
|
88
100
|
#
|
89
101
|
# @return [Boolean]
|
90
|
-
def proceed_with_merge_request?
|
91
|
-
|
92
|
-
|
93
|
-
|
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.")
|
94
111
|
return false
|
95
112
|
end
|
96
113
|
|
@@ -109,8 +126,8 @@ module GitlabQuality
|
|
109
126
|
end
|
110
127
|
|
111
128
|
context.update_matched_line(matched_lines.last, file_contents.dup) do |line|
|
112
|
-
if line.include?(',')
|
113
|
-
line[line.index(',')] = format(BLOCKING_METADATA, suffix: ',')
|
129
|
+
if line.sub(DESCRIPTION_REGEX, '').include?(',')
|
130
|
+
line[line.index(',', end_of_description_index(line))] = format(BLOCKING_METADATA, suffix: ',')
|
114
131
|
else
|
115
132
|
line[line.rindex(' ')] = format(BLOCKING_METADATA, suffix: ' ')
|
116
133
|
end
|
@@ -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.
|
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,
|
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
|
-
|
141
|
+
Runtime::Logger.info(" Failure issue '#{failure_issue_url}' is closed. Will not proceed with creating MR.")
|
125
142
|
return false
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
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.
|
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
|
@@ -179,7 +193,15 @@ module GitlabQuality
|
|
179
193
|
def fetch_dri_id(product_group, devops_stage)
|
180
194
|
assignee_handle = ENV.fetch('QA_TEST_DRI_HANDLE', nil) || set_dri_via_group(product_group, devops_stage)
|
181
195
|
|
182
|
-
[
|
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)
|
183
205
|
end
|
184
206
|
|
185
207
|
# Post a message on Slack
|
@@ -221,6 +243,34 @@ module GitlabQuality
|
|
221
243
|
merge_request_client.find(options: { search: title, in: 'title', state: 'opened' })
|
222
244
|
end
|
223
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
|
+
|
224
274
|
private
|
225
275
|
|
226
276
|
attr_reader :token, :specs_file, :dry_run, :processor
|
@@ -250,6 +300,13 @@ module GitlabQuality
|
|
250
300
|
project: project
|
251
301
|
)
|
252
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
|
253
310
|
end
|
254
311
|
end
|
255
312
|
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.
|
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
|
11
|
+
date: 2024-03-01 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: climate_control
|