vinted-parallel_tests 0.13.3 → 1.7.0.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.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/Readme.md +86 -47
  3. data/bin/parallel_cucumber +5 -1
  4. data/bin/parallel_rspec +5 -1
  5. data/bin/parallel_spinach +9 -0
  6. data/bin/parallel_test +5 -1
  7. data/lib/parallel_tests.rb +15 -2
  8. data/lib/parallel_tests/cli.rb +115 -35
  9. data/lib/parallel_tests/cucumber/failures_logger.rb +9 -7
  10. data/lib/parallel_tests/cucumber/runner.rb +16 -77
  11. data/lib/parallel_tests/cucumber/scenario_line_logger.rb +52 -0
  12. data/lib/parallel_tests/cucumber/scenarios.rb +34 -0
  13. data/lib/parallel_tests/{cucumber → gherkin}/io.rb +1 -1
  14. data/lib/parallel_tests/{cucumber/gherkin_listener.rb → gherkin/listener.rb} +11 -6
  15. data/lib/parallel_tests/gherkin/runner.rb +116 -0
  16. data/lib/parallel_tests/{cucumber → gherkin}/runtime_logger.rb +2 -2
  17. data/lib/parallel_tests/grouper.rb +34 -17
  18. data/lib/parallel_tests/rspec/failures_logger.rb +22 -14
  19. data/lib/parallel_tests/rspec/logger_base.rb +5 -1
  20. data/lib/parallel_tests/rspec/runner.rb +5 -4
  21. data/lib/parallel_tests/rspec/runtime_logger.rb +10 -5
  22. data/lib/parallel_tests/rspec/summary_logger.rb +11 -11
  23. data/lib/parallel_tests/spinach/runner.rb +19 -0
  24. data/lib/parallel_tests/tasks.rb +41 -18
  25. data/lib/parallel_tests/test/runner.rb +80 -28
  26. data/lib/parallel_tests/test/runtime_logger.rb +86 -55
  27. data/lib/parallel_tests/version.rb +1 -1
  28. metadata +18 -35
  29. data/.gitignore +0 -2
  30. data/.rspec +0 -2
  31. data/.travis.yml +0 -6
  32. data/Gemfile +0 -8
  33. data/Gemfile.lock +0 -48
  34. data/Rakefile +0 -6
  35. data/ReadmeRails2.md +0 -48
  36. data/parallel_tests.gemspec +0 -14
  37. data/spec/integration_spec.rb +0 -285
  38. data/spec/parallel_tests/cli_spec.rb +0 -71
  39. data/spec/parallel_tests/cucumber/failure_logger_spec.rb +0 -43
  40. data/spec/parallel_tests/cucumber/gherkin_listener_spec.rb +0 -97
  41. data/spec/parallel_tests/cucumber/runner_spec.rb +0 -179
  42. data/spec/parallel_tests/grouper_spec.rb +0 -52
  43. data/spec/parallel_tests/rspec/failures_logger_spec.rb +0 -82
  44. data/spec/parallel_tests/rspec/runner_spec.rb +0 -187
  45. data/spec/parallel_tests/rspec/runtime_logger_spec.rb +0 -126
  46. data/spec/parallel_tests/rspec/summary_logger_spec.rb +0 -37
  47. data/spec/parallel_tests/tasks_spec.rb +0 -151
  48. data/spec/parallel_tests/test/runner_spec.rb +0 -413
  49. data/spec/parallel_tests/test/runtime_logger_spec.rb +0 -90
  50. data/spec/parallel_tests_spec.rb +0 -137
  51. data/spec/spec_helper.rb +0 -157
@@ -1,20 +1,22 @@
1
1
  require 'cucumber/formatter/rerun'
2
- require 'parallel_tests/cucumber/io'
2
+ require 'parallel_tests/gherkin/io'
3
3
 
4
4
  module ParallelTests
5
5
  module Cucumber
6
6
  class FailuresLogger < ::Cucumber::Formatter::Rerun
7
- include Io
7
+ include ParallelTests::Gherkin::Io
8
8
 
9
9
  def initialize(runtime, path_or_io, options)
10
+ super
10
11
  @io = prepare_io(path_or_io)
11
12
  end
12
13
 
13
- def after_feature(feature)
14
- unless @lines.empty?
15
- lock_output do
16
- @lines.each do |line|
17
- @io.puts "#{feature.file}:#{line}"
14
+ def done
15
+ return if @failures.empty?
16
+ lock_output do
17
+ @failures.each do |file, lines|
18
+ lines.each do |line|
19
+ @io.print "#{file}:#{line} "
18
20
  end
