parallel_tests 2.28.0 → 3.7.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.
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'cucumber/formatter/rerun'
2
3
  require 'parallel_tests/gherkin/io'
3
4
 
@@ -21,7 +22,6 @@ module ParallelTests
21
22
  end
22
23
  end
23
24
  end
24
-
25
25
  end
26
26
  end
27
27
  end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+ begin
3
+ gem "cuke_modeler", "~> 3.0"
4
+ require 'cuke_modeler'
5
+ rescue LoadError
6
+ raise 'Grouping by number of cucumber steps requires the `cuke_modeler` modeler gem with requirement `~> 3.0`. Add `gem "cuke_modeler", "~> 3.0"` to your `Gemfile`, run `bundle install` and try again.'
7
+ end
8
+
9
+ module ParallelTests
10
+ module Cucumber
11
+ class FeaturesWithSteps
12
+ class << self
13
+ def all(tests, options)
14
+ ignore_tag_pattern = options[:ignore_tag_pattern].nil? ? nil : Regexp.compile(options[:ignore_tag_pattern])
15
+ # format of hash will be FILENAME => NUM_STEPS
16
+ steps_per_file = tests.each_with_object({}) do |file, steps|
17
+ feature = ::CukeModeler::FeatureFile.new(file).feature
18
+
19
+ # skip feature if it matches tag regex
20
+ next if feature.tags.grep(ignore_tag_pattern).any?
21
+
22
+ # count the number of steps in the file
23
+ # will only include a feature if the regex does not match
24
+ all_steps = feature.scenarios.map { |a| a.steps.count if a.tags.grep(ignore_tag_pattern).empty? }.compact
25
+ steps[file] = all_steps.sum
26
+ end
27
+ steps_per_file.sort_by { |_, value| -value }
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -1,16 +1,21 @@
1
+ # frozen_string_literal: true
1
2
  require "parallel_tests/gherkin/runner"
2
3
 
3
4
  module ParallelTests
4
5
  module Cucumber
5
6
  class Runner < ParallelTests::Gherkin::Runner
6
- SCENARIOS_RESULTS_BOUNDARY_REGEX = /^(Failing|Flaky) Scenarios:$/
7
- SCENARIO_REGEX = /^cucumber features\/.+:\d+/
7
+ SCENARIOS_RESULTS_BOUNDARY_REGEX = /^(Failing|Flaky) Scenarios:$/.freeze
8
+ SCENARIO_REGEX = %r{^cucumber features/.+:\d+}.freeze
8
9
 
9
10
  class << self
10
11
  def name
11
12
  'cucumber'
12
13
  end
13
14
 
15
+ def default_test_folder
16
+ 'features'
17
+ end
18
+
14
19
  def line_is_result?(line)
15
20
  super || line =~ SCENARIO_REGEX || line =~ SCENARIOS_RESULTS_BOUNDARY_REGEX
16
21
  end
@@ -21,9 +26,7 @@ module ParallelTests
21
26
  scenario_groups = results.slice_before(SCENARIOS_RESULTS_BOUNDARY_REGEX).group_by(&:first)
22
27
  scenario_groups.each do |header, group|
23
28
  scenarios = group.flatten.grep(SCENARIO_REGEX)
24
- if scenarios.any?
25
- output << ([header] + scenarios).join("\n")
26
- end
29
+ output << ([header] + scenarios).join("\n") if scenarios.any?
27
30
  end
28
31
 
29
32
  output << super
@@ -1,37 +1,34 @@
1
- require 'cucumber/tag_expressions/parser'
2
- require 'cucumber/core/gherkin/tag_expression'
3
-
1
+ # frozen_string_literal: true
4
2
  module ParallelTests
5
3
  module Cucumber
6
4
  module Formatters
7
5
  class ScenarioLineLogger
8
6
  attr_reader :scenarios
9
7
 
