From 6ff53713aef4ce597b809525fc61f3b326acc40e Mon Sep 17 00:00:00 2001 From: Michael Grosser Date: Thu, 29 May 2025 12:15:17 +0200 Subject: [PATCH 1/4] cleanup --- CHANGELOG.md | 10 ++++------ lib/parallel_tests/cli.rb | 6 +++--- lib/parallel_tests/gherkin/runner.rb | 6 +++--- lib/parallel_tests/rspec/runner.rb | 4 ++-- lib/parallel_tests/test/runner.rb | 15 ++++++++------- 5 files changed, 20 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d1bda7b2..7a0fbf90 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 `-` diff --git a/lib/parallel_tests/cli.rb b/lib/parallel_tests/cli.rb index 9399122a..61327383 100644 --- a/lib/parallel_tests/cli.rb +++ b/lib/parallel_tests/cli.rb @@ -273,11 +273,11 @@ def parse_options!(argv) 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("-e", "--exec COMMAND", "execute COMMAND in 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. + 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 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) } diff --git a/lib/parallel_tests/gherkin/runner.rb b/lib/parallel_tests/gherkin/runner.rb index 10a34336..5182e8d3 100644 --- a/lib/parallel_tests/gherkin/runner.rb +++ b/lib/parallel_tests/gherkin/runner.rb @@ -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 @@ -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, diff --git a/lib/parallel_tests/rspec/runner.rb b/lib/parallel_tests/rspec/runner.rb index 7e0e0104..3b351983 100644 --- a/lib/parallel_tests/rspec/runner.rb +++ b/lib/parallel_tests/rspec/runner.rb @@ -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 @@ -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 diff --git a/lib/parallel_tests/test/runner.rb b/lib/parallel_tests/test/runner.rb index 09940175..5e8669e9 100644 --- a/lib/parallel_tests/test/runner.rb +++ b/lib/parallel_tests/test/runner.rb @@ -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 @@ -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] ] From 0e33487c2653231f64c5bdcc99856b0b26c32c58 Mon Sep 17 00:00:00 2001 From: Michael Grosser Date: Thu, 29 May 2025 13:51:48 +0200 Subject: [PATCH 2/4] fix --- spec/integration_spec.rb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/spec/integration_spec.rb b/spec/integration_spec.rb index b3d06d77..a51069a3 100644 --- a/spec/integration_spec.rb +++ b/spec/integration_spec.rb @@ -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) From 55e5dcb2337a7947654f1bbd042564c041083c60 Mon Sep 17 00:00:00 2001 From: Michael Grosser Date: Thu, 29 May 2025 18:08:01 +0200 Subject: [PATCH 3/4] readme --- lib/parallel_tests/cli.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/parallel_tests/cli.rb b/lib/parallel_tests/cli.rb index 61327383..60dc6c4d 100644 --- a/lib/parallel_tests/cli.rb +++ b/lib/parallel_tests/cli.rb @@ -277,7 +277,7 @@ def parse_options!(argv) opts.on("--exec-args COMMAND", <<~TEXT.rstrip.split("\n").join("\n#{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 + > 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) } From f6d6daac16e489a6b4f2e2d10a5ddbd371ef8f88 Mon Sep 17 00:00:00 2001 From: Michael Grosser Date: Thu, 29 May 2025 18:18:16 +0200 Subject: [PATCH 4/4] automate readme --- Rakefile | 12 ++++++++++ Readme.md | 15 ++++++++----- lib/parallel_tests/cli.rb | 46 +++++++++++++++++++++++++++++---------- 3 files changed, 56 insertions(+), 17 deletions(-) diff --git a/Rakefile b/Rakefile index d184aff7..afbf7687 100644 --- a/Rakefile +++ b/Rakefile @@ -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 = "" + parts = File.read(file).split(separator) + parts[1] = output + File.write file, parts.join(separator) +end diff --git a/Readme.md b/Readme.md index 6619cdaf..d922091b 100644 --- a/Readme.md +++ b/Readme.md @@ -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: - + -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 @@ -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' @@ -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): @@ -291,7 +291,9 @@ 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 @@ -299,6 +301,7 @@ Options are: --quiet Print only tests output -v, --version Show Version -h, --help Show this. + You can run any kind of code in parallel with -e / --exec diff --git a/lib/parallel_tests/cli.rb b/lib/parallel_tests/cli.rb index 60dc6c4d..d53fbef5 100644 --- a/lib/parallel_tests/cli.rb +++ b/lib/parallel_tests/cli.rb @@ -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', ...) @@ -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 @@ -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 @@ -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 @@ -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 COMMAND in 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 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 + 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 } @@ -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 } @@ -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