parallel_tests 2.21.3 → 4.2.1

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.
@@ -1,7 +1,9 @@
1
+ # frozen_string_literal: true
1
2
  require 'optparse'
2
3
  require 'tempfile'
3
4
  require 'parallel_tests'
4
5
  require 'shellwords'
6
+ require 'pathname'
5
7
 
6
8
  module ParallelTests
7
9
  class CLI
@@ -13,12 +15,12 @@ module ParallelTests
13
15
  ENV['DISABLE_SPRING'] ||= '1'
14
16
 
15
17
  num_processes = ParallelTests.determine_number_of_processes(options[:count])
16
- num_processes = num_processes * (options[:multiply] || 1)
18
+ num_processes *= (options[:multiply] || 1)
17
19
 
18
20
  options[:first_is_1] ||= first_is_1?
19
21
 
20
22
  if options[:execute]
21
- execute_shell_command_in_parallel(options[:execute], num_processes, options)
23
+ execute_command_in_parallel(options[:execute], num_processes, options)
22
24
  else
23
25
  run_tests_in_parallel(num_processes, options)
24
26
  end
@@ -30,9 +32,23 @@ module ParallelTests
30
32
  @graceful_shutdown_attempted ||= false
31
33
  Kernel.exit if @graceful_shutdown_attempted
32
34
 
33
- # The Pid class's synchronize method can't be called directly from a trap
34
- # Using Thread workaround https://github.com/ddollar/foreman/issues/332
35
- Thread.new { ParallelTests.stop_all_processes }
35
+ # In a shell, all sub-processes also get an interrupt, so they shut themselves down.
36
+ # In a background process this does not happen and we need to do it ourselves.
37
+ # We cannot always send the interrupt since then the sub-processes would get interrupted twice when in foreground
38
+ # and that messes with interrupt handling.
39
+ #
40
+ # (can simulate detached with `(bundle exec parallel_rspec test/a_spec.rb -n 2 &)`)
41
+ # also the integration test "passes on int signal to child processes" is detached.
42
+ #
43
+ # On windows getpgid does not work so we resort to always killing which is the smaller bug.
44
+ #
45
+ # The ParallelTests::Pids `synchronize` method can't be called directly from a trap,
46
+ # using Thread workaround https://github.com/ddollar/foreman/issues/332
47
+ Thread.new do
48
+ if Gem.win_platform? || ((child_pid = ParallelTests.pids.all.first) && Process.getpgid(child_pid) != Process.pid)
49
+ ParallelTests.stop_all_processes
50
+ end
51
+ end
36
52
 
37
53
  @graceful_shutdown_attempted = true
38
54
  end
@@ -40,16 +56,13 @@ module ParallelTests
40
56
  def execute_in_parallel(items, num_processes, options)
41
57
  Tempfile.open 'parallel_tests-lock' do |lock|
42
58
  ParallelTests.with_pid_file do
43
- progress_indicator = simulate_output_for_ci if options[:serialize_stdout]
44
-
45
- Parallel.map(items, :in_threads => num_processes) do |item|
46
- result = yield(item)
47
- if progress_indicator && progress_indicator.alive?
48
- progress_indicator.exit
49
- puts
59
+ simulate_output_for_ci options[:serialize_stdout] do
60
+ Parallel.map(items, in_threads: num_processes) do |item|
61
+ result = yield(item)
62
+ reprint_output(result, lock.path) if options[:serialize_stdout]
63
+ ParallelTests.stop_all_processes if options[:fail_fast] && result[:exit_status] != 0
64
+ result
50
65
  end
51
- reprint_output(result, lock.path) if options[:serialize_stdout]
52
- result
53
66
  end
54
67
  end
55
68
  end
@@ -58,33 +71,45 @@ module ParallelTests
58
71
  def run_tests_in_parallel(num_processes, options)
59
72
  test_results = nil
60
73
 
61
- report_time_taken do
74
+ run_tests_proc = -> do
62
75
  groups = @runner.tests_in_groups(options[:files], num_processes, options)
