parallel_tests 1.1.0 → 1.1.1

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