parallel_tests 0.6.20 → 0.7.0.alpha

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. data/.gitignore +1 -0
  2. data/Gemfile +2 -4
  3. data/Gemfile.lock +7 -7
  4. data/Rakefile +18 -16
  5. data/Readme.md +15 -12
  6. data/bin/parallel_test +2 -98
  7. data/lib/parallel_tests.rb +2 -125
  8. data/lib/parallel_tests/cli.rb +102 -0
  9. data/lib/parallel_tests/cucumber/runner.rb +40 -0
  10. data/lib/parallel_tests/cucumber/runtime_logger.rb +58 -0
  11. data/lib/parallel_tests/grouper.rb +2 -2
  12. data/lib/parallel_tests/railtie.rb +3 -3
  13. data/lib/{parallel_specs/spec_failures_logger.rb → parallel_tests/spec/failures_logger.rb} +4 -3
  14. data/lib/{parallel_specs/spec_logger_base.rb → parallel_tests/spec/logger_base.rb} +7 -3
  15. data/lib/parallel_tests/spec/runner.rb +56 -0
  16. data/lib/{parallel_specs/spec_runtime_logger.rb → parallel_tests/spec/runtime_logger.rb} +2 -2
  17. data/lib/{parallel_specs/spec_summary_logger.rb → parallel_tests/spec/summary_logger.rb} +2 -2
  18. data/lib/parallel_tests/tasks.rb +0 -25
  19. data/lib/parallel_tests/test/runner.rb +126 -0
  20. data/lib/parallel_tests/test/runtime_logger.rb +92 -0
  21. data/lib/parallel_tests/version.rb +3 -0
  22. data/parallel_tests.gemspec +10 -61
  23. data/spec/parallel_tests/cucumber/runner_spec.rb +76 -0
  24. data/spec/{parallel_specs/spec_failure_logger_spec.rb → parallel_tests/spec/failure_logger_spec.rb} +8 -8
  25. data/spec/parallel_tests/spec/runner_spec.rb +178 -0
  26. data/spec/{parallel_specs/spec_runtime_logger_spec.rb → parallel_tests/spec/runtime_logger_spec.rb} +4 -4
  27. data/spec/{parallel_specs/spec_summary_logger_spec.rb → parallel_tests/spec/summary_logger_spec.rb} +2 -2
  28. data/spec/parallel_tests/test/runner_spec.rb +179 -0
  29. data/spec/parallel_tests/{runtime_logger_spec.rb → test/runtime_logger_spec.rb} +19 -16
  30. data/spec/parallel_tests_spec.rb +2 -158
  31. data/spec/spec_helper.rb +9 -7
  32. metadata +30 -26
  33. data/VERSION +0 -1
  34. data/lib/parallel_cucumber.rb +0 -36
  35. data/lib/parallel_cucumber/runtime_logger.rb +0 -57
  36. data/lib/parallel_specs.rb +0 -52
  37. data/lib/parallel_tests/runtime_logger.rb +0 -78
  38. data/lib/tasks/parallel_tests.rake +0 -1
  39. data/spec/parallel_cucumber_spec.rb +0 -72
  40. data/spec/parallel_specs_spec.rb +0 -173
