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.
Files changed (41) hide show
  1. data/.gitignore +2 -0
  2. data/Gemfile +8 -0
  3. data/Gemfile.lock +44 -0
  4. data/Rakefile +6 -0
  5. data/Readme.md +232 -0
  6. data/ReadmeRails2.md +48 -0
  7. data/bin/parallel_cucumber +2 -0
  8. data/bin/parallel_rspec +2 -0
  9. data/bin/parallel_test +6 -0
  10. data/lib/parallel_tests.rb +30 -0
  11. data/lib/parallel_tests/cli.rb +159 -0
  12. data/lib/parallel_tests/cucumber/gherkin_listener.rb +60 -0
  13. data/lib/parallel_tests/cucumber/runner.rb +90 -0
  14. data/lib/parallel_tests/cucumber/runtime_logger.rb +58 -0
  15. data/lib/parallel_tests/grouper.rb +53 -0
  16. data/lib/parallel_tests/railtie.rb +8 -0
  17. data/lib/parallel_tests/rspec/failures_logger.rb +44 -0
  18. data/lib/parallel_tests/rspec/logger_base.rb +52 -0
  19. data/lib/parallel_tests/rspec/runner.rb +59 -0
  20. data/lib/parallel_tests/rspec/runtime_logger.rb +34 -0
  21. data/lib/parallel_tests/rspec/summary_logger.rb +19 -0
  22. data/lib/parallel_tests/tasks.rb +134 -0
  23. data/lib/parallel_tests/test/runner.rb +134 -0
  24. data/lib/parallel_tests/test/runtime_logger.rb +92 -0
  25. data/lib/parallel_tests/version.rb +3 -0
  26. data/parallel_tests.gemspec +14 -0
  27. data/spec/integration_spec.rb +244 -0
  28. data/spec/parallel_tests/cli_spec.rb +36 -0
  29. data/spec/parallel_tests/cucumber/gherkin_listener_spec.rb +48 -0
  30. data/spec/parallel_tests/cucumber/runner_spec.rb +173 -0
  31. data/spec/parallel_tests/grouper_spec.rb +52 -0
  32. data/spec/parallel_tests/rspec/failure_logger_spec.rb +82 -0
  33. data/spec/parallel_tests/rspec/runner_spec.rb +178 -0
  34. data/spec/parallel_tests/rspec/runtime_logger_spec.rb +76 -0
  35. data/spec/parallel_tests/rspec/summary_logger_spec.rb +37 -0
  36. data/spec/parallel_tests/tasks_spec.rb +151 -0
  37. data/spec/parallel_tests/test/runner_spec.rb +273 -0
  38. data/spec/parallel_tests/test/runtime_logger_spec.rb +84 -0
  39. data/spec/parallel_tests_spec.rb +73 -0
  40. data/spec/spec_helper.rb +151 -0
  41. 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,8 @@
1
+ # rake tasks for Rails 3+
2
+ module ParallelTests
3
+ class Railtie < ::Rails::Railtie
4
+ rake_tasks do
5
+ require "parallel_tests/tasks"
6
+ end
7
+ end
8
+ 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