10
- def initialize(tag_expression = ::Cucumber::Core::Gherkin::TagExpression.new([]))
8
+ def initialize(tag_expression = nil)
11
9
  @scenarios = []
12
10
  @tag_expression = tag_expression
13
11
  end
14
12
 
15
13
  def visit_feature_element(uri, feature_element, feature_tags, line_numbers: [])
16
- scenario_tags = feature_element[:tags].map { |tag| tag[:name] }
14
+ scenario_tags = feature_element.tags.map(&:name)
17
15
  scenario_tags = feature_tags + scenario_tags
18
- if feature_element[:examples].nil? # :Scenario
19
- test_line = feature_element[:location][:line]
16
+ if feature_element.is_a?(CukeModeler::Scenario) # :Scenario
17
+ test_line = feature_element.source_line
20
18
 
21
19
  # We don't accept the feature_element if the current tags are not valid
22
- return unless @tag_expression.evaluate(scenario_tags)
20
+ return unless matches_tags?(scenario_tags)
23
21
  # or if it is not at the correct location
24
22
  return if line_numbers.any? && !line_numbers.include?(test_line)
25
23
 
26
- @scenarios << [uri, feature_element[:location][:line]].join(":")
24
+ @scenarios << [uri, feature_element.source_line].join(":")
27
25
  else # :ScenarioOutline
28
- feature_element[:examples].each do |example|
29
- example_tags = example[:tags].map { |tag| tag[:name] }
26
+ feature_element.examples.each do |example|
27
+ example_tags = example.tags.map(&:name)
30
28
  example_tags = scenario_tags + example_tags
31
- next unless @tag_expression.evaluate(example_tags)
32
- rows = example[:tableBody].select { |body| body[:type] == :TableRow }
33
- rows.each do |row|
34
- test_line = row[:location][:line]
29
+ next unless matches_tags?(example_tags)
30
+ example.rows[1..-1].each do |row|
31
+ test_line = row.source_line
35
32
  next if line_numbers.any? && !line_numbers.include?(test_line)
36
33
 
37
34
  @scenarios << [uri, test_line].join(':')
@@ -40,7 +37,12 @@ module ParallelTests
40
37
  end
41
38
  end
42
39
 
43
- def method_missing(*args)
40
+ def method_missing(*); end # # rubocop:disable Style/MissingRespondToMissing
41
+
42
+ private
43
+
44
+ def matches_tags?(tags)
45
+ @tag_expression.nil? || @tag_expression.evaluate(tags)
44
46
  end
45
47
  end
46
48
  end
@@ -1,21 +1,27 @@
1
+ # frozen_string_literal: true
1
2
  require 'cucumber/tag_expressions/parser'
2
- require 'cucumber/core/gherkin/tag_expression'
3
3
  require 'cucumber/runtime'
4
4
  require 'cucumber'
5
5
  require 'parallel_tests/cucumber/scenario_line_logger'
6
6
  require 'parallel_tests/gherkin/listener'
7
- require 'gherkin/errors'
8
7
  require 'shellwords'
9
8
 
9
+ begin
10
+ gem "cuke_modeler", "~> 3.0"
11
+ require 'cuke_modeler'
12
+ rescue LoadError
13
+ raise 'Grouping by individual cucumber scenarios requires the `cuke_modeler` modeler gem with requirement `~> 3.0`. Add `gem "cuke_modeler", "~> 3.0"` to your `Gemfile`, run `bundle install` and try again.'
14
+ end
15
+
10
16
  module ParallelTests
11
17
  module Cucumber
12
18
  class Scenarios
13
19
  class << self
14
- def all(files, options={})
20
+ def all(files, options = {})
15
21
  # Parse tag expression from given test options and ignore tag pattern. Refer here to understand how new tag expression syntax works - https://github.com/cucumber/cucumber/tree/master/tag-expressions
16
22
  tags = []
17
23
  words = options[:test_options].to_s.shellsplit
