parallel_tests 2.28.0 → 3.7.0

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