1212
1313use Behat \Gherkin \Dialect \CucumberDialectProvider ;
1414use Behat \Gherkin \Exception \ParserException ;
15+ use Behat \Gherkin \GherkinCompatibilityMode ;
1516use Behat \Gherkin \Lexer ;
1617use Behat \Gherkin \Loader \CucumberNDJsonAstLoader ;
17- use Behat \Gherkin \Node \FeatureNode ;
1818use Behat \Gherkin \Parser ;
1919use FilesystemIterator ;
2020use PHPUnit \Framework \Attributes \DataProvider ;
2828 * Tests the parser against the upstream cucumber/gherkin test data.
2929 *
3030 * @group cucumber-compatibility
31+ *
32+ * @phpstan-type TCucumberParsingTestCase array{mode: GherkinCompatibilityMode, file: SplFileInfo}
33+ * @phpstan-type TKnownIncompatibilityMap array<value-of<GherkinCompatibilityMode>, array<string,string>>
3134 */
3235class CompatibilityTest extends TestCase
3336{
3437 private const GHERKIN_TESTDATA_PATH = __DIR__ . '/../../vendor/cucumber/gherkin-monorepo/testdata ' ;
3538 private const EXTRA_TESTDATA_PATH = __DIR__ . '/extra_testdata ' ;
3639
3740 /**
38- * @var array<string, string>
41+ * @phpstan- var TKnownIncompatibilityMap
3942 */
4043 private array $ notParsingCorrectly = [
41- 'complex_background.feature ' => 'Rule keyword not supported ' ,
42- 'docstrings.feature ' => 'Escaped delimiters in docstrings are not unescaped ' ,
43- 'datatables_with_new_lines.feature ' => 'Escaped newlines in table cells are not unescaped ' ,
44- 'escaped_pipes.feature ' => 'Escaped newlines in table cells are not unescaped ' ,
45- 'rule.feature ' => 'Rule keyword not supported ' ,
46- 'rule_with_tag.feature ' => 'Rule keyword not supported ' ,
47- 'tags.feature ' => 'Rule keyword not supported ' ,
48- 'descriptions.feature ' => 'Examples table descriptions not supported ' ,
49- 'descriptions_with_comments.feature ' => 'Examples table descriptions not supported ' ,
50- 'feature_keyword_in_scenario_description.feature ' => 'Scenario descriptions not supported ' ,
51- 'padded_example.feature ' => 'Table padding is not trimmed as aggressively ' ,
52- 'spaces_in_language.feature ' => 'Whitespace not supported around language selector ' ,
53- 'rule_without_name_and_description.feature ' => 'Rule is wrongly parsed as Description ' ,
54- 'incomplete_background_2.feature ' => 'Background descriptions not supported ' ,
44+ 'legacy ' => [
45+ 'complex_background.feature ' => 'Rule keyword not supported ' ,
46+ 'docstrings.feature ' => 'Escaped delimiters in docstrings are not unescaped ' ,
47+ 'datatables_with_new_lines.feature ' => 'Escaped newlines in table cells are not unescaped ' ,
48+ 'escaped_pipes.feature ' => 'Escaped newlines in table cells are not unescaped ' ,
49+ 'rule.feature ' => 'Rule keyword not supported ' ,
50+ 'rule_with_tag.feature ' => 'Rule keyword not supported ' ,
51+ 'tags.feature ' => 'Rule keyword not supported ' ,
52+ 'descriptions.feature ' => 'Examples table descriptions not supported ' ,
53+ 'descriptions_with_comments.feature ' => 'Examples table descriptions not supported ' ,
54+ 'feature_keyword_in_scenario_description.feature ' => 'Scenario descriptions not supported ' ,
55+ 'padded_example.feature ' => 'Table padding is not trimmed as aggressively ' ,
56+ 'spaces_in_language.feature ' => 'Whitespace not supported around language selector ' ,
57+ 'rule_without_name_and_description.feature ' => 'Rule is wrongly parsed as Description ' ,
58+ 'incomplete_background_2.feature ' => 'Background descriptions not supported ' ,
59+ ],
60+ 'gherkin-32 ' => [
61+ 'complex_background.feature ' => 'Rule keyword not supported ' ,
62+ 'docstrings.feature ' => 'Escaped delimiters in docstrings are not unescaped ' ,
63+ 'datatables_with_new_lines.feature ' => 'Escaped newlines in table cells are not unescaped ' ,
64+ 'escaped_pipes.feature ' => 'Escaped newlines in table cells are not unescaped ' ,
65+ 'rule.feature ' => 'Rule keyword not supported ' ,
66+ 'rule_with_tag.feature ' => 'Rule keyword not supported ' ,
67+ 'tags.feature ' => 'Rule keyword not supported ' ,
68+ 'descriptions.feature ' => 'Examples table descriptions not supported ' ,
69+ 'descriptions_with_comments.feature ' => 'Examples table descriptions not supported ' ,
70+ 'feature_keyword_in_scenario_description.feature ' => 'Scenario descriptions not supported ' ,
71+ 'padded_example.feature ' => 'Table padding is not trimmed as aggressively ' ,
72+ 'spaces_in_language.feature ' => 'Whitespace not supported around language selector ' ,
73+ 'rule_without_name_and_description.feature ' => 'Rule is wrongly parsed as Description ' ,
74+ 'incomplete_background_2.feature ' => 'Background descriptions not supported ' ,
75+ ],
5576 ];
5677
5778 /**
58- * @var array<string, string>
79+ * @phpstan- var TKnownIncompatibilityMap
5980 */
6081 private array $ parsedButShouldNotBe = [
61- 'invalid_language.feature ' => 'Invalid language is silently ignored ' ,
82+ 'legacy ' => [
83+ 'invalid_language.feature ' => 'Invalid language is silently ignored ' ,
84+ ],
85+ 'gherkin-32 ' => [
86+ 'invalid_language.feature ' => 'Invalid language is silently ignored ' ,
87+ ],
6288 ];
6389
6490 /**
65- * @var array<string, string>
91+ * @phpstan- var TKnownIncompatibilityMap
6692 */
6793 private array $ deprecatedInsteadOfParseError = [
68- 'whitespace_in_tags.feature ' => '/Whitespace in tags is deprecated/ ' ,
94+ 'legacy ' => [
95+ 'whitespace_in_tags.feature ' => '/Whitespace in tags is deprecated/ ' ,
96+ ],
97+ 'gherkin-32 ' => [
98+ 'whitespace_in_tags.feature ' => '/Whitespace in tags is deprecated/ ' ,
99+ ],
69100 ];
70101
71102 private Parser $ parser ;
@@ -74,10 +105,14 @@ class CompatibilityTest extends TestCase
74105
75106 private static ?StepNodeComparator $ stepNodeComparator = null ;
76107
108+ private static ?FeatureNodeComparator $ featureNodeComparator = null ;
109+
77110 public static function setUpBeforeClass (): void
78111 {
79112 self ::$ stepNodeComparator = new StepNodeComparator ();
80113 Factory::getInstance ()->register (self ::$ stepNodeComparator );
114+ self ::$ featureNodeComparator = new FeatureNodeComparator ();
115+ Factory::getInstance ()->register (self ::$ featureNodeComparator );
81116 }
82117
83118 public static function tearDownAfterClass (): void
@@ -86,6 +121,10 @@ public static function tearDownAfterClass(): void
86121 Factory::getInstance ()->unregister (self ::$ stepNodeComparator );
87122 self ::$ stepNodeComparator = null ;
88123 }
124+ if (self ::$ featureNodeComparator !== null ) {
125+ Factory::getInstance ()->unregister (self ::$ featureNodeComparator );
126+ self ::$ featureNodeComparator = null ;
127+ }
89128 }
90129
91130 protected function setUp (): void
@@ -96,36 +135,43 @@ protected function setUp(): void
96135 }
97136
98137 #[DataProvider('goodCucumberFeatures ' )]
99- public function testFeaturesParseTheSameAsCucumber (SplFileInfo $ file ): void
138+ public function testFeaturesParseTheSameAsCucumber (GherkinCompatibilityMode $ mode , SplFileInfo $ file ): void
100139 {
101- if (isset ($ this ->notParsingCorrectly [$ file ->getFilename ()])) {
102- $ this ->markTestIncomplete ($ this ->notParsingCorrectly [$ file ->getFilename ()]);
140+ if (isset ($ this ->notParsingCorrectly [$ mode -> value ][ $ file ->getFilename ()])) {
141+ $ this ->markTestIncomplete ($ this ->notParsingCorrectly [$ mode -> value ][ $ file ->getFilename ()]);
103142 }
104143
144+ assert (self ::$ featureNodeComparator instanceof FeatureNodeComparator);
145+ self ::$ featureNodeComparator ->setGherkinCompatibilityMode ($ mode );
146+ $ this ->parser ->setGherkinCompatibilityMode ($ mode );
147+
105148 $ gherkinFile = $ file ->getPathname ();
106149 $ actual = $ this ->parser ->parse (Filesystem::readFile ($ gherkinFile ), $ gherkinFile );
107150 $ cucumberFeatures = $ this ->loader ->load ($ gherkinFile . '.ast.ndjson ' );
108151
109152 $ expected = $ cucumberFeatures ? $ cucumberFeatures [0 ] : null ;
110153
111154 $ this ->assertEquals (
112- $ this -> normaliseFeature ( $ expected) ,
113- $ this -> normaliseFeature ( $ actual) ,
114- Filesystem::readFile ($ gherkinFile )
155+ $ expected ,
156+ $ actual ,
157+ Filesystem::readFile ($ gherkinFile ),
115158 );
116159 }
117160
118161 #[DataProvider('badCucumberFeatures ' )]
119- public function testBadFeaturesDoNotParse (SplFileInfo $ file ): void
162+ public function testBadFeaturesDoNotParse (GherkinCompatibilityMode $ mode , SplFileInfo $ file ): void
120163 {
121- if (isset ($ this ->parsedButShouldNotBe [$ file ->getFilename ()])) {
122- $ this ->markTestIncomplete ($ this ->parsedButShouldNotBe [$ file ->getFilename ()]);
164+ if (isset ($ this ->parsedButShouldNotBe [$ mode -> value ][ $ file ->getFilename ()])) {
165+ $ this ->markTestIncomplete ($ this ->parsedButShouldNotBe [$ mode -> value ][ $ file ->getFilename ()]);
123166 }
124167
125168 $ gherkinFile = $ file ->getPathname ();
169+ $ this ->parser ->setGherkinCompatibilityMode ($ mode );
126170
127- if (isset ($ this ->deprecatedInsteadOfParseError [$ file ->getFilename ()])) {
128- $ this ->expectDeprecationErrorMatches ($ this ->deprecatedInsteadOfParseError [$ file ->getFilename ()]);
171+ if (isset ($ this ->deprecatedInsteadOfParseError [$ mode ->value ][$ file ->getFilename ()])) {
172+ $ this ->expectDeprecationErrorMatches (
173+ $ this ->deprecatedInsteadOfParseError [$ mode ->value ][$ file ->getFilename ()],
174+ );
129175 } else {
130176 // Note that the exception message is not part of compatibility testing and therefore cannot be checked.
131177 $ this ->expectException (ParserException::class);
@@ -135,7 +181,7 @@ public function testBadFeaturesDoNotParse(SplFileInfo $file): void
135181 }
136182
137183 /**
138- * @return iterable<string, array{file: SplFileInfo} >
184+ * @phpstan- return iterable<string, TCucumberParsingTestCase >
139185 */
140186 public static function goodCucumberFeatures (): iterable
141187 {
@@ -144,7 +190,7 @@ public static function goodCucumberFeatures(): iterable
144190 }
145191
146192 /**
147- * @return iterable<string, array{file: SplFileInfo} >
193+ * @phpstan- return iterable<string, TCucumberParsingTestCase >
148194 */
149195 public static function badCucumberFeatures (): iterable
150196 {
@@ -153,7 +199,7 @@ public static function badCucumberFeatures(): iterable
153199 }
154200
155201 /**
156- * @return iterable<string, array{file: SplFileInfo} >
202+ * @phpstan- return iterable<string, TCucumberParsingTestCase >
157203 */
158204 private static function getCucumberFeatures (string $ folder ): iterable
159205 {
@@ -163,37 +209,16 @@ private static function getCucumberFeatures(string $folder): iterable
163209 */
164210 foreach ($ fileIterator as $ file ) {
165211 if ($ file ->isFile () && $ file ->getExtension () === 'feature ' ) {
166- yield $ file ->getFilename () => ['file ' => $ file ];
212+ foreach (GherkinCompatibilityMode::cases () as $ mode ) {
213+ yield $ file ->getFilename () . ' ( ' . $ mode ->value . ') ' => [
214+ 'mode ' => $ mode ,
215+ 'file ' => $ file ,
216+ ];
217+ }
167218 }
168219 }
169220 }
170221
171- /**
172- * Remove features that aren't present in the cucumber source.
173- */
174- private function normaliseFeature (?FeatureNode $ feature ): ?FeatureNode
175- {
176- if (is_null ($ feature )) {
177- return null ;
178- }
179-
180- return new FeatureNode (
181- $ feature ->getTitle (),
182- // We currently handle whitespace in feature descriptions differently to cucumber
183- // https://github.com/Behat/Gherkin/issues/209
184- // We need to be able to ignore that difference so that we can still run cucumber tests that
185- // include a description but are covering other features.
186- $ feature ->getDescription () === null ? null : preg_replace ('/^\s+/m ' , '' , $ feature ->getDescription ()),
187- $ feature ->getTags (),
188- $ feature ->getBackground (),
189- $ feature ->getScenarios (),
190- $ feature ->getKeyword (),
191- $ feature ->getLanguage (),
192- $ feature ->getFile (),
193- $ feature ->getLine (),
194- );
195- }
196-
197222 private function expectDeprecationErrorMatches (string $ message ): void
198223 {
199224 set_error_handler (
0 commit comments