18
- words.each_with_index { |w,i| tags << words[i+1] if ["-t", "--tags"].include?(w) }
24
+ words.each_with_index { |w, i| tags << words[i + 1] if ["-t", "--tags"].include?(w) }
19
25
  if ignore = options[:ignore_tag_pattern]
20
26
  tags << "not (#{ignore})"
21
27
  end
@@ -26,8 +32,7 @@ module ParallelTests
26
32
 
27
33
  private
28
34
 
29
- def split_into_scenarios(files, tags='')
30
-
35
+ def split_into_scenarios(files, tags = '')
31
36
  # Create the tag expression instance from cucumber tag expressions parser, this is needed to know if the scenario matches with the tags invoked by the request
32
37
  # Create the ScenarioLineLogger which will filter the scenario we want
33
38
  args = []
@@ -35,37 +40,22 @@ module ParallelTests
35
40
  scenario_line_logger = ParallelTests::Cucumber::Formatters::ScenarioLineLogger.new(*args)
36
41
 
37
42
  # here we loop on the files map, each file will contain one or more scenario
38
- features ||= files.map do |path|
43
+ files.each do |path|
39
44
  # Gather up any line numbers attached to the file path
40
45
  path, *test_lines = path.split(/:(?=\d+)/)
41
46
  test_lines.map!(&:to_i)
42
47
 
43
- # We encode the file and get the content of it
44
- source = ::Cucumber::Runtime::NormalisedEncodingFile.read(path)
45
48
  # We create a Gherkin document, this will be used to decode the details of each scenario
46
- document = ::Cucumber::Core::Gherkin::Document.new(path, source)
47
-
48
- # We create a parser for the gherkin document
49
- parser = ::Gherkin::Parser.new()
50
- scanner = ::Gherkin::TokenScanner.new(document.body)
51
-
52
- begin
53
- # We make an attempt to parse the gherkin document, this could be failed if the document is not well formatted
54
- result = parser.parse(scanner)
55
- feature_tags = result[:feature][:tags].map { |tag| tag[:name] }
56
-
57
- # We loop on each children of the feature
58
- result[:feature][:children].each do |feature_element|
59
- # If the type of the child is not a scenario or scenario outline, we continue, we are only interested by the name of the scenario here
60
- next unless /Scenario/.match(feature_element[:type])
49
+ document = ::CukeModeler::FeatureFile.new(path)
50
+ feature = document.feature
61
51
 
62
- # It's a scenario, we add it to the scenario_line_logger
63
- scenario_line_logger.visit_feature_element(document.uri, feature_element, feature_tags, line_numbers: test_lines)
64
- end
52
+ # We make an attempt to parse the gherkin document, this could be failed if the document is not well formatted
53
+ feature_tags = feature.tags.map(&:name)
65
54
 
66
- rescue StandardError => e
67
- # Exception if the document is no well formated or error in the tags
68
- raise ::Cucumber::Core::Gherkin::ParseError.new("#{document.uri}: #{e.message}")
55
+ # We loop on each children of the feature
56
+ feature.tests.each do |test|
57
+ # It's a scenario, we add it to the scenario_line_logger
58
+ scenario_line_logger.visit_feature_element(document.path, test, feature_tags, line_numbers: test_lines)
69
59
  end
70
60
  end
71
61
 
@@ -1,9 +1,9 @@
1
+ # frozen_string_literal: true
1
2
  require 'parallel_tests'
2
3
 
3
4
  module ParallelTests
4
5
  module Gherkin
5
6
  module Io
6
-
7
7
  def prepare_io(path_or_io)
8
8
  if path_or_io.respond_to?(:write)
9
9
  path_or_io
@@ -24,7 +24,7 @@ module ParallelTests
24
24
 
25
25
  # do not let multiple processes get in each others way
26
26
  def lock_output
27
- if File === @io
27
+ if @io.is_a?(File)
28
28
  begin
29
29
  @io.flock File::LOCK_EX
30
30
  yield
@@ -35,7 +35,6 @@ module ParallelTests
35
35
  yield
