parallel_tests 0.6.20 → 0.7.0.alpha

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