19
21
  end
20
22
  end
@@ -1,96 +1,35 @@
1
- require "parallel_tests/test/runner"
2
- require 'shellwords'
1
+ require "parallel_tests/gherkin/runner"
3
2
 
4
3
  module ParallelTests
5
4
  module Cucumber
6
- class Runner < ParallelTests::Test::Runner
7
- NAME = 'Cucumber'
8
-
5
+ class Runner < ParallelTests::Gherkin::Runner
9
6
  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"
7
+ def name
8
+ 'cucumber'
46
9
  end
47
10
 
48
11
  def line_is_result?(line)
49
- line =~ /^\d+ (steps?|scenarios?)/
12
+ super or line =~ failing_scenario_regex
50
13
  end
51
14
 
52
- # cucumber has 2 result lines per test run, that cannot be added
53
- # 1 scenario (1 failed)
54
- # 1 step (1 failed)
55
15
  def summarize_results(results)
56
- sort_order = %w[scenario step failed undefined skipped pending passed]
16
+ output = []
57
17
 
58
- %w[scenario step].map do |group|
59
- group_results = results.grep /^\d+ #{group}/
60
- next if group_results.empty?
18
+ failing_scenarios = results.grep(failing_scenario_regex)
19
+ if failing_scenarios.any?
20
+ failing_scenarios.unshift("Failing Scenarios:")
21
+ output << failing_scenarios.join("\n")
22
+ end
61
23
 
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
24
+ output << super
71
25
 
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
26
+ output.join("\n\n")
78
27
  end
79
28
 
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
29
+ private
87
30
 
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
31
+ def failing_scenario_regex
32
+ /^cucumber features\/.+:\d+/
94
33
  end
95
34
  end
96
35
  end
@@ -0,0 +1,52 @@
1
+ require 'gherkin/tag_expression'
2
+
3
+ module ParallelTests
4
+ module Cucumber
5
+ module Formatters
6
+ class ScenarioLineLogger
7
+ attr_reader :scenarios
8
+
9
+ def initialize(tag_expression = ::Gherkin::TagExpression.new([]))
10
+ raise "ScenarioLineLogger not supported anymore after upgrading to Cucumber 2.0, please fix!"
11
+ @scenarios = []
12
+ @tag_expression = tag_expression
13
+ end
14
+
15
+ def visit_feature_element(feature_element)
16
+ return unless @tag_expression.evaluate(feature_element.source_tags)
17
+
18
+ case feature_element
19
+ when ::Cucumber::Ast::Scenario
20
+ line = if feature_element.respond_to?(:line)
21
+ feature_element.line
22
+ else
23
+ feature_element.instance_variable_get(:@line)
24
+ end
25
+ @scenarios << [feature_element.feature.file, line].join(":")
26
+ when ::Cucumber::Ast::ScenarioOutline
27
+ sections = feature_element.instance_variable_get(:@example_sections)
28
+ sections.each { |section|
29
+ rows = if section[1].respond_to?(:rows)
30
+ section[1].rows
31
+ else
32
+ section[1].instance_variable_get(:@rows)
33
+ end
34
+ rows.each_with_index { |row, index|
35
+ next if index == 0 # slices didn't work with jruby data structure
36
+ line = if row.respond_to?(:line)
37
+ row.line
38
+ else
39
+ row.instance_variable_get(:@line)
40
+ end
41
+ @scenarios << [feature_element.feature.file, line].join(":")
42
+ }
43
+ }
44
+ end
45
+ end
46
+
47
+ def method_missing(*args)
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,34 @@
1
+ require 'gherkin/tag_expression'
2
+ require 'cucumber/runtime'
3
+ require 'cucumber'
4
+ require 'parallel_tests/cucumber/scenario_line_logger'
5
+ require 'parallel_tests/gherkin/listener'
6
+
7
+ module ParallelTests
8
+ module Cucumber
9
+ class Scenarios
10
+ class << self
11
+ def all(files, options={})
12
+ tags = []
13
+ tags.concat options[:ignore_tag_pattern].to_s.split(/\s*,\s*/).map {|tag| "~#{tag}" }
14
+ tags.concat options[:test_options].to_s.scan(/(?:-t|--tags) (~?@[\w,~@]+)/).flatten
15
+ split_into_scenarios files, tags.uniq
16
+ end
17
+
18
+ private
19
+
20
+ def split_into_scenarios(files, tags=[])
21
+ tag_expression = ::Gherkin::TagExpression.new(tags)
22
+ scenario_line_logger = ParallelTests::Cucumber::Formatters::ScenarioLineLogger.new(tag_expression)
23
+ loader = ::Cucumber::Runtime::FeaturesLoader.new(files, [], tag_expression)
24
+
25
+ loader.features.each do |feature|
26
+ feature.accept(scenario_line_logger)
27
+ end
28
+
29
+ scenario_line_logger.scenarios
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -1,7 +1,7 @@
1
1
  require 'parallel_tests'