36
36
  end
37
37
  end
38
-
39
38
  end
40
39
  end
41
40
  end
@@ -1,5 +1,4 @@
1
- require 'gherkin/parser'
2
-
1
+ # frozen_string_literal: true
3
2
  module ParallelTests
4
3
  module Gherkin
5
4
  class Listener
@@ -8,8 +7,10 @@ module ParallelTests
8
7
  attr_writer :ignore_tag_pattern
9
8
 
10
9
  def initialize
11
- @steps, @uris = [], []
10
+ @steps = []
11
+ @uris = []
12
12
  @collect = {}
13
+ @feature, @ignore_tag_pattern = nil
13
14
  reset_counters!
14
15
  end
15
16
 
@@ -17,7 +18,7 @@ module ParallelTests
17
18
  @feature = feature
18
19
  end
19
20
 
20
- def background(*args)
21
+ def background(*)
21
22
  @background = 1
22
23
  end
23
24
 
@@ -32,7 +33,7 @@ module ParallelTests
32
33
  @outline = 1
33
34
  end
34
35
 
35
- def step(*args)
36
+ def step(*)
36
37
  return if @ignoring
37
38
  if @background == 1
38
39
  @background_steps += 1
@@ -52,12 +53,10 @@ module ParallelTests
52
53
  # @param [Gherkin::Formatter::Model::Examples] examples
53
54
  #
54
55
  def examples(examples)
55
- if examples.rows.size > 0
56
- @collect[@uri] += (@outline_steps * examples.rows.size)
57
- end
56
+ @collect[@uri] += (@outline_steps * examples.rows.size) unless examples.rows.empty?
58
57
  end
59
58
 
60
- def eof(*args)
59
+ def eof(*)
61
60
  @collect[@uri] += (@background_steps * @scenarios)
62
61
  reset_counters!
63
62
  end
@@ -68,8 +67,7 @@ module ParallelTests
68
67
  end
69
68
 
70
69
  # ignore lots of other possible callbacks ...
71
- def method_missing(*args)
72
- end
70
+ def method_missing(*); end # rubocop:disable Style/MissingRespondToMissing
73
71
 
74
72
  private
75
73
 
@@ -80,7 +78,7 @@ module ParallelTests
80
78
 
81
79
  # Set @ignoring if we should ignore this scenario/outline based on its tags
82
80
  def should_ignore(scenario)
83
- @ignoring = @ignore_tag_pattern && all_tags(scenario).find{ |tag| @ignore_tag_pattern === tag.name }
81
+ @ignoring = @ignore_tag_pattern && all_tags(scenario).find { |tag| @ignore_tag_pattern === tag.name }
84
82
  end
85
83
  end
86
84
  end
@@ -1,23 +1,25 @@
1
+ # frozen_string_literal: true
1
2
  require "parallel_tests/test/runner"
2
3
  require 'shellwords'
3
4
 
4
5
  module ParallelTests
5
6
  module Gherkin
6
7
  class Runner < ParallelTests::Test::Runner
7
-
8
8
  class << self
9
9
  def run_tests(test_files, process_number, num_processes, options)
10
10
  combined_scenarios = test_files
11
11
 
12
12
  if options[:group_by] == :scenarios
13
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(':')}" }
14
+ combined_scenarios = grouped.map do |file, files_and_lines|
15
+ "#{file}:#{files_and_lines.map(&:last).join(':')}"
16
+ end
15
17
  end
16
18
 
17
19
  sanitized_test_files = combined_scenarios.map { |val| WINDOWS ? "\"#{val}\"" : Shellwords.escape(val) }
18
20
 
19
21
  options[:env] ||= {}
20
- options[:env] = options[:env].merge({'AUTOTEST' => '1'}) if $stdout.tty? # display color when we are in a terminal
22
+ options[:env] = options[:env].merge({ 'AUTOTEST' => '1' }) if $stdout.tty?
21
23
 
