friendlyfashion-parallel_tests 0.9.0
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.
- data/.gitignore +2 -0
 - data/Gemfile +8 -0
 - data/Gemfile.lock +44 -0
 - data/Rakefile +6 -0
 - data/Readme.md +232 -0
 - data/ReadmeRails2.md +48 -0
 - data/bin/parallel_cucumber +2 -0
 - data/bin/parallel_rspec +2 -0
 - data/bin/parallel_test +6 -0
 - data/lib/parallel_tests.rb +30 -0
 - data/lib/parallel_tests/cli.rb +159 -0
 - data/lib/parallel_tests/cucumber/gherkin_listener.rb +60 -0
 - data/lib/parallel_tests/cucumber/runner.rb +90 -0
 - data/lib/parallel_tests/cucumber/runtime_logger.rb +58 -0
 - data/lib/parallel_tests/grouper.rb +53 -0
 - data/lib/parallel_tests/railtie.rb +8 -0
 - data/lib/parallel_tests/rspec/failures_logger.rb +44 -0
 - data/lib/parallel_tests/rspec/logger_base.rb +52 -0
 - data/lib/parallel_tests/rspec/runner.rb +59 -0
 - data/lib/parallel_tests/rspec/runtime_logger.rb +34 -0
 - data/lib/parallel_tests/rspec/summary_logger.rb +19 -0
 - data/lib/parallel_tests/tasks.rb +134 -0
 - data/lib/parallel_tests/test/runner.rb +134 -0
 - data/lib/parallel_tests/test/runtime_logger.rb +92 -0
 - data/lib/parallel_tests/version.rb +3 -0
 - data/parallel_tests.gemspec +14 -0
 - data/spec/integration_spec.rb +244 -0
 - data/spec/parallel_tests/cli_spec.rb +36 -0
 - data/spec/parallel_tests/cucumber/gherkin_listener_spec.rb +48 -0
 - data/spec/parallel_tests/cucumber/runner_spec.rb +173 -0
 - data/spec/parallel_tests/grouper_spec.rb +52 -0
 - data/spec/parallel_tests/rspec/failure_logger_spec.rb +82 -0
 - data/spec/parallel_tests/rspec/runner_spec.rb +178 -0
 - data/spec/parallel_tests/rspec/runtime_logger_spec.rb +76 -0
 - data/spec/parallel_tests/rspec/summary_logger_spec.rb +37 -0
 - data/spec/parallel_tests/tasks_spec.rb +151 -0
 - data/spec/parallel_tests/test/runner_spec.rb +273 -0
 - data/spec/parallel_tests/test/runtime_logger_spec.rb +84 -0
 - data/spec/parallel_tests_spec.rb +73 -0
 - data/spec/spec_helper.rb +151 -0
 - metadata +109 -0
 
| 
         @@ -0,0 +1,60 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            require 'gherkin'
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module ParallelTests
         
     | 
| 
      
 4 
     | 
    
         
            +
              module Cucumber
         
     | 
| 
      
 5 
     | 
    
         
            +
                class GherkinListener
         
     | 
| 
      
 6 
     | 
    
         
            +
                  attr_reader :collect
         
     | 
| 
      
 7 
     | 
    
         
            +
             
     | 
| 
      
 8 
     | 
    
         
            +
                  def initialize
         
     | 
| 
      
 9 
     | 
    
         
            +
                    @steps, @uris = [], []
         
     | 
| 
      
 10 
     | 
    
         
            +
                    @collect = {}
         
     | 
| 
      
 11 
     | 
    
         
            +
                    reset_counters!
         
     | 
| 
      
 12 
     | 
    
         
            +
                  end
         
     | 
| 
      
 13 
     | 
    
         
            +
             
     | 
| 
      
 14 
     | 
    
         
            +
                  def background(*args)
         
     | 
| 
      
 15 
     | 
    
         
            +
                    @background = 1
         
     | 
| 
      
 16 
     | 
    
         
            +
                  end
         
     | 
| 
      
 17 
     | 
    
         
            +
             
     | 
| 
      
 18 
     | 
    
         
            +
                  def scenario(*args)
         
     | 
