parallelized_specs 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +10 -0
- data/Gemfile.lock +30 -0
- data/Rakefile +21 -0
- data/Readme.md +240 -0
- data/VERSION +1 -0
- data/bin/parallel_spec +2 -0
- data/bin/parallel_test +97 -0
- data/lib/parallel_specs/spec_error_count_logger.rb +30 -0
- data/lib/parallel_specs/spec_error_logger.rb +45 -0
- data/lib/parallel_specs/spec_failures_logger.rb +43 -0
- data/lib/parallel_specs/spec_logger_base.rb +56 -0
- data/lib/parallel_specs/spec_runtime_logger.rb +34 -0
- data/lib/parallel_specs/spec_start_finish_logger.rb +38 -0
- data/lib/parallel_specs/spec_summary_logger.rb +19 -0
- data/lib/parallel_specs.rb +52 -0
- data/lib/parallel_tests/grouper.rb +49 -0
- data/lib/parallel_tests/railtie.rb +10 -0
- data/lib/parallel_tests/runtime_logger.rb +78 -0
- data/lib/parallel_tests/tasks.rb +80 -0
- data/lib/parallel_tests.rb +163 -0
- data/lib/tasks/parallel_tests.rake +1 -0
- data/parallelized_specs.gemspec +64 -0
- data/spec/integration_spec.rb +133 -0
- data/spec/parallel_specs/spec_failure_logger_spec.rb +82 -0
- data/spec/parallel_specs/spec_runtime_logger_spec.rb +76 -0
- data/spec/parallel_specs/spec_summary_logger_spec.rb +33 -0
- data/spec/parallel_specs_spec.rb +165 -0
- data/spec/parallel_tests/runtime_logger_spec.rb +74 -0
- data/spec/parallel_tests_spec.rb +229 -0
- data/spec/spec_helper.rb +149 -0
- metadata +109 -0
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'parallel_specs/spec_logger_base'
|
2
|
+
|
3
|
+
class ParallelSpecs::SpecFailuresLogger < ParallelSpecs::SpecLoggerBase
|
4
|
+
# RSpec 1: does not keep track of failures, so we do
|
5
|
+
def example_failed(example, *args)
|
6
|
+
if RSPEC_1
|
7
|
+
@failed_examples ||= []
|
8
|
+
@failed_examples << example
|
9
|
+
else
|
10
|
+
super
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
# RSpec 1: dumps 1 failed spec
|
15
|
+
def dump_failure(*args)
|
16
|
+
end
|
17
|
+
|
18
|
+
# RSpec 2: dumps all failed specs
|
19
|
+
def dump_failures(*args)
|
20
|
+
end
|
21
|
+
|
22
|
+
def dump_summary(*args)
|
23
|
+
lock_output do
|
24
|
+
if RSPEC_1
|
25
|
+
dump_commands_to_rerun_failed_examples_rspec_1
|
26
|
+
else
|
27
|
+
dump_commands_to_rerun_failed_examples
|
28
|
+
end
|
29
|
+
end
|
30
|
+
@output.flush
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def dump_commands_to_rerun_failed_examples_rspec_1
|
36
|
+
(@failed_examples||[]).each do |example|
|
37
|
+
file, line = example.location.to_s.split(':')
|
38
|
+
next unless file and line
|
39
|
+
file.gsub!(%r(^.*?/spec/), './spec/')
|
40
|
+
@output.puts "#{ParallelSpecs.executable} #{file}:#{line} # #{example.description}"
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'parallel_specs'
|
2
|
+
|
3
|
+
begin
|
4
|
+
require 'rspec/core/formatters/base_text_formatter'
|
5
|
+
base = RSpec::Core::Formatters::BaseTextFormatter
|
6
|
+
rescue LoadError
|
7
|
+
require 'spec/runner/formatter/base_text_formatter'
|
8
|
+
base = Spec::Runner::Formatter::BaseTextFormatter
|
9
|
+
end
|
10
|
+
ParallelSpecs::SpecLoggerBaseBase = base
|
11
|
+
|
12
|
+
class ParallelSpecs::SpecLoggerBase < ParallelSpecs::SpecLoggerBaseBase
|
13
|
+
RSPEC_1 = !defined?(RSpec::Core::Formatters::BaseTextFormatter) # do not test for Spec, this will trigger deprecation warning in rspec 2
|
14
|
+
|
15
|
+
def initialize(*args)
|
16
|
+
super
|
17
|
+
|
18
|
+
@output ||= args[1] || args[0] # rspec 1 has output as second argument
|
19
|
+
|
20
|
+
if String === @output # a path ?
|
21
|
+
FileUtils.mkdir_p(File.dirname(@output))
|
22
|
+
File.open(@output, 'w'){} # overwrite previous results
|
23
|
+
@output = File.open(@output, 'a')
|
24
|
+
elsif File === @output # close and restart in append mode
|
25
|
+
@output.close
|
26
|
+
@output = File.open(@output.path, 'a')
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def dump_summary(*args);end
|
31
|
+
|
32
|
+
def dump_failures(*args);end
|
33
|
+
|
34
|
+
def dump_failure(*args);end
|
35
|
+
|
36
|
+
def dump_pending(*args);end
|
37
|
+
|
38
|
+
#stolen from Rspec
|
39
|
+
def close
|
40
|
+
@output.close if (IO === @output) & (@output != $stdout)
|
41
|
+
end
|
42
|
+
|
43
|
+
# do not let multiple processes get in each others way
|
44
|
+
def lock_output
|
45
|
+
if File === @output
|
46
|
+
begin
|
47
|
+
@output.flock File::LOCK_EX
|
48
|
+
yield
|
49
|
+
ensure
|
50
|
+
@output.flock File::LOCK_UN
|
51
|
+
end
|
52
|
+
else
|
53
|
+
yield
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'parallel_specs/spec_logger_base'
|
2
|
+
|
3
|
+
class ParallelSpecs::SpecRuntimeLogger < ParallelSpecs::SpecLoggerBase
|
4
|
+
def initialize(*args)
|
5
|
+
super
|
6
|
+
@example_times = Hash.new(0)
|
7
|
+
end
|
8
|
+
|
9
|
+
def example_started(*args)
|
10
|
+
@time = Time.now
|
11
|
+
end
|
12
|
+
|
13
|
+
def example_passed(example)
|
14
|
+
file = example.location.split(':').first
|
15
|
+
@example_times[file] += Time.now - @time
|
16
|
+
end
|
17
|
+
|
18
|
+
def dump_summary(*args);end
|
19
|
+
def dump_failures(*args);end
|
20
|
+
def dump_failure(*args);end
|
21
|
+
def dump_pending(*args);end
|
22
|
+
|
23
|
+
def start_dump(*args)
|
24
|
+
return unless ENV['TEST_ENV_NUMBER'] #only record when running in parallel
|
25
|
+
# TODO: Figure out why sometimes time can be less than 0
|
26
|
+
lock_output do
|
27
|
+
@example_times.each do |file, time|
|
28
|
+
relative_path = file.sub(/^#{Regexp.escape Dir.pwd}\//,'')
|
29
|
+
@output.puts "#{relative_path}:#{time > 0 ? time : 0}"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
@output.flush
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'parallel_specs'
|
2
|
+
require File.join(File.dirname(__FILE__), 'spec_logger_base')
|
3
|
+
|
4
|
+
class ParallelSpecs::SpecStartFinishLogger < ParallelSpecs::SpecLoggerBase
|
5
|
+
def initialize(options, output=nil)
|
6
|
+
output ||= options # rspec 2 has output as first argument
|
7
|
+
|
8
|
+
output = "#{output}_#{ENV['TEST_ENV_NUMBER']}.log"
|
9
|
+
if String === output
|
10
|
+
FileUtils.mkdir_p(File.dirname(output))
|
11
|
+
File.open(output, 'w'){} # overwrite previous results
|
12
|
+
@output = File.open(output, 'a')
|
13
|
+
elsif File === output
|
14
|
+
output.close # close file opened with 'w'
|
15
|
+
@output = File.open(output.path, 'a')
|
16
|
+
else
|
17
|
+
@output = output
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def example_started(example)
|
22
|
+
@output.puts ""
|
23
|
+
@output.puts "started spec: #{example.description}"
|
24
|
+
end
|
25
|
+
|
26
|
+
def example_passed(example)
|
27
|
+
@output.puts "finished spec: #{example.description}"
|
28
|
+
end
|
29
|
+
|
30
|
+
def example_pending(example, message)
|
31
|
+
@output.puts "finished spec: #{example.description}"
|
32
|
+
end
|
33
|
+
|
34
|
+
def example_failed(example, count, failure)
|
35
|
+
@output.puts "finished spec: #{example.description}"
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'parallel_specs/spec_failures_logger'
|
2
|
+
|
3
|
+
class ParallelSpecs::SpecSummaryLogger < ParallelSpecs::SpecLoggerBase
|
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,52 @@
|
|
1
|
+
require 'parallel_tests'
|
2
|
+
|
3
|
+
class ParallelSpecs < ParallelTests
|
4
|
+
def self.run_tests(test_files, process_number, options)
|
5
|
+
exe = executable # expensive, so we cache
|
6
|
+
version = (exe =~ /\brspec\b/ ? 2 : 1)
|
7
|
+
cmd = "#{rspec_1_color if version == 1}#{exe} #{options[:test_options]} #{rspec_2_color if version == 2}#{spec_opts(version)} #{test_files*' '}"
|
8
|
+
execute_command(cmd, process_number, options)
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.executable
|
12
|
+
cmd = if File.file?("script/spec")
|
13
|
+
"script/spec"
|
14
|
+
elsif bundler_enabled?
|
15
|
+
cmd = (run("bundle show rspec") =~ %r{/rspec-1[^/]+$} ? "spec" : "rspec")
|
16
|
+
"bundle exec #{cmd}"
|
17
|
+
else
|
18
|
+
%w[spec rspec].detect{|cmd| system "#{cmd} --version > /dev/null 2>&1" }
|
19
|
+
end
|
20
|
+
cmd or raise("Can't find executables rspec or spec")
|
21
|
+
end
|
22
|
+
|
23
|
+
# legacy <-> people log to this file using rspec options
|
24
|
+
def self.runtime_log
|
25
|
+
'tmp/parallel_profile.log'
|
26
|
+
end
|
27
|
+
|
28
|
+
protected
|
29
|
+
|
30
|
+
# so it can be stubbed....
|
31
|
+
def self.run(cmd)
|
32
|
+
`#{cmd}`
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.rspec_1_color
|
36
|
+
'RSPEC_COLOR=1 ; export RSPEC_COLOR ;' if $stdout.tty?
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.rspec_2_color
|
40
|
+
'--color --tty ' if $stdout.tty?
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.spec_opts(rspec_version)
|
44
|
+
options_file = ['spec/parallel_spec.opts', 'spec/spec.opts'].detect{|f| File.file?(f) }
|
45
|
+
return unless options_file
|
46
|
+
"-O #{options_file}"
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.test_suffix
|
50
|
+
"_spec.rb"
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
class ParallelTests
|
2
|
+
class Grouper
|
3
|
+
def self.in_groups(items, num_groups)
|
4
|
+
groups = Array.new(num_groups){ [] }
|
5
|
+
|
6
|
+
until items.empty?
|
7
|
+
num_groups.times do |group_number|
|
8
|
+
groups[group_number] << items.shift
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
groups.map!(&:sort!)
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.in_even_groups_by_size(items_with_sizes, num_groups, options={})
|
16
|
+
groups = Array.new(num_groups){{:items => [], :size => 0}}
|
17
|
+
|
18
|
+
# add all files that should run in a single process to one group
|
19
|
+
(options[:single_process]||[]).each do |pattern|
|
20
|
+
matched, items_with_sizes = items_with_sizes.partition{|item, size| item =~ pattern }
|
21
|
+
smallest = smallest_group(groups)
|
22
|
+
matched.each{|item,size| add_to_group(smallest, item, size) }
|
23
|
+
end
|
24
|
+
|
25
|
+
# add all other files
|
26
|
+
largest_first(items_with_sizes).each do |item, size|
|
27
|
+
smallest = smallest_group(groups)
|
28
|
+
add_to_group(smallest, item, size)
|
29
|
+
end
|
30
|
+
|
31
|
+
groups.map!{|g| g[:items].sort }
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.largest_first(files)
|
35
|
+
files.sort_by{|item, size| size }.reverse
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def self.smallest_group(groups)
|
41
|
+
groups.min_by{|g| g[:size] }
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.add_to_group(group, item, size)
|
45
|
+
group[:items] << item
|
46
|
+
group[:size] += size
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
class ParallelTests::RuntimeLogger
|
2
|
+
@@has_started = false
|
3
|
+
|
4
|
+
def self.log(test, start_time, end_time)
|
5
|
+
return if test.is_a? Test::Unit::TestSuite # don't log for suites-of-suites
|
6
|
+
|
7
|
+
if !@@has_started # make empty log file
|
8
|
+
File.open(ParallelTests.runtime_log, 'w') do end
|
9
|
+
@@has_started = true
|
10
|
+
end
|
11
|
+
|
12
|
+
File.open(ParallelTests.runtime_log, 'a') do |output|
|
13
|
+
begin
|
14
|
+
output.flock File::LOCK_EX
|
15
|
+
output.puts(self.message(test, start_time, end_time))
|
16
|
+
ensure
|
17
|
+
output.flock File::LOCK_UN
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.message(test, start_time, end_time)
|
23
|
+
delta="%.2f" % (end_time.to_f-start_time.to_f)
|
24
|
+
filename=class_directory(test.class) + class_to_filename(test.class) + ".rb"
|
25
|
+
message="#{filename}:#{delta}"
|
26
|
+
end
|
27
|
+
|
28
|
+
# Note: this is a best guess at conventional test directory structure, and may need
|
29
|
+
# tweaking / post-processing to match correctly for any given project
|
30
|
+
def self.class_directory(suspect)
|
31
|
+
result = "test/"
|
32
|
+
|
33
|
+
if defined?(Rails)
|
34
|
+
result += case suspect.superclass.name
|
35
|
+
when "ActionDispatch::IntegrationTest"
|
36
|
+
"integration/"
|
37
|
+
when "ActionDispatch::PerformanceTest"
|
38
|
+
"performance/"
|
39
|
+
when "ActionController::TestCase"
|
40
|
+
"functional/"
|
41
|
+
when "ActionView::TestCase"
|
42
|
+
"unit/helpers/"
|
43
|
+
else
|
44
|
+
"unit/"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
result
|
48
|
+
end
|
49
|
+
|
50
|
+
# based on https://github.com/grosser/single_test/blob/master/lib/single_test.rb#L117
|
51
|
+
def self.class_to_filename(suspect)
|
52
|
+
word = suspect.to_s.dup
|
53
|
+
return word unless word.match /^[A-Z]/ and not word.match %r{/[a-z]}
|
54
|
+
|
55
|
+
word.gsub!(/([A-Z]+)([A-Z][a-z])/,'\1_\2')
|
56
|
+
word.gsub!(/([a-z\d])([A-Z])/,'\1_\2')
|
57
|
+
word.gsub!(/\:\:/,'/')
|
58
|
+
word.tr!("-", "_")
|
59
|
+
word.downcase!
|
60
|
+
word
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
|
65
|
+
require 'test/unit/testsuite'
|
66
|
+
class Test::Unit::TestSuite
|
67
|
+
|
68
|
+
alias :run_without_timing :run unless defined? @@timing_installed
|
69
|
+
|
70
|
+
def run(result, &progress_block)
|
71
|
+
start_time=Time.now
|
72
|
+
run_without_timing(result, &progress_block)
|
73
|
+
end_time=Time.now
|
74
|
+
ParallelTests::RuntimeLogger.log(self.tests.first, start_time, end_time)
|
75
|
+
end
|
76
|
+
@@timing_installed = true
|
77
|
+
|
78
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
namespace :parallel do
|
2
|
+
def run_in_parallel(cmd, options)
|
3
|
+
count = (options[:count] ? options[:count].to_i : nil)
|
4
|
+
executable = File.join(File.dirname(__FILE__), '..', '..', 'bin', 'parallel_test')
|
5
|
+
command = "#{executable} --exec '#{cmd}' -n #{count} #{'--non-parallel' if options[:non_parallel]}"
|
6
|
+
abort unless system(command)
|
7
|
+
end
|
8
|
+
|
9
|
+
desc "create test databases via db:create --> parallel:create[num_cpus]"
|
10
|
+
task :create, :count do |t,args|
|
11
|
+
run_in_parallel('rake db:create RAILS_ENV=test', args)
|
12
|
+
end
|
13
|
+
|
14
|
+
desc "drop test databases via db:drop --> parallel:drop[num_cpus]"
|
15
|
+
task :drop, :count do |t,args|
|
16
|
+
run_in_parallel('rake db:drop RAILS_ENV=test', args)
|
17
|
+
end
|
18
|
+
|
19
|
+
desc "update test databases by dumping and loading --> parallel:prepare[num_cpus]"
|
20
|
+
task(:prepare, [:count] => 'db:abort_if_pending_migrations') do |t,args|
|
21
|
+
if defined?(ActiveRecord) && ActiveRecord::Base.schema_format == :ruby
|
22
|
+
# dump then load in parallel
|
23
|
+
Rake::Task['db:schema:dump'].invoke
|
24
|
+
Rake::Task['parallel:load_schema'].invoke(args[:count])
|
25
|
+
else
|
26
|
+
# there is no separate dump / load for schema_format :sql -> do it safe and slow
|
27
|
+
args = args.to_hash.merge(:non_parallel => true) # normal merge returns nil
|
28
|
+
run_in_parallel('rake db:test:prepare --trace', args)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# when dumping/resetting takes too long
|
33
|
+
desc "update test databases via db:migrate --> parallel:migrate[num_cpus]"
|
34
|
+
task :migrate, :count do |t,args|
|
35
|
+
run_in_parallel('rake db:migrate RAILS_ENV=test', args)
|
36
|
+
end
|
37
|
+
|
38
|
+
# just load the schema (good for integration server <-> no development db)
|
39
|
+
desc "load dumped schema for test databases via db:schema:load --> parallel:load_schema[num_cpus]"
|
40
|
+
task :load_schema, :count do |t,args|
|
41
|
+
run_in_parallel('rake db:test:load', args)
|
42
|
+
end
|
43
|
+
|
44
|
+
['test', 'spec', 'features'].each do |type|
|
45
|
+
desc "run #{type} in parallel with parallel:#{type}[num_cpus]"
|
46
|
+
task type, :count, :pattern, :options, :arguments do |t,args|
|
47
|
+
$LOAD_PATH << File.expand_path(File.join(File.dirname(__FILE__), '..'))
|
48
|
+
require "parallel_tests"
|
49
|
+
count, pattern, options = ParallelTests.parse_rake_args(args)
|
50
|
+
executable = File.join(File.dirname(__FILE__), '..', '..', 'bin', 'parallel_test')
|
51
|
+
command = "#{executable} --type #{type} -n #{count} -p '#{pattern}' -r '#{Rails.root}' -o '#{options}' #{args[:arguments]}"
|
52
|
+
abort unless system(command) # allow to chain tasks e.g. rake parallel:spec parallel:features
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
#backwards compatability
|
58
|
+
#spec:parallel:prepare
|
59
|
+
#spec:parallel
|
60
|
+
#test:parallel
|
61
|
+
namespace :spec do
|
62
|
+
namespace :parallel do
|
63
|
+
task :prepare, :count do |t,args|
|
64
|
+
$stderr.puts "WARNING -- Deprecated! use parallel:prepare"
|
65
|
+
Rake::Task['parallel:prepare'].invoke(args[:count])
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
task :parallel, :count, :pattern do |t,args|
|
70
|
+
$stderr.puts "WARNING -- Deprecated! use parallel:spec"
|
71
|
+
Rake::Task['parallel:spec'].invoke(args[:count], args[:pattern])
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
namespace :test do
|
76
|
+
task :parallel, :count, :pattern do |t,args|
|
77
|
+
$stderr.puts "WARNING -- Deprecated! use parallel:test"
|
78
|
+
Rake::Task['parallel:test'].invoke(args[:count], args[:pattern])
|
79
|
+
end
|
80
|
+
end
|