Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cucumber 4 support #762

Merged
merged 9 commits into from Jun 10, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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)
deivid-rodriguez marked this conversation as resolved.
Show resolved Hide resolved
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