63
- groups.reject! &:empty?
76
+ groups.reject!(&:empty?)
64
77
 
65
- test_results = if options[:only_group]
66
- groups_to_run = options[:only_group].collect{|i| groups[i - 1]}.compact
67
- report_number_of_tests(groups_to_run)
68
- execute_in_parallel(groups_to_run, groups_to_run.size, options) do |group|
69
- run_tests(group, groups_to_run.index(group), 1, options)
70
- end
71
- else
72
- report_number_of_tests(groups)
78
+ if options[:only_group]
79
+ groups = options[:only_group].map { |i| groups[i - 1] }.compact
80
+ num_processes = 1
81
+ end
73
82
 
74
- execute_in_parallel(groups, groups.size, options) do |group|
75
- run_tests(group, groups.index(group), num_processes, options)
76
- end
83
+ report_number_of_tests(groups) unless options[:quiet]
84
+ test_results = execute_in_parallel(groups, groups.size, options) do |group|
85
+ run_tests(group, groups.index(group), num_processes, options)
77
86
  end
87
+ report_results(test_results, options) unless options[:quiet]
88
+ end
78
89
 
79
- report_results(test_results, options)
90
+ if options[:quiet]
91
+ run_tests_proc.call
92
+ else
93
+ report_time_taken(&run_tests_proc)
80
94
  end
81
95
 
82
- abort final_fail_message if any_test_failed?(test_results)
96
+ if any_test_failed?(test_results)
97
+ warn final_fail_message
98
+
99
+ # return the highest exit status to allow sub-processes to send things other than 1
100
+ exit_status = if options[:highest_exit_status]
101
+ test_results.map { |data| data.fetch(:exit_status) }.max
102
+ else
103
+ 1
104
+ end
105
+
106
+ exit exit_status
107
+ end
83
108
  end
84
109
 
85
110
  def run_tests(group, process_number, num_processes, options)
86
111
  if group.empty?
87
- {:stdout => '', :exit_status => 0, :command => '', :seed => nil}
112
+ { stdout: '', exit_status: 0, command: nil, seed: nil }
88
113
  else
89
114
  @runner.run_tests(group, process_number, num_processes, options)
90
115
  end
@@ -92,6 +117,7 @@ module ParallelTests
92
117
 
93
118
  def reprint_output(result, lockfile)
94
119
  lock(lockfile) do
120
+ $stdout.puts
95
121
  $stdout.puts result[:stdout]
96
122
  $stdout.flush
97
123
  end
@@ -99,18 +125,16 @@ module ParallelTests
99
125
 
100
126
  def lock(lockfile)
101
127
  File.open(lockfile) do |lock|
102
- begin
103
- lock.flock File::LOCK_EX
104
- yield
105
- ensure
106
- # This shouldn't be necessary, but appears to be
107
- lock.flock File::LOCK_UN
108
- end
128
+ lock.flock File::LOCK_EX
129
+ yield
130
+ ensure
131
+ # This shouldn't be necessary, but appears to be
132
+ lock.flock File::LOCK_UN
109
133
  end
110
134
  end
111
135
 
112
136
  def report_results(test_results, options)
113
- results = @runner.find_results(test_results.map { |result| result[:stdout] }*"")
137
+ results = @runner.find_results(test_results.map { |result| result[:stdout] } * "")
114
138
  puts ""
115
139
  puts @runner.summarize_results(results)
116
140
 
@@ -121,13 +145,12 @@ module ParallelTests
121
145
  failing_sets = test_results.reject { |r| r[:exit_status] == 0 }
122
146
  return if failing_sets.none?
123
147
 
124
- if options[:verbose]
148
+ if options[:verbose] || options[:verbose_command]
125
149
  puts "\n\nTests have failed for a parallel_test group. Use the following command to run the group again:\n\n"
126
150
  failing_sets.each do |failing_set|
127
151
  command = failing_set[:command]
