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 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