parallelized_specs 0.0.1

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.
@@ -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