confiner 0.2.3 → 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: 6e9601fa7e82b448423188a4dde817864c6f7f57f1a1efd9aae67c1b2e8708c7
4
- data.tar.gz: 23b2a999e52c58e5081fc9f4857e45a5a0c1783c0b1eca373f4ad4b57394a97f
3
+ metadata.gz: dcdbf54c2eb2a930389aafac447765559b7d0303bfe98eb2c1fdf54399ee03e7
4
+ data.tar.gz: a475b92cef079c62dd767b155f9653222f8d804e393db5cfb5e044d08aae85e7
5
5
  SHA512:
6
- metadata.gz: 48ad12ecefe1473ec4d481f3ebce5d325b9a0a052af1705a9a69621ea78a6d2fb5461fa4a068a83808943dc055f71fec936f54764894bc7a94de6e810e6bbb68
7
- data.tar.gz: f74349e0a4003152ab16e7e86b4c34b77c58376ad7550fdf1796d4f7bd9633f0fc177abc56133607c4ce3c6108ca43b4598eacaf44eaa029c6127e301f80410b
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,20 +6,46 @@ 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}}" }
@@ -33,19 +59,23 @@ module Confiner
33
59
  def quarantine
34
60
  log :gitlab, 'Beginning Quarantine Process', 2
35
61
 
62
+ if environment&.any?
63
+ raise ArgumentError, 'Specify both environment[name] and environment[pattern]' unless environment['name'] && environment['pattern']
64
+ end
65
+
36
66
  # store the examples from the pipelines
37
67
  @examples = get_examples
38
68
 
39
69
  quarantines = @examples.select(&:failed?).map(&:name).uniq.each_with_object([]) do |failed_example, quarantines|
40
70
  # count the number of failures consecutively for this example
41
71
 
42
- number_of_failures = @examples.select { _1.name == failed_example && _1.failed? }.size
43
- if number_of_failures >= threshold
72
+ fails = @examples.select { _1.name == failed_example && _1.failed? }
73
+ if fails.size >= threshold
44
74
  quarantines << failed_example
45
75
 
46
76
  example = @examples.find { _1.name == failed_example }
47
77
 
48
- log :quarantine, "Quarantining #{failed_example} (#{number_of_failures} >= #{threshold})", 3
78
+ log :quarantine, "Quarantining #{failed_example} (#{fails.size} >= #{threshold})", 3
49
79
 
50
80
  # check to see if there is a merge request first
51
81
  # if there is no merge request...
@@ -60,9 +90,45 @@ module Confiner
60
90
  file_contents = get_example_file_contents(example)
61
91
  new_contents, changed_line_no = add_quarantine_metadata(file_contents, example, failure_issue)
62
92
 
63
- branch = create_branch(failure_issue, example)
64
- commit_changes(branch, example, new_contents)
65
- 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
66
132
 
67
133
  log :quarantine, "Done Quarantining #{failed_example}", 3
68
134
  rescue => e
@@ -82,7 +148,84 @@ module Confiner
82
148
 
83
149
  # Dequarantine Action - Automatically Dequarantine tests
84
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
85
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
227
+
228
+ log :gitlab, 'Done with Dequarantine Process', 2
86
229
  end
87
230
 
88
231
  private
@@ -102,13 +245,16 @@ module Confiner
102
245
  # @param [Integer] threshold the amount of pipelines to fetch
103
246
  # @note The threshold defaults to default threshold multiplied by two to the amount of pipelines
104
247
  # @return [Array<Example>] array of examples
105
- def get_examples(threshold: self.threshold * 2)
248
+ def get_examples(threshold: self.threshold * 2, job_pattern: Regexp.new(self.job_pattern))
106
249
  examples = []
107
250
 
108
251
  get_last_n_runs(threshold: threshold).each do |run|
109
252
  run.each do |pipeline|
110
253
  # fetch the pipeline test report
111
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
+
112
258
  log :suite, "Suite: #{suite['name']} (#{pipeline['web_url']})", 4
113
259
  suite['test_cases'].each do |example|
114
260
  examples << Example.new(**example, occurrence: { job: suite['name'], pipeline_url: pipeline['web_url'] })
@@ -157,44 +303,56 @@ module Confiner
157
303
  # @param [Gitlab::ObjectifiedHash] failure_issue the failure issue
158
304
  # @return [Array<String, Integer>] first value holds the new content, the second value holds the line number of the test
159
305
  def add_quarantine_metadata(content, example, failure_issue)
160
- 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
161
320
 
162
- best_match = -1
163
- lines = content_to_return.split("\n")
164
- example.name.split.reverse.each do |word|
165
- # given a full-descriptive RSpec example name:
166
- # Plan Group Iterations creates a group iteration
167
- # scan these words backwards, incrementing the index until we find the best match
168
- new_match = content_to_return.index(word)
169
- best_match = new_match if new_match > best_match
321
+ line
170
322
  end
323
+ end
171
324
 
