friendlyfashion-parallel_tests 0.9.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 +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
|