| 
      
 19 
     | 
    
         
            +
                    @scenarios += 1
         
     | 
| 
      
 20 
     | 
    
         
            +
                    @outline = @background = 0
         
     | 
| 
      
 21 
     | 
    
         
            +
                  end
         
     | 
| 
      
 22 
     | 
    
         
            +
             
     | 
| 
      
 23 
     | 
    
         
            +
                  def scenario_outline(*args)
         
     | 
| 
      
 24 
     | 
    
         
            +
                    @outline = 1
         
     | 
| 
      
 25 
     | 
    
         
            +
                  end
         
     | 
| 
      
 26 
     | 
    
         
            +
             
     | 
| 
      
 27 
     | 
    
         
            +
                  def step(*args)
         
     | 
| 
      
 28 
     | 
    
         
            +
                    if @background == 1
         
     | 
| 
      
 29 
     | 
    
         
            +
                      @background_steps += 1
         
     | 
| 
      
 30 
     | 
    
         
            +
                    elsif @outline > 0
         
     | 
| 
      
 31 
     | 
    
         
            +
                      @outline_steps += 1
         
     | 
| 
      
 32 
     | 
    
         
            +
                    else
         
     | 
| 
      
 33 
     | 
    
         
            +
                      @collect[@uri] += 1
         
     | 
| 
      
 34 
     | 
    
         
            +
                    end
         
     | 
| 
      
 35 
     | 
    
         
            +
                  end
         
     | 
| 
      
 36 
     | 
    
         
            +
             
     | 
| 
      
 37 
     | 
    
         
            +
                  def uri(path)
         
     | 
| 
      
 38 
     | 
    
         
            +
                    @uri = path
         
     | 
| 
      
 39 
     | 
    
         
            +
                    @collect[@uri] = 0
         
     | 
| 
      
 40 
     | 
    
         
            +
                  end
         
     | 
| 
      
 41 
     | 
    
         
            +
             
     | 
| 
      
 42 
     | 
    
         
            +
                  def examples(*args)
         
     | 
| 
      
 43 
     | 
    
         
            +
                    @examples += 1
         
     | 
| 
      
 44 
     | 
    
         
            +
                  end
         
     | 
| 
      
 45 
     | 
    
         
            +
             
     | 
| 
      
 46 
     | 
    
         
            +
                  def eof(*args)
         
     | 
| 
      
 47 
     | 
    
         
            +
                    @collect[@uri] += (@background_steps * @scenarios) + (@outline_steps * @examples)
         
     | 
| 
      
 48 
     | 
    
         
            +
                    reset_counters!
         
     | 
| 
      
 49 
     | 
    
         
            +
                  end
         
     | 
| 
      
 50 
     | 
    
         
            +
             
     | 
| 
      
 51 
     | 
    
         
            +
                  def reset_counters!
         
     | 
| 
      
 52 
     | 
    
         
            +
                    @examples = @outline = @outline_steps = @background = @background_steps = @scenarios = 0
         
     | 
| 
      
 53 
     | 
    
         
            +
                  end
         
     | 
| 
      
 54 
     | 
    
         
            +
             
     | 
| 
      
 55 
     | 
    
         
            +
                  # ignore lots of other possible callbacks ...
         
     | 
| 
      
 56 
     | 
    
         
            +
                  def method_missing(*args)
         
     | 
| 
      
 57 
     | 
    
         
            +
                  end
         
     | 
| 
      
 58 
     | 
    
         
            +
                end
         
     | 
| 
      
 59 
     | 
    
         
            +
              end
         
     | 
| 
      
 60 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,90 @@ 
     | 
|
| 
      
 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 ParallelTests::Cucumber::RuntimeLogger --out #{runtime_log}"
         
     | 
| 
      
 9 
     | 
    
         
            +
                    cmd = [
         
     | 
| 
      
 10 
     | 
    
         
            +
                      color,
         
     | 
| 
      
 11 
     | 
    
         
            +
                      executable,
         
     | 
| 
      
 12 
     | 
    
         
            +
                      (runtime_logging if File.directory?(File.dirname(runtime_log))),
         
     | 
| 
      
 13 
     | 
    
         
            +
                      cucumber_opts(options[:test_options]),
         
     | 
| 
      
 14 
     | 
    
         
            +
                      *test_files
         
     | 
| 
      
 15 
     | 
    
         
            +
                    ].compact.join(" ")
         
     | 
