parallel_tests 0.6.20 → 0.7.0.alpha

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. data/.gitignore +1 -0
  2. data/Gemfile +2 -4
  3. data/Gemfile.lock +7 -7
  4. data/Rakefile +18 -16
  5. data/Readme.md +15 -12
  6. data/bin/parallel_test +2 -98
  7. data/lib/parallel_tests.rb +2 -125
  8. data/lib/parallel_tests/cli.rb +102 -0
  9. data/lib/parallel_tests/cucumber/runner.rb +40 -0
  10. data/lib/parallel_tests/cucumber/runtime_logger.rb +58 -0
  11. data/lib/parallel_tests/grouper.rb +2 -2
  12. data/lib/parallel_tests/railtie.rb +3 -3
  13. data/lib/{parallel_specs/spec_failures_logger.rb → parallel_tests/spec/failures_logger.rb} +4 -3
  14. data/lib/{parallel_specs/spec_logger_base.rb → parallel_tests/spec/logger_base.rb} +7 -3
  15. data/lib/parallel_tests/spec/runner.rb +56 -0
  16. data/lib/{parallel_specs/spec_runtime_logger.rb → parallel_tests/spec/runtime_logger.rb} +2 -2
  17. data/lib/{parallel_specs/spec_summary_logger.rb → parallel_tests/spec/summary_logger.rb} +2 -2
  18. data/lib/parallel_tests/tasks.rb +0 -25
  19. data/lib/parallel_tests/test/runner.rb +126 -0
  20. data/lib/parallel_tests/test/runtime_logger.rb +92 -0
  21. data/lib/parallel_tests/version.rb +3 -0
  22. data/parallel_tests.gemspec +10 -61
  23. data/spec/parallel_tests/cucumber/runner_spec.rb +76 -0
  24. data/spec/{parallel_specs/spec_failure_logger_spec.rb → parallel_tests/spec/failure_logger_spec.rb} +8 -8
  25. data/spec/parallel_tests/spec/runner_spec.rb +178 -0
  26. data/spec/{parallel_specs/spec_runtime_logger_spec.rb → parallel_tests/spec/runtime_logger_spec.rb} +4 -4
  27. data/spec/{parallel_specs/spec_summary_logger_spec.rb → parallel_tests/spec/summary_logger_spec.rb} +2 -2
  28. data/spec/parallel_tests/test/runner_spec.rb +179 -0
  29. data/spec/parallel_tests/{runtime_logger_spec.rb → test/runtime_logger_spec.rb} +19 -16
  30. data/spec/parallel_tests_spec.rb +2 -158
  31. data/spec/spec_helper.rb +9 -7
  32. metadata +30 -26
  33. data/VERSION +0 -1
  34. data/lib/parallel_cucumber.rb +0 -36
  35. data/lib/parallel_cucumber/runtime_logger.rb +0 -57
  36. data/lib/parallel_specs.rb +0 -52
  37. data/lib/parallel_tests/runtime_logger.rb +0 -78
  38. data/lib/tasks/parallel_tests.rake +0 -1
  39. data/spec/parallel_cucumber_spec.rb +0 -72
  40. data/spec/parallel_specs_spec.rb +0 -173
@@ -0,0 +1 @@
1
+ *.sh
data/Gemfile CHANGED
@@ -1,11 +1,9 @@
1
1
  source :rubygems
2
+ gemspec
2
3
 
3
- gem 'parallel'
4
-
5
- group :dev do
4
+ group :development do
6
5
  gem 'test-unit', :platform => :ruby_19
7
6
  gem 'rspec', '>=2.4'
8
7
  gem 'cucumber'
9
8
  gem 'rake'
10
- gem 'jeweler'
11
9
  end
@@ -1,3 +1,9 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ parallel_tests (0.7.0.alpha)
5
+ parallel
6
+
1
7
  GEM
2
8
  remote: http://rubygems.org/
3
9
  specs:
@@ -11,11 +17,6 @@ GEM
11
17
  diff-lcs (1.1.2)
12
18
  gherkin (2.7.6)
13
19
  json (>= 1.4.6)
14
- git (1.2.5)
15
- jeweler (1.6.3)
16
- bundler (~> 1.0)
17
- git (>= 1.2.5)
18
- rake
19
20
  json (1.6.4)
20
21
  parallel (0.5.1)
21
22
  rake (0.8.7)
@@ -35,8 +36,7 @@ PLATFORMS
35
36
 
36
37
  DEPENDENCIES
