parallel_tests 3.8.1 → 4.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e4a7071d3e5afb39f01dd57304c8fab20d985f25c2f3699a7f6e69074715e576
4
- data.tar.gz: 64b9cb4fb05ed5cfc04554bc6fb04960fbefdf0ac756062fe01e7e15383fb1bc
3
+ metadata.gz: 1bcc6e9cd8a207f7a7ec8253139040265cf1c1962e5d25629bca37e5cd2b111e
4
+ data.tar.gz: 7db53c69e2048799c12504a6ce7b56b6b7b4833ef77fdc298ba50c61fe8ed742
5
5
  SHA512:
6
- metadata.gz: 61639ad8b786efee4bd6e868d244531008d7eade53e3fb419f26ce7688a0b10d8609624f078e48562b3ac5ffb4560515e2ac869ebb6ddcb7616c10e869d22f70
7
- data.tar.gz: 2f15bd174e7e5064efa7186cdb604620cdd8aa0fb11d7702c4db3f8f558ca972385d65af51dade0ee8adaf80229673785f57c9160e3d38046c56a0ce539217c3
6
+ metadata.gz: f682ef1d3752cd3893e186879d905954456b51900ce0d4cfd2213f1c226d13fb38d1a3f251dc75bccb8da663273f59ef977e0aa92707f3ede49db381a53c40f5
7
+ data.tar.gz: 7a3bad92876b0b225f381b60b89e7049f84e9069a89b710749ecad0e6beed6a60952ed3d850dbe90621a731a974b7694b9182bb02a0f5e3e5292ccb2b6226614
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:
@@ -402,6 +401,9 @@ inspired by [pivotal labs](https://blog.pivotal.io/labs/labs/parallelize-your-rs
402
401
  - [Vikram B Kumar](https://github.com/v-kumar)
403
402
  - [Joshua Pinter](https://github.com/joshuapinter)
404
403
  - [Zach Dennis](https://github.com/zdennis)
404
+ - [Jon Dufresne](https://github.com/jdufresne)
405
+ - [Eric Kessler](https://github.com/enkessler)
406
+ - [Adis Osmonov](https://github.com/adis-io)
405
407
 
406
408
  [Michael Grosser](http://grosser.it)<br/>
407
409
  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
@@ -38,7 +38,7 @@ module ParallelTests
38
38
  # add all files that should run in a multiple isolated processes to their own groups
39
39
  group_features_by_size(items_to_group(single_items), groups[0..(isolate_count - 1)])
40
40
  # group the non-isolated by size
41
- group_features_by_size(items_to_group(items), groups[isolate_count..-1])
41
+ group_features_by_size(items_to_group(items), groups[isolate_count..])
42
42
  else
43
43
  # add all files that should run in a single non-isolated process to first group
44
44
  single_items.each { |item, size| add_to_group(groups.first, item, size) }
@@ -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,7 @@ 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]
94
87
  end
95
88
 
96
89
  def schema_format_based_on_rails_version
@@ -109,6 +102,29 @@ module ParallelTests
109
102
  end
110
103
  end
111
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
+
112
128
  private
113
129
 
114
130
  def rails_7_or_greater?
@@ -125,22 +141,28 @@ end
125
141
  namespace :parallel do
126
142
  desc "Setup test databases via db:setup --> parallel:setup[num_cpus]"
127
143
  task :setup, :count do |_, args|
128
- command = "#{ParallelTests::Tasks.rake_bin} db:setup RAILS_ENV=#{ParallelTests::Tasks.rails_env}"
144
+ command = [$0, "db:setup", "RAILS_ENV=#{ParallelTests::Tasks.rails_env}"]
129
145
  ParallelTests::Tasks.run_in_parallel(ParallelTests::Tasks.suppress_schema_load_output(command), args)
130
146
  end
131
147
 
132
148
  desc "Create test databases via db:create --> parallel:create[num_cpus]"
133
149
  task :create, :count do |_, args|
134
150
  ParallelTests::Tasks.run_in_parallel(
135
- "#{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
136
153
  )
137
154
  end
138
155
 
139
156
  desc "Drop test databases via db:drop --> parallel:drop[num_cpus]"
140
157
  task :drop, :count do |_, args|
141
158
  ParallelTests::Tasks.run_in_parallel(
142
- "#{ParallelTests::Tasks.rake_bin} db:drop RAILS_ENV=#{ParallelTests::Tasks.rails_env} " \
143
- "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
144
166
  )
145
167
  end
146
168
 
@@ -162,7 +184,7 @@ namespace :parallel do
162
184
  # slow: dump and load in in serial
163
185
  args = args.to_hash.merge(non_parallel: true) # normal merge returns nil
164
186
  task_name = Rake::Task.task_defined?('db:test:prepare') ? 'db:test:prepare' : 'app:db:test:prepare'
165
- ParallelTests::Tasks.run_in_parallel("#{ParallelTests::Tasks.rake_bin} #{task_name}", args)
187
+ ParallelTests::Tasks.run_in_parallel([$0, task_name], args)
166
188
  next
167
189
  end
168
190
  end
@@ -171,23 +193,29 @@ namespace :parallel do
171
193
  desc "Update test databases via db:migrate --> parallel:migrate[num_cpus]"
172
194
  task :migrate, :count do |_, args|
173
195
  ParallelTests::Tasks.run_in_parallel(
174
- "#{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
175
198
  )
176
199
  end
177
200
 
178
201
  desc "Rollback test databases via db:rollback --> parallel:rollback[num_cpus]"
179
202
  task :rollback, :count do |_, args|
180
203
  ParallelTests::Tasks.run_in_parallel(
181
- "#{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
182
206
  )
183
207
  end
184
208
 
185
209
  # just load the schema (good for integration server <-> no development db)
186
210
  desc "Load dumped schema for test databases via db:schema:load --> parallel:load_schema[num_cpus]"
187
211
  task :load_schema, :count do |_, args|
188
- command =
189
- "#{ParallelTests::Tasks.rake_bin} #{ParallelTests::Tasks.purge_before_load} " \
190
- "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
+ ]
191
219
  ParallelTests::Tasks.run_in_parallel(ParallelTests::Tasks.suppress_schema_load_output(command), args)
192
220
  end
193
221
 
@@ -196,23 +224,34 @@ namespace :parallel do
196
224
  desc "Load structure for test databases via db:schema:load --> parallel:load_structure[num_cpus]"
197
225
  task :load_structure, :count do |_, args|
198
226
  ParallelTests::Tasks.run_in_parallel(
199
- "#{ParallelTests::Tasks.rake_bin} #{ParallelTests::Tasks.purge_before_load} " \
200
- "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
201
235
  )
202
236
  end
203
237
 
204
238
  desc "Load the seed data from db/seeds.rb via db:seed --> parallel:seed[num_cpus]"
205
239
  task :seed, :count do |_, args|
206
240
  ParallelTests::Tasks.run_in_parallel(
207
- "#{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
208
247
  )
209
248
  end
210
249
 
211
250
  desc "Launch given rake command in parallel"
212
251
  task :rake, :command, :count do |_, args|
213
252
  ParallelTests::Tasks.run_in_parallel(
214
- "RAILS_ENV=#{ParallelTests::Tasks.rails_env} #{ParallelTests::Tasks.rake_bin} " \
215
- "#{args.command}", args
253
+ [$0, args.command, "RAILS_ENV=#{ParallelTests::Tasks.rails_env}"],
254
+ args
216
255
  )
217
256
  end
218
257
 
@@ -221,27 +260,9 @@ namespace :parallel do
221
260
  task type, [:count, :pattern, :options, :pass_through] do |_t, args|
222
261
  ParallelTests::Tasks.check_for_pending_migrations
223
262
  ParallelTests::Tasks.load_lib
263
+ command = ParallelTests::Tasks.build_run_command(type, args)
224
264
 
225
- count, pattern, options, pass_through = ParallelTests::Tasks.parse_args(args)
226
- test_framework = {
227
- 'spec' => 'rspec',
228
- 'test' => 'test',
229
- 'features' => 'cucumber',
230
- 'features-spinach' => 'spinach'
231
- }[type]
232
-
233
- type = 'features' if test_framework == 'spinach'
234
- # Using the relative path to find the binary allow to run a specific version of it
235
- executable = File.join(File.dirname(__FILE__), '..', '..', 'bin', 'parallel_test')
236
-
237
- command =
238
- "#{ParallelTests.with_ruby_binary(Shellwords.escape(executable))} #{type} " \
239
- "--type #{test_framework} " \
240
- "-n #{count} " \
241
- "--pattern '#{pattern}' " \
242
- "--test-options '#{options}' " \
243
- "#{pass_through}"
244
- 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
245
266
  end
246
267
  end
247
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.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.8.1'
3
+ VERSION = '4.2.0'
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.8.1
4
+ version: 4.2.0
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: 2022-03-28 00:00:00.000000000 Z
11
+ date: 2023-02-06 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.8.1/Readme.md
72
- source_code_uri: https://github.com/grosser/parallel_tests/tree/v3.8.1
71
+ documentation_uri: https://github.com/grosser/parallel_tests/blob/v4.2.0/Readme.md
72
+ source_code_uri: https://github.com/grosser/parallel_tests/tree/v4.2.0
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.3.10
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: []