| 
      
 16 
     | 
    
         
            +
                    execute_command(cmd, process_number, options)
         
     | 
| 
      
 17 
     | 
    
         
            +
                  end
         
     | 
| 
      
 18 
     | 
    
         
            +
             
     | 
| 
      
 19 
     | 
    
         
            +
                  def self.executable
         
     | 
| 
      
 20 
     | 
    
         
            +
                    if ParallelTests.bundler_enabled?
         
     | 
| 
      
 21 
     | 
    
         
            +
                      "bundle exec cucumber"
         
     | 
| 
      
 22 
     | 
    
         
            +
                    elsif File.file?("script/cucumber")
         
     | 
| 
      
 23 
     | 
    
         
            +
                      "script/cucumber"
         
     | 
| 
      
 24 
     | 
    
         
            +
                    else
         
     | 
| 
      
 25 
     | 
    
         
            +
                      "cucumber"
         
     | 
| 
      
 26 
     | 
    
         
            +
                    end
         
     | 
| 
      
 27 
     | 
    
         
            +
                  end
         
     | 
| 
      
 28 
     | 
    
         
            +
             
     | 
| 
      
 29 
     | 
    
         
            +
                  def self.runtime_log
         
     | 
| 
      
 30 
     | 
    
         
            +
                    'tmp/parallel_runtime_cucumber.log'
         
     | 
| 
      
 31 
     | 
    
         
            +
                  end
         
     | 
| 
      
 32 
     | 
    
         
            +
             
     | 
| 
      
 33 
     | 
    
         
            +
                  def self.test_file_name
         
     | 
| 
      
 34 
     | 
    
         
            +
                    "feature"
         
     | 
| 
      
 35 
     | 
    
         
            +
                  end
         
     | 
| 
      
 36 
     | 
    
         
            +
             
     | 
| 
      
 37 
     | 
    
         
            +
                  def self.test_suffix
         
     | 
| 
      
 38 
     | 
    
         
            +
                    ".feature"
         
     | 
| 
      
 39 
     | 
    
         
            +
                  end
         
     | 
| 
      
 40 
     | 
    
         
            +
             
     | 
| 
      
 41 
     | 
    
         
            +
                  def self.line_is_result?(line)
         
     | 
| 
      
 42 
     | 
    
         
            +
                    line =~ /^\d+ (steps?|scenarios?)/
         
     | 
| 
      
 43 
     | 
    
         
            +
                  end
         
     | 
| 
      
 44 
     | 
    
         
            +
             
     | 
| 
      
 45 
     | 
    
         
            +
                  # cucumber has 2 result lines per test run, that cannot be added
         
     | 
| 
      
 46 
     | 
    
         
            +
                  # 1 scenario (1 failed)
         
     | 
| 
      
 47 
     | 
    
         
            +
                  # 1 step (1 failed)
         
     | 
| 
      
 48 
     | 
    
         
            +
                  def self.summarize_results(results)
         
     | 
| 
      
 49 
     | 
    
         
            +
                    sort_order = %w[scenario step failed undefined skipped pending passed]
         
     | 
| 
      
 50 
     | 
    
         
            +
             
     | 
| 
      
 51 
     | 
    
         
            +
                    %w[scenario step].map do |group|
         
     | 
| 
      
 52 
     | 
    
         
            +
                      group_results = results.grep /^\d+ #{group}/
         
     | 
| 
      
 53 
     | 
    
         
            +
                      next if group_results.empty?
         
     | 
| 
      
 54 
     | 
    
         
            +
             
     | 
| 
      
 55 
     | 
    
         
            +
                      sums = sum_up_results(group_results)
         
     | 
| 
      
 56 
     | 
    
         
            +
                      sums = sums.sort_by { |word, _| sort_order.index(word) || 999 }
         
     | 
