Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Merge pull request #10 from codegram/racc

Rewrite PEG parser into an LALR(1) using Rexical/Racc
  • Loading branch information...
commit b778e088bbf8d4f696bb6b5a665f8020619ad279 2 parents f8d200b + a5570e9
@txus txus authored
View
1  .gitignore
@@ -2,3 +2,4 @@
.bundle
Gemfile.lock
pkg/*
+.rbx
View
20 Rakefile
@@ -8,4 +8,22 @@ Rake::TestTask.new do |t|
t.test_files = FileList['./test/**/*_test.rb']
end
-task :default => [:test]
+desc "Regenerate Gherkin-ruby lexer and parser."
+task :regenerate do
+ has_rex = `which rex`
+ has_racc = `which racc`
+
+ if has_rex && has_racc
+ `rex lib/gherkin/parser/gherkin.rex -o lib/gherkin/parser/lexer.rb`
+ `racc lib/gherkin/parser/gherkin.y -o lib/gherkin/parser/parser.rb`
+ else
+ puts "You need both Rexical and Racc to do that. Install them by doing:"
+ puts
+ puts "\t\tgem install rexical"
+ puts "\t\tgem install racc"
+ puts
+ puts "Or just type `bundle install`."
+ end
+end
+
+task :default => [:regenerate, :test]
View
4 Readme.md
@@ -1,10 +1,9 @@
# gherkin-ruby
-Gherkin-ruby is a pure Ruby implementation of a [Gherkin](http://github.com/cucumber/gherkin) parser, using [Parslet](http://github.com/kschiess/parslet).
+Gherkin-ruby is a pure Ruby implementation of a [Gherkin](http://github.com/cucumber/gherkin) parser.
## Why this one over the official, fast, Ragel-based Gherkin parser?
* Less than 200 LOC.
-* No C-exts
* No Java/.NET crap.
* Fast enough for our purposes (using it for the [Spinach](http://github.com/codegram/spinach) project)
@@ -74,3 +73,4 @@ visitor.visit(ast)
## Todo
* Some optimization
+
View
3  gherkin-ruby.gemspec
@@ -13,7 +13,8 @@ Gem::Specification.new do |s|
s.rubyforge_project = "gherkin-ruby"
- s.add_runtime_dependency 'parslet'
+ s.add_runtime_dependency 'rexical'
+ s.add_runtime_dependency 'racc'
s.add_development_dependency 'minitest'
s.files = `git ls-files`.split("\n")
View
9 lib/gherkin.rb
@@ -1,15 +1,10 @@
-require 'parslet'
require_relative "gherkin/version"
require_relative 'gherkin/ast'
require_relative 'gherkin/parser'
-require_relative 'gherkin/transform'
module Gherkin
def self.parse(input)
- parser = Parser.new
- transform = Transform.new
-
- parsed = parser.parse input
- transform.apply(parsed)
+ parser = Parser.new
+ parser.parse(input)
end
end
View
35 lib/gherkin/ast.rb
@@ -1,25 +1,28 @@
module Gherkin
module AST
class Node
- attr_reader :line, :column
+ attr_reader :filename, :line
def accept(visitor)
name = self.class.name.split('::').last
visitor.send("visit_#{name}".to_sym, self)
end
+
+ def pos(filename, line=nil)
+ @filename, @line = filename, line
+ end
end
class Feature < Node
attr_reader :name, :background, :scenarios
+ attr_writer :background, :scenarios
include Enumerable
def initialize(name, scenarios=[], background=nil)
- @line, @column = name.line_and_column
-
- @name = name.to_s
+ @name = name
@background = background
- @scenarios = scenarios
+ @scenarios = scenarios
end
def each
@@ -29,18 +32,18 @@ def each
class Background < Node
attr_reader :steps
+ attr_writer :steps
include Enumerable
def initialize(steps=[])
- if steps.any?
- @line = steps.first.line - 1
- @column = 3
- end
-
@steps = steps
end
+ def line
+ @steps.first.line - 1 if @steps.any?
+ end
+
def each
@steps.each
end
@@ -52,13 +55,15 @@ class Scenario < Node
include Enumerable
def initialize(name, steps=[], tags=[])
- @line, @column = name.line_and_column
-
@name = name.to_s
@steps = steps
@tags = tags
end
+ def line
+ @steps.first.line - 1 if @steps.any?
+ end
+
def each
@steps.each
end
@@ -67,8 +72,6 @@ def each
class Step < Node
attr_reader :name, :keyword
def initialize(name, keyword)
- @line, @column = name.line_and_column
-
@name = name.to_s
@keyword = keyword.to_s
end
@@ -77,9 +80,7 @@ def initialize(name, keyword)
class Tag < Node
attr_reader :name
def initialize(name)
- @line, @column = name.line_and_column
-
- @name = name.to_s
+ @name = name
end
end
end
View
44 lib/gherkin/parser.rb
@@ -1,43 +1 @@
-require 'parslet'
-
-module Gherkin
- class Parser < Parslet::Parser
- def indent(num=2)
- str(' ' * num)
- end
-
- rule(:space) { match('\s').repeat }
- rule(:space?) { space.maybe }
- rule(:newline) { match('\n').repeat }
- rule(:text) { match('[^\n]').repeat }
- rule(:identifier) { match('\w').repeat }
-
- rule(:feature_line) { str('Feature:') >> space? >> text.as(:name) }
- rule(:scenario_line) { indent(2) >> str('Scenario:') >> space? >> text.as(:name) }
-
- rule(:step_keyword) { str('Given') | str('When') | str('Then') | str('And') | str('But') }
-
- rule(:comment) { str('#') >> text.as(:comment) }
-
- rule(:description_text) { (str('Background:') | str('Scenario:') | str('@')).absent? >> text }
- rule(:description) { (indent(2) >> description_text >> newline).repeat }
-
- rule(:background_line) { indent(2) >> str('Background:') }
-
- rule(:step) { indent(4) >> step_keyword.as(:keyword) >> space? >> text.as(:name) }
- rule(:steps) { (step.as(:step) >> newline.maybe).repeat }
-
- rule(:tags) { indent(2) >> (str('@') >> identifier.as(:tag) >> str(' ').maybe).repeat(1) }
-
- rule(:scenario) { (tags.as(:tags) >> newline).maybe >> scenario_line >> newline >> steps.as(:steps) }
- rule(:scenarios) { (scenario.as(:scenario) >> newline.maybe).repeat }
-
- rule(:background) { background_line >> newline >> steps.as(:steps) }
-
- rule(:feature) { feature_line >> newline >> description >> background.as(:background).maybe >> scenarios.as(:scenarios) }
-
- rule(:main) { feature.as(:feature) }
-
- root :main
- end
-end
+require_relative 'parser/parser'
View
44 lib/gherkin/parser/gherkin.rex
@@ -0,0 +1,44 @@
+# Compile with: rex gherkin.rex -o lexer.rb
+
+class Gherkin::Parser
+
+macro
+ BLANK [\ \t]+
+
+rule
+ # Whitespace
+ {BLANK} # no action
+ \#.*$
+
+ # Literals
+ \n { [:NEWLINE, text] }
+
+ # Keywords
+ Feature: { [:FEATURE, text[0..-2]] }
+ Background: { [:BACKGROUND, text[0..-2]] }
+ Scenario: { [:SCENARIO, text[0..-2]] }
+
+ # Tags
+ @\w+ { [:TAG, text[1..-1]] }
+
+ # Step keywords
+ Given { [:GIVEN, text] }
+ When { [:WHEN, text] }
+ Then { [:THEN, text] }
+ And { [:AND, text] }
+ But { [:BUT, text] }
+
+ # Text
+ [^#\n]* { [:TEXT, text.strip] }
+
+inner
+ def tokenize(code)
+ scan_setup(code)
+ tokens = []
+ while token = next_token
+ tokens << token
+ end
+ tokens
+ end
+
+end
View
100 lib/gherkin/parser/gherkin.y
@@ -0,0 +1,100 @@
+# Compile with: racc gherkin.y -o parser.rb
+
+class Gherkin::Parser
+
+# Declare tokens produced by the lexer
+token NEWLINE
+token FEATURE BACKGROUND SCENARIO
+token TAG
+token GIVEN WHEN THEN AND BUT
+token TEXT
+
+rule
+
+ Root:
+ Feature { result = val[0]; }
+ |
+ Feature
+ Scenarios { result = val[0]; result.scenarios = val[1] }
+ ;
+
+ Newline:
+ NEWLINE
+ | Newline NEWLINE
+ ;
+
+ Feature:
+ FeatureHeader { result = val[0] }
+ | FeatureHeader
+ Background { result = val[0]; result.background = val[1] }
+ ;
+
+ FeatureHeader:
+ FeatureName { result = val[0] }
+ | FeatureName Newline { result = val[0] }
+ | FeatureName Newline
+ Description { result = val[0] }
+ ;
+
+ FeatureName:
+ FEATURE TEXT { result = AST::Feature.new(val[1]); result.pos(filename, lineno) }
+ | Newline FEATURE TEXT { result = AST::Feature.new(val[2]); result.pos(filename, lineno) }
+ ;
+
+ Description:
+ TEXT Newline
+ | Description TEXT Newline
+ ;
+
+ Background:
+ BackgroundHeader
+ Steps { result = val[0]; result.steps = val[1] }
+ ;
+
+ BackgroundHeader:
+ BACKGROUND Newline { result = AST::Background.new; result.pos(filename, lineno) }
+ ;
+
+ Steps:
+ Step Newline { result = [val[0]] }
+ | Steps Step Newline { result = val[0] << val[1] }
+ ;
+
+ Step:
+ Keyword TEXT { result = AST::Step.new(val[1], val[0]); result.pos(filename, lineno) }
+ ;
+
+ Keyword:
+ GIVEN | WHEN | THEN | AND | BUT
+ ;
+
+ Scenarios:
+ Scenario { result = [val[0]] }
+ | Scenarios Scenario { result = val[0] << val[1] }
+ | Scenarios Newline Scenario { result = val[0] << val[2] }
+ ;
+
+ Scenario:
+ SCENARIO TEXT Newline
+ Steps { result = AST::Scenario.new(val[1], val[3]); result.pos(filename, lineno - 1) }
+ | Tags Newline
+ SCENARIO TEXT Newline
+ Steps { result = AST::Scenario.new(val[3], val[5], val[0]); result.pos(filename, lineno - 2) }
+ ;
+
+ Tags:
+ TAG { result = [AST::Tag.new(val[0])] }
+ | Tags TAG { result = val[0] << AST::Tag.new(val[1]) }
+ ;
+
+end
+
+---- header
+ require_relative "lexer"
+ require_relative "../ast"
+
+---- inner
+
+ def parse(input)
+ scan_str(input)
+ end
View
120 lib/gherkin/parser/lexer.rb
@@ -0,0 +1,120 @@
+#--
+# DO NOT MODIFY!!!!
+# This file is automatically generated by rex 1.0.5
+# from lexical definition file "lib/gherkin/parser/gherkin.rex".
+#++
+
+require 'racc/parser'
+# Compile with: rex gherkin.rex -o lexer.rb
+
+class Gherkin::Parser < Racc::Parser
+ require 'strscan'
+
+ class ScanError < StandardError ; end
+
+ attr_reader :lineno
+ attr_reader :filename
+ attr_accessor :state
+
+ def scan_setup(str)
+ @ss = StringScanner.new(str)
+ @lineno = 1
+ @state = nil
+ end
+
+ def action
+ yield
+ end
+
+ def scan_str(str)
+ scan_setup(str)
+ do_parse
+ end
+ alias :scan :scan_str
+
+ def load_file( filename )
+ @filename = filename
+ open(filename, "r") do |f|
+ scan_setup(f.read)
+ end
+ end
+
+ def scan_file( filename )
+ load_file(filename)
+ do_parse
+ end
+
+
+ def next_token
+ return if @ss.eos?
+
+ # skips empty actions
+ until token = _next_token or @ss.eos?; end
+ token
+ end
+
+ def _next_token
+ text = @ss.peek(1)
+ @lineno += 1 if text == "\n"
+ token = case @state
+ when nil
+ case
+ when (text = @ss.scan(/[ \t]+/))
+ ;
+
+ when (text = @ss.scan(/\#.*$/))
+ ;
+
+ when (text = @ss.scan(/\n/))
+ action { [:NEWLINE, text] }
+
+ when (text = @ss.scan(/Feature:/))
+ action { [:FEATURE, text[0..-2]] }
+
+ when (text = @ss.scan(/Background:/))
+ action { [:BACKGROUND, text[0..-2]] }
+
+ when (text = @ss.scan(/Scenario:/))
+ action { [:SCENARIO, text[0..-2]] }
+
+ when (text = @ss.scan(/@\w+/))
+ action { [:TAG, text[1..-1]] }
+
+ when (text = @ss.scan(/Given/))
+ action { [:GIVEN, text] }
+
+ when (text = @ss.scan(/When/))
+ action { [:WHEN, text] }
+
+ when (text = @ss.scan(/Then/))
+ action { [:THEN, text] }
+
+ when (text = @ss.scan(/And/))
+ action { [:AND, text] }
+
+ when (text = @ss.scan(/But/))
+ action { [:BUT, text] }
+
+ when (text = @ss.scan(/[^#\n]*/))
+ action { [:TEXT, text.strip] }
+
+ else
+ text = @ss.string[@ss.pos .. -1]
+ raise ScanError, "can not match: '" + text + "'"
+ end # if
+
+ else
+ raise ScanError, "undefined state: '" + state.to_s + "'"
+ end # case state
+ token
+ end # def _next_token
+
+ def tokenize(code)
+ scan_setup(code)
+ tokens = []
+ while token = next_token
+ tokens << token
+ end
+ tokens
+ end
+end # class
View
359 lib/gherkin/parser/parser.rb
@@ -0,0 +1,359 @@
+#
+# DO NOT MODIFY!!!!
+# This file is automatically generated by Racc 1.4.7
+# from Racc grammer file "".
+#
+
+require 'racc/parser.rb'
+
+ require_relative "lexer"
+ require_relative "../ast"
+
+module Gherkin
+ class Parser < Racc::Parser
+
+module_eval(<<'...end gherkin.y/module_eval...', 'gherkin.y', 97)
+
+ def parse(input)
+ scan_str(input)
+ end
+...end gherkin.y/module_eval...
+##### State transition tables begin ###
+
+racc_action_table = [
+ 14, 3, 14, 15, 14, 26, 14, 11, 13, 3,
+ 38, 31, 32, 33, 34, 35, 14, 14, 11, 13,
+ 41, 31, 32, 33, 34, 35, 31, 32, 33, 34,
+ 35, 31, 32, 33, 34, 35, 31, 32, 33, 34,
+ 35, 3, 3, 7, 11, 13, 31, 32, 33, 34,
+ 35, 3, 3, 3, 18, 3, 44, 14, 45, 20,
+ 21, 48, 3, 14, 3, 14, 27, 3, 14, 14,
+ 8, 24 ]
+
+racc_action_check = [
+ 19, 12, 4, 4, 22, 12, 51, 22, 22, 38,
+ 19, 51, 51, 51, 51, 51, 40, 25, 2, 2,
+ 25, 40, 40, 40, 40, 40, 47, 47, 47, 47,
+ 47, 52, 52, 52, 52, 52, 17, 17, 17, 17,
+ 17, 9, 0, 0, 9, 9, 28, 28, 28, 28,
+ 28, 6, 24, 18, 5, 29, 30, 36, 37, 7,
+ 8, 41, 42, 43, 45, 46, 15, 48, 49, 50,
+ 1, 11 ]
+
+racc_action_pointer = [
+ 40, 70, 13, nil, 0, 50, 49, 47, 60, 39,
+ nil, 59, -1, nil, nil, 54, nil, 29, 51, -2,
+ nil, nil, 2, nil, 50, 15, nil, nil, 39, 53,
+ 44, nil, nil, nil, nil, nil, 55, 46, 7, nil,
+ 14, 49, 60, 61, nil, 62, 63, 19, 65, 66,
+ 67, 4, 24 ]
+
+racc_action_default = [
+ -31, -31, -1, -3, -31, -5, -7, -31, -31, -2,
+ -24, -31, -31, -29, -4, -31, -6, -31, -31, -8,
+ -10, 53, -31, -25, -31, -31, -30, -11, -14, -31,
+ -31, -19, -20, -21, -22, -23, -15, -9, -31, -26,
+ -31, -31, -31, -16, -18, -31, -12, -27, -31, -17,
+ -13, -31, -28 ]
+
+racc_goto_table = [
+ 4, 10, 28, 42, 6, 16, 19, 5, 23, 22,
+ 37, 17, 25, 9, 2, 1, nil, nil, 36, nil,
+ nil, 39, 42, nil, 40, 47, nil, 42, nil, 43,
+ nil, nil, nil, nil, nil, nil, 52, nil, 46, nil,
+ nil, nil, 49, nil, nil, 50, nil, nil, 51 ]
+
+racc_goto_check = [
+ 4, 13, 10, 11, 7, 6, 4, 5, 13, 4,
+ 8, 9, 4, 3, 2, 1, nil, nil, 4, nil,
+ nil, 13, 11, nil, 4, 10, nil, 11, nil, 4,
+ nil, nil, nil, nil, nil, nil, 10, nil, 4, nil,
+ nil, nil, 4, nil, nil, 4, nil, nil, 4 ]
+
+racc_goto_pointer = [
+ nil, 15, 14, 11, 0, 7, 0, 4, -9, 6,
+ -15, -25, nil, -1, nil ]
+
+racc_goto_default = [
+ nil, nil, nil, nil, nil, nil, nil, nil, nil, nil,
+ nil, 29, 30, nil, 12 ]
+
+racc_reduce_table = [
+ 0, 0, :racc_error,
+ 1, 14, :_reduce_1,
+ 2, 14, :_reduce_2,
+ 1, 17, :_reduce_none,
+ 2, 17, :_reduce_none,
+ 1, 15, :_reduce_5,
+ 2, 15, :_reduce_6,
+ 1, 18, :_reduce_7,
+ 2, 18, :_reduce_8,
+ 3, 18, :_reduce_9,
+ 2, 20, :_reduce_10,
+ 3, 20, :_reduce_11,
+ 2, 21, :_reduce_none,
+ 3, 21, :_reduce_none,
+ 2, 19, :_reduce_14,
+ 2, 22, :_reduce_15,
+ 2, 23, :_reduce_16,
+ 3, 23, :_reduce_17,
+ 2, 24, :_reduce_18,
+ 1, 25, :_reduce_none,
+ 1, 25, :_reduce_none,
+ 1, 25, :_reduce_none,
+ 1, 25, :_reduce_none,
+ 1, 25, :_reduce_none,
+ 1, 16, :_reduce_24,
+ 2, 16, :_reduce_25,
+ 3, 16, :_reduce_26,
+ 4, 26, :_reduce_27,
+ 6, 26, :_reduce_28,
+ 1, 27, :_reduce_29,
+ 2, 27, :_reduce_30 ]
+
+racc_reduce_n = 31
+
+racc_shift_n = 53
+
+racc_token_table = {
+ false => 0,
+ :error => 1,
+ :NEWLINE => 2,
+ :FEATURE => 3,
+ :BACKGROUND => 4,
+ :SCENARIO => 5,
+ :TAG => 6,
+ :GIVEN => 7,
+ :WHEN => 8,
+ :THEN => 9,
+ :AND => 10,
+ :BUT => 11,
+ :TEXT => 12 }
+
+racc_nt_base = 13
+
+racc_use_result_var = true
+
+Racc_arg = [
+ racc_action_table,
+ racc_action_check,
+ racc_action_default,
+ racc_action_pointer,
+ racc_goto_table,
+ racc_goto_check,
+ racc_goto_default,
+ racc_goto_pointer,
+ racc_nt_base,
+ racc_reduce_table,
+ racc_token_table,
+ racc_shift_n,
+ racc_reduce_n,
+ racc_use_result_var ]
+
+Racc_token_to_s_table = [
+ "$end",
+ "error",
+ "NEWLINE",
+ "FEATURE",
+ "BACKGROUND",
+ "SCENARIO",
+ "TAG",
+ "GIVEN",
+ "WHEN",
+ "THEN",
+ "AND",
+ "BUT",
+ "TEXT",
+ "$start",
+ "Root",
+ "Feature",
+ "Scenarios",
+ "Newline",
+ "FeatureHeader",
+ "Background",
+ "FeatureName",
+ "Description",
+ "BackgroundHeader",
+ "Steps",
+ "Step",
+ "Keyword",
+ "Scenario",
+ "Tags" ]
+
+Racc_debug_parser = false
+
+##### State transition tables end #####
+
+# reduce 0 omitted
+
+module_eval(<<'.,.,', 'gherkin.y', 14)
+ def _reduce_1(val, _values, result)
+ result = val[0];
+ result
+ end
+.,.,
+
+module_eval(<<'.,.,', 'gherkin.y', 17)
+ def _reduce_2(val, _values, result)
+ result = val[0]; result.scenarios = val[1]
+ result
+ end
+.,.,
+
+# reduce 3 omitted
+
+# reduce 4 omitted
+
+module_eval(<<'.,.,', 'gherkin.y', 26)
+ def _reduce_5(val, _values, result)
+ result = val[0]
+ result
+ end
+.,.,
+
+module_eval(<<'.,.,', 'gherkin.y', 28)
+ def _reduce_6(val, _values, result)
+ result = val[0]; result.background = val[1]
+ result
+ end
+.,.,
+
+module_eval(<<'.,.,', 'gherkin.y', 32)
+ def _reduce_7(val, _values, result)
+ result = val[0]
+ result
+ end
+.,.,
+
+module_eval(<<'.,.,', 'gherkin.y', 33)
+ def _reduce_8(val, _values, result)
+ result = val[0]
+ result
+ end
+.,.,
+
+module_eval(<<'.,.,', 'gherkin.y', 35)
+ def _reduce_9(val, _values, result)
+ result = val[0]
+ result
+ end
+.,.,
+
+module_eval(<<'.,.,', 'gherkin.y', 39)
+ def _reduce_10(val, _values, result)
+ result = AST::Feature.new(val[1]); result.pos(filename, lineno)
+ result
+ end
+.,.,
+
+module_eval(<<'.,.,', 'gherkin.y', 40)
+ def _reduce_11(val, _values, result)
+ result = AST::Feature.new(val[2]); result.pos(filename, lineno)
+ result
+ end
+.,.,
+
+# reduce 12 omitted
+
+# reduce 13 omitted
+
+module_eval(<<'.,.,', 'gherkin.y', 50)
+ def _reduce_14(val, _values, result)
+ result = val[0]; result.steps = val[1]
+ result
+ end
+.,.,
+
+module_eval(<<'.,.,', 'gherkin.y', 54)
+ def _reduce_15(val, _values, result)
+ result = AST::Background.new; result.pos(filename, lineno)
+ result
+ end
+.,.,
+
+module_eval(<<'.,.,', 'gherkin.y', 58)
+ def _reduce_16(val, _values, result)
+ result = [val[0]]
+ result
+ end
+.,.,
+
+module_eval(<<'.,.,', 'gherkin.y', 59)
+ def _reduce_17(val, _values, result)
+ result = val[0] << val[1]
+ result
+ end
+.,.,
+
+module_eval(<<'.,.,', 'gherkin.y', 63)
+ def _reduce_18(val, _values, result)
+ result = AST::Step.new(val[1], val[0]); result.pos(filename, lineno)
+ result
+ end
+.,.,
+
+# reduce 19 omitted
+
+# reduce 20 omitted
+
+# reduce 21 omitted
+
+# reduce 22 omitted
+
+# reduce 23 omitted
+
+module_eval(<<'.,.,', 'gherkin.y', 71)
+ def _reduce_24(val, _values, result)
+ result = [val[0]]
+ result
+ end
+.,.,
+
+module_eval(<<'.,.,', 'gherkin.y', 72)
+ def _reduce_25(val, _values, result)
+ result = val[0] << val[1]
+ result
+ end
+.,.,
+
+module_eval(<<'.,.,', 'gherkin.y', 73)
+ def _reduce_26(val, _values, result)
+ result = val[0] << val[2]
+ result
+ end
+.,.,
+
+module_eval(<<'.,.,', 'gherkin.y', 78)
+ def _reduce_27(val, _values, result)
+ result = AST::Scenario.new(val[1], val[3]); result.pos(filename, lineno - 1)
+ result
+ end
+.,.,
+
+module_eval(<<'.,.,', 'gherkin.y', 81)
+ def _reduce_28(val, _values, result)
+ result = AST::Scenario.new(val[3], val[5], val[0]); result.pos(filename, lineno - 2)
+ result
+ end
+.,.,
+
+module_eval(<<'.,.,', 'gherkin.y', 85)
+ def _reduce_29(val, _values, result)
+ result = [AST::Tag.new(val[0])]
+ result
+ end
+.,.,
+
+module_eval(<<'.,.,', 'gherkin.y', 86)
+ def _reduce_30(val, _values, result)
+ result = val[0] << AST::Tag.new(val[1])
+ result
+ end
+.,.,
+
+def _reduce_none(val, _values, result)
+ val[0]
+end
+
+ end # class Parser
+ end # module Gherkin
View
50 lib/gherkin/transform.rb
@@ -1,50 +0,0 @@
-module Gherkin
- class Transform < Parslet::Transform
- # Match feature with background
- rule(
- feature: {
- name: simple(:name),
- background: {
- steps: subtree(:background_steps)
- },
- scenarios: subtree(:scenarios)
- }
- ) { AST::Feature.new(name, scenarios, AST::Background.new(background_steps)) }
-
- # Match feature without background
- rule(
- feature: {
- name: simple(:name),
- scenarios: subtree(:scenarios)
- }
- ) { AST::Feature.new(name, scenarios, AST::Background.new([])) }
-
- # Match scenarios without tags
- rule(
- scenario: {
- name: simple(:name),
- steps: subtree(:steps)
- }
- ) { AST::Scenario.new(name, steps, []) }
-
- # Match scenarios with tags
- rule(
- scenario: {
- name: simple(:name),
- steps: subtree(:steps),
- tags: subtree(:tags)
- }
- ) { AST::Scenario.new(name, steps, tags) }
-
- rule(
- step: {
- name: simple(:name),
- keyword: simple(:keyword),
- }
- ) { AST::Step.new(name, keyword) }
-
- rule(
- tag: simple(:name)
- ) { AST::Tag.new(name) }
- end
-end
View
51 test/gherkin/ast_test.rb
@@ -32,51 +32,42 @@ module AST
node.ancestors.must_include Node
end
- it 'has a line and column' do
- name = OpenStruct.new(line_and_column: [2, 13])
- def name.to_s; 'Name'; end
-
- instance = node.new(name)
+ it 'has a line' do
+ instance = node.new("Name")
instance.name.must_equal 'Name'
- instance.line.must_equal 2
- instance.column.must_equal 13
+ instance.must_respond_to :line
end
end
end
describe Feature do
it 'is Enumerable' do
- name = OpenStruct.new(line_and_column: [2, 13])
- def name.to_s; 'Name'; end
-
background = ['foo', 'bar']
elements = ['+foo', '+bar']
- instance = Feature.new(name, elements, background )
+ instance = Feature.new("My feature", elements, background )
instance.background.each.to_a.must_equal ['foo', 'bar']
instance.each.to_a.must_equal ['+foo', '+bar']
end
end
describe Scenario do
- it 'is Enumerable' do
- name = OpenStruct.new(line_and_column: [2, 13])
- def name.to_s; 'Name'; end
-
- elements = ['foo', 'bar']
+ let(:steps) do
+ steps = [
+ OpenStruct.new(line: 4),
+ OpenStruct.new(line: 5),
+ ]
+ end
- instance = Scenario.new(name, elements)
- instance.each.to_a.must_equal ['foo', 'bar']
+ it 'is Enumerable' do
+ instance = Scenario.new("Name", steps)
+ instance.each.to_a.must_equal steps
end
it 'has tags' do
- name = OpenStruct.new(line_and_column: [2, 13])
- def name.to_s; 'Name'; end
-
- steps = ['foo', 'bar']
tags = ['javascript', 'wip']
- instance = Scenario.new(name, steps, tags)
+ instance = Scenario.new("Name", steps, tags)
instance.tags.must_equal tags
end
end
@@ -99,10 +90,10 @@ def name.to_s; 'Name'; end
end
describe 'when there are background steps' do
- it 'records line and column' do
+ it 'records line' do
instance = Background.new(steps)
+ instance.pos("file", 3)
instance.line.must_equal 3
- instance.column.must_equal 3
end
end
@@ -110,7 +101,6 @@ def name.to_s; 'Name'; end
it 'does not' do
instance = Background.new([])
instance.line.must_equal nil
- instance.column.must_equal nil
end
end
end
@@ -120,15 +110,12 @@ def name.to_s; 'Name'; end
Step.ancestors.must_include Node
end
- it 'has a line and column' do
- name = OpenStruct.new(line_and_column: [2, 13])
- def name.to_s; 'Name'; end
-
- instance = Step.new(name, 'Given')
+ it 'has a line' do
+ instance = Step.new("Name", 'Given')
+ instance.pos("file", 2)
instance.name.must_equal 'Name'
instance.keyword.must_equal 'Given'
instance.line.must_equal 2
- instance.column.must_equal 13
end
end
end
View
43 test/gherkin/parser/lexer_test.rb
@@ -0,0 +1,43 @@
+require 'test_helper'
+
+module Gherkin
+ describe Parser do
+ let(:lexer) { Parser.new }
+
+ it 'ignores commments' do
+ lexer.tokenize("# this is a comment").must_equal []
+ end
+
+ it 'parses newlines' do
+ lexer.tokenize("\n\n").must_equal [[:NEWLINE, "\n"], [:NEWLINE, "\n"]]
+ end
+
+ it 'parses text and strips it' do
+ lexer.tokenize(" Arbitrary text ").must_equal [[:TEXT, "Arbitrary text"]]
+ end
+
+ it 'parses tags' do
+ lexer.tokenize("@javascript @wip").must_equal [
+ [:TAG, "javascript"],
+ [:TAG, "wip"],
+ ]
+ end
+
+ describe 'Keywords' do
+ %w(Feature: Background: Scenario:).each do |keyword|
+ it "parses #{keyword}:" do
+ name = keyword[0..-2]
+ lexer.tokenize(keyword).must_equal [[name.upcase.to_sym, name]]
+ end
+ end
+ end
+
+ describe 'Step keywords' do
+ %w(Given When Then And But).each do |keyword|
+ it "parses #{keyword}" do
+ lexer.tokenize(keyword).must_equal [[keyword.upcase.to_sym, keyword]]
+ end
+ end
+ end
+ end
+end
View
96 test/gherkin/parser/parser_test.rb
@@ -0,0 +1,96 @@
+require 'test_helper'
+
+module Gherkin
+ describe Parser do
+ def parse(input)
+ parser = Parser.new
+ parser.parse(input)
+ end
+
+ describe 'Feature name and description' do
+ it 'parses feature header without description' do
+ feature = parse(
+ "Feature: my feature"
+ )
+ feature.must_be_kind_of AST::Feature
+ feature.name.must_equal "my feature"
+ end
+
+ it 'parses feature header with description' do
+ feature = parse(
+ "Feature: my feature\n In order to do something\n As a user\n"
+ )
+ feature.must_be_kind_of AST::Feature
+ feature.name.must_equal "my feature"
+ end
+
+ it 'parses feature with background' do
+ feature = parse("""
+Feature: Do something
+
+ Background:
+ Given blah foo bar
+ Then something else
+""")
+ feature.name.must_equal "Do something"
+ feature.background.must_be_kind_of AST::Background
+ steps = feature.background.steps
+
+ given_step = steps.first
+ then_step = steps.last
+
+ given_step.keyword.must_equal "Given"
+ given_step.name.must_equal "blah foo bar"
+ then_step.keyword.must_equal "Then"
+ then_step.name.must_equal "something else"
+ end
+
+ it 'parses feature with scenarios' do
+ feature = parse("""
+Feature: Do something
+
+ Scenario: Foo bar baz
+ Given blah foo bar
+ Then something else
+
+ Scenario: Foo bar baz blah
+ Given blah foo bar
+ Then something else
+""")
+ scenarios = feature.scenarios
+
+ first_scenario = scenarios.first
+ last_scenario = scenarios.last
+
+ first_scenario.name.must_equal "Foo bar baz"
+ first_scenario.steps.first.name.must_equal "blah foo bar"
+ first_scenario.steps.last.name.must_equal "something else"
+
+ last_scenario.name.must_equal "Foo bar baz blah"
+ last_scenario.steps.first.name.must_equal "blah foo bar"
+ last_scenario.steps.last.name.must_equal "something else"
+ end
+
+ it 'parses feature with scenarios with tags' do
+ feature = parse("""
+Feature: Do something
+
+ Scenario: Foo bar baz
+ Given blah foo bar
+ Then something else
+
+ @javascript @wip
+ Scenario: Foo bar baz blah
+ Given blah foo bar
+ Then something else
+""")
+ scenarios = feature.scenarios
+
+ last_scenario = scenarios.last
+
+ last_scenario.tags.first.name.must_equal "javascript"
+ last_scenario.tags.last.name.must_equal "wip"
+ end
+ end
+ end
+end
View
200 test/gherkin/parser_test.rb
@@ -1,169 +1,73 @@
require_relative '../test_helper'
-require 'parslet'
-
-def p(rule, input, tag=nil)
- tag ||= rule
- parser = Gherkin::Parser.new
- parser.send(rule).parse(input)[tag]
-end
module Gherkin
describe 'Feature parsing' do
- it 'parses the Feature name' do
- p(:feature_line, "Feature: Test how Gherkin works", :name).must_equal "Test how Gherkin works"
- end
-
- it 'parses the Scenario name' do
- p(:scenario_line, " Scenario: Formal greeting", :name).must_equal "Formal greeting"
- end
-
- describe 'Steps' do
- it 'parses a Given step' do
- step = " Given I have an empty array"
- p(:step, step, :name).must_equal "I have an empty array"
- p(:step, step, :keyword).must_equal "Given"
- end
-
- it 'parses a When step' do
- step = " When I have an empty array"
- p(:step, step, :name).must_equal "I have an empty array"
- p(:step, step, :keyword).must_equal "When"
- end
-
- it 'parses a Then step' do
- step = " Then I have an empty array"
- p(:step, step, :name).must_equal "I have an empty array"
- p(:step, step, :keyword).must_equal "Then"
- end
-
- it 'parses an And step' do
- step = " And I have an empty array"
- p(:step, step, :name).must_equal "I have an empty array"
- p(:step, step, :keyword).must_equal "And"
- end
-
- it 'parses a But step' do
- step = " But I have an empty array"
- p(:step, step, :name).must_equal "I have an empty array"
- p(:step, step, :keyword).must_equal "But"
- end
- end
- end
-
- describe 'Comment parsing' do
- it 'parses a comment ignoring its content' do
- p(:comment, "# My comment").size.must_be :>, 0
- end
- end
-
- describe 'Description parsing' do
- it 'parses multiline descriptions' do
- description = """ In order to know what the heck is Gherkin
+ before do
+ @scenario = """Feature: My Feature
+ In order to do something #w000t peoeple
As a developer
- I want it to behave in an expected way
-"""
- parser = Gherkin::Parser.new
- parser.description.parse(description)
- end
- end
-
- describe 'Tags parsing' do
- it 'parses many tags' do
- javascript = p(:tags, " @javascript @wip", 0)
- wip = p(:tags, " @javascript @wip", 1)
- javascript[:tag].must_equal 'javascript'
- wip[:tag].must_equal 'wip'
- end
+ I want to be happy #yeah
- it 'parses one tag' do
- p(:tags, " @javascript", 0)[:tag].must_equal 'javascript'
- end
- end
+ # Attend people. This is going to be fun
- describe 'Background parsing' do
- it 'parses a background' do
- background = " Background:\n When I do something\n Then blah"
- steps = p(:background, background, :steps)
- steps.first[:step][:name].must_equal 'I do something'
- steps.last[:step][:name].must_equal 'blah'
- end
- end
-
- describe 'Parses scenario objects' do
- it 'parses a Scenario' do
- parser = Gherkin::Parser.new
- scenario = " Scenario: Parse a scenario\n Given something happens\n Then something cooler happens"
- result = parser.scenario.parse(scenario)
-
- result[:name].must_equal 'Parse a scenario'
- result[:steps][0][:step][:name].must_equal 'something happens'
- result[:steps][1][:step][:name].must_equal 'something cooler happens'
- end
-
- it 'parses a Scenario with a tag' do
- parser = Gherkin::Parser.new
- scenario = " @javascript\n Scenario: Parse a scenario\n Given something happens\n Then something cooler happens"
- result = parser.scenario.parse(scenario)
-
- result[:tags].first[:tag].must_equal 'javascript'
- result[:name].must_equal 'Parse a scenario'
- result[:steps][0][:step][:name].must_equal 'something happens'
- result[:steps][1][:step][:name].must_equal 'something cooler happens'
- end
- end
+ Background:
+ Given something happens before anything else happens
+ And more things happens before anything else happens
+ # And I wipe the hard drive
- describe 'Parses feature objects without background' do
- it 'parses a Feature' do
- parser = Gherkin::Parser.new
- scenario = """Feature: My Feature
- @javascript @wip
- Scenario: something happens
+ Scenario: something happens # yeah
Given something happens
Then something cooler happens
+ @javascript @wip #@destroy
Scenario: something else happens
Given foo
Then bar
"""
- result = parser.parse(scenario)
- result[:feature][:name].must_equal 'My Feature'
-
- result[:feature][:scenarios][0][:scenario][:tags].first[:tag].must_equal 'javascript'
- result[:feature][:scenarios][0][:scenario][:tags].last[:tag].must_equal 'wip'
- result[:feature][:scenarios][0][:scenario][:name].must_equal 'something happens'
- result[:feature][:scenarios][0][:scenario][:steps][0][:step][:name].must_equal 'something happens'
- result[:feature][:scenarios][0][:scenario][:steps][1][:step][:name].must_equal 'something cooler happens'
-
- result[:feature][:scenarios][1][:scenario][:name].must_equal 'something else happens'
- result[:feature][:scenarios][1][:scenario][:steps][0][:step][:name].must_equal 'foo'
- result[:feature][:scenarios][1][:scenario][:steps][1][:step][:name].must_equal 'bar'
+ parser = Gherkin::Parser.new
+ @result = parser.parse(@scenario)
end
- end
-
- describe 'Parses feature objects with background' do
- it 'parses a Feature' do
- parser = Gherkin::Parser.new
- scenario = """Feature: My Feature
-
- Background:
- Given something happens
- And something cooler happens
-
- Scenario: something else happens
- Given foo
- Then bar
-"""
- result = parser.parse(scenario)
-
- result[:feature][:name].must_equal 'My Feature'
-
- result[:feature][:background][:steps][0][:step][:name].must_equal 'something happens'
- result[:feature][:background][:steps][1][:step][:name].must_equal 'something cooler happens'
- result[:feature][:scenarios][0][:scenario][:name].must_equal 'something else happens'
- result[:feature][:scenarios][0][:scenario][:steps][0][:step][:name].must_equal 'foo'
- result[:feature][:scenarios][0][:scenario][:steps][1][:step][:name].must_equal 'bar'
+ it 'generates a nice tree' do
+ @result.must_be_kind_of AST::Feature
+ @result.line.must_equal 1
+
+ background = @result.background
+ background.must_be_kind_of AST::Background
+ background.line.must_equal 8
+ background.steps.first.keyword.must_equal 'Given'
+ background.steps.first.name.must_equal 'something happens before anything else happens'
+ background.steps.first.line.must_equal 9
+ background.steps.last.keyword.must_equal 'And'
+ background.steps.last.name.must_equal 'more things happens before anything else happens'
+ background.steps.last.line.must_equal 10
+
+ first_scenario = @result.scenarios.first
+ first_scenario.must_be_kind_of AST::Scenario
+ first_scenario.line.must_equal 13
+ first_scenario.name.must_equal 'something happens'
+ first_scenario.steps.first.keyword.must_equal 'Given'
+ first_scenario.steps.first.name.must_equal 'something happens'
+ first_scenario.steps.first.line.must_equal 14
+ first_scenario.steps.last.keyword.must_equal 'Then'
+ first_scenario.steps.last.name.must_equal 'something cooler happens'
+ first_scenario.steps.last.line.must_equal 15
+
+ last_scenario = @result.scenarios.last
+ last_scenario.must_be_kind_of AST::Scenario
+ last_scenario.line.must_equal 18
+ last_scenario.name.must_equal 'something else happens'
+
+ last_scenario.tags.first.name.must_equal 'javascript'
+ last_scenario.tags.last.name.must_equal 'wip'
+
+ last_scenario.steps.first.keyword.must_equal 'Given'
+ last_scenario.steps.first.name.must_equal 'foo'
+ last_scenario.steps.first.line.must_equal 19
+ last_scenario.steps.last.keyword.must_equal 'Then'
+ last_scenario.steps.last.name.must_equal 'bar'
+ last_scenario.steps.last.line.must_equal 20
end
end
end
View
74 test/gherkin/transform_test.rb
@@ -1,74 +0,0 @@
-require_relative '../test_helper'
-require 'parslet'
-
-module Gherkin
- describe 'Feature parsing' do
- before do
- @scenario = """Feature: My Feature
- In order to do something
- As a developer
- I want to be happy
-
- Background:
- Given something happens before anything else happens
- And more things happens before anything else happens
-
- Scenario: something happens
- Given something happens
- Then something cooler happens
-
- @javascript @wip
- Scenario: something else happens
- Given foo
- Then bar
-"""
-
- parser = Gherkin::Parser.new
- result = parser.parse(@scenario)
- transform = Gherkin::Transform.new
- @result = transform.apply(result)
- end
-
- it 'generates a nice tree' do
- @result.must_be_kind_of AST::Feature
- @result.line.must_equal 1
-
- background = @result.background
- background.must_be_kind_of AST::Background
- background.line.must_equal 6
- background.column.must_equal 3
- background.steps.first.keyword.must_equal 'Given'
- background.steps.first.name.must_equal 'something happens before anything else happens'
- background.steps.first.line.must_equal 7
- background.steps.last.keyword.must_equal 'And'
- background.steps.last.name.must_equal 'more things happens before anything else happens'
- background.steps.last.line.must_equal 8
-
- first_scenario = @result.scenarios.first
- first_scenario.must_be_kind_of AST::Scenario
- first_scenario.line.must_equal 10
- first_scenario.name.must_equal 'something happens'
- first_scenario.steps.first.keyword.must_equal 'Given'
- first_scenario.steps.first.name.must_equal 'something happens'
- first_scenario.steps.first.line.must_equal 11
- first_scenario.steps.last.keyword.must_equal 'Then'
- first_scenario.steps.last.name.must_equal 'something cooler happens'
- first_scenario.steps.last.line.must_equal 12
-
- last_scenario = @result.scenarios.last
- last_scenario.must_be_kind_of AST::Scenario
- last_scenario.line.must_equal 15
- last_scenario.name.must_equal 'something else happens'
-
- last_scenario.tags.first.name.must_equal 'javascript'
- last_scenario.tags.last.name.must_equal 'wip'
-
- last_scenario.steps.first.keyword.must_equal 'Given'
- last_scenario.steps.first.name.must_equal 'foo'
- last_scenario.steps.first.line.must_equal 16
- last_scenario.steps.last.keyword.must_equal 'Then'
- last_scenario.steps.last.name.must_equal 'bar'
- last_scenario.steps.last.line.must_equal 17
- end
- end
-end
Please sign in to comment.
Something went wrong with that request. Please try again.