diff --git a/Gemfile b/Gemfile index bba195e55e..78355c1431 100644 --- a/Gemfile +++ b/Gemfile @@ -17,3 +17,7 @@ elsif !ENV['CUCUMBER_USE_RELEASED_GEMS'] end gem 'cucumber-expressions', path: ENV['CUCUMBER_EXPRESSIONS_RUBY'] if ENV['CUCUMBER_EXPRESSIONS_RUBY'] + +gem 'gherkin', path: ENV['GHERKIN_RUBY'] if ENV['GHERKIN_RUBY'] + +gem 'cucumber-messages', path: ENV['CUCUMBER_MESSAGES_RUBY'] if ENV['CUCUMBER_MESSAGES_RUBY'] diff --git a/cucumber.gemspec b/cucumber.gemspec index 87aaa6043c..bccecb3159 100644 --- a/cucumber.gemspec +++ b/cucumber.gemspec @@ -15,7 +15,7 @@ Gem::Specification.new do |s| s.add_dependency 'cucumber-expressions', '~> 6.0.1' s.add_dependency 'cucumber-wire', '~> 0.0.1' s.add_dependency 'diff-lcs', '~> 1.3' - s.add_dependency 'gherkin', '~> 5.1.0' + s.add_dependency 'gherkin', '~> 6.0' s.add_dependency 'multi_json', '>= 1.7.5', '< 2.0' s.add_dependency 'multi_test', '>= 0.1.2' diff --git a/features/docs/cli/i18n.feature b/features/docs/cli/i18n.feature index cfafa790eb..78e3a4d6d6 100644 --- a/features/docs/cli/i18n.feature +++ b/features/docs/cli/i18n.feature @@ -16,7 +16,7 @@ Feature: i18n """ | feature | "Funcionalidade", "Característica", "Caracteristica" | | background | "Contexto", "Cenário de Fundo", "Cenario de Fundo", "Fundo" | - | scenario | "Cenário", "Cenario" | + | scenario | "Exemplo", "Cenário", "Cenario" | | scenario_outline | "Esquema do Cenário", "Esquema do Cenario", "Delineação do Cenário", "Delineacao do Cenario" | | examples | "Exemplos", "Cenários", "Cenarios" | | given | "* ", "Dado ", "Dada ", "Dados ", "Dadas " | diff --git a/features/docs/gherkin/background.feature b/features/docs/gherkin/background.feature index 666c0c78f9..9b100178de 100644 --- a/features/docs/gherkin/background.feature +++ b/features/docs/gherkin/background.feature @@ -461,7 +461,7 @@ Feature: Background Examples: - Scenario: | 10 | + Example: | 10 | Then I should have '10' global cukes Scenario Outline: failing background @@ -469,7 +469,7 @@ Feature: Background Examples: - Scenario: | 10 | + Example: | 10 | And '10' global cukes FAIL (RuntimeError) ./features/step_definitions/cuke_steps.rb:8:in `/^'(.+)' global cukes$/' diff --git a/features/docs/gherkin/expand_option_for_outlines.feature b/features/docs/gherkin/expand_option_for_outlines.feature index 25154a2935..3db4e92c86 100644 --- a/features/docs/gherkin/expand_option_for_outlines.feature +++ b/features/docs/gherkin/expand_option_for_outlines.feature @@ -32,12 +32,12 @@ Feature: Scenario outlines --expand option Examples: - Scenario: | blue | blue | right | + Example: | blue | blue | right | Given the secret code is blue When I guess blue Then I am right - Scenario: | red | blue | wrong | + Example: | red | blue | wrong | Given the secret code is red When I guess blue Then I am wrong diff --git a/lib/cucumber/events/gherkin_source_parsed.rb b/lib/cucumber/events/gherkin_source_parsed.rb index 81b16f2638..deba2ac1c8 100644 --- a/lib/cucumber/events/gherkin_source_parsed.rb +++ b/lib/cucumber/events/gherkin_source_parsed.rb @@ -3,10 +3,7 @@ module Cucumber module Events # Fired after we've parsed the contents of a feature file - class GherkinSourceParsed < Core::Event.new(:uri, :gherkin_document) - # The uri of the file - attr_reader :uri - + class GherkinSourceParsed < Core::Event.new(:gherkin_document) # The Gherkin Ast attr_reader :gherkin_document end diff --git a/lib/cucumber/formatter/ast_lookup.rb b/lib/cucumber/formatter/ast_lookup.rb index b51fcd7d77..9ed2fbea1f 100644 --- a/lib/cucumber/formatter/ast_lookup.rb +++ b/lib/cucumber/formatter/ast_lookup.rb @@ -12,7 +12,7 @@ def initialize(config) end def on_gherkin_source_parsed(event) - @gherkin_documents[event.uri] = event.gherkin_document + @gherkin_documents[event.gherkin_document[:uri]] = event.gherkin_document end def gherkin_document(uri) @@ -21,13 +21,13 @@ def gherkin_document(uri) def scenario_source(test_case) uri = test_case.location.file - @test_case_lookups[uri] ||= create_test_case_lookup(gherkin_document(uri)) + @test_case_lookups[uri] ||= TestCaseLookupBuilder.new(gherkin_document(uri)).lookup_hash @test_case_lookups[uri][test_case.location.lines.max] end def step_source(test_step) uri = test_step.location.file - @test_step_lookups[uri] ||= create_test_step_lookup(gherkin_document(uri)) + @test_step_lookups[uri] ||= TestStepLookupBuilder.new(gherkin_document(uri)).lookup_hash @test_step_lookups[uri][test_step.location.lines.min] end @@ -61,52 +61,99 @@ def snippet_step_keyword(test_step) private def step_keyword_lookup(uri) - @step_keyword_lookups[uri] ||= create_keyword_lookup(gherkin_document(uri)) + @step_keyword_lookups[uri] ||= KeywordLookupBuilder.new(gherkin_document(uri)).lookup_hash end - def create_test_case_lookup(gherkin_document) - feature = gherkin_document[:feature] - lookup_hash = {} - feature[:children].each do |child| - if child[:type] == :Scenario - lookup_hash[child[:location][:line]] = ScenarioSource.new(:Scenario, child) - elsif child[:type] == :ScenarioOutline - child[:examples].each do |examples| - examples[:tableBody].each do |row| - lookup_hash[row[:location][:line]] = ScenarioOutlineSource.new(:ScenarioOutline, child, examples, row) + class TestCaseLookupBuilder + attr_reader :lookup_hash + + def initialize(gherkin_document) + @lookup_hash = {} + process_scenario_container(gherkin_document[:feature]) + end + + private + + def process_scenario_container(container) + container[:children].each do |child| + if !child[:rule].nil? + process_scenario_container(child[:rule]) + elsif !child[:scenario].nil? + if child[:scenario][:examples].empty? + @lookup_hash[child[:scenario][:location][:line]] = ScenarioSource.new(:Scenario, child[:scenario]) + + else + child[:scenario][:examples].each do |examples| + examples[:table_body].each do |row| + @lookup_hash[row[:location][:line]] = ScenarioOutlineSource.new(:ScenarioOutline, child[:scenario], examples, row) + end + end end end end end - lookup_hash end - def create_test_step_lookup(gherkin_document) - feature = gherkin_document[:feature] - lookup_hash = {} - feature[:children].each do |child| - child[:steps].each do |step| - lookup_hash[step[:location][:line]] = StepSource.new(:Step, step) + class TestStepLookupBuilder + attr_reader :lookup_hash + + def initialize(gherkin_document) + @lookup_hash = {} + process_scenario_container(gherkin_document[:feature]) + end + + private + + def process_scenario_container(container) + container[:children].each do |child| + if !child[:rule].nil? + process_scenario_container(child[:rule]) + elsif !child[:scenario].nil? + child[:scenario][:steps].each do |step| + @lookup_hash[step[:location][:line]] = StepSource.new(:Step, step) + end + elsif !child[:background].nil? + child[:background][:steps].each do |step| + @lookup_hash[step[:location][:line]] = StepSource.new(:Step, step) + end + end end end - lookup_hash end KeywordSearchNode = Struct.new(:keyword, :previous_node) - def create_keyword_lookup(gherkin_document) - lookup = {} - original_previous_node = nil - gherkin_document[:feature][:children].each do |child| - previous_node = original_previous_node - child[:steps].each do |step| - node = KeywordSearchNode.new(step[:keyword], previous_node) - lookup[step[:location][:line]] = node - previous_node = node + class KeywordLookupBuilder + attr_reader :lookup_hash + + def initialize(gherkin_document) + @lookup_hash = {} + process_scenario_container(gherkin_document[:feature], nil) + end + + private + + def process_scenario_container(container, original_previous_node) + container[:children].each do |child| + previous_node = original_previous_node + if !child[:rule].nil? + process_scenario_container(child[:rule], original_previous_node) + elsif !child[:scenario].nil? + child[:scenario][:steps].each do |step| + node = KeywordSearchNode.new(step[:keyword], previous_node) + @lookup_hash[step[:location][:line]] = node + previous_node = node + end + elsif !child[:background].nil? + child[:background][:steps].each do |step| + node = KeywordSearchNode.new(step[:keyword], previous_node) + @lookup_hash[step[:location][:line]] = node + previous_node = node + original_previous_node = previous_node + end + end end - original_previous_node = previous_node if child[:type] == :Background end - lookup end end end diff --git a/lib/cucumber/formatter/json.rb b/lib/cucumber/formatter/json.rb index 420d5764ce..81c34b0ae5 100644 --- a/lib/cucumber/formatter/json.rb +++ b/lib/cucumber/formatter/json.rb @@ -167,10 +167,8 @@ def create_step_hash(test_step) name: test_step.text, line: test_step.location.lines.min } - unless step_source[:argument].nil? - step_hash[:doc_string] = create_doc_string_hash(step_source[:argument]) if step_source[:argument][:type] == :DocString - step_hash[:rows] = create_data_table_value(step_source[:argument]) if step_source[:argument][:type] == :DataTable - end + step_hash[:doc_string] = create_doc_string_hash(step_source[:doc_string]) unless step_source[:doc_string].nil? + step_hash[:rows] = create_data_table_value(step_source[:data_table]) unless step_source[:data_table].nil? step_hash end @@ -234,7 +232,7 @@ def initialize(test_case, ast_lookup) uri = test_case.location.file feature = ast_lookup.gherkin_document(uri)[:feature] feature(feature, uri) - background(feature[:children].first) if feature[:children].first[:type] == :Background + background(feature[:children].first[:background]) unless feature[:children].first[:background].nil? scenario(ast_lookup.scenario_source(test_case), test_case) end @@ -300,7 +298,7 @@ def create_id_from_scenario_source(scenario_source) end def calculate_row_number(scenario_source) - scenario_source.examples[:tableBody].each_with_index do |row, index| + scenario_source.examples[:table_body].each_with_index do |row, index| return index + 2 if row == scenario_source.row end end diff --git a/lib/cucumber/formatter/pretty.rb b/lib/cucumber/formatter/pretty.rb index a95cc68b3d..bf503622d6 100644 --- a/lib/cucumber/formatter/pretty.rb +++ b/lib/cucumber/formatter/pretty.rb @@ -192,7 +192,7 @@ def same_feature_as_previous_test_case?(location) def feature_has_background? feature_children = gherkin_document[:feature][:children] return false if feature_children.empty? - feature_children[0][:type] == :Background + !feature_children.first[:background].nil? end def print_step_header(test_case) @@ -296,7 +296,7 @@ def print_description(description) def print_background_data @io.puts - background = gherkin_document[:feature][:children][0] + background = gherkin_document[:feature][:children].first[:background] @source_indent = calculate_source_indent_for_ast_node(background) if options[:source] print_comments(background[:location][:line], 2) print_background_line(background) @@ -355,12 +355,10 @@ def gherkin_document def print_multiline_argument(test_step, result, indent) step = step_source(test_step).step - multiline_arg = step[:argument] - return unless multiline_arg - if multiline_arg[:type] == :DocString - print_doc_string(multiline_arg[:content], result.to_sym, indent) - elsif multiline_arg[:type] == :DataTable - print_data_table(step[:argument], result.to_sym, indent) + if !step[:doc_string].nil? + print_doc_string(step[:doc_string][:content], result.to_sym, indent) + elsif !step[:data_table].nil? + print_data_table(step[:data_table], result.to_sym, indent) end end @@ -371,7 +369,7 @@ def print_data_table(data_table, status, indent) end end - def print_outline_data(scenario_outline) # rubocop:disable Metrics/AbcSize, Metrics/PerceivedComplexity + def print_outline_data(scenario_outline) # rubocop:disable Metrics/AbcSize print_comments(scenario_outline[:location][:line], 2) print_tags(scenario_outline[:tags], 2) @source_indent = calculate_source_indent_for_ast_node(scenario_outline) if options[:source] @@ -387,8 +385,8 @@ def print_outline_data(scenario_outline) # rubocop:disable Metrics/AbcSize, Metr end @io.puts next if options[:no_multiline] - print_doc_string(step[:argument][:content], :skipped, 6) if step[:argument] && step[:argument][:type] == :DocString - print_data_table(step[:argument], :skipped, 6) if step[:argument] && step[:argument][:type] == :DataTable + print_doc_string(step[:doc_string][:content], :skipped, 6) unless step[:doc_string].nil? + print_data_table(step[:data_table], :skipped, 6) unless step[:data_table].nil? end @io.flush end @@ -405,8 +403,8 @@ def print_examples_data(examples) print_keyword_name(examples[:keyword], examples[:name], 4) print_description(examples[:description]) unless options[:expand] - print_comments(examples[:tableHeader][:location][:line], 6) - @io.puts(gherkin_source.split("\n")[examples[:tableHeader][:location][:line] - 1].strip.indent(6)) + print_comments(examples[:table_header][:location][:line], 6) + @io.puts(gherkin_source.split("\n")[examples[:table_header][:location][:line] - 1].strip.indent(6)) end @io.flush end diff --git a/lib/cucumber/gherkin/data_table_parser.rb b/lib/cucumber/gherkin/data_table_parser.rb index b276e6c6f4..d63fcc5204 100644 --- a/lib/cucumber/gherkin/data_table_parser.rb +++ b/lib/cucumber/gherkin/data_table_parser.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true -require 'gherkin/token_scanner' -require 'gherkin/parser' +require 'gherkin/gherkin' require 'gherkin/dialect' module Cucumber @@ -12,11 +11,14 @@ def initialize(builder) end def parse(text) - token_scanner = ::Gherkin::TokenScanner.new(feature_header + text) - parser = ::Gherkin::Parser.new - gherkin_document = parser.parse(token_scanner) + gherkin_document = nil + messages = ::Gherkin::Gherkin.from_source('dummy', feature_header + text, include_source: false, include_pickles: false) + messages.each do |message| + gherkin_document = message.gherkinDocument.to_hash unless message.gherkinDocument.nil? + end - gherkin_document[:feature][:children][0][:steps][0][:argument][:rows].each do |row| + return if gherkin_document.nil? + gherkin_document[:feature][:children][0][:scenario][:steps][0][:data_table][:rows].each do |row| @builder.row(row[:cells].map { |cell| cell[:value] }) end end diff --git a/lib/cucumber/gherkin/steps_parser.rb b/lib/cucumber/gherkin/steps_parser.rb index 09531ef59b..071a2eb350 100644 --- a/lib/cucumber/gherkin/steps_parser.rb +++ b/lib/cucumber/gherkin/steps_parser.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true -require 'gherkin/token_scanner' -require 'gherkin/token_matcher' -require 'gherkin/parser' +require 'gherkin/gherkin' require 'gherkin/dialect' module Cucumber @@ -15,12 +13,13 @@ def initialize(builder, language) def parse(text) dialect = ::Gherkin::Dialect.for(@language) - token_matcher = ::Gherkin::TokenMatcher.new(@language) - token_scanner = ::Gherkin::TokenScanner.new(feature_header(dialect) + text) - parser = ::Gherkin::Parser.new - gherkin_document = parser.parse(token_scanner, token_matcher) + gherkin_document = nil + messages = ::Gherkin::Gherkin.from_source('dummy', feature_header(dialect) + text, default_dialect: @language, include_source: false, include_pickles: false) + messages.each do |message| + gherkin_document = message.gherkinDocument.to_hash unless message.gherkinDocument.nil? + end - @builder.steps(gherkin_document[:feature][:children][0][:steps]) + @builder.steps(gherkin_document[:feature][:children][0][:scenario][:steps]) end def feature_header(dialect) diff --git a/lib/cucumber/runtime/support_code.rb b/lib/cucumber/runtime/support_code.rb index 33029be516..f9c3a88abb 100644 --- a/lib/cucumber/runtime/support_code.rb +++ b/lib/cucumber/runtime/support_code.rb @@ -27,14 +27,10 @@ def step(step) end def multiline_arg(step, location) - argument = step[:argument] - - if argument - if argument[:type] == :DocString - MultilineArgument.from(argument[:content], location, argument[:content_type]) - else - MultilineArgument::DataTable.from(argument[:rows].map { |row| row[:cells].map { |cell| cell[:value] } }) - end + if !step[:doc_string].nil? + MultilineArgument.from(step[:doc_string][:content], location, step[:doc_string][:content_type]) + elsif !step[:data_table].nil? + MultilineArgument::DataTable.from(step[:data_table][:rows].map { |row| row[:cells].map { |cell| cell[:value] } }) else MultilineArgument.from(nil) end diff --git a/spec/cucumber/formatter/pretty_spec.rb b/spec/cucumber/formatter/pretty_spec.rb index 5eb7646eca..a17406cc0b 100644 --- a/spec/cucumber/formatter/pretty_spec.rb +++ b/spec/cucumber/formatter/pretty_spec.rb @@ -452,6 +452,46 @@ module Formatter #comment11 | dummy | #comment12 +OUTPUT + end + end + + describe 'with the rule keyword' do + define_feature <<-FEATURE + Feature: Some rules + + Background: FB + Given fb + + Rule: A + The rule A description + + Background: AB + Given ab + + Example: Example A + Given a + + Rule: B + The rule B description + + Example: Example B + Given b + FEATURE + + it 'ignores the rule keyword' do + expect(@out.string).to include < # spec.feature:4 Examples: Fruit - Scenario: | apples | # spec.feature:8 + Example: | apples | # spec.feature:8 Given there are apples # spec.feature:8 - Scenario: | bananas | # spec.feature:9 + Example: | bananas | # spec.feature:9 Given there are bananas # spec.feature:9 Examples: Vegetables - Scenario: | broccoli | # spec.feature:12 + Example: | broccoli | # spec.feature:12 Given there are broccoli # spec.feature:12 - Scenario: | carrots | # spec.feature:13 + Example: | carrots | # spec.feature:13 Given there are carrots # spec.feature:13 OUTPUT lines.split("\n").each do |line| @@ -785,8 +825,8 @@ module Formatter it 'the scenario line controls the source indentation' do lines = <<-OUTPUT Examples: - Scenario: | Hominidae | Very long cell content | # spec.feature:8 - Given there are Hominidae # spec.feature:8 + Example: | Hominidae | Very long cell content | # spec.feature:8 + Given there are Hominidae # spec.feature:8 OUTPUT lines.split("\n").each do |line|