| 
      
 57 
     | 
    
         
            +
                      sums.map! do |word, number|
         
     | 
| 
      
 58 
     | 
    
         
            +
                        plural = "s" if word == group and number != 1
         
     | 
| 
      
 59 
     | 
    
         
            +
                        "#{number} #{word}#{plural}"
         
     | 
| 
      
 60 
     | 
    
         
            +
                      end
         
     | 
| 
      
 61 
     | 
    
         
            +
                      "#{sums[0]} (#{sums[1..-1].join(", ")})"
         
     | 
| 
      
 62 
     | 
    
         
            +
                    end.compact.join("\n")
         
     | 
| 
      
 63 
     | 
    
         
            +
                  end
         
     | 
| 
      
 64 
     | 
    
         
            +
             
     | 
| 
      
 65 
     | 
    
         
            +
                  def self.cucumber_opts(given)
         
     | 
| 
      
 66 
     | 
    
         
            +
                    if given =~ /--profile/ or given =~ /(^|\s)-p /
         
     | 
| 
      
 67 
     | 
    
         
            +
                      given
         
     | 
| 
      
 68 
     | 
    
         
            +
                    else
         
     | 
| 
      
 69 
     | 
    
         
            +
                      [given, profile_from_config].compact.join(" ")
         
     | 
| 
      
 70 
     | 
    
         
            +
                    end
         
     | 
| 
      
 71 
     | 
    
         
            +
                  end
         
     | 
| 
      
 72 
     | 
    
         
            +
             
     | 
| 
      
 73 
     | 
    
         
            +
                  def self.profile_from_config
         
     | 
| 
      
 74 
     | 
    
         
            +
                    # copied from https://github.com/cucumber/cucumber/blob/master/lib/cucumber/cli/profile_loader.rb#L85
         
     | 
| 
      
 75 
     | 
    
         
            +
                    config = Dir.glob('{,.config/,config/}cucumber{.yml,.yaml}').first
         
     | 
| 
      
 76 
     | 
    
         
            +
                    if config && File.read(config) =~ /^parallel:/
         
     | 
| 
      
 77 
     | 
    
         
            +
                      "--profile parallel"
         
     | 
| 
      
 78 
     | 
    
         
            +
                    end
         
     | 
| 
      
 79 
     | 
    
         
            +
                  end
         
     | 
| 
      
 80 
     | 
    
         
            +
             
     | 
| 
      
 81 
     | 
    
         
            +
                  def self.tests_in_groups(tests, num_groups, options={})
         
     | 
| 
      
 82 
     | 
    
         
            +
                    if options[:group_by] == :steps
         
     | 
| 
      
 83 
     | 
    
         
            +
                      Grouper.by_steps(find_tests(tests, options), num_groups)
         
     | 
| 
      
 84 
     | 
    
         
            +
                    else
         
     | 
| 
      
 85 
     | 
    
         
            +
                      super
         
     | 
| 
      
 86 
     | 
    
         
            +
                    end
         
     | 
| 
      
 87 
     | 
    
         
            +
                  end
         
     | 
| 
      
 88 
     | 
    
         
            +
                end
         
     | 
| 
      
 89 
     | 
    
         
            +
              end
         
     | 
| 
      
 90 
     | 
    
         
            +
            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
         
     | 
| 
         @@ -0,0 +1,53 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            module ParallelTests
         
     | 
| 
      
 2 
     | 
    
         
            +
              class Grouper
         
     | 
| 
      
 3 
     | 
    
         
            +
                def self.in_even_groups_by_size(items_with_sizes, num_groups, options = {})
         
     | 
| 
      
 4 
     | 
    
         
            +
                  groups = Array.new(num_groups) { {:items => [], :size => 0} }
         
     | 
| 
      
 5 
     | 
    
         
            +
             
     | 
| 
      
 6 
     | 
    
         
            +
                  # add all files that should run in a single process to one group
         
     | 
| 
      
 7 
     | 
    
         
            +
                  (options[:single_process] || []).each do |pattern|
         
     | 
| 
      
 8 
     | 
    
         
            +
                    matched, items_with_sizes = items_with_sizes.partition { |item, size| item =~ pattern }
         
     | 
