confiner 0.2.1 → 0.3.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/lib/confiner/cli.rb +15 -7
- data/lib/confiner/plugin.rb +9 -1
- data/lib/confiner/plugins/gitlab.rb +260 -75
- data/lib/confiner/version.rb +5 -0
- data/lib/confiner.rb +0 -2
- metadata +17 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: dcdbf54c2eb2a930389aafac447765559b7d0303bfe98eb2c1fdf54399ee03e7
|
4
|
+
data.tar.gz: a475b92cef079c62dd767b155f9653222f8d804e393db5cfb5e044d08aae85e7
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5ab67f9debe3399df85e64a246481d2b4fa50a5056de956af84203ab34e818d5b4d61869526a4b9ff84eb7cb4a7d3317d7967d6afcfee18c903385f5f82f9595
|
7
|
+
data.tar.gz: 06bc73016ec031a83b158cda3d91442d9cb6c24e42495365fe9c9010e05d12a9fc420f09108cfc1b35e70d3a53d12e4ae11075501c25a56035befb614f3233dd
|
data/lib/confiner/cli.rb
CHANGED
@@ -8,19 +8,19 @@ module Confiner
|
|
8
8
|
include Logger
|
9
9
|
|
10
10
|
attr_accessor :action, :plugins
|
11
|
-
attr_reader :parser, :
|
11
|
+
attr_reader :parser, :plugin_arguments
|
12
12
|
|
13
13
|
def initialize(*options)
|
14
|
-
@
|
15
|
-
@
|
16
|
-
@rules_files = []
|
14
|
+
@plugin_arguments = [] # arguments to be passed to the plugin(s)
|
15
|
+
@rules_files = [] # which files were loaded
|
17
16
|
@rules = []
|
17
|
+
@plugin_options = { debug: false, dry_run: false }
|
18
18
|
|
19
19
|
# default logging to standard out
|
20
20
|
Logger.log_to = $stdout
|
21
21
|
|
22
22
|
if options.include?('--')
|
23
|
-
@
|
23
|
+
@plugin_arguments = options[options.index('--')..]
|
24
24
|
options = options[0..options.index('--')]
|
25
25
|
end
|
26
26
|
|
@@ -64,6 +64,14 @@ module Confiner
|
|
64
64
|
exit(0)
|
65
65
|
end
|
66
66
|
|
67
|
+
opts.on('--dry-run', 'Dry run') do
|
68
|
+
@plugin_options[:dry_run] = true
|
69
|
+
end
|
70
|
+
|
71
|
+
opts.on('--debug', 'Toggle debug mode') do
|
72
|
+
@plugin_options[:debug] = true
|
73
|
+
end
|
74
|
+
|
67
75
|
opts.on('-o OUTPUT', '--output-to OUTPUT', 'File to output the log to') do |output_to|
|
68
76
|
Logger.log_to = output_to
|
69
77
|
end
|
@@ -111,9 +119,9 @@ module Confiner
|
|
111
119
|
log :rule, rule.keys.map { |k| "\t#{k}=#{rule[k]}" }.join(',')
|
112
120
|
|
113
121
|
rule['plugin']['args'] ||= {}
|
114
|
-
rule['plugin']['args'].transform_keys!(&:to_sym) # 2.5 compatability
|
122
|
+
rule['plugin']['args'].transform_keys!(&:to_sym) # ruby 2.5 compatability
|
115
123
|
|
116
|
-
plugin = Plugins.const_get(translate_plugin_name(rule['plugin']['name'])).new(**rule['plugin']['args'])
|
124
|
+
plugin = Plugins.const_get(translate_plugin_name(rule['plugin']['name'])).new(@plugin_options, **rule['plugin']['args'])
|
117
125
|
|
118
126
|
# perform verification of actions before execution
|
119
127
|
rule['actions'].each do |action|
|
data/lib/confiner/plugin.rb
CHANGED
@@ -7,6 +7,11 @@ module Confiner
|
|
7
7
|
|
8
8
|
attr_reader :examples
|
9
9
|
|
10
|
+
DEFAULT_PLUGIN_ARGS = {
|
11
|
+
debug: false, # Used for debugging logging
|
12
|
+
dry_run: false # Used for when external requests are inhibited
|
13
|
+
}.freeze
|
14
|
+
|
10
15
|
class << self
|
11
16
|
# Define arguments that the plugin will accept
|
12
17
|
# @param [Array<Symbol, Hash>] args the arguments that this plugin accepts
|
@@ -35,7 +40,10 @@ module Confiner
|
|
35
40
|
end
|
36
41
|
end
|
37
42
|
|
38
|
-
def initialize(**args)
|
43
|
+
def initialize(options, **args)
|
44
|
+
@options = DEFAULT_PLUGIN_ARGS.merge(options)
|
45
|
+
@options = Struct.new(*@options.keys).new(*@options.values)
|
46
|
+
|
39
47
|
args.each do |k, v|
|
40
48
|
v = ENV[v[1..]] if v[0] == '$' # get environment variable
|
41
49
|
|
@@ -6,24 +6,52 @@ module Confiner
|
|
6
6
|
module Plugins
|
7
7
|
class Gitlab < Plugin
|
8
8
|
arguments :private_token, # the GitLab API Private-Token to use
|
9
|
-
:project_id, # the project where the pipelines are
|
9
|
+
:project_id, # the project where the pipelines are fetched from
|
10
10
|
:target_project, # where failure issues will be searched, and where an MR will be filed
|
11
11
|
:failure_issue_labels, # what labels to search for when searching for the failure issue
|
12
|
-
:failure_issue_prefix, # what prefix an issue has in GitLab Issues to search for the failure issue
|
12
|
+
:failure_issue_prefix, # what prefix an issue has in GitLab Issues to search for the failure issue (comma separated)
|
13
|
+
:environment => {}, # metadata about the environment the tests are running and which will be confined
|
13
14
|
:timeout => 10, # the timeout that HTTParty will consume (timeout of requests)
|
14
15
|
:threshold => 3, # the failure / pass threshold
|
15
16
|
:endpoint => 'https://gitlab.com/api/v4', # the GitLab API Endpoint (e.g. https://gitlab.com/api/v4)
|
16
17
|
:pwd => '.', # the path of the working directory for the examples
|
17
|
-
:ref => 'main'
|
18
|
+
:ref => 'main', # the default Git ref used when updating
|
19
|
+
:job_pattern => '.+' # the regex pattern to match names of jobs in GitLab (Job = Suite Name)
|
18
20
|
|
19
21
|
MERGE_REQUEST_TITLE = '[QUARANTINE] %s'
|
20
|
-
QUARANTINE_METADATA = %(, quarantine: { issue: '%
|
21
|
-
|
22
|
-
|
22
|
+
QUARANTINE_METADATA = %(, quarantine: { issue: '%{issue_url}', type: :investigating })
|
23
|
+
ONLY_QUARANTINE_METADATA = %(, quarantine: { issue: '%{issue_url}', type: :investigating, only: { %{pattern} } })
|
24
|
+
|
25
|
+
# GitLab Confiner Plugin
|
26
|
+
# @param [Hash] args the arguments for GitLab
|
27
|
+
# @option args [String] :private_token the private token for GitLab to connect to
|
28
|
+
# @option args [String] :project_id the project id (or name) where the pipelines are fetched from
|
29
|
+
# @option args [String] :target_project where failure issues will be searched, and where an MR will be filed
|
30
|
+
# @option args [String] :failure_issue_labels what labels to search for when searching for the failure issue (comma separated)
|
31
|
+
# @option args [String] :failure_issue_prefix what prefix an issue has in GitLab Issues to search for the failure issue
|
32
|
+
#
|
33
|
+
# @option args [Hash] :environment metadata about the environment the tests are running and which will be confined
|
34
|
+
# @option :environment [String] :name the name of the environment
|
35
|
+
# @option :environment [String] :pattern the pattern of how to quarantine/dequarantine
|
36
|
+
#
|
37
|
+
# @option args [Integer] :timeout the timeout that HTTParty will consume (timeout of requests)
|
38
|
+
# @option args [Integer] :threshold the failure / pass threshold
|
39
|
+
# @option args [String] :endpoint the GitLab API Endpoint (e.g. https://gitlab.com/api/v4)
|
40
|
+
# @option args [String] :pwd the path of the working directory for where the tests are located
|
41
|
+
# @option args [String] :ref the default Git ref used when updating
|
42
|
+
# @option args [String] :job_pattern the regex pattern to match names of jobs in GitLab (Job = Suite Name)
|
43
|
+
#
|
44
|
+
# @example
|
45
|
+
# gitlab = Confiner::Plugins::Gitlab.new({ debug: true }, private_token: 'ABC-123', project_id: 'my-group/my-project', target_project: 'my-group/my-project', failure_issue_labels: 'QA,test', failure_issue_prefix: 'Failure in ', timeout: 10, threshold: 3, endpoint: 'https://gitlab.com/api/v4', pwd: 'qa', ref: 'master')
|
46
|
+
# gitlab.quarantine
|
47
|
+
# gitlab.dequarantine
|
48
|
+
def initialize(options, **args)
|
23
49
|
super
|
24
50
|
|
25
51
|
ENV['GITLAB_API_HTTPARTY_OPTIONS'] = ENV.fetch('GITLAB_API_HTTPARTY_OPTIONS') { "{read_timeout: #{timeout}}" }
|
26
52
|
|
53
|
+
raise ArgumentError, 'Missing private_token' if private_token.nil?
|
54
|
+
|
27
55
|
@gitlab_client = ::Gitlab.client(private_token: private_token, endpoint: endpoint)
|
28
56
|
end
|
29
57
|
|
@@ -31,17 +59,23 @@ module Confiner
|
|
31
59
|
def quarantine
|
32
60
|
log :gitlab, 'Beginning Quarantine Process', 2
|
33
61
|
|
62
|
+
if environment&.any?
|
63
|
+
raise ArgumentError, 'Specify both environment[name] and environment[pattern]' unless environment['name'] && environment['pattern']
|
64
|
+
end
|
65
|
+
|
34
66
|
# store the examples from the pipelines
|
35
67
|
@examples = get_examples
|
36
68
|
|
37
|
-
@examples.select(&:failed?).map(&:name).uniq.
|
69
|
+
quarantines = @examples.select(&:failed?).map(&:name).uniq.each_with_object([]) do |failed_example, quarantines|
|
38
70
|
# count the number of failures consecutively for this example
|
39
71
|
|
40
|
-
|
41
|
-
if
|
72
|
+
fails = @examples.select { _1.name == failed_example && _1.failed? }
|
73
|
+
if fails.size >= threshold
|
74
|
+
quarantines << failed_example
|
75
|
+
|
42
76
|
example = @examples.find { _1.name == failed_example }
|
43
77
|
|
44
|
-
log :quarantine, "Quarantining #{failed_example} (#{
|
78
|
+
log :quarantine, "Quarantining #{failed_example} (#{fails.size} >= #{threshold})", 3
|
45
79
|
|
46
80
|
# check to see if there is a merge request first
|
47
81
|
# if there is no merge request...
|
@@ -56,9 +90,45 @@ module Confiner
|
|
56
90
|
file_contents = get_example_file_contents(example)
|
57
91
|
new_contents, changed_line_no = add_quarantine_metadata(file_contents, example, failure_issue)
|
58
92
|
|
59
|
-
|
60
|
-
|
61
|
-
|
93
|
+
log(:debug, new_contents, 4) if @options.debug
|
94
|
+
|
95
|
+
if @options.dry_run
|
96
|
+
log :dry_run, 'Skipping creating branch, committing and filing merge request', 4
|
97
|
+
else
|
98
|
+
branch = create_branch(failure_issue, 'quarantine', example)
|
99
|
+
commit_changes(branch, <<~COMMIT_MESSAGE, example, new_contents)
|
100
|
+
Quarantine end-to-end test
|
101
|
+
|
102
|
+
Quarantine #{example.name}
|
103
|
+
COMMIT_MESSAGE
|
104
|
+
|
105
|
+
create_merge_request('[QUARANTINE]', example, branch) do
|
106
|
+
markdown_occurrences = []
|
107
|
+
|
108
|
+
fails.each do |fail|
|
109
|
+
markdown_occurrences << "1. [#{fail.occurrence[:job]}](#{fail.occurrence[:pipeline_url]})"
|
110
|
+
end
|
111
|
+
|
112
|
+
meets_or_exceeds = fails.size > threshold ? 'exceeds' : 'meets'
|
113
|
+
|
114
|
+
<<~MARKDOWN
|
115
|
+
## What does this MR do?
|
116
|
+
|
117
|
+
Quarantines the test `#{example.name}` (https://gitlab.com/#{target_project}/-/blob/#{ref}/#{example.file}#L#{changed_line_no})
|
118
|
+
|
119
|
+
This test has been found by [Confiner](https://gitlab.com/gitlab-org/quality/confiner) to have failed
|
120
|
+
#{fails.size} times, which #{meets_or_exceeds} the threshold of #{threshold} times.
|
121
|
+
|
122
|
+
#{markdown_occurrences.join("\n")}
|
123
|
+
|
124
|
+
> #{failure_issue['web_url']}
|
125
|
+
|
126
|
+
<div align="center">
|
127
|
+
(This MR was automatically generated by [Confiner](https://gitlab.com/gitlab-org/quality/confiner) at #{Time.now.utc})
|
128
|
+
</div>
|
129
|
+
MARKDOWN
|
130
|
+
end
|
131
|
+
end
|
62
132
|
|
63
133
|
log :quarantine, "Done Quarantining #{failed_example}", 3
|
64
134
|
rescue => e
|
@@ -67,12 +137,95 @@ module Confiner
|
|
67
137
|
end
|
68
138
|
end
|
69
139
|
|
140
|
+
if quarantines.any?
|
141
|
+
log :quarantine, "Found #{quarantines.size} candidates to be quarantined", 3
|
142
|
+
else
|
143
|
+
log :quarantine, 'Found no candidates to be quarantined', 3
|
144
|
+
end
|
145
|
+
|
70
146
|
log :gitlab, 'Done with Quarantine Process', 2
|
71
147
|
end
|
72
148
|
|
73
149
|
# Dequarantine Action - Automatically Dequarantine tests
|
74
150
|
def dequarantine
|
151
|
+
log :gitlab, 'Beginning Dequarantine Process', 2
|
152
|
+
|
153
|
+
@examples = get_examples
|
154
|
+
|
155
|
+
dequarantines = @examples.select(&:passed?).map(&:name).uniq.each_with_object([]) do |passed_example, dequarantines|
|
156
|
+
passes = @examples.select { _1.name == passed_example && _1.passed? }
|
157
|
+
fails = @examples.select { _1.name == passed_example && _1.failed? }
|
158
|
+
|
159
|
+
if passes.size >= threshold
|
160
|
+
next log(:dequarantine, "Detected #{fails.size} failures for #{passed_example}, thus, not de-quarantining", 3) unless fails.size.zero?
|
161
|
+
|
162
|
+
dequarantines << passed_example
|
163
|
+
|
164
|
+
example = @examples.find { _1.name == passed_example }
|
165
|
+
|
166
|
+
log :dequarantine, "Dequarantining #{example} (#{passes.size} >= #{threshold})", 3
|
167
|
+
|
168
|
+
begin
|
169
|
+
file_contents = get_example_file_contents(example)
|
170
|
+
new_contents, changed_line_no, failure_issue = remove_quarantine_metadata(file_contents, example)
|
171
|
+
|
172
|
+
next log(:warn, <<~MESSAGE.tr("\n", ' '), 4) if file_contents == new_contents
|
173
|
+
Unable to dequarantine. This is likely due to the quarantine being applied to the outer context.
|
174
|
+
See https://gitlab.com/gitlab-org/quality/confiner/-/issues/3
|
175
|
+
MESSAGE
|
176
|
+
|
177
|
+
if @options.dry_run
|
178
|
+
log :dry_run, 'Skipping creating branch, committing and filing merge request', 4
|
179
|
+
else
|
180
|
+
branch = create_branch(failure_issue, 'dequarantine', example)
|
181
|
+
commit_changes(branch, <<~COMMIT_MESSAGE, example, new_contents)
|
182
|
+
Dequarantine end-to-end test
|
183
|
+
|
184
|
+
Dequarantine #{example.name}
|
185
|
+
COMMIT_MESSAGE
|
186
|
+
|
187
|
+
create_merge_request('[DEQUARANTINE]',example, branch) do
|
188
|
+
markdown_occurrences = []
|
189
|
+
|
190
|
+
passes.each do |pass|
|
191
|
+
markdown_occurrences << "1. [#{pass.occurrence[:job]}](#{pass.occurrence[:pipeline_url]})"
|
192
|
+
end
|
193
|
+
|
194
|
+
meets_or_exceeds = passes.size > threshold ? 'exceeds' : 'meets'
|
195
|
+
|
196
|
+
<<~MARKDOWN
|
197
|
+
## What does this MR do?
|
198
|
+
|
199
|
+
Dequarantines the test `#{example.name}` (https://gitlab.com/#{target_project}/-/blob/#{ref}/#{example.file}#L#{changed_line_no})
|
200
|
+
|
201
|
+
This quarantined test has been found by [Confiner](https://gitlab.com/gitlab-org/quality/confiner) to have passed
|
202
|
+
#{passes.size} times consecutively, which #{meets_or_exceeds} the threshold of #{threshold} times.
|
203
|
+
|
204
|
+
#{markdown_occurrences.join("\n")}
|
205
|
+
|
206
|
+
> #{failure_issue}
|
207
|
+
|
208
|
+
<div align="center">
|
209
|
+
(This MR was automatically generated by [Confiner](https://gitlab.com/gitlab-org/quality/confiner) at #{Time.now.utc})
|
210
|
+
</div>
|
211
|
+
MARKDOWN
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
log :dequarantine, "Done Dequarantining #{passed_example}", 3
|
216
|
+
rescue => e
|
217
|
+
log :fatal, "There was an issue dequarantining #{example}. Error was #{e.message}\n#{e.backtrace}"
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
if dequarantines.any?
|
223
|
+
log :dequarantine, "Found #{dequarantines.size} candidates to be dequarantined", 3
|
224
|
+
else
|
225
|
+
log :dequarantine, 'Found no candidates to be dequarantined', 3
|
226
|
+
end
|
75
227
|
|
228
|
+
log :gitlab, 'Done with Dequarantine Process', 2
|
76
229
|
end
|
77
230
|
|
78
231
|
private
|
@@ -84,21 +237,24 @@ module Confiner
|
|
84
237
|
def get_last_n_runs(threshold:)
|
85
238
|
pipelines = [] # collection of both passing and failing pipelines
|
86
239
|
|
87
|
-
pipelines << @gitlab_client.pipelines(project_id, per_page: threshold, status: :success)
|
88
|
-
pipelines << @gitlab_client.pipelines(project_id, per_page: threshold, status: :failed)
|
240
|
+
pipelines << @gitlab_client.pipelines(project_id, per_page: threshold, status: :success, ref: ref)
|
241
|
+
pipelines << @gitlab_client.pipelines(project_id, per_page: threshold, status: :failed, ref: ref)
|
89
242
|
end
|
90
243
|
|
91
244
|
# Get examples from pipelines
|
92
245
|
# @param [Integer] threshold the amount of pipelines to fetch
|
93
246
|
# @note The threshold defaults to default threshold multiplied by two to the amount of pipelines
|
94
247
|
# @return [Array<Example>] array of examples
|
95
|
-
def get_examples(threshold: self.threshold * 2)
|
248
|
+
def get_examples(threshold: self.threshold * 2, job_pattern: Regexp.new(self.job_pattern))
|
96
249
|
examples = []
|
97
250
|
|
98
251
|
get_last_n_runs(threshold: threshold).each do |run|
|
99
252
|
run.each do |pipeline|
|
100
253
|
# fetch the pipeline test report
|
101
254
|
@gitlab_client.pipeline_test_report(project_id, pipeline['id'])['test_suites'].each do |suite|
|
255
|
+
# skip if the job name does not match the job_pattern argument
|
256
|
+
next log(:skip, "Skipping #{suite['name']}", 4) unless suite['name'].match(job_pattern)
|
257
|
+
|
102
258
|
log :suite, "Suite: #{suite['name']} (#{pipeline['web_url']})", 4
|
103
259
|
suite['test_cases'].each do |example|
|
104
260
|
examples << Example.new(**example, occurrence: { job: suite['name'], pipeline_url: pipeline['web_url'] })
|
@@ -147,44 +303,56 @@ module Confiner
|
|
147
303
|
# @param [Gitlab::ObjectifiedHash] failure_issue the failure issue
|
148
304
|
# @return [Array<String, Integer>] first value holds the new content, the second value holds the line number of the test
|
149
305
|
def add_quarantine_metadata(content, example, failure_issue)
|
150
|
-
|
306
|
+
find_example_match_in_contents(content, example) do |line|
|
307
|
+
if line.include?(',')
|
308
|
+
line[line.index(',')] = if environment['name'] && environment['pattern']
|
309
|
+
ONLY_QUARANTINE_METADATA % { issue_url: failure_issue['web_url'], pattern: environment['pattern'] }
|
310
|
+
else
|
311
|
+
QUARANTINE_METADATA % { issue_url: failure_issue['web_url'] }
|
312
|
+
end << ','
|
313
|
+
else
|
314
|
+
line[line.rindex(' ')] = if environment['name'] && environment['pattern']
|
315
|
+
ONLY_QUARANTINE_METADATA % { issue_url: failure_issue['web_url'], pattern: environment['pattern'] }
|
316
|
+
else
|
317
|
+
QUARANTINE_METADATA % { issue_url: failure_issue['web_url'] }
|
318
|
+
end << ' '
|
319
|
+
end
|
151
320
|
|
152
|
-
|
153
|
-
lines = content_to_return.split("\n")
|
154
|
-
example.name.split.reverse.each do |word|
|
155
|
-
# given a full-descriptive RSpec example name:
|
156
|
-
# Plan Group Iterations creates a group iteration
|
157
|
-
# scan these words backwards, incrementing the index until we find the best match
|
158
|
-
new_match = content_to_return.index(word)
|
159
|
-
best_match = new_match if new_match > best_match
|
321
|
+
line
|
160
322
|
end
|
323
|
+
end
|
161
324
|
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
line
|
325
|
+
# Add dequarantine metadata to the file content and replace it
|
326
|
+
# @param [String] content the content to
|
327
|
+
# @param [Example] example the example to find and replace
|
328
|
+
# @return [Array<(String, Integer, String)>]
|
329
|
+
def remove_quarantine_metadata(content, example)
|
330
|
+
issue = +''
|
331
|
+
|
332
|
+
[
|
333
|
+
find_example_match_in_contents(content, example) do |line|
|
334
|
+
if line.include?('quarantine:')
|
335
|
+
issue = line[/issue:( *)?'(.+)',/, 2]# pluck the issue URL from the line before removing
|
336
|
+
line.gsub(/,( *)quarantine:( *)?{.+}/, '')
|
174
337
|
else
|
175
|
-
line
|
338
|
+
line
|
176
339
|
end
|
177
|
-
end
|
178
|
-
end
|
340
|
+
end,
|
179
341
|
|
180
|
-
|
342
|
+
issue
|
343
|
+
].flatten
|
181
344
|
end
|
182
345
|
|
183
346
|
# Create a branch from the ref
|
184
|
-
# @param [Gitlab::ObjectifiedHash] failure_issue the existing failure issue
|
347
|
+
# @param [Gitlab::ObjectifiedHash, String] failure_issue the existing failure issue fetched from the GitLab API, or the full URL
|
348
|
+
# @param [String] name_prefix the prefix to attach to the branch name
|
349
|
+
# @param [Confiner::Example] example the example
|
185
350
|
# @return [Gitlab::ObjectifiedHash] the new branch
|
186
|
-
def create_branch(failure_issue, example)
|
187
|
-
|
351
|
+
def create_branch(failure_issue, name_prefix, example)
|
352
|
+
issue_id = failure_issue.is_a?(String)? failure_issue.split('/').last : failure_issue['iid'] # split url segment, last segment of path is the issue id
|
353
|
+
branch_name = [issue_id, name_prefix, environment['name'] || '', example.name.gsub(/\W/, '-')]
|
354
|
+
|
355
|
+
branch = @gitlab_client.create_branch(target_project, branch_name.join('-'), ref)
|
188
356
|
|
189
357
|
log :branch, "Created branch #{branch['name']} (#{branch['web_url']})", 4
|
190
358
|
|
@@ -193,16 +361,11 @@ module Confiner
|
|
193
361
|
|
194
362
|
# Commit changes to a branch
|
195
363
|
# @param [Gitlab::ObjectifiedHash] branch the branch to commit to
|
364
|
+
# @param [String] message the message to commit
|
196
365
|
# @param [Example] example the example
|
197
366
|
# @param [Gitlab::ObjectifiedHash] new_content the new content to commit
|
198
367
|
# @return [Gitlab::ObjectifiedHash] the commit
|
199
|
-
def commit_changes(branch, example, new_content)
|
200
|
-
message = <<~COMMIT
|
201
|
-
Quarantine end-to-end test
|
202
|
-
|
203
|
-
Quarantine #{example.name}
|
204
|
-
COMMIT
|
205
|
-
|
368
|
+
def commit_changes(branch, message, example, new_content)
|
206
369
|
commit = @gitlab_client.create_commit(target_project, branch['name'], message, [
|
207
370
|
{ action: :update, file_path: example.file, content: new_content}
|
208
371
|
])
|
@@ -213,43 +376,65 @@ module Confiner
|
|
213
376
|
end
|
214
377
|
|
215
378
|
# Create a Merge Request with a given branch
|
379
|
+
# @param [String] title_prefix the prefix of the title
|
216
380
|
# @param [Example] example the example
|
217
381
|
# @param [Gitlab::ObjectifiedHash] branch the branch
|
218
|
-
# @param [Integer] changed_line_number the line that was changed to quarantine
|
219
|
-
# @param [Gitlab::ObjectifiedHash] failure_issue the failure issue
|
220
382
|
# @return [Gitlab::ObjectifiedHash] the created merge request
|
221
|
-
def create_merge_request(example, branch,
|
222
|
-
|
223
|
-
markdown_occurrences = []
|
383
|
+
def create_merge_request(title_prefix, example, branch, &block)
|
384
|
+
description = block.call
|
224
385
|
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
end
|
229
|
-
end
|
386
|
+
environment_name = environment['name'] ? "[#{environment['name']}]" : ''
|
387
|
+
merge_request = @gitlab_client.create_merge_request(target_project,
|
388
|
+
"%{prefix} %{environment_name} %{example_name}" % { prefix: title_prefix, environment_name: environment_name, example_name: example.name },
|
230
389
|
|
231
|
-
|
232
|
-
|
390
|
+
source_branch: branch['name'],
|
391
|
+
target_branch: ref,
|
392
|
+
description: description,
|
393
|
+
labels: failure_issue_labels,
|
394
|
+
squash: true,
|
395
|
+
remove_source_branch: true)
|
233
396
|
|
234
|
-
|
397
|
+
log :mr, "Created merge request #{merge_request['iid']} (#{merge_request['web_url']})", 4
|
235
398
|
|
236
|
-
|
237
|
-
|
399
|
+
merge_request
|
400
|
+
end
|
238
401
|
|
239
|
-
|
402
|
+
private
|
240
403
|
|
241
|
-
|
404
|
+
# Find an example in file contents and transform the line
|
405
|
+
# @param [String] content the file contents
|
406
|
+
# @param [Confiner::Example] example the name of the example
|
407
|
+
# @yield [String] the line of the contents found
|
408
|
+
# @return [Array<String, Integer>] first item return is the new file contents. second item is the line number where the match was found
|
409
|
+
def find_example_match_in_contents(content, example)
|
410
|
+
content_to_return = content.dup
|
242
411
|
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
412
|
+
best_match = -1
|
413
|
+
lines = content_to_return.split("\n")
|
414
|
+
example.name.split.reverse.each do |word|
|
415
|
+
# given a full-descriptive RSpec example name:
|
416
|
+
# Plan Group Iterations creates a group iteration
|
417
|
+
# scan these words backwards, incrementing the index until we find the best match
|
418
|
+
new_match = content_to_return.index(word) || -1
|
419
|
+
best_match = new_match if new_match > best_match
|
420
|
+
end
|
247
421
|
|
248
|
-
|
422
|
+
# the matched line where the example is
|
423
|
+
match = content_to_return[best_match..].split("\n").first
|
424
|
+
matched_line_no = 0
|
249
425
|
|
250
|
-
|
426
|
+
lines.each_with_index do |line, line_no|
|
427
|
+
if line.match?(match)
|
428
|
+
matched_line_no = line_no + 1
|
251
429
|
|
252
|
-
|
430
|
+
log :rspec, "Found match on line #{example.file}:#{matched_line_no}", 4
|
431
|
+
|
432
|
+
resulting_line = block_given? ? yield(line) : line
|
433
|
+
lines[line_no] = resulting_line
|
434
|
+
end
|
435
|
+
end
|
436
|
+
|
437
|
+
[lines.join("\n") << "\n", matched_line_no]
|
253
438
|
end
|
254
439
|
end
|
255
440
|
end
|
data/lib/confiner.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: confiner
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- GitLab Quality
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-
|
11
|
+
date: 2022-03-22 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rspec
|
@@ -24,6 +24,20 @@ dependencies:
|
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: 3.10.0
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rspec-parameterized
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 0.5.1
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 0.5.1
|
27
41
|
- !ruby/object:Gem::Dependency
|
28
42
|
name: zeitwerk
|
29
43
|
requirement: !ruby/object:Gem::Requirement
|
@@ -68,6 +82,7 @@ files:
|
|
68
82
|
- lib/confiner/plugin.rb
|
69
83
|
- lib/confiner/plugins/debug.rb
|
70
84
|
- lib/confiner/plugins/gitlab.rb
|
85
|
+
- lib/confiner/version.rb
|
71
86
|
homepage: https://gitlab.com/gitlab-org/quality/confiner
|
72
87
|
licenses:
|
73
88
|
- MIT
|