172
- # the matched line where the example is
173
- match = content_to_return[best_match..].split("\n").first
174
- matched_line_no = 0
175
-
176
- lines.each_with_index do |line, line_no|
177
- if line.match?(match)
178
- matched_line_no = line_no + 1
179
-
180
- log :rspec, "Found match on line #{example.file}:#{matched_line_no}", 4
181
-
182
- if line.include?(',')
183
- 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:( *)?{.+}/, '')
184
337
  else
185
- line[line.rindex(' ')] = QUARANTINE_METADATA % failure_issue['web_url'] << ' '
338
+ line
186
339
  end
187
- end
188
- end
340
+ end,
189
341
 
190
- [lines.join("\n") << "\n", matched_line_no]
342
+ issue
343
+ ].flatten
191
344
  end
192
345
 
193
346
  # Create a branch from the ref
194
- # @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
195
350
  # @return [Gitlab::ObjectifiedHash] the new branch
196
- def create_branch(failure_issue, example)
197
- branch = @gitlab_client.create_branch(target_project, "#{failure_issue['iid']}-quarantine-#{example.name.gsub(/\W/, '-')}", 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)
198
356
 
199
357
  log :branch, "Created branch #{branch['name']} (#{branch['web_url']})", 4
200
358
 
@@ -203,16 +361,11 @@ module Confiner
203
361
 
204
362
  # Commit changes to a branch
205
363
  # @param [Gitlab::ObjectifiedHash] branch the branch to commit to
364
+ # @param [String] message the message to commit
206
365
  # @param [Example] example the example
207
366
  # @param [Gitlab::ObjectifiedHash] new_content the new content to commit
208
367
  # @return [Gitlab::ObjectifiedHash] the commit
209
- def commit_changes(branch, example, new_content)
210
- message = <<~COMMIT
211
- Quarantine end-to-end test
212
-
213
- Quarantine #{example.name}
214
- COMMIT
215
-
368
+ def commit_changes(branch, message, example, new_content)
216
369
  commit = @gitlab_client.create_commit(target_project, branch['name'], message, [
217
370
  { action: :update, file_path: example.file, content: new_content}
218
371
  ])
@@ -223,45 +376,65 @@ module Confiner
223
376
  end
224
377
 
225
378
  # Create a Merge Request with a given branch
379
+ # @param [String] title_prefix the prefix of the title
226
380
  # @param [Example] example the example
227
381
  # @param [Gitlab::ObjectifiedHash] branch the branch
228
- # @param [Integer] changed_line_number the line that was changed to quarantine
229
- # @param [Gitlab::ObjectifiedHash] failure_issue the failure issue
230
382
  # @return [Gitlab::ObjectifiedHash] the created merge request
231
- def create_merge_request(example, branch, changed_line_number, failure_issue)
232
- occurrences = @examples.select { _1.name == example.name && _1.failed? }
233
- markdown_occurrences = []
383
+ def create_merge_request(title_prefix, example, branch, &block)
384
+ description = block.call
234
385
 
235
- @examples.map do |eggsample|
236
- if eggsample.name == example.name && eggsample.failed?
237
- markdown_occurrences << "1. [#{eggsample.occurrence[:job]}](#{eggsample.occurrence[:pipeline_url]})"
238
- end
239
- 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 },
389
+
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)
240
396
 
241
- meets_or_exceeds_threshold = occurrences.size > threshold ? 'exceeds' : 'meets'
397
+ log :mr, "Created merge request #{merge_request['iid']} (#{merge_request['web_url']})", 4
242
398
 
243
- description = <<~MARKDOWN
244
- ## What does this MR do?
399
+ merge_request
400
+ end
245
401
 
246
- Quarantines the test `#{example.name}` (https://gitlab.com/#{target_project}/-/blob/#{ref}/#{example.file}#L#{changed_line_number})
402
+ private
247
403
 
248
- This test has been found by [Confiner](https://gitlab.com/gitlab-org/quality/confiner) to have failed
249
- #{occurrences.size} times, which #{meets_or_exceeds_threshold} the threshold of #{threshold} times.
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
250
411
 
251
- #{markdown_occurrences.join("\n")}
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
252
421
 
253
- > #{failure_issue['web_url']}
422
+ # the matched line where the example is
423
+ match = content_to_return[best_match..].split("\n").first
424
+ matched_line_no = 0
254
425
 
255
- <div align="center">
256
- (This MR was automatically generated by [Confiner](https://gitlab.com/gitlab-org/quality/confiner) at #{Time.now.utc})
257
- </div>
258
- MARKDOWN
426
+ lines.each_with_index do |line, line_no|
427
+ if line.match?(match)
428
+ matched_line_no = line_no + 1
259
429
 
260
- 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)
430
+ log :rspec, "Found match on line #{example.file}:#{matched_line_no}", 4
261
431
 
262
- log :mr, "Created merge request #{merge_request['iid']} (#{merge_request['web_url']})", 4
432
+ resulting_line = block_given? ? yield(line) : line
433
+ lines[line_no] = resulting_line
434
+ end
435
+ end
263
436
 
264
- merge_request
437
+ [lines.join("\n") << "\n", matched_line_no]
265
438
  end
266
439
  end
267
440
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Confiner
4
- VERSION = '0.2.3'
4
+ VERSION = '0.3.0'
5
5
  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.3
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-03-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