Skip to content

Commit

Permalink
Cucumber 4 support (#762)
Browse files Browse the repository at this point in the history
  • Loading branch information
deivid-rodriguez committed Jun 10, 2020
1 parent 3fdb33f commit d9af8f6
Show file tree
Hide file tree
Showing 8 changed files with 138 additions and 101 deletions.
3 changes: 2 additions & 1 deletion Gemfile
Expand Up @@ -5,6 +5,7 @@ gem 'bump'
gem 'test-unit'
gem 'minitest', '~> 5.5.0'
gem 'rspec', '~> 3.3'
gem 'cucumber', "~> 3.0"
gem 'cucumber', "~> 4.0"
gem 'cuke_modeler', '~> 3.0'
gem 'spinach'
gem 'rake'
107 changes: 68 additions & 39 deletions Gemfile.lock
Expand Up @@ -7,65 +7,94 @@ PATH
GEM
remote: https://rubygems.org/
specs:
backports (3.11.4)
builder (3.2.3)
bump (0.5.3)
activesupport (5.2.4.3)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 0.7, < 2)
minitest (~> 5.1)
tzinfo (~> 1.1)
builder (3.2.4)
bump (0.9.0)
colorize (0.8.1)
cucumber (3.1.2)
builder (>= 2.1.2)
cucumber-core (~> 3.2.0)
cucumber-expressions (~> 6.0.1)
cucumber-wire (~> 0.0.1)
diff-lcs (~> 1.3)
gherkin (~> 5.1.0)
multi_json (>= 1.7.5, < 2.0)
multi_test (>= 0.1.2)
cucumber-core (3.2.1)
backports (>= 3.8.0)
cucumber-tag_expressions (~> 1.1.0)
gherkin (~> 5.0)
cucumber-expressions (6.0.1)
cucumber-tag_expressions (1.1.1)
cucumber-wire (0.0.1)
concurrent-ruby (1.1.6)
cucumber (4.0.0)
builder (~> 3.2, >= 3.2.3)
cucumber-core (~> 7.0, >= 7.0.0)
cucumber-cucumber-expressions (~> 10.1, >= 10.1.0)
cucumber-gherkin (~> 13.0, >= 13.0.0)
cucumber-html-formatter (~> 6.0, >= 6.0.1)
cucumber-messages (~> 12.1, >= 12.1.1)
cucumber-wire (~> 3.0, >= 3.0.0)
diff-lcs (~> 1.3, >= 1.3)
multi_test (~> 0.1, >= 0.1.2)
sys-uname (~> 1.0, >= 1.0.2)
cucumber-core (7.0.0)
cucumber-gherkin (~> 13.0, >= 13.0.0)
cucumber-messages (~> 12.1, >= 12.1.1)
cucumber-tag-expressions (~> 2.0, >= 2.0.4)
cucumber-cucumber-expressions (10.2.0)
cucumber-gherkin (13.0.0)
cucumber-messages (~> 12.0, >= 12.0.0)
cucumber-html-formatter (6.0.2)
cucumber-messages (~> 12.1, >= 12.1.1)
cucumber-messages (12.1.1)
protobuf-cucumber (~> 3.10, >= 3.10.8)
cucumber-tag-expressions (2.0.4)
cucumber-wire (3.0.0)
cucumber-core (~> 7.0, >= 7.0.0)
cucumber-cucumber-expressions (~> 10.1, >= 10.1.0)
cucumber-messages (~> 12.1, >= 12.1.1)
cuke_modeler (3.0.0)
cucumber-gherkin (< 14.0)
diff-lcs (1.3)
gherkin (5.1.0)
ffi (1.13.1)
gherkin-ruby (0.3.2)
i18n (1.8.3)
concurrent-ruby (~> 1.0)
json (2.3.0)
json (2.3.0-java)
middleware (0.1.0)
minitest (5.5.1)
multi_json (1.13.1)
multi_test (0.1.2)
parallel (1.19.1)
power_assert (0.4.1)
power_assert (1.2.0)
protobuf-cucumber (3.10.8)
activesupport (>= 3.2)
middleware
thor
thread_safe
rake (13.0.1)
rspec (3.5.0)
rspec-core (~> 3.5.0)
rspec-expectations (~> 3.5.0)
rspec-mocks (~> 3.5.0)
rspec-core (3.5.4)
rspec-support (~> 3.5.0)
rspec-expectations (3.5.0)
rspec (3.9.0)
rspec-core (~> 3.9.0)
rspec-expectations (~> 3.9.0)
rspec-mocks (~> 3.9.0)
rspec-core (3.9.2)
rspec-support (~> 3.9.3)
rspec-expectations (3.9.2)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.5.0)
rspec-mocks (3.5.0)
rspec-support (~> 3.9.0)
rspec-mocks (3.9.1)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.5.0)
rspec-support (3.5.0)
spinach (0.9.0)
rspec-support (~> 3.9.0)
rspec-support (3.9.3)
spinach (0.11.0)
colorize
gherkin-ruby (>= 0.3.2)
json
test-unit (3.2.3)
sys-uname (1.2.1)
ffi (>= 1.0.0)
test-unit (3.3.6)
power_assert
thor (1.0.1)
thread_safe (0.3.6)
tzinfo (1.2.7)
thread_safe (~> 0.1)

