parallel_tests 3.7.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 042cf590688332180e07fcf6472cc9b88b740276df0511677cf77f7fe649fa2e
4
- data.tar.gz: cb4fd166b030e16574bf5d4790e5d1e0a3d57001d5474ab81c3d54613af976e2
3
+ metadata.gz: a9044c95c595a48f89a621563c609a4c9fa89d851792ca694a30fcaf947a2119
4
+ data.tar.gz: 248e38e467b9070c1666819e2c1dc9149b791644b42e8a90cd53df82ebb80eed
5
5
  SHA512:
6
- metadata.gz: 98e679733a74273ac77e71db47bc3af3f0f5a928dab351be9419644a0e7fe2bc004a81c929928ef0bd98ea2b8e5b63d1ab9fadcf75e22d098a65c5164343fe0b
7
- data.tar.gz: 2ce2dd070733cd0bb61d0cb610f886a5590d5e267cc43613ab7d08fc251d989ebd4a58ad2f0a9c88478499fe55ff4cb7386fabc2eba8ee7fb3440d4899594654
6
+ metadata.gz: 363e8d317a43d03043a270c19d05f60071bd05a9b435a0545454e81b96d875b694dd4fadc74669ba136990d57d863883132a19ff7fd1c9c4169e2c16a8e2db38
7
+ data.tar.gz: ef251fa00adf1310c5281ee4985119ce6b4810d121d87a3a4a7f6b6a03aa968ed9be7847eec127e74a118983e4454b181635d1538b1a17b5f242fbb11839caa9
data/Readme.md CHANGED
@@ -1,8 +1,7 @@
1
1
  # parallel_tests
2
2
 
