vinted-parallel_tests 0.13.3

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 (47) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +2 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +6 -0
  5. data/Gemfile +8 -0
  6. data/Gemfile.lock +48 -0
  7. data/Rakefile +6 -0
  8. data/Readme.md +293 -0
  9. data/ReadmeRails2.md +48 -0
  10. data/bin/parallel_cucumber +5 -0
  11. data/bin/parallel_rspec +5 -0
  12. data/bin/parallel_test +5 -0
  13. data/lib/parallel_tests/cli.rb +187 -0
  14. data/lib/parallel_tests/cucumber/failures_logger.rb +25 -0
  15. data/lib/parallel_tests/cucumber/gherkin_listener.rb +82 -0
  16. data/lib/parallel_tests/cucumber/io.rb +41 -0
  17. data/lib/parallel_tests/cucumber/runner.rb +98 -0
  18. data/lib/parallel_tests/cucumber/runtime_logger.rb +28 -0
  19. data/lib/parallel_tests/grouper.rb +56 -0
  20. data/lib/parallel_tests/railtie.rb +8 -0
  21. data/lib/parallel_tests/rspec/failures_logger.rb +44 -0
  22. data/lib/parallel_tests/rspec/logger_base.rb +52 -0
  23. data/lib/parallel_tests/rspec/runner.rb +72 -0
  24. data/lib/parallel_tests/rspec/runtime_logger.rb +54 -0
  25. data/lib/parallel_tests/rspec/summary_logger.rb +19 -0
  26. data/lib/parallel_tests/tasks.rb +139 -0
  27. data/lib/parallel_tests/test/runner.rb +168 -0
  28. data/lib/parallel_tests/test/runtime_logger.rb +97 -0
  29. data/lib/parallel_tests/version.rb +3 -0
  30. data/lib/parallel_tests.rb +61 -0
  31. data/parallel_tests.gemspec +14 -0
  32. data/spec/integration_spec.rb +285 -0
  33. data/spec/parallel_tests/cli_spec.rb +71 -0
  34. data/spec/parallel_tests/cucumber/failure_logger_spec.rb +43 -0
  35. data/spec/parallel_tests/cucumber/gherkin_listener_spec.rb +97 -0
  36. data/spec/parallel_tests/cucumber/runner_spec.rb +179 -0
  37. data/spec/parallel_tests/grouper_spec.rb +52 -0
  38. data/spec/parallel_tests/rspec/failures_logger_spec.rb +82 -0
  39. data/spec/parallel_tests/rspec/runner_spec.rb +187 -0
  40. data/spec/parallel_tests/rspec/runtime_logger_spec.rb +126 -0
  41. data/spec/parallel_tests/rspec/summary_logger_spec.rb +37 -0
  42. data/spec/parallel_tests/tasks_spec.rb +151 -0
  43. data/spec/parallel_tests/test/runner_spec.rb +413 -0
  44. data/spec/parallel_tests/test/runtime_logger_spec.rb +90 -0
  45. data/spec/parallel_tests_spec.rb +137 -0
  46. data/spec/spec_helper.rb +157 -0
  47. metadata +110 -0
