parallel_tests 1.1.0 → 1.1.1

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.
@@ -0,0 +1,116 @@
1
+ require "parallel_tests/test/runner"
2
+ require 'shellwords'
3
+
4
+ module ParallelTests
5
+ module Gherkin
6
+ class Runner < ParallelTests::Test::Runner
7
+
8
+ class << self
9
+ def run_tests(test_files, process_number, num_processes, options)
10
+ combined_scenarios = test_files
11
+
12
+ if options[:group_by] == :scenarios
13
+ grouped = test_files.map { |t| t.split(':') }.group_by(&:first)
14
+ combined_scenarios = grouped.map {|file,files_and_lines| "#{file}:#{files_and_lines.map(&:last).join(':')}" }
15
+ end
16
+
17
+ sanitized_test_files = combined_scenarios.map { |val| WINDOWS ? "\"#{val}\"" : Shellwords.escape(val) }
18
+
19
+ options[:env] ||= {}
20
+ options[:env] = options[:env].merge({'AUTOTEST' => '1'}) if $stdout.tty? # display color when we are in a terminal
21
+
22
+ cmd = [
23
+ executable,
24
+ (runtime_logging if File.directory?(File.dirname(runtime_log))),
25
+ cucumber_opts(options[:test_options]),
26
+ *sanitized_test_files
27
+ ].compact.join(' ')
28
+ execute_command(cmd, process_number, num_processes, options)
29
+ end
30
+
31
+ def test_file_name
32
+ @test_file_name || 'feature'
33
+ end
34
+
35
+ def test_suffix
36
+ /\.feature$/
37
+ end
38
+
39
+ def line_is_result?(line)
40
+ line =~ /^\d+ (steps?|scenarios?)/
41
+ end
42
+
43
+ # cucumber has 2 result lines per test run, that cannot be added
44
+ # 1 scenario (1 failed)
45
+ # 1 step (1 failed)
46
+ def summarize_results(results)
47
+ sort_order = %w[scenario step failed undefined skipped pending passed]
48
+
49
+ %w[scenario step].map do |group|
50
+ group_results = results.grep(/^\d+ #{group}/)
51
+ next if group_results.empty?
52
+
53
+ sums = sum_up_results(group_results)
54
+ sums = sums.sort_by { |word, _| sort_order.index(word) || 999 }
55
+ sums.map! do |word, number|
56
+ plural = "s" if word == group and number != 1
57
+ "#{number} #{word}#{plural}"
58
+ end
59
+ "#{sums[0]} (#{sums[1..-1].join(", ")})"
60
+ end.compact.join("\n")
61
+ end
62
+
63
+ def cucumber_opts(given)
64
+ if given =~ /--profile/ or given =~ /(^|\s)-p /
65
+ given
66
+ else
67
+ [given, profile_from_config].compact.join(" ")
68
+ end
69
+ end
70
+
71
+ def profile_from_config
72
+ # copied from https://github.com/cucumber/cucumber/blob/master/lib/cucumber/cli/profile_loader.rb#L85
73
+ config = Dir.glob("{,.config/,config/}#{name}{.yml,.yaml}").first
74
+ if config && File.read(config) =~ /^parallel:/
75
+ "--profile parallel"
76
+ end
77
+ end
78
+
79
+ def tests_in_groups(tests, num_groups, options={})
80
+ if options[:group_by] == :scenarios
81
+ @test_file_name = "scenario"
82
+ end
83
+ method = "by_#{options[:group_by]}"
84
+ if Grouper.respond_to?(method)
85
+ Grouper.send(method, find_tests(tests, options), num_groups, options)
86
+ else
87
+ super
88
+ end
89
+ end
90
+
91
+
92
+ def runtime_logging
93
+ " --format ParallelTests::Gherkin::RuntimeLogger --out #{runtime_log}"
94
+ end
95
+
96
+ def runtime_log
97
+ "tmp/parallel_runtime_#{name}.log"
98
+ end
99
+
100
+ def determine_executable
101
+ case
102
+ when File.exists?("bin/#{name}")
103
+ "bin/#{name}"
104
+ when ParallelTests.bundler_enabled?
105
+ "bundle exec #{name}"
106
+ when File.file?("script/#{name}")
107
+ "script/#{name}"
108
+ else
109
+ "#{name}"
110
+ end
111
+ end
112
+
113
+ end
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,28 @@
1
+ require 'parallel_tests/gherkin/io'
2
+
3
+ module ParallelTests
4
+ module Gherkin
5
+ class RuntimeLogger
6
+ include Io
7
+
8
+ def initialize(step_mother, path_or_io, options=nil)
9
+ @io = prepare_io(path_or_io)
10
+ @example_times = Hash.new(0)
11
+ end
12
+
13
+ def before_feature(_)
14
+ @start_at = ParallelTests.now.to_f
15
+ end
16
+
17
+ def after_feature(feature)
18
+ @example_times[feature.file] += ParallelTests.now.to_f - @start_at
19
+ end
20
+
21
+ def after_features(*args)
22
+ lock_output do
23
+ @io.puts @example_times.map { |file, time| "#{file}:#{time}" }
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,73 @@
1
+ module ParallelTests
2
+ class Grouper
3
+ class << self
4
+ def by_steps(tests, num_groups, options)
5
+ features_with_steps = build_features_with_steps(tests, options)
6
+ in_even_groups_by_size(features_with_steps, num_groups)
7
+ end
8
+
9
+ def by_scenarios(tests, num_groups, options={})
10
+ scenarios = group_by_scenarios(tests, options)
11
+ in_even_groups_by_size(scenarios, num_groups)
12
+ end
13
+
14
+ def in_even_groups_by_size(items, num_groups, options= {})
15
+ groups = Array.new(num_groups) { {:items => [], :size => 0} }
16
+
17
+ # add all files that should run in a single process to one group
18
+ (options[:single_process] || []).each do |pattern|
19
+ matched, items = items.partition { |item, size| item =~ pattern }
20
+ matched.each { |item, size| add_to_group(groups.first, item, size) }
21
+ end
22
+
23
+ groups_to_fill = (options[:isolate] ? groups[1..-1] : groups)
24
+ group_features_by_size(items_to_group(items), groups_to_fill)
25
+
26
+ groups.map!{|g| g[:items].sort }
27
+ end
28
+
29
+ private
30
+
31
+ def largest_first(files)
32
+ files.sort_by{|item, size| size }.reverse
33
+ end
34
+
35
+ def smallest_group(groups)
36
+ groups.min_by{|g| g[:size] }
37
+ end
38
+
39
+ def add_to_group(group, item, size)
40
+ group[:items] << item
41
+ group[:size] += size
42
+ end
43
+
44
+ def build_features_with_steps(tests, options)
45
+ require 'parallel_tests/gherkin/listener'
46
+ listener = ParallelTests::Gherkin::Listener.new
47
+ listener.ignore_tag_pattern = Regexp.compile(options[:ignore_tag_pattern]) if options[:ignore_tag_pattern]
48
+ parser = ::Gherkin::Parser::Parser.new(listener, true, 'root')
49
+ tests.each{|file|
50
+ parser.parse(File.read(file), file, 0)
51
+ }
52
+ listener.collect.sort_by{|_,value| -value }
53
+ end
54
+
55
+ def group_by_scenarios(tests, options={})
56
+ require 'parallel_tests/cucumber/scenarios'
57
+ ParallelTests::Cucumber::Scenarios.all(tests, options)
58
+ end
59
+
60
+ def group_features_by_size(items, groups_to_fill)
61
+ items.each do |item, size|
62
+ size ||= 1
63
+ smallest = smallest_group(groups_to_fill)
64
+ add_to_group(smallest, item, size)
65
+ end
66
+ end
67
+
68
+ def items_to_group(items)
69
+ items.first && items.first.size == 2 ? largest_first(items) : items
70
+ end
71
+ end
72
+ end
73
+ 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,54 @@
1
+ require 'parallel_tests/rspec/logger_base'
2
+ require 'parallel_tests/rspec/runner'
3
+
4
+ class ParallelTests::RSpec::FailuresLogger < ParallelTests::RSpec::LoggerBase
5
+ if RSPEC_3
6
+ RSpec::Core::Formatters.register self, :dump_failures, :dump_summary
7
+ end
8
+
9
+ # RSpec 1: does not keep track of failures, so we do
10
+ def example_failed(example, *args)
11
+ if RSPEC_1
12
+ @failed_examples ||= []
13
+ @failed_examples << example
14
+ else
15
+ super
16
+ end
17
+ end
18
+
19
+ if RSPEC_1
20
+ def dump_failure(*args)
21
+ end
22
+ else
23
+ def dump_failures(*args)
24
+ end
25
+ end
26
+
27
+ def dump_summary(*args)
28
+ lock_output do
29
+ if RSPEC_1
30
+ dump_commands_to_rerun_failed_examples_rspec_1
31
+ elsif RSPEC_3
32
+ notification = args.first
33
+ unless notification.failed_examples.empty?
34
+ colorizer = ::RSpec::Core::Formatters::ConsoleCodes
35
+ output.puts notification.colorized_rerun_commands(colorizer)
36
+ end
37
+ else
38
+ dump_commands_to_rerun_failed_examples
39
+ end
40
+ end
41
+ @output.flush
42
+ end
43
+
44
+ private
45
+
46
+ def dump_commands_to_rerun_failed_examples_rspec_1
47
+ (@failed_examples||[]).each do |example|
48
+ file, line = example.location.to_s.split(':')
49
+ next unless file and line
50
+ file.gsub!(%r(^.*?/spec/), './spec/')
51
+ @output.puts "#{ParallelTests::RSpec::Runner.send(:executable)} #{file}:#{line} # #{example.description}"
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,55 @@
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
+ RSPEC_3 = !RSPEC_1 && RSpec::Core::Version::STRING.start_with?('3')
19
+
20
+ def initialize(*args)
21
+ super
22
+
23
+ @output ||= args[1] || args[0] # rspec 1 has output as second argument
24
+
25
+ if String === @output # a path ?
26
+ FileUtils.mkdir_p(File.dirname(@output))
27
+ File.open(@output, 'w'){} # overwrite previous results
28
+ @output = File.open(@output, 'a')
29
+ elsif File === @output # close and restart in append mode
30
+ @output.close
31
+ @output = File.open(@output.path, 'a')
32
+ end
33
+ end
34
+
35
+ #stolen from Rspec
36
+ def close(*args)
37
+ @output.close if (IO === @output) & (@output != $stdout)
38
+ end
39
+
40
+ protected
41
+
42
+ # do not let multiple processes get in each others way
43
+ def lock_output
44
+ if File === @output
45
+ begin
46
+ @output.flock File::LOCK_EX
47
+ yield
48
+ ensure
49
+ @output.flock File::LOCK_UN
50
+ end
51
+ else
52
+ yield
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,73 @@
1
+ require "parallel_tests/test/runner"
2
+
3
+ module ParallelTests
4
+ module RSpec
5
+ class Runner < ParallelTests::Test::Runner
6
+ DEV_NULL = (WINDOWS ? "NUL" : "/dev/null")
7
+ NAME = 'RSpec'
8
+
9
+ class << self
10
+ def run_tests(test_files, process_number, num_processes, options)
11
+ exe = executable # expensive, so we cache
12
+ version = (exe =~ /\brspec\b/ ? 2 : 1)
13
+ cmd = [exe, options[:test_options], (rspec_2_color if version == 2), spec_opts, *test_files].compact.join(" ")
14
+ options = options.merge(:env => rspec_1_color) if version == 1
15
+ execute_command(cmd, process_number, num_processes, options)
16
+ end
17
+
18
+ def determine_executable
19
+ cmd = case
20
+ when File.exists?("bin/rspec")
21
+ WINDOWS ? "ruby bin/rspec" : "bin/rspec"
22
+ when File.file?("script/spec")
23
+ "script/spec"
24
+ when ParallelTests.bundler_enabled?
25
+ cmd = (run("bundle show rspec-core") =~ %r{Could not find gem.*} ? "spec" : "rspec")
26
+ "bundle exec #{cmd}"
27
+ else
28
+ %w[spec rspec].detect{|cmd| system "#{cmd} --version > #{DEV_NULL} 2>&1" }
29
+ end
30
+
31
+ cmd or raise("Can't find executables rspec or spec")
32
+ end
33
+
34
+ def runtime_log
35
+ 'tmp/parallel_runtime_rspec.log'
36
+ end
37
+
38
+ def test_file_name
39
+ "spec"
40
+ end
41
+
42
+ def test_suffix
43
+ /_spec\.rb$/
44
+ end
45
+
46
+ private
47
+
48
+ # so it can be stubbed....
49
+ def run(cmd)
50
+ `#{cmd}`
51
+ end
52
+
53
+ def rspec_1_color
54
+ if $stdout.tty?
55
+ {'RSPEC_COLOR' => "1"}
56
+ else
57
+ {}
58
+ end
59
+ end
60
+
61
+ def rspec_2_color
62
+ '--color --tty' if $stdout.tty?
63
+ end
64
+
65
+ def spec_opts
66
+ options_file = ['.rspec_parallel', 'spec/parallel_spec.opts', 'spec/spec.opts'].detect{|f| File.file?(f) }
67
+ return unless options_file
68
+ "-O #{options_file}"
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,59 @@
1
+ require 'parallel_tests'
2
+ require 'parallel_tests/rspec/logger_base'
3
+
4
+ class ParallelTests::RSpec::RuntimeLogger < ParallelTests::RSpec::LoggerBase
5
+ def initialize(*args)
6
+ super
7
+ @example_times = Hash.new(0)
8
+ @group_nesting = 0 unless RSPEC_1
9
+ end
10
+
11
+ if RSPEC_3
12
+ RSpec::Core::Formatters.register self, :example_group_started, :example_group_finished, :start_dump
13
+ end
14
+
15
+ if RSPEC_1
16
+ def example_started(*args)
17
+ @time = ParallelTests.now
18
+ super
19
+ end
20
+
21
+ def example_passed(example)
22
+ file = example.location.split(':').first
23
+ @example_times[file] += ParallelTests.now - @time
24
+ super
25
+ end
26
+ else
27
+ def example_group_started(example_group)
28
+ @time = ParallelTests.now if @group_nesting == 0
29
+ @group_nesting += 1
30
+ super
31
+ end
32
+
33
+ def example_group_finished(notification)
34
+ @group_nesting -= 1
35
+ if @group_nesting == 0
36
+ path = (RSPEC_3 ? notification.group.file_path : notification.file_path)
37
+ @example_times[path] += ParallelTests.now - @time
38
+ end
39
+ super if defined?(super)
40
+ end
41
+ end
42
+
43
+ def dump_summary(*args);end
44
+ def dump_failures(*args);end
45
+ def dump_failure(*args);end
46
+ def dump_pending(*args);end
47
+
48
+ def start_dump(*args)
49
+ return unless ENV['TEST_ENV_NUMBER'] #only record when running in parallel
50
+ # TODO: Figure out why sometimes time can be less than 0
51
+ lock_output do
52
+ @example_times.each do |file, time|
53
+ relative_path = file.sub(/^#{Regexp.escape Dir.pwd}\//,'')
54
+ @output.puts "#{relative_path}:#{time > 0 ? time : 0}"
55
+ end
56
+ end
57
+ @output.flush
58
+ end
59
+ end