From f4c8c3318edbf60e3d392eb956dd75b5354c0f47 Mon Sep 17 00:00:00 2001 From: Joost Nijhuis Date: Tue, 26 Jul 2022 15:46:55 +0200 Subject: [PATCH 1/2] Add descriptions support for the nodes: ExampleTable, Outline and Scenario. --- src/Behat/Gherkin/Lexer.php | 4 ++- src/Behat/Gherkin/Loader/ArrayLoader.php | 6 ++-- .../Loader/CucumberNDJsonAstLoader.php | 10 ++++-- src/Behat/Gherkin/Node/ExampleTableNode.php | 36 ++++++++++++++++++- src/Behat/Gherkin/Node/OutlineNode.php | 21 +++++++++-- src/Behat/Gherkin/Node/ScenarioNode.php | 20 +++++++++-- src/Behat/Gherkin/Parser.php | 36 ++++++++++++++++--- .../Gherkin/Cucumber/CompatibilityTest.php | 5 --- .../Fixtures/etalons/complex_descriptions.yml | 13 +++---- .../Gherkin/Fixtures/etalons/issue_13.yml | 18 +++++----- .../Fixtures/etalons/multiline_name.yml | 20 +++++------ .../etalons/multiline_name_with_newlines.yml | 33 ++++++----------- tests/Behat/Gherkin/ParserExceptionsTest.php | 32 +++++++++-------- 13 files changed, 164 insertions(+), 90 deletions(-) diff --git a/src/Behat/Gherkin/Lexer.php b/src/Behat/Gherkin/Lexer.php index 1f3b3c40..95767bc1 100644 --- a/src/Behat/Gherkin/Lexer.php +++ b/src/Behat/Gherkin/Lexer.php @@ -481,7 +481,9 @@ protected function scanPyStringContent() return null; } - $token = $this->scanText(); + $token = $this->takeToken('Text', $this->line); + $this->consumeLine(); + // swallow trailing spaces $token['value'] = preg_replace('/^\s{0,' . $this->pyStringSwallow . '}/u', '', $token['value'] ?? ''); diff --git a/src/Behat/Gherkin/Loader/ArrayLoader.php b/src/Behat/Gherkin/Loader/ArrayLoader.php index 145bed9d..fb53c0ec 100644 --- a/src/Behat/Gherkin/Loader/ArrayLoader.php +++ b/src/Behat/Gherkin/Loader/ArrayLoader.php @@ -139,13 +139,14 @@ protected function loadScenarioHash(array $hash, $line = 0) 'keyword' => 'Scenario', 'line' => $line, 'steps' => array(), + 'description' => null ), $hash ); $steps = $this->loadStepsHash($hash['steps']); - return new ScenarioNode($hash['title'], $hash['tags'], $steps, $hash['keyword'], $hash['line']); + return new ScenarioNode($hash['title'], $hash['tags'], $steps, $hash['keyword'], $hash['line'], $hash['description']); } /** @@ -166,6 +167,7 @@ protected function loadOutlineHash(array $hash, $line = 0) 'line' => $line, 'steps' => array(), 'examples' => array(), + 'description' => null ), $hash ); @@ -189,7 +191,7 @@ protected function loadOutlineHash(array $hash, $line = 0) $examples[] = new ExampleTableNode($exHash, $examplesKeyword);; } - return new OutlineNode($hash['title'], $hash['tags'], $steps, $examples, $hash['keyword'], $hash['line']); + return new OutlineNode($hash['title'], $hash['tags'], $steps, $examples, $hash['keyword'], $hash['line'], $hash['description']); } /** diff --git a/src/Behat/Gherkin/Loader/CucumberNDJsonAstLoader.php b/src/Behat/Gherkin/Loader/CucumberNDJsonAstLoader.php index 9fe763bc..ea0480ca 100644 --- a/src/Behat/Gherkin/Loader/CucumberNDJsonAstLoader.php +++ b/src/Behat/Gherkin/Loader/CucumberNDJsonAstLoader.php @@ -85,7 +85,8 @@ static function ($child) { self::getSteps(isset($child['scenario']['steps']) ? $child['scenario']['steps'] : []), self::getTables($child['scenario']['examples']), $child['scenario']['keyword'], - $child['scenario']['location']['line'] + $child['scenario']['location']['line'], + $child['scenario']['description'] ?? null ); } else { @@ -94,7 +95,8 @@ static function ($child) { self::getTags($child['scenario']), self::getSteps(isset($child['scenario']['steps']) ? $child['scenario']['steps'] : []), $child['scenario']['keyword'], - $child['scenario']['location']['line'] + $child['scenario']['location']['line'], + $child['scenario']['description'] ?? null ); } @@ -184,7 +186,9 @@ static function($cell) { return new ExampleTableNode( $table, $tableJson['keyword'], - self::getTags($tableJson) + self::getTags($tableJson), + $tableJson['name'] ?? null, + $tableJson['description'] ?? null ); }, $json diff --git a/src/Behat/Gherkin/Node/ExampleTableNode.php b/src/Behat/Gherkin/Node/ExampleTableNode.php index 91753511..35c73ef5 100644 --- a/src/Behat/Gherkin/Node/ExampleTableNode.php +++ b/src/Behat/Gherkin/Node/ExampleTableNode.php @@ -27,17 +27,31 @@ class ExampleTableNode extends TableNode */ private $keyword; + /** + * @var null|string + */ + private $name; + + /** + * @var null|string + */ + private $description; + /** * Initializes example table. * * @param array $table Table in form of [$rowLineNumber => [$val1, $val2, $val3]] * @param string $keyword * @param string[] $tags + * @param null|string $name + * @param null|string $description */ - public function __construct(array $table, $keyword, array $tags = array()) + public function __construct(array $table, $keyword, array $tags = [], ?string $name = null, ?string $description = null) { $this->keyword = $keyword; $this->tags = $tags; + $this->name = $name; + $this->description = $description; parent::__construct($table); } @@ -70,4 +84,24 @@ public function getKeyword() { return $this->keyword; } + + /** + * Returns the name. + * + * @return null|string + */ + public function getName() + { + return $this->name; + } + + /** + * Returns the description. + * + * @return null|string + */ + public function getDescription() + { + return $this->description; + } } diff --git a/src/Behat/Gherkin/Node/OutlineNode.php b/src/Behat/Gherkin/Node/OutlineNode.php index 8e68b25a..5f4eaa32 100644 --- a/src/Behat/Gherkin/Node/OutlineNode.php +++ b/src/Behat/Gherkin/Node/OutlineNode.php @@ -18,7 +18,7 @@ class OutlineNode implements ScenarioInterface { /** - * @var string + * @var null|string */ private $title; /** @@ -45,6 +45,10 @@ class OutlineNode implements ScenarioInterface * @var null|ExampleNode[] */ private $examples; + /** + * @var null|string + */ + private $description; /** * Initializes outline. @@ -55,6 +59,7 @@ class OutlineNode implements ScenarioInterface * @param ExampleTableNode|ExampleTableNode[] $tables * @param string $keyword * @param integer $line + * @param null|string $description */ public function __construct( $title, @@ -62,7 +67,8 @@ public function __construct( array $steps, $tables, $keyword, - $line + $line, + ?string $description = null ) { $this->title = $title; $this->tags = $tags; @@ -74,6 +80,7 @@ public function __construct( } else { $this->tables = $tables; } + $this->description = $description; } /** @@ -219,6 +226,16 @@ public function getLine() return $this->line; } + /** + * Returns the description. + * + * @return null|string + */ + public function getDescription() + { + return $this->description; + } + /** * Creates examples for this outline using examples table. * diff --git a/src/Behat/Gherkin/Node/ScenarioNode.php b/src/Behat/Gherkin/Node/ScenarioNode.php index 277ab968..d86a3736 100644 --- a/src/Behat/Gherkin/Node/ScenarioNode.php +++ b/src/Behat/Gherkin/Node/ScenarioNode.php @@ -18,7 +18,7 @@ class ScenarioNode implements ScenarioInterface { /** - * @var string + * @var null|string */ private $title; /** @@ -37,6 +37,10 @@ class ScenarioNode implements ScenarioInterface * @var integer */ private $line; + /** + * @var null|string + */ + private $description; /** * Initializes scenario. @@ -46,14 +50,16 @@ class ScenarioNode implements ScenarioInterface * @param StepNode[] $steps * @param string $keyword * @param integer $line + * @param null|string $description */ - public function __construct($title, array $tags, array $steps, $keyword, $line) + public function __construct($title, array $tags, array $steps, $keyword, $line, ?string $description = null) { $this->title = $title; $this->tags = $tags; $this->steps = $steps; $this->keyword = $keyword; $this->line = $line; + $this->description = $description; } /** @@ -147,4 +153,14 @@ public function getLine() { return $this->line; } + + /** + * Returns the description. + * + * @return null|string + */ + public function getDescription() + { + return $this->description; + } } diff --git a/src/Behat/Gherkin/Parser.php b/src/Behat/Gherkin/Parser.php index cc6ed005..08b76704 100644 --- a/src/Behat/Gherkin/Parser.php +++ b/src/Behat/Gherkin/Parser.php @@ -375,7 +375,8 @@ protected function parseScenario() { $token = $this->expectTokenType('Scenario'); - $title = trim($token['value'] ?? ''); + $title = $token['value'] ?? ''; + $description = $this->parseDescription(); $tags = $this->popTags(); $keyword = $token['keyword']; $line = $token['line']; @@ -422,7 +423,7 @@ protected function parseScenario() array_pop($this->passedNodesStack); - return new ScenarioNode(rtrim($title) ?: null, $tags, $steps, $keyword, $line); + return new ScenarioNode(rtrim($title) ?: null, $tags, $steps, $keyword, $line, $description); } /** @@ -437,6 +438,7 @@ protected function parseOutline() $token = $this->expectTokenType('Outline'); $title = trim($token['value'] ?? ''); + $description = $this->parseDescription(); $tags = $this->popTags(); $keyword = $token['keyword']; @@ -505,7 +507,7 @@ protected function parseOutline() )); } - return new OutlineNode(rtrim($title) ?: null, $tags, $steps, $examples, $keyword, $line); + return new OutlineNode(rtrim($title) ?: null, $tags, $steps, $examples, $keyword, $line, $description); } /** @@ -550,12 +552,15 @@ protected function parseStep() */ protected function parseExamples() { - $keyword = ($this->expectTokenType('Examples'))['keyword']; + $token = $this->expectTokenType('Examples'); + $keyword = $token['keyword']; + $name = $token['value'] ?? null; $tags = empty($this->tags) ? array() : $this->popTags(); + $description = $this->parseDescription(); $table = $this->parseTableRows(); try { - return new ExampleTableNode($table, $keyword, $tags); + return new ExampleTableNode($table, $keyword, $tags, $name, $description); } catch(NodeException $e) { $this->rethrowNodeException($e); } @@ -733,6 +738,27 @@ private function parseTableRows() return $table; } + /** + * @return string|null + */ + private function parseDescription() + { + $result = null; + while (null !== ($node = $this->acceptTokenType('Newline') ?? $this->acceptTokenType('Text'))) { + if ($node['type'] === 'Newline') { + $result .= "\n"; + } else { + if ($result === null) { + $result = $node['value'] ?? "\n"; + } else { + $result .= "\n" . $node['value'] ?? "\n"; + } + } + } + + return null !== $result ? trim($result, "\n") : null; + } + /** * Changes step node type for types But, And to type of previous step if it exists else sets to Given * diff --git a/tests/Behat/Gherkin/Cucumber/CompatibilityTest.php b/tests/Behat/Gherkin/Cucumber/CompatibilityTest.php index acb43e4f..fad95417 100644 --- a/tests/Behat/Gherkin/Cucumber/CompatibilityTest.php +++ b/tests/Behat/Gherkin/Cucumber/CompatibilityTest.php @@ -3,15 +3,11 @@ namespace Behat\Gherkin\Cucumber; use Behat\Gherkin\Exception\ParserException; -use Behat\Gherkin\Gherkin; use Behat\Gherkin\Keywords; use Behat\Gherkin\Lexer; -use Behat\Gherkin\Loader\ArrayLoader; use Behat\Gherkin\Loader\CucumberNDJsonAstLoader; use Behat\Gherkin\Loader\LoaderInterface; -use Behat\Gherkin\Node\FeatureNode; use Behat\Gherkin\Node\ScenarioInterface; -use Behat\Gherkin\Node\ScenarioNode; use Behat\Gherkin\Node\StepNode; use Behat\Gherkin\Parser; use PHPUnit\Framework\TestCase; @@ -30,7 +26,6 @@ class CompatibilityTest extends TestCase 'rule.feature' => 'Rule keyword not supported', 'rule_with_tag.feature' => 'Rule keyword not supported', 'tags.feature' => 'Rule keyword not supported', - 'descriptions.feature' => 'Examples table descriptions not supported', 'incomplete_scenario_outline.feature' => 'Scenario and Scenario outline not yet synonyms', 'padded_example.feature' => 'Scenario and Scenario outline not yet synonyms', 'scenario_outline.feature' => 'Scenario and Scenario outline not yet synonyms', diff --git a/tests/Behat/Gherkin/Fixtures/etalons/complex_descriptions.yml b/tests/Behat/Gherkin/Fixtures/etalons/complex_descriptions.yml index d0d65dc2..dc792052 100644 --- a/tests/Behat/Gherkin/Fixtures/etalons/complex_descriptions.yml +++ b/tests/Behat/Gherkin/Fixtures/etalons/complex_descriptions.yml @@ -13,14 +13,9 @@ feature: scenarios: - - type: scenario - title: |- - Some - | complex | description | - - """ - hell yeah - """ - line: 16 + type: scenario + title: Some + description: " | complex | description |\n\n\"\"\"\nhell yeah\n\"\"\"" + line: 16 steps: - { keyword_type: 'Given', type: 'Given', text: 'one two three', line: 22 } diff --git a/tests/Behat/Gherkin/Fixtures/etalons/issue_13.yml b/tests/Behat/Gherkin/Fixtures/etalons/issue_13.yml index 6c2d64d2..3e03a44e 100644 --- a/tests/Behat/Gherkin/Fixtures/etalons/issue_13.yml +++ b/tests/Behat/Gherkin/Fixtures/etalons/issue_13.yml @@ -6,11 +6,10 @@ feature: scenarios: - - type: scenario - title: |- - testing py string in scenario - second line - line: 4 + type: scenario + title: testing py string in scenario + description: second line + line: 4 steps: - keyword_type: Given @@ -26,11 +25,10 @@ feature: 6000 - - type: outline - title: |- - testing py string in scenario outline - second line - line: 14 + type: outline + title: testing py string in scenario outline + description: second line + line: 14 steps: - keyword_type: Given diff --git a/tests/Behat/Gherkin/Fixtures/etalons/multiline_name.yml b/tests/Behat/Gherkin/Fixtures/etalons/multiline_name.yml index b41bc58d..6abb12b4 100644 --- a/tests/Behat/Gherkin/Fixtures/etalons/multiline_name.yml +++ b/tests/Behat/Gherkin/Fixtures/etalons/multiline_name.yml @@ -11,22 +11,18 @@ feature: scenarios: - - type: scenario - title: |- - I'm a multiline name - which goes on and on and on for three lines - yawn - line: 6 + type: scenario + title: I'm a multiline name + description: " which goes on and on and on for three lines\n yawn" + line: 6 steps: - { keyword_type: Given, 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 + type: outline + title: I'm a multiline name + description: " which goes on and on and on for three lines\n yawn" + line: 11 steps: - { keyword_type: Given, type: 'Given', text: ' without a table', line: 14 } examples: diff --git a/tests/Behat/Gherkin/Fixtures/etalons/multiline_name_with_newlines.yml b/tests/Behat/Gherkin/Fixtures/etalons/multiline_name_with_newlines.yml index d7aebc06..a623b2ac 100644 --- a/tests/Behat/Gherkin/Fixtures/etalons/multiline_name_with_newlines.yml +++ b/tests/Behat/Gherkin/Fixtures/etalons/multiline_name_with_newlines.yml @@ -15,25 +15,17 @@ feature: scenarios: - - type: scenario - title: |- - - I'm a multiline name - which goes on and on and on for three lines - yawn - line: 13 + type: scenario + description: " I'm a multiline name\n which goes on and on and on for three lines\n yawn" + line: 13 steps: - { keyword_type: Given, type: Given, text: passing without a table, line: 19 } - - type: outline - title: |- - I'm a multiline name - - which goes on and on and on for three lines - - yawn - line: 21 + type: outline + title: I'm a multiline name + description: " which goes on and on and on for three lines\n\n yawn" + line: 21 steps: - { keyword_type: 'Given', type: 'Given', text: ' without a table', line: 28 } examples: @@ -50,14 +42,9 @@ feature: 45: [state] 46: [passing] - - type: outline - title: |- - - - I'm a multiline name - which goes on and on and on for three lines - yawn - line: 48 + type: outline + description: " I'm a multiline name\n which goes on and on and on for three lines\n yawn" + line: 48 steps: - { keyword_type: 'Given', type: 'Given', text: ' without a table', line: 55 } examples: diff --git a/tests/Behat/Gherkin/ParserExceptionsTest.php b/tests/Behat/Gherkin/ParserExceptionsTest.php index ec315c1e..9f3a1d23 100644 --- a/tests/Behat/Gherkin/ParserExceptionsTest.php +++ b/tests/Behat/Gherkin/ParserExceptionsTest.php @@ -119,21 +119,23 @@ public function testTextInScenario() $feature = $this->gherkin->parse($feature); $this->assertCount(2, $scenarios = $feature->getScenarios()); - $firstTitle = <<assertEquals($firstTitle, $scenarios[0]->getTitle()); - $secondTitle = <<assertEquals($firstDescription, $scenarios[0]->getDescription()); + $secondTitle = 'bug user edit date'; + $this->assertEquals($secondTitle, $scenarios[1]->getTitle()); + $secondDescription = <<assertEquals($secondTitle, $scenarios[1]->getTitle()); + $this->assertEquals($secondDescription, $scenarios[1]->getDescription()); } public function testAmbigiousLanguage() From 2bb78a66e5131b4afb7df9d1f2455b58d6d536b8 Mon Sep 17 00:00:00 2001 From: Joost Nijhuis Date: Tue, 13 Sep 2022 18:15:40 +0200 Subject: [PATCH 2/2] Processed review (1) --- src/Behat/Gherkin/Parser.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Behat/Gherkin/Parser.php b/src/Behat/Gherkin/Parser.php index 08b76704..a02a973e 100644 --- a/src/Behat/Gherkin/Parser.php +++ b/src/Behat/Gherkin/Parser.php @@ -375,7 +375,7 @@ protected function parseScenario() { $token = $this->expectTokenType('Scenario'); - $title = $token['value'] ?? ''; + $title = trim($token['value'] ?? ''); $description = $this->parseDescription(); $tags = $this->popTags(); $keyword = $token['keyword'];