@@ -0,0 +1,40 @@
1
+ require 'parallel_tests/test/runner'
2
+
3
+ module ParallelTests
4
+ module Cucumber
5
+ class Runner < ParallelTests::Test::Runner
6
+ def self.run_tests(test_files, process_number, options)
7
+ color = ($stdout.tty? ? 'AUTOTEST=1 ; export AUTOTEST ;' : '')#display color when we are in a terminal
8
+ runtime_logging = " --format ParallelCucumber::RuntimeLogger --out #{runtime_log}"
9
+ cmd = "#{color} #{executable}"
10
+ cmd << runtime_logging if File.directory?(File.dirname(runtime_log))
11
+ cmd << " #{options[:test_options]} #{test_files*' '}"
12
+ execute_command(cmd, process_number, options)
13
+ end
14
+
15
+ def self.executable
16
+ if ParallelTests.bundler_enabled?
17
+ "bundle exec cucumber"
18
+ elsif File.file?("script/cucumber")
19
+ "script/cucumber"
20
+ else
21
+ "cucumber"
22
+ end
23
+ end
24
+
25
+ def self.runtime_log
26
+ 'tmp/parallel_runtime_cucumber.log'
27
+ end
28
+
29
+ protected
30
+
31
+ def self.test_suffix
32
+ ".feature"
33
+ end
34
+
35
+ def self.line_is_result?(line)
36
+ line =~ /^\d+ (steps|scenarios)/
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,58 @@
1
+ module ParallelTests
2
+ module Cucumber
3
+ class RuntimeLogger
4
+ def initialize(step_mother, path_or_io, options=nil)
5
+ @io = prepare_io(path_or_io)
6
+ @example_times = Hash.new(0)
7
+ end
8
+
9
+ def before_feature(_)
10
+ @start_at = Time.now.to_f
11
+ end
12
+
13
+ def after_feature(feature)
14
+ @example_times[feature.file] += Time.now.to_f - @start_at
15
+ end
16
+
17
+ def after_features(*args)
18
+ lock_output do
19
+ @io.puts @example_times.map { |file, time| "#{file}:#{time}" }
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ def prepare_io(path_or_io)
26
+ if path_or_io.respond_to?(:write)
27
+ path_or_io
28
+ else # its a path
29
+ File.open(path_or_io, 'w').close # clean out the file
30
+ file = File.open(path_or_io, 'a')
31
+
32
+ at_exit do
33
+ unless file.closed?
34
+ file.flush
35
+ file.close
36
+ end
37
+ end
38
+
39
+ file
40
+ end
41
+ end
42
+
43
+ # do not let multiple processes get in each others way
44
+ def lock_output
45
+ if File === @io
46
+ begin
47
+ @io.flock File::LOCK_EX
48
+ yield
49
+ ensure
50
+ @io.flock File::LOCK_UN
51
+ end
52
+ else
53
+ yield
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -1,4 +1,4 @@
1
- class ParallelTests
1
+ module ParallelTests
2
2
  class Grouper
3
3
  def self.in_groups(items, num_groups)
4
4
  groups = Array.new(num_groups){ [] }
@@ -35,7 +35,7 @@ class ParallelTests
35
35
  files.sort_by{|item, size| size }.reverse
36
36
  end
37
37
 
38
- private
38
+ private
39
39
 
40
40
  def self.smallest_group(groups)
41
41
  groups.min_by{|g| g[:size] }
@@ -1,9 +1,9 @@
1
- # add rake tasks if we are inside Rails
1
+ # add rake tasks if we are inside Rails 3
2
2
  if defined?(Rails::Railtie)
3
- class ParallelTests
3
+ module ParallelTests
4
4
  class Railtie < ::Rails::Railtie
5
5
  rake_tasks do
6
- load File.expand_path("../../tasks/parallel_tests.rake", __FILE__)
6
+ load File.expand_path("../tasks.rake", __FILE__)
7
7
  end
8
8
  end
9
9
  end
@@ -1,6 +1,7 @@
1
- require 'parallel_specs/spec_logger_base'
1
+ require 'parallel_tests/spec/logger_base'
2
+ require 'parallel_tests/spec/runner'
2
3
 
3
- class ParallelSpecs::SpecFailuresLogger < ParallelSpecs::SpecLoggerBase
4
+ class ParallelTests::Spec::FailuresLogger < ParallelTests::Spec::LoggerBase
4
5
  # RSpec 1: does not keep track of failures, so we do
5
6
  def example_failed(example, *args)
6
7
  if RSPEC_1
@@ -37,7 +38,7 @@ class ParallelSpecs::SpecFailuresLogger < ParallelSpecs::SpecLoggerBase
37
38
  file, line = example.location.to_s.split(':')
38
39
  next unless file and line
39
40
  file.gsub!(%r(^.*?/spec/), './spec/')
40
- @output.puts "#{ParallelSpecs.executable} #{file}:#{line} # #{example.description}"
41
+ @output.puts "#{ParallelTests::Spec::Runner.executable} #{file}:#{line} # #{example.description}"
41
42
  end
42
43
  end
43
44
  end
@@ -1,4 +1,7 @@
1
- require 'parallel_specs'
1
+ module ParallelTests
2
+ module Spec
3
+ end
4
+ end
2
5
 
3
6
  begin
4
7
  require 'rspec/core/formatters/base_text_formatter'
@@ -7,9 +10,10 @@ rescue LoadError
7
10
  require 'spec/runner/formatter/base_text_formatter'
8
11
  base = Spec::Runner::Formatter::BaseTextFormatter