37
38
  cucumber
38
- jeweler
39
- parallel
39
+ parallel_tests!
40
40
  rake
41
41
  rspec (>= 2.4)
42
42
  test-unit
data/Rakefile CHANGED
@@ -1,20 +1,22 @@
1
- task :default => :spec
2
- require "rspec/core/rake_task"
3
- RSpec::Core::RakeTask.new(:spec) do |t|
4
- t.rspec_opts = '--backtrace --color'
1
+ require 'bundler/gem_tasks'
2
+
3
+ task :default do
4
+ sh "rspec spec/"
5
5
  end
6
6
 
7
- begin
8
- require 'jeweler'
9
- Jeweler::Tasks.new do |gem|
10
- gem.name = "parallel_tests"
11
- gem.summary = "Run tests / specs / features in parallel"
12
- gem.email = "grosser.michael@gmail.com"
13
- gem.homepage = "http://github.com/grosser/#{gem.name}"
14
- gem.authors = "Michael Grosser"
15
- end
7
+ # extracted from https://github.com/grosser/project_template
8
+ rule /^version:bump:.*/ do |t|
9
+ sh "git status | grep 'nothing to commit'" # ensure we are not dirty
10
+ index = ['major', 'minor','patch'].index(t.name.split(':').last)
11
+ file = 'lib/GEM_NAME/version.rb'
12
+
13
+ version_file = File.read(file)
14
+ old_version, *version_parts = version_file.match(/(\d+)\.(\d+)\.(\d+)/).to_a
15
+ version_parts[index] = version_parts[index].to_i + 1
16
+ version_parts[2] = 0 if index < 2 # remove patch for minor
17
+ version_parts[1] = 0 if index < 1 # remove minor for major
18
+ new_version = version_parts * '.'
19
+ File.open(file,'w'){|f| f.write(version_file.sub(old_version, new_version)) }
16
20
 
17
- Jeweler::GemcutterTasks.new
18
- rescue LoadError
19
- puts "Jeweler, or one of its dependencies, is not available. Install it with: sudo gem install jeweler"
21
+ sh "bundle && git add #{file} Gemfile.lock && git commit -m 'bump version to #{new_version}'"
20
22
  end
data/Readme.md CHANGED
@@ -89,16 +89,16 @@ Even process runtimes
89
89
 
90
90
  Log test runtime to give each process the same runtime.
91
91
 
92
- Rspec: Add to your `spec/parallel_spec.opts` (or `spec/spec.opts`) :
92
+ Rspec: Add to your `.rspec_parallel` (or `.rspec`) :
93
93
 
94
94
  RSpec 1.x:
95
95
  --format progress
96
- --require parallel_specs/spec_runtime_logger
97
- --format ParallelSpecs::SpecRuntimeLogger:tmp/parallel_profile.log
96
+ --require parallel_tests/spec/runtime_logger
97
+ --format ParallelTests::Spec::RuntimeLogger:tmp/parallel_profile.log
98
98
  RSpec >= 2.4:
99
99
  If installed as plugin: -I vendor/plugins/parallel_tests/lib
100
100
  --format progress
101
- --format ParallelSpecs::SpecRuntimeLogger --out tmp/parallel_profile.log
101
+ --format ParallelTests::Spec::RuntimeLogger --out tmp/parallel_profile.log
102
102
 
103
103
  Test::Unit: Add to your `test_helper.rb`:
104
104
  require 'parallel_tests/runtime_logger'
@@ -109,16 +109,16 @@ SpecSummaryLogger
109
109
 
110
110
  This logger logs the test output without the different processes overwriting each other.
111
111
 
112
- Add the following to your `spec/parallel_spec.opts` (or `spec/spec.opts`) :
112
+ Add the following to your `.rspec_parallel` (or `.rspec`) :
113
113
 
114
114
  RSpec 1.x:
115
115
  --format progress
116
- --require parallel_specs/spec_summary_logger
117
- --format ParallelSpecs::SpecSummaryLogger:tmp/spec_summary.log
116
+ --require parallel_tests/spec/summary_logger
117
+ --format ParallelTests::Spec::SummaryLogger:tmp/spec_summary.log
118
118
  RSpec >= 2.2:
119
119
  If installed as plugin: -I vendor/plugins/parallel_tests/lib
120
120
  --format progress
121
- --format ParallelSpecs::SpecSummaryLogger --out tmp/spec_summary.log
121
+ --format ParallelTests::Spec::SummaryLogger --out tmp/spec_summary.log
122
122
 
