Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

initial commit

  • Loading branch information...
commit 33e97d95d0b262e3a5fe823ef515f209f58aa5d6 0 parents
@everzet everzet authored
Showing with 3,226 additions and 0 deletions.
  1. +3 −0  .gitmodules
  2. +22 −0 LICENSE
  3. 0  README.md
  4. +11 −0 autoload.php.dist
  5. +14 −0 phpdoc.ini.dist
  6. +25 −0 phpunit.xml.dist
  7. +6 −0 src/Behat/Gherkin/Exception/Exception.php
  8. +39 −0 src/Behat/Gherkin/Keywords/EnglishKeywords.php
  9. +15 −0 src/Behat/Gherkin/Keywords/KeywordsInterface.php
  10. +400 −0 src/Behat/Gherkin/Lexer.php
  11. +28 −0 src/Behat/Gherkin/Node/AbstractNode.php
  12. +72 −0 src/Behat/Gherkin/Node/BackgroundNode.php
  13. +227 −0 src/Behat/Gherkin/Node/FeatureNode.php
  14. +38 −0 src/Behat/Gherkin/Node/OutlineNode.php
  15. +76 −0 src/Behat/Gherkin/Node/PyStringNode.php
  16. +94 −0 src/Behat/Gherkin/Node/ScenarioNode.php
  17. +164 −0 src/Behat/Gherkin/Node/StepNode.php
  18. +188 −0 src/Behat/Gherkin/Node/TableNode.php
  19. +368 −0 src/Behat/Gherkin/Parser.php
  20. +109 −0 tests/Behat/Gherkin/Fixtures/YamlParser.php
  21. +29 −0 tests/Behat/Gherkin/Fixtures/etalons/addition.yml
  22. +18 −0 tests/Behat/Gherkin/Fixtures/etalons/background.yml
  23. +13 −0 tests/Behat/Gherkin/Fixtures/etalons/empty_scenario.yml
  24. +27 −0 tests/Behat/Gherkin/Fixtures/etalons/fibonacci.yml
  25. +13 −0 tests/Behat/Gherkin/Fixtures/etalons/long_title_feature.yml
  26. +44 −0 tests/Behat/Gherkin/Fixtures/etalons/multiline_name.yml
  27. +33 −0 tests/Behat/Gherkin/Fixtures/etalons/outline_with_spaces.yml
  28. +28 −0 tests/Behat/Gherkin/Fixtures/etalons/outline_with_step_table.yml
  29. +21 −0 tests/Behat/Gherkin/Fixtures/etalons/pystring.yml
  30. +18 −0 tests/Behat/Gherkin/Fixtures/etalons/start_comments.yml
  31. +29 −0 tests/Behat/Gherkin/Fixtures/etalons/tables.yml
  32. +35 −0 tests/Behat/Gherkin/Fixtures/etalons/tags_sample.yml
  33. +29 −0 tests/Behat/Gherkin/Fixtures/etalons/trimpystring.yml
  34. +37 −0 tests/Behat/Gherkin/Fixtures/etalons/undefined_multiline_args.yml
  35. +17 −0 tests/Behat/Gherkin/Fixtures/features/addition.feature
  36. +7 −0 tests/Behat/Gherkin/Fixtures/features/background.feature
  37. +8 −0 tests/Behat/Gherkin/Fixtures/features/empty_outline.feature
  38. +7 −0 tests/Behat/Gherkin/Fixtures/features/empty_scenario.feature
  39. +19 −0 tests/Behat/Gherkin/Fixtures/features/fibonacci.feature
  40. +4 −0 tests/Behat/Gherkin/Fixtures/features/long_title_feature.feature
  41. +23 −0 tests/Behat/Gherkin/Fixtures/features/multiline_name.feature
  42. +23 −0 tests/Behat/Gherkin/Fixtures/features/multiplepystrings.feature
  43. +28 −0 tests/Behat/Gherkin/Fixtures/features/outline_with_spaces.feature
  44. +13 −0 tests/Behat/Gherkin/Fixtures/features/outline_with_step_table.feature
  45. +8 −0 tests/Behat/Gherkin/Fixtures/features/pystring.feature
  46. +11 −0 tests/Behat/Gherkin/Fixtures/features/ru_addition.feature
  47. +10 −0 tests/Behat/Gherkin/Fixtures/features/ru_commented.feature
  48. +17 −0 tests/Behat/Gherkin/Fixtures/features/ru_consecutive_calculations.feature
  49. +16 −0 tests/Behat/Gherkin/Fixtures/features/ru_division.feature
  50. +12 −0 tests/Behat/Gherkin/Fixtures/features/start_comments.feature
  51. +15 −0 tests/Behat/Gherkin/Fixtures/features/tables.feature
  52. +17 −0 tests/Behat/Gherkin/Fixtures/features/tags_sample.feature
  53. +9 −0 tests/Behat/Gherkin/Fixtures/features/test_unit.feature
  54. +14 −0 tests/Behat/Gherkin/Fixtures/features/trimpystring.feature
  55. +12 −0 tests/Behat/Gherkin/Fixtures/features/undefined_multiline_args.feature
  56. +54 −0 tests/Behat/Gherkin/Node/BackgroundNodeTest.php
  57. +113 −0 tests/Behat/Gherkin/Node/FeatureNodeTest.php
  58. +96 −0 tests/Behat/Gherkin/Node/OutlineNodeTest.php
  59. +78 −0 tests/Behat/Gherkin/Node/PyStringNodeTest.php
  60. +84 −0 tests/Behat/Gherkin/Node/ScenarioNodeTest.php
  61. +86 −0 tests/Behat/Gherkin/Node/StepNodeTest.php
  62. +85 −0 tests/Behat/Gherkin/Node/TableNodeTest.php
  63. +59 −0 tests/Behat/Gherkin/ParserTest.php
  64. +7 −0 tests/bootstrap.php
  65. +1 −0  vendor/symfony
