friendlyfashion-parallel_tests 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +2 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +44 -0
- data/Rakefile +6 -0
- data/Readme.md +232 -0
- data/ReadmeRails2.md +48 -0
- data/bin/parallel_cucumber +2 -0
- data/bin/parallel_rspec +2 -0
- data/bin/parallel_test +6 -0
- data/lib/parallel_tests.rb +30 -0
- data/lib/parallel_tests/cli.rb +159 -0
- data/lib/parallel_tests/cucumber/gherkin_listener.rb +60 -0
- data/lib/parallel_tests/cucumber/runner.rb +90 -0
- data/lib/parallel_tests/cucumber/runtime_logger.rb +58 -0
- data/lib/parallel_tests/grouper.rb +53 -0
- data/lib/parallel_tests/railtie.rb +8 -0
- data/lib/parallel_tests/rspec/failures_logger.rb +44 -0
- data/lib/parallel_tests/rspec/logger_base.rb +52 -0
- data/lib/parallel_tests/rspec/runner.rb +59 -0
- data/lib/parallel_tests/rspec/runtime_logger.rb +34 -0
- data/lib/parallel_tests/rspec/summary_logger.rb +19 -0
- data/lib/parallel_tests/tasks.rb +134 -0
- data/lib/parallel_tests/test/runner.rb +134 -0
- data/lib/parallel_tests/test/runtime_logger.rb +92 -0
- data/lib/parallel_tests/version.rb +3 -0
- data/parallel_tests.gemspec +14 -0
- data/spec/integration_spec.rb +244 -0
- data/spec/parallel_tests/cli_spec.rb +36 -0
- data/spec/parallel_tests/cucumber/gherkin_listener_spec.rb +48 -0
- data/spec/parallel_tests/cucumber/runner_spec.rb +173 -0
- data/spec/parallel_tests/grouper_spec.rb +52 -0
- data/spec/parallel_tests/rspec/failure_logger_spec.rb +82 -0
- data/spec/parallel_tests/rspec/runner_spec.rb +178 -0
- data/spec/parallel_tests/rspec/runtime_logger_spec.rb +76 -0
- data/spec/parallel_tests/rspec/summary_logger_spec.rb +37 -0
- data/spec/parallel_tests/tasks_spec.rb +151 -0
- data/spec/parallel_tests/test/runner_spec.rb +273 -0
- data/spec/parallel_tests/test/runtime_logger_spec.rb +84 -0
- data/spec/parallel_tests_spec.rb +73 -0
- data/spec/spec_helper.rb +151 -0
- metadata +109 -0
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'parallel_tests/rspec/failures_logger'
|
2
|
+
|
3
|
+
class ParallelTests::RSpec::SummaryLogger < ParallelTests::RSpec::LoggerBase
|
4
|
+
# RSpec 1: dumps 1 failed spec
|
5
|
+
def dump_failure(*args)
|
6
|
+
lock_output do
|
7
|
+
super
|
8
|
+
end
|
9
|
+
@output.flush
|
10
|
+
end
|
11
|
+
|
12
|
+
# RSpec 2: dumps all failed specs
|
13
|
+
def dump_failures(*args)
|
14
|
+
lock_output do
|
15
|
+
super
|
16
|
+
end
|
17
|
+
@output.flush
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,134 @@
|
|
1
|
+
require 'rake'
|
2
|
+
|
3
|
+
module ParallelTests
|
4
|
+
module Tasks
|
5
|
+
class << self
|
6
|
+
def rails_env
|
7
|
+
ENV['RAILS_ENV'] || 'test'
|
8
|
+
end
|
9
|
+
|
10
|
+
def run_in_parallel(cmd, options={})
|
11
|
+
count = " -n #{options[:count]}" if options[:count]
|
12
|
+
executable = File.expand_path("../../../bin/parallel_test", __FILE__)
|
13
|
+
command = "#{executable} --exec '#{cmd}'#{count}#{' --non-parallel' if options[:non_parallel]}"
|
14
|
+
command << " --advance-number #{options[:advance_number]}" if options[:advance_number]
|
15
|
+
abort unless system(command)
|
16
|
+
end
|
17
|
+
|
18
|
+
# this is a crazy-complex solution for a very simple problem:
|
19
|
+
# removing certain lines from the output without chaning the exit-status
|
20
|
+
# normally I'd not do this, but it has been lots of fun and a great learning experience :)
|
21
|
+
#
|
22
|
+
# - sed does not support | without -r
|
23
|
+
# - grep changes 0 exitstatus to 1 if nothing matches
|
24
|
+
# - sed changes 1 exitstatus to 0
|
25
|
+
# - pipefail makes pipe fail with exitstatus of first failed command
|
26
|
+
# - pipefail is not supported in (zsh)
|
27
|
+
# - defining a new rake task like silence_schema would force users to load parallel_tests in test env
|
28
|
+
# - do not use ' since run_in_parallel uses them to quote stuff
|
29
|
+
# - simple system "set -o pipefail" returns nil even though set -o pipefail exists with 0
|
30
|
+
def suppress_output(command, ignore_regex)
|
31
|
+
activate_pipefail = "set -o pipefail"
|
32
|
+
remove_ignored_lines = %Q{(grep -v "#{ignore_regex}" || test 1)}
|
33
|
+
|
34
|
+
if system("#{activate_pipefail} && test 1")
|
35
|
+
"#{activate_pipefail} && (#{command}) | #{remove_ignored_lines}"
|
36
|
+
else
|
37
|
+
command
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def check_for_pending_migrations
|
42
|
+
abort_migrations = "db:abort_if_pending_migrations"
|
43
|
+
if Rake::Task.task_defined?(abort_migrations)
|
44
|
+
Rake::Task[abort_migrations].invoke
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# parallel:spec[:count, :pattern, :options]
|
49
|
+
def parse_args(args)
|
50
|
+
# order as given by user
|
51
|
+
args = [args[:count], args[:pattern], args[:options]]
|
52
|
+
|
53
|
+
# count given or empty ?
|
54
|
+
# parallel:spec[2,models,options]
|
55
|
+
# parallel:spec[,models,options]
|
56
|
+
count = args.shift if args.first.to_s =~ /^\d*$/
|
57
|
+
num_processes = count.to_i unless count.to_s.empty?
|
58
|
+
pattern = args.shift
|
59
|
+
options = args.shift
|
60
|
+
|
61
|
+
[num_processes, pattern.to_s, options.to_s]
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
namespace :parallel do
|
68
|
+
desc "create test databases via db:create --> parallel:create[num_cpus]"
|
69
|
+
task :create, :count do |t,args|
|
70
|
+
ParallelTests::Tasks.run_in_parallel("rake db:create RAILS_ENV=#{ParallelTests::Tasks.rails_env}", args)
|
71
|
+
end
|
72
|
+
|
73
|
+
desc "drop test databases via db:drop --> parallel:drop[num_cpus]"
|
74
|
+
task :drop, :count do |t,args|
|
75
|
+
ParallelTests::Tasks.run_in_parallel("rake db:drop RAILS_ENV=#{ParallelTests::Tasks.rails_env}", args)
|
76
|
+
end
|
77
|
+
|
78
|
+
desc "update test databases by dumping and loading --> parallel:prepare[num_cpus]"
|
79
|
+
task(:prepare, [:count]) do |t,args|
|
80
|
+
ParallelTests::Tasks.check_for_pending_migrations
|
81
|
+
if defined?(ActiveRecord) && ActiveRecord::Base.schema_format == :ruby
|
82
|
+
# dump then load in parallel
|
83
|
+
Rake::Task['db:schema:dump'].invoke
|
84
|
+
Rake::Task['parallel:load_schema'].invoke(args[:count])
|
85
|
+
else
|
86
|
+
# there is no separate dump / load for schema_format :sql -> do it safe and slow
|
87
|
+
args = args.to_hash.merge(:non_parallel => true) # normal merge returns nil
|
88
|
+
ParallelTests::Tasks.run_in_parallel('rake db:test:prepare --trace', args)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
# when dumping/resetting takes too long
|
93
|
+
desc "update test databases via db:migrate --> parallel:migrate[num_cpus]"
|
94
|
+
task :migrate, :count do |t,args|
|
95
|
+
ParallelTests::Tasks.run_in_parallel("rake db:migrate RAILS_ENV=#{ParallelTests::Tasks.rails_env}", args)
|
96
|
+
end
|
97
|
+
|
98
|
+
# just load the schema (good for integration server <-> no development db)
|
99
|
+
desc "load dumped schema for test databases via db:schema:load --> parallel:load_schema[num_cpus]"
|
100
|
+
task :load_schema, :count do |t,args|
|
101
|
+
command = "rake db:schema:load RAILS_ENV=#{ParallelTests::Tasks.rails_env}"
|
102
|
+
ParallelTests::Tasks.run_in_parallel(ParallelTests::Tasks.suppress_output(command, "^ ->\\|^-- "), args)
|
103
|
+
end
|
104
|
+
|
105
|
+
desc "load the seed data from db/seeds.rb via db:seed --> parallel:seed[num_cpus]"
|
106
|
+
task :seed, :count do |t,args|
|
107
|
+
ParallelTests::Tasks.run_in_parallel("rake db:seed RAILS_ENV=#{ParallelTests::Tasks.rails_env}", args)
|
108
|
+
end
|
109
|
+
|
110
|
+
['test', 'spec', 'features'].each do |type|
|
111
|
+
desc "run #{type} in parallel with parallel:#{type}[num_cpus]"
|
112
|
+
task type, [:count, :pattern, :options] do |t, args|
|
113
|
+
ParallelTests::Tasks.check_for_pending_migrations
|
114
|
+
|
115
|
+
$LOAD_PATH << File.expand_path(File.join(File.dirname(__FILE__), '..'))
|
116
|
+
require "parallel_tests"
|
117
|
+
|
118
|
+
count, pattern, options = ParallelTests::Tasks.parse_args(args)
|
119
|
+
test_framework = {
|
120
|
+
'spec' => 'rspec',
|
121
|
+
'test' => 'test',
|
122
|
+
'features' => 'cucumber'
|
123
|
+
}[type]
|
124
|
+
|
125
|
+
executable = File.join(File.dirname(__FILE__), '..', '..', 'bin', 'parallel_test')
|
126
|
+
command = "#{executable} #{type} --type #{test_framework} " \
|
127
|
+
"-n #{count} " \
|
128
|
+
"--pattern '#{pattern}' " \
|
129
|
+
"--test-options '#{options}'"
|
130
|
+
|
131
|
+
abort unless system(command) # allow to chain tasks e.g. rake parallel:spec parallel:features
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
@@ -0,0 +1,134 @@
|
|
1
|
+
module ParallelTests
|
2
|
+
module Test
|
3
|
+
class Runner
|
4
|
+
# --- usually overwritten by other runners
|
5
|
+
|
6
|
+
def self.runtime_log
|
7
|
+
'tmp/parallel_runtime_test.log'
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.test_suffix
|
11
|
+
"_test.rb"
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.test_file_name
|
15
|
+
"test"
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.run_tests(test_files, process_number, options)
|
19
|
+
require_list = test_files.map { |filename| %{"#{File.expand_path filename}"} }.join(",")
|
20
|
+
cmd = "ruby -Itest -e '[#{require_list}].each {|f| require f }' -- #{options[:test_options]}"
|
21
|
+
execute_command(cmd, process_number, options)
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.line_is_result?(line)
|
25
|
+
line =~ /\d+ failure/
|
26
|
+
end
|
27
|
+
|
28
|
+
# --- usually used by other runners
|
29
|
+
|
30
|
+
# finds all tests and partitions them into groups
|
31
|
+
def self.tests_in_groups(tests, num_groups, options={})
|
32
|
+
tests = find_tests(tests, options)
|
33
|
+
|
34
|
+
tests = if options[:group_by] == :found
|
35
|
+
tests.map { |t| [t, 1] }
|
36
|
+
else
|
37
|
+
with_runtime_info(tests)
|
38
|
+
end
|
39
|
+
Grouper.in_even_groups_by_size(tests, num_groups, options)
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.execute_command(cmd, process_number, options)
|
43
|
+
cmd = "TEST_ENV_NUMBER=#{test_env_number(process_number, options)} ; export TEST_ENV_NUMBER; #{cmd}"
|
44
|
+
f = open("|#{cmd}", 'r')
|
45
|
+
output = fetch_output(f)
|
46
|
+
f.close
|
47
|
+
{:stdout => output, :exit_status => $?.exitstatus}
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.find_results(test_output)
|
51
|
+
test_output.split("\n").map {|line|
|
52
|
+
line = line.gsub(/\.|F|\*/,'').gsub(/\e\[\d+m/,'')
|
53
|
+
next unless line_is_result?(line)
|
54
|
+
line
|
55
|
+
}.compact
|
56
|
+
end
|
57
|
+
|
58
|
+
def self.test_env_number(process_number, options)
|
59
|
+
n = options[:advance_number].to_i + process_number + 1
|
60
|
+
n == 0 ? '' : n
|
61
|
+
end
|
62
|
+
|
63
|
+
def self.summarize_results(results)
|
64
|
+
sums = sum_up_results(results)
|
65
|
+
sums.sort.map{|word, number| "#{number} #{word}#{'s' if number != 1}" }.join(', ')
|
66
|
+
end
|
67
|
+
|
68
|
+
protected
|
69
|
+
|
70
|
+
def self.sum_up_results(results)
|
71
|
+
results = results.join(' ').gsub(/s\b/,'') # combine and singularize results
|
72
|
+
counts = results.scan(/(\d+) (\w+)/)
|
73
|
+
sums = counts.inject(Hash.new(0)) do |sum, (number, word)|
|
74
|
+
sum[word] += number.to_i
|
75
|
+
sum
|
76
|
+
end
|
77
|
+
|
78
|
+
sums
|
79
|
+
end
|
80
|
+
|
81
|
+
# read output of the process and print it in chunks
|
82
|
+
def self.fetch_output(process)
|
83
|
+
all = ''
|
84
|
+
while buffer = process.readpartial(1000000)
|
85
|
+
all << buffer
|
86
|
+
$stdout.print buffer
|
87
|
+
$stdout.flush
|
88
|
+
end rescue EOFError
|
89
|
+
|
90
|
+
all
|
91
|
+
end
|
92
|
+
|
93
|
+
def self.with_runtime_info(tests)
|
94
|
+
lines = File.read(runtime_log).split("\n") rescue []
|
95
|
+
|
96
|
+
# use recorded test runtime if we got enough data
|
97
|
+
if lines.size * 1.5 > tests.size
|
98
|
+
puts "Using recorded test runtime"
|
99
|
+
times = Hash.new(1)
|
100
|
+
lines.each do |line|
|
101
|
+
test, time = line.split(":")
|
102
|
+
next unless test and time
|
103
|
+
times[File.expand_path(test)] = time.to_f
|
104
|
+
end
|
105
|
+
tests.sort.map{|test| [test, times[File.expand_path(test)]] }
|
106
|
+
else # use file sizes
|
107
|
+
tests.sort.map{|test| [test, File.stat(test).size] }
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def self.find_tests(tests, options = {})
|
112
|
+
(tests || []).map do |file_or_folder|
|
113
|
+
if File.directory?(file_or_folder)
|
114
|
+
files = files_in_folder(file_or_folder, options)
|
115
|
+
files.grep(/#{Regexp.escape test_suffix}$/).grep(options[:pattern]||//)
|
116
|
+
else
|
117
|
+
file_or_folder
|
118
|
+
end
|
119
|
+
end.flatten.uniq
|
120
|
+
end
|
121
|
+
|
122
|
+
def self.files_in_folder(folder, options={})
|
123
|
+
pattern = if options[:symlinks] == false # not nil or true
|
124
|
+
"**/*"
|
125
|
+
else
|
126
|
+
# follow one symlink and direct children
|
127
|
+
# http://stackoverflow.com/questions/357754/can-i-traverse-symlinked-directories-in-ruby-with-a-glob
|
128
|
+
"**{,/*/**}/*"
|
129
|
+
end
|
130
|
+
Dir[File.join(folder, pattern)].uniq
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
require 'parallel_tests/test/runner'
|
2
|
+
|
3
|
+
module ParallelTests
|
4
|
+
module Test
|
5
|
+
class RuntimeLogger
|
6
|
+
@@has_started = false
|
7
|
+
|
8
|
+
def self.log(test, start_time, end_time)
|
9
|
+
return if test.is_a? ::Test::Unit::TestSuite # don't log for suites-of-suites
|
10
|
+
|
11
|
+
if !@@has_started # make empty log file
|
12
|
+
File.open(logfile, 'w'){}
|
13
|
+
@@has_started = true
|
14
|
+
end
|
15
|
+
|
16
|
+
locked_appending_to(logfile) do |file|
|
17
|
+
file.puts(message(test, start_time, end_time))
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.message(test, start_time, end_time)
|
22
|
+
delta = "%.2f" % (end_time.to_f-start_time.to_f)
|
23
|
+
filename = class_directory(test.class) + class_to_filename(test.class) + ".rb"
|
24
|
+
"#{filename}:#{delta}"
|
25
|
+
end
|
26
|
+
|
27
|
+
# Note: this is a best guess at conventional test directory structure, and may need
|
28
|
+
# tweaking / post-processing to match correctly for any given project
|
29
|
+
def self.class_directory(suspect)
|
30
|
+
result = "test/"
|
31
|
+
|
32
|
+
if defined?(Rails)
|
33
|
+
result += case suspect.superclass.name
|
34
|
+
when "ActionDispatch::IntegrationTest"
|
35
|
+
"integration/"
|
36
|
+
when "ActionDispatch::PerformanceTest"
|
37
|
+
"performance/"
|
38
|
+
when "ActionController::TestCase"
|
39
|
+
"functional/"
|
40
|
+
when "ActionView::TestCase"
|
41
|
+
"unit/helpers/"
|
42
|
+
else
|
43
|
+
"unit/"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
result
|
47
|
+
end
|
48
|
+
|
49
|
+
# based on https://github.com/grosser/single_test/blob/master/lib/single_test.rb#L117
|
50
|
+
def self.class_to_filename(suspect)
|
51
|
+
word = suspect.to_s.dup
|
52
|
+
return word unless word.match /^[A-Z]/ and not word.match %r{/[a-z]}
|
53
|
+
|
54
|
+
word.gsub!(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
55
|
+
word.gsub!(/([a-z\d])([A-Z])/, '\1_\2')
|
56
|
+
word.gsub!(/\:\:/, '/')
|
57
|
+
word.tr!("-", "_")
|
58
|
+
word.downcase!
|
59
|
+
word
|
60
|
+
end
|
61
|
+
|
62
|
+
def self.locked_appending_to(file)
|
63
|
+
File.open(file, 'a') do |f|
|
64
|
+
begin
|
65
|
+
f.flock File::LOCK_EX
|
66
|
+
yield f
|
67
|
+
ensure
|
68
|
+
f.flock File::LOCK_UN
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def self.logfile
|
74
|
+
ParallelTests::Test::Runner.runtime_log
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
require 'test/unit/testsuite'
|
81
|
+
class ::Test::Unit::TestSuite
|
82
|
+
alias :run_without_timing :run unless defined? @@timing_installed
|
83
|
+
|
84
|
+
def run(result, &progress_block)
|
85
|
+
start_time=Time.now
|
86
|
+
run_without_timing(result, &progress_block)
|
87
|
+
end_time=Time.now
|
88
|
+
ParallelTests::Test::RuntimeLogger.log(self.tests.first, start_time, end_time)
|
89
|
+
end
|
90
|
+
|
91
|
+
@@timing_installed = true
|
92
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
$LOAD_PATH.unshift File.expand_path("../lib", __FILE__)
|
2
|
+
name = "friendlyfashion-parallel_tests"
|
3
|
+
require "parallel_tests/version"
|
4
|
+
|
5
|
+
Gem::Specification.new name, ParallelTests::VERSION do |s|
|
6
|
+
s.summary = "Run Test::Unit / RSpec / Cucumber in parallel"
|
7
|
+
s.authors = ["Laurynas Butkus", "Tomas Varaneckas", "Justas Janauskas"]
|
8
|
+
s.email = ["laurynas.butkus@gmail.com", "tomas.varaneckas@gmail.com", "jjanauskas@gmail.com"]
|
9
|
+
s.homepage = "http://github.com/friendlyfashion/parallel_tests"
|
10
|
+
s.files = `git ls-files`.split("\n")
|
11
|
+
s.license = "MIT"
|
12
|
+
s.executables = ["parallel_cucumber", "parallel_rspec", "parallel_test"]
|
13
|
+
s.add_runtime_dependency "parallel"
|
14
|
+
end
|
@@ -0,0 +1,244 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe 'CLI' do
|
4
|
+
before do
|
5
|
+
`rm -rf #{folder}`
|
6
|
+
end
|
7
|
+
|
8
|
+
after do
|
9
|
+
`rm -rf #{folder}`
|
10
|
+
end
|
11
|
+
|
12
|
+
def folder
|
13
|
+
"/tmp/parallel_tests_tests"
|
14
|
+
end
|
15
|
+
|
16
|
+
def write(file, content)
|
17
|
+
path = "#{folder}/#{file}"
|
18
|
+
ensure_folder File.dirname(path)
|
19
|
+
File.open(path, 'w'){|f| f.write content }
|
20
|
+
path
|
21
|
+
end
|
22
|
+
|
23
|
+
def read(file)
|
24
|
+
File.read "#{folder}/#{file}"
|
25
|
+
end
|
26
|
+
|
27
|
+
def bin_folder
|
28
|
+
"#{File.expand_path(File.dirname(__FILE__))}/../bin"
|
29
|
+
end
|
30
|
+
|
31
|
+
def executable(options={})
|
32
|
+
"#{bin_folder}/parallel_#{options[:type] || 'test'}"
|
33
|
+
end
|
34
|
+
|
35
|
+
def ensure_folder(folder)
|
36
|
+
`mkdir -p #{folder}` unless File.exist?(folder)
|
37
|
+
end
|
38
|
+
|
39
|
+
def run_tests(test_folder, options={})
|
40
|
+
ensure_folder folder
|
41
|
+
processes = "-n #{options[:processes]||2}" unless options[:processes] == false
|
42
|
+
command = "cd #{folder} && #{options[:export]} #{executable(options)} #{test_folder} #{processes} #{options[:add]} 2>&1"
|
43
|
+
result = `#{command}`
|
44
|
+
raise "FAILED #{command}\n#{result}" if $?.success? == !!options[:fail]
|
45
|
+
result
|
46
|
+
end
|
47
|
+
|
48
|
+
it "runs tests in parallel" do
|
49
|
+
write 'spec/xxx_spec.rb', 'describe("it"){it("should"){puts "TEST1"}}'
|
50
|
+
write 'spec/xxx2_spec.rb', 'describe("it"){it("should"){puts "TEST2"}}'
|
51
|
+
result = run_tests "spec", :type => 'rspec'
|
52
|
+
|
53
|
+
# test ran and gave their puts
|
54
|
+
result.should include('TEST1')
|
55
|
+
result.should include('TEST2')
|
56
|
+
|
57
|
+
# all results present
|
58
|
+
result.scan('1 example, 0 failure').size.should == 2 # 2 results
|
59
|
+
result.scan('2 examples, 0 failures').size.should == 1 # 1 summary
|
60
|
+
result.scan(/Finished in \d+\.\d+ seconds/).size.should == 2
|
61
|
+
result.scan(/Took \d+\.\d+ seconds/).size.should == 1 # parallel summary
|
62
|
+
end
|
63
|
+
|
64
|
+
it "does not run any tests if there are none" do
|
65
|
+
write 'spec/xxx_spec.rb', '1'
|
66
|
+
result = run_tests "spec", :type => 'rspec'
|
67
|
+
result.should include('No examples found')
|
68
|
+
result.should include('Took')
|
69
|
+
end
|
70
|
+
|
71
|
+
it "fails when tests fail" do
|
72
|
+
write 'spec/xxx_spec.rb', 'describe("it"){it("should"){puts "TEST1"}}'
|
73
|
+
write 'spec/xxx2_spec.rb', 'describe("it"){it("should"){1.should == 2}}'
|
74
|
+
result = run_tests "spec", :fail => true, :type => 'rspec'
|
75
|
+
|
76
|
+
result.scan('1 example, 1 failure').size.should == 1
|
77
|
+
result.scan('1 example, 0 failure').size.should == 1
|
78
|
+
result.scan('2 examples, 1 failure').size.should == 1
|
79
|
+
end
|
80
|
+
|
81
|
+
context "with given commands" do
|
82
|
+
it "can exec given commands with ENV['TEST_ENV_NUM']" do
|
83
|
+
result = `#{executable} -e 'ruby -e "print ENV[:TEST_ENV_NUMBER.to_s].to_i"' -n 4`
|
84
|
+
result.gsub('"','').split('').sort.should == %w[0 2 3 4]
|
85
|
+
end
|
86
|
+
|
87
|
+
it "can exec given command non-parallel" do
|
88
|
+
result = `#{executable} -e 'ruby -e "sleep(rand(10)/100.0); puts ENV[:TEST_ENV_NUMBER.to_s].inspect"' -n 4 --non-parallel`
|
89
|
+
result.split("\n").should == %w["" "2" "3" "4"]
|
90
|
+
end
|
91
|
+
|
92
|
+
it "exists with success if all sub-processes returned success" do
|
93
|
+
system("#{executable} -e 'cat /dev/null' -n 4").should == true
|
94
|
+
end
|
95
|
+
|
96
|
+
it "exists with failure if any sub-processes returned failure" do
|
97
|
+
system("#{executable} -e 'test -e xxxx' -n 4").should == false
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
it "runs through parallel_rspec" do
|
102
|
+
version = `#{executable} -v`
|
103
|
+
`#{bin_folder}/parallel_rspec -v`.should == version
|
104
|
+
end
|
105
|
+
|
106
|
+
it "runs through parallel_cucumber" do
|
107
|
+
version = `#{executable} -v`
|
108
|
+
`#{bin_folder}/parallel_cucumber -v`.should == version
|
109
|
+
end
|
110
|
+
|
111
|
+
it "runs with --group-by found" do
|
112
|
+
# it only tests that it does not blow up, as it did before fixing...
|
113
|
+
write "spec/x1_spec.rb", "puts '111'"
|
114
|
+
run_tests "spec", :type => 'rspec', :add => '--group-by found'
|
115
|
+
end
|
116
|
+
|
117
|
+
it "runs faster with more processes" do
|
118
|
+
2.times{|i|
|
119
|
+
write "spec/xxx#{i}_spec.rb", 'describe("it"){it("should"){sleep 5}}; $stderr.puts ENV["TEST_ENV_NUMBER"]'
|
120
|
+
}
|
121
|
+
t = Time.now
|
122
|
+
run_tests("spec", :processes => 2, :type => 'rspec')
|
123
|
+
expected = 10
|
124
|
+
(Time.now - t).should <= expected
|
125
|
+
end
|
126
|
+
|
127
|
+
it "can run with given files" do
|
128
|
+
write "spec/x1_spec.rb", "puts '111'"
|
129
|
+
write "spec/x2_spec.rb", "puts '222'"
|
130
|
+
write "spec/x3_spec.rb", "puts '333'"
|
131
|
+
result = run_tests "spec/x1_spec.rb spec/x3_spec.rb", :type => 'rspec'
|
132
|
+
result.should include('111')
|
133
|
+
result.should include('333')
|
134
|
+
result.should_not include('222')
|
135
|
+
end
|
136
|
+
|
137
|
+
it "runs successfully without any files" do
|
138
|
+
results = run_tests "", :type => 'rspec'
|
139
|
+
results.should include("2 processes for 0 specs")
|
140
|
+
results.should include("Took")
|
141
|
+
end
|
142
|
+
|
143
|
+
it "can run with test-options" do
|
144
|
+
write "spec/x1_spec.rb", "111"
|
145
|
+
write "spec/x2_spec.rb", "111"
|
146
|
+
result = run_tests "spec",
|
147
|
+
:add => "--test-options ' --version'",
|
148
|
+
:processes => 2,
|
149
|
+
:type => 'rspec'
|
150
|
+
result.should =~ /\d+\.\d+\.\d+.*\d+\.\d+\.\d+/m # prints version twice
|
151
|
+
end
|
152
|
+
|
153
|
+
it "runs with PARALLEL_TEST_PROCESSORS processes" do
|
154
|
+
processes = 5
|
155
|
+
processes.times{|i|
|
156
|
+
write "spec/x#{i}_spec.rb", "puts %{ENV-\#{ENV['TEST_ENV_NUMBER']}-}"
|
157
|
+
}
|
158
|
+
result = run_tests "spec",
|
159
|
+
:export => "PARALLEL_TEST_PROCESSORS=#{processes}",
|
160
|
+
:processes => processes,
|
161
|
+
:type => 'rspec'
|
162
|
+
result.scan(/ENV-.?-/).should =~ ["ENV--", "ENV-2-", "ENV-3-", "ENV-4-", "ENV-5-"]
|
163
|
+
end
|
164
|
+
|
165
|
+
it "filters test by given pattern and relative paths" do
|
166
|
+
write "spec/x_spec.rb", "puts 'XXX'"
|
167
|
+
write "spec/y_spec.rb", "puts 'YYY'"
|
168
|
+
write "spec/z_spec.rb", "puts 'ZZZ'"
|
169
|
+
result = run_tests "spec", :add => "-p '^spec/(x|z)'", :type => "rspec"
|
170
|
+
result.should include('XXX')
|
171
|
+
result.should_not include('YYY')
|
172
|
+
result.should include('ZZZ')
|
173
|
+
end
|
174
|
+
|
175
|
+
context "Test::Unit" do
|
176
|
+
it "runs" do
|
177
|
+
write "test/x1_test.rb", "require 'test/unit'; class XTest < Test::Unit::TestCase; def test_xxx; end; end"
|
178
|
+
result = run_tests("test")
|
179
|
+
result.should include('1 test')
|
180
|
+
end
|
181
|
+
|
182
|
+
it "passes test options" do
|
183
|
+
write "test/x1_test.rb", "require 'test/unit'; class XTest < Test::Unit::TestCase; def test_xxx; end; end"
|
184
|
+
result = run_tests("test", :add => '--test-options "-v"')
|
185
|
+
result.should include('test_xxx') # verbose output of every test
|
186
|
+
end
|
187
|
+
|
188
|
+
it "runs successfully without any files" do
|
189
|
+
results = run_tests("")
|
190
|
+
results.should include("2 processes for 0 tests")
|
191
|
+
results.should include("Took")
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
context "Cucumber" do
|
196
|
+
before do
|
197
|
+
write "features/steps/a.rb", "
|
198
|
+
Given('I print TEST_ENV_NUMBER'){ puts \"YOUR TEST ENV IS \#{ENV['TEST_ENV_NUMBER']}!\" }
|
199
|
+
And('I sleep a bit'){ sleep 0.2 }
|
200
|
+
"
|
201
|
+
end
|
202
|
+
|
203
|
+
it "passes TEST_ENV_NUMBER when running with pattern (issue #86)" do
|
204
|
+
write "features/good1.feature", "Feature: xxx\n Scenario: xxx\n Given I print TEST_ENV_NUMBER"
|
205
|
+
write "features/good2.feature", "Feature: xxx\n Scenario: xxx\n Given I print TEST_ENV_NUMBER"
|
206
|
+
write "features/b.feature", "Feature: xxx\n Scenario: xxx\n Given I FAIL"
|
207
|
+
write "features/steps/a.rb", "Given('I print TEST_ENV_NUMBER'){ puts \"YOUR TEST ENV IS \#{ENV['TEST_ENV_NUMBER']}!\" }"
|
208
|
+
|
209
|
+
result = run_tests "features", :type => "cucumber", :add => '--pattern good'
|
210
|
+
|
211
|
+
result.should include('YOUR TEST ENV IS 2!')
|
212
|
+
result.should include('YOUR TEST ENV IS !')
|
213
|
+
result.should_not include('I FAIL')
|
214
|
+
end
|
215
|
+
|
216
|
+
it "writes a runtime log" do
|
217
|
+
log = "tmp/parallel_runtime_cucumber.log"
|
218
|
+
write(log, "x")
|
219
|
+
2.times{|i|
|
220
|
+
# needs sleep so that runtime loggers dont overwrite each other initially
|
221
|
+
write "features/good#{i}.feature", "Feature: xxx\n Scenario: xxx\n Given I print TEST_ENV_NUMBER\n And I sleep a bit"
|
222
|
+
}
|
223
|
+
run_tests "features", :type => "cucumber"
|
224
|
+
read(log).gsub(/\.\d+/,'').split("\n").should =~ [
|
225
|
+
"features/good0.feature:0",
|
226
|
+
"features/good1.feature:0"
|
227
|
+
]
|
228
|
+
end
|
229
|
+
|
230
|
+
it "runs each feature once when there are more processes then features (issue #89)" do
|
231
|
+
2.times{|i|
|
232
|
+
write "features/good#{i}.feature", "Feature: xxx\n Scenario: xxx\n Given I print TEST_ENV_NUMBER"
|
233
|
+
}
|
234
|
+
result = run_tests "features", :type => "cucumber", :add => '-n 3'
|
235
|
+
result.scan(/YOUR TEST ENV IS \d?!/).sort.should == ["YOUR TEST ENV IS !", "YOUR TEST ENV IS 2!"]
|
236
|
+
end
|
237
|
+
|
238
|
+
it "runs successfully without any files" do
|
239
|
+
results = run_tests("", :type => "cucumber")
|
240
|
+
results.should include("2 processes for 0 features")
|
241
|
+
results.should include("Took")
|
242
|
+
end
|
243
|
+
end
|
244
|
+
end
|