Skip to content
Merged

cleanup #1009

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 4 additions & 6 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,16 @@

## Unreleased

### Breaking Changes

### Added

### Fixed
only add here if you are working on a PR

## 5.3.0 - 2025-05-24
### Breaking Changes

### Added

- The `--exec-args` option, which allows users to run shell commands in parallel with test files as arguments

### Fixed

## 5.2.0 - 2025-05-08

- The `specify-groups` option supports reading from STDIN when set to `-`
Expand Down
12 changes: 12 additions & 0 deletions Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,15 @@ task :bundle_all do
end
end
end

desc "render the README option section"
task :readme do
output = `bundle exec ./bin/parallel_test -h`
abort "Command failed: #{output}" unless $?.success?
output.sub!(/.*Options are:/m, "") || raise
file = "README.md"
separator = "<!-- rake readme -->"
parts = File.read(file).split(separator)
parts[1] = output
File.write file, parts.join(separator)
end
15 changes: 9 additions & 6 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@ Setup for non-rails
`parallel_cucumber -n 2 -o '-p foo_profile --tags @only_this_tag or @only_that_tag --format summary'`

Options are:
<!-- copy output from bundle exec ./bin/parallel_test -h -->
<!-- rake readme -->
-n PROCESSES How many processes to use, default: available CPUs
-p, --pattern PATTERN run tests matching this regex pattern
--exclude-pattern PATTERN exclude tests matching this regex pattern
Expand All @@ -258,7 +258,7 @@ Options are:
--failure-exit-code INT Specify the exit code to use when tests fail
--specify-groups SPECS Use 'specify-groups' if you want to specify multiple specs running in multiple
processes in a specific formation. Commas indicate specs in the same process,
pipes indicate specs in a new process. If SPECS is a `-` the value for this
pipes indicate specs in a new process. If SPECS is a '-' the value for this
option is read from STDIN instead. Cannot use with --single, --isolate, or
--isolate-n. Ex.
$ parallel_tests -n 3 . --specify-groups '1_spec.rb,2_spec.rb|3_spec.rb'
Expand All @@ -268,10 +268,10 @@ Options are:
--only-group GROUP_INDEX[,GROUP_INDEX]
Only run the given group numbers.
Changes `--group-by` default to 'filesize'.
-e, --exec COMMAND execute this code parallel and with ENV['TEST_ENV_NUMBER']
--exec-args COMMAND execute this code parallel with test files as arguments, Ex.
-e, --exec COMMAND execute COMMAND in parallel and with ENV['TEST_ENV_NUMBER']
--exec-args COMMAND execute COMMAND in parallel with test files as arguments, for example:
$ parallel_tests --exec-args echo
echo spec/a_spec.rb spec/b_spec.rb
> echo spec/a_spec.rb spec/b_spec.rb
-o, --test-options 'OPTIONS' execute test commands with those options
-t, --type TYPE test(default) / rspec / cucumber / spinach
--suffix PATTERN override built in test file pattern (should match suffix):
Expand All @@ -291,14 +291,17 @@ Options are:
--unknown-runtime SECONDS Use given number as unknown runtime (otherwise use average time)
--first-is-1 Use "1" as TEST_ENV_NUMBER to not reuse the default test environment
--fail-fast Stop all groups when one group fails (best used with --test-options '--fail-fast' if supported
--test-file-limit LIMIT Limit to this number of files per test run by batching (for windows set to ~100 to stay below 8192 max command limit, might have bugs from reusing test-env-number and summarizing partial results)
--test-file-limit LIMIT Limit to this number of files per test run by batching
(for windows set to ~100 to stay below 8192 max command limit, might have bugs from reusing test-env-number
and summarizing partial results)
--verbose Print debug output
--verbose-command Combines options --verbose-process-command and --verbose-rerun-command
--verbose-process-command Print the command that will be executed by each process before it begins
--verbose-rerun-command After a process fails, print the command executed by that process
--quiet Print only tests output
-v, --version Show Version
-h, --help Show this.
<!-- rake readme -->

You can run any kind of code in parallel with -e / --exec

Expand Down
48 changes: 36 additions & 12 deletions lib/parallel_tests/cli.rb
Original file line number Diff line number Diff line change
Expand Up @@ -191,8 +191,9 @@ def any_test_failed?(test_results)
end

def parse_options!(argv)
newline_padding = " " * 37
newline_padding = 37 # poor man's way of getting a decent table like layout for -h output on 120 char width terminal
options = {}

OptionParser.new do |opts|
opts.banner = <<~BANNER
Run all tests in parallel, giving each process ENV['TEST_ENV_NUMBER'] ('', '2', '3', ...)
Expand All @@ -205,12 +206,14 @@ def parse_options!(argv)

Options are:
BANNER

opts.on("-n PROCESSES", Integer, "How many processes to use, default: available CPUs") { |n| options[:count] = n }
opts.on("-p", "--pattern PATTERN", "run tests matching this regex pattern") { |pattern| options[:pattern] = /#{pattern}/ }
opts.on("--exclude-pattern", "--exclude-pattern PATTERN", "exclude tests matching this regex pattern") { |pattern| options[:exclude_pattern] = /#{pattern}/ }

opts.on(
"--group-by TYPE",
<<~TEXT.rstrip.split("\n").join("\n#{newline_padding}")
heredoc(<<~TEXT, newline_padding)
group tests by:
found - order of finding files
steps - number of cucumber/spinach steps
Expand All @@ -220,6 +223,7 @@ def parse_options!(argv)
default - runtime when runtime log is filled otherwise filesize
TEXT
) { |type| options[:group_by] = type.to_sym }

opts.on("-m COUNT", "--multiply-processes COUNT", Float, "use given number as a multiplier of processes to run") do |m|
options[:multiply_processes] = m
end
Expand Down Expand Up @@ -251,7 +255,7 @@ def parse_options!(argv)

opts.on(
"--specify-groups SPECS",
<<~TEXT.rstrip.split("\n").join("\n#{newline_padding}")
heredoc(<<~TEXT, newline_padding)
Use 'specify-groups' if you want to specify multiple specs running in multiple
processes in a specific formation. Commas indicate specs in the same process,
pipes indicate specs in a new process. If SPECS is a '-' the value for this
Expand All @@ -267,34 +271,40 @@ def parse_options!(argv)
opts.on(
"--only-group GROUP_INDEX[,GROUP_INDEX]",
Array,
<<~TEXT.rstrip.split("\n").join("\n#{newline_padding}")
heredoc(<<~TEXT, newline_padding)
Only run the given group numbers.
Changes `--group-by` default to 'filesize'.
TEXT
) { |groups| options[:only_group] = groups.map(&:to_i) }

opts.on("-e", "--exec COMMAND", "execute this code parallel and with ENV['TEST_ENV_NUMBER']") { |arg| options[:execute] = Shellwords.shellsplit(arg) }
opts.on("--exec-args COMMAND", <<~TEXT.rstrip.split("\n").join("\n#{newline_padding}")
execute this code parallel with test files as arguments, Ex.
$ parallel_tests --exec-args echo
echo spec/a_spec.rb spec/b_spec.rb#{' '}
TEXT
opts.on("-e", "--exec COMMAND", "execute COMMAND in parallel and with ENV['TEST_ENV_NUMBER']") { |arg| options[:execute] = Shellwords.shellsplit(arg) }
opts.on(
"--exec-args COMMAND",
heredoc(<<~TEXT, newline_padding)
execute COMMAND in parallel with test files as arguments, for example:
$ parallel_tests --exec-args echo
> echo spec/a_spec.rb spec/b_spec.rb
TEXT
) { |arg| options[:execute_args] = Shellwords.shellsplit(arg) }

opts.on("-o", "--test-options 'OPTIONS'", "execute test commands with those options") { |arg| options[:test_options] = Shellwords.shellsplit(arg) }

opts.on("-t", "--type TYPE", "test(default) / rspec / cucumber / spinach") do |type|
@runner = load_runner(type)
rescue NameError, LoadError => e
puts "Runner for `#{type}` type has not been found! (#{e})"
abort
end

opts.on(
"--suffix PATTERN",
<<~TEXT.rstrip.split("\n").join("\n#{newline_padding}")
heredoc(<<~TEXT, newline_padding)
override built in test file pattern (should match suffix):
'_spec.rb$' - matches rspec files
'_(test|spec).rb$' - matches test or spec files
TEXT
) { |pattern| options[:suffix] = /#{pattern}/ }

opts.on("--serialize-stdout", "Serialize stdout output, nothing will be written until everything is done") { options[:serialize_stdout] = true }
opts.on("--prefix-output-with-test-env-number", "Prefixes test env number to the output when not using --serialize-stdout") { options[:prefix_output_with_test_env_number] = true }
opts.on("--combine-stderr", "Combine stderr into stdout, useful in conjunction with --serialize-stdout") { options[:combine_stderr] = true }
Expand All @@ -308,7 +318,17 @@ def parse_options!(argv)
opts.on("--unknown-runtime SECONDS", Float, "Use given number as unknown runtime (otherwise use average time)") { |time| options[:unknown_runtime] = time }
opts.on("--first-is-1", "Use \"1\" as TEST_ENV_NUMBER to not reuse the default test environment") { options[:first_is_1] = true }
opts.on("--fail-fast", "Stop all groups when one group fails (best used with --test-options '--fail-fast' if supported") { options[:fail_fast] = true }
opts.on("--test-file-limit LIMIT", Integer, "Limit to this number of files per test run by batching (for windows set to ~100 to stay below 8192 max command limit, might have bugs from reusing test-env-number and summarizing partial results)") { |limit| options[:test_file_limit] = limit }

opts.on(
"--test-file-limit LIMIT",
Integer,
heredoc(<<~TEXT, newline_padding)
Limit to this number of files per test run by batching
(for windows set to ~100 to stay below 8192 max command limit, might have bugs from reusing test-env-number
and summarizing partial results)
TEXT
) { |limit| options[:test_file_limit] = limit }

opts.on("--verbose", "Print debug output") { options[:verbose] = true }
opts.on("--verbose-command", "Combines options --verbose-process-command and --verbose-rerun-command") { options.merge! verbose_process_command: true, verbose_rerun_command: true }
opts.on("--verbose-process-command", "Print the command that will be executed by each process before it begins") { options[:verbose_process_command] = true }
Expand Down Expand Up @@ -458,5 +478,9 @@ def simulate_output_for_ci(simulate)
yield
end
end

def heredoc(text, newline_padding)
text.rstrip.gsub("\n", "\n#{' ' * newline_padding}")
end
end
end
6 changes: 3 additions & 3 deletions lib/parallel_tests/gherkin/runner.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ def run_tests(test_files, process_number, num_processes, options)
options[:env] ||= {}
options[:env] = options[:env].merge({ 'AUTOTEST' => '1' }) if $stdout.tty?

execute_command(get_cmd(combined_scenarios, options), process_number, num_processes, options)
execute_command(build_command(combined_scenarios, options), process_number, num_processes, options)
end

def test_file_name
Expand All @@ -37,8 +37,8 @@ def line_is_result?(line)
line =~ /^\d+ (steps?|scenarios?)/
end

def build_cmd(file_list, options)
cmd = [
def build_test_command(file_list, options)
[
*executable,
*(runtime_logging if File.directory?(File.dirname(runtime_log))),
*file_list,
Expand Down
4 changes: 2 additions & 2 deletions lib/parallel_tests/rspec/runner.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ module RSpec
class Runner < ParallelTests::Test::Runner
class << self
def run_tests(test_files, process_number, num_processes, options)
execute_command(get_cmd(test_files, options), process_number, num_processes, options)
execute_command(build_command(test_files, options), process_number, num_processes, options)
end

def determine_executable
Expand Down Expand Up @@ -41,7 +41,7 @@ def line_is_result?(line)
line =~ /\d+ examples?, \d+ failures?/
end

def build_cmd(file_list, options)
def build_test_command(file_list, options)
[*executable, *options[:test_options], *color, *spec_opts, *file_list]
end

Expand Down
15 changes: 8 additions & 7 deletions lib/parallel_tests/test/runner.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ def test_file_name

def run_tests(test_files, process_number, num_processes, options)
require_list = test_files.map { |file| file.gsub(" ", "\\ ") }.join(" ")
execute_command(get_cmd(require_list, options), process_number, num_processes, options)
execute_command(build_command(require_list, options), process_number, num_processes, options)
end

# ignores other commands runner noise
Expand Down Expand Up @@ -160,20 +160,21 @@ def determine_executable
["ruby"]
end

def get_cmd(file_list, options = {})
def build_command(file_list, options)
if options[:execute_args]
[*options[:execute_args], *file_list]
options[:execute_args] + file_list
else
build_cmd(file_list, options)
build_test_command(file_list, options)
end
end

def build_cmd(file_list, options = {})
# load all test files, to be overwritten by other runners
def build_test_command(file_list, options)
[
*executable,
'-Itest',
'-Itest', # adding ./test directory to the load path for compatibility to common setups
'-e',
"%w[#{file_list}].each { |f| require %{./\#{f}} }",
"%w[#{file_list}].each { |f| require %{./\#{f}} }", # using %w to keep things readable
'--',
*options[:test_options]
]
Expand Down
5 changes: 3 additions & 2 deletions spec/integration_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -346,10 +346,11 @@ def test_unicode
end

it "runs two commands in parallel with files as arguments" do
write 'spec/xxx_spec.rb', 'describe("it"){it("should"){puts "TEST1"}}'
write 'spec/xxx_spec.rb', 'p ARGV; describe("it"){it("should"){puts "TEST1"}}'
write 'spec/xxx2_spec.rb', 'describe("it"){it("should"){puts "TEST2"}}'

result = run_tests "spec", type: 'rspec', add: ["--exec-args", "echo 'hello world' && rspec"]
# need to `--` so sh uses them as arguments that then go into $@
result = run_tests "spec", type: 'rspec', add: ["--exec-args", "sh -c \"echo 'hello world' && rspec $@\" --"]

expect(result).to include_exactly_times('hello world', 2)
expect(result).to include_exactly_times('TEST1', 1)
Expand Down
Loading