3
3
  [![Gem Version](https://badge.fury.io/rb/parallel_tests.svg)](https://rubygems.org/gems/parallel_tests)
4
- [![Build Status](https://travis-ci.org/grosser/parallel_tests.svg)](https://travis-ci.org/grosser/parallel_tests/builds)
5
- [![Build status](https://github.com/grosser/parallel_tests/workflows/windows/badge.svg)](https://github.com/grosser/parallel_tests/actions?query=workflow%3Awindows)
4
+ [![Build status](https://github.com/grosser/parallel_tests/workflows/test/badge.svg)](https://github.com/grosser/parallel_tests/actions?query=workflow%3Atest)
6
5
 
7
6
  Speedup Test::Unit + RSpec + Cucumber + Spinach by running parallel on multiple CPU cores.<br/>
8
7
  ParallelTests splits tests into even groups (by number of lines or runtime) and runs each group in a single process with its own database.
@@ -42,7 +41,7 @@ test:
42
41
 
43
42
  ### Setup environment from scratch (create db and loads schema, useful for CI)
44
43
  rake parallel:setup
45
-
44
+
46
45
  ### Drop all test databases
47
46
  rake parallel:drop
48
47
 
@@ -193,15 +192,15 @@ Setup for non-rails
193
192
  - use `ENV['TEST_ENV_NUMBER']` inside your tests to select separate db/memcache/etc. (docker compose: expose it)
194
193
 
195
194
  - Only run a subset of files / folders:
196
-
195
+
197
196
  `parallel_test test/bar test/baz/foo_text.rb`
198
197
 
199
198
  - Pass test-options and files via `--`:
200
-
199
+
201
200
  `parallel_rspec -- -t acceptance -f progress -- spec/foo_spec.rb spec/acceptance`
202
-
201
+
203
202
  - Pass in test options, by using the -o flag (wrap everything in quotes):
204
-
203
+
205
204
  `parallel_cucumber -n 2 -o '-p foo_profile --tags @only_this_tag or @only_that_tag --format summary'`
206
205
 
207
206
  Options are:
@@ -250,8 +249,7 @@ Options are:
250
249
  --first-is-1 Use "1" as TEST_ENV_NUMBER to not reuse the default test environment
251
250
  --fail-fast Stop all groups when one group fails (best used with --test-options '--fail-fast' if supported
252
251
  --verbose Print debug output
253
- --verbose-process-command Displays only the command that will be executed by each process
254
- --verbose-rerun-command When there are failures, displays the command executed by each process that failed
252
+ --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
255
253
  --quiet Print only tests output
256
254
  -v, --version Show Version
257
255
  -h, --help Show this.
@@ -402,6 +400,9 @@ inspired by [pivotal labs](https://blog.pivotal.io/labs/labs/parallelize-your-rs
402
400
  - [Vikram B Kumar](https://github.com/v-kumar)
403
401
  - [Joshua Pinter](https://github.com/joshuapinter)
404
402
  - [Zach Dennis](https://github.com/zdennis)
403
+ - [Jon Dufresne](https://github.com/jdufresne)
404
+ - [Eric Kessler](https://github.com/enkessler)
405
+ - [Adis Osmonov](https://github.com/adis-io)
405
406
 
406
407
  [Michael Grosser](http://grosser.it)<br/>
407
408
  michael@grosser.it<br/>
@@ -20,7 +20,7 @@ module ParallelTests
20
20
  options[:first_is_1] ||= first_is_1?
21
21
 
22
22
  if options[:execute]
23
- execute_shell_command_in_parallel(options[:execute], num_processes, options)
23
+ execute_command_in_parallel(options[:execute], num_processes, options)
24
24
  else
25
25
  run_tests_in_parallel(num_processes, options)
26
26
  end
@@ -32,9 +32,23 @@ module ParallelTests
32
32
  @graceful_shutdown_attempted ||= false
33
33
  Kernel.exit if @graceful_shutdown_attempted
34
34
 
35
- # The Pid class's synchronize method can't be called directly from a trap
36
- # Using Thread workaround https://github.com/ddollar/foreman/issues/332
37
- 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
38
52
 
39
53
  @graceful_shutdown_attempted = true
40
54
  end
@@ -61,20 +75,15 @@ module ParallelTests
61
75
  groups = @runner.tests_in_groups(options[:files], num_processes, options)
62
76
  groups.reject!(&:empty?)
63
77
 
64
- test_results = if options[:only_group]
65
- groups_to_run = options[:only_group].map { |i| groups[i - 1] }.compact
66
- report_number_of_tests(groups_to_run) unless options[:quiet]
67
- execute_in_parallel(groups_to_run, groups_to_run.size, options) do |group|
68
- run_tests(group, groups_to_run.index(group), 1, options)
69
- end
70
- else
71
- report_number_of_tests(groups) unless options[:quiet]
72
-
73
- execute_in_parallel(groups, groups.size, options) do |group|
74
- run_tests(group, groups.index(group), num_processes, options)
75
- end
78
+ if options[:only_group]
79
+ groups = options[:only_group].map { |i| groups[i - 1] }.compact
80
+ num_processes = 1
76
81
  end
77
82
 
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)
86
+ end
78
87
  report_results(test_results, options) unless options[:quiet]
79
88
  end
80
89
 
@@ -100,7 +109,7 @@ module ParallelTests
100
109
 
101
110
  def run_tests(group, process_number, num_processes, options)
102
111
  if group.empty?
103
- { stdout: '', exit_status: 0, command: '', seed: nil }
112
+ { stdout: '', exit_status: 0, command: nil, seed: nil }
104
113
  else
105
114
  @runner.run_tests(group, process_number, num_processes, options)
106
115
  end
@@ -136,13 +145,12 @@ module ParallelTests
136
145
  failing_sets = test_results.reject { |r| r[:exit_status] == 0 }
137
146
  return if failing_sets.none?
138
147
 
139
- if options[:verbose] || options[:verbose_rerun_command]
148
+ if options[:verbose] || options[:verbose_command]
140
149
  puts "\n\nTests have failed for a parallel_test group. Use the following command to run the group again:\n\n"
141
150
  failing_sets.each do |failing_set|
142
151
  command = failing_set[:command]
143
- command = command.gsub(/;export [A-Z_]+;/, ' ') # remove ugly export statements
144
152
  command = @runner.command_with_seed(command, failing_set[:seed]) if failing_set[:seed]
145
- puts command
153
+ @runner.print_command(command, failing_set[:env] || {})
146
154
  end
147
155
  end
148
156
  end
@@ -229,7 +237,7 @@ module ParallelTests
229
237
  processes in a specific formation. Commas indicate specs in the same process,
230
238
  pipes indicate specs in a new process. Cannot use with --single, --isolate, or
231
239
  --isolate-n. Ex.
232
- $ parallel_tests -n 3 . --specify-groups '1_spec.rb,2_spec.rb|3_spec.rb'
240
+ $ parallel_test -n 3 . --specify-groups '1_spec.rb,2_spec.rb|3_spec.rb'
233
241
  Process 1 will contain 1_spec.rb and 2_spec.rb
234
242
  Process 2 will contain 3_spec.rb
235
243
  Process 3 will contain all other specs
@@ -238,8 +246,8 @@ module ParallelTests
238
246
 
239
247
  opts.on("--only-group INT[,INT]", Array) { |groups| options[:only_group] = groups.map(&:to_i) }
240
248
 
241
- opts.on("-e", "--exec [COMMAND]", "execute this code parallel and with ENV['TEST_ENV_NUMBER']") { |path| options[:execute] = path }
242
- 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) }
243
251
  opts.on("-t", "--type [TYPE]", "test(default) / rspec / cucumber / spinach") do |type|
244
252
  @runner = load_runner(type)
245
253
  rescue NameError, LoadError => e
@@ -267,8 +275,7 @@ module ParallelTests
267
275
  opts.on("--first-is-1", "Use \"1\" as TEST_ENV_NUMBER to not reuse the default test environment") { options[:first_is_1] = true }
268
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 }
269
277
  opts.on("--verbose", "Print debug output") { options[:verbose] = true }
270
- opts.on("--verbose-process-command", "Displays only the command that will be executed by each process") { options[:verbose_process_command] = true }
271
- opts.on("--verbose-rerun-command", "When there are failures, displays the command executed by each process that failed") { options[:verbose_rerun_command] = 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 }
272
279
  opts.on("--quiet", "Print only tests output") { options[:quiet] = true }
273
280
  opts.on("-v", "--version", "Show Version") do
274
281
  puts ParallelTests::VERSION
@@ -322,20 +329,20 @@ module ParallelTests
322
329
  def extract_file_paths(argv)
323
330
  dash_index = argv.rindex("--")
324
331
  file_args_at = (dash_index || -1) + 1
325
- [argv[file_args_at..-1], argv[0...(dash_index || 0)]]
332
+ [argv[file_args_at..], argv[0...(dash_index || 0)]]
326
333
  end
327
334
 
328
335
  def extract_test_options(argv)
329
336
  dash_index = argv.index("--") || -1
330
- argv[dash_index + 1..-1]
337
+ argv[dash_index + 1..]
331
338
  end
332
339
 
333
340
  def append_test_options(options, argv)
334
341
  new_opts = extract_test_options(argv)
335
342
  return if new_opts.empty?
336
343
 
337
- prev_and_new = [options[:test_options], new_opts.shelljoin]
338
- options[:test_options] = prev_and_new.compact.join(' ')
344
+ options[:test_options] ||= []
345
+ options[:test_options] += new_opts
339
346
  end
340
347
 
341
348
  def load_runner(type)
@@ -345,7 +352,7 @@ module ParallelTests
345
352
  klass_name.split('::').inject(Object) { |x, y| x.const_get(y) }
346
353
  end
347
354
 
348
- def execute_shell_command_in_parallel(command, num_processes, options)
355
+ def execute_command_in_parallel(command, num_processes, options)
349
356
  runs = if options[:only_group]
350
357
  options[:only_group].map { |g| g - 1 }
351
358
  else
@@ -397,7 +404,7 @@ module ParallelTests
397
404
  def simulate_output_for_ci(simulate)
398
405
  if simulate
399
406
  progress_indicator = Thread.new do
400
- interval = Float(ENV.fetch('PARALLEL_TEST_HEARTBEAT_INTERVAL', 60))
407
+ interval = Float(ENV['PARALLEL_TEST_HEARTBEAT_INTERVAL'] || 60)
401
408
  loop do
402
409
  sleep interval
403
410
  print '.'
@@ -35,8 +35,8 @@ module ParallelTests
35
35
  end
36
36
 
37
37
  def command_with_seed(cmd, seed)
38
- clean = cmd.sub(/\s--order\s+random(:\d+)?\b/, '')
39
- "#{clean} --order random:#{seed}"
38
+ clean = remove_command_arguments(cmd, '--order')
39
+ [*clean, '--order', "random:#{seed}"]
40
40
  end
41
41
  end
42
42
  end
@@ -27,7 +27,7 @@ module ParallelTests
27
27
  example_tags = example.tags.map(&:name)
28
28
  example_tags = scenario_tags + example_tags
29
29
  next unless matches_tags?(example_tags)
30
- example.rows[1..-1].each do |row|
30
+ example.rows[1..].each do |row|
31
31
  test_line = row.source_line
32
32
  next if line_numbers.any? && !line_numbers.include?(test_line)
33
33
 
@@ -4,7 +4,6 @@ require 'cucumber/runtime'
4
4
  require 'cucumber'
5
5
  require 'parallel_tests/cucumber/scenario_line_logger'
6
6
  require 'parallel_tests/gherkin/listener'
7
- require 'shellwords'
8
7
 
9
8
  begin
10
9
  gem "cuke_modeler", "~> 3.0"
@@ -20,7 +19,7 @@ module ParallelTests
20
19
  def all(files, options = {})
21
20
  # Parse tag expression from given test options and ignore tag pattern. Refer here to understand how new tag expression syntax works - https://github.com/cucumber/cucumber/tree/master/tag-expressions
22
21
  tags = []
23
- words = options[:test_options].to_s.shellsplit
22
+ words = options[:test_options] || []
24
23
  words.each_with_index { |w, i| tags << words[i + 1] if ["-t", "--tags"].include?(w) }
25
24
  if ignore = options[:ignore_tag_pattern]
26
25
  tags << "not (#{ignore})"
@@ -53,7 +52,9 @@ module ParallelTests
53
52
  feature_tags = feature.tags.map(&:name)
54
53
 
55
54
  # We loop on each children of the feature
56
- feature.tests.each do |test|
55
+ test_models = feature.tests
56
+ test_models += feature.rules.flat_map(&:tests) if feature.respond_to?(:rules) # cuke_modeler >= 3.2 supports rules
57
+ test_models.each do |test|
57
58
  # It's a scenario, we add it to the scenario_line_logger
58
59
  scenario_line_logger.visit_feature_element(document.path, test, feature_tags, line_numbers: test_lines)
59
60
  end
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
  require "parallel_tests/test/runner"
3
- require 'shellwords'
4
3
 
5
4
  module ParallelTests
6
5
  module Gherkin
@@ -16,17 +15,13 @@ module ParallelTests
16
15
  end
17
16
  end
18
17
 
19
- sanitized_test_files = combined_scenarios.map { |val| WINDOWS ? "\"#{val}\"" : Shellwords.escape(val) }
20
-
21
18
  options[:env] ||= {}
22
19
  options[:env] = options[:env].merge({ 'AUTOTEST' => '1' }) if $stdout.tty?
23
20
 
24
- cmd = [
25
- executable,
26
- (runtime_logging if File.directory?(File.dirname(runtime_log))),
27
- *sanitized_test_files,
28
- cucumber_opts(options[:test_options])
29
- ].compact.reject(&:empty?).join(' ')
21
+ cmd = executable
22
+ cmd += runtime_logging if File.directory?(File.dirname(runtime_log))
23
+ cmd += combined_scenarios
24
+ cmd += cucumber_opts(options[:test_options])
30
25
  execute_command(cmd, process_number, num_processes, options)
31
26
  end
32
27
 
@@ -62,22 +57,22 @@ module ParallelTests
62
57
  plural = "s" if (word == group) && (number != 1)
63
58
  "#{number} #{word}#{plural}"
64
59
  end
65
- "#{sums[0]} (#{sums[1..-1].join(", ")})"
60
+ "#{sums[0]} (#{sums[1..].join(", ")})"
66
61
  end.compact.join("\n")
67
62
  end
68
63
 
69
64
  def cucumber_opts(given)
70
- if given =~ (/--profile/) || given =~ (/(^|\s)-p /)
65
+ if given&.include?('--profile') || given&.include?('-p')
71
66
  given
72
67
  else
73
- [given, profile_from_config].compact.join(" ")
68
+ [*given, *profile_from_config]
74
69
  end
75
70
  end
76
71
 
77
72
  def profile_from_config
78
73
  # copied from https://github.com/cucumber/cucumber/blob/master/lib/cucumber/cli/profile_loader.rb#L85
79
74
  config = Dir.glob("{,.config/,config/}#{name}{.yml,.yaml}").first
80
- "--profile parallel" if config && File.read(config) =~ /^parallel:/
75
+ ['--profile', 'parallel'] if config && File.read(config) =~ /^parallel:/
81
76
  end
82
77
 
83
78
  def tests_in_groups(tests, num_groups, options = {})
@@ -91,7 +86,7 @@ module ParallelTests
91
86
  end
92
87
 
93
88
  def runtime_logging
94
- "--format ParallelTests::Gherkin::RuntimeLogger --out #{runtime_log}"
89
+ ['--format', 'ParallelTests::Gherkin::RuntimeLogger', '--out', runtime_log]
95
90
  end
96
91
 
97
92
  def runtime_log
@@ -102,11 +97,11 @@ module ParallelTests
102
97
  if File.exist?("bin/#{name}")
103
98
  ParallelTests.with_ruby_binary("bin/#{name}")
104
99
  elsif ParallelTests.bundler_enabled?
105
- "bundle exec #{name}"
100
+ ["bundle", "exec", name]
106
101
  elsif File.file?("script/#{name}")
107
102
  ParallelTests.with_ruby_binary("script/#{name}")
108
103
  else
109
- name.to_s
104
+ [name.to_s]
110
105
  end
111
106
  end
112
107
  end
@@ -26,10 +26,6 @@ module ParallelTests
26
26
 
27
27
  isolate_count = isolate_count(options)
28
28
 
29
- if isolate_count >= num_groups
30
- raise 'Number of isolated processes must be less than total the number of processes'
31
- end
32
-
33
29
  if isolate_count >= num_groups
34
30
  raise 'Number of isolated processes must be >= total number of processes'
35
31
  end
@@ -38,7 +34,7 @@ module ParallelTests
38
34
  # add all files that should run in a multiple isolated processes to their own groups
39
35
  group_features_by_size(items_to_group(single_items), groups[0..(isolate_count - 1)])
40
36
  # group the non-isolated by size
41
- group_features_by_size(items_to_group(items), groups[isolate_count..-1])
37
+ group_features_by_size(items_to_group(items), groups[isolate_count..])
42
38
  else
43
39
  # add all files that should run in a single non-isolated process to first group
44
40
  single_items.each { |item, size| add_to_group(groups.first, item, size) }
@@ -43,14 +43,14 @@ module ParallelTests
43
43
 
44
44
  def read
45
45
  sync do
46
- contents = IO.read(file_path)
46
+ contents = File.read(file_path)
47
47
  return if contents.empty?
48
48
  @pids = JSON.parse(contents)
49
49
  end
50
50
  end
51
51
 
52
52
  def save
53
- sync { IO.write(file_path, pids.to_json) }
53
+ sync { File.write(file_path, pids.to_json) }
54
54
  end
55
55
 
56
56
  def sync(&block)
@@ -7,8 +7,7 @@ module ParallelTests
7
7
  DEV_NULL = (WINDOWS ? "NUL" : "/dev/null")
8
8
  class << self
9
9
  def run_tests(test_files, process_number, num_processes, options)
10
- exe = executable # expensive, so we cache
11
- cmd = [exe, options[:test_options], color, spec_opts, *test_files].compact.join(" ")
10
+ cmd = [*executable, *options[:test_options], *color, *spec_opts, *test_files]
12
11
  execute_command(cmd, process_number, num_processes, options)
13
12
  end
14
13
 
@@ -16,9 +15,9 @@ module ParallelTests
16
15
  if File.exist?("bin/rspec")
17
16
  ParallelTests.with_ruby_binary("bin/rspec")
18
17
  elsif ParallelTests.bundler_enabled?
19
- "bundle exec rspec"
18
+ ["bundle", "exec", "rspec"]
20
19
  else
21
- "rspec"
20
+ ["rspec"]
22
21
  end
23
22
  end
24
23
 
@@ -48,8 +47,8 @@ module ParallelTests
48
47
  # --order rand:1234
49
48
  # --order random:1234
50
49
  def command_with_seed(cmd, seed)
51
- clean = cmd.sub(/\s--(seed\s+\d+|order\s+rand(om)?(:\d+)?)\b/, '')
52
- "#{clean} --seed #{seed}"
50
+ clean = remove_command_arguments(cmd, '--seed', '--order')
51
+ [*clean, '--seed', seed]
53
52
  end
54
53
 
55
54
  # Summarize results from threads and colorize results based on failure and pending counts.
@@ -71,19 +70,13 @@ module ParallelTests
71
70
 
72
71
  private
73
72
 
74
- # so it can be stubbed....
75
- def run(cmd)
76
- `#{cmd}`
77
- end
78
-
79
73
  def color
80
- '--color --tty' if $stdout.tty?
74
+ ['--color', '--tty'] if $stdout.tty?
81
75
  end
82
76
 
83
77
  def spec_opts
84
78
  options_file = ['.rspec_parallel', 'spec/parallel_spec.opts', 'spec/spec.opts'].detect { |f| File.file?(f) }
85
- return unless options_file
86
- "-O #{options_file}"
79
+ ["-O", options_file] if options_file
87
80
  end
88
81
  end
89
82
  end
@@ -9,16 +9,8 @@ module ParallelTests
9
9
  'test'
10
10
  end
11
11
 
12
- def rake_bin
13
- # Prevent 'Exec format error' Errno::ENOEXEC on Windows
14
- return "rake" if RUBY_PLATFORM =~ /mswin|mingw|cygwin/
15
- binstub_path = File.join('bin', 'rake')
16
- return binstub_path if File.exist?(binstub_path)
17
- "rake"
18
- end
19
-
20
12
  def load_lib
21
- $LOAD_PATH << File.expand_path(File.join(File.dirname(__FILE__), '..'))
13
+ $LOAD_PATH << File.expand_path('..', __dir__)
22
14
  require "parallel_tests"
23
15
  end
24
16
 
@@ -30,12 +22,15 @@ module ParallelTests
30
22
 
31
23
  def run_in_parallel(cmd, options = {})
32
24
  load_lib
33
- count = " -n #{options[:count]}" unless options[:count].to_s.empty?
25
+
34
26
  # Using the relative path to find the binary allow to run a specific version of it
35
27
  executable = File.expand_path('../../bin/parallel_test', __dir__)
36
- non_parallel = (options[:non_parallel] ? ' --non-parallel' : '')
37
- command = "#{ParallelTests.with_ruby_binary(Shellwords.escape(executable))} --exec '#{cmd}'#{count}#{non_parallel}"
38
- abort unless system(command)
28
+ command = ParallelTests.with_ruby_binary(executable)
29
+ command += ['--exec', Shellwords.join(cmd)]
30
+ command += ['-n', options[:count]] unless options[:count].to_s.empty?
31
+ command << '--non-parallel' if options[:non_parallel]
32
+
33
+ abort unless system(*command)
39
34
  end
40
35
 
41
36
  # this is a crazy-complex solution for a very simple problem:
@@ -48,16 +43,14 @@ module ParallelTests
48
43
  # - pipefail makes pipe fail with exitstatus of first failed command
49
44
  # - pipefail is not supported in (zsh)
50
45
  # - defining a new rake task like silence_schema would force users to load parallel_tests in test env
51
- # - do not use ' since run_in_parallel uses them to quote stuff
52
46
  # - simple system "set -o pipefail" returns nil even though set -o pipefail exists with 0
53
47
  def suppress_output(command, ignore_regex)
54
48
  activate_pipefail = "set -o pipefail"
55
- remove_ignored_lines = %{(grep -v "#{ignore_regex}" || test 1)}
49
+ remove_ignored_lines = %{(grep -v #{Shellwords.escape(ignore_regex)} || true)}
56
50
 
57
- if File.executable?('/bin/bash') && system('/bin/bash', '-c', "#{activate_pipefail} 2>/dev/null && test 1")
58
- # We need to shell escape single quotes (' becomes '"'"') because
59
- # run_in_parallel wraps command in single quotes
60
- %{/bin/bash -c '"'"'#{activate_pipefail} && (#{command}) | #{remove_ignored_lines}'"'"'}
51
+ if system('/bin/bash', '-c', "#{activate_pipefail} 2>/dev/null")
52
+ shell_command = "#{activate_pipefail} && (#{Shellwords.shelljoin(command)}) | #{remove_ignored_lines}"
53
+ ['/bin/bash', '-c', shell_command]
61
54
  else
62
55
  command
63
56
  end
@@ -90,7 +83,56 @@ module ParallelTests
90
83
  options = args.shift
91
84
  pass_through = args.shift
92
85
 
93
- [num_processes, pattern.to_s, options.to_s, pass_through.to_s]
86
+ [num_processes, pattern, options, pass_through]
87
+ end
88
+
89
+ def schema_format_based_on_rails_version
90
+ if rails_7_or_greater?
91
+ ActiveRecord.schema_format
92
+ else
93
+ ActiveRecord::Base.schema_format
94
+ end
95
+ end
96
+
97
+ def schema_type_based_on_rails_version
98
+ if rails_61_or_greater? || schema_format_based_on_rails_version == :ruby
99
+ "schema"
100
+ else
101
+ "structure"
102
+ end
103
+ end
104
+
105
+ def build_run_command(type, args)
106
+ count, pattern, options, pass_through = ParallelTests::Tasks.parse_args(args)
107
+ test_framework = {
108
+ 'spec' => 'rspec',
109
+ 'test' => 'test',
110
+ 'features' => 'cucumber',
111
+ 'features-spinach' => 'spinach'
112
+ }.fetch(type)
113
+
114
+ type = 'features' if test_framework == 'spinach'
115
+
116
+ # Using the relative path to find the binary allow to run a specific version of it
117
+ executable = File.expand_path('../../bin/parallel_test', __dir__)
118
+ executable = ParallelTests.with_ruby_binary(executable)
119
+
120
+ command = [*executable, type, '--type', test_framework]
121
+ command += ['-n', count.to_s] if count
122
+ command += ['--pattern', pattern] if pattern
123
+ command += ['--test-options', options] if options
124
+ command += Shellwords.shellsplit pass_through if pass_through
125
+ command
126
+ end
127
+
128
+ private
129
+
130
+ def rails_7_or_greater?
131
+ Gem::Version.new(Rails.version) >= Gem::Version.new('7.0')
132
+ end
133
+
134
+ def rails_61_or_greater?
135
+ Gem::Version.new(Rails.version) >= Gem::Version.new('6.1.0')
94
136
  end
95
137
  end
96
138
  end
@@ -99,36 +141,38 @@ end
99
141
  namespace :parallel do
100
142
  desc "Setup test databases via db:setup --> parallel:setup[num_cpus]"
101
143
  task :setup, :count do |_, args|
102
- command = "#{ParallelTests::Tasks.rake_bin} db:setup RAILS_ENV=#{ParallelTests::Tasks.rails_env}"
144
+ command = [$0, "db:setup", "RAILS_ENV=#{ParallelTests::Tasks.rails_env}"]
103
145
  ParallelTests::Tasks.run_in_parallel(ParallelTests::Tasks.suppress_schema_load_output(command), args)
104
146
  end
105
147
 
106
148
  desc "Create test databases via db:create --> parallel:create[num_cpus]"
107
149
  task :create, :count do |_, args|
108
150
  ParallelTests::Tasks.run_in_parallel(
109
- "#{ParallelTests::Tasks.rake_bin} db:create RAILS_ENV=#{ParallelTests::Tasks.rails_env}", args
151
+ [$0, "db:create", "RAILS_ENV=#{ParallelTests::Tasks.rails_env}"],
152
+ args
110
153
  )
111
154
  end
112
155
 
113
156
  desc "Drop test databases via db:drop --> parallel:drop[num_cpus]"
114
157
  task :drop, :count do |_, args|
115
158
  ParallelTests::Tasks.run_in_parallel(
116
- "#{ParallelTests::Tasks.rake_bin} db:drop RAILS_ENV=#{ParallelTests::Tasks.rails_env} " \
117
- "DISABLE_DATABASE_ENVIRONMENT_CHECK=1", args
159
+ [
160
+ $0,
161
+ "db:drop",
162
+ "RAILS_ENV=#{ParallelTests::Tasks.rails_env}",
163
+ "DISABLE_DATABASE_ENVIRONMENT_CHECK=1"
164
+ ],
165
+ args
118
166
  )
119
167
  end
120
168
 
121
169
  desc "Update test databases by dumping and loading --> parallel:prepare[num_cpus]"
122
170
  task(:prepare, [:count]) do |_, args|
123
171
  ParallelTests::Tasks.check_for_pending_migrations
124
- if defined?(ActiveRecord::Base) && [:ruby, :sql].include?(ActiveRecord::Base.schema_format)
172
+
173
+ if defined?(ActiveRecord) && [:ruby, :sql].include?(ParallelTests::Tasks.schema_format_based_on_rails_version)
125
174
  # fast: dump once, load in parallel
126
- type =
127
- if Gem::Version.new(Rails.version) >= Gem::Version.new('6.1.0')
128
- "schema"
129
- else
130
- ActiveRecord::Base.schema_format == :ruby ? "schema" : "structure"
131
- end
175
+ type = ParallelTests::Tasks.schema_type_based_on_rails_version
132
176
 
133
177
  Rake::Task["db:#{type}:dump"].invoke
134
178
 
@@ -140,7 +184,7 @@ namespace :parallel do
140
184
  # slow: dump and load in in serial
141
185
  args = args.to_hash.merge(non_parallel: true) # normal merge returns nil
142
186
  task_name = Rake::Task.task_defined?('db:test:prepare') ? 'db:test:prepare' : 'app:db:test:prepare'
143
- ParallelTests::Tasks.run_in_parallel("#{ParallelTests::Tasks.rake_bin} #{task_name}", args)
187
+ ParallelTests::Tasks.run_in_parallel([$0, task_name], args)
144
188
  next
145
189
  end
146
190
  end
@@ -149,22 +193,29 @@ namespace :parallel do
149
193
  desc "Update test databases via db:migrate --> parallel:migrate[num_cpus]"
150
194
  task :migrate, :count do |_, args|
151
195
  ParallelTests::Tasks.run_in_parallel(
152
- "#{ParallelTests::Tasks.rake_bin} db:migrate RAILS_ENV=#{ParallelTests::Tasks.rails_env}", args
196
+ [$0, "db:migrate", "RAILS_ENV=#{ParallelTests::Tasks.rails_env}"],
197
+ args
153
198
  )
154
199
  end
155
200
 
156
201
  desc "Rollback test databases via db:rollback --> parallel:rollback[num_cpus]"
157
202
  task :rollback, :count do |_, args|
158
203
  ParallelTests::Tasks.run_in_parallel(
159
- "#{ParallelTests::Tasks.rake_bin} db:rollback RAILS_ENV=#{ParallelTests::Tasks.rails_env}", args
204
+ [$0, "db:rollback", "RAILS_ENV=#{ParallelTests::Tasks.rails_env}"],
205
+ args
160
206
  )
161
207
  end
162
208
 
163
209
  # just load the schema (good for integration server <-> no development db)
164
210
  desc "Load dumped schema for test databases via db:schema:load --> parallel:load_schema[num_cpus]"
165
211
  task :load_schema, :count do |_, args|
166
- command = "#{ParallelTests::Tasks.rake_bin} #{ParallelTests::Tasks.purge_before_load} " \
167
- "db:schema:load RAILS_ENV=#{ParallelTests::Tasks.rails_env} DISABLE_DATABASE_ENVIRONMENT_CHECK=1"
212
+ command = [
213
+ $0,
214
+ ParallelTests::Tasks.purge_before_load,
215
+ "db:schema:load",
216
+ "RAILS_ENV=#{ParallelTests::Tasks.rails_env}",
217
+ "DISABLE_DATABASE_ENVIRONMENT_CHECK=1"
218
+ ]
168
219
  ParallelTests::Tasks.run_in_parallel(ParallelTests::Tasks.suppress_schema_load_output(command), args)
169
220
  end
170
221
 
@@ -173,23 +224,34 @@ namespace :parallel do
173
224
  desc "Load structure for test databases via db:schema:load --> parallel:load_structure[num_cpus]"
174
225
  task :load_structure, :count do |_, args|
175
226
  ParallelTests::Tasks.run_in_parallel(
176
- "#{ParallelTests::Tasks.rake_bin} #{ParallelTests::Tasks.purge_before_load} " \
177
- "db:structure:load RAILS_ENV=#{ParallelTests::Tasks.rails_env} DISABLE_DATABASE_ENVIRONMENT_CHECK=1", args
227
+ [
228
+ $0,
229
+ ParallelTests::Tasks.purge_before_load,
230
+ "db:structure:load",
231
+ "RAILS_ENV=#{ParallelTests::Tasks.rails_env}",
232
+ "DISABLE_DATABASE_ENVIRONMENT_CHECK=1"
233
+ ],
234
+ args
178
235
  )
179
236
  end
180
237
 
181
238
  desc "Load the seed data from db/seeds.rb via db:seed --> parallel:seed[num_cpus]"
182
239
  task :seed, :count do |_, args|
183
240
  ParallelTests::Tasks.run_in_parallel(
184
- "#{ParallelTests::Tasks.rake_bin} db:seed RAILS_ENV=#{ParallelTests::Tasks.rails_env}", args
241
+ [
242
+ $0,
243
+ "db:seed",
244
+ "RAILS_ENV=#{ParallelTests::Tasks.rails_env}"
245
+ ],
246
+ args
185
247
  )
186
248
  end
187
249
 
188
250
  desc "Launch given rake command in parallel"
189
251
  task :rake, :command, :count do |_, args|
190
252
  ParallelTests::Tasks.run_in_parallel(
191
- "RAILS_ENV=#{ParallelTests::Tasks.rails_env} #{ParallelTests::Tasks.rake_bin} " \
192
- "#{args.command}", args
253
+ [$0, args.command, "RAILS_ENV=#{ParallelTests::Tasks.rails_env}"],
254
+ args
193
255
  )
194
256
  end
195
257
 
@@ -198,26 +260,9 @@ namespace :parallel do
198
260
  task type, [:count, :pattern, :options, :pass_through] do |_t, args|
199
261
  ParallelTests::Tasks.check_for_pending_migrations
200
262
  ParallelTests::Tasks.load_lib
263
+ command = ParallelTests::Tasks.build_run_command(type, args)
201
264
 
202
- count, pattern, options, pass_through = ParallelTests::Tasks.parse_args(args)
203
- test_framework = {
204
- 'spec' => 'rspec',
205
- 'test' => 'test',
206
- 'features' => 'cucumber',
207
- 'features-spinach' => 'spinach'
208
- }[type]
209
-
210
- type = 'features' if test_framework == 'spinach'
211
- # Using the relative path to find the binary allow to run a specific version of it
212
- executable = File.join(File.dirname(__FILE__), '..', '..', 'bin', 'parallel_test')
213
-
214
- command = "#{ParallelTests.with_ruby_binary(Shellwords.escape(executable))} #{type} " \
215
- "--type #{test_framework} " \
216
- "-n #{count} " \
217
- "--pattern '#{pattern}' " \
218
- "--test-options '#{options}' " \
219
- "#{pass_through}"
220
- abort unless system(command) # allow to chain tasks e.g. rake parallel:spec parallel:features
265
+ abort unless system(*command) # allow to chain tasks e.g. rake parallel:spec parallel:features
221
266
  end
222
267
  end
223
268
  end
@@ -1,9 +1,12 @@
1
1
  # frozen_string_literal: true
2
+ require 'shellwords'
2
3
  require 'parallel_tests'
3
4
 
4
5
  module ParallelTests
5
6
  module Test
6
7
  class Runner
8
+ RuntimeLogTooSmallError = Class.new(StandardError)
9
+
7
10
  class << self
8
11
  # --- usually overwritten by other runners
9
12
 
@@ -25,7 +28,14 @@ module ParallelTests
25
28
 
26
29
  def run_tests(test_files, process_number, num_processes, options)
27
30
  require_list = test_files.map { |file| file.gsub(" ", "\\ ") }.join(" ")
28
- cmd = "#{executable} -Itest -e '%w[#{require_list}].each { |f| require %{./\#{f}} }' -- #{options[:test_options]}"
31
+ cmd = [
32
+ *executable,
33
+ '-Itest',
34
+ '-e',
35
+ "%w[#{require_list}].each { |f| require %{./\#{f}} }",
36
+ '--',
37
+ *options[:test_options]
38
+ ]
29
39
  execute_command(cmd, process_number, num_processes, options)
30
40
  end
31
41
 
@@ -63,7 +73,7 @@ module ParallelTests
63
73
  []
64
74
  end
65
75
  if runtimes.size * 1.5 > tests.size
66
- puts "Using recorded test runtime"
76
+ puts "Using recorded test runtime" unless options[:quiet]
67
77
  sort_by_runtime(tests, runtimes)
68
78
  else
69
79
  sort_by_filesize(tests)
@@ -76,22 +86,33 @@ module ParallelTests
76
86
  end
77
87
 
78
88
  def execute_command(cmd, process_number, num_processes, options)
89
+ number = test_env_number(process_number, options).to_s
79
90
  env = (options[:env] || {}).merge(
80
- "TEST_ENV_NUMBER" => test_env_number(process_number, options).to_s,
91
+ "TEST_ENV_NUMBER" => number,
81
92
  "PARALLEL_TEST_GROUPS" => num_processes.to_s,
82
93
  "PARALLEL_PID_FILE" => ParallelTests.pid_file_path
83
94
  )
84
- cmd = "nice #{cmd}" if options[:nice]
85
- cmd = "#{cmd} 2>&1" if options[:combine_stderr]
95
+ cmd = ["nice", *cmd] if options[:nice]
96
+
97
+ # being able to run with for example `-output foo-$TEST_ENV_NUMBER` worked originally and is convenient
98
+ cmd = cmd.map { |c| c.gsub("$TEST_ENV_NUMBER", number).gsub("${TEST_ENV_NUMBER}", number) }
86
99
 
87
- puts cmd if report_process_command?(options) && !options[:serialize_stdout]
100
+ print_command(cmd, env) if report_process_command?(options) && !options[:serialize_stdout]
88
101
 
89
102
  execute_command_and_capture_output(env, cmd, options)
90
103
  end
91
104
 
105
+ def print_command(command, env)
106
+ env_str = ['TEST_ENV_NUMBER', 'PARALLEL_TEST_GROUPS'].map { |e| "#{e}=#{env[e]}" }.join(' ')
107
+ puts [env_str, Shellwords.shelljoin(command)].compact.join(' ')
108
+ end
109
+
92
110
  def execute_command_and_capture_output(env, cmd, options)
111
+ popen_options = {} # do not add `pgroup: true`, it will break `binding.irb` inside the test
112
+ popen_options[:err] = [:child, :out] if options[:combine_stderr]
113
+
93
114
  pid = nil
94
- output = IO.popen(env, cmd) do |io|
115
+ output = IO.popen(env, cmd, popen_options) do |io|
95
116
  pid = io.pid
96
117
  ParallelTests.pids.add(pid)
97
118
  capture_output(io, env, options)
@@ -100,9 +121,9 @@ module ParallelTests
100
121
  exitstatus = $?.exitstatus
101
122
  seed = output[/seed (\d+)/, 1]
102
123
 
103
- output = [cmd, output].join("\n") if report_process_command?(options) && options[:serialize_stdout]
124
+ output = "#{Shellwords.shelljoin(cmd)}\n#{output}" if report_process_command?(options) && options[:serialize_stdout]
104
125
 
105
- { stdout: output, exit_status: exitstatus, command: cmd, seed: seed }
126
+ { env: env, stdout: output, exit_status: exitstatus, command: cmd, seed: seed }
106
127
  end
107
128
 
108
129
  def find_results(test_output)
@@ -129,18 +150,22 @@ module ParallelTests
129
150
 
130
151
  # remove old seed and add new seed
131
152
  def command_with_seed(cmd, seed)
132
- clean = cmd.sub(/\s--seed\s+\d+\b/, '')
133
- "#{clean} --seed #{seed}"
153
+ clean = remove_command_arguments(cmd, '--seed')
154
+ [*clean, '--seed', seed]
134
155
  end
135
156
 
136
157
  protected
137
158
 
138
159
  def executable
139
- ENV['PARALLEL_TESTS_EXECUTABLE'] || determine_executable
160
+ if (executable = ENV['PARALLEL_TESTS_EXECUTABLE'])
161
+ [executable]
162
+ else
163
+ determine_executable
164
+ end
140
165
  end
141
166
 
142
167
  def determine_executable
143
- "ruby"
168
+ ["ruby"]
144
169
  end
145
170
 
146
171
  def sum_up_results(results)
@@ -184,7 +209,7 @@ module ParallelTests
184
209
  allowed_missing -= 1 unless time = runtimes[test]
185
210
  if allowed_missing < 0
186
211
  log = options[:runtime_log] || runtime_log
187
- raise "Runtime log file '#{log}' does not contain sufficient data to sort #{tests.size} test files, please update or remove it."
212
+ raise RuntimeLogTooSmallError, "Runtime log file '#{log}' does not contain sufficient data to sort #{tests.size} test files, please update or remove it."
188
213
  end
189
214
  [test, time]
190
215
  end
@@ -237,6 +262,21 @@ module ParallelTests
237
262
  Dir[File.join(folder, pattern)].uniq.sort
238
263
  end
239
264
 
265
+ def remove_command_arguments(command, *args)
266
+ remove_next = false
267
+ command.select do |arg|
268
+ if remove_next
269
+ remove_next = false
270
+ false
271
+ elsif args.include?(arg)
272
+ remove_next = true
273
+ false
274
+ else
275
+ true
276
+ end
277
+ end
278
+ end
279
+
240
280
  private
241
281
 
242
282
  # fill gaps with unknown-runtime if given, average otherwise
@@ -250,7 +290,7 @@ module ParallelTests
250
290
  end
251
291
 
252
292
  def report_process_command?(options)
253
- options[:verbose] || options[:verbose_process_command]
293
+ options[:verbose] || options[:verbose_command]
254
294
  end
255
295
  end
256
296
  end
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module ParallelTests
3
- VERSION = '3.7.3'
3
+ VERSION = '4.2.1'
4
4
  end
@@ -76,7 +76,7 @@ module ParallelTests
76
76
  end
77
77
 
78
78
  def with_ruby_binary(command)
79
- WINDOWS ? "#{RUBY_BINARY} -- #{command}" : command
79
+ WINDOWS ? [RUBY_BINARY, '--', command] : [command]
80
80
  end
81
81
 
82
82
  def wait_for_other_processes_to_finish
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: parallel_tests
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.7.3
4
+ version: 4.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michael Grosser
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-09-17 00:00:00.000000000 Z
11
+ date: 2023-05-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: parallel
@@ -24,7 +24,7 @@ dependencies:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
26
  version: '0'
27
- description:
27
+ description:
28
28
  email: michael@grosser.it
29
29
  executables:
30
30
  - parallel_spinach
@@ -68,10 +68,10 @@ licenses:
68
68
  - MIT
69
69
  metadata:
70
70
  bug_tracker_uri: https://github.com/grosser/parallel_tests/issues
71
- documentation_uri: https://github.com/grosser/parallel_tests/blob/v3.7.3/Readme.md
72
- source_code_uri: https://github.com/grosser/parallel_tests/tree/v3.7.3
71
+ documentation_uri: https://github.com/grosser/parallel_tests/blob/v4.2.1/Readme.md
72
+ source_code_uri: https://github.com/grosser/parallel_tests/tree/v4.2.1
73
73
  wiki_uri: https://github.com/grosser/parallel_tests/wiki
74
- post_install_message:
74
+ post_install_message:
75
75
  rdoc_options: []
76
76
  require_paths:
77
77
  - lib
@@ -79,15 +79,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
79
79
  requirements:
80
80
  - - ">="
81
81
  - !ruby/object:Gem::Version
82
- version: 2.5.0
82
+ version: 2.7.0
83
83
  required_rubygems_version: !ruby/object:Gem::Requirement
84
84
  requirements:
85
85
  - - ">="
86
86
  - !ruby/object:Gem::Version
87
87
  version: '0'
88
88
  requirements: []
89
- rubygems_version: 3.2.16
90
- signing_key:
89
+ rubygems_version: 3.3.3
90
+ signing_key:
91
91
  specification_version: 4
92
92
  summary: Run Test::Unit / RSpec / Cucumber / Spinach in parallel
93
93
  test_files: []