| 
      
 9 
     | 
    
         
            +
                    matched.each { |item, size| add_to_group(groups.first, item, size) }
         
     | 
| 
      
 10 
     | 
    
         
            +
                  end
         
     | 
| 
      
 11 
     | 
    
         
            +
             
     | 
| 
      
 12 
     | 
    
         
            +
                  groups_to_fill = (options[:isolate] ? groups[1..-1] : groups)
         
     | 
| 
      
 13 
     | 
    
         
            +
             
     | 
| 
      
 14 
     | 
    
         
            +
                  # add all other files
         
     | 
| 
      
 15 
     | 
    
         
            +
                  largest_first(items_with_sizes).each do |item, size|
         
     | 
| 
      
 16 
     | 
    
         
            +
                    smallest = smallest_group(groups_to_fill)
         
     | 
| 
      
 17 
     | 
    
         
            +
                    add_to_group(smallest, item, size)
         
     | 
| 
      
 18 
     | 
    
         
            +
                  end
         
     | 
| 
      
 19 
     | 
    
         
            +
             
     | 
| 
      
 20 
     | 
    
         
            +
                  groups.map!{|g| g[:items].sort }
         
     | 
| 
      
 21 
     | 
    
         
            +
                end
         
     | 
| 
      
 22 
     | 
    
         
            +
             
     | 
| 
      
 23 
     | 
    
         
            +
                def self.largest_first(files)
         
     | 
| 
      
 24 
     | 
    
         
            +
                  files.sort_by{|item, size| size }.reverse
         
     | 
| 
      
 25 
     | 
    
         
            +
                end
         
     | 
| 
      
 26 
     | 
    
         
            +
             
     | 
| 
      
 27 
     | 
    
         
            +
                private
         
     | 
| 
      
 28 
     | 
    
         
            +
             
     | 
| 
      
 29 
     | 
    
         
            +
                def self.smallest_group(groups)
         
     | 
| 
      
 30 
     | 
    
         
            +
                  groups.min_by{|g| g[:size] }
         
     | 
| 
      
 31 
     | 
    
         
            +
                end
         
     | 
| 
      
 32 
     | 
    
         
            +
             
     | 
| 
      
 33 
     | 
    
         
            +
                def self.add_to_group(group, item, size)
         
     | 
| 
      
 34 
     | 
    
         
            +
                  group[:items] << item
         
     | 
| 
      
 35 
     | 
    
         
            +
                  group[:size] += size
         
     | 
| 
      
 36 
     | 
    
         
            +
                end
         
     | 
| 
      
 37 
     | 
    
         
            +
             
     | 
| 
      
 38 
     | 
    
         
            +
                def self.by_steps(tests, num_groups)
         
     | 
| 
      
 39 
     | 
    
         
            +
                  features_with_steps = build_features_with_steps(tests)
         
     | 
| 
      
 40 
     | 
    
         
            +
                  in_even_groups_by_size(features_with_steps, num_groups)
         
     | 
| 
      
 41 
     | 
    
         
            +
                end
         
     | 
| 
      
 42 
     | 
    
         
            +
             
     | 
| 
      
 43 
     | 
    
         
            +
                def self.build_features_with_steps(tests)
         
     | 
| 
      
 44 
     | 
    
         
            +
                  require 'parallel_tests/cucumber/gherkin_listener'
         
     | 
| 
      
 45 
     | 
    
         
            +
                  listener = Cucumber::GherkinListener.new
         
     | 
| 
      
 46 
     | 
    
         
            +
                  parser = Gherkin::Parser::Parser.new(listener, true, 'root')
         
     | 
| 
      
 47 
     | 
    
         
            +
                  tests.each{|file|
         
     | 
| 
      
 48 
     | 
    
         
            +
                    parser.parse(File.read(file), file, 0)
         
     | 
| 
      
 49 
     | 
    
         
            +
                  }
         
     | 
| 
      
 50 
     | 
    
         
            +
                  listener.collect.sort_by{|_,value| -value }
         
     | 
| 
      
 51 
     | 
    
         
            +
                end
         
     | 
