friendlyfashion-parallel_tests 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
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