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')) {