2
2
 
3
3
  module ParallelTests
4
- module Cucumber
4
+ module Gherkin
5
5
  module Io
6
6
 
7
7
  def prepare_io(path_or_io)
@@ -1,8 +1,8 @@
1
1
  require 'gherkin'
2
2
 
3
3
  module ParallelTests
4
- module Cucumber
5
- class GherkinListener
4
+ module Gherkin
5
+ class Listener
6
6
  attr_reader :collect
7
7
 
8
8
  attr_writer :ignore_tag_pattern
@@ -48,17 +48,22 @@ module ParallelTests
48
48
  @collect[@uri] = 0
49
49
  end
50
50
 
51
- def examples(*args)
52
- @examples += 1
51
+ #
52
+ # @param [Gherkin::Formatter::Model::Examples] examples
53
+ #
54
+ def examples(examples)
55
+ if examples.rows.size > 0
56
+ @collect[@uri] += (@outline_steps * examples.rows.size)
57
+ end
53
58
  end
54
59
 
55
60
  def eof(*args)
56
- @collect[@uri] += (@background_steps * @scenarios) + (@outline_steps * @examples)
61
+ @collect[@uri] += (@background_steps * @scenarios)
57
62
  reset_counters!
58
63
  end
59
64
 
60
65
  def reset_counters!
61
- @examples = @outline = @outline_steps = @background = @background_steps = @scenarios = 0
66
+ @outline = @outline_steps = @background = @background_steps = @scenarios = 0
62
67
  @ignoring = nil
63
68
  end
64
69
 
@@ -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.reject(&:empty?).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.exist?("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
@@ -1,7 +1,7 @@
1
- require 'parallel_tests/cucumber/io'
1
+ require 'parallel_tests/gherkin/io'
2
2
 
3
3
  module ParallelTests
4
- module Cucumber
4
+ module Gherkin
5
5
  class RuntimeLogger
6
6
  include Io
7
7
 
@@ -6,34 +6,34 @@ module ParallelTests
6
6
  in_even_groups_by_size(features_with_steps, num_groups)
7
7
  end
8
8
 
9
- def in_even_groups_by_size(items_with_sizes, num_groups, options = {})
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= {})
10
15
  groups = Array.new(num_groups) { {:items => [], :size => 0} }
11
16
 
12
17
  # add all files that should run in a single process to one group
13
18
  (options[:single_process] || []).each do |pattern|
14
- matched, items_with_sizes = items_with_sizes.partition { |item, size| item =~ pattern }
19
+ matched, items = items.partition { |item, _size| item =~ pattern }
15
20
  matched.each { |item, size| add_to_group(groups.first, item, size) }
16
21
  end
17
22
 
18
23
  groups_to_fill = (options[:isolate] ? groups[1..-1] : groups)
24
+ group_features_by_size(items_to_group(items), groups_to_fill)
19
25
 
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 }
26
+ groups.map! { |g| g[:items].sort }
27
27
  end
28
28
 
29
29
  private
30
30
 
31
31
  def largest_first(files)
32
- files.sort_by{|item, size| size }.reverse
32
+ files.sort_by{|_item, size| size }.reverse
33
33
  end
34
34
 
35
35
  def smallest_group(groups)
36
- groups.min_by{|g| g[:size] }
36
+ groups.min_by { |g| g[:size] }
37
37
  end
38
38
 
39
39
  def add_to_group(group, item, size)
@@ -42,14 +42,31 @@ module ParallelTests
42
42
  end
43
43
 
44
44
  def build_features_with_steps(tests, options)
45
- require 'parallel_tests/cucumber/gherkin_listener'
46
- listener = Cucumber::GherkinListener.new
45
+ require 'parallel_tests/gherkin/listener'
46
+ listener = ParallelTests::Gherkin::Listener.new
47
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|
48
+ parser = ::Gherkin::Parser::Parser.new(listener, true, 'root')
49
+ tests.each do |file|
50
50
  parser.parse(File.read(file), file, 0)
51
- }
52
- listener.collect.sort_by{|_,value| -value }
51
+ end
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
53
70
  end
54
71
  end
55
72
  end