@@ -0,0 +1,82 @@
1
+ require 'gherkin'
2
+
3
+ module ParallelTests
4
+ module Cucumber
5
+ class GherkinListener
6
+ attr_reader :collect
7
+
8
+ attr_writer :ignore_tag_pattern
9
+
10
+ def initialize
11
+ @steps, @uris = [], []
12
+ @collect = {}
13
+ reset_counters!
14
+ end
15
+
16
+ def feature(feature)
17
+ @feature = feature
18
+ end
19
+
20
+ def background(*args)
21
+ @background = 1
22
+ end
23
+
24
+ def scenario(scenario)
25
+ @outline = @background = 0
26
+ return if should_ignore(scenario)
27
+ @scenarios += 1
28
+ end
29
+
30
+ def scenario_outline(outline)
31
+ return if should_ignore(outline)
32
+ @outline = 1
33
+ end
34
+
35
+ def step(*args)
36
+ return if @ignoring
37
+ if @background == 1
38
+ @background_steps += 1
39
+ elsif @outline > 0
40
+ @outline_steps += 1
41
+ else
42
+ @collect[@uri] += 1
43
+ end
44
+ end
45
+
46
+ def uri(path)
47
+ @uri = path
48
+ @collect[@uri] = 0
49
+ end
50
+
51
+ def examples(*args)
52
+ @examples += 1
53
+ end
54
+
55
+ def eof(*args)
56
+ @collect[@uri] += (@background_steps * @scenarios) + (@outline_steps * @examples)
57
+ reset_counters!
58
+ end
59
+
60
+ def reset_counters!
61
+ @examples = @outline = @outline_steps = @background = @background_steps = @scenarios = 0
62
+ @ignoring = nil
63
+ end
64
+
65
+ # ignore lots of other possible callbacks ...
66
+ def method_missing(*args)
67
+ end
68
+
69
+ private
70
+
71
+ # Return a combination of tags declared on this scenario/outline and the feature it belongs to
72
+ def all_tags(scenario)
73
+ (scenario.tags || []) + ((@feature && @feature.tags) || [])
74
+ end
75
+
76
+ # Set @ignoring if we should ignore this scenario/outline based on its tags
77
+ def should_ignore(scenario)
78
+ @ignoring = @ignore_tag_pattern && all_tags(scenario).find{ |tag| @ignore_tag_pattern === tag.name }
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,41 @@
1
+ require 'parallel_tests'
2
+
3
+ module ParallelTests
4
+ module Cucumber
5
+ module Io
6
+
7
+ def prepare_io(path_or_io)
8
+ if path_or_io.respond_to?(:write)
9
+ path_or_io
10
+ else # its a path
11
+ File.open(path_or_io, 'w').close # clean out the file
12
+ file = File.open(path_or_io, 'a')
13
+
14
+ at_exit do
15
+ unless file.closed?
16
+ file.flush
17
+ file.close
18
+ end
19
+ end
20
+
21
+ file
22
+ end
23
+ end
24
+
25
+ # do not let multiple processes get in each others way
26
+ def lock_output
27
+ if File === @io
28
+ begin
29
+ @io.flock File::LOCK_EX
30
+ yield
31
+ ensure
32
+ @io.flock File::LOCK_UN
33
+ end
34
+ else
35
+ yield
36
+ end
37
+ end
38
+
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,98 @@
1
+ require "parallel_tests/test/runner"
2
+ require 'shellwords'
3
+
4
+ module ParallelTests
5
+ module Cucumber
6
+ class Runner < ParallelTests::Test::Runner
7
+ NAME = 'Cucumber'
8
+
9
+ class << self
10
+ def run_tests(test_files, process_number, num_processes, options)
11
+ sanitized_test_files = test_files.map { |val| Shellwords.escape(val) }
12
+ options = options.merge(:env => {"AUTOTEST" => "1"}) if $stdout.tty? # display color when we are in a terminal
13
+ runtime_logging = " --format ParallelTests::Cucumber::RuntimeLogger --out #{runtime_log}"
14
+ cmd = [
15
+ executable,
16
+ (runtime_logging if File.directory?(File.dirname(runtime_log))),
17
+ cucumber_opts(options[:test_options]),
18
+ *sanitized_test_files
19
+ ].compact.join(" ")
20
+ execute_command(cmd, process_number, num_processes, options)
21
+ end
22
+
23
+ def determine_executable
24
+ case
25
+ when File.exists?("bin/cucumber")
26
+ "bin/cucumber"
27
+ when ParallelTests.bundler_enabled?
28
+ "bundle exec cucumber"
29
+ when File.file?("script/cucumber")
30
+ "script/cucumber"
31
+ else
32
+ "cucumber"
33
+ end
34
+ end
35
+
36
+ def runtime_log
37
+ 'tmp/parallel_runtime_cucumber.log'
38
+ end
39
+
40
+ def test_file_name
41
+ "feature"
42
+ end
43
+
44
+ def test_suffix
45
+ ".feature"
46
+ end
47
+
48
+ def line_is_result?(line)
49
+ line =~ /^\d+ (steps?|scenarios?)/
50
+ end
51
+
52
+ # cucumber has 2 result lines per test run, that cannot be added
53
+ # 1 scenario (1 failed)
54
+ # 1 step (1 failed)
55
+ def summarize_results(results)
56
+ sort_order = %w[scenario step failed undefined skipped pending passed]
57
+
58
+ %w[scenario step].map do |group|
59
+ group_results = results.grep /^\d+ #{group}/
60
+ next if group_results.empty?
61
+
62
+ sums = sum_up_results(group_results)
63
+ sums = sums.sort_by { |word, _| sort_order.index(word) || 999 }
64
+ sums.map! do |word, number|
65
+ plural = "s" if word == group and number != 1
66
+ "#{number} #{word}#{plural}"
67
+ end
68
+ "#{sums[0]} (#{sums[1..-1].join(", ")})"
69
+ end.compact.join("\n")
70
+ end
71
+
72
+ def cucumber_opts(given)
73
+ if given =~ /--profile/ or given =~ /(^|\s)-p /
74
+ given
75
+ else
76
+ [given, profile_from_config].compact.join(" ")
77
+ end
78
+ end
79
+
80
+ def profile_from_config
81
+ # copied from https://github.com/cucumber/cucumber/blob/master/lib/cucumber/cli/profile_loader.rb#L85
82
+ config = Dir.glob('{,.config/,config/}cucumber{.yml,.yaml}').first
83
+ if config && File.read(config) =~ /^parallel:/
84
+ "--profile parallel"
85
+ end
86
+ end
87
+
88
+ def tests_in_groups(tests, num_groups, options={})
89
+ if options[:group_by] == :steps
90
+ Grouper.by_steps(find_tests(tests, options), num_groups, options)
91
+ else
92
+ super
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,28 @@
1
+ require 'parallel_tests/cucumber/io'
2
+
3
+ module ParallelTests
4
+ module Cucumber
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,56 @@
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 in_even_groups_by_size(items_with_sizes, num_groups, options = {})
10
+ groups = Array.new(num_groups) { {:items => [], :size => 0} }
11
+
12
+ # add all files that should run in a single process to one group
13
+ (options[:single_process] || []).each do |pattern|
14
+ matched, items_with_sizes = items_with_sizes.partition { |item, size| item =~ pattern }
15
+ matched.each { |item, size| add_to_group(groups.first, item, size) }
16
+ end
17
+
18
+ groups_to_fill = (options[:isolate] ? groups[1..-1] : groups)
19
+
20
+ # add all other files
21
+ largest_first(items_with_sizes).each do |item, size|
22
+ smallest = smallest_group(groups_to_fill)
23
+ add_to_group(smallest, item, size)
24
+ end
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/cucumber/gherkin_listener'
46
+ listener = Cucumber::GherkinListener.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
+ end
55
+ end
56
+ 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.send(: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,72 @@
1
+ require "parallel_tests/test/runner"
2
+
3
+ module ParallelTests
4
+ module RSpec
5
+ class Runner < ParallelTests::Test::Runner
6
+ NAME = 'RSpec'
7
+
8
+ class << self
9
+ def run_tests(test_files, process_number, num_processes, options)
10
+ exe = executable # expensive, so we cache
11
+ version = (exe =~ /\brspec\b/ ? 2 : 1)
12
+ cmd = [exe, options[:test_options], (rspec_2_color if version == 2), spec_opts, *test_files].compact.join(" ")
13
+ options = options.merge(:env => rspec_1_color) if version == 1
14
+ execute_command(cmd, process_number, num_processes, options)
15
+ end
16
+
17
+ def determine_executable
18
+ cmd = case
19
+ when File.exists?("bin/rspec")
20
+ "bin/rspec"
21
+ when File.file?("script/spec")
22
+ "script/spec"
23
+ when ParallelTests.bundler_enabled?
24
+ cmd = (run("bundle show rspec-core") =~ %r{Could not find gem.*} ? "spec" : "rspec")
25
+ "bundle exec #{cmd}"
26
+ else
27
+ %w[spec rspec].detect{|cmd| system "#{cmd} --version > /dev/null 2>&1" }
28
+ end
29
+
30
+ cmd or raise("Can't find executables rspec or spec")
31
+ end
32
+
33
+ def runtime_log
34
+ 'tmp/parallel_runtime_rspec.log'
35
+ end
36
+
37
+ def test_file_name
38
+ "spec"
39
+ end
40
+
41
+ def test_suffix
42
+ "_spec.rb"
43
+ end
44
+
45
+ private
46
+
47
+ # so it can be stubbed....
48
+ def run(cmd)
49
+ `#{cmd}`
50
+ end
51
+
52
+ def rspec_1_color
53
+ if $stdout.tty?
54
+ {'RSPEC_COLOR' => "1"}
55
+ else
56
+ {}
57
+ end
58
+ end
59
+
60
+ def rspec_2_color
61
+ '--color --tty' if $stdout.tty?
62
+ end
63
+
64
+ def spec_opts
65
+ options_file = ['.rspec_parallel', 'spec/parallel_spec.opts', 'spec/spec.opts'].detect{|f| File.file?(f) }
66
+ return unless options_file
67
+ "-O #{options_file}"
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,54 @@
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 if !RSPEC_1
9
+ end
10
+
11
+ if RSPEC_1
12
+ def example_started(*args)
13
+ @time = ParallelTests.now
14
+ super
15
+ end
16
+
17
+ def example_passed(example)
18
+ file = example.location.split(':').first
19
+ @example_times[file] += ParallelTests.now - @time
20
+ super
21
+ end
22
+ else
23
+ def example_group_started(example_group)
24
+ @time = ParallelTests.now if @group_nesting == 0
25
+ @group_nesting += 1
26
+ super
27
+ end
28
+
29
+ def example_group_finished(example_group)
30
+ @group_nesting -= 1
31
+ if @group_nesting == 0
32
+ @example_times[example_group.file_path] += ParallelTests.now - @time
33
+ end
34
+ super
35
+ end
36
+ end
37
+
38
+ def dump_summary(*args);end
39
+ def dump_failures(*args);end
40
+ def dump_failure(*args);end
41
+ def dump_pending(*args);end
42
+
43
+ def start_dump(*args)
44
+ return unless ENV['TEST_ENV_NUMBER'] #only record when running in parallel
45
+ # TODO: Figure out why sometimes time can be less than 0
46
+ lock_output do
47
+ @example_times.each do |file, time|
48
+ relative_path = file.sub(/^#{Regexp.escape Dir.pwd}\//,'')
49
+ @output.puts "#{relative_path}:#{time > 0 ? time : 0}"
50
+ end
51
+ end
52
+ @output.flush
53
+ end
54
+ end
@@ -0,0 +1,19 @@
1
+ require 'parallel_tests/rspec/failures_logger'
2
+
3
+ class ParallelTests::RSpec::SummaryLogger < ParallelTests::RSpec::LoggerBase
4
+ # RSpec 1: dumps 1 failed spec
5
+ def dump_failure(*args)
6
+ lock_output do
7
+ super
8
+ end
9
+ @output.flush
10
+ end
11
+
12
+ # RSpec 2: dumps all failed specs
13
+ def dump_failures(*args)
14
+ lock_output do
15
+ super
16
+ end
17
+ @output.flush
18
+ end
19
+ end