| 
      
 52 
     | 
    
         
            +
              end
         
     | 
| 
      
 53 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,44 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            require 'parallel_tests/rspec/logger_base'
         
     | 
| 
      
 2 
     | 
    
         
            +
            require 'parallel_tests/rspec/runner'
         
     | 
| 
      
 3 
     | 
    
         
            +
             
     | 
| 
      
 4 
     | 
    
         
            +
            class ParallelTests::RSpec::FailuresLogger < ParallelTests::RSpec::LoggerBase
         
     | 
| 
      
 5 
     | 
    
         
            +
              # RSpec 1: does not keep track of failures, so we do
         
     | 
| 
      
 6 
     | 
    
         
            +
              def example_failed(example, *args)
         
     | 
| 
      
 7 
     | 
    
         
            +
                if RSPEC_1
         
     | 
| 
      
 8 
     | 
    
         
            +
                  @failed_examples ||= []
         
     | 
| 
      
 9 
     | 
    
         
            +
                  @failed_examples << example
         
     | 
| 
      
 10 
     | 
    
         
            +
                else
         
     | 
| 
      
 11 
     | 
    
         
            +
                  super
         
     | 
| 
      
 12 
     | 
    
         
            +
                end
         
     | 
| 
      
 13 
     | 
    
         
            +
              end
         
     | 
| 
      
 14 
     | 
    
         
            +
             
     | 
| 
      
 15 
     | 
    
         
            +
              # RSpec 1: dumps 1 failed spec
         
     | 
| 
      
 16 
     | 
    
         
            +
              def dump_failure(*args)
         
     | 
| 
      
 17 
     | 
    
         
            +
              end
         
     | 
| 
      
 18 
     | 
    
         
            +
             
     | 
| 
      
 19 
     | 
    
         
            +
              # RSpec 2: dumps all failed specs
         
     | 
| 
      
 20 
     | 
    
         
            +
              def dump_failures(*args)
         
     | 
| 
      
 21 
     | 
    
         
            +
              end
         
     | 
| 
      
 22 
     | 
    
         
            +
             
     | 
| 
      
 23 
     | 
    
         
            +
              def dump_summary(*args)
         
     | 
| 
      
 24 
     | 
    
         
            +
                lock_output do
         
     | 
| 
      
 25 
     | 
    
         
            +
                  if RSPEC_1
         
     | 
| 
      
 26 
     | 
    
         
            +
                    dump_commands_to_rerun_failed_examples_rspec_1
         
     | 
| 
      
 27 
     | 
    
         
            +
                  else
         
     | 
| 
      
 28 
     | 
    
         
            +
                    dump_commands_to_rerun_failed_examples
         
     | 
| 
      
 29 
     | 
    
         
            +
                  end
         
     | 
| 
      
 30 
     | 
    
         
            +
                end
         
     | 
| 
      
 31 
     | 
    
         
            +
                @output.flush
         
     | 
| 
      
 32 
     | 
    
         
            +
              end
         
     | 
| 
      
 33 
     | 
    
         
            +
             
     | 
| 
      
 34 
     | 
    
         
            +
              private
         
     | 
| 
      
 35 
     | 
    
         
            +
             
     | 
| 
      
 36 
     | 
    
         
            +
              def dump_commands_to_rerun_failed_examples_rspec_1
         
     | 
| 
      
 37 
     | 
    
         
            +
                (@failed_examples||[]).each do |example|
         
     | 
| 
      
 38 
     | 
    
         
            +
                  file, line = example.location.to_s.split(':')
         
     | 
| 
      
 39 
     | 
    
         
            +
                  next unless file and line
         
     | 
| 
      
 40 
     | 
    
         
            +
                  file.gsub!(%r(^.*?/spec/), './spec/')
         
     | 
| 
      
 41 
     | 
    
         
            +
                  @output.puts "#{ParallelTests::RSpec::Runner.executable} #{file}:#{line} # #{example.description}"
         
     | 
| 
      
 42 
     | 
    
         
            +
                end
         
     | 
| 
      
 43 
     | 
    
         
            +
              end
         
     | 
| 
      
 44 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,52 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            module ParallelTests
         
     | 