9
12
  end
10
- ParallelSpecs::SpecLoggerBaseBase = base
11
13
 
12
- class ParallelSpecs::SpecLoggerBase < ParallelSpecs::SpecLoggerBaseBase
14
+ ParallelTests::Spec::LoggerBaseBase = base
15
+
16
+ class ParallelTests::Spec::LoggerBase < ParallelTests::Spec::LoggerBaseBase
13
17
  RSPEC_1 = !defined?(RSpec::Core::Formatters::BaseTextFormatter) # do not test for Spec, this will trigger deprecation warning in rspec 2
14
18
 
15
19
  def initialize(*args)
@@ -0,0 +1,56 @@
1
+ require 'parallel_tests/test/runner'
2
+
3
+ module ParallelTests
4
+ module Spec
5
+ class Runner < ParallelTests::Test::Runner
6
+ def self.run_tests(test_files, process_number, options)
7
+ exe = executable # expensive, so we cache
8
+ version = (exe =~ /\brspec\b/ ? 2 : 1)
9
+ cmd = "#{rspec_1_color if version == 1}#{exe} #{options[:test_options]} #{rspec_2_color if version == 2}#{spec_opts(version)} #{test_files*' '}"
10
+ execute_command(cmd, process_number, options)
11
+ end
12
+
13
+ def self.executable
14
+ cmd = if File.file?("script/spec")
15
+ "script/spec"
16
+ elsif ParallelTests.bundler_enabled?
17
+ cmd = (run("bundle show rspec") =~ %r{/rspec-1[^/]+$} ? "spec" : "rspec")
18
+ "bundle exec #{cmd}"
19
+ else
20
+ %w[spec rspec].detect{|cmd| system "#{cmd} --version > /dev/null 2>&1" }
21
+ end
22
+ cmd or raise("Can't find executables rspec or spec")
23
+ end
24
+
25
+ # legacy <-> people log to this file using rspec options
26
+ def self.runtime_log
27
+ 'tmp/parallel_profile.log'
28
+ end
29
+
30
+ protected
31
+
32
+ # so it can be stubbed....
33
+ def self.run(cmd)
34
+ `#{cmd}`
35
+ end
36
+
37
+ def self.rspec_1_color
38
+ 'RSPEC_COLOR=1 ; export RSPEC_COLOR ;' if $stdout.tty?
39
+ end
40
+
41
+ def self.rspec_2_color
42
+ '--color --tty ' if $stdout.tty?
43
+ end
44
+
45
+ def self.spec_opts(rspec_version)
46
+ options_file = ['.rspec_parallel', 'spec/parallel_spec.opts', 'spec/spec.opts'].detect{|f| File.file?(f) }
47
+ return unless options_file
48
+ "-O #{options_file}"
49
+ end
50
+
51
+ def self.test_suffix
52
+ "_spec.rb"
53
+ end
54
+ end
55
+ end
56
+ end
@@ -1,6 +1,6 @@
1
- require 'parallel_specs/spec_logger_base'
1
+ require 'parallel_tests/spec/logger_base'
2
2
 
3
- class ParallelSpecs::SpecRuntimeLogger < ParallelSpecs::SpecLoggerBase
3
+ class ParallelTests::Spec::RuntimeLogger < ParallelTests::Spec::LoggerBase
4
4
  def initialize(*args)
5
5
  super
6
6
  @example_times = Hash.new(0)
@@ -1,6 +1,6 @@
1
- require 'parallel_specs/spec_failures_logger'
1
+ require 'parallel_tests/spec/failures_logger'
2
2
 
3
- class ParallelSpecs::SpecSummaryLogger < ParallelSpecs::SpecLoggerBase
3
+ class ParallelTests::Spec::SummaryLogger < ParallelTests::Spec::LoggerBase
4
4
  # RSpec 1: dumps 1 failed spec
5
5
  def dump_failure(*args)
6
6
  lock_output do
@@ -53,28 +53,3 @@ namespace :parallel do
53
53
  end
54
54
  end
