diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 93d5d1d1..2ee28e8d 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -17,15 +17,21 @@ jobs:
php: [7.2, 7.3, 7.4, 8.0, 8.1]
composer-flags: [ "" ]
symfony-version: [ "" ]
+ cucumber: [false]
include:
- php: 7.2
- symfony-version: '3.*'
+ composer-flags: '--prefer-lowest'
- php: 7.3
symfony-version: '4.*'
- php: 7.4
symfony-version: '5.*'
- php: 8.0
- symfony-version: '5.*'
+ symfony-version: '6.*'
+ - php: 8.1
+ composer-flags: '--prefer-lowest'
+ cucumber: true
+ - php: 8.1
+ cucumber: true
steps:
- uses: actions/checkout@v2
@@ -40,8 +46,16 @@ jobs:
if: matrix.symfony-version != ''
run: composer require --no-update "symfony/symfony:${{ matrix.symfony-version }}"
+ - name: Require cucumber
+ if: matrix.cucumber == true
+ run: composer require --no-update "cucumber/gherkin"
+
- name: Install dependencies
run: composer update ${{ matrix.composer-flags }}
- name: Run tests (phpunit)
run: ./vendor/bin/phpunit
+
+ - name: Run cucumber tests (phpunit)
+ if: matrix.cucumber == true
+ run: ./vendor/bin/phpunit --group=cucumber
diff --git a/composer.json b/composer.json
index 7a48c3ee..1c4d2870 100644
--- a/composer.json
+++ b/composer.json
@@ -18,11 +18,15 @@
},
"require-dev": {
- "symfony/yaml": "~3|~4|~5",
- "phpunit/phpunit": "~8|~9",
+ "symfony/yaml": "^4.0||^5.0||^6.0",
+ "phpunit/phpunit": "^8.5.19||^9.0",
"cucumber/cucumber": "dev-gherkin-24.0.0"
},
+ "conflict": {
+ "cucumber/messages": "<19.0.0"
+ },
+
"suggest": {
"symfony/yaml": "If you want to parse features, represented in YAML files"
},
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index fbe1da6d..84beaf54 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -11,6 +11,12 @@
stopOnFailure="false"
bootstrap="vendor/autoload.php"
>
+
+
+ cucumber
+
+
+
./tests/Behat/Gherkin/
diff --git a/src/Behat/Gherkin/Cucumber/BackgroundNodeMapper.php b/src/Behat/Gherkin/Cucumber/BackgroundNodeMapper.php
new file mode 100644
index 00000000..8fd096ce
--- /dev/null
+++ b/src/Behat/Gherkin/Cucumber/BackgroundNodeMapper.php
@@ -0,0 +1,50 @@
+stepNodeMapper = $stepNodeMapper;
+ }
+
+ /**
+ * @param FeatureChild[] $children
+ *
+ * @return BackgroundNode|null
+ */
+ public function map(array $children) : ?BackgroundNode
+ {
+ foreach($children as $child) {
+ if ($child->background) {
+
+ $title = $child->background->name;
+ if ($child->background->description) {
+ $title .= "\n" . $child->background->description;
+ }
+
+ return new BackgroundNode(
+ MultilineStringFormatter::format(
+ $title,
+ $child->background->location
+ ),
+ $this->stepNodeMapper->map($child->background->steps),
+ $child->background->keyword,
+ $child->background->location->line
+ );
+ }
+ }
+
+ return null;
+ }
+}
diff --git a/src/Behat/Gherkin/Cucumber/ExampleTableNodeMapper.php b/src/Behat/Gherkin/Cucumber/ExampleTableNodeMapper.php
new file mode 100644
index 00000000..7e0ce1bf
--- /dev/null
+++ b/src/Behat/Gherkin/Cucumber/ExampleTableNodeMapper.php
@@ -0,0 +1,65 @@
+tagMapper = $tagMapper;
+ }
+
+ /**
+ * @param Examples[] $exampleTables
+ *
+ * @return ExampleTableNode[]
+ */
+ public function map(array $exampleTables) : array
+ {
+ $exampleTableNodes = [];
+
+ foreach ($exampleTables as $exampleTable) {
+ $exampleTableNodes[] = new ExampleTableNode(
+ $this->getTableArray($exampleTable),
+ $exampleTable->keyword,
+ $this->tagMapper->map($exampleTable->tags)
+ );
+ }
+
+ return $exampleTableNodes;
+ }
+
+ private function getTableArray(Examples $exampleTable) : array
+ {
+ $array = [];
+
+ if ($exampleTable->tableHeader) {
+ $array[$exampleTable->tableHeader->location->line] = array_map(
+ function (TableCell $cell) {
+ return $cell->value;
+ },
+ $exampleTable->tableHeader->cells
+ );
+ }
+
+ foreach ($exampleTable->tableBody as $row) {
+ $array[$row->location->line] = array_map(
+ function (TableCell $cell) {
+ return $cell->value;
+ },
+ $row->cells
+ );
+ }
+
+ return $array;
+ }
+}
diff --git a/src/Behat/Gherkin/Cucumber/FeatureNodeMapper.php b/src/Behat/Gherkin/Cucumber/FeatureNodeMapper.php
new file mode 100644
index 00000000..d58aa16b
--- /dev/null
+++ b/src/Behat/Gherkin/Cucumber/FeatureNodeMapper.php
@@ -0,0 +1,59 @@
+tagMapper = $tagMapper;
+ $this->backgroundMapper = $backgroundMapper;
+ $this->scenarioMapper = $scenarioMapper;
+ }
+
+ function map(GherkinDocument $gherkinDocument) : ?FeatureNode
+ {
+ if (!$gherkinDocument->feature) {
+ return null;
+ }
+
+ return new FeatureNode(
+ $gherkinDocument->feature->name,
+ MultilineStringFormatter::format(
+ $gherkinDocument->feature->description,
+ $gherkinDocument->feature->location
+ ) ?: null, // background has empty = null
+ $this->tagMapper->map($gherkinDocument->feature->tags),
+ $this->backgroundMapper->map($gherkinDocument->feature->children),
+ $this->scenarioMapper->map($gherkinDocument->feature->children),
+ $gherkinDocument->feature->keyword,
+ $gherkinDocument->feature->language,
+ $gherkinDocument->uri,
+ $gherkinDocument->feature->location->line
+ );
+ }
+
+}
diff --git a/src/Behat/Gherkin/Cucumber/KeywordTypeMapper.php b/src/Behat/Gherkin/Cucumber/KeywordTypeMapper.php
new file mode 100644
index 00000000..f8035b6a
--- /dev/null
+++ b/src/Behat/Gherkin/Cucumber/KeywordTypeMapper.php
@@ -0,0 +1,30 @@
+column-1 ?: 0) + 2;
+
+ return preg_replace(
+ ["/^[^\n\S]{0,$maxIndent}/um", '/[^\n\S]+$/um'],
+ ['', ''],
+ $string
+ );
+ }
+}
diff --git a/src/Behat/Gherkin/Cucumber/PyStringNodeMapper.php b/src/Behat/Gherkin/Cucumber/PyStringNodeMapper.php
new file mode 100644
index 00000000..0af2989e
--- /dev/null
+++ b/src/Behat/Gherkin/Cucumber/PyStringNodeMapper.php
@@ -0,0 +1,33 @@
+split($docString->content),
+ $docString->location->line
+ )
+ ];
+ }
+
+ private function split(string $content) {
+ $content = strtr($content, array("\r\n" => "\n", "\r" => "\n"));
+
+ return explode("\n", $content);
+ }
+}
diff --git a/src/Behat/Gherkin/Cucumber/ScenarioNodeMapper.php b/src/Behat/Gherkin/Cucumber/ScenarioNodeMapper.php
new file mode 100644
index 00000000..3dba3ab8
--- /dev/null
+++ b/src/Behat/Gherkin/Cucumber/ScenarioNodeMapper.php
@@ -0,0 +1,111 @@
+tagMapper = $tagMapper;
+ $this->stepNodeMapper = $stepNodeMapper;
+ $this->exampleTableNodeMapper = $exampleTableNodeMapper;
+ }
+
+ /**
+ * @param FeatureChild[] $children
+ *
+ * @return ScenarioInterface[]
+ */
+ public function map(array $children): array
+ {
+ $scenarios = [];
+
+ foreach ($children as $child) {
+ if ($child->scenario) {
+
+ $childScenario = $child->scenario;
+
+ $scenario = $this->buildScenarioNode($childScenario);
+
+ if ($child->scenario->examples) {
+ $scenario = new OutlineNode(
+ $scenario->getTitle(),
+ $scenario->getTags(),
+ $scenario->getSteps(),
+ $this->exampleTableNodeMapper->map($child->scenario->examples),
+ $scenario->getKeyword(),
+ $scenario->getLine()
+ );
+ }
+
+ $scenarios[] = $scenario;
+ }
+
+ if ($child->rule) {
+
+ $ruleTags = $this->tagMapper->map($child->rule->tags);
+
+ foreach ($child->rule->children as $ruleChild) {
+
+ // there's no sensible way to merge this up into the feature
+ if ($ruleChild->background) {
+ throw new ParserException('Backgrounds in Rules are not currently supported');
+ }
+
+ if ($ruleChild->scenario) {
+ $scenarios[] = $this->buildScenarioNode($ruleChild->scenario, $ruleTags);
+ }
+ }
+ }
+
+ }
+
+ return $scenarios;
+ }
+
+ private function buildScenarioNode(?Scenario $scenario, array $extraTags = []): ScenarioNode
+ {
+ $title = $scenario->name;
+ if ($scenario->description) {
+ $title .= "\n" . $scenario->description;
+ }
+
+ return new ScenarioNode (
+ MultilineStringFormatter::format(
+ $title,
+ $scenario->location
+ ),
+
+ array_values(array_unique(array_merge($extraTags, $this->tagMapper->map($scenario->tags)))),
+ $this->stepNodeMapper->map($scenario->steps),
+ $scenario->keyword,
+ $scenario->location->line
+ );
+ }
+}
diff --git a/src/Behat/Gherkin/Cucumber/StepNodeMapper.php b/src/Behat/Gherkin/Cucumber/StepNodeMapper.php
new file mode 100644
index 00000000..35176e22
--- /dev/null
+++ b/src/Behat/Gherkin/Cucumber/StepNodeMapper.php
@@ -0,0 +1,62 @@
+keywordTypeMapper = $keywordTypeMapper;
+ $this->pyStringNodeMapper = $pyStringNodeMapper;
+ $this->tableNodeMapper = $tableNodeMapper;
+ }
+
+ /**
+ * @param Step[] $steps
+ * @return StepNode[]
+ */
+ public function map(array $steps)
+ {
+ $stepNodes = [];
+ $prevType = null;
+
+ foreach ($steps as $step) {
+ $stepNodes[] = new StepNode(
+ // behat does not include space at end of keyword
+ rtrim($step->keyword),
+ $step->text,
+ array_merge(
+ $this->pyStringNodeMapper->map($step->docString),
+ $this->tableNodeMapper->map($step->dataTable),
+ ),
+ $step->location->line,
+ $prevType = $this->keywordTypeMapper->map($step->keywordType, $prevType)
+ );
+ }
+
+ return $stepNodes;
+ }
+
+}
diff --git a/src/Behat/Gherkin/Cucumber/TableNodeMapper.php b/src/Behat/Gherkin/Cucumber/TableNodeMapper.php
new file mode 100644
index 00000000..6e897301
--- /dev/null
+++ b/src/Behat/Gherkin/Cucumber/TableNodeMapper.php
@@ -0,0 +1,36 @@
+rows as $row) {
+ $rows[$row->location->line] = array_map(
+ function(TableCell $cell) {
+ return $cell->value;
+ },
+ $row->cells
+ );
+ }
+
+ return [
+ new TableNode($rows)
+ ];
+ }
+}
diff --git a/src/Behat/Gherkin/Cucumber/TagMapper.php b/src/Behat/Gherkin/Cucumber/TagMapper.php
new file mode 100644
index 00000000..e9827b56
--- /dev/null
+++ b/src/Behat/Gherkin/Cucumber/TagMapper.php
@@ -0,0 +1,21 @@
+name, '@');
+ },
+ $tags
+ );
+ }
+}
diff --git a/src/Behat/Gherkin/Lexer.php b/src/Behat/Gherkin/Lexer.php
index 1f3b3c40..0806ae08 100644
--- a/src/Behat/Gherkin/Lexer.php
+++ b/src/Behat/Gherkin/Lexer.php
@@ -627,6 +627,7 @@ protected function scanText()
*/
private function getStepKeywordType($native)
{
+
// Consider "*" as a AND keyword so that it is normalized to the previous step type
if ('*' === $native) {
return 'And';
@@ -644,6 +645,7 @@ private function getStepKeywordType($native)
foreach ($this->stepKeywordTypesCache as $type => $keywords) {
if (in_array($native, $keywords) || in_array($native . '<', $keywords)) {
+
return $type;
}
}
diff --git a/src/Behat/Gherkin/Loader/CucumberGherkinLoader.php b/src/Behat/Gherkin/Loader/CucumberGherkinLoader.php
new file mode 100644
index 00000000..9e5d4879
--- /dev/null
+++ b/src/Behat/Gherkin/Loader/CucumberGherkinLoader.php
@@ -0,0 +1,124 @@
+mapper = new FeatureNodeMapper(
+ $tagMapper,
+ new BackgroundNodeMapper(
+ $stepNodeMapper
+ ),
+ new ScenarioNodeMapper(
+ $tagMapper,
+ $stepNodeMapper,
+ new ExampleTableNodeMapper(
+ $tagMapper
+ )
+ )
+ );
+
+ $this->parser = new GherkinParser(false, false, true, false);
+ }
+
+ /**
+ * Checks if current loader supports provided resource.
+ *
+ * @param mixed $path Resource to load
+ *
+ * @return bool
+ */
+ public function supports($path)
+ {
+ return is_string($path)
+ && is_file($absolute = $this->findAbsolutePath($path))
+ && 'feature' === pathinfo($absolute, PATHINFO_EXTENSION);
+ }
+
+ /**
+ * Whether this Loader is available for use
+ */
+ public static function isAvailable() : bool
+ {
+ return class_exists(GherkinParser::class);
+ }
+
+ /**
+ * Sets cache layer.
+ *
+ * @param CacheInterface $cache Cache layer
+ */
+ public function setCache(CacheInterface $cache)
+ {
+ $this->cache = $cache;
+ }
+
+ /**
+ * Loads features from provided resource.
+ *
+ * @param string $path Resource to load
+ *
+ * @return FeatureNode[]
+ */
+ public function load($resource)
+ {
+ $path = $this->findAbsolutePath($resource);
+
+ if ($this->cache && $this->cache->isFresh($path, filemtime($path))) {
+ return [$this->cache->read($path)];
+ }
+
+ $envelopes = $this->parser->parseString($path, file_get_contents($path));
+ foreach ($envelopes as $envelope) {
+ if ($envelope->gherkinDocument) {
+ if ($feature = $this->mapper->map($envelope->gherkinDocument)) {
+ break;
+ }
+ }
+ }
+
+ if ($this->cache) {
+ $this->cache->write($path, $feature);
+ }
+
+ return [$feature];
+ }
+
+}
diff --git a/tests/Behat/Gherkin/Acceptance/CompatibilityTestTrait.php b/tests/Behat/Gherkin/Acceptance/CompatibilityTestTrait.php
new file mode 100644
index 00000000..7c05f048
--- /dev/null
+++ b/tests/Behat/Gherkin/Acceptance/CompatibilityTestTrait.php
@@ -0,0 +1,57 @@
+getYamlParser()->load($yamlFile)[0];
+ $feature = $this->parseFeature($featureFile);
+
+ $this->compare($fixture, $feature);
+ }
+
+ public function etalonsProvider()
+ {
+ foreach (glob(__DIR__ . '/../Fixtures/etalons/*.yml') as $file) {
+ $testname = basename($file, '.yml');
+
+ if (!in_array($testname, $this->etalons_skip)) {
+ yield $testname => [$file, __DIR__ . '/../Fixtures/features/'.$testname.'.feature'];
+ }
+
+ }
+ }
+
+ protected function getYamlParser()
+ {
+ if (null === $this->yaml) {
+ $this->yaml = new YamlFileLoader();
+ }
+
+ return $this->yaml;
+ }
+
+ private function compare(FeatureNode $fixture, ?FeatureNode $feature): void
+ {
+ $rc = new \ReflectionClass(FeatureNode::class);
+ $rp = $rc->getProperty('file');
+ $rp->setAccessible(true);
+ $rp->setValue($fixture, null);
+ $rp->setValue($feature, null);
+
+ $this->assertEquals($fixture, $feature);
+ }
+
+}
diff --git a/tests/Behat/Gherkin/Acceptance/CucumberParserTest.php b/tests/Behat/Gherkin/Acceptance/CucumberParserTest.php
new file mode 100644
index 00000000..132f7158
--- /dev/null
+++ b/tests/Behat/Gherkin/Acceptance/CucumberParserTest.php
@@ -0,0 +1,28 @@
+loader = new CucumberGherkinLoader();
+ }
+
+ use CompatibilityTestTrait;
+
+ protected function parseFeature($featureFile): FeatureNode
+ {
+ return $this->loader->load($featureFile)[0];
+ }
+}
diff --git a/tests/Behat/Gherkin/Acceptance/LegacyParserTest.php b/tests/Behat/Gherkin/Acceptance/LegacyParserTest.php
new file mode 100644
index 00000000..94bd278d
--- /dev/null
+++ b/tests/Behat/Gherkin/Acceptance/LegacyParserTest.php
@@ -0,0 +1,71 @@
+getGherkinParser()->parse(file_get_contents($featureFile), $featureFile);
+ }
+
+ protected function getGherkinParser()
+ {
+ if (null === $this->gherkin) {
+ $keywords = new ArrayKeywords(array(
+ 'en' => array(
+ 'feature' => 'Feature',
+ 'background' => 'Background',
+ 'scenario' => 'Scenario',
+ 'scenario_outline' => 'Scenario Outline',
+ 'examples' => 'Examples',
+ 'given' => 'Given',
+ 'when' => 'When',
+ 'then' => 'Then',
+ 'and' => 'And',
+ 'but' => 'But'
+ ),
+ 'ru' => array(
+ 'feature' => 'Функционал',
+ 'background' => 'Предыстория',
+ 'scenario' => 'Сценарий',
+ 'scenario_outline' => 'Структура сценария',
+ 'examples' => 'Примеры',
+ 'given' => 'Допустим',
+ 'when' => 'Если',
+ 'then' => 'То',
+ 'and' => 'И',
+ 'but' => 'Но'
+ ),
+ 'ja' => array (
+ 'feature' => 'フィーチャ',
+ 'background' => '背景',
+ 'scenario' => 'シナリオ',
+ 'scenario_outline' => 'シナリオアウトライン',
+ 'examples' => '例|サンプル',
+ 'given' => '前提<',
+ 'when' => 'もし<',
+ 'then' => 'ならば<',
+ 'and' => 'かつ<',
+ 'but' => 'しかし<'
+ )
+ ));
+ $this->gherkin = new Parser(new Lexer($keywords));
+ }
+
+ return $this->gherkin;
+ }
+}
diff --git a/tests/Behat/Gherkin/Cucumber/BackgroundNodeMapperTest.php b/tests/Behat/Gherkin/Cucumber/BackgroundNodeMapperTest.php
new file mode 100644
index 00000000..6a26f003
--- /dev/null
+++ b/tests/Behat/Gherkin/Cucumber/BackgroundNodeMapperTest.php
@@ -0,0 +1,102 @@
+mapper = new BackgroundNodeMapper(
+ new StepNodeMapper(
+ new KeywordTypeMapper(),
+ new PyStringNodeMapper(),
+ new TableNodeMapper()
+ )
+ );
+ }
+
+ public function testItReturnsNullIfNoChildrenAreBackgrounds()
+ {
+ $result = $this->mapper->map([]);
+
+ self::assertNull($result);
+ }
+
+ public function testItReturnsABackgroundNodeIfOneIsPresent()
+ {
+ $result = $this->mapper->map([
+ new FeatureChild(null, new Background())
+ ]);
+
+ self::assertInstanceOf(BackgroundNode::class, $result);
+ }
+
+ public function testItPopulatesTitle()
+ {
+ $result = $this->mapper->map([new FeatureChild(null,
+ new Background(new Location(),'','Background title','')
+ )]);
+
+ self::assertSame('Background title', $result->getTitle());
+ }
+
+ public function testItPopulatesTitleFromDescriptionWhenMultiline()
+ {
+ $result = $this->mapper->map([new FeatureChild(null,
+ new Background(new Location(), '', 'title', "across\nmany\nlines")
+ )]);
+
+ self::assertSame("title\nacross\nmany\nlines", $result->getTitle());
+ }
+
+ public function testItPopulatesKeyword()
+ {
+ $result = $this->mapper->map([new FeatureChild(null,
+ new Background(new Location(),'Background','','')
+ )]);
+
+ self::assertSame('Background', $result->getKeyword());
+ }
+
+ public function testItPopulatesLine()
+ {
+ $result = $this->mapper->map([new FeatureChild(null,
+ new Background(new Location(100, 0))
+ )]);
+
+ self::assertSame(100, $result->getLine());
+ }
+
+ public function testItPopulatesSteps()
+ {
+ $result = $this->mapper->map([new FeatureChild(null,
+ new Background(new Location(), '', '', '', [
+ new Step()
+ ])
+ )]);
+
+ self::assertCount(1, $result->getSteps());
+ self::assertInstanceOf(StepNode::class, $result->getSteps()[0]);
+ }
+}
diff --git a/tests/Behat/Gherkin/Cucumber/CompatibilityTest.php b/tests/Behat/Gherkin/Cucumber/CompatibilityTest.php
index acb43e4f..f3ff39b5 100644
--- a/tests/Behat/Gherkin/Cucumber/CompatibilityTest.php
+++ b/tests/Behat/Gherkin/Cucumber/CompatibilityTest.php
@@ -1,6 +1,6 @@
mapper = new ExampleTableNodeMapper(new TagMapper());
+ }
+
+ public function testItMapsEmptyArrayToEmpty()
+ {
+ $result = $this->mapper->map([]);
+
+ self::assertSame([], $result);
+ }
+
+ public function testItMapsKeyword()
+ {
+ $tables = $this->mapper->map([
+ new Examples(new Location(), [], 'Examples')
+ ]);
+
+ self::assertCount(1, $tables);
+ self::assertSame('Examples', $tables[0]->getKeyword());
+ }
+
+ public function testItMapsHeaderAndRowsIntoOneTable()
+ {
+ {
+ $tables = $this->mapper->map([
+ new Examples(new Location(), [], '','','',
+ new TableRow(new Location(100, 0), [
+ new TableCell(new Location(), 'header-1'),
+ new TableCell(new Location(), 'header-2'),
+ ]),
+ [
+ new TableRow(new Location(101, 0), [
+ new TableCell(new Location(), 'value-3'),
+ new TableCell(new Location(), 'value-4'),
+ ]),
+ new TableRow(new Location(102, 0), [
+ new TableCell(new Location(), 'value-5'),
+ new TableCell(new Location(), 'value-6'),
+ ])
+ ]
+ )
+ ]);
+
+ $expectedHash = [
+ [
+ 'header-1'=>'value-3',
+ 'header-2'=>'value-4'
+ ],
+ [
+ 'header-1'=>'value-5',
+ 'header-2'=>'value-6'
+ ]
+ ];
+
+ self::assertCount(1, $tables);
+ self::assertSame($expectedHash, $tables[0]->getHash());
+ }
+ }
+
+ public function testItMapsLineFromHeaderRow()
+ {
+ $tables = $this->mapper->map([
+ new Examples(new Location(), [], '','','',
+ new TableRow(new Location(100, 0), [])
+ )
+ ]);
+
+ self::assertCount(1, $tables);
+ self::assertSame(100, $tables[0]->getLine());
+ }
+
+ public function testItMapsTags()
+ {
+
+ $tables = $this->mapper->map([
+ new Examples(new Location(), [
+ new Tag(new Location(), '@foo'),
+ new Tag(new Location(), '@bar')
+ ])
+ ]);
+
+ self::assertCount(1, $tables);
+ self::assertSame(['foo', 'bar'], $tables[0]->getTags());
+ }
+}
diff --git a/tests/Behat/Gherkin/Cucumber/FeatureNodeMapperTest.php b/tests/Behat/Gherkin/Cucumber/FeatureNodeMapperTest.php
new file mode 100644
index 00000000..0c125603
--- /dev/null
+++ b/tests/Behat/Gherkin/Cucumber/FeatureNodeMapperTest.php
@@ -0,0 +1,165 @@
+mapper = new FeatureNodeMapper(
+ $tagMapper,
+ new BackgroundNodeMapper(
+ $stepNodeMapper
+ ),
+ new ScenarioNodeMapper(
+ $tagMapper,
+ $stepNodeMapper,
+ new ExampleTableNodeMapper(
+ $tagMapper
+ )
+ )
+ );
+ }
+
+ public function testItReturnsNullIfThereIsNoFeature()
+ {
+ $result = $this->mapper->map(new GherkinDocument());
+
+ self::assertSame(null, $result);
+ }
+
+ public function testItReturnsAFeatureIfThereIsOne()
+ {
+ $feature = $this->mapper->map(new GherkinDocument(
+ '', new Feature()
+ ));
+
+ self::assertInstanceOf(FeatureNode::class, $feature);
+ }
+
+ public function testItMapsTheTitle()
+ {
+ $feature = $this->mapper->map(new GherkinDocument(
+ '', new Feature(new Location(), [], '', '', 'This is the feature title')
+ ));
+
+ self::assertSame('This is the feature title', $feature->getTitle());
+ }
+
+ public function testItMapsTheDescription()
+ {
+ $feature = $this->mapper->map(new GherkinDocument(
+ '', new Feature(new Location(), [], '', '', '', 'This is the feature description')
+ ));
+
+ self::assertSame('This is the feature description', $feature->getDescription());
+ }
+
+ public function testItTrimsTheDescription()
+ {
+ $feature = $this->mapper->map(new GherkinDocument(
+ '', new Feature(new Location(0,1), [], '', '', '', ' This is the feature description')
+ ));
+
+ self::assertSame('This is the feature description', $feature->getDescription());
+ }
+
+ public function testItMapsTheKeyword()
+ {
+ $feature = $this->mapper->map(new GherkinDocument(
+ '', new Feature(new Location(), [], '', 'Given')
+ ));
+
+ self::assertSame('Given', $feature->getKeyword());
+ }
+
+ public function testItMapsTheLanguage()
+ {
+ $feature = $this->mapper->map(new GherkinDocument(
+ '', new Feature(new Location(), [], 'zh')
+ ));
+
+ self::assertSame('zh', $feature->getLanguage());
+ }
+
+ public function testItMapsTheFile()
+ {
+ $feature = $this->mapper->map(new GherkinDocument(
+ '/foo/bar.feature', new Feature()
+ ));
+
+ self::assertSame('/foo/bar.feature', $feature->getFile());
+ }
+
+ public function testItMapsTheLine()
+ {
+ $feature = $this->mapper->map(new GherkinDocument(
+ '', new Feature(new Location(100,0))
+ ));
+
+ self::assertSame(100, $feature->getLine());
+ }
+
+ public function testItMapsTheTags()
+ {
+ $feature = $this->mapper->map(new GherkinDocument(
+ '', new Feature(new Location(),[
+ new Tag(new Location(), '@foo'),
+ new Tag(new Location(), '@bar')
+ ])
+ ));
+
+ self::assertSame(['foo', 'bar'], $feature->getTags());
+ }
+
+ public function testItMapsTheBackground()
+ {
+ $feature = $this->mapper->map(new GherkinDocument(
+ '', new Feature(new Location(), [], '', '', '', '',
+ [new FeatureChild(null, new Background())]
+ )
+ ));
+
+ self::assertInstanceOf(BackgroundNode::class, $feature->getBackground());
+ }
+
+ public function testItMapsScenarios()
+ {
+ $feature = $this->mapper->map(new GherkinDocument(
+ '', new Feature(new Location(), [], '', '', '', '',
+ [new FeatureChild(null, null, new Scenario())]
+ )
+ ));
+
+ self::assertCount(1, $feature->getScenarios());
+ }
+}
diff --git a/tests/Behat/Gherkin/Cucumber/KeywordTypeMapperTest.php b/tests/Behat/Gherkin/Cucumber/KeywordTypeMapperTest.php
new file mode 100644
index 00000000..3c669f86
--- /dev/null
+++ b/tests/Behat/Gherkin/Cucumber/KeywordTypeMapperTest.php
@@ -0,0 +1,53 @@
+mapper = new KeywordTypeMapper();
+ }
+
+ /**
+ * @dataProvider regularKeywords
+ */
+ public function testItMapsRegularKeywordsCorrectly($expected, KeywordType $input)
+ {
+ self::assertSame($expected, $this->mapper->map($input, null));
+ }
+
+ public function regularKeywords()
+ {
+ yield ['Given', KeywordType::CONTEXT];
+ yield ['When', KeywordType::ACTION];
+ yield ['Then', KeywordType::OUTCOME];
+ }
+
+ public function testItMapsToGivenIfNullIsPassed()
+ {
+ self::assertSame('Given', $this->mapper->map(null, null));
+ }
+
+ public function testItMapsToGivenIfUnknownIsPassed()
+ {
+ self::assertSame('Given', $this->mapper->map(KeywordType::UNKNOWN, null));
+ }
+
+ public function testItMapsToGivenIfConjunctionIsFirstStep()
+ {
+ self::assertSame('Given', $this->mapper->map(KeywordType::CONJUNCTION, null));
+ }
+
+ public function testItMapsConjunctionToPreviousType()
+ {
+ self::assertSame('When', $this->mapper->map(KeywordType::CONJUNCTION, 'When'));
+ }
+}
diff --git a/tests/Behat/Gherkin/Cucumber/MultilineStringFormatterTest.php b/tests/Behat/Gherkin/Cucumber/MultilineStringFormatterTest.php
new file mode 100644
index 00000000..60eeed35
--- /dev/null
+++ b/tests/Behat/Gherkin/Cucumber/MultilineStringFormatterTest.php
@@ -0,0 +1,46 @@
+mapper = new PyStringNodeMapper();
+ }
+
+ public function testItMapsNullToEmpty()
+ {
+ $result = $this->mapper->map(null);
+
+ self::assertSame([], $result);
+ }
+
+ public function testItMapsLine()
+ {
+ $stringNodes = $this->mapper->map(new DocString(new Location(100, 0)));
+
+ self::assertCount(1, $stringNodes);
+ self::assertSame(100, $stringNodes[0]->getLine());
+ }
+
+ public function testItMapsStringSplitByNewline()
+ {
+ $stringNodes = $this->mapper->map(new DocString(new Location(), '', "foo\nbar\r\nbaz"));
+
+ self::assertCount(1, $stringNodes);
+ self::assertSame(['foo','bar','baz'], $stringNodes[0]->getStrings());
+ }
+}
diff --git a/tests/Behat/Gherkin/Cucumber/ScenarioNodeMapperTest.php b/tests/Behat/Gherkin/Cucumber/ScenarioNodeMapperTest.php
new file mode 100644
index 00000000..a2addbfd
--- /dev/null
+++ b/tests/Behat/Gherkin/Cucumber/ScenarioNodeMapperTest.php
@@ -0,0 +1,204 @@
+mapper = new ScenarioNodeMapper(
+ $tagMapper,
+ new StepNodeMapper(
+ new KeywordTypeMapper(),
+ new PyStringNodeMapper(),
+ new TableNodeMapper()
+ ),
+ new ExampleTableNodeMapper(
+ $tagMapper
+ )
+ );
+ }
+
+ public function testItMapsEmptyArrayToEmpty()
+ {
+ $result = $this->mapper->map([]);
+
+ self::assertSame([], $result);
+ }
+
+ public function testItMapsAScenario()
+ {
+ $scenarios = $this->mapper->map([new FeatureChild(null, null,
+ new Scenario(new Location())
+ )]);
+
+ self::assertCount(1, $scenarios);
+ self::assertInstanceOf(ScenarioNode::class, $scenarios[0]);
+ }
+
+ public function testItMapsScenarioTitle()
+ {
+ $scenarios = $this->mapper->map([new FeatureChild(null, null,
+ new Scenario(new Location(), [], '', 'Scenario title')
+ )]);
+
+ self::assertCount(1, $scenarios);
+ self::assertSame('Scenario title', $scenarios[0]->getTitle());
+ }
+
+ public function testItMapsDescriptionAsMultiLineScenarioTitle()
+ {
+ $scenarios = $this->mapper->map([new FeatureChild(null, null,
+ new Scenario(new Location(), [], '', 'title', "across\nmany\nlines")
+ )]);
+
+ self::assertCount(1, $scenarios);
+ self::assertSame("title\nacross\nmany\nlines", $scenarios[0]->getTitle());
+ }
+
+ public function testItTrimsScenarioTitle()
+ {
+ $scenarios = $this->mapper->map([new FeatureChild(null, null,
+ new Scenario(new Location(0,1), [], '', ' title')
+ )]);
+
+ self::assertCount(1, $scenarios);
+ self::assertSame("title", $scenarios[0]->getTitle());
+ }
+
+ public function testItMapsScenarioKeyword()
+ {
+ $scenarios = $this->mapper->map([new FeatureChild(null, null,
+ new Scenario(new Location(), [], 'Scenario', '')
+ )]);
+
+ self::assertCount(1, $scenarios);
+ self::assertSame('Scenario', $scenarios[0]->getKeyword());
+ }
+
+ public function testItMapsScenarioLine()
+ {
+ $scenarios = $this->mapper->map([new FeatureChild(null, null,
+ new Scenario(new Location(100, 0))
+ )]);
+
+ self::assertCount(1, $scenarios);
+ self::assertSame(100, $scenarios[0]->getLine());
+ }
+
+ public function testItMapsScenarioTags()
+ {
+ $scenarios = $this->mapper->map([new FeatureChild(null, null,
+ new Scenario(new Location(), [new Tag(new Location(), 'foo')])
+ )]);
+
+ self::assertCount(1, $scenarios);
+ self::assertSame(['foo'], $scenarios[0]->getTags());
+ }
+
+ public function testItMapsScenarioSteps()
+ {
+ $scenarios = $this->mapper->map([new FeatureChild(null, null,
+ new Scenario(new Location(), [], '', '', '',
+ [new Step() ]
+ )
+ )]);
+
+ self::assertCount(1, $scenarios);
+ self::assertCount(1, $scenarios[0]->getSteps());
+ }
+
+ public function testItMapsScenarioWithExamplesAsScenarioOutline()
+ {
+ $scenarios = $this->mapper->map([new FeatureChild(null, null,
+ new Scenario(new Location(), [], '', '', '', [],
+ [new Examples()]
+ )
+ )]);
+
+ self::assertCount(1, $scenarios);
+ self::assertInstanceOf(OutlineNode::class, $scenarios[0]);
+ }
+
+ public function testItMapsExamples()
+ {
+ $scenarios = $this->mapper->map([new FeatureChild(null, null,
+ new Scenario(new Location(), [], '', '', '', [],
+ [new Examples()]
+ )
+ )]);
+
+ self::assertCount(1, $scenarios);
+ self::assertCount(1, $scenarios[0]->getExampleTables());
+ }
+
+ public function testItMapsRuleScenariosIntoFeature()
+ {
+ $scenarios = $this->mapper->map([new FeatureChild(
+ new Rule(new Location(), [], '', '', '', [
+ new RuleChild(null, new Scenario(new Location()))
+ ])
+ )]);
+
+ self::assertCount(1, $scenarios);
+ }
+
+ public function testItThrowsAParserErrorWhenBackgroundInRuleIsFound()
+ {
+ $this->expectException(ParserException::class);
+
+ $scenarios = $this->mapper->map([new FeatureChild(
+ new Rule(new Location(), [], '', '', '', [
+ new RuleChild(new Background(new Location()), null)
+ ])
+ )]);
+ }
+
+ public function testItMapsRuleScenariosWithUnduplicatedMergedTags()
+ {
+ $scenarios = $this->mapper->map([new FeatureChild(
+ new Rule(new Location(), [
+ new Tag(new Location(), '@foo'),
+ new Tag(new Location(), '@bar')
+ ], '', '', '', [
+ new RuleChild(null, new Scenario(new Location(), [
+ new Tag(new Location(), '@bar'),
+ new Tag(new Location(), '@baz')
+ ]))
+ ])
+ )]);
+
+ self::assertCount(1, $scenarios);
+ self::assertSame(['foo', 'bar', 'baz'], $scenarios[0]->getTags());
+ }
+}
diff --git a/tests/Behat/Gherkin/Cucumber/StepNodeMapperTest.php b/tests/Behat/Gherkin/Cucumber/StepNodeMapperTest.php
new file mode 100644
index 00000000..67ee05e2
--- /dev/null
+++ b/tests/Behat/Gherkin/Cucumber/StepNodeMapperTest.php
@@ -0,0 +1,113 @@
+mapper = new StepNodeMapper(
+ new KeywordTypeMapper(),
+ new PyStringNodeMapper(),
+ new TableNodeMapper()
+ );
+ }
+
+ public function testItMapsEmptyArray()
+ {
+ $steps = $this->mapper->map([]);
+
+ self::assertSame([], $steps);
+ }
+
+ public function testItMapsSteps()
+ {
+ $steps = $this->mapper->map([new Step()]);
+
+ self::assertCount(1, $steps);
+ self::assertInstanceOf(StepNode::class, $steps[0]);
+ }
+
+ public function testItMapsKeyword()
+ {
+ $steps = $this->mapper->map([new Step(
+ new Location(), 'Given '
+ )]);
+ $step = $steps[0];
+
+ self::assertSame('Given', $step->getKeyword());
+ }
+
+ public function testItMapsText()
+ {
+ $steps = $this->mapper->map([new Step(
+ new Location(), '', null, 'I have five carrots'
+ )]);
+ $step = $steps[0];
+
+ self::assertSame('I have five carrots', $step->getText());
+ }
+
+ public function testItMapsLine()
+ {
+ $steps = $this->mapper->map([new Step(
+ new Location(100, 0)
+ )]);
+ $step = $steps[0];
+
+ self::assertSame(100, $step->getLine());
+ }
+
+ public function testItMapsKeywordType()
+ {
+ $steps = $this->mapper->map([new Step(
+ new Location(), '', Step\KeywordType::CONTEXT
+ )]);
+ $step = $steps[0];
+
+ self::assertSame('Given', $step->getKeywordType());
+ }
+
+ public function testItMapsDocstringsToArguments()
+ {
+ $steps = $this->mapper->map([new Step(
+ new Location(), '', null,'', new DocString()
+ )]);
+ $step = $steps[0];
+
+ self::assertCount(1, $step->getArguments());
+ self::assertInstanceOf(PyStringNode::class, $step->getArguments()[0]);
+ }
+
+ public function testItMapsTablesToArguments()
+ {
+ $steps = $this->mapper->map([new Step(
+ new Location(), '', null,'', null, new DataTable()
+ )]);
+ $step = $steps[0];
+
+ self::assertCount(1, $step->getArguments());
+ self::assertInstanceOf(TableNode::class, $step->getArguments()[0]);
+ }
+}
diff --git a/tests/Behat/Gherkin/Cucumber/TableNodeMapperTest.php b/tests/Behat/Gherkin/Cucumber/TableNodeMapperTest.php
new file mode 100644
index 00000000..089f2f22
--- /dev/null
+++ b/tests/Behat/Gherkin/Cucumber/TableNodeMapperTest.php
@@ -0,0 +1,67 @@
+mapper = new TableNodeMapper();
+ }
+
+ public function testItMapsNullToEmptyArray()
+ {
+ $result = $this->mapper->map(null);
+
+ self::assertSame([], $result);
+ }
+
+ public function testItMapsCells()
+ {
+ $tables = $this->mapper->map(
+ new DataTable(new Location(), [
+ new TableRow(new Location(), [
+ new TableCell(new Location(100, 10), 'foo'),
+ new TableCell(new Location(100, 20), 'bar')
+ ]),
+ new TableRow(new Location(101, 0), [
+ new TableCell(new Location(101, 10), 'baz'),
+ new TableCell(new Location(101, 20), 'boz')
+ ])
+ ])
+ );
+
+ self::assertCount(1, $tables);
+ self::assertSame(['foo', 'bar'], $tables[0]->getRow(0));
+ self::assertSame(['baz', 'boz'], $tables[0]->getRow(1));
+ }
+
+ public function testItMapsLineFromFirstTableRow()
+ {
+ $tables = $this->mapper->map(
+ new DataTable(new Location(), [
+ new TableRow(new Location(100, 10), [
+ new TableCell(new Location(), 'foo')
+ ])
+ ])
+ );
+
+ self::assertCount(1, $tables);
+ self::assertSame(100, $tables[0]->getLine());
+ }
+}
diff --git a/tests/Behat/Gherkin/Fixtures/etalons/ru_addition.yml b/tests/Behat/Gherkin/Fixtures/etalons/ru_addition.yml
index 9c0db4aa..0fb3675a 100644
--- a/tests/Behat/Gherkin/Fixtures/etalons/ru_addition.yml
+++ b/tests/Behat/Gherkin/Fixtures/etalons/ru_addition.yml
@@ -17,5 +17,5 @@ feature:
steps:
- { keyword_type: 'Given', type: 'Допустим', text: 'я ввожу число 50', line: 8 }
- { keyword_type: 'Given', type: 'И', text: 'затем ввожу число 70', line: 9 }
- - { keyword_type: 'Then', type: 'Если', text: 'я нажимаю "+"', line: 10 }
- - { keyword_type: 'When', type: 'То', text: 'результатом должно быть число 120', line: 11 }
+ - { keyword_type: 'When', type: 'Если', text: 'я нажимаю "+"', line: 10 }
+ - { keyword_type: 'Then', type: 'То', text: 'результатом должно быть число 120', line: 11 }
diff --git a/tests/Behat/Gherkin/Fixtures/etalons/ru_consecutive_calculations.yml b/tests/Behat/Gherkin/Fixtures/etalons/ru_consecutive_calculations.yml
index 358d8678..b16784c7 100644
--- a/tests/Behat/Gherkin/Fixtures/etalons/ru_consecutive_calculations.yml
+++ b/tests/Behat/Gherkin/Fixtures/etalons/ru_consecutive_calculations.yml
@@ -20,15 +20,15 @@ feature:
title: сложение с результатом последней операций
line: 9
steps:
- - { keyword_type: Then, type: Если, text: 'я ввожу число 4', line: 10 }
- - { keyword_type: Then, type: И, text: 'нажимаю "+"', line: 11 }
- - { keyword_type: When, type: То, text: 'результатом должно быть число 12', line: 12 }
+ - { keyword_type: When, type: Если, text: 'я ввожу число 4', line: 10 }
+ - { keyword_type: When, type: И, text: 'нажимаю "+"', line: 11 }
+ - { keyword_type: Then, type: То, text: 'результатом должно быть число 12', line: 12 }
-
type: scenario
keyword: Сценарий
title: деление результата последней операции
line: 14
steps:
- - { keyword_type: Then, type: Если, text: 'я ввожу число 2', line: 15 }
- - { keyword_type: Then, type: И, text: 'нажимаю "/"', line: 16 }
- - { keyword_type: When, type: То, text: 'результатом должно быть число 4', line: 17 }
+ - { keyword_type: When, type: Если, text: 'я ввожу число 2', line: 15 }
+ - { keyword_type: When, type: И, text: 'нажимаю "/"', line: 16 }
+ - { keyword_type: Then, type: То, text: 'результатом должно быть число 4', line: 17 }
diff --git a/tests/Behat/Gherkin/Fixtures/etalons/ru_division.yml b/tests/Behat/Gherkin/Fixtures/etalons/ru_division.yml
index 192eeb55..595684b0 100644
--- a/tests/Behat/Gherkin/Fixtures/etalons/ru_division.yml
+++ b/tests/Behat/Gherkin/Fixtures/etalons/ru_division.yml
@@ -16,8 +16,8 @@ feature:
steps:
- { keyword_type: Given, type: 'Допустим', text: 'я ввожу число <делимое>', line: 7 }
- { keyword_type: Given, type: 'И', text: 'затем ввожу число <делитель>', line: 8 }
- - { keyword_type: Then, type: 'Если', text: 'я нажимаю "/"', line: 9 }
- - { keyword_type: When, type: 'То', text: 'результатом должно быть число <частное>', line: 10 }
+ - { keyword_type: When, type: 'Если', text: 'я нажимаю "/"', line: 9 }
+ - { keyword_type: Then, type: 'То', text: 'результатом должно быть число <частное>', line: 10 }
examples:
keyword: Примеры
diff --git a/tests/Behat/Gherkin/Fixtures/etalons/rules.yml b/tests/Behat/Gherkin/Fixtures/etalons/rules.yml
new file mode 100644
index 00000000..0f13a4fe
--- /dev/null
+++ b/tests/Behat/Gherkin/Fixtures/etalons/rules.yml
@@ -0,0 +1,14 @@
+feature:
+ title: ''
+ language: en
+ line: 1
+ description: ~
+
+ scenarios:
+ -
+ type: scenario
+ title: ''
+ tags: [rule-tag, scenario-tag]
+ line: 7
+ steps:
+ - { keyword_type: Given, type: Given, text: A, line: 8 }
diff --git a/tests/Behat/Gherkin/Fixtures/features/rules.feature b/tests/Behat/Gherkin/Fixtures/features/rules.feature
new file mode 100644
index 00000000..2970eaaa
--- /dev/null
+++ b/tests/Behat/Gherkin/Fixtures/features/rules.feature
@@ -0,0 +1,8 @@
+Feature:
+
+ @rule-tag
+ Rule:
+
+ @scenario-tag
+ Scenario:
+ Given A
diff --git a/tests/Behat/Gherkin/ParserTest.php b/tests/Behat/Gherkin/ParserTest.php
index 5102b2c4..e5173ace 100644
--- a/tests/Behat/Gherkin/ParserTest.php
+++ b/tests/Behat/Gherkin/ParserTest.php
@@ -15,36 +15,6 @@ class ParserTest extends TestCase
private $gherkin;
private $yaml;
- public function parserTestDataProvider()
- {
- $data = array();
-
- foreach (glob(__DIR__ . '/Fixtures/etalons/*.yml') as $file) {
- $testname = basename($file, '.yml');
-
- $data[$testname] = array($testname);
- }
-
- return $data;
- }
-
- /**
- * @dataProvider parserTestDataProvider
- *
- * @param string $fixtureName name of the fixture
- */
- public function testParser($fixtureName)
- {
- $etalon = $this->parseEtalon($fixtureName . '.yml');
- $features = $this->parseFixture($fixtureName . '.feature');
-
- $this->assertIsArray($features);
- $this->assertEquals(1, count($features));
- $fixture = $features[0];
-
- $this->assertEquals($etalon, $fixture);
- }
-
public function testParserResetsTagsBetweenFeatures()
{
$parser = $this->getGherkinParser();
@@ -97,30 +67,6 @@ protected function getGherkinParser()
'then' => 'Then',
'and' => 'And',
'but' => 'But'
- ),
- 'ru' => array(
- 'feature' => 'Функционал',
- 'background' => 'Предыстория',
- 'scenario' => 'Сценарий',
- 'scenario_outline' => 'Структура сценария',
- 'examples' => 'Примеры',
- 'given' => 'Допустим',
- 'when' => 'То',
- 'then' => 'Если',
- 'and' => 'И',
- 'but' => 'Но'
- ),
- 'ja' => array (
- 'feature' => 'フィーチャ',
- 'background' => '背景',
- 'scenario' => 'シナリオ',
- 'scenario_outline' => 'シナリオアウトライン',
- 'examples' => '例|サンプル',
- 'given' => '前提<',
- 'when' => 'もし<',
- 'then' => 'ならば<',
- 'and' => 'かつ<',
- 'but' => 'しかし<'
)
));
$this->gherkin = new Parser(new Lexer($keywords));
@@ -129,40 +75,6 @@ protected function getGherkinParser()
return $this->gherkin;
}
- protected function getYamlParser()
- {
- if (null === $this->yaml) {
- $this->yaml = new YamlFileLoader();
- }
-
- return $this->yaml;
- }
-
- protected function parseFixture($fixture)
- {
- $file = __DIR__ . '/Fixtures/features/' . $fixture;
-
- return array($this->getGherkinParser()->parse(file_get_contents($file), $file));
- }
-
- protected function parseEtalon($etalon)
- {
- $features = $this->getYamlParser()->load(__DIR__ . '/Fixtures/etalons/' . $etalon);
- $feature = $features[0];
-
- return new FeatureNode(
- $feature->getTitle(),
- $feature->getDescription(),
- $feature->getTags(),
- $feature->getBackground(),
- $feature->getScenarios(),
- $feature->getKeyword(),
- $feature->getLanguage(),
- __DIR__ . '/Fixtures/features/' . basename($etalon, '.yml') . '.feature',
- $feature->getLine()
- );
- }
-
public function testParsingManyCommentsShouldPass()
{
if (! extension_loaded('xdebug')) {