| 
      
 2 
     | 
    
         
            +
              module RSpec
         
     | 
| 
      
 3 
     | 
    
         
            +
              end
         
     | 
| 
      
 4 
     | 
    
         
            +
            end
         
     | 
| 
      
 5 
     | 
    
         
            +
             
     | 
| 
      
 6 
     | 
    
         
            +
            begin
         
     | 
| 
      
 7 
     | 
    
         
            +
              require 'rspec/core/formatters/base_text_formatter'
         
     | 
| 
      
 8 
     | 
    
         
            +
              base = RSpec::Core::Formatters::BaseTextFormatter
         
     | 
| 
      
 9 
     | 
    
         
            +
            rescue LoadError
         
     | 
| 
      
 10 
     | 
    
         
            +
              require 'spec/runner/formatter/base_text_formatter'
         
     | 
| 
      
 11 
     | 
    
         
            +
              base = Spec::Runner::Formatter::BaseTextFormatter
         
     | 
| 
      
 12 
     | 
    
         
            +
            end
         
     | 
| 
      
 13 
     | 
    
         
            +
             
     | 
| 
      
 14 
     | 
    
         
            +
            ParallelTests::RSpec::LoggerBaseBase = base
         
     | 
| 
      
 15 
     | 
    
         
            +
             
     | 
| 
      
 16 
     | 
    
         
            +
            class ParallelTests::RSpec::LoggerBase < ParallelTests::RSpec::LoggerBaseBase
         
     | 
| 
      
 17 
     | 
    
         
            +
              RSPEC_1 = !defined?(RSpec::Core::Formatters::BaseTextFormatter) # do not test for Spec, this will trigger deprecation warning in rspec 2
         
     | 
| 
      
 18 
     | 
    
         
            +
             
     | 
| 
      
 19 
     | 
    
         
            +
              def initialize(*args)
         
     | 
| 
      
 20 
     | 
    
         
            +
                super
         
     | 
| 
      
 21 
     | 
    
         
            +
             
     | 
| 
      
 22 
     | 
    
         
            +
                @output ||= args[1] || args[0] # rspec 1 has output as second argument
         
     | 
| 
      
 23 
     | 
    
         
            +
             
     | 
| 
      
 24 
     | 
    
         
            +
                if String === @output # a path ?
         
     | 
| 
      
 25 
     | 
    
         
            +
                  FileUtils.mkdir_p(File.dirname(@output))
         
     | 
| 
      
 26 
     | 
    
         
            +
                  File.open(@output, 'w'){} # overwrite previous results
         
     | 
| 
      
 27 
     | 
    
         
            +
                  @output = File.open(@output, 'a')
         
     | 
| 
      
 28 
     | 
    
         
            +
                elsif File === @output # close and restart in append mode
         
     | 
| 
      
 29 
     | 
    
         
            +
                  @output.close
         
     | 
| 
      
 30 
     | 
    
         
            +
                  @output = File.open(@output.path, 'a')
         
     | 
| 
      
 31 
     | 
    
         
            +
                end
         
     | 
| 
      
 32 
     | 
    
         
            +
              end
         
     | 
| 
      
 33 
     | 
    
         
            +
             
     | 
| 
      
 34 
     | 
    
         
            +
              #stolen from Rspec
         
     | 
| 
      
 35 
     | 
    
         
            +
              def close
         
     | 
| 
      
 36 
     | 
    
         
            +
                @output.close  if (IO === @output) & (@output != $stdout)
         
     | 
| 
      
 37 
     | 
    
         
            +
              end
         
     | 
| 
      
 38 
     | 
    
         
            +
             
     | 
| 
      
 39 
     | 
    
         
            +
              # do not let multiple processes get in each others way
         
     | 
| 
      
 40 
     | 
    
         
            +
              def lock_output
         
     | 
| 
      
 41 
     | 
    
         
            +
                if File === @output
         
     | 
| 
      
 42 
     | 
    
         
            +
                  begin
         
     | 
| 
      
 43 
     | 
    
         
            +
                    @output.flock File::LOCK_EX
         
     | 