123
123
  SpecFailuresLogger
124
124
  -----------------------
@@ -129,16 +129,16 @@ E.g.
129
129
 
130
130
  rspec /path/to/my_spec.rb:123 # should do something
131
131
 
132
- Add the following to your `spec/parallel_spec.opts` (or `spec/spec.opts`) :
132
+ Add the following to your `.rspec_parallel` (or `.rspec`) :
133
133
 
134
134
  RSpec 1.x:
135
135
  --format progress
136
- --require parallel_specs/spec_failures_logger
137
- --format ParallelSpecs::SpecFailuresLogger:tmp/failing_specs.log
136
+ --require parallel_tests/spec/failures_logger
137
+ --format ParallelTests::Spec::FailuresLogger:tmp/failing_specs.log
138
138
  RSpec >= 2.4:
139
139
  If installed as plugin: -I vendor/plugins/parallel_tests/lib
140
140
  --format progress
141
- --format ParallelSpecs::SpecFailuresLogger --out tmp/failing_specs.log
141
+ --format ParallelTests::Spec::FailuresLogger --out tmp/failing_specs.log
142
142
 
143
143
  Setup for non-rails
144
144
  ===================
@@ -200,6 +200,9 @@ TIPS
200
200
 
201
201
  TODO
202
202
  ====