PLATFORMS
java
ruby
x64-mingw32

DEPENDENCIES
bump
cucumber (~> 3.0)
cucumber (~> 4.0)
cuke_modeler (~> 3.0)
minitest (~> 5.5.0)
parallel_tests!
rake
Expand Down
31 changes: 31 additions & 0 deletions lib/parallel_tests/cucumber/features_with_steps.rb
@@ -0,0 +1,31 @@
begin
gem "cuke_modeler", "~> 3.0"
require 'cuke_modeler'
rescue LoadError
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.'
end

module ParallelTests
module Cucumber
class FeaturesWithSteps
class << self
def all(tests, options)
ignore_tag_pattern = options[:ignore_tag_pattern].nil? ? nil : Regexp.compile(options[:ignore_tag_pattern])
# format of hash will be FILENAME => NUM_STEPS
steps_per_file = tests.each_with_object({}) do |file,steps|
feature = ::CukeModeler::FeatureFile.new(file).feature

# skip feature if it matches tag regex
next if feature.tags.grep(ignore_tag_pattern).any?

# count the number of steps in the file
# will only include a feature if the regex does not match
all_steps = feature.scenarios.map{|a| a.steps.count if a.tags.grep(ignore_tag_pattern).empty? }.compact
steps[file] = all_steps.inject(0,:+)
end
steps_per_file.sort_by { |_, value| -value }
end
end
end
end
end
32 changes: 17 additions & 15 deletions lib/parallel_tests/cucumber/scenario_line_logger.rb
@@ -1,37 +1,33 @@
require 'cucumber/tag_expressions/parser'
require 'cucumber/core/gherkin/tag_expression'

module ParallelTests
module Cucumber
module Formatters
class ScenarioLineLogger
attr_reader :scenarios

def initialize(tag_expression = ::Cucumber::Core::Gherkin::TagExpression.new([]))
def initialize(tag_expression = nil)
@scenarios = []
@tag_expression = tag_expression
end

def visit_feature_element(uri, feature_element, feature_tags, line_numbers: [])
scenario_tags = feature_element[:tags].map { |tag| tag[:name] }
scenario_tags = feature_element.tags.map { |tag| tag.name }
scenario_tags = feature_tags + scenario_tags
if feature_element[:examples].nil? # :Scenario
test_line = feature_element[:location][:line]
if feature_element.is_a?(CukeModeler::Scenario) # :Scenario
test_line = feature_element.source_line

# We don't accept the feature_element if the current tags are not valid
return unless @tag_expression.evaluate(scenario_tags)
return unless matches_tags?(scenario_tags)
# or if it is not at the correct location
return if line_numbers.any? && !line_numbers.include?(test_line)

@scenarios << [uri, feature_element[:location][:line]].join(":")
@scenarios << [uri, feature_element.source_line].join(":")
else # :ScenarioOutline
feature_element[:examples].each do |example|
example_tags = example[:tags].map { |tag| tag[:name] }
feature_element.examples.each do |example|
example_tags = example.tags.map(&:name)
example_tags = scenario_tags + example_tags
next unless @tag_expression.evaluate(example_tags)
rows = example[:tableBody].select { |body| body[:type] == :TableRow }
rows.each do |row|
test_line = row[:location][:line]
next unless matches_tags?(example_tags)
example.rows[1..-1].each do |row|
test_line = row.source_line
next if line_numbers.any? && !line_numbers.include?(test_line)

@scenarios << [uri, test_line].join(':')
Expand All @@ -42,6 +38,12 @@ def visit_feature_element(uri, feature_element, feature_tags, line_numbers: [])

def method_missing(*args)
end

private

