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
|