203
+ - move everything Rails 2 related to a e.g. Rails2Readme.md and link it
204
+ - unify runtime-log location
205
+ - add tests for cucumber runtime formatter
203
206
  - make jRuby compatible [basics](http://yehudakatz.com/2009/07/01/new-rails-isolation-testing/)
204
207
  - make windows compatible
205
208
 
@@ -1,101 +1,5 @@
1
1
  #!/usr/bin/env ruby
2
- require 'rubygems'
3
- require 'optparse'
4
- require 'parallel'
5
- raise "please ' gem install parallel '" if Gem::Version.new(Parallel::VERSION) < Gem::Version.new('0.4.2')
6
2
  $LOAD_PATH << File.join(File.dirname(__FILE__), '..', 'lib')
7
3
  require "parallel_tests"
8
-
9
- options = {}
10
- OptionParser.new do |opts|
11
- opts.banner = <<BANNER
12
- Run all tests in parallel, giving each process ENV['TEST_ENV_NUMBER'] ('', '2', '3', ...)
13
-
14
- [optional] Only run selected files & folders:
15
- parallel_test test/bar test/baz/xxx_text.rb
16
-
17
- Options are:
18
- BANNER
19
- opts.on("-n [PROCESSES]", Integer, "How many processes to use, default: available CPUs"){|n| options[:count] = n }
20
- opts.on("-p", '--pattern [PATTERN]', "run tests matching this pattern"){|pattern| options[:pattern] = pattern }
21
- opts.on("--no-sort", "do not sort files before running them"){ |no_sort| options[:no_sort] = no_sort }
22
- opts.on("-m [FLOAT]", "--multiply-processes [FLOAT]", Float, "use given number as a multiplier of processes to run"){ |multiply| options[:multiply] = multiply }
23
- opts.on("-r", '--root [PATH]', "execute test commands from this path"){|path| options[:root] = path }
24
- opts.on("-s [PATTERN]", "--single [PATTERN]", "Run all matching files in only one process") do |pattern|
25
- options[:single_process] ||= []
26
- options[:single_process] << /#{pattern}/
27
- end
28
- opts.on("-e", '--exec [COMMAND]', "execute this code parallel and with ENV['TEST_ENV_NUM']"){|path| options[:execute] = path }
29
- opts.on("-o", "--test-options '[OPTIONS]'", "execute test commands with those options"){|arg| options[:test_options] = arg }
30
- opts.on("-t", "--type [TYPE]", "which type of tests to run? test, spec or features"){|type| options[:type] = type }
31
- opts.on("--non-parallel", "execute same commands but do not in parallel, needs --exec"){ options[:non_parallel] = true }
32
- opts.on("--chunk-timeout [TIMEOUT]", "timeout before re-printing the output of a child-process"){|timeout| options[:chunk_timeout] = timeout.to_f }
33
- opts.on('-v', '--version', 'Show Version'){ puts ParallelTests::VERSION; exit}
34
- opts.on("-h", "--help", "Show this.") { puts opts; exit }
35
- end.parse!
36
-
37
- raise "--no-sort and --single-process are not supported" if options[:no_sort] and options[:single_process]
38
-
39
- # get files to run from arguments
40
- options[:files] = ARGV if ARGV.size > 0
41
-
42
- num_processes = ParallelTests.determine_number_of_processes(options[:count])
43
- num_processes = num_processes * (options[:multiply] || 1)
44
-
45
- if options[:execute]
46
- runs = (0...num_processes).to_a
47
- results = if options[:non_parallel]
48
- runs.map do |i|
49
- ParallelTests.execute_command(options[:execute], i, options)
50
- end
51
- else
52
- Parallel.map(runs, :in_processes => num_processes) do |i|
53
- ParallelTests.execute_command(options[:execute], i, options)
54
- end
55
- end.flatten
56
- abort if results.any?{|r| r[:exit_status] != 0 }
57
- else
58
- lib, name, task = {
59
- 'test' => ["tests", "test", "test"],
60
- 'spec' => ["specs", "spec", "spec"],
61
- 'features' => ["cucumber", "feature", "features"]
62
- }[options[:type]||'test']
63
-
64
- require "parallel_#{lib}"
65
- klass = eval("Parallel#{lib.capitalize}")
66
-
67
- start = Time.now
68
-
69
- tests_folder = task
70
- tests_folder = File.join(options[:root], tests_folder) unless options[:root].to_s.empty?
71
-
72
- groups = klass.tests_in_groups(options[:files] || tests_folder, num_processes, options)
73
- num_processes = groups.size
74
-
75
- #adjust processes to groups
76
- abort "no #{name}s found!" if groups.size == 0
77
-
78
- num_tests = groups.inject(0){|sum,item| sum + item.size }
79
- puts "#{num_processes} processes for #{num_tests} #{name}s, ~ #{num_tests / groups.size} #{name}s per process"
80
-
81
- test_results = Parallel.map(groups, :in_processes => num_processes) do |group|
82
- if group.empty?
83
- {:stdout => '', :exit_status => 0}
84
- else
85
- klass.run_tests(group, groups.index(group), options)
86
- end
87
- end
88
-
89
- #parse and print results
90
- results = klass.find_results(test_results.map{|result| result[:stdout] }*"")
91
- puts ""
92
- puts klass.summarize_results(results)
93
-
94
- #report total time taken
95
- puts ""
96
- puts "Took #{Time.now - start} seconds"
97
-
98
- #exit with correct status code so rake parallel:test && echo 123 works
99
- failed = test_results.any?{|result| result[:exit_status] != 0 }
100
- abort "#{name.capitalize}s Failed" if failed
101
- end
4
+ require "parallel_tests/cli"
5
+ ParallelTest::CLI.run(ARGV)
@@ -1,10 +1,8 @@
1
1
  require 'parallel'
2
+ require 'parallel_tests/version'
2
3
  require 'parallel_tests/grouper'
3
- require 'parallel_tests/railtie'
4
-
5
- class ParallelTests
6
- VERSION = File.read( File.join(File.dirname(__FILE__),'..','VERSION') ).strip
7
4
 
5
+ module ParallelTests
8
6
  def self.determine_number_of_processes(count)
9
7
  [
10
8
  count,
@@ -29,88 +27,6 @@ class ParallelTests
29
27
  [num_processes, pattern.to_s, options.to_s]
30
28
  end
31
29
 
32
- # finds all tests and partitions them into groups
33
- def self.tests_in_groups(root, num_groups, options={})
34
- tests = find_tests(root, options)
35
- if options[:no_sort] == true
36
- Grouper.in_groups(tests, num_groups)
37
- else
38
- tests = with_runtime_info(tests)
39
- Grouper.in_even_groups_by_size(tests, num_groups, options)
40
- end
41
- end
42
-
43
- def self.run_tests(test_files, process_number, options)
44
- require_list = test_files.map { |filename| %{"#{File.expand_path filename}"} }.join(",")
45
- cmd = "ruby -Itest -e '[#{require_list}].each {|f| require f }' -- #{options[:test_options]}"
46
- execute_command(cmd, process_number, options)
47
- end
48
-
49
- def self.execute_command(cmd, process_number, options)
50
- cmd = "TEST_ENV_NUMBER=#{test_env_number(process_number)} ; export TEST_ENV_NUMBER; #{cmd}"
51
- f = open("|#{cmd}", 'r')
52
- output = fetch_output(f, options)
53
- f.close
54
- {:stdout => output, :exit_status => $?.exitstatus}
55
- end
56
-
57
- def self.find_results(test_output)
58
- test_output.split("\n").map {|line|
59
- line = line.gsub(/\.|F|\*/,'')
60
- next unless line_is_result?(line)
61
- line
62
- }.compact
63
- end
64
-
65
- def self.test_env_number(process_number)
66
- process_number == 0 ? '' : process_number + 1
67
- end
68
-
69
- def self.runtime_log
70
- 'tmp/parallel_runtime_test.log'
71
- end
72
-
73
- def self.summarize_results(results)
74
- results = results.join(' ').gsub(/s\b/,'') # combine and singularize results
75
- counts = results.scan(/(\d+) (\w+)/)
76
- sums = counts.inject(Hash.new(0)) do |sum, (number, word)|
77
- sum[word] += number.to_i
78
- sum
79
- end
80
- sums.sort.map{|word, number| "#{number} #{word}#{'s' if number != 1}" }.join(', ')
81
- end
82
-
83
- protected
84
-
85
- # read output of the process and print in in chucks
86
- def self.fetch_output(process, options)
87
- all = ''
88
- buffer = ''
89
- timeout = options[:chunk_timeout] || 0.2
90
- flushed = Time.now.to_f
91
-
92
- while char = process.getc
93
- char = (char.is_a?(Fixnum) ? char.chr : char) # 1.8 <-> 1.9
94
- all << char
95
-
96
- # print in chunks so large blocks stay together
97
- now = Time.now.to_f
98
- buffer << char
99
- if flushed + timeout < now
100
- $stdout.print buffer
101
- $stdout.flush
102
- buffer = ''
103
- flushed = now
104
- end
105
- end
106
-
107
- # print the remainder
108
- $stdout.print buffer
109
- $stdout.flush
110
-
111
- all
112
- end
113
-
114
30
  # copied from http://github.com/carlhuda/bundler Bundler::SharedHelpers#find_gemfile
115
31
  def self.bundler_enabled?
116
32
  return true if Object.const_defined?(:Bundler)
@@ -126,43 +42,4 @@ class ParallelTests
126
42
 
127
43
  false
128
44
  end
129
-
130
- def self.line_is_result?(line)
131
- line =~ /\d+ failure/
132
- end
133
-
134
- def self.test_suffix
135
- "_test.rb"
136
- end
137
-
138
- def self.with_runtime_info(tests)
139
- lines = File.read(runtime_log).split("\n") rescue []
140
-
141
- # use recorded test runtime if we got enough data
142
- if lines.size * 1.5 > tests.size
143
- puts "Using recorded test runtime"
144
- times = Hash.new(1)
145
- lines.each do |line|
146
- test, time = line.split(":")
147
- next unless test and time
148
- times[File.expand_path(test)] = time.to_f
149
- end
150
- tests.sort.map{|test| [test, times[test]] }
151
- else # use file sizes
152
- tests.sort.map{|test| [test, File.stat(test).size] }
153
- end
154
- end
155
-
156
- def self.find_tests(root, options={})
157
- if root.is_a?(Array)
158
- root
159
- else
160
- # follow one symlink and direct children
161
- # http://stackoverflow.com/questions/357754/can-i-traverse-symlinked-directories-in-ruby-with-a-glob
162
- files = Dir["#{root}/**{,/*/**}/*#{test_suffix}"].uniq
163
- files = files.map{|f| f.sub(root+'/','') }
164
- files = files.grep(/#{options[:pattern]}/)
165
- files.map{|f| "#{root}/#{f}" }
166
- end
167
- end
168
45
  end
@@ -0,0 +1,102 @@
1
+ require 'optparse'
2
+ require 'parallel_tests/test/runner'
3
+
4
+ module ParallelTest
5
+ module CLI
6
+ def self.run(argv)
7
+ options = {}
8
+ OptionParser.new do |opts|
9
+ opts.banner = <<BANNER
10
+ Run all tests in parallel, giving each process ENV['TEST_ENV_NUMBER'] ('', '2', '3', ...)
11
+
12
+ [optional] Only run selected files & folders:
13
+ parallel_test test/bar test/baz/xxx_text.rb
14
+
15
+ Options are:
16
+ BANNER
17
+ opts.on("-n [PROCESSES]", Integer, "How many processes to use, default: available CPUs") { |n| options[:count] = n }
18
+ opts.on("-p", '--pattern [PATTERN]', "run tests matching this pattern") { |pattern| options[:pattern] = pattern }
19
+ opts.on("--no-sort", "do not sort files before running them") { |no_sort| options[:no_sort] = no_sort }
20
+ opts.on("-m [FLOAT]", "--multiply-processes [FLOAT]", Float, "use given number as a multiplier of processes to run") { |multiply| options[:multiply] = multiply }
21
+ opts.on("-r", '--root [PATH]', "execute test commands from this path") { |path| options[:root] = path }
22
+ opts.on("-s [PATTERN]", "--single [PATTERN]", "Run all matching files in only one process") do |pattern|
23
+ options[:single_process] ||= []
24
+ options[:single_process] << /#{pattern}/
25
+ end
26
+ opts.on("-e", '--exec [COMMAND]', "execute this code parallel and with ENV['TEST_ENV_NUM']") { |path| options[:execute] = path }
27
+ opts.on("-o", "--test-options '[OPTIONS]'", "execute test commands with those options") { |arg| options[:test_options] = arg }
28
+ opts.on("-t", "--type [TYPE]", "which type of tests to run? test, spec or features") { |type| options[:type] = type }
29
+ opts.on("--non-parallel", "execute same commands but do not in parallel, needs --exec") { options[:non_parallel] = true }
30
+ opts.on("--chunk-timeout [TIMEOUT]", "timeout before re-printing the output of a child-process") { |timeout| options[:chunk_timeout] = timeout.to_f }
31
+ opts.on('-v', '--version', 'Show Version') { puts ParallelTests::VERSION; exit }
32
+ opts.on("-h", "--help", "Show this.") { puts opts; exit }
33
+ end.parse!(argv)
34
+
35
+ raise "--no-sort and --single-process are not supported" if options[:no_sort] and options[:single_process]
36
+
37
+ # get files to run from arguments
38
+ options[:files] = argv if argv.size > 0
39
+
40
+ num_processes = ParallelTests.determine_number_of_processes(options[:count])
41
+ num_processes = num_processes * (options[:multiply] || 1)
42
+
43
+ if options[:execute]
44
+ runs = (0...num_processes).to_a
45
+ results = if options[:non_parallel]
46
+ runs.map do |i|
47
+ ParallelTests::Test::Runner.execute_command(options[:execute], i, options)
48
+ end
49
+ else
50
+ Parallel.map(runs, :in_processes => num_processes) do |i|
51
+ ParallelTests::Test::Runner.execute_command(options[:execute], i, options)
52
+ end
53
+ end.flatten
54
+ abort if results.any? { |r| r[:exit_status] != 0 }
55
+ else
56
+ lib, name, task = {
57
+ 'test' => ["test", "test", "test"],
58
+ 'spec' => ["spec", "spec", "spec"],
59
+ 'features' => ["cucumber", "feature", "features"]
60
+ }[options[:type]||'test']
61
+
62
+ require "parallel_tests/#{lib}/runner"
63
+ klass = eval("ParallelTests::#{lib.capitalize}::Runner")
64
+
65
+ start = Time.now
66
+
67
+ tests_folder = task
68
+ tests_folder = File.join(options[:root], tests_folder) unless options[:root].to_s.empty?
69
+
70
+ groups = klass.tests_in_groups(options[:files] || tests_folder, num_processes, options)
71
+ num_processes = groups.size
72
+
73
+ #adjust processes to groups
74
+ abort "no #{name}s found!" if groups.size == 0
75
+
76
+ num_tests = groups.inject(0) { |sum, item| sum + item.size }
77
+ puts "#{num_processes} processes for #{num_tests} #{name}s, ~ #{num_tests / groups.size} #{name}s per process"
78
+
79
+ test_results = Parallel.map(groups, :in_processes => num_processes) do |group|
80
+ if group.empty?
81
+ {:stdout => '', :exit_status => 0}
82
+ else
83
+ klass.run_tests(group, groups.index(group), options)
84
+ end
85
+ end
86
+
87
+ #parse and print results
88
+ results = klass.find_results(test_results.map { |result| result[:stdout] }*"")
89
+ puts ""
90
+ puts klass.summarize_results(results)
91
+
92
+ #report total time taken
93
+ puts ""
94
+ puts "Took #{Time.now - start} seconds"
95
+
96
+ #exit with correct status code so rake parallel:test && echo 123 works
97
+ failed = test_results.any? { |result| result[:exit_status] != 0 }
98
+ abort "#{name.capitalize}s Failed" if failed
99
+ end
100
+ end
101
+ end
102
+ end