3  .gitmodules
@@ -0,0 +1,3 @@
+[submodule "vendor/symfony"]
+ path = vendor/symfony
+ url = https://github.com/symfony/symfony.git
22 LICENSE
@@ -0,0 +1,22 @@
+Copyright (c) 2011 Konstantin Kudryashov <ever.zet@gmail.com>
+
+Permission is hereby granted, free of charge, to any person
+obtaining a copy of this software and associated documentation
+files (the "Software"), to deal in the Software without
+restriction, including without limitation the rights to use,
+copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following
+conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
0  README.md
No changes.
11 autoload.php.dist
@@ -0,0 +1,11 @@
+<?php
+
+require_once __DIR__ . '/vendor/symfony/src/Symfony/Component/HttpFoundation/UniversalClassLoader.php';
+use Symfony\Component\HttpFoundation\UniversalClassLoader;
+
+$loader = new UniversalClassLoader();
+$loader->registerNamespaces(array(
+ 'Behat' => __DIR__ . '/src',
+ 'Symfony' => __DIR__ . '/vendor/symfony/src'
+));
+$loader->register();
14 phpdoc.ini.dist
@@ -0,0 +1,14 @@
+files = "*.php"
+ignore = "CVS, .svn, .git, _compiled"
+source_path = "./src"
+doclet = standard
+overview = readme.html
+package_comment_dir = ./
+public = on
+d = "api"
+default_package = "Behat Gherkin"
+windowtitle = "Behat Gherkin"
+doctitle = "Behat Gherkin: PHP 5.3 Gherkin parser"
+header = "Behat Gherkin"
+footer = "Behat Gherkin"
+tree = on
25 phpunit.xml.dist
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<phpunit backupGlobals="false"
+ backupStaticAttributes="false"
+ colors="false"
+ convertErrorsToExceptions="true"
+ convertNoticesToExceptions="true"
+ convertWarningsToExceptions="true"
+ processIsolation="false"
+ stopOnFailure="false"
+ syntaxCheck="false"
+ bootstrap="tests/bootstrap.php"
+>
+ <testsuites>
+ <testsuite name="Gherkin Test Suite">
+ <directory>./tests/Behat/Gherkin/</directory>
+ </testsuite>
+ </testsuites>
+
+ <filter>
+ <whitelist>
+ <directory>./src/Behat/Gherkin/</directory>
+ </whitelist>
+ </filter>
+</phpunit>
6 src/Behat/Gherkin/Exception/Exception.php
@@ -0,0 +1,6 @@
+<?php
+
+namespace Behat\Gherkin\Exception;
+
+class Exception extends \Exception
+{}
39 src/Behat/Gherkin/Keywords/EnglishKeywords.php
@@ -0,0 +1,39 @@
+<?php
+
+namespace Behat\Gherkin\Keywords;
+
+class EnglishKeywords implements KeywordsInterface
+{
+ public function setLanguage($language)
+ {}
+
+ public function getFeatureKeyword()
+ {
+ return 'Feature';
+ }
+
+ public function getBackgroundKeyword()
+ {
+ return 'Background';
+ }
+
+ public function getScenarioKeyword()
+ {
+ return 'Scenario';
+ }
+
+ public function getOutlineKeyword()
+ {
+ return 'Scenario Outline';
+ }
+
+ public function getExamplesKeyword()
+ {
+ return 'Examples';
+ }
+
+ public function getStepKeywords()
+ {
+ return array('Given', 'When', 'Then', 'And', 'Or');
+ }
+}
15 src/Behat/Gherkin/Keywords/KeywordsInterface.php
@@ -0,0 +1,15 @@
+<?php
+
+namespace Behat\Gherkin\Keywords;
+
+interface KeywordsInterface
+{
+ function setLanguage($language);
+
+ function getFeatureKeyword();
+ function getBackgroundKeyword();
+ function getScenarioKeyword();
+ function getOutlineKeyword();
+ function getExamplesKeyword();
+ function getStepKeywords();
+}
400 src/Behat/Gherkin/Lexer.php
@@ -0,0 +1,400 @@
+<?php
+
+namespace Behat\Gherkin;
+
+use Behat\Gherkin\Exception\Exception,
+ Behat\Gherkin\Keywords\KeywordsInterface;
+
+class Lexer
+{
+ protected $input;
+ protected $keywords;
+ protected $line = 1;
+ protected $deferredObjects = array();
+ protected $stash = array();
+ protected $inPyString = false;
+ protected $lastIndentString = '';
+
+ /**
+ * Initialize Lexer.
+ *
+ * @param KeywordsInterface $keywords keywords holder
+ */
+ public function __construct(KeywordsInterface $keywords)
+ {
+ $this->keywords = $keywords;
+ }
+
+ /**
+ * Set lexer input.
+ *
+ * @param string $input input string
+ */
+ public function setInput($input)
+ {
+ $this->input = preg_replace(array('/\r\n|\r/', '/\t/'), array("\n", ' '), $input);
+ $this->line = 1;
+ $this->deferredObjects = array();
+ $this->stash = array();
+ $this->inPyString = false;
+ $this->lastIndentString = '';
+ }
+
+ /**
+ * Set keywords language.
+ *
+ * @param string $language
+ */
+ public function setLanguage($language)
+ {
+ $this->keywords->setLanguage($language);
+ }
+
+ /**
+ * Return next token or previously stashed one.
+ *
+ * @return Object
+ */
+ public function getAdvancedToken()
+ {
+ if ($token = $this->getStashedToken()) {
+ return $token;
+ }
+
+ return $this->getNextToken();
+ }
+
+ /**
+ * Return current line number.
+ *
+ * @return integer
+ */
+ public function getCurrentLine()
+ {
+ return $this->line;
+ }
+
+ /**
+ * Defer token.
+ *
+ * @param Object $token token to defer
+ */
+ public function deferToken(\stdClass $token)
+ {
+ $this->deferredObjects[] = $token;
+ }
+
+ /**
+ * Predict for number of tokens.
+ *
+ * @param integer $number number of tokens to predict
+ *
+ * @return Object predicted token
+ */
+ public function predictToken($number = 1)
+ {
+ $fetch = $number - count($this->stash);
+
+ while ($fetch-- > 0) {
+ $this->stash[] = $this->getNextToken();
+ }
+
+ return $this->stash[--$number];
+ }
+
+ /**
+ * Construct token with specified parameters.
+ *
+ * @param string $type token type
+ * @param string $value token value
+ *
+ * @return Object new token object
+ */
+ public function takeToken($type, $value = null)
+ {
+ return (Object) array(
+ 'type' => $type,
+ 'line' => $this->line,
+ 'value' => $value ?: null
+ );
+ }
+
+ /**
+ * Return stashed token.
+ *
+ * @return Object|boolean token if has stashed, false otherways
+ */
+ protected function getStashedToken()
+ {
+ return count($this->stash) ? array_shift($this->stash) : null;
+ }
+
+ /**
+ * Return deferred token.
+ *
+ * @return Object|boolean token if has deferred, false otherways
+ */
+ protected function getDeferredToken()
+ {
+ return count($this->deferredObjects) ? array_shift($this->deferredObjects) : null;
+ }
+
+ /**
+ * Return next token.
+ *
+ * @return Object
+ */
+ protected function getNextToken()
+ {
+ return $this->getDeferredToken()
+ ?: $this->scanEOS()
+ ?: $this->scanPyStringOperator()
+ ?: $this->scanPyStringContent()
+ ?: $this->scanTableRow()
+ ?: $this->scanFeature()
+ ?: $this->scanBackground()
+ ?: $this->scanScenario()
+ ?: $this->scanOutline()
+ ?: $this->scanExamples()
+ ?: $this->scanStep()
+ ?: $this->scanNewline()
+ ?: $this->scanComment()
+ ?: $this->scanTags()
+ ?: $this->scanText();
+ }
+
+ /**
+ * Consume input.
+ *
+ * @param integer $length length of input to consume
+ */
+ protected function consumeInput($length)
+ {
+ $this->input = mb_substr($this->input, $length);
+ }
+
+ /**
+ * Scan for token with specified regex.
+ *
+ * @param string $regex regular expression
+ * @param string $type expected token type
+ *
+ * @return Object|null
+ */
+ protected function scanInput($regex, $type)
+ {
+ $matches = array();
+ if (preg_match($regex, $this->input, $matches)) {
+ $this->consumeInput(mb_strlen($matches[0]));
+
+ return $this->takeToken($type, $matches[1]);
+ }
+ }
+
+ /**
+ * Scan EOS from input & return it if found.
+ *
+ * @return Object|null
+ */
+ protected function scanEOS()
+ {
+ if (mb_strlen($this->input)) {
+ return;
+ }
+
+ return $this->takeToken('EOS');
+ }
+
+ /**
+ * Scan Feature from input & return it if found.
+ *
+ * @return Object|null
+ */
+ protected function scanFeature()
+ {
+ return $this->scanInput('/^' . $this->keywords->getFeatureKeyword() . '\: *([^\n]*)/', 'Feature');
+ }
+
+ /**
+ * Scan Background from input & return it if found.
+ *
+ * @return Object|null
+ */
+ protected function scanBackground()
+ {
+ return $this->scanInput('/^' . $this->keywords->getBackgroundKeyword() . '\: *([^\n]*)/', 'Background');
+ }
+
+ /**
+ * Scan Scenario from input & return it if found.
+ *
+ * @return Object|null
+ */
+ protected function scanScenario()
+ {
+ return $this->scanInput('/^' . $this->keywords->getScenarioKeyword() . '\: *([^\n]*)/', 'Scenario');
+ }
+
+ /**
+ * Scan Scenario Outline from input & return it if found.
+ *
+ * @return Object|null
+ */
+ protected function scanOutline()
+ {
+ return $this->scanInput('/^' . $this->keywords->getOutlineKeyword() . '\: *([^\n]*)/', 'Outline');
+ }
+
+ /**
+ * Scan Scenario Outline Examples from input & return it if found.
+ *
+ * @return Object|null
+ */
+ protected function scanExamples()
+ {
+ return $this->scanInput('/^' . $this->keywords->getExamplesKeyword() . '\: *([^\n]*)/', 'Examples');
+ }
+
+ /**
+ * Scan Step from input & return it if found.
+ *
+ * @return Object|null
+ */
+ protected function scanStep()
+ {
+ $matches = array();
+ $keywords = $this->keywords->getStepKeywords();
+
+ if (preg_match('/^(' . implode('|', $keywords) . ') +([^\n]+)/', $this->input, $matches)) {
+ $this->consumeInput(mb_strlen($matches[0]));
+ $token = $this->takeToken('Step', $matches[1]);
+ $token->text = $matches[2];
+
+ return $token;
+ }
+ }
+
+ /**
+ * Scan PyString from input & return it if found.
+ *
+ * @return Object|null
+ */
+ protected function scanPyStringOperator()
+ {
+ $matches = array();
+
+ if (preg_match('/^"""[^\n]*/', $this->input, $matches)) {
+ $this->consumeInput(mb_strlen($matches[0]));
+ $this->inPyString =! $this->inPyString;
+
+ $token = $this->takeToken('PyStringOperator');
+ $token->swallow = mb_strlen($this->lastIndentString);
+
+ return $token;
+ }
+ }
+
+ /**
+ * Scan PyString content.
+ *
+ * @return Object|null
+ */
+ protected function scanPyStringContent()
+ {
+ if ($this->inPyString) {
+ $matches = array();
+ if (preg_match('/^([^\n]+)/', $this->input, $matches)) {
+ $this->consumeInput(mb_strlen($matches[0]));
+
+ return $this->takeToken('Text', $this->lastIndentString . $matches[1]);
+ }
+ }
+ }
+
+ /**
+ * Scan Table Row from input & return it if found.
+ *
+ * @return Object|null
+ */
+ protected function scanTableRow()
+ {
+ $matches = array();
+
+ if (preg_match('/^\|([^\n]+)\|/', $this->input, $matches)) {
+ $this->consumeInput(mb_strlen($matches[0]));
+ $token = $this->takeToken('TableRow');
+
+ // Split & trim row columns
+ $columns = explode('|', $matches[1]);
+ $columns = array_map(function($column) {
+ return trim($column);
+ }, $columns);
+ $token->columns = $columns;
+
+ return $token;
+ }
+ }
+
+ /**
+ * Scan Newline from input & return it if found.
+ *
+ * @return Object|null
+ */
+ protected function scanNewline()
+ {
+ $matches = array();
+
+ if (preg_match('/^\n( *)/', $this->input, $matches)) {
+ $this->line++;
+ $this->lastIndentString = $matches[1];
+
+ $this->consumeInput(mb_strlen($matches[0]));
+ $token = $this->takeToken('Newline');
+
+ return $token;
+ }
+ }
+
+ /**
+ * Scan Tags from input & return it if found.
+ *
+ * @return Object|null
+ */
+ protected function scanTags()
+ {
+ $matches = array();
+
+ if (preg_match('/^@([^\n]+)/', $this->input, $matches)) {
+ $this->consumeInput(mb_strlen($matches[0]));
+ $token = $this->takeToken('Tag');
+
+ $tags = explode('@', $matches[1]);
+ $tags = array_map(function($tag){
+ return trim($tag);
+ }, $tags);
+ $token->tags = $tags;
+
+ return $token;
+ }
+ }
+
+ /**
+ * Scan Comment from input & return it if found.
+ *
+ * @return Object|null
+ */
+ protected function scanComment()
+ {
+ return $this->scanInput('/^\#([^\n]*)/', 'Comment');
+ }
+
+ /**
+ * Scan text from input & return it if found.
+ *
+ * @return Object|null
+ */
+ protected function scanText()
+ {
+ return $this->scanInput('/^([^\n\#]+)/', 'Text');
+ }
+}
28 src/Behat/Gherkin/Node/AbstractNode.php
@@ -0,0 +1,28 @@
+<?php
+
+namespace Behat\Gherkin\Node;
+
+abstract class AbstractNode
+{
+ private $line;
+
+ /**
+ * Initialize node.
+ *
+ * @param integer $line line number
+ */
+ public function __construct($line = 0)
+ {
+ $this->line = $line;
+ }
+
+ /**
+ * Return definition line number.
+ *
+ * @return integer
+ */
+ public function getLine()
+ {
+ return $this->line;
+ }
+}
72 src/Behat/Gherkin/Node/BackgroundNode.php
@@ -0,0 +1,72 @@
+<?php
+
+namespace Behat\Gherkin\Node;
+
+class BackgroundNode extends AbstractNode
+{
+ private $steps = array();
+ private $feature;
+
+ /**
+ * Add Step to the background.
+ *
+ * @param StepNode $step
+ */
+ public function addStep(StepNode $step)
+ {
+ $step->setParent($this);
+ $this->steps[] = $step;
+ }
+
+ /**
+ * Set steps array of the background.
+ *
+ * @param array $steps array of StepNode
+ */
+ public function setSteps(array $steps)
+ {
+ foreach ($steps as $step) {
+ $this->addStep($step);
+ }
+ }
+
+ /**
+ * Check if background has steps.
+ *
+ * @return boolean
+ */
+ public function hasSteps()
+ {
+ return count($this->steps) > 0;
+ }
+
+ /**
+ * Return steps array.
+ *
+ * @return array array of StepNode
+ */
+ public function getSteps()
+ {
+ return $this->steps;
+ }
+
+ /**
+ * Set parent feature of the node.
+ *
+ * @param FeatureNode $feature
+ */
+ public function setFeature(FeatureNode $feature)
+ {
+ $this->feature = $feature;
+ }
+
+ /**
+ * Return parent feature of the node.
+ *
+ * @return FeatureNode
+ */
+ public function getFeature()
+ {
+ return $this->feature;
+ }
+}
227 src/Behat/Gherkin/Node/FeatureNode.php
@@ -0,0 +1,227 @@
+<?php
+
+namespace Behat\Gherkin\Node;
+
+class FeatureNode extends AbstractNode
+{
+ private $title;
+ private $description;
+ private $file;
+ private $background;
+ private $language = 'en';
+ private $scenarios = array();
+ private $tags = array();
+
+ /**
+ * Initialize feature.
+ *
+ * @param string $title feature title
+ * @param string $description feature description (3-liner)
+ * @param string $file feature filename
+ * @param integer $line definition line
+ */
+ public function __construct($title = null, $description = null, $file = null, $line = 0)
+ {
+ parent::__construct($line);
+
+ $this->title = $title;
+ $this->description = $description;
+ $this->file = $file;
+ }
+
+ /**
+ * Set feature title.
+ *
+ * @param string $title
+ */
+ public function setTitle($title)
+ {
+ $this->title = $title;
+ }
+
+ /**
+ * Return feature title.
+ *
+ * @return string
+ */
+ public function getTitle()
+ {
+ return $this->title;
+ }
+
+ /**
+ * Set feature description (3-liner).
+ *
+ * @param string $description
+ */
+ public function setDescription($description)
+ {
+ $this->description = $description;
+ }
+
+ /**
+ * Return feature description.
+ *
+ * @return string
+ */
+ public function getDescription()
+ {
+ return $this->description;
+ }
+
+ /**
+ * Set language of the feature.
+ *
+ * @param string $language en|ru|pt-BR etc.
+ */
+ public function setLanguage($language)
+ {
+ $this->language = $language;
+ }
+
+ /**
+ * Return language of the feature.
+ *
+ * @return string
+ */
+ public function getLanguage()
+ {
+ return $this->language;
+ }
+
+ /**
+ * Set feature background.
+ *
+ * @param BackgroundNode $background
+ */
+ public function setBackground(BackgroundNode $background)
+ {
+ $background->setFeature($this);
+ $this->background = $background;
+ }
+
+ /**
+ * Check if feature has background.
+ *
+ * @return boolean
+ */
+ public function hasBackground()
+ {
+ return null !== $this->background;
+ }
+
+ /**
+ * Return feature background.
+ *
+ * @return BackgroundNode
+ */
+ public function getBackground()
+ {
+ return $this->background;
+ }
+
+ /**
+ * Add Scenario or Outline to feature.
+ *
+ * @param ScenarioNode $scenario
+ */
+ public function addScenario(ScenarioNode $scenario)
+ {
+ $scenario->setFeature($this);
+ $this->scenarios[] = $scenario;
+ }
+
+ /**
+ * Set Scenarios or Outlines list of the feature.
+ *
+ * @param array $scenarios array of ScenariosNode & OutlineNode
+ */
+ public function setScenarios(array $scenarios)
+ {
+ foreach ($scenarios as $scenario) {
+ $this->addScenario($scenario);
+ }
+ }
+
+ /**
+ * Check that feature has scenarios.
+ *
+ * @return boolean
+ */
+ public function hasScenarios()
+ {
+ return count($this->scenarios) > 0;
+ }
+
+ /**
+ * Return added Scenarios or Outlines.
+ *
+ * @return array array of ScenariosNode & OutlineNode
+ */
+ public function getScenarios()
+ {
+ return $this->scenarios;
+ }
+
+ /**
+ * Set feature tags.
+ *
+ * @param array $tags
+ */
+ public function setTags(array $tags)
+ {
+ $this->tags = $tags;
+ }
+
+ /**
+ * Add tag to feature.
+ *
+ * @param string $tag
+ */
+ public function addTag($tag)
+ {
+ $this->tags[] = $tag;
+ }
+
+ /**
+ * Check if feature has tags.
+ *
+ * @return boolean
+ */
+ public function hasTags()
+ {
+ return count($this->tags) > 0;
+ }
+
+ /**
+ * Check if feature has tag.
+ *
+ * @param string $tag
+ *
+ * @return boolean
+ */
+ public function hasTag($tag)
+ {
+ return in_array($tag, $this->tags);
+ }
+
+ /**
+ * Return feature tags.
+ *
+ * @return array
+ */
+ public function getTags()
+ {
+ return $this->tags;
+ }
+
+ /**
+ * Return feature filename.
+ *
+ * @return string
+ */
+ public function getFile()
+ {
+ return $this->file;
+ }
+}
38 src/Behat/Gherkin/Node/OutlineNode.php
@@ -0,0 +1,38 @@
+<?php
+
+namespace Behat\Gherkin\Node;
+
+class OutlineNode extends ScenarioNode
+{
+ private $examples;
+
+ /**
+ * Set outline examples table.
+ *
+ * @param TableNode $examples
+ */
+ public function setExamples(TableNode $examples)
+ {
+ $this->examples = $examples;
+ }
+
+ /**
+ * Check if outline has examples.
+ *
+ * @return boolean
+ */
+ public function hasExamples()
+ {
+ return null !== $this->examples;
+ }
+
+ /**
+ * Return examples table.
+ *
+ * @return TableNode
+ */
+ public function getExamples()
+ {
+ return $this->examples;
+ }
+}
76 src/Behat/Gherkin/Node/PyStringNode.php
@@ -0,0 +1,76 @@
+<?php
+
+namespace Behat\Gherkin\Node;
+
+class PyStringNode
+{
+ private $ltrimCount;
+ private $lines = array();
+
+ /**
+ * Initialize PyString.
+ *
+ * @param string $string initial string
+ * @param integer $ltrimCount left-trim count
+ */
+ public function __construct($string = null, $ltrimCount = 0)
+ {
+ $this->ltrimCount = $ltrimCount;
+
+ if (null !== $string) {
+ $string = preg_replace("/\r\n|\r/", "\n", $string);
+
+ foreach (explode("\n", $string) as $line) {
+ $this->addLine($line);
+ }
+ }
+ }
+
+ /**
+ * Replace PyString holders with tokens.
+ *
+ * @param array $tokens hash (search => replace)
+ */
+ public function replaceTokens(array $tokens)
+ {
+ foreach ($tokens as $key => $value) {
+ foreach (array_keys($this->lines) as $line) {
+ $this->lines[$line] = str_replace('<' . $key . '>', $value, $this->lines[$line], $count);
+ }
+ }
+ }
+
+ /**
+ * Add line to the PyString.
+ *
+ * @param string $line
+ */
+ public function addLine($line)
+ {
+ if ($this->ltrimCount >= 1) {
+ $line = preg_replace('/^\s{1,' . $this->ltrimCount . '}/', '', $line);
+ }
+
+ $this->lines[] = $line;
+ }
+
+ /**
+ * Return PyString lines.
+ *
+ * @return array
+ */
+ public function getLines()
+ {
+ return $this->lines;
+ }
+
+ /**
+ * Convert PyString lines array into string.
+ *
+ * @return string
+ */
+ public function __toString()
+ {
+ return implode("\n", $this->lines);
+ }
+}
94 src/Behat/Gherkin/Node/ScenarioNode.php
@@ -0,0 +1,94 @@
+<?php
+
+namespace Behat\Gherkin\Node;
+
+class ScenarioNode extends BackgroundNode
+{
+ private $title;
+ private $tags = array();
+
+ /**
+ * Initialize scenario.
+ *
+ * @param string $title scenario title
+ * @param integer $line definition line
+ */
+ public function __construct($title = null, $line = 0)
+ {
+ parent::__construct($line);
+
+ $this->title = $title;
+ }
+
+ /**
+ * Set scenario title.
+ *
+ * @param string $title
+ */
+ public function setTitle($title)
+ {
+ $this->title = $title;
+ }
+
+ /**
+ * Return scenario title.
+ *
+ * @return string
+ */
+ public function getTitle()
+ {
+ return $this->title;
+ }
+
+ /**
+ * Set scenario tags.
+ *
+ * @param array $tags
+ */
+ public function setTags(array $tags)
+ {
+ $this->tags = $tags;
+ }
+
+ /**
+ * Add tag to scenario.
+ *
+ * @param string $tag
+ */
+ public function addTag($tag)
+ {
+ $this->tags[] = $tag;
+ }
+
+ /**
+ * Check if scenario has tags.
+ *
+ * @return boolean
+ */
+ public function hasTags()
+ {
+ return count($this->tags) > 0;
+ }
+
+ /**
+ * Check if scenario has tag.
+ *
+ * @param string $tag
+ *
+ * @return boolean
+ */
+ public function hasTag($tag)
+ {
+ return in_array($tag, $this->tags);
+ }
+
+ /**
+ * Return scenario tags.
+ *
+ * @return array
+ */
+ public function getTags()
+ {
+ return $this->tags;
+ }
+}
164 src/Behat/Gherkin/Node/StepNode.php
@@ -0,0 +1,164 @@
+<?php
+
+namespace Behat\Gherkin\Node;
+
+class StepNode extends AbstractNode
+{
+ private $type;
+ private $text;
+ private $parent;
+ private $tokens = array();
+ private $arguments = array();
+
+ /**
+ * Initizalize step.
+ *
+ * @param string $type step type
+ * @param string $text step text
+ * @param integer $line definition line
+ */
+ public function __construct($type, $text = null, $line = 0)
+ {
+ parent::__construct($line);
+
+ $this->type = $type;
+ $this->text = $text;
+ }
+
+ /**
+ * Set step type.
+ *
+ * @param string $type Given|When|Then|And etc.
+ */
+ public function setType($type)
+ {
+ $this->type = $type;
+ }
+
+ /**
+ * Return step type.
+ *
+ * @return string
+ */
+ public function getType()
+ {
+ return $this->type;
+ }
+
+ /**
+ * Set step text.
+ *
+ * @param string $text
+ */
+ public function setText($text)
+ {
+ $this->text = $text;
+ }
+
+ /**
+ * Return untokenized step text.
+ *
+ * @return string
+ */
+ public function getCleanText()
+ {
+ return $this->text;
+ }
+
+ /**
+ * Return tokenized step text.
+ *
+ * @see setTokens
+ * @return string
+ */
+ public function getText()
+ {
+ $text = $this->text;
+
+ foreach ($this->tokens as $key => $value) {
+ $text = str_replace('<' . $key . '>', $value, $text);
+ }
+
+ return $text;
+ }
+
+ /**
+ * Set text tokens (replacers).
+ *
+ * @param array $tokens hash of tokens (search => replace, search => replace, ...)
+ */
+ public function setTokens(array $tokens)
+ {
+ $this->tokens = $tokens;
+ }
+
+ /**
+ * Return tokens (replacers).
+ *
+ * @return array
+ */
+ public function getTokens()
+ {
+ return $this->tokens;
+ }
+
+ /**
+ * Add argument to step.
+ *
+ * @param PyStringNode|TableNode $argument
+ */
+ public function addArgument($argument)
+ {
+ $this->arguments[] = $argument;
+ }
+
+ /**
+ * Set step arguments.
+ *
+ * @param array $arguments
+ */
+ public function setArguments(array $arguments)
+ {
+ $this->arguments = $arguments;
+ }
+
+ /**
+ * Check if step has arguments.
+ *
+ * @return boolean
+ */
+ public function hasArguments()
+ {
+ return count($this->arguments) > 0;
+ }
+
+ /**
+ * Return step arguments.
+ *
+ * @return array
+ */
+ public function getArguments()
+ {
+ return $this->arguments;
+ }
+
+ /**
+ * Set parent node of the step.
+ *
+ * @param BackgroundNode $node
+ */
+ public function setParent(BackgroundNode $node)
+ {
+ $this->parent = $node;
+ }
+
+ /**
+ * Return parent node of the step.
+ *
+ * @return BackgroundNode
+ */
+ public function getParent()
+ {
+ return $this->parent;
+ }
+}
188 src/Behat/Gherkin/Node/TableNode.php
@@ -0,0 +1,188 @@
+<?php
+
+namespace Behat\Gherkin\Node;
+
+class TableNode
+{
+ private $rows = array();
+
+ /**
+ * Initialize table.
+ *
+ * @param string $table initial table string
+ */
+ public function __construct($table = null)
+ {
+ if (null !== $table) {
+ $table = preg_replace("/\r\n|\r/", "\n", $table);
+
+ foreach (explode("\n", $table) as $row) {
+ $this->addRow($row);
+ }
+ }
+ }
+
+ /**
+ * Add row to the string.
+ *
+ * @param string $row columns hash (column1 => value, column2 => value)
+ */
+ public function addRow($row)
+ {
+ if (is_array($row)) {
+ $this->rows[] = $row;
+ } else {
+ $row = preg_replace("/^\s*\||\|\s*$/", '', $row);
+
+ $this->rows[] = array_map(function($item) {
+ return preg_replace("/^\s*|\s*$/", '', $item);
+ }, explode('|', $row));
+ }
+ }
+
+ /**
+ * Return table rows.
+ *
+ * @return array
+ */
+ public function getRows()
+ {
+ return $this->rows;
+ }
+
+ /**
+ * Return specific row in a table.
+ *
+ * @param integer $rowNum row number
+ *
+ * @return array columns hash (column1 => value, column2 => value)
+ */
+ public function getRow($rowNum)
+ {
+ return $this->rows[$rowNum];
+ }
+
+ /**
+ * Convert row into delimited string.
+ *
+ * @param integer $rowNum row number
+ *
+ * @return string
+ */
+ public function getRowAsString($rowNum)
+ {
+ $values = array();
+ foreach ($this->getRow($rowNum) as $col => $value) {
+ $values[] = $this->padRight(' '.$value.' ', $this->getMaxLengthForColumn($col) + 2);
+ }
+
+ return sprintf('|%s|', implode('|', $values));
+ }
+
+ /**
+ * Replace column value holders with tokens.
+ *
+ * @param array $tokens hash (search => replace)
+ */
+ public function replaceTokens(array $tokens)
+ {
+ foreach ($tokens as $key => $value) {
+ foreach (array_keys($this->rows) as $row) {
+ foreach (array_keys($this->rows[$row]) as $col) {
+ $this->rows[$row][$col] = str_replace('<'.$key.'>', $value, $this->rows[$row][$col], $count);
+ }
+ }
+ }
+ }
+
+ /**
+ * Return table hash, formed by columns (ColumnHash).
+ *
+ * @return array
+ */
+ public function getHash()
+ {
+ $rows = $this->getRows();
+ $keys = array_shift($rows);
+
+ $hash = array();
+ foreach ($rows as $row) {
+ $hash[] = array_combine($keys, $row);
+ }
+
+ return $hash;
+ }
+
+ /**
+ * Return table hash, formed by rows (RowsHash).
+ *
+ * @return array
+ */
+ public function getRowsHash()
+ {
+ $hash = array();
+ $rows = $this->getRows();
+
+ foreach ($this->getRows() as $row) {
+ $hash[$row[0]] = $row[1];
+ }
+
+ return $hash;
+ }
+
+ /**
+ * Convert table into string
+ *
+ * @return string
+ */
+ public function __toString()
+ {
+ $string = '';
+
+ for ($i = 0; $i < count($this->getRows()); $i++) {
+ if ('' !== $string) {
+ $string .= "\n";
+ }
+ $string .= $this->getRowAsString($i);
+ }
+
+ return $string;
+ }
+
+ /**
+ * Return max length of specific column.
+ *
+ * @param integer $columnNum column number
+ *
+ * @return integer
+ */
+ protected function getMaxLengthForColumn($columnNum)
+ {
+ $max = 0;
+
+ foreach ($this->getRows() as $row) {
+ if (($tmp = mb_strlen($row[$columnNum])) > $max) {
+ $max = $tmp;
+ }
+ }
+
+ return $max;
+ }
+
+ /**
+ * Pad string right.
+ *
+ * @param string $text
+ * @param integer $length
+ *
+ * @return string
+ */
+ protected function padRight($text, $length)
+ {
+ while ($length > mb_strlen($text)) {
+ $text = $text . ' ';
+ }
+
+ return $text;
+ }
+}
368 src/Behat/Gherkin/Parser.php
@@ -0,0 +1,368 @@
+<?php
+
+namespace Behat\Gherkin;
+
+use Behat\Gherkin\Exception\Exception,
+ Behat\Gherkin\Node;
+
+class Parser
+{
+ private $file;
+ private $lexer;
+
+ /**
+ * Initialize Parser.
+ *
+ * @param Lexer $lexer lexer instance
+ */
+ public function __construct(Lexer $lexer)
+ {
+ $this->lexer = $lexer;
+ }
+
+ /**
+ * Parse input & return features array.
+ *
+ * @param string $input Gherkin filename or string document
+ *
+ * @return array array of feature nodes
+ */
+ public function parse($input)
+ {
+ $features = array();
+ $language = 'en';
+
+ if (is_file($input)) {
+ $this->file = $input;
+ $input = file_get_contents($this->file);
+ } else {
+ $this->file = null;
+ }
+
+ $this->lexer->setInput($input);
+ $this->lexer->setLanguage($language);
+
+ while ('EOS' !== $this->lexer->predictToken()->type) {
+ if ('Newline' === $this->lexer->predictToken()->type) {
+ $this->lexer->getAdvancedToken();
+ } elseif ('Comment' === $this->lexer->predictToken()->type) {
+ $matches = array();
+ if (preg_match('/^ *language: *([\w_\-]+)/', $this->parseExpression(), $matches)) {
+ $this->lexer->setLanguage($language = $matches[1]);
+ }
+ } elseif ('Feature' === $this->lexer->predictToken()->type) {
+ $feature = $this->parseExpression();
+ $feature->setLanguage($language);
+ $features[] = $feature;
+ } else {
+ $this->expectTokenType('Feature');
+ }
+ }
+
+ return $features;
+ }
+
+ /**
+ * Expect given type or throw Exception.
+ *
+ * @param string $type type
+ */
+ protected function expectTokenType($type)
+ {
+ if ($type === $this->lexer->predictToken()->type) {
+ return $this->lexer->getAdvancedToken();
+ } else {
+ throw new Exception(sprintf('Expected %s, but got %s on line: %d%s',
+ $type, $this->lexer->predictToken()->type, $this->lexer->getCurrentLine(),
+ $this->file ? ' in file: ' . $this->file : ''
+ ));
+ }
+ }
+
+ /**
+ * Parse current expression & return Node.
+ *
+ * @return string|Node\*
+ */
+ protected function parseExpression()
+ {
+ switch ($this->lexer->predictToken()->type) {
+ case 'Feature':
+ return $this->parseFeature();
+ case 'Background':
+ return $this->parseBackground();
+ case 'Scenario':
+ return $this->parseScenario();
+ case 'Outline':
+ return $this->parseOutline();
+ case 'TableRow':
+ return $this->parseTable();
+ case 'PyStringOperator':
+ return $this->parsePyString();
+ case 'Step':
+ return $this->parseStep();
+ case 'Comment':
+ return $this->parseComment();
+ case 'Text':
+ return $this->parseText();
+ case 'Tag':
+ $token = $this->lexer->getAdvancedToken();
+ $this->skipNewlines();
+ $this->lexer->deferToken($this->lexer->getAdvancedToken());
+ $this->lexer->deferToken($token);
+
+ return $this->parseExpression();
+ }
+ }
+
+ /**
+ * Parse Feature & return it's node.
+ *
+ * @return Node\FeatureNode
+ */
+ protected function parseFeature()
+ {
+ $token = $this->expectTokenType('Feature');
+ $node = new Node\FeatureNode(
+ trim($token->value) ?: null, null, $this->file, $this->lexer->getCurrentLine()
+ );
+ $this->skipNewlines();
+
+ // Parse tags
+ if ('Tag' === $this->lexer->predictToken()->type) {
+ $node->setTags($this->lexer->getAdvancedToken()->tags);
+ }
+
+ // Parse feature description
+ while ('Text' === $this->lexer->predictToken()->type) {
+ $text = trim($this->parseExpression());
+ if (null === $node->getDescription()) {
+ $node->setDescription($text);
+ } else {
+ $node->setDescription($node->getDescription() . "\n" . $text);
+ }
+ }
+
+ // Parse background
+ if ('Background' === $this->lexer->predictToken()->type) {
+ $node->setBackground($this->parseExpression());
+ }
+
+ // Parse scenarios & outlines
+ while ('Scenario' === $this->lexer->predictToken()->type
+ || 'Outline' === $this->lexer->predictToken()->type) {
+ $node->addScenario($this->parseExpression());
+ }
+
+ return $node;
+ }
+
+ /**
+ * Parse Background & return it's node.
+ *
+ * @return Node\BackgroundNode
+ */
+ protected function parseBackground()
+ {
+ $token = $this->expectTokenType('Background');
+ $node = new Node\BackgroundNode($this->lexer->getCurrentLine());
+ $this->skipNewlines();
+
+ // Parse steps
+ while ('Step' === $this->lexer->predictToken()->type) {
+ $node->addStep($this->parseExpression());
+ }
+
+ return $node;
+ }
+
+ /**
+ * Parse Scenario Outline & return it's node.
+ *
+ * @return Node\OutlineNode
+ */
+ protected function parseOutline()
+ {
+ $token = $this->expectTokenType('Outline');
+ $node = new Node\OutlineNode(trim($token->value) ?: null, $this->lexer->getCurrentLine());
+ $this->skipNewlines();
+
+ // Parse tags
+ if ('Tag' === $this->lexer->predictToken()->type) {
+ $node->setTags($this->lexer->getAdvancedToken()->tags);
+ }
+
+ // Parse scenario title
+ while ('Text' === $this->lexer->predictToken()->type) {
+ $text = trim($this->parseExpression());
+ if (null === $node->getTitle()) {
+ $node->setTitle($text);
+ } else {
+ $node->setTitle($node->getTitle() . "\n" . $text);
+ }
+ }
+
+ // Parse steps
+ while ('Step' === $this->lexer->predictToken()->type) {
+ $node->addStep($this->parseExpression());
+ }
+
+ // Examples block
+ $this->expectTokenType('Examples');
+ $this->skipNewlines();
+
+ // Parse examples table
+ $node->setExamples($this->parseTable());
+
+ return $node;
+ }
+
+ /**
+ * Parse Scenario & return it's node.
+ *
+ * @return Node\ScenarioNode
+ */
+ protected function parseScenario()
+ {
+ $token = $this->expectTokenType('Scenario');
+ $node = new Node\ScenarioNode(trim($token->value) ?: null, $this->lexer->getCurrentLine());
+ $this->skipNewlines();
+
+ // Parse tags
+ if ('Tag' === $this->lexer->predictToken()->type) {
+ $node->setTags($this->lexer->getAdvancedToken()->tags);
+ }
+
+ // Parse scenario title
+ while ('Text' === $this->lexer->predictToken()->type) {
+ $text = trim($this->parseExpression());
+ if (null === $node->getTitle()) {
+ $node->setTitle($text);
+ } else {
+ $node->setTitle($node->getTitle() . "\n" . $text);
+ }
+ }
+
+ // Parse scenario steps
+ while ('Step' === $this->lexer->predictToken()->type) {
+ $node->addStep($this->parseExpression());
+ }
+
+ return $node;
+ }
+
+ /**
+ * Parse Step & return it's node.
+ *
+ * @return Node\StepNode
+ */
+ protected function parseStep()
+ {
+ $token = $this->expectTokenType('Step');
+ $node = new Node\StepNode(
+ $token->value, trim($token->text) ?: null, $this->lexer->getCurrentLine()
+ );
+ $this->skipNewlines();
+
+ // Parse step text
+ while ('Text' === $this->lexer->predictToken()->type) {
+ $text = trim($this->parseExpression());
+ if (null === $node->getText()) {
+ $node->setText($text);
+ } else {
+ $node->setText($node->getText() . "\n" . $text);
+ }
+ }
+
+ // Parse PyString argument
+ if ('PyStringOperator' === $this->lexer->predictToken()->type) {
+ $node->addArgument($this->parseExpression());
+ }
+
+ // Parse Table argument
+ if ('TableRow' === $this->lexer->predictToken()->type) {
+ $node->addArgument($this->parseExpression());
+ }
+
+ return $node;
+ }
+
+ /**
+ * Parse Table & return it's node.
+ *
+ * @return Node\TableNode
+ */
+ protected function parseTable()
+ {
+ $token = $this->expectTokenType('TableRow');
+ $node = new Node\TableNode();
+ $node->addRow($token->columns);
+ $this->skipNewlines();
+
+ while ('TableRow' === $this->lexer->predictToken()->type) {
+ $token = $this->expectTokenType('TableRow');
+ $node->addRow($token->columns);
+ $this->skipNewlines();
+ }
+
+ return $node;
+ }
+
+ /**
+ * Parse PyString & return it's node.
+ *
+ * @return Node\PyStringNode
+ */
+ protected function parsePyString()
+ {
+ $token = $this->expectTokenType('PyStringOperator');
+ $node = new Node\PyStringNode(null, $token->swallow);
+ $this->skipNewlines();
+
+ while ('PyStringOperator' !== $this->lexer->predictToken()->type
+ && 'Text' === $this->lexer->predictToken()->type) {
+ $node->addLine($this->parseExpression());
+ }
+ $this->expectTokenType('PyStringOperator');
+ $this->skipNewlines();
+
+ return $node;
+ }
+
+ /**
+ * Parse next comment token.
+ *
+ * @return string
+ */
+ protected function parseComment()
+ {
+ $token = $this->expectTokenType('Comment');
+ $this->skipNewlines();
+
+ return $token->value;
+ }
+
+ /**
+ * Parse next text token.
+ *
+ * @return string
+ */
+ protected function parseText()
+ {
+ $token = $this->expectTokenType('Text');
+ $this->skipNewlines();
+
+ return $token->value;
+ }
+
+ /**
+ * Skip newlines in input.
+ */
+ private function skipNewlines()
+ {
+ while ('Newline' === $this->lexer->predictToken()->type
+ || 'Comment' === $this->lexer->predictToken()->type) {
+ $this->lexer->getAdvancedToken();
+ }
+ }
+}
109 tests/Behat/Gherkin/Fixtures/YamlParser.php
@@ -0,0 +1,109 @@
+<?php
+
+namespace Tests\Behat\Gherkin\Fixtures;
+
+use Symfony\Component\Yaml\Yaml;
+
+use Behat\Gherkin\Node;
+
+class YamlParser
+{
+ public function parse($yamlPath, $featurePath)
+ {
+ $yaml = Yaml::load($yamlPath);
+
+ return $this->parseFeature($yaml, $featurePath);
+ }
+
+ protected function parseFeature(array $yaml, $featurePath)
+ {
+ $featureHash = $yaml['feature'];
+
+ $featureNode = new Node\FeatureNode(
+ $featureHash['title'], null, $featurePath, $featureHash['line']
+ );
+ $featureNode->setLanguage($featureHash['language']);
+ $featureNode->setDescription($featureHash['description']);
+
+ if (isset($featureHash['tags'])) {
+ $featureNode->setTags($featureHash['tags']);
+ }
+
+ if (isset($featureHash['background'])) {
+ $this->addBackground($featureNode, $featureHash['background']);
+ }
+
+ if (isset($featureHash['scenarios'])) {
+ $this->addScenarios($featureNode, $featureHash['scenarios']);
+ }
+
+ return $featureNode;
+ }
+
+ protected function addBackground($node, array $backgroundHash)
+ {
+ $backgroundNode = new Node\BackgroundNode($backgroundHash['line']);
+ $this->addSteps($backgroundNode, $backgroundHash['steps']);
+ $node->setBackground($backgroundNode);
+ }
+
+ protected function addScenarios($node, array $scenarios)
+ {
+ foreach ($scenarios as $key => $scenarioHash) {
+ if ('scenario' === $scenarioHash['type']) {
+ $scenarioNode = new Node\ScenarioNode($scenarioHash['title'], $scenarioHash['line']);
+ } else {
+ $scenarioNode = new Node\OutlineNode($scenarioHash['title'], $scenarioHash['line']);
+ if (isset($scenarioHash['examples'])) {
+ $scenarioNode->setExamples($this->parseTable($scenarioHash['examples']));
+ }
+ }
+ if (isset($scenarioHash['tags'])) {
+ $scenarioNode->setTags($scenarioHash['tags']);
+ }
+ if (isset($scenarioHash['steps'])) {
+ $this->addSteps($scenarioNode, $scenarioHash['steps']);
+ }
+ $node->addScenario($scenarioNode);
+ }
+ }
+
+ protected function addSteps($node, array $stepsHash)
+ {
+ $steps = array();
+
+ foreach ($stepsHash as $key => $stepHash) {
+ $stepNode = new Node\StepNode(
+ $stepHash['type'], $stepHash['text'], $stepHash['line']
+ );
+
+ if (isset($stepHash['arguments'])) {
+ foreach ($stepHash['arguments'] as $key => $hash) {
+ if ('pystring' === $hash['type']) {
+ $stepNode->addArgument($this->parsePyString($hash));
+ } elseif ('table' === $hash['type']) {
+ $stepNode->addArgument($this->parseTable($hash['rows']));
+ }
+ }
+ }
+
+ $node->addStep($stepNode);
+ }
+ }
+
+ protected function parsePyString(array $pystrHash)
+ {
+ return new Node\PyStringNode($pystrHash['text'], $pystrHash['swallow']);
+ }
+
+ protected function parseTable(array $tableHash)
+ {
+ $table = new Node\TableNode();
+
+ foreach ($tableHash as $key => $hash) {
+ $table->addRow($hash);
+ }
+
+ return $table;
+ }
+}
29 tests/Behat/Gherkin/Fixtures/etalons/addition.yml
@@ -0,0 +1,29 @@
+feature:
+ title: Addition
+ language: en
+ line: 2
+ description: |-
+ In order to avoid silly mistakes
+ As a math idiot
+ I want to be told the sum of two numbers
+
+ scenarios:
+ -
+ type: scenario
+ title: Add two numbers
+ line: 7
+ steps:
+ - { type: 'Given', text: 'I have entered 11 into the calculator', line: 8 }
+ - { type: 'And', text: 'I have entered 12 into the calculator', line: 9 }
+ - { type: 'When', text: 'I press add', line: 10 }
+ - { type: 'Then', text: 'the result should be 23 on the screen', line: 11 }
+
+ -
+ type: scenario
+ title: Div two numbers
+ line: 13
+ steps:
+ - { type: 'Given', text: 'I have entered 10 into the calculator', line: 14 }
+ - { type: 'And', text: 'I have entered 2 into the calculator', line: 15 }
+ - { type: 'When', text: 'I press div', line: 16 }
+ - { type: 'Then', text: 'the result should be 5 on the screen', line: 17 }
18 tests/Behat/Gherkin/Fixtures/etalons/background.yml
@@ -0,0 +1,18 @@
+feature:
+ title: Feature with background
+ language: en
+ line: 1
+ description: ~
+
+ background:
+ line: 3
+ steps:
+ - { type: Given, text: a passing step, line: 4 }
+
+ scenarios:
+ -
+ type: scenario
+ title: ~
+ line: 6
+ steps:
+ - { type: Given, text: a failing step, line: 7 }
13 tests/Behat/Gherkin/Fixtures/etalons/empty_scenario.yml
@@ -0,0 +1,13 @@
+feature:
+ title: Cucumber command line
+ language: en
+ line: 1
+ description: |-
+ In order to write better software
+ Developers should be able to execute requirements as tests
+
+ scenarios:
+ -
+ type: scenario
+ title: Pending Scenario at the end of a file with whitespace after it
+ line: 6
27 tests/Behat/Gherkin/Fixtures/etalons/fibonacci.yml
@@ -0,0 +1,27 @@
+feature:
+ title: Fibonacci
+ language: en
+ line: 1
+ description: |-
+ In order to calculate super fast fibonacci series
+ As a pythonista
+ I want to use Python for that
+
+ scenarios:
+ -
+ type: outline
+ title: Series
+ line: 6
+ steps:
+ - { type: 'When', text: 'I ask python to calculate fibonacci up to <n>', line: 7 }
+ - { type: 'Then', text: 'it should give me <series>', line: 8 }
+
+ examples:
+ - [ n , series ]
+ - [ 1 , '[]' ]
+ - [ 2 , '[1, 1]' ]
+ - [ 3 , '[1, 1, 2]' ]
+ - [ 4 , '[1, 1, 2, 3]' ]
+ - [ 6 , '[1, 1, 2, 3, 5]' ]
+ - [ 9 , '[1, 1, 2, 3, 5, 8]' ]
+ - [ 100 , '[1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89]' ]
13 tests/Behat/Gherkin/Fixtures/etalons/long_title_feature.yml
@@ -0,0 +1,13 @@
+feature:
+ title: https://rspec.lighthouseapp.com/projects/16211/tickets/246-distorted-console-output-for-slightly-complicated-step-regexp-match
+ language: en
+ line: 1
+ description: ~
+
+ scenarios:
+ -
+ type: scenario
+ title: See "No Record(s) Found" for Zero Existing
+ line: 3
+ steps:
+ - { type: Given, text: no public holiday exists in the system, line: 4 }
44 tests/Behat/Gherkin/Fixtures/etalons/multiline_name.yml
@@ -0,0 +1,44 @@
+feature:
+ title: multiline
+ language: en
+ line: 1
+ description: ~
+
+ background:
+ line: 3
+ steps:
+ - { type: Given, text: passing without a table, line: 4 }
+
+ scenarios:
+ -
+ type: scenario
+ title: |-
+ I'm a multiline name
+ which goes on and on and on for three lines
+ yawn
+ line: 6
+ steps:
+ - { type: Given, text: passing without a table, line: 9 }
+
+ -
+ type: outline
+ title: |-
+ I'm a multiline name
+ which goes on and on and on for three lines
+ yawn
+ line: 11
+ steps:
+ - { type: 'Given', text: '<state> without a table', line: 14 }
+ examples:
+ - [state]
+ - [passing]
+
+ -
+ type: outline
+ title: name
+ line: 19
+ steps:
+ - { type: 'Given', text: '<state> without a table', line: 20 }
+ examples:
+ - [state]
+ - [passing]
33 tests/Behat/Gherkin/Fixtures/etalons/outline_with_spaces.yml
@@ -0,0 +1,33 @@
+feature:
+ title: Login
+ language: en
+ line: 1
+ description: |-
+ To ensure the safety of the application
+ A regular user of the system
+ Must authenticate before using the app
+
+ scenarios:
+ -
+ type: outline
+ title: Failed Login
+ line: 7
+ steps:
+ - { type: 'Given', text: 'the user "known_user"', line: 8 }
+ - { type: 'When', text: 'I go to the main page', line: 10 }
+ - { type: 'Then', text: 'I should see the login form', line: 11 }
+ - { type: 'When', text: 'I fill in "login" with "<login>"', line: 13 }
+ - { type: 'And', text: 'I fill in "password" with "<password>"', line: 14 }
+ - { type: 'And', text: 'I press "Log In"', line: 15 }
+ - { type: 'Then', text: 'the login request should fail', line: 16 }
+ - { type: 'And', text: 'I should see the error message "Login or Password incorrect"', line: 17 }
+ examples:
+ - [login, password]
+ - ['', '']
+ - [unknown_user, '']
+ - [known_user, '']
+ - ['', wrong_password]
+ - ['', known_userpass]
+ - [unknown_user, wrong_password]
+ - [unknown_user, known_userpass]
+ - [known_user, wrong_password]
28 tests/Behat/Gherkin/Fixtures/etalons/outline_with_step_table.yml
@@ -0,0 +1,28 @@
+feature:
+ title: Unsubstituted argument placeholder
+ language: en
+ line: 1
+ description: ~
+
+ scenarios:
+ -
+ type: outline
+ title: 'See Annual Leave Details (as Management & Human Resource)'
+ line: 3
+ steps:
+ -
+ type: Given
+ text: the following users exist in the system
+ line: 4
+ arguments:
+ -
+ type: table
+ rows:
+ - [ name, email, role_assignments, group_memberships ]
+ - [ Jane, jane@fmail.com, <role>, Sales (manager) ]
+ - [ Max, max@fmail.com, '', Sales (member) ]
+ - [ Carol, carol@fmail.com, '', Sales (member) ]
+ - [ Cat, cat@fmail.com, '', '' ]
+ examples:
+ - [ role ]
+ - [ HUMAN RESOURCE ]
21 tests/Behat/Gherkin/Fixtures/etalons/pystring.yml
@@ -0,0 +1,21 @@
+feature:
+ title: A py string feature
+ language: en
+ line: 1
+ description: ~
+
+ scenarios:
+ -
+ type: scenario
+ title: ~
+ line: 3
+ steps:
+ -
+ type: Then
+ text: I should see
+ line: 4
+ arguments:
+ -
+ type: pystring
+ swallow: 6
+ text: a string
18 tests/Behat/Gherkin/Fixtures/etalons/start_comments.yml
@@ -0,0 +1,18 @@
+feature:
+ title: Using the Console Formatter
+ language: en
+ line: 3
+ description: |-
+ In order to verify this error
+ I want to run this feature using the progress format
+ So that it can be fixed
+
+ scenarios:
+ -
+ type: scenario
+ title: A normal feature</