parallel_tests 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +1 -0
- data/README.markdown +119 -0
- data/Rakefile +19 -0
- data/VERSION +1 -0
- data/bin/parallel_cucumber +2 -0
- data/bin/parallel_spec +2 -0
- data/bin/parallel_test +74 -0
- data/lib/parallel_cucumber.rb +33 -0
- data/lib/parallel_specs.rb +27 -0
- data/lib/parallel_specs/spec_runtime_logger.rb +49 -0
- data/lib/parallel_tests.rb +119 -0
- data/parallel_tests.gemspec +61 -0
- data/spec/integration_spec.rb +83 -0
- data/spec/parallel_cucumber_spec.rb +101 -0
- data/spec/parallel_specs_spec.rb +134 -0
- data/spec/parallel_tests_spec.rb +130 -0
- data/spec/spec_helper.rb +78 -0
- data/tasks/parallel_specs.rake +56 -0
- metadata +85 -0
data/.gitignore
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
*.sh
|
data/README.markdown
ADDED
@@ -0,0 +1,119 @@
|
|
1
|
+
Speedup Test::Unit + RSpec + Cucumber by running parallel on multiple CPUs.
|
2
|
+
|
3
|
+
Setup for Rails
|
4
|
+
===============
|
5
|
+
|
6
|
+
sudo gem install parallel
|
7
|
+
script/plugin install git://github.com/grosser/parallel_tests.git
|
8
|
+
|
9
|
+
### 1: Add to `config/database.yml`
|
10
|
+
test:
|
11
|
+
database: xxx_test<%= ENV['TEST_ENV_NUMBER'] %>
|
12
|
+
|
13
|
+
### 2: Create additional database(s)
|
14
|
+
script/db_console
|
15
|
+
create database xxx_test2;
|
16
|
+
...
|
17
|
+
|
18
|
+
### 3: Copy development schema (repeat after migrations)
|
19
|
+
rake parallel:prepare
|
20
|
+
|
21
|
+
### 4: Run!
|
22
|
+
rake parallel:test # Test::Unit
|
23
|
+
rake parallel:spec # RSpec
|
24
|
+
rake parallel:features # Cucumber
|
25
|
+
|
26
|
+
rake parallel:test[1] --> force 1 CPU --> 86 seconds
|
27
|
+
rake parallel:test --> got 2 CPUs? --> 47 seconds
|
28
|
+
rake parallel:test --> got 4 CPUs? --> 26 seconds
|
29
|
+
...
|
30
|
+
|
31
|
+
Test just a subfolder (e.g. use one integration server per subfolder)
|
32
|
+
rake parallel:test[models]
|
33
|
+
rake parallel:test[something/else]
|
34
|
+
|
35
|
+
partial paths are OK too...
|
36
|
+
rake parallel:test[functional] == rake parallel:test[fun]
|
37
|
+
|
38
|
+
Example output
|
39
|
+
--------------
|
40
|
+
2 processes for 210 specs, ~ 105 specs per process
|
41
|
+
... test output ...
|
42
|
+
|
43
|
+
Results:
|
44
|
+
877 examples, 0 failures, 11 pending
|
45
|
+
843 examples, 0 failures, 1 pending
|
46
|
+
|
47
|
+
Took 29.925333 seconds
|
48
|
+
|
49
|
+
Even process runtimes (for specs only atm)
|
50
|
+
-----------------
|
51
|
+
Add to your `spec/parallel_specs.opts` (or `spec/spec.opts`) :
|
52
|
+
--format ParallelSpecs::SpecRuntimeLogger:tmp/parallel_profile.log
|
53
|
+
It will log test runtime and partition the test-load accordingly.
|
54
|
+
|
55
|
+
Setup for non-rails
|
56
|
+
===================
|
57
|
+
sudo gem install parallel_tests
|
58
|
+
# go to your project dir
|
59
|
+
parallel_test OR parallel_spec OR parallel_cucumber
|
60
|
+
# [Optional] use ENV['TEST_ENV_NUMBER'] inside your tests for separate db/resources/etc.
|
61
|
+
|
62
|
+
Options are:
|
63
|
+
-n [PROCESSES] How many processes to use, default: available CPUs
|
64
|
+
-p, --path [PATH] run tests inside this path only
|
65
|
+
-r, --root [PATH] execute test commands from this path
|
66
|
+
-e, --exec [COMMAND] execute this code parallel and with ENV['TEST_ENV_NUM']
|
67
|
+
-o, --test-options [SOMETHING] execute test commands with those options
|
68
|
+
-t, --type [TYPE] which type of tests to run? test, spec or features
|
69
|
+
-v, --version Show Version
|
70
|
+
-h, --help Show this.
|
71
|
+
|
72
|
+
You can run any kind of code with -e / --execute
|
73
|
+
parallel_test -n 5 -e 'ruby -e "puts %[hello from process #{ENV[:TEST_ENV_NUMBER.to_s].inspect}]"'
|
74
|
+
hello from process "2"
|
75
|
+
hello from process ""
|
76
|
+
hello from process "3"
|
77
|
+
hello from process "5"
|
78
|
+
hello from process "4"
|
79
|
+
|
80
|
+
<table>
|
81
|
+
<tr><td></td><td>1 Process</td><td>2 Processes</td><td>4 Processes</td></tr>
|
82
|
+
<tr><td>RSpec spec-suite</td><td>18</td><td>14</td><td>10</td></tr>
|
83
|
+
<tr><td>Rails-ActionPack</td><td>88</td><td>53</td><td>44</td></tr>
|
84
|
+
</table>
|
85
|
+
|
86
|
+
TIPS
|
87
|
+
====
|
88
|
+
- [RSpec] add a `spec/parallel_spec.opts` to use different options, e.g. no --drb (default: `spec/spec.opts`)
|
89
|
+
- [RSpec] if something looks fishy try to delete `script/spec`
|
90
|
+
- [RSpec] if `script/spec` is missing parallel:spec uses just `spec` (which solves some issues with double-loaded environment.rb)
|
91
|
+
- [RSpec] 'script/spec_server' or [spork](http://github.com/timcharper/spork/tree/master) do not work in parallel
|
92
|
+
- [RSpec] `./script/generate rspec` if you are running rspec from gems (this plugin uses script/spec which may fail if rspec files are outdated)
|
93
|
+
- [Bundler] if you have a `.bundle/environment.rb` then `bundle exec xxx` will be used to run tests
|
94
|
+
- with zsh this would be `rake "parallel:prepare[3]"`
|
95
|
+
|
96
|
+
TODO
|
97
|
+
====
|
98
|
+
- build parallel:bootstrap [idea/basics](http://github.com/garnierjm/parallel_specs/commit/dd8005a2639923dc5adc6400551c4dd4de82bf9a)
|
99
|
+
- make jRuby compatible [basics](http://yehudakatz.com/2009/07/01/new-rails-isolation-testing/)
|
100
|
+
- make windows compatible (does anyone care ?)
|
101
|
+
|
102
|
+
Authors
|
103
|
+
====
|
104
|
+
inspired by [pivotal labs](http://pivotallabs.com/users/miked/blog/articles/849-parallelize-your-rspec-suite)
|
105
|
+
|
106
|
+
###Contributors (alphabetical)
|
107
|
+
- [Charles Finkel](http://charlesfinkel.com/)
|
108
|
+
- [Jason Morrison](http://jayunit.net)
|
109
|
+
- [Joakim Kolsjö](http://www.rubyblocks.se)
|
110
|
+
- [Kpumuk](http://kpumuk.info/)
|
111
|
+
- [Maksim Horbu](http://github.com/mhorbul)
|
112
|
+
- [Rohan Deshpande](http://github.com/rdeshpande)
|
113
|
+
- [Tchandy](http://thiagopradi.net/)
|
114
|
+
- [Terence Lee](http://hone.heroku.com/)
|
115
|
+
- [Will Bryant](http://willbryant.net/)
|
116
|
+
|
117
|
+
[Michael Grosser](http://pragmatig.wordpress.com)
|
118
|
+
grosser.michael@gmail.com
|
119
|
+
Hereby placed under public domain, do what you want, just do not hold me accountable...
|
data/Rakefile
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
task :default => :spec
|
2
|
+
require 'spec/rake/spectask'
|
3
|
+
Spec::Rake::SpecTask.new {|t| t.spec_opts = ['--color']}
|
4
|
+
|
5
|
+
begin
|
6
|
+
require 'jeweler'
|
7
|
+
project_name = 'parallel_tests'
|
8
|
+
Jeweler::Tasks.new do |gem|
|
9
|
+
gem.name = project_name
|
10
|
+
gem.summary = "Run tests / specs / features in parallel"
|
11
|
+
gem.email = "grosser.michael@gmail.com"
|
12
|
+
gem.homepage = "http://github.com/grosser/#{project_name}"
|
13
|
+
gem.authors = ["Michael Grosser"]
|
14
|
+
end
|
15
|
+
|
16
|
+
Jeweler::GemcutterTasks.new
|
17
|
+
rescue LoadError
|
18
|
+
puts "Jeweler, or one of its dependencies, is not available. Install it with: sudo gem install jeweler"
|
19
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.3.0
|
data/bin/parallel_spec
ADDED
data/bin/parallel_test
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'rubygems'
|
3
|
+
require 'optparse'
|
4
|
+
lib_folder = File.join(File.dirname(__FILE__), '..', 'lib')
|
5
|
+
require File.join(lib_folder, "parallel_tests")
|
6
|
+
|
7
|
+
options = {}
|
8
|
+
OptionParser.new do |opts|
|
9
|
+
opts.banner = <<BANNER
|
10
|
+
Run tests in parallel, giving each process ENV['TEST_ENV_NUMBER'] ('', '2', '3', ...)
|
11
|
+
|
12
|
+
Options are:
|
13
|
+
BANNER
|
14
|
+
opts.on("-n [PROCESSES]", Integer, "How many processes to use, default: available CPUs"){|n| options[:count] = n }
|
15
|
+
opts.on("-p", '--path [PATH]', "run tests inside this path only"){|path| options[:path_prefix] = path }
|
16
|
+
opts.on("-r", '--root [PATH]', "execute test commands from this path"){|path| options[:root] = path }
|
17
|
+
opts.on("-e", '--exec [COMMAND]', "execute this code parallel and with ENV['TEST_ENV_NUM']"){|path| options[:execute] = path }
|
18
|
+
opts.on("-o", '--test-options [SOMETHING]', "execute test commands with those options"){|arg| options[:test_options] = arg }
|
19
|
+
opts.on("-t", "--type [TYPE]", "which type of tests to run? test, spec or features"){|type| options[:type] = type }
|
20
|
+
opts.on('-v', '--version', 'Show Version'){ puts ParallelTests::VERSION; exit}
|
21
|
+
opts.on("-h", "--help", "Show this.") { puts opts; exit }
|
22
|
+
end.parse!
|
23
|
+
|
24
|
+
require 'parallel'
|
25
|
+
num_processes = options[:count] || Parallel.processor_count
|
26
|
+
|
27
|
+
if options[:execute]
|
28
|
+
require File.join(lib_folder, "parallel_tests")
|
29
|
+
Parallel.in_processes(num_processes) do |i|
|
30
|
+
ParallelTests.execute_command(options[:execute], i)
|
31
|
+
end
|
32
|
+
else
|
33
|
+
lib, name, task = {
|
34
|
+
'test' => ["tests", "test", "test"],
|
35
|
+
'spec' => ["specs", "spec", "spec"],
|
36
|
+
'features' => ["cucumber", "feature", "features"]
|
37
|
+
}[options[:type]||'test']
|
38
|
+
|
39
|
+
require File.join(lib_folder, "parallel_#{lib}")
|
40
|
+
klass = eval("Parallel#{lib.capitalize}")
|
41
|
+
|
42
|
+
start = Time.now
|
43
|
+
|
44
|
+
tests_folder = File.join(task, options[:path_prefix].to_s)
|
45
|
+
tests_folder = File.join(options[:root], tests_folder) unless options[:root].to_s.empty?
|
46
|
+
|
47
|
+
groups = klass.tests_in_groups(tests_folder, num_processes)
|
48
|
+
num_processes = groups.size
|
49
|
+
|
50
|
+
#adjust processes to groups
|
51
|
+
abort "no #{name}s found!" if groups.size == 0
|
52
|
+
|
53
|
+
num_tests = groups.inject(0){|sum,item| sum + item.size }
|
54
|
+
puts "#{num_processes} processes for #{num_tests} #{name}s, ~ #{num_tests / groups.size} #{name}s per process"
|
55
|
+
|
56
|
+
output = Parallel.map(groups, :in_processes => num_processes) do |group|
|
57
|
+
klass.run_tests(group, groups.index(group), options[:test_options])
|
58
|
+
end
|
59
|
+
|
60
|
+
#parse and print results
|
61
|
+
results = klass.find_results(output*"")
|
62
|
+
puts ""
|
63
|
+
puts "Results:"
|
64
|
+
results.each{|r| puts r}
|
65
|
+
|
66
|
+
#report total time taken
|
67
|
+
puts ""
|
68
|
+
puts "Took #{Time.now - start} seconds"
|
69
|
+
|
70
|
+
#exit with correct status code
|
71
|
+
# - rake parallel:test && echo 123 ==> 123 should not show up when test failed
|
72
|
+
# - rake parallel:test db:reset ==> works when tests succeed
|
73
|
+
abort "#{name.capitalize}s Failed" if klass.failed?(results)
|
74
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'parallel_tests')
|
2
|
+
|
3
|
+
class ParallelCucumber < ParallelTests
|
4
|
+
def self.run_tests(test_files, process_number, options)
|
5
|
+
color = ($stdout.tty? ? 'export AUTOTEST=1 ;' : '')#display color when we are in a terminal
|
6
|
+
cmd = "export RAILS_ENV=test ; #{color} #{executable} #{options} #{test_files*' '}"
|
7
|
+
execute_command(cmd, process_number)
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.executable
|
11
|
+
if File.file?(".bundle/environment.rb")
|
12
|
+
"bundle exec cucumber"
|
13
|
+
elsif File.file?("script/cucumber")
|
14
|
+
"script/cucumber"
|
15
|
+
else
|
16
|
+
"cucumber"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
protected
|
21
|
+
|
22
|
+
def self.line_is_result?(line)
|
23
|
+
line =~ /^\d+ (steps|scenarios)/
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.line_is_failure?(line)
|
27
|
+
line =~ /^\d+ (steps|scenarios).*(\d{2,}|[1-9]) failed/
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.find_tests(root)
|
31
|
+
Dir["#{root}**/**/*.feature"]
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'parallel_tests')
|
2
|
+
|
3
|
+
class ParallelSpecs < ParallelTests
|
4
|
+
def self.run_tests(test_files, process_number, options)
|
5
|
+
spec_opts = ['spec/parallel_spec.opts', 'spec/spec.opts'].detect{|f| File.file?(f) }
|
6
|
+
spec_opts = (spec_opts ? "-O #{spec_opts}" : nil)
|
7
|
+
color = ($stdout.tty? ? 'export RSPEC_COLOR=1 ;' : '')#display color when we are in a terminal
|
8
|
+
cmd = "export RAILS_ENV=test ; #{color} #{executable} #{options} #{spec_opts} #{test_files*' '}"
|
9
|
+
execute_command(cmd, process_number)
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.executable
|
13
|
+
if File.file?(".bundle/environment.rb")
|
14
|
+
"bundle exec spec"
|
15
|
+
elsif File.file?("script/spec")
|
16
|
+
"script/spec"
|
17
|
+
else
|
18
|
+
"spec"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
protected
|
23
|
+
|
24
|
+
def self.find_tests(root)
|
25
|
+
Dir["#{root}**/**/*_spec.rb"]
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'spec/runner/formatter/progress_bar_formatter'
|
2
|
+
|
3
|
+
class ParallelSpecs::SpecRuntimeLogger < Spec::Runner::Formatter::BaseTextFormatter
|
4
|
+
def initialize(options, output)
|
5
|
+
if String === output
|
6
|
+
FileUtils.mkdir_p(File.dirname(output))
|
7
|
+
File.open(output,'w'){|f| f.write ''} # clean the file
|
8
|
+
@output = File.open(output, 'a+') #append so that multiple processes can write at once
|
9
|
+
else
|
10
|
+
@output = output
|
11
|
+
end
|
12
|
+
@example_times = Hash.new(0)
|
13
|
+
end
|
14
|
+
|
15
|
+
def example_started(*args)
|
16
|
+
@time = Time.now
|
17
|
+
end
|
18
|
+
|
19
|
+
def example_passed(example)
|
20
|
+
file = example.location.split(':').first
|
21
|
+
@example_times[file] += Time.now - @time
|
22
|
+
end
|
23
|
+
|
24
|
+
def start_dump(*args)
|
25
|
+
return unless ENV['TEST_ENV_NUMBER'] #only record when running in parallel
|
26
|
+
# TODO: Figure out why sometimes time can be less than 0
|
27
|
+
@output.puts @example_times.map { |file, time| "#{file}:#{time > 0 ? time : 0}" }
|
28
|
+
@output.flush
|
29
|
+
end
|
30
|
+
|
31
|
+
# stubs so that rspec doe not crash
|
32
|
+
|
33
|
+
def example_pending(*args)
|
34
|
+
end
|
35
|
+
|
36
|
+
def dump_summary(*args)
|
37
|
+
end
|
38
|
+
|
39
|
+
def dump_pending(*args)
|
40
|
+
end
|
41
|
+
|
42
|
+
def dump_failure(*args)
|
43
|
+
end
|
44
|
+
|
45
|
+
#stolen from Rspec
|
46
|
+
def close
|
47
|
+
@output.close if (IO === @output) & (@output != $stdout)
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,119 @@
|
|
1
|
+
require 'parallel'
|
2
|
+
|
3
|
+
class ParallelTests
|
4
|
+
VERSION = File.read( File.join(File.dirname(__FILE__),'..','VERSION') ).strip
|
5
|
+
|
6
|
+
# parallel:spec[2,controller] <-> parallel:spec[controller]
|
7
|
+
def self.parse_rake_args (args)
|
8
|
+
num_processes = Parallel.processor_count
|
9
|
+
options = ""
|
10
|
+
if args[:count].to_s =~ /^\d*$/ # number or empty
|
11
|
+
num_processes = args[:count] unless args[:count].to_s.empty?
|
12
|
+
prefix = args[:path_prefix]
|
13
|
+
options = args[:options] if args[:options]
|
14
|
+
else # something stringy
|
15
|
+
prefix = args[:count]
|
16
|
+
end
|
17
|
+
[num_processes.to_i, prefix.to_s, options]
|
18
|
+
end
|
19
|
+
|
20
|
+
# finds all tests and partitions them into groups
|
21
|
+
def self.tests_in_groups(root, num)
|
22
|
+
tests_with_sizes = slow_specs_first(find_tests_with_sizes(root))
|
23
|
+
|
24
|
+
groups = []
|
25
|
+
current_group = current_size = 0
|
26
|
+
tests_with_sizes.each do |test, size|
|
27
|
+
# inserts into next group if current is full and we are not in the last group
|
28
|
+
if (0.5*size + current_size) > group_size(tests_with_sizes, num) and num > current_group + 1
|
29
|
+
current_size = size
|
30
|
+
current_group += 1
|
31
|
+
else
|
32
|
+
current_size += size
|
33
|
+
end
|
34
|
+
groups[current_group] ||= []
|
35
|
+
groups[current_group] << test
|
36
|
+
end
|
37
|
+
groups.compact
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.run_tests(test_files, process_number, options)
|
41
|
+
require_list = test_files.map { |filename| "\"#{filename}\"" }.join(",")
|
42
|
+
cmd = "export RAILS_ENV=test ; ruby -Itest #{options} -e '[#{require_list}].each {|f| require f }'"
|
43
|
+
execute_command(cmd, process_number)
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.execute_command(cmd, process_number)
|
47
|
+
cmd = "export TEST_ENV_NUMBER=#{test_env_number(process_number)} ; #{cmd}"
|
48
|
+
f = open("|#{cmd}", 'r')
|
49
|
+
all = ''
|
50
|
+
while char = f.getc
|
51
|
+
char = (char.is_a?(Fixnum) ? char.chr : char) # 1.8 <-> 1.9
|
52
|
+
all << char
|
53
|
+
print char
|
54
|
+
STDOUT.flush
|
55
|
+
end
|
56
|
+
all
|
57
|
+
end
|
58
|
+
|
59
|
+
def self.find_results(test_output)
|
60
|
+
test_output.split("\n").map {|line|
|
61
|
+
line = line.gsub(/\.|F|\*/,'')
|
62
|
+
next unless line_is_result?(line)
|
63
|
+
line
|
64
|
+
}.compact
|
65
|
+
end
|
66
|
+
|
67
|
+
def self.failed?(results)
|
68
|
+
return true if results.empty?
|
69
|
+
!! results.detect{|line| line_is_failure?(line)}
|
70
|
+
end
|
71
|
+
|
72
|
+
def self.test_env_number(process_number)
|
73
|
+
process_number == 0 ? '' : process_number + 1
|
74
|
+
end
|
75
|
+
|
76
|
+
protected
|
77
|
+
|
78
|
+
def self.slow_specs_first(tests)
|
79
|
+
tests.sort_by{|test, size| size }.reverse
|
80
|
+
end
|
81
|
+
|
82
|
+
def self.line_is_result?(line)
|
83
|
+
line =~ /\d+ failure/
|
84
|
+
end
|
85
|
+
|
86
|
+
def self.line_is_failure?(line)
|
87
|
+
line =~ /(\d{2,}|[1-9]) (failure|error)/
|
88
|
+
end
|
89
|
+
|
90
|
+
def self.group_size(tests_with_sizes, num_groups)
|
91
|
+
total_size = tests_with_sizes.inject(0) { |sum, test| sum += test[1] }
|
92
|
+
total_size / num_groups.to_f
|
93
|
+
end
|
94
|
+
|
95
|
+
def self.find_tests_with_sizes(root)
|
96
|
+
tests = find_tests(root).sort
|
97
|
+
|
98
|
+
#TODO get the real root, atm this only works for complete runs when root point to e.g. real_root/spec
|
99
|
+
runtime_file = File.join(root,'..','tmp','parallel_profile.log')
|
100
|
+
lines = File.read(runtime_file).split("\n") rescue []
|
101
|
+
|
102
|
+
if lines.size * 1.5 > tests.size
|
103
|
+
# use recorded test runtime if we got enough data
|
104
|
+
times = Hash.new(1)
|
105
|
+
lines.each do |line|
|
106
|
+
test, time = line.split(":")
|
107
|
+
times[test] = time.to_f
|
108
|
+
end
|
109
|
+
tests.map { |test| [ test, times[test] ] }
|
110
|
+
else
|
111
|
+
# use file sizes
|
112
|
+
tests.map { |test| [ test, File.stat(test).size ] }
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def self.find_tests(root)
|
117
|
+
Dir["#{root}**/**/*_test.rb"]
|
118
|
+
end
|
119
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = %q{parallel_tests}
|
8
|
+
s.version = "0.3.0"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = ["Michael Grosser"]
|
12
|
+
s.date = %q{2010-03-02}
|
13
|
+
s.email = %q{grosser.michael@gmail.com}
|
14
|
+
s.executables = ["parallel_test", "parallel_spec", "parallel_cucumber"]
|
15
|
+
s.extra_rdoc_files = [
|
16
|
+
"README.markdown"
|
17
|
+
]
|
18
|
+
s.files = [
|
19
|
+
".gitignore",
|
20
|
+
"README.markdown",
|
21
|
+
"Rakefile",
|
22
|
+
"VERSION",
|
23
|
+
"bin/parallel_cucumber",
|
24
|
+
"bin/parallel_spec",
|
25
|
+
"bin/parallel_test",
|
26
|
+
"lib/parallel_cucumber.rb",
|
27
|
+
"lib/parallel_specs.rb",
|
28
|
+
"lib/parallel_specs/spec_runtime_logger.rb",
|
29
|
+
"lib/parallel_tests.rb",
|
30
|
+
"parallel_tests.gemspec",
|
31
|
+
"spec/integration_spec.rb",
|
32
|
+
"spec/parallel_cucumber_spec.rb",
|
33
|
+
"spec/parallel_specs_spec.rb",
|
34
|
+
"spec/parallel_tests_spec.rb",
|
35
|
+
"spec/spec_helper.rb",
|
36
|
+
"tasks/parallel_specs.rake"
|
37
|
+
]
|
38
|
+
s.homepage = %q{http://github.com/grosser/parallel_tests}
|
39
|
+
s.rdoc_options = ["--charset=UTF-8"]
|
40
|
+
s.require_paths = ["lib"]
|
41
|
+
s.rubygems_version = %q{1.3.6}
|
42
|
+
s.summary = %q{Run tests / specs / features in parallel}
|
43
|
+
s.test_files = [
|
44
|
+
"spec/spec_helper.rb",
|
45
|
+
"spec/parallel_tests_spec.rb",
|
46
|
+
"spec/parallel_specs_spec.rb",
|
47
|
+
"spec/parallel_cucumber_spec.rb",
|
48
|
+
"spec/integration_spec.rb"
|
49
|
+
]
|
50
|
+
|
51
|
+
if s.respond_to? :specification_version then
|
52
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
53
|
+
s.specification_version = 3
|
54
|
+
|
55
|
+
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
56
|
+
else
|
57
|
+
end
|
58
|
+
else
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
@@ -0,0 +1,83 @@
|
|
1
|
+
describe 'CLI' do
|
2
|
+
before do
|
3
|
+
`rm -rf #{folder}`
|
4
|
+
end
|
5
|
+
|
6
|
+
after do
|
7
|
+
`rm -rf #{folder}`
|
8
|
+
end
|
9
|
+
|
10
|
+
def folder
|
11
|
+
"/tmp/parallel_tests_tests"
|
12
|
+
end
|
13
|
+
|
14
|
+
def write(file, content)
|
15
|
+
path = "#{folder}/spec/#{file}"
|
16
|
+
`mkdir -p #{File.dirname(path)}` unless File.exist?(File.dirname(path))
|
17
|
+
File.open(path, 'w'){|f| f.write content }
|
18
|
+
path
|
19
|
+
end
|
20
|
+
|
21
|
+
def bin_folder
|
22
|
+
"#{File.expand_path(File.dirname(__FILE__))}/../bin"
|
23
|
+
end
|
24
|
+
|
25
|
+
def executable
|
26
|
+
"#{bin_folder}/parallel_test"
|
27
|
+
end
|
28
|
+
|
29
|
+
def run_specs(options={})
|
30
|
+
`cd #{folder} && #{executable} -t spec -n #{options[:processes]||2} 2>&1 && echo 'i ran!'`
|
31
|
+
end
|
32
|
+
|
33
|
+
it "runs tests in parallel" do
|
34
|
+
write 'xxx_spec.rb', 'describe("it"){it("should"){puts "TEST1"}}'
|
35
|
+
write 'xxx2_spec.rb', 'describe("it"){it("should"){puts "TEST2"}}'
|
36
|
+
result = run_specs
|
37
|
+
|
38
|
+
# test ran and gave their puts
|
39
|
+
result.should include('TEST1')
|
40
|
+
result.should include('TEST2')
|
41
|
+
|
42
|
+
# all results present
|
43
|
+
result.scan('1 example, 0 failure').size.should == 4 # 2 results + 2 result summary
|
44
|
+
result.scan(/Finished in \d+\.\d+ seconds/).size.should == 2
|
45
|
+
result.scan(/Took \d+\.\d+ seconds/).size.should == 1 # parallel summary
|
46
|
+
|
47
|
+
result.should include('i ran!')
|
48
|
+
end
|
49
|
+
|
50
|
+
it "fails when tests fail" do
|
51
|
+
write 'xxx_spec.rb', 'describe("it"){it("should"){puts "TEST1"}}'
|
52
|
+
write 'xxx2_spec.rb', 'describe("it"){it("should"){1.should == 2}}'
|
53
|
+
result = run_specs
|
54
|
+
|
55
|
+
result.scan('1 example, 1 failure').size.should == 2
|
56
|
+
result.scan('1 example, 0 failure').size.should == 2
|
57
|
+
result.should =~ /specs failed/i
|
58
|
+
result.should_not include('i ran!')
|
59
|
+
end
|
60
|
+
|
61
|
+
it "can exec given commands with ENV['TEST_ENV_NUM']" do
|
62
|
+
result = `#{executable} -e 'ruby -e "puts ENV[:TEST_ENV_NUMBER.to_s].inspect"' -n 4`
|
63
|
+
result.split("\n").sort.should == %w["" "2" "3" "4"]
|
64
|
+
end
|
65
|
+
|
66
|
+
it "can run through parallel_spec / parallel_cucumber" do
|
67
|
+
version = `#{executable} -v`
|
68
|
+
`#{bin_folder}/parallel_spec -v`.should == version
|
69
|
+
`#{bin_folder}/parallel_cucumber -v`.should == version
|
70
|
+
end
|
71
|
+
|
72
|
+
it "runs faster with more processes" do
|
73
|
+
write 'xxx_spec.rb', 'describe("it"){it("should"){sleep 2}}'
|
74
|
+
write 'xxx2_spec.rb', 'describe("it"){it("should"){sleep 2}}'
|
75
|
+
write 'xxx3_spec.rb', 'describe("it"){it("should"){sleep 2}}'
|
76
|
+
write 'xxx4_spec.rb', 'describe("it"){it("should"){sleep 2}}'
|
77
|
+
write 'xxx5_spec.rb', 'describe("it"){it("should"){sleep 2}}'
|
78
|
+
write 'xxx6_spec.rb', 'describe("it"){it("should"){sleep 2}}'
|
79
|
+
t = Time.now
|
80
|
+
run_specs :processes => 6
|
81
|
+
(Time.now - t).should < 5
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper'
|
2
|
+
|
3
|
+
describe ParallelCucumber do
|
4
|
+
test_tests_in_groups(ParallelCucumber, 'features', ".feature")
|
5
|
+
|
6
|
+
describe :run_tests do
|
7
|
+
before(:each) do
|
8
|
+
File.stub!(:file?).with('.bundle/environment.rb').and_return false
|
9
|
+
File.stub!(:file?).with('script/cucumber').and_return true
|
10
|
+
end
|
11
|
+
|
12
|
+
it "uses TEST_ENV_NUMBER=blank when called for process 0" do
|
13
|
+
ParallelCucumber.should_receive(:open).with{|x,y| x=~/TEST_ENV_NUMBER= /}.and_return mock(:getc=>false)
|
14
|
+
ParallelCucumber.run_tests(['xxx'],0,'')
|
15
|
+
end
|
16
|
+
|
17
|
+
it "uses TEST_ENV_NUMBER=2 when called for process 1" do
|
18
|
+
ParallelCucumber.should_receive(:open).with{|x,y| x=~/TEST_ENV_NUMBER=2/}.and_return mock(:getc=>false)
|
19
|
+
ParallelCucumber.run_tests(['xxx'],1,'')
|
20
|
+
end
|
21
|
+
|
22
|
+
it "returns the output" do
|
23
|
+
io = open('spec/spec_helper.rb')
|
24
|
+
ParallelCucumber.stub!(:print)
|
25
|
+
ParallelCucumber.should_receive(:open).and_return io
|
26
|
+
ParallelCucumber.run_tests(['xxx'],1,'').should =~ /\$LOAD_PATH << File/
|
27
|
+
end
|
28
|
+
|
29
|
+
it "runs bundle exec cucumber when on bundler 0.9" do
|
30
|
+
File.stub!(:file?).with('.bundle/environment.rb').and_return true
|
31
|
+
ParallelCucumber.should_receive(:open).with{|x,y| x =~ %r{bundle exec cucumber}}.and_return mock(:getc=>false)
|
32
|
+
ParallelCucumber.run_tests(['xxx'],1,'')
|
33
|
+
end
|
34
|
+
|
35
|
+
it "runs script/cucumber when script/cucumber is found" do
|
36
|
+
ParallelCucumber.should_receive(:open).with{|x,y| x =~ %r{script/cucumber}}.and_return mock(:getc=>false)
|
37
|
+
ParallelCucumber.run_tests(['xxx'],1,'')
|
38
|
+
end
|
39
|
+
|
40
|
+
it "runs cucumber by default" do
|
41
|
+
File.stub!(:file?).with('script/cucumber').and_return false
|
42
|
+
ParallelCucumber.should_receive(:open).with{|x,y| x !~ %r{(script/cucumber)|(bundle exec cucumber)}}.and_return mock(:getc=>false)
|
43
|
+
ParallelCucumber.run_tests(['xxx'],1,'')
|
44
|
+
end
|
45
|
+
|
46
|
+
it "uses options passed in" do
|
47
|
+
ParallelCucumber.should_receive(:open).with{|x,y| x =~ %r{script/cucumber -p default}}.and_return mock(:getc=>false)
|
48
|
+
ParallelCucumber.run_tests(['xxx'],1,'-p default')
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
describe :find_results do
|
53
|
+
it "finds multiple results in test output" do
|
54
|
+
output = <<EOF
|
55
|
+
And I should not see "/en/" # features/step_definitions/webrat_steps.rb:87
|
56
|
+
|
57
|
+
7 scenarios (3 failed, 4 passed)
|
58
|
+
33 steps (3 failed, 2 skipped, 28 passed)
|
59
|
+
/apps/rs/features/signup.feature:2
|
60
|
+
Given I am on "/" # features/step_definitions/common_steps.rb:12
|
61
|
+
When I click "register" # features/step_definitions/common_steps.rb:6
|
62
|
+
And I should have "2" emails # features/step_definitions/user_steps.rb:25
|
63
|
+
|
64
|
+
4 scenarios (4 passed)
|
65
|
+
40 steps (40 passed)
|
66
|
+
|
67
|
+
EOF
|
68
|
+
ParallelCucumber.find_results(output).should == ["7 scenarios (3 failed, 4 passed)", "33 steps (3 failed, 2 skipped, 28 passed)", "4 scenarios (4 passed)", "40 steps (40 passed)"]
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
describe :failed do
|
73
|
+
it "fails with single failed" do
|
74
|
+
ParallelCucumber.failed?(['40 steps (40 passed)','33 steps (3 failed, 2 skipped, 28 passed)']).should == true
|
75
|
+
end
|
76
|
+
|
77
|
+
it "fails with multiple failed tests" do
|
78
|
+
ParallelCucumber.failed?(['33 steps (3 failed, 2 skipped, 28 passed)','33 steps (3 failed, 2 skipped, 28 passed)']).should == true
|
79
|
+
end
|
80
|
+
|
81
|
+
it "fails with a single scenario failure during setup phase" do
|
82
|
+
ParallelCucumber.failed?(['1 scenarios (1 failed)']).should == true
|
83
|
+
end
|
84
|
+
|
85
|
+
it "fails with scenario failures during setup phase when other steps pass" do
|
86
|
+
ParallelCucumber.failed?(['7 scenarios (3 failed, 4 passed)','40 steps (40 passed)']).should == true
|
87
|
+
end
|
88
|
+
|
89
|
+
it "does not fail with successful tests" do
|
90
|
+
ParallelCucumber.failed?(['4 scenarios (4 passed)','40 steps (40 passed)','4 scenarios (4 passed)','40 steps (40 passed)']).should == false
|
91
|
+
end
|
92
|
+
|
93
|
+
it "does not fail with 0 failures" do
|
94
|
+
ParallelCucumber.failed?(['4 scenarios (4 passed 0 failed)','40 steps (40 passed 0 failed)','4 scenarios (4 passed 0 failed)','40 steps (40 passed)']).should == false
|
95
|
+
end
|
96
|
+
|
97
|
+
it "does fail with 10 failures" do
|
98
|
+
ParallelCucumber.failed?(['40 steps (40 passed 10 failed)','40 steps (40 passed)']).should == true
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,134 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper'
|
2
|
+
|
3
|
+
describe ParallelSpecs do
|
4
|
+
test_tests_in_groups(ParallelSpecs, 'spec', '_spec.rb')
|
5
|
+
|
6
|
+
describe :run_tests do
|
7
|
+
before do
|
8
|
+
File.stub!(:file?).with('.bundle/environment.rb').and_return false
|
9
|
+
File.stub!(:file?).with('script/spec').and_return true
|
10
|
+
File.stub!(:file?).with('spec/spec.opts').and_return true
|
11
|
+
File.stub!(:file?).with('spec/parallel_spec.opts').and_return false
|
12
|
+
end
|
13
|
+
|
14
|
+
it "uses TEST_ENV_NUMBER=blank when called for process 0" do
|
15
|
+
ParallelSpecs.should_receive(:open).with{|x,y|x=~/TEST_ENV_NUMBER= /}.and_return mock(:getc=>false)
|
16
|
+
ParallelSpecs.run_tests(['xxx'],0,'')
|
17
|
+
end
|
18
|
+
|
19
|
+
it "uses TEST_ENV_NUMBER=2 when called for process 1" do
|
20
|
+
ParallelSpecs.should_receive(:open).with{|x,y| x=~/TEST_ENV_NUMBER=2/}.and_return mock(:getc=>false)
|
21
|
+
ParallelSpecs.run_tests(['xxx'],1,'')
|
22
|
+
end
|
23
|
+
|
24
|
+
it "runs with color when called from cmdline" do
|
25
|
+
ParallelSpecs.should_receive(:open).with{|x,y| x=~/RSPEC_COLOR=1/}.and_return mock(:getc=>false)
|
26
|
+
$stdout.should_receive(:tty?).and_return true
|
27
|
+
ParallelSpecs.run_tests(['xxx'],1,'')
|
28
|
+
end
|
29
|
+
|
30
|
+
it "runs without color when not called from cmdline" do
|
31
|
+
ParallelSpecs.should_receive(:open).with{|x,y| x !~ /RSPEC_COLOR/}.and_return mock(:getc=>false)
|
32
|
+
$stdout.should_receive(:tty?).and_return false
|
33
|
+
ParallelSpecs.run_tests(['xxx'],1,'')
|
34
|
+
end
|
35
|
+
|
36
|
+
it "run bundle exec spec when on bundler 0.9" do
|
37
|
+
File.stub!(:file?).with('.bundle/environment.rb').and_return true
|
38
|
+
ParallelSpecs.should_receive(:open).with{|x,y| x =~ %r{bundle exec spec}}.and_return mock(:getc=>false)
|
39
|
+
ParallelSpecs.run_tests(['xxx'],1,'')
|
40
|
+
end
|
41
|
+
|
42
|
+
it "runs script/spec when script/spec can be found" do
|
43
|
+
File.should_receive(:file?).with('script/spec').and_return true
|
44
|
+
ParallelSpecs.should_receive(:open).with{|x,y| x =~ %r{script/spec}}.and_return mock(:getc=>false)
|
45
|
+
ParallelSpecs.run_tests(['xxx'],1,'')
|
46
|
+
end
|
47
|
+
|
48
|
+
it "runs spec when script/spec cannot be found" do
|
49
|
+
File.stub!(:file?).with('script/spec').and_return false
|
50
|
+
ParallelSpecs.should_receive(:open).with{|x,y| x !~ %r{(script/spec)|(bundle exec spec)}}.and_return mock(:getc=>false)
|
51
|
+
ParallelSpecs.run_tests(['xxx'],1,'')
|
52
|
+
end
|
53
|
+
|
54
|
+
it "uses no -O when no opts where found" do
|
55
|
+
File.stub!(:file?).with('spec/spec.opts').and_return false
|
56
|
+
ParallelSpecs.should_receive(:open).with{|x,y| x !~ %r{spec/spec.opts}}.and_return mock(:getc=>false)
|
57
|
+
ParallelSpecs.run_tests(['xxx'],1,'')
|
58
|
+
end
|
59
|
+
|
60
|
+
it "uses spec/spec.opts when found" do
|
61
|
+
ParallelSpecs.should_receive(:open).with{|x,y| x =~ %r{script/spec\s+-O spec/spec.opts}}.and_return mock(:getc=>false)
|
62
|
+
ParallelSpecs.run_tests(['xxx'],1,'')
|
63
|
+
end
|
64
|
+
|
65
|
+
it "uses spec/parallel_spec.opts when found" do
|
66
|
+
File.should_receive(:file?).with('spec/parallel_spec.opts').and_return true
|
67
|
+
ParallelSpecs.should_receive(:open).with{|x,y| x =~ %r{script/spec\s+-O spec/parallel_spec.opts}}.and_return mock(:getc=>false)
|
68
|
+
ParallelSpecs.run_tests(['xxx'],1,'')
|
69
|
+
end
|
70
|
+
|
71
|
+
it "uses options passed in" do
|
72
|
+
ParallelSpecs.should_receive(:open).with{|x,y| x =~ %r{script/spec -f n}}.and_return mock(:getc=>false)
|
73
|
+
ParallelSpecs.run_tests(['xxx'],1,'-f n')
|
74
|
+
end
|
75
|
+
|
76
|
+
it "returns the output" do
|
77
|
+
io = open('spec/spec_helper.rb')
|
78
|
+
ParallelSpecs.stub!(:print)
|
79
|
+
ParallelSpecs.should_receive(:open).and_return io
|
80
|
+
ParallelSpecs.run_tests(['xxx'],1,'').should =~ /\$LOAD_PATH << File/
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
describe :find_results do
|
85
|
+
it "finds multiple results in spec output" do
|
86
|
+
output = <<EOF
|
87
|
+
....F...
|
88
|
+
..
|
89
|
+
failute fsddsfsd
|
90
|
+
...
|
91
|
+
ff.**..
|
92
|
+
0 examples, 0 failures, 0 pending
|
93
|
+
ff.**..
|
94
|
+
1 example, 1 failure, 1 pending
|
95
|
+
EOF
|
96
|
+
|
97
|
+
ParallelSpecs.find_results(output).should == ['0 examples, 0 failures, 0 pending','1 example, 1 failure, 1 pending']
|
98
|
+
end
|
99
|
+
|
100
|
+
it "is robust against scrambeled output" do
|
101
|
+
output = <<EOF
|
102
|
+
....F...
|
103
|
+
..
|
104
|
+
failute fsddsfsd
|
105
|
+
...
|
106
|
+
ff.**..
|
107
|
+
0 exFampl*es, 0 failures, 0 pend.ing
|
108
|
+
ff.**..
|
109
|
+
1 exampF.les, 1 failures, 1 pend.ing
|
110
|
+
EOF
|
111
|
+
|
112
|
+
ParallelSpecs.find_results(output).should == ['0 examples, 0 failures, 0 pending','1 examples, 1 failures, 1 pending']
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
describe :failed do
|
117
|
+
it "fails with single failed specs" do
|
118
|
+
ParallelSpecs.failed?(['0 examples, 0 failures, 0 pending','1 examples, 1 failure, 1 pending']).should == true
|
119
|
+
end
|
120
|
+
|
121
|
+
it "fails with multiple failed specs" do
|
122
|
+
ParallelSpecs.failed?(['0 examples, 1 failure, 0 pending','1 examples, 111 failures, 1 pending']).should == true
|
123
|
+
end
|
124
|
+
|
125
|
+
it "does not fail with successful specs" do
|
126
|
+
ParallelSpecs.failed?(['0 examples, 0 failures, 0 pending','1 examples, 0 failures, 1 pending']).should == false
|
127
|
+
end
|
128
|
+
|
129
|
+
it "does fail with 10 failures" do
|
130
|
+
ParallelSpecs.failed?(['0 examples, 10 failures, 0 pending','1 examples, 0 failures, 1 pending']).should == true
|
131
|
+
end
|
132
|
+
|
133
|
+
end
|
134
|
+
end
|
@@ -0,0 +1,130 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper'
|
2
|
+
|
3
|
+
describe ParallelTests do
|
4
|
+
test_tests_in_groups(ParallelTests, 'test', '_test.rb')
|
5
|
+
|
6
|
+
describe :parse_rake_args do
|
7
|
+
it "should return the count" do
|
8
|
+
args = {:count => 2}
|
9
|
+
ParallelTests.parse_rake_args(args).should == [2, '', ""]
|
10
|
+
end
|
11
|
+
|
12
|
+
it "should default to the prefix" do
|
13
|
+
args = {:count => "models"}
|
14
|
+
ParallelTests.parse_rake_args(args).should == [2, "models", ""]
|
15
|
+
end
|
16
|
+
|
17
|
+
it "should return the count and prefix" do
|
18
|
+
args = {:count => 2, :path_prefix => "models"}
|
19
|
+
ParallelTests.parse_rake_args(args).should == [2, "models", ""]
|
20
|
+
end
|
21
|
+
|
22
|
+
it "should return the count, prefix, and options" do
|
23
|
+
args = {:count => 2, :path_prefix => "plain", :options => "-p default" }
|
24
|
+
ParallelTests.parse_rake_args(args).should == [2, "plain", "-p default"]
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
describe :run_tests do
|
29
|
+
it "uses TEST_ENV_NUMBER=blank when called for process 0" do
|
30
|
+
ParallelTests.should_receive(:open).with{|x,y|x=~/TEST_ENV_NUMBER= /}.and_return mock(:getc=>false)
|
31
|
+
ParallelTests.run_tests(['xxx'],0,'')
|
32
|
+
end
|
33
|
+
|
34
|
+
it "uses TEST_ENV_NUMBER=2 when called for process 1" do
|
35
|
+
ParallelTests.should_receive(:open).with{|x,y| x=~/TEST_ENV_NUMBER=2/}.and_return mock(:getc=>false)
|
36
|
+
ParallelTests.run_tests(['xxx'],1,'')
|
37
|
+
end
|
38
|
+
|
39
|
+
it "uses options" do
|
40
|
+
ParallelTests.should_receive(:open).with{|x,y| x=~ %r{ruby -Itest -v}}.and_return mock(:getc=>false)
|
41
|
+
ParallelTests.run_tests(['xxx'],1,'-v')
|
42
|
+
end
|
43
|
+
|
44
|
+
it "returns the output" do
|
45
|
+
io = open('spec/spec_helper.rb')
|
46
|
+
ParallelTests.stub!(:print)
|
47
|
+
ParallelTests.should_receive(:open).and_return io
|
48
|
+
ParallelTests.run_tests(['xxx'],1,'').should =~ /\$LOAD_PATH << File/
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
describe :find_results do
|
53
|
+
it "finds multiple results in test output" do
|
54
|
+
output = <<EOF
|
55
|
+
Loaded suite /opt/ruby-enterprise/lib/ruby/gems/1.8/gems/rake-0.8.4/lib/rake/rake_test_loader
|
56
|
+
Started
|
57
|
+
..............
|
58
|
+
Finished in 0.145069 seconds.
|
59
|
+
|
60
|
+
10 tests, 20 assertions, 0 failures, 0 errors
|
61
|
+
Loaded suite /opt/ruby-enterprise/lib/ruby/gems/1.8/gems/rake-0.8.4/lib/rake/rake_test_loader
|
62
|
+
Started
|
63
|
+
..............
|
64
|
+
Finished in 0.145069 seconds.
|
65
|
+
|
66
|
+
14 tests, 20 assertions, 0 failures, 0 errors
|
67
|
+
|
68
|
+
EOF
|
69
|
+
|
70
|
+
ParallelTests.find_results(output).should == ['10 tests, 20 assertions, 0 failures, 0 errors','14 tests, 20 assertions, 0 failures, 0 errors']
|
71
|
+
end
|
72
|
+
|
73
|
+
it "is robust against scrambeled output" do
|
74
|
+
output = <<EOF
|
75
|
+
Loaded suite /opt/ruby-enterprise/lib/ruby/gems/1.8/gems/rake-0.8.4/lib/rake/rake_test_loader
|
76
|
+
Started
|
77
|
+
..............
|
78
|
+
Finished in 0.145069 seconds.
|
79
|
+
|
80
|
+
10 tests, 20 assertions, 0 failures, 0 errors
|
81
|
+
Loaded suite /opt/ruby-enterprise/lib/ruby/gems/1.8/gems/rake-0.8.4/lib/rake/rake_test_loader
|
82
|
+
Started
|
83
|
+
..............
|
84
|
+
Finished in 0.145069 seconds.
|
85
|
+
|
86
|
+
14 te.dsts, 20 assertions, 0 failures, 0 errors
|
87
|
+
EOF
|
88
|
+
|
89
|
+
ParallelTests.find_results(output).should == ['10 tests, 20 assertions, 0 failures, 0 errors','14 tedsts, 20 assertions, 0 failures, 0 errors']
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
describe :failed do
|
94
|
+
it "fails with single failed" do
|
95
|
+
ParallelTests.failed?(['10 tests, 20 assertions, 0 failures, 0 errors','10 tests, 20 assertions, 1 failure, 0 errors']).should == true
|
96
|
+
end
|
97
|
+
|
98
|
+
it "fails with single error" do
|
99
|
+
ParallelTests.failed?(['10 tests, 20 assertions, 0 failures, 1 errors','10 tests, 20 assertions, 0 failures, 0 errors']).should == true
|
100
|
+
end
|
101
|
+
|
102
|
+
it "fails with failed and error" do
|
103
|
+
ParallelTests.failed?(['10 tests, 20 assertions, 0 failures, 1 errors','10 tests, 20 assertions, 1 failures, 1 errors']).should == true
|
104
|
+
end
|
105
|
+
|
106
|
+
it "fails with multiple failed tests" do
|
107
|
+
ParallelTests.failed?(['10 tests, 20 assertions, 2 failures, 0 errors','10 tests, 1 assertion, 1 failures, 0 errors']).should == true
|
108
|
+
end
|
109
|
+
|
110
|
+
it "does not fail with successful tests" do
|
111
|
+
ParallelTests.failed?(['10 tests, 20 assertions, 0 failures, 0 errors','10 tests, 20 assertions, 0 failures, 0 errors']).should == false
|
112
|
+
end
|
113
|
+
|
114
|
+
it "does fail with 10 failures" do
|
115
|
+
ParallelTests.failed?(['10 tests, 20 assertions, 10 failures, 0 errors','10 tests, 20 assertions, 0 failures, 0 errors']).should == true
|
116
|
+
end
|
117
|
+
|
118
|
+
it "is not failed with empty results" do
|
119
|
+
ParallelTests.failed?(['0 tests, 0 assertions, 0 failures, 0 errors']).should == false
|
120
|
+
end
|
121
|
+
|
122
|
+
it "is failed when there are no results" do
|
123
|
+
ParallelTests.failed?([]).should == true
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
it "has a version" do
|
128
|
+
ParallelTests::VERSION.should =~ /^\d+\.\d+\.\d+$/
|
129
|
+
end
|
130
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,78 @@
|
|
1
|
+
# ---- requirements
|
2
|
+
$LOAD_PATH << File.expand_path("../lib", File.dirname(__FILE__))
|
3
|
+
require 'rubygems'
|
4
|
+
|
5
|
+
FAKE_RAILS_ROOT = '/tmp/pspecs/fixtures'
|
6
|
+
|
7
|
+
require 'parallel_specs'
|
8
|
+
require 'parallel_cucumber'
|
9
|
+
|
10
|
+
def size_of(group)
|
11
|
+
group.inject(0) { |sum, test| sum += File.stat(test).size }
|
12
|
+
end
|
13
|
+
|
14
|
+
def test_tests_in_groups(klass, folder, suffix)
|
15
|
+
test_root = "#{FAKE_RAILS_ROOT}/#{folder}"
|
16
|
+
|
17
|
+
describe :tests_in_groups do
|
18
|
+
before :all do
|
19
|
+
system "rm -rf #{FAKE_RAILS_ROOT}; mkdir -p #{test_root}/temp"
|
20
|
+
|
21
|
+
@files = [0,1,2,3,4,5,6,7].map do |i|
|
22
|
+
size = 99
|
23
|
+
file = "#{test_root}/temp/x#{i}#{suffix}"
|
24
|
+
File.open(file, 'w') { |f| f.puts 'x' * size }
|
25
|
+
file
|
26
|
+
end
|
27
|
+
|
28
|
+
@log = "#{FAKE_RAILS_ROOT}/tmp/parallel_profile.log"
|
29
|
+
`mkdir #{File.dirname(@log)}`
|
30
|
+
`rm -f #{@log}`
|
31
|
+
end
|
32
|
+
|
33
|
+
it "finds all tests" do
|
34
|
+
found = klass.tests_in_groups(test_root, 1)
|
35
|
+
all = [ Dir["#{test_root}/**/*#{suffix}"] ]
|
36
|
+
(found.flatten - all.flatten).should == []
|
37
|
+
end
|
38
|
+
|
39
|
+
it "partitions them into groups by equal size" do
|
40
|
+
groups = klass.tests_in_groups(test_root, 2)
|
41
|
+
groups.size.should == 2
|
42
|
+
size_of(groups[0]).should == 400
|
43
|
+
size_of(groups[1]).should == 400
|
44
|
+
end
|
45
|
+
|
46
|
+
it 'should partition correctly with a group size of 4' do
|
47
|
+
groups = klass.tests_in_groups(test_root, 4)
|
48
|
+
groups.size.should == 4
|
49
|
+
size_of(groups[0]).should == 200
|
50
|
+
size_of(groups[1]).should == 200
|
51
|
+
size_of(groups[2]).should == 200
|
52
|
+
size_of(groups[3]).should == 200
|
53
|
+
end
|
54
|
+
|
55
|
+
it 'should partition correctly with an uneven group size' do
|
56
|
+
groups = klass.tests_in_groups(test_root, 3)
|
57
|
+
groups.size.should == 3
|
58
|
+
size_of(groups[0]).should == 300
|
59
|
+
size_of(groups[1]).should == 300
|
60
|
+
size_of(groups[2]).should == 200
|
61
|
+
end
|
62
|
+
|
63
|
+
it "partitions by runtime when runtime-data is available" do
|
64
|
+
File.open(@log,'w') do |f|
|
65
|
+
@files[1..-1].each{|file| f.puts "#{file}:#{@files.index(file)}"}
|
66
|
+
f.puts "#{@files[0]}:10"
|
67
|
+
end
|
68
|
+
|
69
|
+
groups = klass.tests_in_groups(test_root, 2)
|
70
|
+
groups.size.should == 2
|
71
|
+
# 10 + 7 = 17
|
72
|
+
groups[0].should == [@files[0],@files[7]]
|
73
|
+
# 6+5+4+3+2+1 = 21
|
74
|
+
# still room for optimization...
|
75
|
+
groups[1].should == [@files[6],@files[5],@files[4],@files[3],@files[2],@files[1]]
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
namespace :parallel do
|
2
|
+
desc "update test databases by running db:test:prepare for each --> parallel:prepare[num_cpus]"
|
3
|
+
task :prepare, :count do |t,args|
|
4
|
+
require File.join(File.dirname(__FILE__), '..', 'lib', "parallel_tests")
|
5
|
+
|
6
|
+
Parallel.in_processes(args[:count] ? args[:count].to_i : nil) do |i|
|
7
|
+
puts "Preparing test database #{i + 1}"
|
8
|
+
`export TEST_ENV_NUMBER=#{ParallelTests.test_env_number(i)} ; rake db:test:prepare`
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
# Useful when dumping/resetting takes too long
|
13
|
+
desc "update test databases by running db:mgrate for each --> parallel:migrate[num_cpus]"
|
14
|
+
task :migrate, :count do |t,args|
|
15
|
+
require File.join(File.dirname(__FILE__), '..', 'lib', "parallel_tests")
|
16
|
+
|
17
|
+
Parallel.in_processes(args[:count] ? args[:count].to_i : nil) do |i|
|
18
|
+
puts "Migrating test database #{i + 1}"
|
19
|
+
`export TEST_ENV_NUMBER=#{ParallelTests.test_env_number(i)} ; rake db:migrate RAILS_ENV=test`
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
['test', 'spec', 'features'].each do |type|
|
24
|
+
desc "run #{type} in parallel with parallel:#{type}[num_cpus]"
|
25
|
+
task type, :count, :path_prefix, :options do |t,args|
|
26
|
+
require File.join(File.dirname(__FILE__), '..', 'lib', "parallel_tests")
|
27
|
+
count, prefix, options = ParallelTests.parse_rake_args(args)
|
28
|
+
sh "#{File.join(File.dirname(__FILE__), '..', 'bin', 'parallel_test')} --type #{type} -n #{count} -p '#{prefix}' -r '#{RAILS_ROOT}' -o '#{options}'"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
#backwards compatability
|
34
|
+
#spec:parallel:prepare
|
35
|
+
#spec:parallel
|
36
|
+
#test:parallel
|
37
|
+
namespace :spec do
|
38
|
+
namespace :parallel do
|
39
|
+
task :prepare, :count do |t,args|
|
40
|
+
$stderr.puts "WARNING -- Deprecated! use parallel:prepare"
|
41
|
+
Rake::Task['parallel:prepare'].invoke(args[:count])
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
task :parallel, :count, :path_prefix do |t,args|
|
46
|
+
$stderr.puts "WARNING -- Deprecated! use parallel:spec"
|
47
|
+
Rake::Task['parallel:spec'].invoke(args[:count], args[:path_prefix])
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
namespace :test do
|
52
|
+
task :parallel, :count, :path_prefix do |t,args|
|
53
|
+
$stderr.puts "WARNING -- Deprecated! use parallel:test"
|
54
|
+
Rake::Task['parallel:test'].invoke(args[:count], args[:path_prefix])
|
55
|
+
end
|
56
|
+
end
|
metadata
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: parallel_tests
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 3
|
8
|
+
- 0
|
9
|
+
version: 0.3.0
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- Michael Grosser
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
|
17
|
+
date: 2010-03-02 00:00:00 +01:00
|
18
|
+
default_executable:
|
19
|
+
dependencies: []
|
20
|
+
|
21
|
+
description:
|
22
|
+
email: grosser.michael@gmail.com
|
23
|
+
executables:
|
24
|
+
- parallel_test
|
25
|
+
- parallel_spec
|
26
|
+
- parallel_cucumber
|
27
|
+
extensions: []
|
28
|
+
|
29
|
+
extra_rdoc_files:
|
30
|
+
- README.markdown
|
31
|
+
files:
|
32
|
+
- .gitignore
|
33
|
+
- README.markdown
|
34
|
+
- Rakefile
|
35
|
+
- VERSION
|
36
|
+
- bin/parallel_cucumber
|
37
|
+
- bin/parallel_spec
|
38
|
+
- bin/parallel_test
|
39
|
+
- lib/parallel_cucumber.rb
|
40
|
+
- lib/parallel_specs.rb
|
41
|
+
- lib/parallel_specs/spec_runtime_logger.rb
|
42
|
+
- lib/parallel_tests.rb
|
43
|
+
- parallel_tests.gemspec
|
44
|
+
- spec/integration_spec.rb
|
45
|
+
- spec/parallel_cucumber_spec.rb
|
46
|
+
- spec/parallel_specs_spec.rb
|
47
|
+
- spec/parallel_tests_spec.rb
|
48
|
+
- spec/spec_helper.rb
|
49
|
+
- tasks/parallel_specs.rake
|
50
|
+
has_rdoc: true
|
51
|
+
homepage: http://github.com/grosser/parallel_tests
|
52
|
+
licenses: []
|
53
|
+
|
54
|
+
post_install_message:
|
55
|
+
rdoc_options:
|
56
|
+
- --charset=UTF-8
|
57
|
+
require_paths:
|
58
|
+
- lib
|
59
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
60
|
+
requirements:
|
61
|
+
- - ">="
|
62
|
+
- !ruby/object:Gem::Version
|
63
|
+
segments:
|
64
|
+
- 0
|
65
|
+
version: "0"
|
66
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
67
|
+
requirements:
|
68
|
+
- - ">="
|
69
|
+
- !ruby/object:Gem::Version
|
70
|
+
segments:
|
71
|
+
- 0
|
72
|
+
version: "0"
|
73
|
+
requirements: []
|
74
|
+
|
75
|
+
rubyforge_project:
|
76
|
+
rubygems_version: 1.3.6
|
77
|
+
signing_key:
|
78
|
+
specification_version: 3
|
79
|
+
summary: Run tests / specs / features in parallel
|
80
|
+
test_files:
|
81
|
+
- spec/spec_helper.rb
|
82
|
+
- spec/parallel_tests_spec.rb
|
83
|
+
- spec/parallel_specs_spec.rb
|
84
|
+
- spec/parallel_cucumber_spec.rb
|
85
|
+
- spec/integration_spec.rb
|