| 
      
 44 
     | 
    
         
            +
                    yield
         
     | 
| 
      
 45 
     | 
    
         
            +
                  ensure
         
     | 
| 
      
 46 
     | 
    
         
            +
                    @output.flock File::LOCK_UN
         
     | 
| 
      
 47 
     | 
    
         
            +
                  end
         
     | 
| 
      
 48 
     | 
    
         
            +
                else
         
     | 
| 
      
 49 
     | 
    
         
            +
                  yield
         
     | 
| 
      
 50 
     | 
    
         
            +
                end
         
     | 
| 
      
 51 
     | 
    
         
            +
              end
         
     | 
| 
      
 52 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,59 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            require 'parallel_tests/test/runner'
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module ParallelTests
         
     | 
| 
      
 4 
     | 
    
         
            +
              module RSpec
         
     | 
| 
      
 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} #{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 
     | 
    
         
            +
                  def self.runtime_log
         
     | 
| 
      
 26 
     | 
    
         
            +
                    'tmp/parallel_runtime_rspec.log'
         
     | 
| 
      
 27 
     | 
    
         
            +
                  end
         
     | 
| 
      
 28 
     | 
    
         
            +
             
     | 
| 
      
 29 
     | 
    
         
            +
                  def self.test_file_name
         
     | 
| 
      
 30 
     | 
    
         
            +
                    "spec"
         
     | 
| 
      
 31 
     | 
    
         
            +
                  end
         
     | 
| 
      
 32 
     | 
    
         
            +
             
     | 
| 
      
 33 
     | 
    
         
            +
                  def self.test_suffix
         
     | 
| 
      
 34 
     | 
    
         
            +
                    "_spec.rb"
         
     | 
| 
      
 35 
     | 
    
         
            +
                  end
         
     | 
| 
      
 36 
     | 
    
         
            +
             
     | 
| 
      
 37 
     | 
    
         
            +
                  private
         
     | 
| 
      
 38 
     | 
    
         
            +
             
     | 
| 
      
 39 
     | 
    
         
            +
                  # so it can be stubbed....
         
     | 
| 
      
 40 
     | 
    
         
            +
                  def self.run(cmd)
         
     | 
| 
      
 41 
     | 
    
         
            +
                    `#{cmd}`
         
     | 
| 
      
 42 
     | 
    
         
            +
                  end
         
     | 
| 
      
 43 
     | 
    
         
            +
             
     | 
| 
      
 44 
     | 
    
         
            +
                  def self.rspec_1_color
         
     | 
| 
      
 45 
     | 
    
         
            +
                    'RSPEC_COLOR=1 ; export RSPEC_COLOR ;' if $stdout.tty?
         
     | 
| 
      
 46 
     | 
    
         
            +
                  end
         
     | 
| 
      
 47 
     | 
    
         
            +
             
     | 
| 
      
 48 
     | 
    
         
            +
                  def self.rspec_2_color
         
     | 
| 
      
 49 
     | 
    
         
            +
                    '--color --tty ' if $stdout.tty?
         
     | 
| 
      
 50 
     | 
    
         
            +
                  end
         
     | 
| 
      
 51 
     | 
    
         
            +
             
     | 
| 
      
 52 
     | 
    
         
            +
                  def self.spec_opts
         
     | 
| 
      
 53 
     | 
    
         
            +
                    options_file = ['.rspec_parallel', 'spec/parallel_spec.opts', 'spec/spec.opts'].detect{|f| File.file?(f) }
         
     | 
| 
      
 54 
     | 
    
         
            +
                    return unless options_file
         
     | 
| 
      
 55 
     | 
    
         
            +
                    "-O #{options_file}"
         
     | 
| 
      
 56 
     | 
    
         
            +
                  end
         
     | 
| 
      
 57 
     | 
    
         
            +
                end
         
     | 
| 
      
 58 
     | 
    
         
            +
              end
         
     | 
| 
      
 59 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,34 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            require 'parallel_tests/rspec/logger_base'
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            class ParallelTests::RSpec::RuntimeLogger < ParallelTests::RSpec::LoggerBase
         
     | 
| 
      
 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
         
     |