def matches_tags?(tags)
@tag_expression.nil? || @tag_expression.evaluate(tags)
end
end
end
end
Expand Down
40 changes: 15 additions & 25 deletions lib/parallel_tests/cucumber/scenarios.rb
@@ -1,12 +1,17 @@
require 'cucumber/tag_expressions/parser'
require 'cucumber/core/gherkin/tag_expression'
require 'cucumber/runtime'
require 'cucumber'
require 'parallel_tests/cucumber/scenario_line_logger'
require 'parallel_tests/gherkin/listener'
require 'gherkin/errors'
require 'shellwords'

begin
gem "cuke_modeler", "~> 3.0"
require 'cuke_modeler'
rescue LoadError
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.'
end

module ParallelTests
module Cucumber
class Scenarios
Expand Down Expand Up @@ -40,32 +45,17 @@ def split_into_scenarios(files, tags='')
path, *test_lines = path.split(/:(?=\d+)/)
test_lines.map!(&:to_i)

# We encode the file and get the content of it
source = ::Cucumber::Runtime::NormalisedEncodingFile.read(path)
# We create a Gherkin document, this will be used to decode the details of each scenario
document = ::Cucumber::Core::Gherkin::Document.new(path, source)

# We create a parser for the gherkin document
parser = ::Gherkin::Parser.new()
scanner = ::Gherkin::TokenScanner.new(document.body)

begin
# We make an attempt to parse the gherkin document, this could be failed if the document is not well formatted
result = parser.parse(scanner)
feature_tags = result[:feature][:tags].map { |tag| tag[:name] }

# We loop on each children of the feature
result[:feature][:children].each do |feature_element|
# 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
next unless /Scenario/.match(feature_element[:type])
document = ::CukeModeler::FeatureFile.new(path)
feature = document.feature

# It's a scenario, we add it to the scenario_line_logger
scenario_line_logger.visit_feature_element(document.uri, feature_element, feature_tags, line_numbers: test_lines)
end
# We make an attempt to parse the gherkin document, this could be failed if the document is not well formatted
feature_tags = feature.tags.map(&:name)

rescue StandardError => e
# Exception if the document is no well formated or error in the tags
raise ::Cucumber::Core::Gherkin::ParseError.new("#{document.uri}: #{e.message}")
# We loop on each children of the feature
feature.tests.each do |test|
# It's a scenario, we add it to the scenario_line_logger
scenario_line_logger.visit_feature_element(document.path, test, feature_tags, line_numbers: test_lines)
end
end

Expand Down
2 changes: 0 additions & 2 deletions lib/parallel_tests/gherkin/listener.rb
@@ -1,5 +1,3 @@
require 'gherkin/parser'

module ParallelTests
module Gherkin
class Listener
Expand Down
2 changes: 1 addition & 1 deletion lib/parallel_tests/gherkin/runtime_logger.rb
Expand Up @@ -14,7 +14,7 @@ def initialize(config)
end

config.on_event :test_case_finished do |event|
@example_times[event.test_case.feature.file] += ParallelTests.now.to_f - @start_at
@example_times[event.test_case.location.file] += ParallelTests.now.to_f - @start_at
end

config.on_event :test_run_finished do |_|
Expand Down
22 changes: 4 additions & 18 deletions lib/parallel_tests/grouper.rb
Expand Up @@ -2,7 +2,7 @@ module ParallelTests
class Grouper
class << self
def by_steps(tests, num_groups, options)
features_with_steps = build_features_with_steps(tests, options)
features_with_steps = group_by_features_with_steps(tests, options)
in_even_groups_by_size(features_with_steps, num_groups)
end

Expand Down Expand Up @@ -41,23 +41,9 @@ def add_to_group(group, item, size)
group[:size] += size
end

def build_features_with_steps(tests, options)
require 'gherkin/parser'
ignore_tag_pattern = options[:ignore_tag_pattern].nil? ? nil : Regexp.compile(options[:ignore_tag_pattern])
parser = ::Gherkin::Parser.new
# format of hash will be FILENAME => NUM_STEPS
steps_per_file = tests.each_with_object({}) do |file,steps|
feature = parser.parse(File.read(file)).fetch(:feature)

# skip feature if it matches tag regex
next if feature[:tags].grep(ignore_tag_pattern).any?

# count the number of steps in the file
# will only include a feature if the regex does not match
all_steps = feature[:children].map{|a| a[:steps].count if a[:tags].grep(ignore_tag_pattern).empty? }.compact
steps[file] = all_steps.inject(0,:+)
end
steps_per_file.sort_by { |_, value| -value }
def group_by_features_with_steps(tests, options)
require 'parallel_tests/cucumber/features_with_steps'
ParallelTests::Cucumber::FeaturesWithSteps.all(tests, options)
end

def group_by_scenarios(tests, options={})
Expand Down

0 comments on commit d9af8f6

Please sign in to comment.