22
24
  cmd = [
23
25
  executable,
@@ -32,6 +34,10 @@ module ParallelTests
32
34
  @test_file_name || 'feature'
33
35
  end
34
36
 
37
+ def default_test_folder
38
+ 'features'
39
+ end
40
+
35
41
  def test_suffix
36
42
  /\.feature$/
37
43
  end
@@ -44,16 +50,16 @@ module ParallelTests
44
50
  # 1 scenario (1 failed)
45
51
  # 1 step (1 failed)
46
52
  def summarize_results(results)
47
- sort_order = %w[scenario step failed flaky undefined skipped pending passed]
53
+ sort_order = ['scenario', 'step', 'failed', 'flaky', 'undefined', 'skipped', 'pending', 'passed']
48
54
 
49
- %w[scenario step].map do |group|
55
+ ['scenario', 'step'].map do |group|
50
56
  group_results = results.grep(/^\d+ #{group}/)
51
57
  next if group_results.empty?
52
58
 
53
59
  sums = sum_up_results(group_results)
54
60
  sums = sums.sort_by { |word, _| sort_order.index(word) || 999 }
55
61
  sums.map! do |word, number|
56
- plural = "s" if word == group and number != 1
62
+ plural = "s" if (word == group) && (number != 1)
57
63
  "#{number} #{word}#{plural}"
58
64
  end
59
65
  "#{sums[0]} (#{sums[1..-1].join(", ")})"
@@ -61,7 +67,7 @@ module ParallelTests
61
67
  end
62
68
 
63
69
  def cucumber_opts(given)
64
- if given =~ /--profile/ or given =~ /(^|\s)-p /
70
+ if given =~ (/--profile/) || given =~ (/(^|\s)-p /)
65
71
  given
66
72
  else
67
73
  [given, profile_from_config].compact.join(" ")
@@ -71,15 +77,11 @@ module ParallelTests
71
77
  def profile_from_config
72
78
  # copied from https://github.com/cucumber/cucumber/blob/master/lib/cucumber/cli/profile_loader.rb#L85
73
79
  config = Dir.glob("{,.config/,config/}#{name}{.yml,.yaml}").first
74
- if config && File.read(config) =~ /^parallel:/
75
- "--profile parallel"
76
- end
80
+ "--profile parallel" if config && File.read(config) =~ /^parallel:/
77
81
  end
78
82
 
79
- def tests_in_groups(tests, num_groups, options={})
80
- if options[:group_by] == :scenarios
81
- @test_file_name = "scenario"
82
- end
83
+ def tests_in_groups(tests, num_groups, options = {})
84
+ @test_file_name = "scenario" if options[:group_by] == :scenarios
83
85
  method = "by_#{options[:group_by]}"
84
86
  if Grouper.respond_to?(method)
85
87
  Grouper.send(method, find_tests(tests, options), num_groups, options)
@@ -88,7 +90,6 @@ module ParallelTests
88
90
  end
89
91
  end
90
92
 
91
-
92
93
  def runtime_logging
93
94
  "--format ParallelTests::Gherkin::RuntimeLogger --out #{runtime_log}"
94
95
  end
@@ -98,18 +99,16 @@ module ParallelTests
98
99
  end
99
100
 
100
101
  def determine_executable
101
- case
102
- when File.exist?("bin/#{name}")
102
+ if File.exist?("bin/#{name}")
103
103
  ParallelTests.with_ruby_binary("bin/#{name}")
104
- when ParallelTests.bundler_enabled?
104
+ elsif ParallelTests.bundler_enabled?
105
105
  "bundle exec #{name}"
106
- when File.file?("script/#{name}")
106
+ elsif File.file?("script/#{name}")
107
107
  ParallelTests.with_ruby_binary("script/#{name}")
108
108
  else
109
- "#{name}"
109
+ name.to_s
110
110
  end
111
111
  end
112
-
113
112
  end
114
113
  end
115
114
  end