128
- command = command.gsub(/;export [A-Z_]+;/, ' ') # remove ugly export statements
129
152
  command = @runner.command_with_seed(command, failing_set[:seed]) if failing_set[:seed]
130
- puts command
153
+ @runner.print_command(command, failing_set[:env] || {})
131
154
  end
132
155
  end
133
156
  end
@@ -135,20 +158,31 @@ module ParallelTests
135
158
  def report_number_of_tests(groups)
136
159
  name = @runner.test_file_name
137
160
  num_processes = groups.size
138
- num_tests = groups.map(&:size).inject(0, :+)
161
+ num_tests = groups.map(&:size).sum
139
162
  tests_per_process = (num_processes == 0 ? 0 : num_tests / num_processes)
140
- puts "#{num_processes} processes for #{num_tests} #{name}s, ~ #{tests_per_process} #{name}s per process"
163
+ puts "#{pluralize(num_processes, 'process')} for #{pluralize(num_tests, name)}, ~ #{pluralize(tests_per_process, name)} per process"
164
+ end
165
+
166
+ def pluralize(n, singular)
167
+ if n == 1
168
+ "1 #{singular}"
169
+ elsif singular.end_with?('s', 'sh', 'ch', 'x', 'z')
170
+ "#{n} #{singular}es"
171
+ else
172
+ "#{n} #{singular}s"
173
+ end
141
174
  end
142
175
 
143
- #exit with correct status code so rake parallel:test && echo 123 works
176
+ # exit with correct status code so rake parallel:test && echo 123 works
144
177
  def any_test_failed?(test_results)
145
178
  test_results.any? { |result| result[:exit_status] != 0 }
146
179
  end
147
180
 
148
181
  def parse_options!(argv)
182
+ newline_padding = " " * 37
149
183
  options = {}
150
184
  OptionParser.new do |opts|
151
- opts.banner = <<-BANNER.gsub(/^ /, '')
185
+ opts.banner = <<~BANNER
152
186
  Run all tests in parallel, giving each process ENV['TEST_ENV_NUMBER'] ('', '2', '3', ...)
153
187
 
154
188
  [optional] Only selected files & folders:
@@ -161,65 +195,100 @@ module ParallelTests
161
195
  BANNER
162
196
  opts.on("-n [PROCESSES]", Integer, "How many processes to use, default: available CPUs") { |n| options[:count] = n }
