parallel_tests 3.7.3 → 4.2.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 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: []