parallelized_specs 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,10 @@
1
+ # add rake tasks if we are inside Rails
2
+ if defined?(Rails::Railtie)
3
+ class ParallelTests
4
+ class Railtie < ::Rails::Railtie
5
+ rake_tasks do
6
+ load File.expand_path("../../tasks/parallel_tests.rake", __FILE__)
7
+ end
8
+ end
9
+ end
10
+ 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