├── .gitignore ├── .ruby-version ├── .travis.yml ├── Gemfile ├── Rakefile ├── Readme.md ├── gherkin-ruby.gemspec ├── lib ├── gherkin_ruby.rb └── gherkin_ruby │ ├── ast.rb │ ├── parser.rb │ ├── parser │ ├── gherkin.rex │ ├── gherkin.y │ ├── lexer.rb │ └── parser.rb │ └── version.rb └── test ├── gherkin ├── ast_test.rb ├── parser │ ├── lexer_test.rb │ └── parser_test.rb └── parser_test.rb └── test_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | .bundle 3 | Gemfile.lock 4 | pkg/* 5 | .rbx 6 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 2.1.2 -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | script: "bundle exec rake test" 2 | rvm: 3 | - 1.9.3 4 | - 2.0.0 5 | - 2.1.2 6 | - rbx-2 7 | - jruby-19mode 8 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | # Specify your gem's dependencies in gherkin.gemspec 4 | gemspec 5 | gem 'rake' 6 | gem 'racc', platform: :ruby 7 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | require 'bundler' 3 | Bundler::GemHelper.install_tasks 4 | 5 | require 'rake/testtask' 6 | Rake::TestTask.new do |t| 7 | t.libs << "test" 8 | t.test_files = FileList['./test/**/*_test.rb'] 9 | end 10 | 11 | desc "Regenerate Gherkin-ruby lexer and parser." 12 | task :regenerate do 13 | has_rex = `which rex` 14 | has_racc = `which racc` 15 | 16 | if has_rex && has_racc 17 | `rex lib/gherkin_ruby/parser/gherkin.rex -o lib/gherkin_ruby/parser/lexer.rb` 18 | `racc #{'--debug' if ENV['DEBUG_RACC']} lib/gherkin_ruby/parser/gherkin.y -o lib/gherkin_ruby/parser/parser.rb` 19 | else 20 | puts "You need both Rexical and Racc to do that. Install them by doing:" 21 | puts 22 | puts "\t\tgem install rexical" 23 | puts "\t\tgem install racc" 24 | puts 25 | puts "Or just type `bundle install`." 26 | end 27 | end 28 | 29 | task :default => [:regenerate, :test] 30 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # gherkin-ruby [![Build Status](https://secure.travis-ci.org/codegram/gherkin-ruby.png)](http://travis-ci.org/codegram/gherkin-ruby) [![Dependency Status](https://gemnasium.com/codegram/gherkin-ruby.png)](http://gemnasium.com/codegram/gherkin-ruby) 2 | Gherkin-ruby is a pure Ruby implementation of a [Gherkin](http://github.com/cucumber/gherkin) parser. 3 | 4 | Tested with MRI 1.9.3, 2.0.0, head, Rubinius 2.0.0-rc1 and Rubinius head. 5 | 6 | ## WARNING: Will be deprecated after Gherkin 3.0 7 | 8 | A new rewrite of the Gherkin parser used by Cucumber is planned (for version 9 | 3.0) gherkin-ruby will not add any more features until then, and will 10 | eventually be deprecated in favor of Gherkin 3.0. 11 | 12 | ## FAQ 13 | 14 | ### Why this one over the official, fast, Ragel-based Gherkin parser? 15 | 16 | * Less than 200 LOC. 17 | * No Java/.NET crap. 18 | * Fast enough for our purposes (using it for the [Spinach](http://github.com/codegram/spinach) project) 19 | 20 | ### Why don't you support tables? 21 | 22 | * Because we believe it's a BDD anti-pattern. Tables show the need for more 23 | unit tests. 24 | 25 | ## Install 26 | 27 | $ gem install gherkin-ruby 28 | 29 | Or in your Gemfile: 30 | 31 | ```ruby 32 | # Gemfile 33 | 34 | gem 'gherkin-ruby' 35 | ``` 36 | 37 | ## Usage 38 | You can easily implement your own visitors to traverse the Abstract Syntax Tree. The following example just prints the step names to standard output: 39 | 40 | ```ruby 41 | class MyVisitor 42 | def visit(ast) 43 | ast.accept(self) 44 | end 45 | 46 | def visit_Feature(feature) 47 | # Do something nasty with the feature 48 | # Set whatever state you want: 49 | # @current_feature = feature 50 | # etc etc 51 | # And keep visiting its children: 52 | 53 | feature.each { |scenario| scenario.accept(self) } 54 | end 55 | 56 | def visit_Scenario(scenario) 57 | # Do something nasty with the scenario 58 | # Set whatever state you want: 59 | # @current_scenario = scenario 60 | # etc etc 61 | # And keep visiting its children: 62 | 63 | scenario.each { |step| step.accept(self) } 64 | end 65 | 66 | def visit_Background(background) 67 | # Do something nasty with the background 68 | # And keep visiting its children: 69 | 70 | background.each { |step| step.accept(self) } 71 | end 72 | 73 | def visit_Tag(tag) 74 | # Do something nasty with the tag 75 | end 76 | 77 | def visit_Step(step) 78 | # Finally, print the step name. 79 | puts "STEP: #{step.name}" 80 | end 81 | end 82 | 83 | ast = Gherkin.parse(File.read('some.feature')) 84 | visitor = MyVisitor.new 85 | visitor.visit(ast) 86 | ``` 87 | 88 | ## Todo 89 | 90 | * Some optimization 91 | 92 | ## FAQ 93 | 94 | 95 | 96 | ## License 97 | 98 | MIT (Expat) License. Copyright 2011-2013 [Codegram Technologies](http://codegram.com) 99 | -------------------------------------------------------------------------------- /gherkin-ruby.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | $:.push File.expand_path("../lib", __FILE__) 3 | require "gherkin_ruby/version" 4 | 5 | Gem::Specification.new do |s| 6 | s.name = "gherkin-ruby" 7 | s.version = GherkinRuby::VERSION 8 | s.authors = ["Marc Divins", "Josep M. Bach"] 9 | s.email = ["marcdivc@gmail.com", "josep.m.bach@gmail.com"] 10 | s.homepage = "http://github.com/codegram/gherkin-ruby" 11 | s.summary = %q{Gherkin-ruby is a Gherkin parser in pure Ruby using Rexical and Racc} 12 | s.description = %q{Gherkin-ruby is a Gherkin parser in pure Ruby using Rexical and Racc} 13 | 14 | s.rubyforge_project = "gherkin-ruby" 15 | 16 | s.add_development_dependency 'minitest' 17 | s.add_development_dependency 'rexical' 18 | 19 | s.files = `git ls-files`.split("\n") 20 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 21 | s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } 22 | s.require_paths = ["lib"] 23 | end 24 | -------------------------------------------------------------------------------- /lib/gherkin_ruby.rb: -------------------------------------------------------------------------------- 1 | require_relative "gherkin_ruby/version" 2 | require_relative 'gherkin_ruby/ast' 3 | require_relative 'gherkin_ruby/parser' 4 | 5 | module GherkinRuby 6 | def self.parse(input) 7 | parser = Parser.new 8 | parser.parse(input) 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/gherkin_ruby/ast.rb: -------------------------------------------------------------------------------- 1 | module GherkinRuby 2 | module AST 3 | class Node 4 | attr_reader :filename, :line 5 | 6 | def accept(visitor) 7 | name = self.class.name.split('::').last 8 | visitor.send("visit_#{name}".to_sym, self) 9 | end 10 | 11 | def pos(filename, line=nil) 12 | @filename, @line = filename, line 13 | end 14 | end 15 | 16 | class Feature < Node 17 | attr_reader :name, :background, :scenarios, :tags 18 | attr_writer :background, :scenarios, :tags 19 | attr_accessor :description 20 | 21 | include Enumerable 22 | 23 | def initialize(name, scenarios=[], tags=[], background=nil) 24 | @name = name 25 | @background = background 26 | @tags = tags 27 | @scenarios = scenarios 28 | end 29 | 30 | def each 31 | @scenarios.each 32 | end 33 | end 34 | 35 | class Background < Node 36 | attr_reader :steps 37 | attr_writer :steps 38 | 39 | include Enumerable 40 | 41 | def initialize(steps=[]) 42 | @steps = steps 43 | end 44 | 45 | def line 46 | @steps.first.line - 1 if @steps.any? 47 | end 48 | 49 | def each 50 | @steps.each 51 | end 52 | end 53 | 54 | class Scenario < Node 55 | attr_reader :name, :steps, :tags 56 | 57 | include Enumerable 58 | 59 | def initialize(name, steps=[], tags=[]) 60 | @name = name.to_s 61 | @steps = steps 62 | @tags = tags 63 | end 64 | 65 | def line 66 | @steps.first.line - 1 if @steps.any? 67 | end 68 | 69 | def each 70 | @steps.each 71 | end 72 | end 73 | 74 | class Step < Node 75 | attr_reader :name, :keyword 76 | def initialize(name, keyword) 77 | @name = name.to_s 78 | @keyword = keyword.to_s 79 | end 80 | end 81 | 82 | class Tag < Node 83 | attr_reader :name 84 | def initialize(name) 85 | @name = name 86 | end 87 | end 88 | end 89 | end 90 | -------------------------------------------------------------------------------- /lib/gherkin_ruby/parser.rb: -------------------------------------------------------------------------------- 1 | require_relative 'parser/parser' 2 | -------------------------------------------------------------------------------- /lib/gherkin_ruby/parser/gherkin.rex: -------------------------------------------------------------------------------- 1 | # Compile with: rex gherkin.rex -o lexer.rb 2 | 3 | class GherkinRuby::Parser 4 | 5 | macro 6 | BLANK [\ \t]+ 7 | 8 | rule 9 | # Whitespace 10 | {BLANK} # no action 11 | \#.*$ 12 | 13 | # Literals 14 | \n { [:NEWLINE, text] } 15 | 16 | # Keywords 17 | Feature: { [:FEATURE, text[0..-2]] } 18 | Background: { [:BACKGROUND, text[0..-2]] } 19 | Scenario: { [:SCENARIO, text[0..-2]] } 20 | 21 | # Tags 22 | @(\w|-)+ { [:TAG, text[1..-1]] } 23 | 24 | # Step keywords 25 | Given { [:GIVEN, text] } 26 | When { [:WHEN, text] } 27 | Then { [:THEN, text] } 28 | And { [:AND, text] } 29 | But { [:BUT, text] } 30 | 31 | # Text 32 | [^#\n]* { [:TEXT, text.strip] } 33 | 34 | inner 35 | def tokenize(code) 36 | scan_setup(code) 37 | tokens = [] 38 | while token = next_token 39 | tokens << token 40 | end 41 | tokens 42 | end 43 | 44 | end 45 | -------------------------------------------------------------------------------- /lib/gherkin_ruby/parser/gherkin.y: -------------------------------------------------------------------------------- 1 | # Compile with: racc gherkin.y -o parser.rb 2 | 3 | class GherkinRuby::Parser 4 | 5 | # Declare tokens produced by the lexer 6 | token NEWLINE 7 | token FEATURE BACKGROUND SCENARIO 8 | token TAG 9 | token GIVEN WHEN THEN AND BUT 10 | token TEXT 11 | 12 | rule 13 | 14 | Root: 15 | Feature { result = val[0]; } 16 | | 17 | Feature 18 | Scenarios { result = val[0]; result.scenarios = val[1] } 19 | | FeatureTags Feature { result = val[1]; result.tags = val[0] } 20 | | FeatureTags Feature 21 | Scenarios { result = val[1]; result.scenarios = val[2]; result.tags = val[0] } 22 | ; 23 | 24 | Newline: 25 | NEWLINE 26 | | Newline NEWLINE 27 | ; 28 | 29 | FeatureTags: 30 | Tags { result = val[0] } 31 | | Newline Tags { result = val[1] } 32 | 33 | Feature: 34 | FeatureHeader { result = val[0] } 35 | | FeatureHeader 36 | Background { result = val[0]; result.background = val[1] } 37 | ; 38 | 39 | FeatureHeader: 40 | FeatureName { result = val[0] } 41 | | FeatureName Newline { result = val[0] } 42 | | FeatureName Newline 43 | Description { result = val[0]; result.description = val[2] } 44 | ; 45 | 46 | FeatureName: 47 | FEATURE TEXT { result = AST::Feature.new(val[1]); result.pos(filename, lineno) } 48 | | Newline FEATURE TEXT { result = AST::Feature.new(val[2]); result.pos(filename, lineno) } 49 | ; 50 | 51 | Description: 52 | TEXT Newline { result = val[0] } 53 | | Description TEXT Newline { result = val[0...-1].flatten } 54 | ; 55 | 56 | Background: 57 | BackgroundHeader 58 | Steps { result = val[0]; result.steps = val[1] } 59 | ; 60 | 61 | BackgroundHeader: 62 | BACKGROUND Newline { result = AST::Background.new; result.pos(filename, lineno) } 63 | ; 64 | 65 | Steps: 66 | Step { result = [val[0]] } 67 | | Step Newline { result = [val[0]] } 68 | | Step Newline Steps { val[2].unshift(val[0]); result = val[2] } 69 | ; 70 | 71 | Step: 72 | Keyword TEXT { result = AST::Step.new(val[1], val[0]); result.pos(filename, lineno) } 73 | ; 74 | 75 | Keyword: 76 | GIVEN | WHEN | THEN | AND | BUT 77 | ; 78 | 79 | Scenarios: 80 | Scenario { result = [val[0]] } 81 | | Scenarios Scenario { result = val[0] << val[1] } 82 | ; 83 | 84 | Scenario: 85 | SCENARIO TEXT Newline 86 | Steps { result = AST::Scenario.new(val[1], val[3]); result.pos(filename, lineno - 1) } 87 | | Tags Newline 88 | SCENARIO TEXT Newline 89 | Steps { result = AST::Scenario.new(val[3], val[5], val[0]); result.pos(filename, lineno - 2) } 90 | ; 91 | 92 | Tags: 93 | TAG { result = [AST::Tag.new(val[0])] } 94 | | Tags TAG { result = val[0] << AST::Tag.new(val[1]) } 95 | ; 96 | 97 | end 98 | 99 | ---- header 100 | require_relative "lexer" 101 | require_relative "../ast" 102 | 103 | ---- inner 104 | 105 | def parse(input) 106 | @yydebug = true if ENV['DEBUG_RACC'] 107 | scan_str(input) 108 | end 109 | -------------------------------------------------------------------------------- /lib/gherkin_ruby/parser/lexer.rb: -------------------------------------------------------------------------------- 1 | #-- 2 | # DO NOT MODIFY!!!! 3 | # This file is automatically generated by rex 1.0.5 4 | # from lexical definition file "lib/gherkin_ruby/parser/gherkin.rex". 5 | #++ 6 | 7 | require 'racc/parser' 8 | # Compile with: rex gherkin.rex -o lexer.rb 9 | 10 | class GherkinRuby::Parser < Racc::Parser 11 | require 'strscan' 12 | 13 | class ScanError < StandardError ; end 14 | 15 | attr_reader :lineno 16 | attr_reader :filename 17 | attr_accessor :state 18 | 19 | def scan_setup(str) 20 | @ss = StringScanner.new(str) 21 | @lineno = 1 22 | @state = nil 23 | end 24 | 25 | def action 26 | yield 27 | end 28 | 29 | def scan_str(str) 30 | scan_setup(str) 31 | do_parse 32 | end 33 | alias :scan :scan_str 34 | 35 | def load_file( filename ) 36 | @filename = filename 37 | open(filename, "r") do |f| 38 | scan_setup(f.read) 39 | end 40 | end 41 | 42 | def scan_file( filename ) 43 | load_file(filename) 44 | do_parse 45 | end 46 | 47 | 48 | def next_token 49 | return if @ss.eos? 50 | 51 | # skips empty actions 52 | until token = _next_token or @ss.eos?; end 53 | token 54 | end 55 | 56 | def _next_token 57 | text = @ss.peek(1) 58 | @lineno += 1 if text == "\n" 59 | token = case @state 60 | when nil 61 | case 62 | when (text = @ss.scan(/[ \t]+/)) 63 | ; 64 | 65 | when (text = @ss.scan(/\#.*$/)) 66 | ; 67 | 68 | when (text = @ss.scan(/\n/)) 69 | action { [:NEWLINE, text] } 70 | 71 | when (text = @ss.scan(/Feature:/)) 72 | action { [:FEATURE, text[0..-2]] } 73 | 74 | when (text = @ss.scan(/Background:/)) 75 | action { [:BACKGROUND, text[0..-2]] } 76 | 77 | when (text = @ss.scan(/Scenario:/)) 78 | action { [:SCENARIO, text[0..-2]] } 79 | 80 | when (text = @ss.scan(/@(\w|-)+/)) 81 | action { [:TAG, text[1..-1]] } 82 | 83 | when (text = @ss.scan(/Given/)) 84 | action { [:GIVEN, text] } 85 | 86 | when (text = @ss.scan(/When/)) 87 | action { [:WHEN, text] } 88 | 89 | when (text = @ss.scan(/Then/)) 90 | action { [:THEN, text] } 91 | 92 | when (text = @ss.scan(/And/)) 93 | action { [:AND, text] } 94 | 95 | when (text = @ss.scan(/But/)) 96 | action { [:BUT, text] } 97 | 98 | when (text = @ss.scan(/[^#\n]*/)) 99 | action { [:TEXT, text.strip] } 100 | 101 | else 102 | text = @ss.string[@ss.pos .. -1] 103 | raise ScanError, "can not match: '" + text + "'" 104 | end # if 105 | 106 | else 107 | raise ScanError, "undefined state: '" + state.to_s + "'" 108 | end # case state 109 | token 110 | end # def _next_token 111 | 112 | def tokenize(code) 113 | scan_setup(code) 114 | tokens = [] 115 | while token = next_token 116 | tokens << token 117 | end 118 | tokens 119 | end 120 | end # class 121 | -------------------------------------------------------------------------------- /lib/gherkin_ruby/parser/parser.rb: -------------------------------------------------------------------------------- 1 | # 2 | # DO NOT MODIFY!!!! 3 | # This file is automatically generated by Racc 1.4.11 4 | # from Racc grammer file "". 5 | # 6 | 7 | require 'racc/parser.rb' 8 | 9 | require_relative "lexer" 10 | require_relative "../ast" 11 | 12 | module GherkinRuby 13 | class Parser < Racc::Parser 14 | 15 | module_eval(<<'...end gherkin.y/module_eval...', 'gherkin.y', 104) 16 | 17 | def parse(input) 18 | @yydebug = true if ENV['DEBUG_RACC'] 19 | scan_str(input) 20 | end 21 | ...end gherkin.y/module_eval... 22 | ##### State transition tables begin ### 23 | 24 | racc_action_table = [ 25 | 18, 18, 20, 4, 26, 10, 18, 21, 14, 10, 26 | 43, 36, 37, 38, 39, 40, 18, 18, 14, 10, 27 | 45, 36, 37, 38, 39, 40, 18, 14, 10, 4, 28 | 9, 36, 37, 38, 39, 40, 36, 37, 38, 39, 29 | 40, 4, 9, 14, 10, 10, 18, 20, 32, 24, 30 | 21, 4, 4, 29, 21, 4, 47, 18, 48, 4, 31 | 27, 51, 11, 4, 18, 4, 18, 4 ] 32 | 33 | racc_action_check = [ 34 | 25, 5, 5, 15, 9, 5, 54, 15, 31, 31, 35 | 25, 54, 54, 54, 54, 54, 44, 30, 12, 12, 36 | 30, 44, 44, 44, 44, 44, 46, 2, 2, 3, 37 | 3, 46, 46, 46, 46, 46, 23, 23, 23, 23, 38 | 23, 0, 0, 16, 16, 0, 17, 17, 20, 7, 39 | 6, 8, 29, 14, 19, 34, 35, 41, 42, 43, 40 | 11, 45, 1, 48, 49, 51, 53, 24 ] 41 | 42 | racc_action_pointer = [ 43 | 39, 62, 22, 27, nil, -1, 44, 45, 49, -8, 44 | nil, 60, 13, nil, 41, 1, 38, 44, nil, 48, 45 | 36, nil, nil, 29, 65, -2, nil, nil, nil, 50, 46 | 15, 3, nil, nil, 53, 44, nil, nil, nil, nil, 47 | nil, 55, 46, 57, 14, 49, 24, nil, 61, 62, 48 | nil, 63, nil, 64, 4, nil ] 49 | 50 | racc_action_default = [ 51 | -35, -35, -1, -35, -5, -35, -7, -9, -11, -35, 52 | -33, -35, -2, -29, -35, -35, -3, -35, -6, -8, 53 | -35, -34, -10, -35, -35, -12, -14, 56, -30, -35, 54 | -35, -4, -15, -18, -20, -35, -24, -25, -26, -27, 55 | -28, -19, -13, -35, -35, -35, -21, -23, -35, -16, 56 | -31, -35, -22, -17, -35, -32 ] 57 | 58 | racc_goto_table = [ 59 | 5, 28, 33, 17, 12, 6, 2, 23, 25, 16, 60 | 19, 42, 22, 3, 1, 30, nil, nil, 31, nil, 61 | 28, nil, nil, 50, 41, 52, nil, nil, nil, 44, 62 | nil, nil, nil, 55, 46, nil, nil, nil, nil, nil, 63 | nil, nil, nil, 49, nil, nil, nil, nil, 53, nil, 64 | nil, 54 ] 65 | 66 | racc_goto_check = [ 67 | 5, 15, 12, 5, 3, 6, 2, 11, 5, 2, 68 | 6, 10, 8, 4, 1, 5, nil, nil, 3, nil, 69 | 15, nil, nil, 12, 5, 12, nil, nil, nil, 5, 70 | nil, nil, nil, 12, 5, nil, nil, nil, nil, nil, 71 | nil, nil, nil, 5, nil, nil, nil, nil, 5, nil, 72 | nil, 5 ] 73 | 74 | racc_goto_pointer = [ 75 | nil, 14, 6, 2, 13, 0, 5, nil, 5, nil, 76 | -14, 0, -21, nil, nil, -11 ] 77 | 78 | racc_goto_default = [ 79 | nil, nil, nil, nil, nil, nil, 15, 7, nil, 8, 80 | nil, nil, nil, 34, 35, 13 ] 81 | 82 | racc_reduce_table = [ 83 | 0, 0, :racc_error, 84 | 1, 14, :_reduce_1, 85 | 2, 14, :_reduce_2, 86 | 2, 14, :_reduce_3, 87 | 3, 14, :_reduce_4, 88 | 1, 18, :_reduce_none, 89 | 2, 18, :_reduce_none, 90 | 1, 17, :_reduce_7, 91 | 2, 17, :_reduce_8, 92 | 1, 15, :_reduce_9, 93 | 2, 15, :_reduce_10, 94 | 1, 20, :_reduce_11, 95 | 2, 20, :_reduce_12, 96 | 3, 20, :_reduce_13, 97 | 2, 22, :_reduce_14, 98 | 3, 22, :_reduce_15, 99 | 2, 23, :_reduce_16, 100 | 3, 23, :_reduce_17, 101 | 2, 21, :_reduce_18, 102 | 2, 24, :_reduce_19, 103 | 1, 25, :_reduce_20, 104 | 2, 25, :_reduce_21, 105 | 3, 25, :_reduce_22, 106 | 2, 26, :_reduce_23, 107 | 1, 27, :_reduce_none, 108 | 1, 27, :_reduce_none, 109 | 1, 27, :_reduce_none, 110 | 1, 27, :_reduce_none, 111 | 1, 27, :_reduce_none, 112 | 1, 16, :_reduce_29, 113 | 2, 16, :_reduce_30, 114 | 4, 28, :_reduce_31, 115 | 6, 28, :_reduce_32, 116 | 1, 19, :_reduce_33, 117 | 2, 19, :_reduce_34 ] 118 | 119 | racc_reduce_n = 35 120 | 121 | racc_shift_n = 56 122 | 123 | racc_token_table = { 124 | false => 0, 125 | :error => 1, 126 | :NEWLINE => 2, 127 | :FEATURE => 3, 128 | :BACKGROUND => 4, 129 | :SCENARIO => 5, 130 | :TAG => 6, 131 | :GIVEN => 7, 132 | :WHEN => 8, 133 | :THEN => 9, 134 | :AND => 10, 135 | :BUT => 11, 136 | :TEXT => 12 } 137 | 138 | racc_nt_base = 13 139 | 140 | racc_use_result_var = true 141 | 142 | Racc_arg = [ 143 | racc_action_table, 144 | racc_action_check, 145 | racc_action_default, 146 | racc_action_pointer, 147 | racc_goto_table, 148 | racc_goto_check, 149 | racc_goto_default, 150 | racc_goto_pointer, 151 | racc_nt_base, 152 | racc_reduce_table, 153 | racc_token_table, 154 | racc_shift_n, 155 | racc_reduce_n, 156 | racc_use_result_var ] 157 | 158 | Racc_token_to_s_table = [ 159 | "$end", 160 | "error", 161 | "NEWLINE", 162 | "FEATURE", 163 | "BACKGROUND", 164 | "SCENARIO", 165 | "TAG", 166 | "GIVEN", 167 | "WHEN", 168 | "THEN", 169 | "AND", 170 | "BUT", 171 | "TEXT", 172 | "$start", 173 | "Root", 174 | "Feature", 175 | "Scenarios", 176 | "FeatureTags", 177 | "Newline", 178 | "Tags", 179 | "FeatureHeader", 180 | "Background", 181 | "FeatureName", 182 | "Description", 183 | "BackgroundHeader", 184 | "Steps", 185 | "Step", 186 | "Keyword", 187 | "Scenario" ] 188 | 189 | Racc_debug_parser = false 190 | 191 | ##### State transition tables end ##### 192 | 193 | # reduce 0 omitted 194 | 195 | module_eval(<<'.,.,', 'gherkin.y', 14) 196 | def _reduce_1(val, _values, result) 197 | result = val[0]; 198 | result 199 | end 200 | .,., 201 | 202 | module_eval(<<'.,.,', 'gherkin.y', 17) 203 | def _reduce_2(val, _values, result) 204 | result = val[0]; result.scenarios = val[1] 205 | result 206 | end 207 | .,., 208 | 209 | module_eval(<<'.,.,', 'gherkin.y', 18) 210 | def _reduce_3(val, _values, result) 211 | result = val[1]; result.tags = val[0] 212 | result 213 | end 214 | .,., 215 | 216 | module_eval(<<'.,.,', 'gherkin.y', 20) 217 | def _reduce_4(val, _values, result) 218 | result = val[1]; result.scenarios = val[2]; result.tags = val[0] 219 | result 220 | end 221 | .,., 222 | 223 | # reduce 5 omitted 224 | 225 | # reduce 6 omitted 226 | 227 | module_eval(<<'.,.,', 'gherkin.y', 29) 228 | def _reduce_7(val, _values, result) 229 | result = val[0] 230 | result 231 | end 232 | .,., 233 | 234 | module_eval(<<'.,.,', 'gherkin.y', 30) 235 | def _reduce_8(val, _values, result) 236 | result = val[1] 237 | result 238 | end 239 | .,., 240 | 241 | module_eval(<<'.,.,', 'gherkin.y', 33) 242 | def _reduce_9(val, _values, result) 243 | result = val[0] 244 | result 245 | end 246 | .,., 247 | 248 | module_eval(<<'.,.,', 'gherkin.y', 35) 249 | def _reduce_10(val, _values, result) 250 | result = val[0]; result.background = val[1] 251 | result 252 | end 253 | .,., 254 | 255 | module_eval(<<'.,.,', 'gherkin.y', 39) 256 | def _reduce_11(val, _values, result) 257 | result = val[0] 258 | result 259 | end 260 | .,., 261 | 262 | module_eval(<<'.,.,', 'gherkin.y', 40) 263 | def _reduce_12(val, _values, result) 264 | result = val[0] 265 | result 266 | end 267 | .,., 268 | 269 | module_eval(<<'.,.,', 'gherkin.y', 42) 270 | def _reduce_13(val, _values, result) 271 | result = val[0]; result.description = val[2] 272 | result 273 | end 274 | .,., 275 | 276 | module_eval(<<'.,.,', 'gherkin.y', 46) 277 | def _reduce_14(val, _values, result) 278 | result = AST::Feature.new(val[1]); result.pos(filename, lineno) 279 | result 280 | end 281 | .,., 282 | 283 | module_eval(<<'.,.,', 'gherkin.y', 47) 284 | def _reduce_15(val, _values, result) 285 | result = AST::Feature.new(val[2]); result.pos(filename, lineno) 286 | result 287 | end 288 | .,., 289 | 290 | module_eval(<<'.,.,', 'gherkin.y', 51) 291 | def _reduce_16(val, _values, result) 292 | result = val[0] 293 | result 294 | end 295 | .,., 296 | 297 | module_eval(<<'.,.,', 'gherkin.y', 52) 298 | def _reduce_17(val, _values, result) 299 | result = val[0...-1].flatten 300 | result 301 | end 302 | .,., 303 | 304 | module_eval(<<'.,.,', 'gherkin.y', 57) 305 | def _reduce_18(val, _values, result) 306 | result = val[0]; result.steps = val[1] 307 | result 308 | end 309 | .,., 310 | 311 | module_eval(<<'.,.,', 'gherkin.y', 61) 312 | def _reduce_19(val, _values, result) 313 | result = AST::Background.new; result.pos(filename, lineno) 314 | result 315 | end 316 | .,., 317 | 318 | module_eval(<<'.,.,', 'gherkin.y', 65) 319 | def _reduce_20(val, _values, result) 320 | result = [val[0]] 321 | result 322 | end 323 | .,., 324 | 325 | module_eval(<<'.,.,', 'gherkin.y', 66) 326 | def _reduce_21(val, _values, result) 327 | result = [val[0]] 328 | result 329 | end 330 | .,., 331 | 332 | module_eval(<<'.,.,', 'gherkin.y', 67) 333 | def _reduce_22(val, _values, result) 334 | val[2].unshift(val[0]); result = val[2] 335 | result 336 | end 337 | .,., 338 | 339 | module_eval(<<'.,.,', 'gherkin.y', 71) 340 | def _reduce_23(val, _values, result) 341 | result = AST::Step.new(val[1], val[0]); result.pos(filename, lineno) 342 | result 343 | end 344 | .,., 345 | 346 | # reduce 24 omitted 347 | 348 | # reduce 25 omitted 349 | 350 | # reduce 26 omitted 351 | 352 | # reduce 27 omitted 353 | 354 | # reduce 28 omitted 355 | 356 | module_eval(<<'.,.,', 'gherkin.y', 79) 357 | def _reduce_29(val, _values, result) 358 | result = [val[0]] 359 | result 360 | end 361 | .,., 362 | 363 | module_eval(<<'.,.,', 'gherkin.y', 80) 364 | def _reduce_30(val, _values, result) 365 | result = val[0] << val[1] 366 | result 367 | end 368 | .,., 369 | 370 | module_eval(<<'.,.,', 'gherkin.y', 85) 371 | def _reduce_31(val, _values, result) 372 | result = AST::Scenario.new(val[1], val[3]); result.pos(filename, lineno - 1) 373 | result 374 | end 375 | .,., 376 | 377 | module_eval(<<'.,.,', 'gherkin.y', 88) 378 | def _reduce_32(val, _values, result) 379 | result = AST::Scenario.new(val[3], val[5], val[0]); result.pos(filename, lineno - 2) 380 | result 381 | end 382 | .,., 383 | 384 | module_eval(<<'.,.,', 'gherkin.y', 92) 385 | def _reduce_33(val, _values, result) 386 | result = [AST::Tag.new(val[0])] 387 | result 388 | end 389 | .,., 390 | 391 | module_eval(<<'.,.,', 'gherkin.y', 93) 392 | def _reduce_34(val, _values, result) 393 | result = val[0] << AST::Tag.new(val[1]) 394 | result 395 | end 396 | .,., 397 | 398 | def _reduce_none(val, _values, result) 399 | val[0] 400 | end 401 | 402 | end # class Parser 403 | end # module GherkinRuby 404 | -------------------------------------------------------------------------------- /lib/gherkin_ruby/version.rb: -------------------------------------------------------------------------------- 1 | module GherkinRuby 2 | VERSION = "0.3.2" 3 | end 4 | -------------------------------------------------------------------------------- /test/gherkin/ast_test.rb: -------------------------------------------------------------------------------- 1 | require_relative '../test_helper' 2 | require 'ostruct' 3 | 4 | class MyVisitor 5 | def visit_MyNode(my_node) 6 | my_node.elements 7 | end 8 | end 9 | 10 | class MyNode < GherkinRuby::AST::Node 11 | attr_reader :elements 12 | def initialize(name, elements) 13 | @name = name 14 | @elements = elements 15 | end 16 | end 17 | 18 | module GherkinRuby 19 | module AST 20 | describe Node do 21 | it 'is visitable' do 22 | my_node = MyNode.new('My Node', ['foo', 'bar', 'baz']) 23 | visitor = MyVisitor.new 24 | 25 | my_node.accept(visitor).must_equal ['foo', 'bar', 'baz'] 26 | end 27 | end 28 | 29 | [Feature, Scenario, Tag].each do |node| 30 | describe node do 31 | it 'is a Node' do 32 | node.ancestors.must_include Node 33 | end 34 | 35 | it 'has a line' do 36 | instance = node.new("Name") 37 | instance.name.must_equal 'Name' 38 | instance.must_respond_to :line 39 | end 40 | end 41 | end 42 | 43 | describe Feature do 44 | it 'is Enumerable' do 45 | tags = ['-foo', '-bar'] 46 | background = ['foo', 'bar'] 47 | elements = ['+foo', '+bar'] 48 | 49 | instance = Feature.new("My feature", elements, tags, background ) 50 | instance.tags.each.to_a.must_equal ['-foo', '-bar'] 51 | instance.background.each.to_a.must_equal ['foo', 'bar'] 52 | instance.each.to_a.must_equal ['+foo', '+bar'] 53 | end 54 | end 55 | 56 | describe Scenario do 57 | before do 58 | @steps = [ 59 | OpenStruct.new(line: 4), 60 | OpenStruct.new(line: 5), 61 | ] 62 | end 63 | 64 | it 'is Enumerable' do 65 | instance = Scenario.new("Name", @steps) 66 | instance.each.to_a.must_equal @steps 67 | end 68 | 69 | it 'has tags' do 70 | tags = ['javascript', 'wip'] 71 | 72 | instance = Scenario.new("Name", @steps, tags) 73 | instance.tags.must_equal tags 74 | end 75 | end 76 | 77 | describe Background do 78 | before do 79 | @steps = [ 80 | OpenStruct.new(line: 4), 81 | OpenStruct.new(line: 5), 82 | ] 83 | end 84 | 85 | it 'is a Node' do 86 | Background.ancestors.must_include Node 87 | end 88 | 89 | it 'is Enumerable' do 90 | instance = Background.new(@steps) 91 | instance.each.to_a.must_equal @steps 92 | end 93 | 94 | describe 'when there are background steps' do 95 | it 'records line' do 96 | instance = Background.new(@steps) 97 | instance.pos("file", 3) 98 | instance.line.must_equal 3 99 | end 100 | end 101 | 102 | describe 'otherwise' do 103 | it 'does not' do 104 | instance = Background.new([]) 105 | instance.line.must_equal nil 106 | end 107 | end 108 | end 109 | 110 | describe Step do 111 | it 'is a Node' do 112 | Step.ancestors.must_include Node 113 | end 114 | 115 | it 'has a line' do 116 | instance = Step.new("Name", 'Given') 117 | instance.pos("file", 2) 118 | instance.name.must_equal 'Name' 119 | instance.keyword.must_equal 'Given' 120 | instance.line.must_equal 2 121 | end 122 | end 123 | end 124 | end 125 | -------------------------------------------------------------------------------- /test/gherkin/parser/lexer_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module GherkinRuby 4 | describe Parser do 5 | before do 6 | @lexer = Parser.new 7 | end 8 | 9 | it 'ignores commments' do 10 | @lexer.tokenize("# this is a comment").must_equal [] 11 | end 12 | 13 | it 'parses newlines' do 14 | @lexer.tokenize("\n\n").must_equal [[:NEWLINE, "\n"], [:NEWLINE, "\n"]] 15 | end 16 | 17 | it 'parses text and strips it' do 18 | @lexer.tokenize(" Arbitrary text ").must_equal [[:TEXT, "Arbitrary text"]] 19 | end 20 | 21 | it 'parses tags' do 22 | @lexer.tokenize("@javascript @wip").must_equal [ 23 | [:TAG, "javascript"], 24 | [:TAG, "wip"], 25 | ] 26 | end 27 | 28 | describe 'Keywords' do 29 | %w(Feature: Background: Scenario:).each do |keyword| 30 | it "parses #{keyword}:" do 31 | name = keyword[0..-2] 32 | @lexer.tokenize(keyword).must_equal [[name.upcase.to_sym, name]] 33 | end 34 | end 35 | end 36 | 37 | describe 'Step keywords' do 38 | %w(Given When Then And But).each do |keyword| 39 | it "parses #{keyword}" do 40 | @lexer.tokenize(keyword).must_equal [[keyword.upcase.to_sym, keyword]] 41 | end 42 | end 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /test/gherkin/parser/parser_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module GherkinRuby 4 | describe Parser do 5 | def parse(input) 6 | parser = Parser.new 7 | parser.parse(input) 8 | end 9 | 10 | describe 'Feature name and description' do 11 | it 'parses feature header without description' do 12 | feature = parse( 13 | "Feature: my feature" 14 | ) 15 | feature.must_be_kind_of AST::Feature 16 | feature.name.must_equal "my feature" 17 | end 18 | 19 | it 'parses feature header with description' do 20 | feature = parse( 21 | "Feature: my feature\n In order to do something\n As a user\n" 22 | ) 23 | feature.must_be_kind_of AST::Feature 24 | feature.name.must_equal "my feature" 25 | end 26 | 27 | it 'parses feature with tags' do 28 | feature = parse(""" 29 | @wip @with-dash 30 | Feature: Do something 31 | """) 32 | feature.name.must_equal "Do something" 33 | feature.tags.first.name.must_equal "wip" 34 | feature.tags.last.name.must_equal "with-dash" 35 | end 36 | 37 | it 'parses feature with tagsi event without newline at start' do 38 | feature = parse( 39 | "@wip\nFeature: Do something" 40 | ) 41 | feature.name.must_equal "Do something" 42 | feature.tags.first.name.must_equal "wip" 43 | end 44 | 45 | it 'parses feature with background' do 46 | feature = parse(""" 47 | Feature: Do something 48 | 49 | Background: 50 | Given blah foo bar 51 | Then something else 52 | """) 53 | feature.name.must_equal "Do something" 54 | feature.background.must_be_kind_of AST::Background 55 | steps = feature.background.steps 56 | 57 | given_step = steps.first 58 | then_step = steps.last 59 | 60 | given_step.keyword.must_equal "Given" 61 | given_step.name.must_equal "blah foo bar" 62 | then_step.keyword.must_equal "Then" 63 | then_step.name.must_equal "something else" 64 | end 65 | 66 | it 'parses feature with scenarios' do 67 | feature = parse(""" 68 | Feature: Do something 69 | 70 | Scenario: Foo bar baz 71 | Given blah foo bar 72 | Then something else 73 | 74 | Scenario: Foo bar baz blah 75 | Given blah foo bar 76 | Then something else 77 | """) 78 | scenarios = feature.scenarios 79 | 80 | first_scenario = scenarios.first 81 | last_scenario = scenarios.last 82 | 83 | first_scenario.name.must_equal "Foo bar baz" 84 | first_scenario.steps.first.name.must_equal "blah foo bar" 85 | first_scenario.steps.last.name.must_equal "something else" 86 | 87 | last_scenario.name.must_equal "Foo bar baz blah" 88 | last_scenario.steps.first.name.must_equal "blah foo bar" 89 | last_scenario.steps.last.name.must_equal "something else" 90 | end 91 | 92 | it 'parses feature with no ending newline' do 93 | feature = parse(%( 94 | Feature: Do something 95 | 96 | Scenario: Foo bar baz 97 | Given blah foo bar 98 | Then something else)) 99 | scenarios = feature.scenarios 100 | scenarios.size.must_equal 1 101 | 102 | first_scenario = scenarios.first 103 | 104 | first_scenario.name.must_equal "Foo bar baz" 105 | first_scenario.steps.first.name.must_equal "blah foo bar" 106 | first_scenario.steps.last.name.must_equal "something else" 107 | end 108 | 109 | it 'parses feature with scenarios with tags' do 110 | feature = parse(""" 111 | Feature: Do something 112 | 113 | Scenario: Foo bar baz 114 | Given blah foo bar 115 | Then something else 116 | 117 | @javascript @wip @with-vcr 118 | Scenario: Foo bar baz blah 119 | Given blah foo bar 120 | Then something else 121 | """) 122 | scenarios = feature.scenarios 123 | 124 | last_scenario = scenarios.last 125 | 126 | last_scenario.tags[0].name.must_equal "javascript" 127 | last_scenario.tags[1].name.must_equal "wip" 128 | last_scenario.tags[2].name.must_equal "with-vcr" 129 | end 130 | end 131 | end 132 | end 133 | -------------------------------------------------------------------------------- /test/gherkin/parser_test.rb: -------------------------------------------------------------------------------- 1 | require_relative '../test_helper' 2 | 3 | module GherkinRuby 4 | describe 'Feature parsing' do 5 | before do 6 | @scenario = """Feature: My Feature 7 | In order to do something #w000t peoeple 8 | As a developer 9 | I want to be happy #yeah 10 | 11 | # Attend people. This is going to be fun 12 | 13 | Background: 14 | Given something happens before anything else happens 15 | And more things happens before anything else happens 16 | # And I wipe the hard drive 17 | 18 | Scenario: something happens # yeah 19 | Given something happens 20 | Then something cooler happens 21 | 22 | @javascript @wip #@destroy 23 | Scenario: something else happens 24 | Given foo 25 | Then bar 26 | """ 27 | 28 | parser = GherkinRuby::Parser.new 29 | @result = parser.parse(@scenario) 30 | end 31 | 32 | it 'generates a nice tree' do 33 | @result.must_be_kind_of AST::Feature 34 | @result.line.must_equal 1 35 | @result.description.must_equal [ 36 | "In order to do something", 37 | "As a developer", 38 | "I want to be happy"] 39 | 40 | background = @result.background 41 | background.must_be_kind_of AST::Background 42 | background.line.must_equal 8 43 | background.steps.first.keyword.must_equal 'Given' 44 | background.steps.first.name.must_equal 'something happens before anything else happens' 45 | background.steps.first.line.must_equal 9 46 | background.steps.last.keyword.must_equal 'And' 47 | background.steps.last.name.must_equal 'more things happens before anything else happens' 48 | background.steps.last.line.must_equal 10 49 | 50 | first_scenario = @result.scenarios.first 51 | first_scenario.must_be_kind_of AST::Scenario 52 | first_scenario.line.must_equal 13 53 | first_scenario.name.must_equal 'something happens' 54 | first_scenario.steps.first.keyword.must_equal 'Given' 55 | first_scenario.steps.first.name.must_equal 'something happens' 56 | first_scenario.steps.first.line.must_equal 14 57 | first_scenario.steps.last.keyword.must_equal 'Then' 58 | first_scenario.steps.last.name.must_equal 'something cooler happens' 59 | first_scenario.steps.last.line.must_equal 15 60 | 61 | last_scenario = @result.scenarios.last 62 | last_scenario.must_be_kind_of AST::Scenario 63 | last_scenario.line.must_equal 18 64 | last_scenario.name.must_equal 'something else happens' 65 | 66 | last_scenario.tags.first.name.must_equal 'javascript' 67 | last_scenario.tags.last.name.must_equal 'wip' 68 | 69 | last_scenario.steps.first.keyword.must_equal 'Given' 70 | last_scenario.steps.first.name.must_equal 'foo' 71 | last_scenario.steps.first.line.must_equal 19 72 | last_scenario.steps.last.keyword.must_equal 'Then' 73 | last_scenario.steps.last.name.must_equal 'bar' 74 | last_scenario.steps.last.line.must_equal 20 75 | end 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | gem 'minitest' 2 | require 'minitest/spec' 3 | require 'minitest/autorun' 4 | require_relative '../lib/gherkin_ruby' 5 | --------------------------------------------------------------------------------