Permalink
Browse files

Merge branch 'group-by-steps'

  • Loading branch information...
2 parents 6e3a433 + 8f2fd31 commit 854c8cef67516e54e8ef84eb1d4468176fbc960b @grosser committed Apr 29, 2012
View
@@ -119,15 +119,19 @@ Setup for non-rails
parallel_test test/bar test/baz/foo_text.rb
Options are:
-
-n [PROCESSES] How many processes to use, default: available CPUs
- -p, --path [PATH] run tests inside this path only
- --no-sort do not sort files before running them
+ -p, --pattern [PATTERN] run tests matching this pattern
+ --group-by group tests by:
+ found - order of finding files
+ steps - number of cucumber steps
+ default - runtime or filesize
-m, --multiply-processes [FLOAT] use given number as a multiplier of processes to run
+ -s, --single [PATTERN] Run all matching files in only one process
-e, --exec [COMMAND] execute this code parallel and with ENV['TEST_ENV_NUM']
-o, --test-options '[OPTIONS]' execute test commands with those options
- -t, --type [TYPE] test(default) / spec / cucumber
+ -t, --type [TYPE] test(default) / rspec / cucumber
--non-parallel execute same commands but do not in parallel, needs --exec
+ --chunk-timeout [TIMEOUT] timeout before re-printing the output of a child-process
-v, --version Show Version
-h, --help Show this.
View
@@ -2,4 +2,5 @@
$LOAD_PATH << File.join(File.dirname(__FILE__), '..', 'lib')
require "parallel_tests"
require "parallel_tests/cli"
+
ParallelTest::CLI.run(ARGV)
View
@@ -81,7 +81,13 @@ def self.parse_options!(argv)
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 pattern") { |pattern| options[:pattern] = /#{pattern}/ }
- opts.on("--no-sort", "do not sort files before running them") { |no_sort| options[:no_sort] = no_sort }
+ opts.on("--group-by [TYPE]", <<-TEXT
+group tests by:
+ found - order of finding files
+ steps - number of cucumber steps
+ default - runtime or filesize
+TEXT
+) { |type| options[:group_by] = type.to_sym }
opts.on("-m [FLOAT]", "--multiply-processes [FLOAT]", Float, "use given number as a multiplier of processes to run") { |multiply| options[:multiply] = multiply }
opts.on("-s [PATTERN]", "--single [PATTERN]", "Run all matching files in only one process") do |pattern|
options[:single_process] ||= []
@@ -96,7 +102,7 @@ def self.parse_options!(argv)
opts.on("-h", "--help", "Show this.") { puts opts; exit }
end.parse!(argv)
- raise "--no-sort and --single-process are not supported" if options[:no_sort] and options[:single_process]
+ raise "--group-by found and --single-process are not supported" if options[:group_by] == :found and options[:single_process]
options[:files] = argv
options
@@ -0,0 +1,60 @@
+require 'gherkin'
+
+module ParallelTests
+ module Cucumber
+ class GherkinListener
+ attr_reader :collect
+
+ def initialize
+ @steps, @uris = [], []
+ @collect = {}
+ reset_counters!
+ end
+
+ def background(*args)
+ @background = 1
+ end
+
+ def scenario(*args)
+ @scenarios += 1
+ @outline = @background = 0
+ end
+
+ def scenario_outline(*args)
+ @outline = 1
+ end
+
+ def step(*args)
+ if @background == 1
+ @background_steps += 1
+ elsif @outline > 0
+ @outline_steps += 1
+ else
+ @collect[@uri] += 1
+ end
+ end
+
+ def uri(path)
+ @uri = path
+ @collect[@uri] = 0
+ end
+
+ def examples(*args)
+ @examples += 1
+ end
+
+ def eof(*args)
+ @collect[@uri] += (@background_steps * @scenarios) + (@outline_steps * @examples)
+ reset_counters!
+ end
+
+ def reset_counters!
+ @examples = @outline = @outline_steps = @background = @background_steps = @scenarios = 0
+ end
+
+ # ignore lots of other possible callbacks ...
+ def method_missing(*args)
+ end
+ end
+ end
+end
@@ -49,6 +49,14 @@ def self.cucumber_opts
"--profile parallel"
end
end
+
+ def self.tests_in_groups(tests, num_groups, options={})
+ if options[:group_by] == :steps
+ Grouper.by_steps(find_tests(tests, options), num_groups)
+ else
+ super
+ end
+ end
end
end
end
@@ -5,7 +5,9 @@ def self.in_groups(items, num_groups)
until items.empty?
num_groups.times do |group_number|
- groups[group_number] << items.shift
+ if item = items.shift
+ groups[group_number] << item
+ end
end
end
@@ -45,5 +47,20 @@ def self.add_to_group(group, item, size)
group[:items] << item
group[:size] += size
end
+
+ def self.by_steps(tests, num_groups)
+ features_with_steps = build_features_with_steps(tests)
+ in_even_groups_by_size(features_with_steps, num_groups)
+ end
+
+ def self.build_features_with_steps(tests)
+ require 'parallel_tests/cucumber/gherkin_listener'
+ listener = Cucumber::GherkinListener.new
+ parser = Gherkin::Parser::Parser.new(listener, true, 'root')
+ tests.each{|file|
+ parser.parse(File.read(file), file, 0)
+ }
+ listener.collect.sort_by{|_,value| -value }
+ end
end
end
@@ -31,7 +31,7 @@ def self.line_is_result?(line)
def self.tests_in_groups(tests, num_groups, options={})
tests = find_tests(tests, options)
- if options[:no_sort] == true
+ if options[:group_by] == :found
Grouper.in_groups(tests, num_groups)
else
tests = with_runtime_info(tests)
View
@@ -108,6 +108,12 @@ def run_tests(test_folder, options={})
`#{bin_folder}/parallel_cucumber -v`.should == version
end
+ it "runs with --group-by found" do
+ # it only tests that it does not blow up, as it did before fixing...
+ write "spec/x1_spec.rb", "puts '111'"
+ run_tests "spec", :type => 'rspec', :add => '--group-by found'
+ end
+
it "runs faster with more processes" do
2.times{|i|
write "spec/xxx#{i}_spec.rb", 'describe("it"){it("should"){sleep 5}}; $stderr.puts ENV["TEST_ENV_NUMBER"]'
@@ -0,0 +1,48 @@
+require 'parallel_tests/cucumber/gherkin_listener'
+
+describe ParallelTests::Cucumber::GherkinListener do
+ describe :collect do
+ before(:each) do
+ @listener = ParallelTests::Cucumber::GherkinListener.new
+ @listener.uri("feature_file")
+ end
+
+ it "returns steps count" do
+ 3.times {@listener.step(nil)}
+ @listener.collect.should == {"feature_file" => 3}
+ end
+
+ it "counts background steps separately" do
+ @listener.background("background")
+ 5.times {@listener.step(nil)}
+ @listener.collect.should == {"feature_file" => 0}
+
+ @listener.scenario("scenario")
+ 2.times {@listener.step(nil)}
+ @listener.collect.should == {"feature_file" => 2}
+
+ @listener.scenario("scenario")
+ @listener.collect.should == {"feature_file" => 2}
+
+ @listener.eof
+ @listener.collect.should == {"feature_file" => 12}
+ end
+
+ it "counts scenario outlines steps separately" do
+ @listener.scenario_outline("outline")
+ 5.times {@listener.step(nil)}
+ @listener.collect.should == {"feature_file" => 0}
+
+ @listener.scenario("scenario")
+ 2.times {@listener.step(nil)}
+ @listener.collect.should == {"feature_file" => 2}
+
+ @listener.scenario("scenario")
+ @listener.collect.should == {"feature_file" => 2}
+
+ 3.times {@listener.examples}
+ @listener.eof
+ @listener.collect.should == {"feature_file" => 17}
+ end
+ end
+end
@@ -0,0 +1,61 @@
+require 'parallel_tests/grouper'
+require 'tmpdir'
+
+describe ParallelTests::Grouper do
+ describe :by_steps do
+ def write(file, content)
+ File.open(file,'w'){|f| f.write content }
+ end
+
+ it "sorts features by steps" do
+ tmpdir = nil
+ result = Dir.mktmpdir do |dir|
+ tmpdir = dir
+ write("#{dir}/a.feature", "Feature: xxx\n Scenario: xxx\n Given something")
+ write("#{dir}/b.feature", "Feature: xxx\n Scenario: xxx\n Given something\n Scenario: yyy\n Given something")
+ write("#{dir}/c.feature", "Feature: xxx\n Scenario: xxx\n Given something")
+ ParallelTests::Grouper.by_steps(["#{dir}/a.feature", "#{dir}/b.feature", "#{dir}/c.feature"],2)
+ end
+
+ # testing inside mktmpdir is always green
+ result.should =~ [
+ ["#{tmpdir}/a.feature", "#{tmpdir}/c.feature"],
+ ["#{tmpdir}/b.feature"]
+ ]
+ end
+ end
+
+ describe :in_even_groups_by_size do
+ let(:files_with_size){ {"1" => 1, "2" => 2, "3" => 3, "4" => 4, "5" => 5} }
+
+ def call(num_groups)
+ ParallelTests::Grouper.in_even_groups_by_size(files_with_size, num_groups)
+ end
+
+ it "groups 1 by 1 for same groups as size" do
+ call(5).should == [["5"], ["4"], ["3"], ["2"], ["1"]]
+ end
+
+ it "groups into even groups" do
+ call(2).should == [["1", "2", "5"], ["3", "4"]]
+ end
+
+ it "groups into a single group" do
+ call(1).should == [["1", "2", "3", "4", "5"]]
+ end
+
+ it "adds empty groups if there are more groups than feature files" do
+ call(6).should == [["5"], ["4"], ["3"], ["2"], ["1"], []]
+ end
+ end
+
+ describe :in_groups do
+ it "groups" do
+ ParallelTests::Grouper.in_groups([1,2,3],2).should == [[1,3],[2]]
+ end
+
+ it "keeps groups sorted" do
+ ParallelTests::Grouper.in_groups([3,2,1],2).should == [[1,3],[2]]
+ end
+ end
+end
@@ -38,7 +38,7 @@ def call(*args)
it "does not sort when passed false do_sort option" do
ParallelTests::Test::Runner.should_not_receive(:smallest_first)
- call [], 1, :no_sort => true
+ call [], 1, :group_by => :found
end
it "does sort when not passed do_sort option" do
View
@@ -135,15 +135,15 @@ def setup_runtime_log
it "partitions by round-robin when not sorting" do
files = ["file1.rb", "file2.rb", "file3.rb", "file4.rb"]
klass.should_receive(:find_tests).and_return(files)
- groups = klass.tests_in_groups(files, 2, :no_sort => true)
+ groups = klass.tests_in_groups(files, 2, :group_by => :found)
groups[0].should == ["file1.rb", "file3.rb"]
groups[1].should == ["file2.rb", "file4.rb"]
end
it "alpha-sorts partitions when not sorting by runtime" do
files = %w[q w e r t y u i o p a s d f g h j k l z x c v b n m]
klass.should_receive(:find_tests).and_return(files)
- groups = klass.tests_in_groups(files, 2, :no_sort => true)
+ groups = klass.tests_in_groups(files, 2, :group_by => :found)
groups[0].should == groups[0].sort
groups[1].should == groups[1].sort
end

0 comments on commit 854c8ce

Please sign in to comment.