55
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
@@ -0,0 +1,126 @@
1
+ module ParallelTests
2
+ module Test
3
+ class Runner
4
+ # finds all tests and partitions them into groups
5
+ def self.tests_in_groups(root, num_groups, options={})
6
+ tests = find_tests(root, options)
7
+ if options[:no_sort] == true
8
+ Grouper.in_groups(tests, num_groups)
9
+ else
10
+ tests = with_runtime_info(tests)
11
+ Grouper.in_even_groups_by_size(tests, num_groups, options)
12
+ end
13
+ end
14
+
15
+ def self.run_tests(test_files, process_number, options)
16
+ require_list = test_files.map { |filename| %{"#{File.expand_path filename}"} }.join(",")
17
+ cmd = "ruby -Itest -e '[#{require_list}].each {|f| require f }' -- #{options[:test_options]}"
18
+ execute_command(cmd, process_number, options)
19
+ end
20
+
21
+ def self.execute_command(cmd, process_number, options)
22
+ cmd = "TEST_ENV_NUMBER=#{test_env_number(process_number)} ; export TEST_ENV_NUMBER; #{cmd}"
23
+ f = open("|#{cmd}", 'r')
24
+ output = fetch_output(f, options)
25
+ f.close
26
+ {:stdout => output, :exit_status => $?.exitstatus}
27
+ end
28
+
29
+ def self.find_results(test_output)
30
+ test_output.split("\n").map {|line|
31
+ line = line.gsub(/\.|F|\*/,'')
32
+ next unless line_is_result?(line)
33
+ line
34
+ }.compact
35
+ end
36
+
37
+ def self.test_env_number(process_number)
38
+ process_number == 0 ? '' : process_number + 1
39
+ end
40
+
41
+ def self.runtime_log
42
+ 'tmp/parallel_runtime_test.log'
43
+ end
44
+
45
+ def self.summarize_results(results)
46
+ results = results.join(' ').gsub(/s\b/,'') # combine and singularize results
47
+ counts = results.scan(/(\d+) (\w+)/)
48
+ sums = counts.inject(Hash.new(0)) do |sum, (number, word)|
49
+ sum[word] += number.to_i
50
+ sum
51
+ end
52
+ sums.sort.map{|word, number| "#{number} #{word}#{'s' if number != 1}" }.join(', ')
53
+ end
54
+
55
+ protected
56
+
57
+ # read output of the process and print in in chucks
58
+ def self.fetch_output(process, options)
59
+ all = ''
60
+ buffer = ''
61
+ timeout = options[:chunk_timeout] || 0.2
62
+ flushed = Time.now.to_f
63
+
64
+ while char = process.getc
65
+ char = (char.is_a?(Fixnum) ? char.chr : char) # 1.8 <-> 1.9
66
+ all << char
67
+
68
+ # print in chunks so large blocks stay together
69
+ now = Time.now.to_f
70
+ buffer << char
71
+ if flushed + timeout < now
72
+ $stdout.print buffer
73
+ $stdout.flush
74
+ buffer = ''
75
+ flushed = now
76
+ end
77
+ end
78
+
79
+ # print the remainder
80
+ $stdout.print buffer
81
+ $stdout.flush
82
+
83
+ all
84
+ end
85
+
86
+ def self.line_is_result?(line)
87
+ line =~ /\d+ failure/
88
+ end
89
+
90
+ def self.test_suffix
91
+ "_test.rb"
92
+ end
93
+
94
+ def self.with_runtime_info(tests)
95
+ lines = File.read(runtime_log).split("\n") rescue []
96
+
97
+ # use recorded test runtime if we got enough data
98
+ if lines.size * 1.5 > tests.size
99
+ puts "Using recorded test runtime"
100
+ times = Hash.new(1)
101
+ lines.each do |line|
102
+ test, time = line.split(":")
103
+ next unless test and time
104
+ times[File.expand_path(test)] = time.to_f
105
+ end
106
+ tests.sort.map{|test| [test, times[test]] }
107
+ else # use file sizes
108
+ tests.sort.map{|test| [test, File.stat(test).size] }
109
+ end
110
+ end
111
+
112
+ def self.find_tests(root, options={})
113
+ if root.is_a?(Array)
114
+ root
115
+ else
116
+ # follow one symlink and direct children
117
+ # http://stackoverflow.com/questions/357754/can-i-traverse-symlinked-directories-in-ruby-with-a-glob
118
+ files = Dir["#{root}/**{,/*/**}/*#{test_suffix}"].uniq
119
+ files = files.map{|f| f.sub(root+'/','') }
120
+ files = files.grep(/#{options[:pattern]}/)
121
+ files.map{|f| "#{root}/#{f}" }
122
+ end
123
+ end
124
+ end
125
+ end
126
+ 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