parallel_tests 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|