confiner 0.2.3 → 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: 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