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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 30ae8847a7ab0729d5e877f42348713b93f0354c6ea0961f9e5b8cd44a8c18e6
4
- data.tar.gz: e8f469de2c467669cc6ebe5cd2c25a8707749b0901f2165049249962bcbb0868
3
+ metadata.gz: dcdbf54c2eb2a930389aafac447765559b7d0303bfe98eb2c1fdf54399ee03e7
4
+ data.tar.gz: a475b92cef079c62dd767b155f9653222f8d804e393db5cfb5e044d08aae85e7
5
5
  SHA512:
6
- metadata.gz: 8f430addea12b346909efd5568a85d9232fd6f4969258d0aaa5cb9e8344fab58791378734e2c89d58069044739218bd45155ef293487b9a93b95ae5185ed55c7
7
- data.tar.gz: 190392762988fa1273a0e32aa6f74aa9f467879a93729e7a139204e09fb5206cab90e3fecca13a51734bd8917bb2b153e907fc869a3274534f79f163c1dd8d7b
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, :plugin_options
11
+ attr_reader :parser, :plugin_arguments
12
12
 
13
13
  def initialize(*options)
14
- @plugins = []
15
- @plugin_options = []
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
- @plugin_options = options[options.index('--')..]
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|
@@ -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 fetch from
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' # the default Git ref used when updating
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: '%s', type: :investigating })
21
-
22
- def initialize(**args)
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.each do |failed_example|
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
- number_of_failures = @examples.select { _1.name == failed_example && _1.failed? }.size
41
- if number_of_failures >= threshold
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} (#{number_of_failures} >= #{threshold})", 3
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
- branch = create_branch(failure_issue, example)
60
- commit_changes(branch, example, new_contents)
61
- create_merge_request(example, branch, changed_line_no, failure_issue)
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
- content_to_return = content.dup
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
- best_match = -1
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
- # the matched line where the example is
163
- match = content_to_return[best_match..].split("\n").first
164
- matched_line_no = 0
165
-
166
- lines.each_with_index do |line, line_no|
167
- if line.match?(match)
168
- matched_line_no = line_no + 1
169
-
170
- log :rspec, "Found match on line #{example.file}:#{matched_line_no}", 4
171
-
172
- if line.include?(',')
173
- line[line.index(',')] = QUARANTINE_METADATA % failure_issue['web_url'] << ','
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[line.rindex(' ')] = QUARANTINE_METADATA % failure_issue['web_url'] << ' '
338
+ line
176
339
  end
177
- end
178
- end
340
+ end,
179
341
 
180
- [lines.join("\n") << "\n", matched_line_no]
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
- branch = @gitlab_client.create_branch(target_project, "#{failure_issue['iid']}-quarantine-#{example.name.tr(' ', '-')}", ref)
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, changed_line_number, failure_issue)
222
- occurrences = @examples.select { _1.name == example.name && _1.failed? }
223
- markdown_occurrences = []
383
+ def create_merge_request(title_prefix, example, branch, &block)
384
+ description = block.call
224
385
 
225
- @examples.map do |eggsample|
226
- if eggsample.name == example.name && eggsample.failed?
227
- markdown_occurrences << "1. [#{eggsample.occurrence[:job]}](#{eggsample.occurrence[:pipeline_url]})"
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
- description = <<~MARKDOWN
232
- ## What does this MR do?
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
- Quarantines the test `#{example.name}` (https://gitlab.com/#{target_project}/-/blob/#{ref}/#{example.file}#L#{changed_line_number})
397
+ log :mr, "Created merge request #{merge_request['iid']} (#{merge_request['web_url']})", 4
235
398
 
236
- This test has been found by [Confiner](https://gitlab.com/gitlab-org/quality/confiner) to have been failing for
237
- more than (or equal to) #{threshold} times. This test has failed #{occurrences.size} times.
399
+ merge_request
400
+ end
238
401
 
239
- #{markdown_occurrences.join("\n")}
402
+ private
240
403
 
241
- > #{failure_issue['web_url']}
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
- <div align="center">
244
- (This MR was automatically generated by [Confiner](https://gitlab.com/gitlab-org/quality/confiner) at #{Time.now.utc})
245
- </div>
246
- MARKDOWN
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
- merge_request = @gitlab_client.create_merge_request(target_project, MERGE_REQUEST_TITLE % example.name, source_branch: branch['name'], target_branch: ref, description: description, labels: failure_issue_labels, squash: true, remove_source_branch: true)
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
- log :mr, "Created merge request #{merge_request['iid']} (#{merge_request['web_url']})", 4
426
+ lines.each_with_index do |line, line_no|
427
+ if line.match?(match)
428
+ matched_line_no = line_no + 1
251
429
 
252
- merge_request
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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Confiner
4
+ VERSION = '0.3.0'
5
+ end
data/lib/confiner.rb CHANGED
@@ -7,7 +7,5 @@ loader.push_dir(__dir__)
7
7
  loader.setup
8
8
 
9
9
  module Confiner
10
- VERSION = '0.2.1'
11
-
12
10
  module Plugins; end
13
11
  end
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.2.1
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-01-07 00:00:00.000000000 Z
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