gitlab_quality-test_tooling 1.17.0 → 1.21.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 +16 -6
- data/exe/generate-test-session +4 -0
- data/lefthook.yml +2 -1
- data/lib/gitlab_quality/test_tooling/concerns/find_set_dri.rb +22 -9
- data/lib/gitlab_quality/test_tooling/gitlab_client/gitlab_client.rb +1 -1
- data/lib/gitlab_quality/test_tooling/gitlab_client/repository_files_client.rb +4 -3
- data/lib/gitlab_quality/test_tooling/report/generate_test_session.rb +25 -15
- data/lib/gitlab_quality/test_tooling/report/relate_failure_issue.rb +2 -4
- data/lib/gitlab_quality/test_tooling/report/report_as_issue.rb +0 -1
- data/lib/gitlab_quality/test_tooling/report/slow_test_issue.rb +1 -1
- data/lib/gitlab_quality/test_tooling/runtime/logger.rb +1 -1
- data/lib/gitlab_quality/test_tooling/test_meta/processor/add_to_blocking_processor.rb +73 -71
- data/lib/gitlab_quality/test_tooling/test_meta/processor/add_to_quarantine_processor.rb +136 -92
- data/lib/gitlab_quality/test_tooling/test_meta/processor/meta_processor.rb +63 -4
- data/lib/gitlab_quality/test_tooling/test_meta/test_meta_updater.rb +80 -33
- data/lib/gitlab_quality/test_tooling/test_result/base_test_result.rb +4 -0
- data/lib/gitlab_quality/test_tooling/test_result/json_test_result.rb +4 -0
- data/lib/gitlab_quality/test_tooling/version.rb +1 -1
- metadata +4 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9549372e9bad8839b0f8ff6a0ca1646579d244cc596d74fbbdc1234bd1e5a176
|
|
4
|
+
data.tar.gz: 065ee0af0ea8e14fcf4719ede74ab1a5950c05f17caef3e070832bd4695089ae
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c8e9cbbef843556bd9cbc6f510223627712fbe035ade98e7ce005f13dfe7082668d42a7018857d7015d3236aa970d0b049e5fdfffc9829b0021c0c5cabeb6297
|
|
7
|
+
data.tar.gz: bce2b74fe864a48a6ab3155595431e81f08cf3ae5c0069612165885b96819492a4258bb613fbd01ea35e51962c12f582be334e6554cfaf877bae617dcd690e5e
|
data/Gemfile.lock
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
gitlab_quality-test_tooling (1.
|
|
5
|
-
activesupport (>= 6.1, < 7.
|
|
4
|
+
gitlab_quality-test_tooling (1.21.0)
|
|
5
|
+
activesupport (>= 6.1, < 7.2)
|
|
6
6
|
amatch (~> 0.4.1)
|
|
7
7
|
gitlab (~> 4.19)
|
|
8
8
|
http (~> 5.0)
|
|
@@ -16,10 +16,15 @@ PATH
|
|
|
16
16
|
GEM
|
|
17
17
|
remote: https://rubygems.org/
|
|
18
18
|
specs:
|
|
19
|
-
activesupport (7.
|
|
19
|
+
activesupport (7.1.3.2)
|
|
20
|
+
base64
|
|
21
|
+
bigdecimal
|
|
20
22
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
|
23
|
+
connection_pool (>= 2.2.5)
|
|
24
|
+
drb
|
|
21
25
|
i18n (>= 1.6, < 2)
|
|
22
26
|
minitest (>= 5.1)
|
|
27
|
+
mutex_m
|
|
23
28
|
tzinfo (~> 2.0)
|
|
24
29
|
addressable (2.8.6)
|
|
25
30
|
public_suffix (>= 2.0.2, < 6.0)
|
|
@@ -28,7 +33,9 @@ GEM
|
|
|
28
33
|
tins (~> 1.0)
|
|
29
34
|
ast (2.4.2)
|
|
30
35
|
backport (1.2.0)
|
|
36
|
+
base64 (0.2.0)
|
|
31
37
|
benchmark (0.3.0)
|
|
38
|
+
bigdecimal (3.1.6)
|
|
32
39
|
binding_of_caller (1.0.0)
|
|
33
40
|
debug_inspector (>= 0.0.1)
|
|
34
41
|
byebug (11.1.3)
|
|
@@ -41,6 +48,7 @@ GEM
|
|
|
41
48
|
coderay (1.1.3)
|
|
42
49
|
colored2 (3.1.2)
|
|
43
50
|
concurrent-ruby (1.2.3)
|
|
51
|
+
connection_pool (2.4.1)
|
|
44
52
|
cork (0.3.0)
|
|
45
53
|
colored2 (~> 3.1)
|
|
46
54
|
crack (0.4.5)
|
|
@@ -65,6 +73,7 @@ GEM
|
|
|
65
73
|
diff-lcs (1.5.0)
|
|
66
74
|
docile (1.4.0)
|
|
67
75
|
domain_name (0.6.20240107)
|
|
76
|
+
drb (2.2.1)
|
|
68
77
|
e2mmap (0.1.0)
|
|
69
78
|
faraday (2.9.0)
|
|
70
79
|
faraday-net_http (>= 2.0, < 3.2)
|
|
@@ -119,7 +128,7 @@ GEM
|
|
|
119
128
|
httparty (0.21.0)
|
|
120
129
|
mini_mime (>= 1.0.0)
|
|
121
130
|
multi_xml (>= 0.5.2)
|
|
122
|
-
i18n (1.14.
|
|
131
|
+
i18n (1.14.4)
|
|
123
132
|
concurrent-ruby (~> 1.0)
|
|
124
133
|
jaro_winkler (1.5.6)
|
|
125
134
|
json (2.7.1)
|
|
@@ -138,10 +147,11 @@ GEM
|
|
|
138
147
|
method_source (1.0.0)
|
|
139
148
|
mini_mime (1.1.5)
|
|
140
149
|
mini_portile2 (2.8.5)
|
|
141
|
-
minitest (5.
|
|
150
|
+
minitest (5.22.2)
|
|
142
151
|
mize (0.4.1)
|
|
143
152
|
protocol (~> 2.0)
|
|
144
153
|
multi_xml (0.6.0)
|
|
154
|
+
mutex_m (0.2.0)
|
|
145
155
|
nap (1.1.0)
|
|
146
156
|
nenv (0.3.0)
|
|
147
157
|
net-http (0.4.1)
|
|
@@ -318,4 +328,4 @@ DEPENDENCIES
|
|
|
318
328
|
webmock (= 3.7.0)
|
|
319
329
|
|
|
320
330
|
BUNDLED WITH
|
|
321
|
-
2.5.
|
|
331
|
+
2.5.6
|
data/exe/generate-test-session
CHANGED
|
@@ -31,6 +31,10 @@ options = OptionParser.new do |opts|
|
|
|
31
31
|
params[:issue_url_file] = issue_url_file
|
|
32
32
|
end
|
|
33
33
|
|
|
34
|
+
opts.on('--pipeline-stages STAGES', String, 'Comma-separated list of pipeline stages to include in test session issue') do |pipeline_stages|
|
|
35
|
+
params[:pipeline_stages] = pipeline_stages.split(',')
|
|
36
|
+
end
|
|
37
|
+
|
|
34
38
|
opts.on('--confidential', "Makes test session issue confidential") do
|
|
35
39
|
params[:confidential] = true
|
|
36
40
|
end
|
data/lefthook.yml
CHANGED
|
@@ -17,7 +17,8 @@ pre-push:
|
|
|
17
17
|
# Changelog git trailer for the first commit of the branch
|
|
18
18
|
changelog-on-first-commit:
|
|
19
19
|
run: |
|
|
20
|
-
|
|
20
|
+
git fetch origin main
|
|
21
|
+
first_commit_message=$(git log --format=%B -n 1 $(git log origin/main..HEAD --pretty=format:"%h" | tail -1))
|
|
21
22
|
if ! echo ${first_commit_message} | grep "Changelog:"; then
|
|
22
23
|
echo Could not find a Changelog: git trailer on the first commit for this branch.
|
|
23
24
|
echo
|
|
@@ -6,19 +6,23 @@ module GitlabQuality
|
|
|
6
6
|
module TestTooling
|
|
7
7
|
module Concerns
|
|
8
8
|
module FindSetDri
|
|
9
|
-
def
|
|
9
|
+
def test_dri(product_group, stage, section)
|
|
10
10
|
parse_json_with_sets
|
|
11
|
+
fetch_section_sets(section)
|
|
11
12
|
fetch_stage_sets(stage)
|
|
12
|
-
|
|
13
|
-
return @sets.sample['username'] if @stage_sets.empty?
|
|
14
|
-
|
|
15
13
|
fetch_group_sets(product_group)
|
|
16
14
|
|
|
17
|
-
if @group_sets.
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
15
|
+
set_dris = if @group_sets.any?
|
|
16
|
+
@group_sets
|
|
17
|
+
elsif @stage_sets.any?
|
|
18
|
+
@stage_sets
|
|
19
|
+
elsif @section_sets.any?
|
|
20
|
+
@section_sets
|
|
21
|
+
else
|
|
22
|
+
@sets
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
set_dris.sample['username']
|
|
22
26
|
end
|
|
23
27
|
|
|
24
28
|
private
|
|
@@ -30,6 +34,15 @@ module GitlabQuality
|
|
|
30
34
|
@sets = JSON.parse(response.body).select { |user| user['role'].include?('software-engineer-in-test') }
|
|
31
35
|
end
|
|
32
36
|
|
|
37
|
+
def fetch_section_sets(section)
|
|
38
|
+
@section_sets = []
|
|
39
|
+
return if section.nil?
|
|
40
|
+
|
|
41
|
+
@section_sets = @sets.select do |user|
|
|
42
|
+
user['role'].include?(section.split("_").map(&:capitalize).join(" "))
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
33
46
|
def fetch_stage_sets(stage)
|
|
34
47
|
@stage_sets = @sets.select do |user|
|
|
35
48
|
user['role'].include?(stage.split("_").map(&:capitalize).join(" "))
|
|
@@ -27,7 +27,7 @@ module GitlabQuality
|
|
|
27
27
|
|
|
28
28
|
raise if @retry_backoff > RETRY_BACK_OFF_DELAY * MAX_RETRY_ATTEMPTS
|
|
29
29
|
|
|
30
|
-
warn("#{
|
|
30
|
+
warn("#{e.class.name} #{e.message}")
|
|
31
31
|
warn("Sleeping for #{@retry_backoff} seconds before retrying...")
|
|
32
32
|
sleep @retry_backoff
|
|
33
33
|
|
|
@@ -4,17 +4,18 @@ module GitlabQuality
|
|
|
4
4
|
module TestTooling
|
|
5
5
|
module GitlabClient
|
|
6
6
|
class RepositoryFilesClient < GitlabClient
|
|
7
|
-
attr_reader :file_path
|
|
7
|
+
attr_reader :file_path, :ref
|
|
8
8
|
|
|
9
|
-
def initialize(file_path:, **kwargs)
|
|
9
|
+
def initialize(file_path:, ref:, **kwargs)
|
|
10
10
|
@file_path = file_path
|
|
11
|
+
@ref = ref
|
|
11
12
|
|
|
12
13
|
super
|
|
13
14
|
end
|
|
14
15
|
|
|
15
16
|
def file_contents
|
|
16
17
|
handle_gitlab_client_exceptions do
|
|
17
|
-
client.file_contents(project, file_path.gsub(%r{^/}, ""))
|
|
18
|
+
client.file_contents(project, file_path.gsub(%r{^/}, ""), ref)
|
|
18
19
|
end
|
|
19
20
|
end
|
|
20
21
|
end
|
|
@@ -7,15 +7,16 @@ module GitlabQuality
|
|
|
7
7
|
module TestTooling
|
|
8
8
|
module Report
|
|
9
9
|
class GenerateTestSession < ReportAsIssue
|
|
10
|
-
def initialize(ci_project_token:, **kwargs)
|
|
10
|
+
def initialize(ci_project_token:, pipeline_stages: nil, **kwargs)
|
|
11
11
|
super
|
|
12
12
|
@ci_project_token = ci_project_token
|
|
13
|
+
@pipeline_stages = Set.new(pipeline_stages)
|
|
13
14
|
@issue_type = 'issue'
|
|
14
15
|
end
|
|
15
16
|
|
|
16
17
|
private
|
|
17
18
|
|
|
18
|
-
attr_reader :ci_project_token
|
|
19
|
+
attr_reader :ci_project_token, :pipeline_stages
|
|
19
20
|
|
|
20
21
|
# rubocop:disable Metrics/AbcSize
|
|
21
22
|
def run!
|
|
@@ -27,6 +28,8 @@ module GitlabQuality
|
|
|
27
28
|
TestResults::JsonTestResults.new(path).to_a
|
|
28
29
|
end
|
|
29
30
|
|
|
31
|
+
tests = tests.select { |test| pipeline_stages.include? test.report["stage"] } unless pipeline_stages.empty?
|
|
32
|
+
|
|
30
33
|
issue = gitlab.create_issue(
|
|
31
34
|
title: "#{Time.now.strftime('%Y-%m-%d')} Test session report | #{Runtime::Env.qa_run_type}",
|
|
32
35
|
description: generate_description(tests),
|
|
@@ -80,20 +83,10 @@ module GitlabQuality
|
|
|
80
83
|
end
|
|
81
84
|
|
|
82
85
|
def generate_failed_jobs_listing
|
|
83
|
-
failed_jobs =
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
endpoint: Runtime::Env.ci_api_v4_url,
|
|
87
|
-
private_token: ci_project_token)
|
|
88
|
-
|
|
89
|
-
gitlab.handle_gitlab_client_exceptions do
|
|
90
|
-
failed_jobs = ci_project_client.pipeline_jobs(
|
|
91
|
-
Runtime::Env.ci_project_id,
|
|
92
|
-
Runtime::Env.ci_pipeline_id,
|
|
93
|
-
scope: 'failed')
|
|
94
|
-
end
|
|
86
|
+
failed_jobs = fetch_pipeline_failed_jobs
|
|
87
|
+
listings = failed_jobs.filter_map do |job|
|
|
88
|
+
next if pipeline_stages.any? && !pipeline_stages.include?(job.stage)
|
|
95
89
|
|
|
96
|
-
listings = failed_jobs.map do |job|
|
|
97
90
|
allowed_to_fail = ' (allowed to fail)' if job.allow_failure
|
|
98
91
|
|
|
99
92
|
"* [#{job.name}](#{job.web_url})#{allowed_to_fail}"
|
|
@@ -272,6 +265,23 @@ module GitlabQuality
|
|
|
272
265
|
* https://dashboards.quality.gitlab.net/d/tR_SmBDVk/main-runs?orgId=1&refresh=1m&var-run_type=#{Runtime::Env.qa_run_type}
|
|
273
266
|
MARKDOWN
|
|
274
267
|
end
|
|
268
|
+
|
|
269
|
+
def fetch_pipeline_failed_jobs
|
|
270
|
+
failed_jobs = []
|
|
271
|
+
|
|
272
|
+
ci_project_client = Gitlab.client(
|
|
273
|
+
endpoint: Runtime::Env.ci_api_v4_url,
|
|
274
|
+
private_token: ci_project_token)
|
|
275
|
+
|
|
276
|
+
gitlab.handle_gitlab_client_exceptions do
|
|
277
|
+
failed_jobs = ci_project_client.pipeline_jobs(
|
|
278
|
+
Runtime::Env.ci_project_id,
|
|
279
|
+
Runtime::Env.ci_pipeline_id,
|
|
280
|
+
scope: 'failed')
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
failed_jobs
|
|
284
|
+
end
|
|
275
285
|
end
|
|
276
286
|
end
|
|
277
287
|
end
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'nokogiri'
|
|
4
|
-
require 'active_support/core_ext/enumerable'
|
|
5
4
|
require 'rubygems/text'
|
|
6
|
-
require 'active_support/core_ext/integer/time'
|
|
7
5
|
require 'amatch'
|
|
8
6
|
|
|
9
7
|
module GitlabQuality
|
|
@@ -330,7 +328,7 @@ module GitlabQuality
|
|
|
330
328
|
def new_issue_assignee_id(test)
|
|
331
329
|
return unless test.product_group?
|
|
332
330
|
|
|
333
|
-
dri =
|
|
331
|
+
dri = test_dri(test.product_group, test.stage, test.section)
|
|
334
332
|
puts " => Assigning #{dri} as DRI for the issue."
|
|
335
333
|
|
|
336
334
|
gitlab.find_user_id(username: dri)
|
|
@@ -339,7 +337,7 @@ module GitlabQuality
|
|
|
339
337
|
def new_issue_due_date(test)
|
|
340
338
|
return unless test.product_group?
|
|
341
339
|
|
|
342
|
-
Date.today
|
|
340
|
+
Date.today.next_month
|
|
343
341
|
end
|
|
344
342
|
|
|
345
343
|
def update_reports(issue, test)
|
|
@@ -60,7 +60,6 @@ module GitlabQuality
|
|
|
60
60
|
| Description | `#{test.name}` |
|
|
61
61
|
| Test level | #{test.level} |
|
|
62
62
|
| Hash | `#{test_hash(test)}` |
|
|
63
|
-
| Reference duration | #{test.run_time} seconds |
|
|
64
63
|
| Expected duration | < #{test.max_duration_for_test} seconds |
|
|
65
64
|
#{"| Test case | #{test.testcase} |" if test.testcase}
|
|
66
65
|
DESCRIPTION
|
|
@@ -93,7 +93,7 @@ module GitlabQuality
|
|
|
93
93
|
current_reports_content: reports_note&.body.to_s,
|
|
94
94
|
test: test,
|
|
95
95
|
reports_section_header: REPORT_SECTION_HEADER,
|
|
96
|
-
item_extra_content: found_label,
|
|
96
|
+
item_extra_content: "(#{test.run_time} seconds) #{found_label}",
|
|
97
97
|
reports_extra_content: REPORTS_DOCUMENTATION
|
|
98
98
|
)
|
|
99
99
|
end
|
|
@@ -38,7 +38,7 @@ module GitlabQuality
|
|
|
38
38
|
console_log = console_logger(source: source, level: Env.log_level)
|
|
39
39
|
file_log = file_logger(source: source, path: log_path)
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
ActiveSupport::BroadcastLogger.new(console_log, file_log, file_log)
|
|
42
42
|
end
|
|
43
43
|
end
|
|
44
44
|
|
|
@@ -6,86 +6,42 @@ module GitlabQuality
|
|
|
6
6
|
module Processor
|
|
7
7
|
class AddToBlockingProcessor < MetaProcessor
|
|
8
8
|
BLOCKING_METADATA = ", :blocking%{suffix}"
|
|
9
|
+
BRANCH_PREFIX = 'blocking-promotion'
|
|
9
10
|
|
|
10
11
|
class << self
|
|
11
|
-
#
|
|
12
|
+
# Creates the merge requests for promoting E2E tests to :blocking
|
|
12
13
|
#
|
|
13
|
-
# @param [Hash] spec the spec to update
|
|
14
14
|
# @param [TestMetaUpdater] context instance of TestMetaUpdater
|
|
15
|
-
def
|
|
15
|
+
def create_merge_requests(context)
|
|
16
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
17
|
|
|
25
|
-
|
|
18
|
+
context.processed_commits.each_value do |record|
|
|
19
|
+
branch, devops_stage, product_group, file, reviewer_id, assignee_handle = extract_data_from_record(record)
|
|
26
20
|
|
|
27
|
-
|
|
21
|
+
mr_title = format("%{prefix} %{file}", prefix: '[E2E] PROMOTE TO BLOCKING:', file: file).truncate(72, omission: '')
|
|
28
22
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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.
|
|
23
|
+
merge_request = context.create_merge_request(mr_title, branch, gitlab_bot_user_id, [reviewer_id]) do
|
|
24
|
+
merge_request_description(record, devops_stage, product_group)
|
|
25
|
+
end
|
|
68
26
|
|
|
69
|
-
|
|
70
|
-
MARKDOWN
|
|
27
|
+
context.post_note_on_merge_request(maintainer_note_on_merge_request(assignee_handle), merge_request.iid)
|
|
71
28
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
29
|
+
if merge_request
|
|
30
|
+
Runtime::Logger.info(" Created MR for promotion to blocking: #{merge_request.web_url}")
|
|
31
|
+
record[:merge_request] = merge_request
|
|
32
|
+
end
|
|
75
33
|
end
|
|
76
|
-
|
|
77
|
-
merge_request
|
|
78
34
|
end
|
|
79
35
|
|
|
80
|
-
# Performs post processing.
|
|
36
|
+
# Performs post processing. Posts a list of MRs in a note on report_issue
|
|
81
37
|
#
|
|
82
|
-
# @param [
|
|
83
|
-
def post_process(
|
|
84
|
-
web_urls =
|
|
38
|
+
# @param [TestMetaUpdater] context instance of TestMetaUpdater
|
|
39
|
+
def post_process(context)
|
|
40
|
+
web_urls = context.processed_commits.values.map { |value| "- #{value[:merge_request].web_url}\n" }.join
|
|
85
41
|
|
|
86
42
|
return if web_urls.empty?
|
|
87
43
|
|
|
88
|
-
context.
|
|
44
|
+
context.post_note_on_issue(<<~ISSUE_NOTE, context.report_issue)
|
|
89
45
|
The following merge requests have been created to promote stable specs to blocking:
|
|
90
46
|
|
|
91
47
|
#{web_urls}
|
|
@@ -94,34 +50,80 @@ module GitlabQuality
|
|
|
94
50
|
|
|
95
51
|
private
|
|
96
52
|
|
|
97
|
-
attr_reader :context, :file_path, :file_contents, :example_name, :mr_title, :changed_line_no
|
|
53
|
+
attr_reader :context, :file_path, :file, :file_contents, :example_name, :mr_title, :changed_line_no
|
|
98
54
|
|
|
99
55
|
# Checks if there is already an MR open
|
|
100
56
|
#
|
|
101
57
|
# @return [Boolean]
|
|
102
|
-
def
|
|
58
|
+
def proceed_with_commit?
|
|
103
59
|
if changed_line_no.negative?
|
|
104
60
|
Runtime::Logger.info(" No lines were changed in #{file_path}. Will not proceed with creating MR.")
|
|
105
61
|
return false
|
|
106
|
-
elsif context.
|
|
62
|
+
elsif context.commit_processed?(file_path, changed_line_no)
|
|
107
63
|
Runtime::Logger.info(" Record already processed for #{file_path}:#{changed_line_no}. Will not proceed with creating MR.")
|
|
108
64
|
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
65
|
end
|
|
113
66
|
|
|
114
67
|
true
|
|
115
68
|
end
|
|
116
69
|
|
|
70
|
+
def commit_message
|
|
71
|
+
<<~COMMIT_MESSAGE
|
|
72
|
+
Promote end-to-end test to blocking
|
|
73
|
+
|
|
74
|
+
#{"Promote to blocking: #{example_name}".truncate(72)}
|
|
75
|
+
COMMIT_MESSAGE
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def maintainer_note_on_merge_request(assignee_handle)
|
|
79
|
+
<<~MARKDOWN
|
|
80
|
+
@#{assignee_handle} Please review this MR, approve and assign it to a maintainer.
|
|
81
|
+
|
|
82
|
+
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}
|
|
83
|
+
MARKDOWN
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def merge_request_description(record, devops_stage, product_group)
|
|
87
|
+
<<~MARKDOWN
|
|
88
|
+
## What does this MR do?
|
|
89
|
+
|
|
90
|
+
Promotes the following e2e tests to the blocking bucket:
|
|
91
|
+
|
|
92
|
+
#{spec_details_from_commits(record[:commits])}
|
|
93
|
+
|
|
94
|
+
This MR was created based on data from reliable e2e test report: #{context.report_issue}
|
|
95
|
+
|
|
96
|
+
/label ~"Quality" ~"QA" ~"type::maintenance"
|
|
97
|
+
/label ~"devops::#{devops_stage}"
|
|
98
|
+
#{context.label_from_product_group(product_group)}
|
|
99
|
+
|
|
100
|
+
<div align="center">
|
|
101
|
+
(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})
|
|
102
|
+
</div>
|
|
103
|
+
MARKDOWN
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def extract_data_from_record(record)
|
|
107
|
+
first_spec = record[:commits].values.first
|
|
108
|
+
product_group = first_spec["product_group"]
|
|
109
|
+
devops_stage = first_spec["stage"]
|
|
110
|
+
reviewer_id, assignee_handle = context.fetch_dri_id(product_group, devops_stage, first_spec["section"])
|
|
111
|
+
|
|
112
|
+
[record[:branch], devops_stage, product_group, first_spec["file"], reviewer_id, assignee_handle]
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def gitlab_bot_user_id
|
|
116
|
+
context.user_id_for_username(Runtime::Env.gitlab_bot_username)
|
|
117
|
+
end
|
|
118
|
+
|
|
117
119
|
# Add blocking metadata to the file content and replace it
|
|
118
120
|
#
|
|
119
121
|
# @return [Array<String, Integer>] first value holds the new content, the second value holds the line number of the test
|
|
120
|
-
def
|
|
122
|
+
def add_metadata # rubocop:disable Metrics/AbcSize
|
|
121
123
|
matched_lines = context.find_example_match_lines(file_contents, example_name)
|
|
122
124
|
|
|
123
125
|
if matched_lines.any? { |line| line[0].include?(':blocking') }
|
|
124
|
-
|
|
126
|
+
Runtime::Logger.info("Example '#{example_name}' is already blocking")
|
|
125
127
|
return [file_contents, -1]
|
|
126
128
|
end
|
|
127
129
|
|
|
@@ -12,149 +12,193 @@ module GitlabQuality
|
|
|
12
12
|
%{indentation} type: %{quarantine_type}
|
|
13
13
|
%{indentation}}%{suffix}
|
|
14
14
|
META
|
|
15
|
+
BRANCH_PREFIX = 'quarantine'
|
|
15
16
|
|
|
16
17
|
class << self
|
|
17
|
-
#
|
|
18
|
+
# Creates the merge requests to quarantine E2E tests
|
|
18
19
|
#
|
|
19
|
-
# @param [Hash<String,String>] spec the spec to update
|
|
20
|
-
# @option spec [String] :file_path the path to the spec file
|
|
21
|
-
# @option spec [String] :stage the stage of the test
|
|
22
|
-
# @option spec [String] :failure_issue the issue url of the failure
|
|
23
|
-
# @option spec [String] :name the name of the example
|
|
24
20
|
# @param [TestMetaUpdater] context instance of TestMetaUpdater
|
|
25
|
-
def
|
|
21
|
+
def create_merge_requests(context)
|
|
26
22
|
@context = context
|
|
27
|
-
@existing_mrs = nil
|
|
28
|
-
@file_path = spec["file_path"]
|
|
29
|
-
testcase = spec["testcase"]
|
|
30
|
-
devops_stage = spec["stage"]
|
|
31
|
-
product_group = spec["product_group"]
|
|
32
|
-
@failure_issue_url = spec["failure_issue"]
|
|
33
|
-
@example_name = spec["name"]
|
|
34
|
-
@issue_id = failure_issue_url.split('/').last # split url segment, last segment of path is the issue id
|
|
35
|
-
@mr_title = format("%{prefix} %{example_name}", prefix: '[QUARANTINE]', example_name: example_name).truncate(72, omission: '')
|
|
36
|
-
@failure_issue = context.fetch_issue(iid: issue_id)
|
|
37
23
|
|
|
38
|
-
|
|
24
|
+
context.processed_commits.each_value do |record|
|
|
25
|
+
branch, devops_stage, product_group, file = extract_data_from_record(record)
|
|
39
26
|
|
|
40
|
-
|
|
27
|
+
mr_title = format("%{prefix} %{file}", prefix: '[E2E] QUARANTINE:', file: file).truncate(72, omission: '')
|
|
41
28
|
|
|
42
|
-
|
|
29
|
+
gitlab_bot_user_id = context.user_id_for_username(Runtime::Env.gitlab_bot_username)
|
|
43
30
|
|
|
44
|
-
|
|
31
|
+
merge_request = context.create_merge_request(mr_title, branch, gitlab_bot_user_id) do
|
|
32
|
+
merge_request_description(record, devops_stage, product_group)
|
|
33
|
+
end
|
|
45
34
|
|
|
46
|
-
|
|
47
|
-
|
|
35
|
+
if merge_request
|
|
36
|
+
Runtime::Logger.info(" Created MR for quarantine: #{merge_request.web_url}")
|
|
37
|
+
record[:merge_request] = merge_request
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
48
41
|
|
|
49
|
-
|
|
50
|
-
|
|
42
|
+
# Performs post processing. Posts a list of MRs in a note on report_issue and Slack.
|
|
43
|
+
# Also posts note on failure issues for un-quarantining of the quarantined
|
|
44
|
+
#
|
|
45
|
+
# @param [TestMetaUpdater] context instance of TestMetaUpdater
|
|
46
|
+
def post_process(context)
|
|
47
|
+
@context = context
|
|
51
48
|
|
|
52
|
-
|
|
49
|
+
web_urls = context.processed_commits.values.map { |value| "- #{value[:merge_request].web_url}\n" }.join
|
|
53
50
|
|
|
54
|
-
|
|
55
|
-
<<~MARKDOWN
|
|
56
|
-
## What does this MR do?
|
|
51
|
+
return if web_urls.empty?
|
|
57
52
|
|
|
58
|
-
|
|
53
|
+
context.post_note_on_issue(mrs_created_note_for_report_issue(web_urls), context.report_issue)
|
|
59
54
|
|
|
60
|
-
|
|
55
|
+
context.post_message_on_slack(mrs_created_message_for_slack(web_urls))
|
|
61
56
|
|
|
62
|
-
|
|
57
|
+
post_unquarantine_note_on_failure_issue
|
|
58
|
+
end
|
|
63
59
|
|
|
64
|
-
|
|
60
|
+
private
|
|
65
61
|
|
|
66
|
-
|
|
62
|
+
attr_reader :context, :file_path, :file_contents, :failure_issue_url, :example_name,
|
|
63
|
+
:mr_title, :failure_issue, :changed_line_no
|
|
67
64
|
|
|
68
|
-
|
|
65
|
+
# Checks if the failure issue is closed or if there is already an MR open
|
|
66
|
+
#
|
|
67
|
+
# @return [Boolean]
|
|
68
|
+
def proceed_with_commit?
|
|
69
|
+
if context.issue_is_closed?(failure_issue)
|
|
70
|
+
Runtime::Logger.info(" Failure issue '#{failure_issue_url}' is closed. Will not proceed with creating MR.")
|
|
71
|
+
return false
|
|
72
|
+
elsif context.commit_processed?(file_path, changed_line_no)
|
|
73
|
+
Runtime::Logger.info(" Record already processed for #{file_path}:#{changed_line_no}. Will not proceed with creating MR.")
|
|
74
|
+
return false
|
|
75
|
+
elsif failure_is_related_to_test_environment?
|
|
76
|
+
Runtime::Logger.info(" Failure issue '#{failure_issue_url}' is environment related. Will not proceed with creating MR.")
|
|
77
|
+
return false
|
|
78
|
+
end
|
|
69
79
|
|
|
70
|
-
|
|
80
|
+
true
|
|
81
|
+
end
|
|
71
82
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
- [ ] Quarantine test check-list
|
|
76
|
-
- [ ] Follow the [Quarantining Tests guide](https://about.gitlab.com/handbook/engineering/infrastructure/test-platform/debugging-qa-test-failures/#quarantining-tests).
|
|
77
|
-
- [ ] Confirm the test has a [`quarantine:` tag with the specified quarantine type](https://about.gitlab.com/handbook/engineering/infrastructure/test-platform/debugging-qa-test-failures/#quarantined-test-types).
|
|
78
|
-
- [ ] Note if the test should be [quarantined for a specific environment](https://docs.gitlab.com/ee/development/testing_guide/end_to_end/execution_context_selection.html#quarantine-a-test-for-a-specific-environment).
|
|
79
|
-
- [ ] (Optionally) In case of an emergency (e.g. blocked deployments), consider adding labels to pick into auto-deploy (~"Pick into auto-deploy" ~"priority::1" ~"severity::1").
|
|
80
|
-
- [ ] To ensure a faster turnaround, ask in the `#quality_maintainers` Slack channel for someone to review and merge the merge request, rather than assigning it directly.
|
|
83
|
+
def failure_is_related_to_test_environment?
|
|
84
|
+
context.issue_scoped_label(failure_issue, 'failure')&.split('::')&.last == 'test-environment'
|
|
85
|
+
end
|
|
81
86
|
|
|
82
|
-
|
|
83
|
-
|
|
87
|
+
def extract_data_from_record(record)
|
|
88
|
+
first_spec = record[:commits].values.first
|
|
89
|
+
[record[:branch], first_spec["stage"], first_spec["product_group"], first_spec["file"]]
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Posts note on failure issue to un-quarantine the test
|
|
93
|
+
#
|
|
94
|
+
def post_unquarantine_note_on_failure_issue
|
|
95
|
+
context.processed_commits.each_value do |record|
|
|
96
|
+
merge_request = record[:merge_request]
|
|
97
|
+
next unless merge_request
|
|
84
98
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
/label ~"devops::#{devops_stage}"
|
|
90
|
-
#{context.label_from_product_group(product_group)}
|
|
99
|
+
record[:commits].each_value do |spec|
|
|
100
|
+
devops_stage = spec["stage"]
|
|
101
|
+
product_group = spec["product_group"]
|
|
102
|
+
failure_issue = spec["failure_issue"]
|
|
91
103
|
|
|
92
|
-
|
|
93
|
-
(This MR was automatically generated by [`gitlab_quality-test_tooling`](https://gitlab.com/gitlab-org/ruby/gems/gitlab_quality-test_tooling) at #{Time.now.utc})
|
|
94
|
-
</div>
|
|
95
|
-
MARKDOWN
|
|
96
|
-
end
|
|
104
|
+
next unless failure_issue
|
|
97
105
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
106
|
+
note = context.post_note_on_issue(unquarantine_note_for_failure(spec, product_group, devops_stage, merge_request),
|
|
107
|
+
failure_issue)
|
|
108
|
+
|
|
109
|
+
Runtime::Logger.info(" Posted note on failure issue for un-quarantine: #{failure_issue}") if note
|
|
110
|
+
end
|
|
101
111
|
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def merge_request_description(record, devops_stage, product_group)
|
|
115
|
+
<<~MARKDOWN
|
|
116
|
+
## What does this MR do?
|
|
102
117
|
|
|
103
|
-
|
|
118
|
+
Quarantines the following e2e tests:
|
|
119
|
+
|
|
120
|
+
#{spec_details_from_commits(record[:commits])}
|
|
121
|
+
|
|
122
|
+
This MR was created based on data from reliable e2e test report: #{context.report_issue}
|
|
123
|
+
|
|
124
|
+
### Check-list
|
|
125
|
+
|
|
126
|
+
- [ ] General code guidelines check-list
|
|
127
|
+
- [ ] [Code review guidelines](https://docs.gitlab.com/ee/development/code_review.html)
|
|
128
|
+
- [ ] [Style guides](https://docs.gitlab.com/ee/development/contributing/style_guides.html)
|
|
129
|
+
- [ ] Quarantine test check-list
|
|
130
|
+
- [ ] Follow the [Quarantining Tests guide](https://about.gitlab.com/handbook/engineering/infrastructure/test-platform/debugging-qa-test-failures/#quarantining-tests).
|
|
131
|
+
- [ ] Confirm the test has a [`quarantine:` tag with the specified quarantine type](https://about.gitlab.com/handbook/engineering/infrastructure/test-platform/debugging-qa-test-failures/#quarantined-test-types).
|
|
132
|
+
- [ ] Note if the test should be [quarantined for a specific environment](https://docs.gitlab.com/ee/development/testing_guide/end_to_end/execution_context_selection.html#quarantine-a-test-for-a-specific-environment).
|
|
133
|
+
- [ ] (Optionally) In case of an emergency (e.g. blocked deployments), consider adding labels to pick into auto-deploy (~"Pick into auto-deploy" ~"priority::1" ~"severity::1").
|
|
134
|
+
- [ ] To ensure a faster turnaround, ask in the `#quality_maintainers` Slack channel for someone to review and merge the merge request, rather than assigning it directly.
|
|
135
|
+
|
|
136
|
+
<!-- Base labels. -->
|
|
137
|
+
/label ~"Quality" ~"QA" ~"type::maintenance" ~"maintenance::pipelines"
|
|
138
|
+
|
|
139
|
+
<!--
|
|
140
|
+
Choose the stage that appears in the test path, e.g. ~"devops::create" for
|
|
141
|
+
`qa/specs/features/browser_ui/3_create/web_ide/add_file_template_spec.rb`.
|
|
142
|
+
-->
|
|
143
|
+
/label ~"devops::#{devops_stage}"
|
|
144
|
+
#{context.label_from_product_group(product_group)}
|
|
145
|
+
|
|
146
|
+
<div align="center">
|
|
147
|
+
(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})
|
|
148
|
+
</div>
|
|
149
|
+
MARKDOWN
|
|
104
150
|
end
|
|
105
151
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
def post_process(merge_requests)
|
|
110
|
-
web_urls = merge_requests.compact.map { |mr| "- #{mr.web_url}\n" }.join
|
|
152
|
+
def commit_message
|
|
153
|
+
<<~COMMIT_MESSAGE
|
|
154
|
+
Quarantine end-to-end test
|
|
111
155
|
|
|
112
|
-
|
|
156
|
+
#{"Quarantine #{example_name}".truncate(72)}
|
|
157
|
+
COMMIT_MESSAGE
|
|
158
|
+
end
|
|
113
159
|
|
|
114
|
-
|
|
160
|
+
def mrs_created_note_for_report_issue(web_urls)
|
|
161
|
+
<<~ISSUE_NOTE
|
|
115
162
|
|
|
116
163
|
The following merge requests have been created to quarantine the unstable tests:
|
|
117
164
|
|
|
118
165
|
#{web_urls}
|
|
119
166
|
ISSUE_NOTE
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def unquarantine_note_for_failure(spec, product_group, devops_stage, merge_request)
|
|
170
|
+
failure_issue_assignee_handle = get_failure_issue_assignee_handle(spec, product_group, devops_stage)
|
|
171
|
+
<<~ISSUE_NOTE
|
|
172
|
+
@#{failure_issue_assignee_handle} This test will be quarantined in #{merge_request.web_url} based on data from reliable e2e test report: #{context.report_issue}
|
|
173
|
+
|
|
174
|
+
Please take this issue forward to un-quarantine the test by either addressing the issue yourself or delegating it to the relevant product group.
|
|
175
|
+
|
|
176
|
+
If this issue is delegated, please make sure to update the assignee. Thanks.
|
|
177
|
+
ISSUE_NOTE
|
|
178
|
+
end
|
|
120
179
|
|
|
121
|
-
|
|
180
|
+
def mrs_created_message_for_slack(web_urls)
|
|
181
|
+
<<~MSG
|
|
122
182
|
*Action Required!* The following merge requests have been created to quarantine the unstable tests identified
|
|
123
183
|
in the reliable test report: #{context.report_issue}
|
|
124
184
|
|
|
125
185
|
#{web_urls}
|
|
126
186
|
|
|
127
|
-
Maintainers are requested to review and merge. Thank you.
|
|
187
|
+
Maintainers are requested to review and merge the above MRs. Thank you.
|
|
128
188
|
MSG
|
|
129
189
|
end
|
|
130
190
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
attr_reader :context, :file_path, :file_contents, :failure_issue_url, :example_name,
|
|
134
|
-
:issue_id, :mr_title, :failure_issue, :changed_line_no
|
|
191
|
+
def get_failure_issue_assignee_handle(spec, product_group, devops_stage)
|
|
192
|
+
return spec["failure_issue_assignee_handle"] if spec["failure_issue_assignee_handle"]
|
|
135
193
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
# @return [Boolean]
|
|
139
|
-
def proceed_with_merge_request? # rubocop:disable Metrics/AbcSize
|
|
140
|
-
if context.issue_is_closed?(failure_issue)
|
|
141
|
-
Runtime::Logger.info(" Failure issue '#{failure_issue_url}' is closed. Will not proceed with creating MR.")
|
|
142
|
-
return false
|
|
143
|
-
elsif context.record_processed?(file_path, changed_line_no)
|
|
144
|
-
Runtime::Logger.info(" Record already processed for #{file_path}:#{changed_line_no}. Will not proceed with creating MR.")
|
|
145
|
-
return false
|
|
146
|
-
elsif existing_mrs&.any?
|
|
147
|
-
Runtime::Logger.info(" An open MR already exists for '#{example_name}': #{existing_mrs.first['web_url']}. Will not proceed with creating MR.")
|
|
148
|
-
return false
|
|
149
|
-
end
|
|
150
|
-
|
|
151
|
-
true
|
|
194
|
+
_, assignee_handle = context.fetch_dri_id(product_group, devops_stage, nil)
|
|
195
|
+
assignee_handle
|
|
152
196
|
end
|
|
153
197
|
|
|
154
198
|
# Add quarantine metadata to the file content and replace it
|
|
155
199
|
#
|
|
156
200
|
# @return [Array<String, Integer>] first value holds the new content, the second value holds the line number of the test
|
|
157
|
-
def
|
|
201
|
+
def add_metadata # rubocop:disable Metrics/AbcSize
|
|
158
202
|
matched_lines = context.find_example_match_lines(file_contents, example_name)
|
|
159
203
|
|
|
160
204
|
context.update_matched_line(matched_lines.last, file_contents.dup) do |line|
|
|
@@ -10,12 +10,12 @@ module GitlabQuality
|
|
|
10
10
|
class << self
|
|
11
11
|
DESCRIPTION_REGEX = /('.*?')|(".*?")/
|
|
12
12
|
|
|
13
|
-
def
|
|
14
|
-
raise '
|
|
13
|
+
def post_process
|
|
14
|
+
raise NotImplementedError, 'Subclass must implement this method'
|
|
15
15
|
end
|
|
16
16
|
|
|
17
|
-
def
|
|
18
|
-
raise '
|
|
17
|
+
def create_merge_requests
|
|
18
|
+
raise NotImplementedError, 'Subclass must implement this method'
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
private_class_method :new
|
|
@@ -36,6 +36,65 @@ module GitlabQuality
|
|
|
36
36
|
description_start_index = line.index(DESCRIPTION_REGEX)
|
|
37
37
|
description_start_index + description_length
|
|
38
38
|
end
|
|
39
|
+
|
|
40
|
+
# List specs in markdown with details such as link to code, testcase, metrics and failure issue
|
|
41
|
+
#
|
|
42
|
+
# @param [Hash<String,Hash>] commits The commits hash to use for spec details
|
|
43
|
+
# @return String
|
|
44
|
+
def spec_details_from_commits(commits)
|
|
45
|
+
commits.each_with_index.map do |(changed_line_number, spec), index|
|
|
46
|
+
<<~MARKDOWN
|
|
47
|
+
#{index + 1}. [`#{spec['name']}`](https://gitlab.com/#{context.project}/-/blob/#{context.ref}/#{spec['file_path']}#L#{changed_line_number.to_i + 1})
|
|
48
|
+
| [Testcase](#{spec['testcase']}) | [Spec metrics](#{context.single_spec_metrics_link(spec['name'])})
|
|
49
|
+
#{failure_issue_text(spec)}
|
|
50
|
+
MARKDOWN
|
|
51
|
+
end.join("\n")
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Returns a string in markdown of failure issue and its link
|
|
55
|
+
#
|
|
56
|
+
# @param [Hash] spec the spec for failure issue
|
|
57
|
+
# @return [String]
|
|
58
|
+
def failure_issue_text(spec)
|
|
59
|
+
spec['failure_issue'].empty? ? '' : "| [Failure issue](#{spec['failure_issue']})"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Creates a commit depending on the context provided and adds it to a Hash of created commits
|
|
63
|
+
#
|
|
64
|
+
# @param [Hash] spec the spec to update
|
|
65
|
+
# @param [TestMetaUpdater] context instance of TestMetaUpdater
|
|
66
|
+
def create_commit(spec, context) # rubocop:disable Metrics/AbcSize
|
|
67
|
+
@context = context
|
|
68
|
+
@file_path = spec["file_path"]
|
|
69
|
+
@file = spec["file"]
|
|
70
|
+
@example_name = spec["name"]
|
|
71
|
+
@failure_issue_url = spec["failure_issue"]
|
|
72
|
+
|
|
73
|
+
issue_id = failure_issue_url&.split('/')&.last # split url segment, last segment of path is the issue id
|
|
74
|
+
existing_branch = context.branch_for_file_path(file_path)
|
|
75
|
+
|
|
76
|
+
@file_contents = context.get_file_contents(file_path: file_path,
|
|
77
|
+
branch: existing_branch && existing_branch['name'])
|
|
78
|
+
|
|
79
|
+
@failure_issue = context.fetch_issue(iid: issue_id) if issue_id
|
|
80
|
+
|
|
81
|
+
spec['failure_issue_assignee_handle'] = @failure_issue['assignee']['username'] if @failure_issue && @failure_issue['assignee']
|
|
82
|
+
|
|
83
|
+
new_content, @changed_line_no = add_metadata
|
|
84
|
+
|
|
85
|
+
return unless proceed_with_commit?
|
|
86
|
+
|
|
87
|
+
branch = existing_branch ||
|
|
88
|
+
context.create_branch("#{self::BRANCH_PREFIX}-#{SecureRandom.hex(4)}", @file, context.ref)
|
|
89
|
+
|
|
90
|
+
context.commit_changes(branch, commit_message, file_path, new_content)
|
|
91
|
+
|
|
92
|
+
context.add_processed_commit(file_path, changed_line_no, branch, spec)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
private
|
|
96
|
+
|
|
97
|
+
attr_reader :failure_issue_url, :failure_issue
|
|
39
98
|
end
|
|
40
99
|
end
|
|
41
100
|
end
|
|
@@ -8,9 +8,10 @@ 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_commits
|
|
12
12
|
|
|
13
13
|
TEST_PLATFORM_MAINTAINERS_SLACK_CHANNEL_ID = 'C0437FV9KBN' # test-platform-maintainers
|
|
14
|
+
BATCH_LIMIT = 10
|
|
14
15
|
|
|
15
16
|
def initialize(token:, project:, specs_file:, processor:, ref: 'master', dry_run: false)
|
|
16
17
|
@specs_file = specs_file
|
|
@@ -19,37 +20,85 @@ module GitlabQuality
|
|
|
19
20
|
@ref = ref
|
|
20
21
|
@dry_run = dry_run
|
|
21
22
|
@processor = processor
|
|
22
|
-
@
|
|
23
|
+
@processed_commits = {}
|
|
23
24
|
end
|
|
24
25
|
|
|
25
26
|
def invoke!
|
|
26
27
|
JSON.parse(File.read(specs_file)).tap do |contents|
|
|
27
28
|
@report_issue = contents['report_issue']
|
|
28
29
|
|
|
29
|
-
results = []
|
|
30
30
|
contents['specs'].each do |spec|
|
|
31
|
-
|
|
31
|
+
processor.create_commit(spec, self)
|
|
32
|
+
break if processed_commits.keys.count >= BATCH_LIMIT
|
|
32
33
|
end
|
|
33
|
-
|
|
34
|
+
|
|
35
|
+
processor.create_merge_requests(self)
|
|
36
|
+
|
|
37
|
+
processor.post_process(self)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Add processed commits.
|
|
42
|
+
#
|
|
43
|
+
# processed_commits has the following form. Note that each key in the :commits hash
|
|
44
|
+
# is the changed line number and the value is the spec changed.
|
|
45
|
+
#
|
|
46
|
+
# {
|
|
47
|
+
# "/file/path/for/spec_1" =>
|
|
48
|
+
# { :commits =>
|
|
49
|
+
# {
|
|
50
|
+
# "34" => {"stage"=> "create", "product_group" => "source_code".. },
|
|
51
|
+
# "38" => {"stage"=> "create", "product_group" => "source_code".. }
|
|
52
|
+
# },
|
|
53
|
+
# :branch => #<Gitlab::ObjectifiedHash>
|
|
54
|
+
# },
|
|
55
|
+
# "/file/path/for/spec_2" =>
|
|
56
|
+
# { :commits =>
|
|
57
|
+
# {
|
|
58
|
+
# "34" => {"stage"=> "create", "product_group" => "source_code".. },
|
|
59
|
+
# "38" => {"stage"=> "create", "product_group" => "source_code".. }
|
|
60
|
+
# },
|
|
61
|
+
# :branch => #<Gitlab::ObjectifiedHash>
|
|
62
|
+
# },
|
|
63
|
+
# }
|
|
64
|
+
#
|
|
65
|
+
# @param [<String>] file_path the file path to the spec
|
|
66
|
+
# @param [<Integer>] changed_line_no the changed line number for the commit
|
|
67
|
+
# @param [<Gitlab::ObjectifiedHash>] branch the branch for the commit
|
|
68
|
+
# @param [<Hash>] spec spec details hash
|
|
69
|
+
# @return [Hash<String,Hash>] processed_commits
|
|
70
|
+
def add_processed_commit(file_path, changed_line_no, branch, spec)
|
|
71
|
+
if processed_commits[file_path].nil?
|
|
72
|
+
processed_commits[file_path] = { commits: { changed_line_no.to_s => spec }, branch: branch }
|
|
73
|
+
elsif processed_commits[file_path][:commits][changed_line_no.to_s].nil?
|
|
74
|
+
processed_commits[file_path][:commits].merge!({ changed_line_no.to_s => spec })
|
|
34
75
|
end
|
|
35
76
|
end
|
|
36
77
|
|
|
37
|
-
#
|
|
78
|
+
# Checks if changes have already been made to given file_path and line number
|
|
79
|
+
#
|
|
80
|
+
# @param [String] file_path path to the file
|
|
81
|
+
# @param [Integer] changed_line_no updated line number
|
|
82
|
+
# @return [Boolean]
|
|
83
|
+
def commit_processed?(file_path, changed_line_no)
|
|
84
|
+
processed_commits[file_path] && processed_commits[file_path][:commits][changed_line_no.to_s]
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Returns the branch for the given file_path
|
|
38
88
|
#
|
|
39
|
-
# @param [
|
|
40
|
-
# @
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
def add_processed_record(record)
|
|
44
|
-
@processed_records.merge!(record)
|
|
89
|
+
# @param [String] file_path path to the file
|
|
90
|
+
# @return [<Gitlab::ObjectifiedHash>]
|
|
91
|
+
def branch_for_file_path(file_path)
|
|
92
|
+
processed_commits[file_path] && processed_commits[file_path][:branch]
|
|
45
93
|
end
|
|
46
94
|
|
|
47
95
|
# Fetch contents of file from the repository
|
|
48
96
|
#
|
|
49
97
|
# @param [String] file_path path to the file
|
|
98
|
+
# @param [String] branch branch ref
|
|
50
99
|
# @return [String] contents of the file
|
|
51
|
-
def get_file_contents(file_path)
|
|
52
|
-
repository_files = GitlabClient::RepositoryFilesClient.new(token: token, project: project, file_path: file_path)
|
|
100
|
+
def get_file_contents(file_path:, branch:)
|
|
101
|
+
repository_files = GitlabClient::RepositoryFilesClient.new(token: token, project: project, file_path: file_path, ref: branch || ref)
|
|
53
102
|
repository_files.file_contents
|
|
54
103
|
end
|
|
55
104
|
|
|
@@ -62,11 +111,17 @@ module GitlabQuality
|
|
|
62
111
|
lines = content.split("\n")
|
|
63
112
|
|
|
64
113
|
matched_lines = []
|
|
114
|
+
example_name_for_parsing = example_name.dup
|
|
65
115
|
|
|
66
116
|
lines.each_with_index do |line, line_index|
|
|
67
117
|
string_within_quotes = spec_desc_string_within_quotes(line)
|
|
68
118
|
|
|
69
|
-
|
|
119
|
+
regex = /^\s?#{Regexp.escape(string_within_quotes)}/ if string_within_quotes
|
|
120
|
+
|
|
121
|
+
if !example_name_for_parsing.empty? && regex && example_name_for_parsing.match(regex)
|
|
122
|
+
example_name_for_parsing.sub!(regex, '')
|
|
123
|
+
matched_lines << [line, line_index]
|
|
124
|
+
end
|
|
70
125
|
rescue StandardError => e
|
|
71
126
|
puts "Error: #{e}"
|
|
72
127
|
end
|
|
@@ -95,10 +150,10 @@ module GitlabQuality
|
|
|
95
150
|
# Create a branch from the ref
|
|
96
151
|
#
|
|
97
152
|
# @param [String] name_prefix the prefix to attach to the branch name
|
|
98
|
-
# @param [String]
|
|
153
|
+
# @param [String] name the branch name
|
|
99
154
|
# @return [Gitlab::ObjectifiedHash] the new branch
|
|
100
|
-
def create_branch(name_prefix,
|
|
101
|
-
branch_name = [name_prefix,
|
|
155
|
+
def create_branch(name_prefix, name, ref)
|
|
156
|
+
branch_name = [name_prefix, name.gsub(/\W/, '-')]
|
|
102
157
|
@branches_client ||= (dry_run ? GitlabClient::BranchesDryClient : GitlabClient::BranchesClient).new(token: token, project: project)
|
|
103
158
|
@branches_client.create(branch_name.join('-'), ref)
|
|
104
159
|
end
|
|
@@ -162,16 +217,17 @@ module GitlabQuality
|
|
|
162
217
|
issue_client.find_issues(iid: iid).first
|
|
163
218
|
end
|
|
164
219
|
|
|
165
|
-
# Post note on
|
|
220
|
+
# Post note on isse
|
|
166
221
|
#
|
|
167
222
|
# @param [String] note the note to post
|
|
168
223
|
# @return [Gitlab::ObjectifiedHash]
|
|
169
|
-
def
|
|
170
|
-
iid =
|
|
224
|
+
def post_note_on_issue(note, issue_url)
|
|
225
|
+
iid = issue_url&.split('/')&.last # split url segment, last segment of path is the issue id
|
|
171
226
|
if iid
|
|
172
227
|
issue_client.create_issue_note(iid: iid, note: note)
|
|
173
228
|
else
|
|
174
|
-
Runtime::Logger.info("#{self.class.name}##{__method__} Note was NOT posted on
|
|
229
|
+
Runtime::Logger.info("#{self.class.name}##{__method__} Note was NOT posted on issue: #{issue_url}")
|
|
230
|
+
nil
|
|
175
231
|
end
|
|
176
232
|
end
|
|
177
233
|
|
|
@@ -190,8 +246,8 @@ module GitlabQuality
|
|
|
190
246
|
# @param [String] product_group
|
|
191
247
|
# @param [String] devops_stage
|
|
192
248
|
# @return [Array<Integer, String>]
|
|
193
|
-
def fetch_dri_id(product_group, devops_stage)
|
|
194
|
-
assignee_handle = ENV.fetch('QA_TEST_DRI_HANDLE', nil) ||
|
|
249
|
+
def fetch_dri_id(product_group, devops_stage, section)
|
|
250
|
+
assignee_handle = ENV.fetch('QA_TEST_DRI_HANDLE', nil) || test_dri(product_group, devops_stage, section)
|
|
195
251
|
|
|
196
252
|
[user_id_for_username(assignee_handle), assignee_handle]
|
|
197
253
|
end
|
|
@@ -243,15 +299,6 @@ module GitlabQuality
|
|
|
243
299
|
merge_request_client.find(options: { search: title, in: 'title', state: 'opened' })
|
|
244
300
|
end
|
|
245
301
|
|
|
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
302
|
# Infers product group label from the provided product group
|
|
256
303
|
#
|
|
257
304
|
# @param [String] product_group product group
|
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.21.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-03-
|
|
11
|
+
date: 2024-03-25 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: climate_control
|
|
@@ -201,7 +201,7 @@ dependencies:
|
|
|
201
201
|
version: '6.1'
|
|
202
202
|
- - "<"
|
|
203
203
|
- !ruby/object:Gem::Version
|
|
204
|
-
version: '7.
|
|
204
|
+
version: '7.2'
|
|
205
205
|
type: :runtime
|
|
206
206
|
prerelease: false
|
|
207
207
|
version_requirements: !ruby/object:Gem::Requirement
|
|
@@ -211,7 +211,7 @@ dependencies:
|
|
|
211
211
|
version: '6.1'
|
|
212
212
|
- - "<"
|
|
213
213
|
- !ruby/object:Gem::Version
|
|
214
|
-
version: '7.
|
|
214
|
+
version: '7.2'
|
|
215
215
|
- !ruby/object:Gem::Dependency
|
|
216
216
|
name: amatch
|
|
217
217
|
requirement: !ruby/object:Gem::Requirement
|