163
197
  opts.on("-p", "--pattern [PATTERN]", "run tests matching this regex pattern") { |pattern| options[:pattern] = /#{pattern}/ }
164
- opts.on("--group-by [TYPE]", <<-TEXT.gsub(/^ /, '')
165
- group tests by:
166
- found - order of finding files
167
- steps - number of cucumber/spinach steps
168
- scenarios - individual cucumber scenarios
169
- filesize - by size of the file
170
- runtime - info from runtime log
171
- default - runtime when runtime log is filled otherwise filesize
198
+ opts.on("--exclude-pattern", "--exclude-pattern [PATTERN]", "exclude tests matching this regex pattern") { |pattern| options[:exclude_pattern] = /#{pattern}/ }
199
+ opts.on(
200
+ "--group-by [TYPE]",
201
+ <<~TEXT.rstrip.split("\n").join("\n#{newline_padding}")
202
+ group tests by:
203
+ found - order of finding files
204
+ steps - number of cucumber/spinach steps
205
+ scenarios - individual cucumber scenarios
206
+ filesize - by size of the file
207
+ runtime - info from runtime log
208
+ default - runtime when runtime log is filled otherwise filesize
172
209
  TEXT
173
- ) { |type| options[:group_by] = type.to_sym }
174
- opts.on("-m [FLOAT]", "--multiply-processes [FLOAT]", Float, "use given number as a multiplier of processes to run") { |multiply| options[:multiply] = multiply }
210
+ ) { |type| options[:group_by] = type.to_sym }
211
+ opts.on("-m [FLOAT]", "--multiply-processes [FLOAT]", Float, "use given number as a multiplier of processes to run") do |multiply|
212
+ options[:multiply] = multiply
213
+ end
175
214
 
176
- opts.on("-s [PATTERN]", "--single [PATTERN]",
177
- "Run all matching files in the same process") do |pattern|
215
+ opts.on("-s [PATTERN]", "--single [PATTERN]", "Run all matching files in the same process") do |pattern|
216
+ (options[:single_process] ||= []) << /#{pattern}/
217
+ end
178
218
 
179
- options[:single_process] ||= []
180
- options[:single_process] << /#{pattern}/
219
+ opts.on("-i", "--isolate", "Do not run any other tests in the group used by --single(-s)") do
220
+ options[:isolate] = true
181
221
  end
182
222
 
183
- opts.on("-i", "--isolate",
184
- "Do not run any other tests in the group used by --single(-s)") do |pattern|
223
+ opts.on(
224
+ "--isolate-n [PROCESSES]",
225
+ Integer,
226
+ "Use 'isolate' singles with number of processes, default: 1."
227
+ ) { |n| options[:isolate_count] = n }
185
228
 
186
- options[:isolate] = true
229
+ opts.on("--highest-exit-status", "Exit with the highest exit status provided by test run(s)") do
230
+ options[:highest_exit_status] = true
187
231
  end
188
232
 
189
- opts.on("--only-group INT[, INT]", Array) { |groups| options[:only_group] = groups.map(&:to_i) }
233
+ opts.on(
234
+ "--specify-groups [SPECS]",
235
+ <<~TEXT.rstrip.split("\n").join("\n#{newline_padding}")
236
+ Use 'specify-groups' if you want to specify multiple specs running in multiple
237
+ processes in a specific formation. Commas indicate specs in the same process,
238
+ pipes indicate specs in a new process. Cannot use with --single, --isolate, or
239
+ --isolate-n. Ex.
240
+ $ parallel_test -n 3 . --specify-groups '1_spec.rb,2_spec.rb|3_spec.rb'
241
+ Process 1 will contain 1_spec.rb and 2_spec.rb
242
+ Process 2 will contain 3_spec.rb
243
+ Process 3 will contain all other specs
244
+ TEXT
245
+ ) { |groups| options[:specify_groups] = groups }
246
+
247
+ opts.on("--only-group INT[,INT]", Array) { |groups| options[:only_group] = groups.map(&:to_i) }
190
248
 
191
- opts.on("-e", "--exec [COMMAND]", "execute this code parallel and with ENV['TEST_ENV_NUMBER']") { |path| options[:execute] = path }
192
- opts.on("-o", "--test-options '[OPTIONS]'", "execute test commands with those options") { |arg| options[:test_options] = arg.lstrip }
249
+ opts.on("-e", "--exec [COMMAND]", "execute this code parallel and with ENV['TEST_ENV_NUMBER']") { |arg| options[:execute] = Shellwords.shellsplit(arg) }
250
+ opts.on("-o", "--test-options '[OPTIONS]'", "execute test commands with those options") { |arg| options[:test_options] = Shellwords.shellsplit(arg) }
193
251
  opts.on("-t", "--type [TYPE]", "test(default) / rspec / cucumber / spinach") do |type|
194
- begin
195
- @runner = load_runner(type)
196
- rescue NameError, LoadError => e
197
- puts "Runner for `#{type}` type has not been found! (#{e})"
198
- abort
199
- end
252
+ @runner = load_runner(type)
253
+ rescue NameError, LoadError => e
254
+ puts "Runner for `#{type}` type has not been found! (#{e})"
255
+ abort
200
256
  end
201
- opts.on("--suffix [PATTERN]", <<-TEXT.gsub(/^ /, '')
202
- override built in test file pattern (should match suffix):
203
- '_spec\.rb$' - matches rspec files
204
- '_(test|spec).rb$' - matches test or spec files
257
+ opts.on(
258
+ "--suffix [PATTERN]",
259
+ <<~TEXT.rstrip.split("\n").join("\n#{newline_padding}")
260
+ override built in test file pattern (should match suffix):
261
+ '_spec\.rb$' - matches rspec files
262
+ '_(test|spec).rb$' - matches test or spec files
205
263
  TEXT
206
- ) { |pattern| options[:suffix] = /#{pattern}/ }
264
+ ) { |pattern| options[:suffix] = /#{pattern}/ }
207
265
  opts.on("--serialize-stdout", "Serialize stdout output, nothing will be written until everything is done") { options[:serialize_stdout] = true }
208
266
  opts.on("--prefix-output-with-test-env-number", "Prefixes test env number to the output when not using --serialize-stdout") { options[:prefix_output_with_test_env_number] = true }
209
267
  opts.on("--combine-stderr", "Combine stderr into stdout, useful in conjunction with --serialize-stdout") { options[:combine_stderr] = true }
210
268
  opts.on("--non-parallel", "execute same commands but do not in parallel, needs --exec") { options[:non_parallel] = true }
211
269
  opts.on("--no-symlinks", "Do not traverse symbolic links to find test files") { options[:symlinks] = false }
212
- opts.on('--ignore-tags [PATTERN]', 'When counting steps ignore scenarios with tags that match this pattern') { |arg| options[:ignore_tag_pattern] = arg }
270
+ opts.on('--ignore-tags [PATTERN]', 'When counting steps ignore scenarios with tags that match this pattern') { |arg| options[:ignore_tag_pattern] = arg }
213
271
  opts.on("--nice", "execute test commands with low priority.") { options[:nice] = true }
214
272
  opts.on("--runtime-log [PATH]", "Location of previously recorded test runtimes") { |path| options[:runtime_log] = path }
215
273
  opts.on("--allowed-missing [INT]", Integer, "Allowed percentage of missing runtimes (default = 50)") { |percent| options[:allowed_missing_percent] = percent }
216
274
  opts.on("--unknown-runtime [FLOAT]", Float, "Use given number as unknown runtime (otherwise use average time)") { |time| options[:unknown_runtime] = time }
217
275
  opts.on("--first-is-1", "Use \"1\" as TEST_ENV_NUMBER to not reuse the default test environment") { options[:first_is_1] = true }
218
- opts.on("--verbose", "Print more output") { options[:verbose] = true }
219
- opts.on("-v", "--version", "Show Version") { puts ParallelTests::VERSION; exit }
220
- opts.on("-h", "--help", "Show this.") { puts opts; exit }
276
+ opts.on("--fail-fast", "Stop all groups when one group fails (best used with --test-options '--fail-fast' if supported") { options[:fail_fast] = true }
277
+ opts.on("--verbose", "Print debug output") { options[:verbose] = true }
278
+ opts.on("--verbose-command", "Displays the command that will be executed by each process and when there are failures displays the command executed by each process that failed") { options[:verbose_command] = true }
279
+ opts.on("--quiet", "Print only tests output") { options[:quiet] = true }
280
+ opts.on("-v", "--version", "Show Version") do
281
+ puts ParallelTests::VERSION
282
+ exit 0
283
+ end
284
+ opts.on("-h", "--help", "Show this.") do
285
+ puts opts
286
+ exit 0
287
+ end
221
288
  end.parse!(argv)
222
289
 
290
+ raise "Both options are mutually exclusive: verbose & quiet" if options[:verbose] && options[:quiet]
291
+
223
292
  if options[:count] == 0
224
293
  options.delete(:count)
225
294
  options[:non_parallel] = true
@@ -227,40 +296,53 @@ module ParallelTests
227
296
 
228
297
  files, remaining = extract_file_paths(argv)
229
298
  unless options[:execute]
230
- abort "Pass files or folders to run" unless files.any?
231
- options[:files] = files
299
+ if files.empty?
300
+ default_test_folder = @runner.default_test_folder
301
+ if File.directory?(default_test_folder)
302
+ files = [default_test_folder]
303
+ else
304
+ abort "Pass files or folders to run"
305
+ end
306
+ end
307
+ options[:files] = files.map { |file_path| Pathname.new(file_path).cleanpath.to_s }
232
308
  end
233
309
 
234
310
  append_test_options(options, remaining)
235
311
 
236
312
  options[:group_by] ||= :filesize if options[:only_group]
237
313
 
238
- raise "--group-by found and --single-process are not supported" if options[:group_by] == :found and options[:single_process]
314
+ if options[:group_by] == :found && options[:single_process]
315
+ raise "--group-by found and --single-process are not supported"
316
+ end
239
317
  allowed = [:filesize, :runtime, :found]
240
318
  if !allowed.include?(options[:group_by]) && options[:only_group]
241
319
  raise "--group-by #{allowed.join(" or ")} is required for --only-group"
242
320
  end
243
321
 
322
+ if options[:specify_groups] && (options.keys & [:single_process, :isolate, :isolate_count]).any?
323
+ raise "Can't pass --specify-groups with any of these keys: --single, --isolate, or --isolate-n"
324
+ end
325
+
244
326
  options
245
327
  end
246
328
 
247
329
  def extract_file_paths(argv)
248
330
  dash_index = argv.rindex("--")
249
331
  file_args_at = (dash_index || -1) + 1
250
- [argv[file_args_at..-1], argv[0...(dash_index || 0)]]
332
+ [argv[file_args_at..], argv[0...(dash_index || 0)]]
251
333
  end
252
334
 
253
335
  def extract_test_options(argv)
254
336
  dash_index = argv.index("--") || -1
255
- argv[dash_index+1..-1]
337
+ argv[dash_index + 1..]
256
338
  end
257
339
 
258
340
  def append_test_options(options, argv)
259
341
  new_opts = extract_test_options(argv)
260
342
  return if new_opts.empty?
261
343
 
262
- prev_and_new = [options[:test_options], new_opts.shelljoin]
263
- options[:test_options] = prev_and_new.compact.join(' ')
344
+ options[:test_options] ||= []
345
+ options[:test_options] += new_opts
264
346
  end
265
347
 
266
348
  def load_runner(type)
@@ -270,9 +352,9 @@ module ParallelTests
270
352
  klass_name.split('::').inject(Object) { |x, y| x.const_get(y) }
271
353
  end
272
354
 
273
- def execute_shell_command_in_parallel(command, num_processes, options)
355
+ def execute_command_in_parallel(command, num_processes, options)
274
356
  runs = if options[:only_group]
275
- options[:only_group].map{|g| g - 1}
357
+ options[:only_group].map { |g| g - 1 }
276
358
  else
277
359
  (0...num_processes).to_a
278
360
  end
@@ -291,22 +373,21 @@ module ParallelTests
291
373
  abort if results.any? { |r| r[:exit_status] != 0 }
292
374
  end
293
375
 
294
- def report_time_taken
295
- seconds = ParallelTests.delta { yield }.to_i
376
+ def report_time_taken(&block)
377
+ seconds = ParallelTests.delta(&block).to_i
296
378
  puts "\nTook #{seconds} seconds#{detailed_duration(seconds)}"
297
379
  end
298
380
 
299
381
  def detailed_duration(seconds)
300
- parts = [ seconds / 3600, seconds % 3600 / 60, seconds % 60 ].drop_while(&:zero?)
382
+ parts = [seconds / 3600, seconds % 3600 / 60, seconds % 60].drop_while(&:zero?)
301
383
  return if parts.size < 2
302
384
  parts = parts.map { |i| "%02d" % i }.join(':').sub(/^0/, '')
303
385
  " (#{parts})"
304
386
  end
305
387
 
306
388
  def final_fail_message
307
- fail_message = "#{@runner.name}s Failed"
389
+ fail_message = "Tests Failed"
308
390
  fail_message = "\e[31m#{fail_message}\e[0m" if use_colors?
309
-
310
391
  fail_message
311
392
  end
312
393
 
@@ -320,13 +401,20 @@ module ParallelTests
320
401
  end
321
402
 
322
403
  # CI systems often fail when there is no output for a long time, so simulate some output
323
- def simulate_output_for_ci
324
- Thread.new do
325
- interval = ENV.fetch('PARALLEL_TEST_HEARTBEAT_INTERVAL', 60).to_f
326
- loop do
327
- sleep interval
328
- print '.'
404
+ def simulate_output_for_ci(simulate)
405
+ if simulate
406
+ progress_indicator = Thread.new do
407
+ interval = Float(ENV['PARALLEL_TEST_HEARTBEAT_INTERVAL'] || 60)
408
+ loop do
409
+ sleep interval
410
+ print '.'
411
+ end
329
412
  end
413
+ test_results = yield
414
+ progress_indicator.exit
415
+ test_results
416
+ else
417
+ yield
330
418
  end
331
419
  end
332
420
  end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'cucumber/formatter/rerun'
2
3
  require 'parallel_tests/gherkin/io'
3
4
 
@@ -21,7 +22,6 @@ module ParallelTests
21
22
  end
22
23
  end
23
24
  end
24
-
25
25
  end
26
26
  end
27
27
  end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+ begin
3
+ gem "cuke_modeler", "~> 3.0"
4
+ require 'cuke_modeler'
5
+ rescue LoadError
6
+ raise 'Grouping by number of cucumber steps requires the `cuke_modeler` modeler gem with requirement `~> 3.0`. Add `gem "cuke_modeler", "~> 3.0"` to your `Gemfile`, run `bundle install` and try again.'
7
+ end
8
+
9
+ module ParallelTests
10
+ module Cucumber
11
+ class FeaturesWithSteps
12
+ class << self
13
+ def all(tests, options)
14
+ ignore_tag_pattern = options[:ignore_tag_pattern].nil? ? nil : Regexp.compile(options[:ignore_tag_pattern])
15
+ # format of hash will be FILENAME => NUM_STEPS
16
+ steps_per_file = tests.each_with_object({}) do |file, steps|
17
+ feature = ::CukeModeler::FeatureFile.new(file).feature
18
+
19
+ # skip feature if it matches tag regex
20
+ next if feature.tags.grep(ignore_tag_pattern).any?
21
+
22
+ # count the number of steps in the file
23
+ # will only include a feature if the regex does not match
24
+ all_steps = feature.scenarios.map { |a| a.steps.count if a.tags.grep(ignore_tag_pattern).empty? }.compact
25
+ steps[file] = all_steps.sum
26
+ end
27
+ steps_per_file.sort_by { |_, value| -value }
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -1,16 +1,21 @@
1
+ # frozen_string_literal: true
1
2
  require "parallel_tests/gherkin/runner"
2
3
 
3
4
  module ParallelTests
4
5
  module Cucumber
5
6
  class Runner < ParallelTests::Gherkin::Runner
6
- SCENARIOS_RESULTS_BOUNDARY_REGEX = /^(Failing|Flaky) Scenarios:$/
7
- SCENARIO_REGEX = /^cucumber features\/.+:\d+/
7
+ SCENARIOS_RESULTS_BOUNDARY_REGEX = /^(Failing|Flaky) Scenarios:$/.freeze
8
+ SCENARIO_REGEX = %r{^cucumber features/.+:\d+}.freeze
8
9
 
9
10
  class << self
10
11
  def name
11
12
  'cucumber'
12
13
  end
13
14
 
15
+ def default_test_folder
16
+ 'features'
17
+ end
18
+
14
19
  def line_is_result?(line)
15
20
  super || line =~ SCENARIO_REGEX || line =~ SCENARIOS_RESULTS_BOUNDARY_REGEX
16
21
  end
@@ -21,9 +26,7 @@ module ParallelTests
21
26
  scenario_groups = results.slice_before(SCENARIOS_RESULTS_BOUNDARY_REGEX).group_by(&:first)
22
27
  scenario_groups.each do |header, group|
23
28
  scenarios = group.flatten.grep(SCENARIO_REGEX)
24
- if scenarios.any?
25
- output << ([header] + scenarios).join("\n")
26
- end
29
+ output << ([header] + scenarios).join("\n") if scenarios.any?
27
30
  end
28
31
 
29
32
  output << super
@@ -32,8 +35,8 @@ module ParallelTests
32
35
  end
33
36
 
34
37
  def command_with_seed(cmd, seed)
35
- clean = cmd.sub(/\s--order\s+random(:\d+)?\b/, '')
36
- "#{clean} --order random:#{seed}"
38
+ clean = remove_command_arguments(cmd, '--order')
39
+ [*clean, '--order', "random:#{seed}"]
37
40
  end
38
41
  end
39
42
  end
@@ -1,36 +1,34 @@
1
- require 'cucumber/core/gherkin/tag_expression'
2
-
1
+ # frozen_string_literal: true
3
2
  module ParallelTests
4
3
  module Cucumber
5
4
  module Formatters
6
5
  class ScenarioLineLogger
7
6
  attr_reader :scenarios
8
7
 
9
- def initialize(tag_expression = ::Cucumber::Core::Gherkin::TagExpression.new([]))
8
+ def initialize(tag_expression = nil)
10
9
  @scenarios = []
11
10
  @tag_expression = tag_expression
12
11
  end
13
12
 
14
13
  def visit_feature_element(uri, feature_element, feature_tags, line_numbers: [])
15
- scenario_tags = feature_element[:tags].map {|tag| ::Cucumber::Core::Ast::Tag.new(tag[:location], tag[:name])}
14
+ scenario_tags = feature_element.tags.map(&:name)
16
15
  scenario_tags = feature_tags + scenario_tags
17
- if feature_element[:examples].nil? # :Scenario
18
- test_line = feature_element[:location][:line]
16
+ if feature_element.is_a?(CukeModeler::Scenario) # :Scenario
17
+ test_line = feature_element.source_line
19
18
 
20
19
  # We don't accept the feature_element if the current tags are not valid
21
- return unless @tag_expression.evaluate(scenario_tags)
20
+ return unless matches_tags?(scenario_tags)
22
21
  # or if it is not at the correct location
23
22
  return if line_numbers.any? && !line_numbers.include?(test_line)
24
23
 
25
- @scenarios << [uri, feature_element[:location][:line]].join(":")
24
+ @scenarios << [uri, feature_element.source_line].join(":")
26
25
  else # :ScenarioOutline
27
- feature_element[:examples].each do |example|
28
- example_tags = example[:tags].map {|tag| ::Cucumber::Core::Ast::Tag.new(tag[:location], tag[:name])}
26
+ feature_element.examples.each do |example|
27
+ example_tags = example.tags.map(&:name)
29
28
  example_tags = scenario_tags + example_tags
30
- next unless @tag_expression.evaluate(example_tags)
31
- rows = example[:tableBody].select { |body| body[:type] == :TableRow }
32
- rows.each do |row|
33
- test_line = row[:location][:line]
29
+ next unless matches_tags?(example_tags)
30
+ example.rows[1..].each do |row|
31
+ test_line = row.source_line
34
32
  next if line_numbers.any? && !line_numbers.include?(test_line)
35
33
 
36
34
  @scenarios << [uri, test_line].join(':')
@@ -39,7 +37,12 @@ module ParallelTests
39
37
  end
40
38
  end
41
39
 
42
- def method_missing(*args)
40
+ def method_missing(*); end # # rubocop:disable Style/MissingRespondToMissing
41
+
42
+ private
43
+
44
+ def matches_tags?(tags)
45
+ @tag_expression.nil? || @tag_expression.evaluate(tags)
43
46
  end
44
47
  end
45
48
  end