gitlab_quality-test_tooling 1.11.0 → 1.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Gemfile.lock +93 -68
- data/README.md +35 -2
- data/exe/flaky-test-issues +7 -2
- data/exe/knapsack-report-issues +54 -0
- data/exe/update-test-meta +70 -0
- data/lefthook.yml +13 -0
- data/lib/gitlab_quality/test_tooling/concerns/find_set_dri.rb +47 -0
- data/lib/gitlab_quality/test_tooling/gitlab_client/branches_client.rb +18 -0
- data/lib/gitlab_quality/test_tooling/gitlab_client/branches_dry_client.rb +15 -0
- data/lib/gitlab_quality/test_tooling/gitlab_client/commits_client.rb +20 -0
- data/lib/gitlab_quality/test_tooling/gitlab_client/commits_dry_client.rb +14 -0
- data/lib/gitlab_quality/test_tooling/gitlab_client/gitlab_client.rb +12 -13
- data/lib/gitlab_quality/test_tooling/gitlab_client/issues_client.rb +6 -6
- data/lib/gitlab_quality/test_tooling/gitlab_client/merge_requests_client.rb +18 -10
- data/lib/gitlab_quality/test_tooling/gitlab_client/merge_requests_dry_client.rb +4 -2
- data/lib/gitlab_quality/test_tooling/gitlab_client/repository_files_client.rb +23 -0
- data/lib/gitlab_quality/test_tooling/knapsack_reports/spec_run_time.rb +85 -0
- data/lib/gitlab_quality/test_tooling/knapsack_reports/spec_run_time_report.rb +60 -0
- data/lib/gitlab_quality/test_tooling/report/concerns/issue_reports.rb +41 -28
- data/lib/gitlab_quality/test_tooling/report/concerns/utils.rb +23 -1
- data/lib/gitlab_quality/test_tooling/report/flaky_test_issue.rb +89 -44
- data/lib/gitlab_quality/test_tooling/report/generate_test_session.rb +1 -4
- data/lib/gitlab_quality/test_tooling/report/knapsack_report_issue.rb +139 -0
- data/lib/gitlab_quality/test_tooling/report/relate_failure_issue.rb +6 -12
- data/lib/gitlab_quality/test_tooling/report/report_as_issue.rb +16 -5
- data/lib/gitlab_quality/test_tooling/report/slow_test_issue.rb +71 -80
- data/lib/gitlab_quality/test_tooling/runtime/env.rb +5 -1
- data/lib/gitlab_quality/test_tooling/slack/post_to_slack_dry.rb +14 -0
- data/lib/gitlab_quality/test_tooling/test_meta/processor/add_to_blocking_processor.rb +143 -0
- data/lib/gitlab_quality/test_tooling/test_meta/processor/add_to_quarantine_processor.rb +199 -0
- data/lib/gitlab_quality/test_tooling/test_meta/processor/meta_processor.rb +44 -0
- data/lib/gitlab_quality/test_tooling/test_meta/test_meta_updater.rb +313 -0
- data/lib/gitlab_quality/test_tooling/version.rb +1 -1
- metadata +40 -9
- data/lib/gitlab_quality/test_tooling/report/concerns/find_set_dri.rb +0 -49
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GitlabQuality
|
|
4
|
+
module TestTooling
|
|
5
|
+
module TestMeta
|
|
6
|
+
module Processor
|
|
7
|
+
class AddToQuarantineProcessor < MetaProcessor
|
|
8
|
+
QUARANTINE_METADATA = <<~META
|
|
9
|
+
,
|
|
10
|
+
%{indentation}quarantine: {
|
|
11
|
+
%{indentation} issue: '%{issue_url}',
|
|
12
|
+
%{indentation} type: %{quarantine_type}
|
|
13
|
+
%{indentation}}%{suffix}
|
|
14
|
+
META
|
|
15
|
+
|
|
16
|
+
class << self
|
|
17
|
+
# Execute the processor
|
|
18
|
+
#
|
|
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
|
+
# @param [TestMetaUpdater] context instance of TestMetaUpdater
|
|
25
|
+
def execute(spec, context) # rubocop:disable Metrics/AbcSize
|
|
26
|
+
@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
|
+
|
|
38
|
+
@file_contents = context.get_file_contents(file_path)
|
|
39
|
+
|
|
40
|
+
new_content, @changed_line_no = add_quarantine_metadata
|
|
41
|
+
|
|
42
|
+
return unless proceed_with_merge_request?
|
|
43
|
+
|
|
44
|
+
branch = context.create_branch("#{issue_id}-quarantine-#{SecureRandom.hex(4)}", example_name, context.ref)
|
|
45
|
+
|
|
46
|
+
context.commit_changes(branch, <<~COMMIT_MESSAGE, file_path, new_content)
|
|
47
|
+
Quarantine end-to-end test
|
|
48
|
+
|
|
49
|
+
#{"Quarantine #{example_name}".truncate(72)}
|
|
50
|
+
COMMIT_MESSAGE
|
|
51
|
+
|
|
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
|
|
55
|
+
<<~MARKDOWN
|
|
56
|
+
## What does this MR do?
|
|
57
|
+
|
|
58
|
+
Quarantines the test [`#{example_name}`](https://gitlab.com/#{context.project}/-/blob/#{context.ref}/#{file_path}#L#{changed_line_no + 1})
|
|
59
|
+
|
|
60
|
+
This test was identified in the reliable e2e test report: #{context.report_issue}
|
|
61
|
+
|
|
62
|
+
[Testcase link](#{testcase})
|
|
63
|
+
|
|
64
|
+
[Spec metrics link](#{context.single_spec_metrics_link(example_name)})
|
|
65
|
+
|
|
66
|
+
### E2E Test Failure issue(s)
|
|
67
|
+
|
|
68
|
+
#{failure_issue_url}
|
|
69
|
+
|
|
70
|
+
### Check-list
|
|
71
|
+
|
|
72
|
+
- [ ] General code guidelines check-list
|
|
73
|
+
- [ ] [Code review guidelines](https://docs.gitlab.com/ee/development/code_review.html)
|
|
74
|
+
- [ ] [Style guides](https://docs.gitlab.com/ee/development/contributing/style_guides.html)
|
|
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.
|
|
81
|
+
|
|
82
|
+
<!-- Base labels. -->
|
|
83
|
+
/label ~"Quality" ~"QA" ~"type::maintenance" ~"maintenance::pipelines"
|
|
84
|
+
|
|
85
|
+
<!--
|
|
86
|
+
Choose the stage that appears in the test path, e.g. ~"devops::create" for
|
|
87
|
+
`qa/specs/features/browser_ui/3_create/web_ide/add_file_template_spec.rb`.
|
|
88
|
+
-->
|
|
89
|
+
/label ~"devops::#{devops_stage}"
|
|
90
|
+
#{context.label_from_product_group(product_group)}
|
|
91
|
+
|
|
92
|
+
<div align="center">
|
|
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
|
|
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
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Performs post processing. Takes a list of MRs and posts them in a note on report_issue and Slack
|
|
107
|
+
#
|
|
108
|
+
# @param [Gitlab::ObjectifiedHash] merge_requests
|
|
109
|
+
def post_process(merge_requests)
|
|
110
|
+
web_urls = merge_requests.compact.map { |mr| "- #{mr.web_url}\n" }.join
|
|
111
|
+
|
|
112
|
+
return if web_urls.empty?
|
|
113
|
+
|
|
114
|
+
context.post_note_on_report_issue(<<~ISSUE_NOTE)
|
|
115
|
+
|
|
116
|
+
The following merge requests have been created to quarantine the unstable tests:
|
|
117
|
+
|
|
118
|
+
#{web_urls}
|
|
119
|
+
ISSUE_NOTE
|
|
120
|
+
|
|
121
|
+
context.post_message_on_slack(<<~MSG)
|
|
122
|
+
*Action Required!* The following merge requests have been created to quarantine the unstable tests identified
|
|
123
|
+
in the reliable test report: #{context.report_issue}
|
|
124
|
+
|
|
125
|
+
#{web_urls}
|
|
126
|
+
|
|
127
|
+
Maintainers are requested to review and merge. Thank you.
|
|
128
|
+
MSG
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
private
|
|
132
|
+
|
|
133
|
+
attr_reader :context, :file_path, :file_contents, :failure_issue_url, :example_name,
|
|
134
|
+
:issue_id, :mr_title, :failure_issue, :changed_line_no
|
|
135
|
+
|
|
136
|
+
# Checks if the failure issue is closed or if there is already an MR open
|
|
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
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Add quarantine metadata to the file content and replace it
|
|
155
|
+
#
|
|
156
|
+
# @return [Array<String, Integer>] first value holds the new content, the second value holds the line number of the test
|
|
157
|
+
def add_quarantine_metadata # rubocop:disable Metrics/AbcSize
|
|
158
|
+
matched_lines = context.find_example_match_lines(file_contents, example_name)
|
|
159
|
+
|
|
160
|
+
context.update_matched_line(matched_lines.last, file_contents.dup) do |line|
|
|
161
|
+
indentation = context.indentation(line)
|
|
162
|
+
|
|
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)
|
|
165
|
+
else
|
|
166
|
+
line[line.rindex(' ')] = format(QUARANTINE_METADATA.rstrip, issue_url: failure_issue_url, indentation: indentation, suffix: ' ', quarantine_type: quarantine_type)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
line
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Returns the quarantine type based on the failure scoped label
|
|
174
|
+
#
|
|
175
|
+
# @return [String]
|
|
176
|
+
def quarantine_type
|
|
177
|
+
case context.issue_scoped_label(failure_issue, 'failure')&.split('::')&.last
|
|
178
|
+
when 'new', 'investigating'
|
|
179
|
+
':investigating'
|
|
180
|
+
when 'broken-test'
|
|
181
|
+
':broken'
|
|
182
|
+
when 'bug'
|
|
183
|
+
':bug'
|
|
184
|
+
when 'flaky-test'
|
|
185
|
+
':flaky'
|
|
186
|
+
when 'stale-test'
|
|
187
|
+
':stale'
|
|
188
|
+
when 'test-environment'
|
|
189
|
+
':test_environment'
|
|
190
|
+
else
|
|
191
|
+
':investigating'
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'active_support/core_ext/string/filters'
|
|
4
|
+
|
|
5
|
+
module GitlabQuality
|
|
6
|
+
module TestTooling
|
|
7
|
+
module TestMeta
|
|
8
|
+
module Processor
|
|
9
|
+
class MetaProcessor
|
|
10
|
+
class << self
|
|
11
|
+
DESCRIPTION_REGEX = /('.*?')|(".*?")/
|
|
12
|
+
|
|
13
|
+
def execute
|
|
14
|
+
raise 'method not implemented'
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def post_process
|
|
18
|
+
raise 'method not implemented'
|
|
19
|
+
end
|
|
20
|
+
|
|
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
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
|
|
5
|
+
module GitlabQuality
|
|
6
|
+
module TestTooling
|
|
7
|
+
module TestMeta
|
|
8
|
+
class TestMetaUpdater
|
|
9
|
+
include TestTooling::Concerns::FindSetDri
|
|
10
|
+
|
|
11
|
+
attr_reader :project, :ref, :report_issue, :processed_records
|
|
12
|
+
|
|
13
|
+
TEST_PLATFORM_MAINTAINERS_SLACK_CHANNEL_ID = 'C0437FV9KBN' # test-platform-maintainers
|
|
14
|
+
|
|
15
|
+
def initialize(token:, project:, specs_file:, processor:, ref: 'master', dry_run: false)
|
|
16
|
+
@specs_file = specs_file
|
|
17
|
+
@token = token
|
|
18
|
+
@project = project
|
|
19
|
+
@ref = ref
|
|
20
|
+
@dry_run = dry_run
|
|
21
|
+
@processor = processor
|
|
22
|
+
@processed_records = {}
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def invoke!
|
|
26
|
+
JSON.parse(File.read(specs_file)).tap do |contents|
|
|
27
|
+
@report_issue = contents['report_issue']
|
|
28
|
+
|
|
29
|
+
results = []
|
|
30
|
+
contents['specs'].each do |spec|
|
|
31
|
+
results << processor.execute(spec, self)
|
|
32
|
+
end
|
|
33
|
+
processor.post_process(results)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
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
|
+
|
|
47
|
+
# Fetch contents of file from the repository
|
|
48
|
+
#
|
|
49
|
+
# @param [String] file_path path to the file
|
|
50
|
+
# @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)
|
|
53
|
+
repository_files.file_contents
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Find all lines that contain any part of the example name
|
|
57
|
+
#
|
|
58
|
+
# @param [String] content the content of the spec file
|
|
59
|
+
# @param [String] example_name the name of example to find
|
|
60
|
+
# @return [Array<String, Integer>] first value holds the matched line, the second value holds the line number of matched line
|
|
61
|
+
def find_example_match_lines(content, example_name)
|
|
62
|
+
lines = content.split("\n")
|
|
63
|
+
|
|
64
|
+
matched_lines = []
|
|
65
|
+
|
|
66
|
+
lines.each_with_index do |line, line_index|
|
|
67
|
+
string_within_quotes = spec_desc_string_within_quotes(line)
|
|
68
|
+
|
|
69
|
+
matched_lines << [line, line_index] if string_within_quotes && example_name.include?(string_within_quotes)
|
|
70
|
+
rescue StandardError => e
|
|
71
|
+
puts "Error: #{e}"
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
matched_lines
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Update the provided matched_line with content from the block if given
|
|
78
|
+
#
|
|
79
|
+
# @param [Array<String, Integer>] matched_line first value holds the line content, the second value holds the line number
|
|
80
|
+
# @param [String] content full orignal content of the spec file
|
|
81
|
+
# @return [Array<String, Integer>] first value holds the new content, the second value holds the line number of the test
|
|
82
|
+
def update_matched_line(matched_line, content)
|
|
83
|
+
lines = content.split("\n")
|
|
84
|
+
|
|
85
|
+
begin
|
|
86
|
+
resulting_line = block_given? ? yield(matched_line[0]) : matched_line[0]
|
|
87
|
+
lines[matched_line[1]] = resulting_line
|
|
88
|
+
rescue StandardError => e
|
|
89
|
+
puts "Error: #{e}"
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
[lines.join("\n") << "\n", matched_line[1]]
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Create a branch from the ref
|
|
96
|
+
#
|
|
97
|
+
# @param [String] name_prefix the prefix to attach to the branch name
|
|
98
|
+
# @param [String] example_name the example
|
|
99
|
+
# @return [Gitlab::ObjectifiedHash] the new branch
|
|
100
|
+
def create_branch(name_prefix, example_name, ref)
|
|
101
|
+
branch_name = [name_prefix, example_name.gsub(/\W/, '-')]
|
|
102
|
+
@branches_client ||= (dry_run ? GitlabClient::BranchesDryClient : GitlabClient::BranchesClient).new(token: token, project: project)
|
|
103
|
+
@branches_client.create(branch_name.join('-'), ref)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Commit changes to a branch
|
|
107
|
+
#
|
|
108
|
+
# @param [Gitlab::ObjectifiedHash] branch the branch to commit to
|
|
109
|
+
# @param [String] message the message to commit
|
|
110
|
+
# @param [String] new_content the new content to commit
|
|
111
|
+
# @return [Gitlab::ObjectifiedHash] the commit
|
|
112
|
+
def commit_changes(branch, message, file_path, new_content)
|
|
113
|
+
@commits_client ||= (dry_run ? GitlabClient::CommitsDryClient : GitlabClient::CommitsClient)
|
|
114
|
+
.new(token: token, project: project)
|
|
115
|
+
@commits_client.create(branch['name'], file_path, new_content, message)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Create a Merge Request with a given branch
|
|
119
|
+
#
|
|
120
|
+
# @param [String] title_prefix the prefix of the title
|
|
121
|
+
# @param [String] example_name the example
|
|
122
|
+
# @param [Gitlab::ObjectifiedHash] branch the branch
|
|
123
|
+
# @param [Integer] assignee_id
|
|
124
|
+
# @param [Array<Integer>] reviewer_ids
|
|
125
|
+
# @param [String] labels comma seperated list of labels
|
|
126
|
+
# @return [Gitlab::ObjectifiedHash] the created merge request
|
|
127
|
+
def create_merge_request(title, branch, assignee_id = nil, reviewer_ids = [], labels = '')
|
|
128
|
+
description = yield
|
|
129
|
+
|
|
130
|
+
merge_request_client.create_merge_request(
|
|
131
|
+
title: title,
|
|
132
|
+
source_branch: branch['name'],
|
|
133
|
+
target_branch: ref,
|
|
134
|
+
description: description,
|
|
135
|
+
labels: labels,
|
|
136
|
+
assignee_id: assignee_id,
|
|
137
|
+
reviewer_ids: reviewer_ids)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Check if issue is closed
|
|
141
|
+
#
|
|
142
|
+
# @param [Gitlab::ObjectifiedHash] issue the issue
|
|
143
|
+
# @return [Boolean] True or False
|
|
144
|
+
def issue_is_closed?(issue)
|
|
145
|
+
issue['state'] == 'closed'
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Get scoped label from issue
|
|
149
|
+
#
|
|
150
|
+
# @param [Gitlab::ObjectifiedHash] issue the issue
|
|
151
|
+
# @param [String] scope
|
|
152
|
+
# @return [String] scoped label
|
|
153
|
+
def issue_scoped_label(issue, scope)
|
|
154
|
+
issue['labels'].detect { |label| label.match(/#{scope}::/) }
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Fetch an issue
|
|
158
|
+
#
|
|
159
|
+
# @param [String] iid: The iid of the issue
|
|
160
|
+
# @return [Gitlab::ObjectifiedHash]
|
|
161
|
+
def fetch_issue(iid:)
|
|
162
|
+
issue_client.find_issues(iid: iid).first
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Post note on report_issue
|
|
166
|
+
#
|
|
167
|
+
# @param [String] note the note to post
|
|
168
|
+
# @return [Gitlab::ObjectifiedHash]
|
|
169
|
+
def post_note_on_report_issue(note)
|
|
170
|
+
iid = report_issue&.split('/')&.last # split url segment, last segment of path is the issue id
|
|
171
|
+
if iid
|
|
172
|
+
issue_client.create_issue_note(iid: iid, note: note)
|
|
173
|
+
else
|
|
174
|
+
Runtime::Logger.info("#{self.class.name}##{__method__} Note was NOT posted on report issue: #{report_issue}")
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Post a note of merge reqest
|
|
179
|
+
#
|
|
180
|
+
# @param [String] note
|
|
181
|
+
# @param [Integer] merge_request_iid
|
|
182
|
+
# @return [Gitlab::ObjectifiedHash]
|
|
183
|
+
def post_note_on_merge_request(note, merge_request_iid)
|
|
184
|
+
merge_request_client.create_note(note: note, merge_request_iid: merge_request_iid)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Fetch the id for the dri of the product group and stage
|
|
188
|
+
# The first item returned is the id of the assignee and the second item is the handle
|
|
189
|
+
#
|
|
190
|
+
# @param [String] product_group
|
|
191
|
+
# @param [String] devops_stage
|
|
192
|
+
# @return [Array<Integer, String>]
|
|
193
|
+
def fetch_dri_id(product_group, devops_stage)
|
|
194
|
+
assignee_handle = ENV.fetch('QA_TEST_DRI_HANDLE', nil) || set_dri_via_group(product_group, devops_stage)
|
|
195
|
+
|
|
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)
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# Post a message on Slack
|
|
208
|
+
#
|
|
209
|
+
# @param [String] message the message to post
|
|
210
|
+
# @return [HTTP::Response]
|
|
211
|
+
def post_message_on_slack(message)
|
|
212
|
+
channel = ENV.fetch('SLACK_QA_CHANNEL', nil) || TEST_PLATFORM_MAINTAINERS_SLACK_CHANNEL_ID
|
|
213
|
+
slack_options = {
|
|
214
|
+
slack_webhook_url: ENV.fetch('CI_SLACK_WEBHOOK_URL', nil),
|
|
215
|
+
channel: channel,
|
|
216
|
+
username: "GitLab Quality Test Tooling",
|
|
217
|
+
icon_emoji: ':warning:',
|
|
218
|
+
message: message
|
|
219
|
+
}
|
|
220
|
+
puts "Posting Slack message to channel: #{channel}"
|
|
221
|
+
|
|
222
|
+
(dry_run ? GitlabQuality::TestTooling::Slack::PostToSlackDry : GitlabQuality::TestTooling::Slack::PostToSlack).new(**slack_options).invoke!
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# Provide indentaiton based on the given line
|
|
226
|
+
#
|
|
227
|
+
# @param[String] line the line to use for indentation
|
|
228
|
+
# @return[String] indentation
|
|
229
|
+
def indentation(line)
|
|
230
|
+
# Indent the same number of spaces as the current line
|
|
231
|
+
no_of_spaces = line[/\A */].size
|
|
232
|
+
# If the first char on current line is not a quote, add two more spaces
|
|
233
|
+
no_of_spaces += /['"]/.match?(line.lstrip[0]) ? 0 : 2
|
|
234
|
+
|
|
235
|
+
" " * no_of_spaces
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
# Returns and existing merge request with the given title
|
|
239
|
+
#
|
|
240
|
+
# @param [String] title: Title of the merge request
|
|
241
|
+
# @return [Array<Gitlab::ObjectifiedHash>] Merge requests
|
|
242
|
+
def existing_merge_requests(title:)
|
|
243
|
+
merge_request_client.find(options: { search: title, in: 'title', state: 'opened' })
|
|
244
|
+
end
|
|
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
|
+
|
|
274
|
+
private
|
|
275
|
+
|
|
276
|
+
attr_reader :token, :specs_file, :dry_run, :processor
|
|
277
|
+
|
|
278
|
+
# Returns any test description string within single or double quotes
|
|
279
|
+
#
|
|
280
|
+
# @param [String] line the line to check for any quoted string
|
|
281
|
+
# @return [String] the match or nil if no match
|
|
282
|
+
def spec_desc_string_within_quotes(line)
|
|
283
|
+
match = line.match(/(?:it|describe|context|\s)+ ['"]([^'"]*)['"]/)
|
|
284
|
+
match ? match[1] : nil
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
# Returns the GitlabIssueClient or GitlabIssueDryClient based on the value of dry_run
|
|
288
|
+
#
|
|
289
|
+
# @return [GitlabIssueDryClient | GitlabIssueClient]
|
|
290
|
+
def issue_client
|
|
291
|
+
@issue_client ||= (dry_run ? GitlabClient::IssuesDryClient : GitlabClient::IssuesClient).new(token: token, project: project)
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
# Returns the MergeRequestDryClient or MergeRequest based on the value of dry_run
|
|
295
|
+
#
|
|
296
|
+
# @return [MergeRequestDryClient | MergeRequest]
|
|
297
|
+
def merge_request_client
|
|
298
|
+
@merge_request_client ||= (dry_run ? GitlabClient::MergeRequestsDryClient : GitlabClient::MergeRequestsClient).new(
|
|
299
|
+
token: token,
|
|
300
|
+
project: project
|
|
301
|
+
)
|
|
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
|
|
310
|
+
end
|
|
311
|
+
end
|
|
312
|
+
end
|
|
313
|
+
end
|