diff --git a/CHANGELOG.md b/CHANGELOG.md index e1253599..a67fa413 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +- Added support for multiple isolated processes. + ### Breaking Changes - None diff --git a/Readme.md b/Readme.md index 29e99ff6..9f49d054 100644 --- a/Readme.md +++ b/Readme.md @@ -205,7 +205,9 @@ Options are: default - runtime when runtime log is filled otherwise filesize -m, --multiply-processes [FLOAT] use given number as a multiplier of processes to run -s, --single [PATTERN] Run all matching files in the same process - -i, --isolate Do not run any other tests in the group used by --single(-s) + -i, --isolate Do not run any other tests in the group used by --single(-s). + Automatically turned on if --isolate-n is set above 0. + --isolate-n Number of processes for isolated groups. Default to 1 when --isolate is on. --only-group INT[, INT] -e, --exec [COMMAND] execute this code parallel and with ENV['TEST_ENV_NUMBER'] -o, --test-options '[OPTIONS]' execute test commands with those options diff --git a/lib/parallel_tests/cli.rb b/lib/parallel_tests/cli.rb index 089cd838..c9b0bca8 100644 --- a/lib/parallel_tests/cli.rb +++ b/lib/parallel_tests/cli.rb @@ -192,6 +192,12 @@ def parse_options!(argv) options[:isolate] = true end + opts.on("--isolate-n [PROCESSES]", + Integer, + "Use 'isolate' singles with number of processes, default: 1.") do |n| + options[:isolate_count] = n + end + opts.on("--only-group INT[, INT]", Array) { |groups| options[:only_group] = groups.map(&:to_i) } opts.on("-e", "--exec [COMMAND]", "execute this code parallel and with ENV['TEST_ENV_NUMBER']") { |path| options[:execute] = path } diff --git a/lib/parallel_tests/grouper.rb b/lib/parallel_tests/grouper.rb index 0db018a7..de0d1bab 100644 --- a/lib/parallel_tests/grouper.rb +++ b/lib/parallel_tests/grouper.rb @@ -15,19 +15,46 @@ def in_even_groups_by_size(items, num_groups, options= {}) groups = Array.new(num_groups) { {:items => [], :size => 0} } # add all files that should run in a single process to one group - (options[:single_process] || []).each do |pattern| - matched, items = items.partition { |item, _size| item =~ pattern } - matched.each { |item, size| add_to_group(groups.first, item, size) } + single_process_patterns = options[:single_process] || [] + + single_items, items = items.partition do |item, _size| + single_process_patterns.any? { |pattern| item =~ pattern } end - groups_to_fill = (options[:isolate] ? groups[1..-1] : groups) - group_features_by_size(items_to_group(items), groups_to_fill) + isolate_count = isolate_count(options) + + if isolate_count >= num_groups + raise 'Number of isolated processes must be less than total the number of processes' + end + + if isolate_count >= 1 + # add all files that should run in a multiple isolated processes to their own groups + group_features_by_size(items_to_group(single_items), groups[0..(isolate_count - 1)]) + # group the non-isolated by size + group_features_by_size(items_to_group(items), groups[isolate_count..-1]) + else + # add all files that should run in a single non-isolated process to first group + single_items.each { |item, size| add_to_group(groups.first, item, size) } + + # group all by size + group_features_by_size(items_to_group(items), groups) + end groups.map! { |g| g[:items].sort } end private + def isolate_count(options) + if options[:isolate_count] && options[:isolate_count] > 1 + options[:isolate_count] + elsif options[:isolate] + 1 + else + 0 + end + end + def largest_first(files) files.sort_by{|_item, size| size }.reverse end diff --git a/spec/parallel_tests/cli_spec.rb b/spec/parallel_tests/cli_spec.rb index 2eecd202..7be6f4c8 100644 --- a/spec/parallel_tests/cli_spec.rb +++ b/spec/parallel_tests/cli_spec.rb @@ -107,6 +107,26 @@ def call(*args) end end + context "single and isolate" do + it "single_process should be an array of patterns" do + expect(call(["test", "--single", '1'])).to eq(defaults.merge(single_process: [/1/])) + end + + it "single_process should be an array of patterns" do + expect(call(["test", "--single", '1', "--single", '2'])).to eq(defaults.merge(single_process: [/1/, /2/])) + end + + it "isolate should set isolate_count defaults" do + expect(call(["test", "--single", '1', "--isolate"])).to eq(defaults.merge(single_process: [/1/], isolate: true)) + end + + it "isolate_n should set isolate_count and turn on isolate" do + expect(call(["test", "-n", "3", "--single", '1', "--isolate-n", "2"])).to eq( + defaults.merge(count: 3, single_process: [/1/], isolate_count: 2) + ) + end + end + context "when the -- option separator is used" do it "interprets arguments as files/directories" do expect(call(%w(-- test))).to eq( files: %w(test)) diff --git a/spec/parallel_tests/grouper_spec.rb b/spec/parallel_tests/grouper_spec.rb index d8710d95..ee571a36 100644 --- a/spec/parallel_tests/grouper_spec.rb +++ b/spec/parallel_tests/grouper_spec.rb @@ -54,9 +54,22 @@ def call(num_groups, options={}) expect(call(2, :single_process => [/1|2|3|4/])).to eq([["1", "2", "3", "4"], ["5"]]) end + it "groups single items into specified isolation groups" do + expect(call(3, :single_process => [/1|2|3|4/], :isolate_count => 2)).to eq([["1", "4"], ["2", "3"], ["5"]]) + end + it "groups single items with others if there are too few" do expect(call(2, :single_process => [/1/])).to eq([["1", "3", "4"], ["2", "5"]]) end + + it "groups must abort when isolate_count is out of bounds" do + expect { + call(3, :single_process => [/1/], :isolate_count => 3) + }.to raise_error( + "Number of isolated processes must be less than total the number of processes" + ) + end + end describe '.by_scenarios' do diff --git a/spec/parallel_tests/test/runner_spec.rb b/spec/parallel_tests/test/runner_spec.rb index 90a6fcf6..34b1778e 100644 --- a/spec/parallel_tests/test/runner_spec.rb +++ b/spec/parallel_tests/test/runner_spec.rb @@ -146,6 +146,26 @@ def call(*args) expect(valid_combinations).to include(actual) end + + it "groups by size and use specified number of isolation groups" do + skip if RUBY_PLATFORM == "java" + expect(ParallelTests::Test::Runner).to receive(:runtimes). + and_return({"aaa1" => 1, "aaa2" => 3, "aaa3" => 2, "bbb" => 3, "ccc" => 1, "ddd" => 2}) + result = call(["aaa1", "aaa2", "aaa3", "bbb", "ccc", "ddd", "eee"], 4, isolate_count: 2, single_process: [/^aaa/], group_by: :runtime) + + isolated_1, isolated_2, *groups = result + expect(isolated_1).to eq(["aaa2"]) + expect(isolated_2).to eq(["aaa1", "aaa3"]) + actual = groups.map(&:to_set).to_set + + # both eee and ccs are the same size, so either can be in either group + valid_combinations = [ + [["bbb", "eee"], ["ccc", "ddd"]].map(&:to_set).to_set, + [["bbb", "ccc"], ["eee", "ddd"]].map(&:to_set).to_set + ] + + expect(valid_combinations).to include(actual) + end end end