confiner 0.2.1 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|