-
Notifications
You must be signed in to change notification settings - Fork 95
add: Parse Scenario as a synonym for Scenario Outline
#316
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
add: Parse Scenario as a synonym for Scenario Outline
#316
Conversation
In cucumber/gherkin, the `Scenario:` and `Scenario Outline:` keywords are interchangeable. Either may have (or not have) an `Examples:` table. We still need to differentiate between `OutlineNode` and `ScenarioNode` in the parsed result, for BC and because Behat treats these differently at runtime. However, we can safely parse a `Scenario:` that contains an `Examples:` table (which would have previously been invalid) to an `OutlineNode` - exactly as if the user had written `Scenario Outline:`. This does require refactoring how we parse tags, because: * Previously, we could make assumptions based on whether we were expecting (or had already seen) an Examples table. * Now, we can't tell from the tags alone whether they belong to an Examples table, or mark the end of the current Scenario and the start of the next. Therefore, we now scan ahead to predict which node the tags are attached to. This also allows us to throw a clearer exception if tags are found in unexpected places in the document.
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #316 +/- ##
============================================
+ Coverage 96.23% 96.70% +0.47%
- Complexity 598 602 +4
============================================
Files 35 36 +1
Lines 1751 1760 +9
============================================
+ Hits 1685 1702 +17
+ Misses 66 58 -8
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. |
| private $tags = []; | ||
| private $languageSpecifierLine; | ||
|
|
||
| private $passedNodesStack = []; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was only used to track recursion into nodes for the purpose of making assumptions about whether we could take tags (and looks like it wasn't always unwound). This is now handled differently.
| // Should not be possible to happen, parseTags should have already picked this up. | ||
| throw new UnexpectedTaggedNodeException($token, $this->file); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have kept this for the moment out of caution, I don't think it can happen now (it doesn't with any of the test examples as Codecov has also detected).
| if (!($node instanceof OutlineNode && $node->hasExamples())) { | ||
| throw new ParserException(sprintf( | ||
| 'Outline should have examples table, but got none for outline "%s" on line: %d%s', | ||
| $node->getTitle(), | ||
| $node->getLine(), | ||
| $this->file ? ' in file: ' . $this->file : '' | ||
| )); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
NB cucumber/gherkin doesn't throw in this case (see #153) - I have kept it for now but plan to look at that separately.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think solving #153 fully is just a matter of removing that exception, by fully making them synonyms.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree, I just mean it's a separate PR/change. It relates to other cases where we currently throw on partially-complete feature files that gherkin accepts and I think we should consider that in the round.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
well, to me, accepting outlines without examples (returning them as a ScenarioNode) is part of making Scenario and Scenario Outline be synonyms. Right now, you are making Scenario a synonym of Scenario Outline but Scenario Outline is not a synonym of Scenario, which is quite weird. Synonyms are meant to be bidirectional relations to me.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right now, you are making Scenario a synonym of Scenario Outline but Scenario Outline is not a synonym of Scenario
Which is why I was careful to title the PR "add: Parse Scenario as a synonym for Scenario Outline" and not "Treat Scenario and Scenario Outlines as synonyms" 😆
Synonyms are meant to be bidirectional relations to me.
Synonyms are very often only synonyms in context, I can think of lots of examples of synonyms that are not always bidirectional, or where one is a generalisation of a more specific term.
A Scenario with an Examples table is clearly equivalent to a Scenario Outline. Is a Scenario Outline (where the writer has taken care to use the specific term) without an Examples table always the same as a Scenario? I think that's open to debate (but of course we'll eventually follow cucumber/gherkin's opinion).
The bigger point is - I am trying to move forward with cucumber/gherkin parity (which has been stalled for years). Some things - like this PR - are very obviously just new features with no BC impact.
Others - like no longer throwing if the user said "Scenario Outline" but did not provide a table - involve changes to existing behaviour that is currently explicitly tested.
Therefore I want to split these changes up into separate commits / PRs so that we can properly review them and consider any BC impact. I do not want to ship another release that breaks people's CI because we overlooked something, so I think we should keep each change as incremental as possible.
I note that I said from the moment I opened the PR that I would be coming back to this specific point (which we also have an open issue for) so tbh I don't see why it should block approving / merging these changes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would say that turning a parsing exception into a supported case won't break CI of anyone: if they have files matching that case, it will potentially fix their CI. Turning invalid files into valid ones is generally not considered a BC break (otherwise, it would be impossible to add any new syntax)
And this is also exactly the kind of changes you are doing for files using Scenario that have an Examples table today (which are also a ParserException).
But anyway, I won't block the merge on that if you decide to keep it split.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fair points and broadly I agree, I just think there's a philosophical difference between changing behaviour that is undefined, and changing behaviour that we have explicit tests for. Hence I prefer the discipline of considering the second type of changes separately. The next PR will be very soon 😄
| if ($examples !== []) { | ||
| return new OutlineNode(rtrim($title) ?: null, $tags, $steps, $examples, $keyword, $line); | ||
| } | ||
|
|
||
| return new OutlineNode(rtrim($title) ?: null, $tags, $steps, $examples, $keyword, $line); | ||
| return new ScenarioNode(rtrim($title) ?: null, $tags, $steps, $keyword, $line); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note that at the moment calling ->getKeyword() on the parsed node returns a hardcoded result. So if the feature file used Scenario: but had an examples table then e.g. Behat's pretty printer would still print Scenario Outline:.
We could add a new property into the nodes to record the actual keyword that we found, but I would be a little nervous about BC impact if the result of OutlineNode->getKeyword() can vary in case anyone is matching on / doing anything specific with that string.
|
|
||
| $this->expectExceptionObject( | ||
| new ParserException('Expected Scenario, Outline or Background, but got Step on line: 6') | ||
| new ParserException('Step can not be tagged, but it is on line: 6') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this new exception is clearer than the previous anyway. I have left the assertion as ParserException (rather than change to the actual UnexpectedTaggedNodeException) to prove that this is all still BC.
| } catch (ParserException $e) { | ||
| // expected - features cannot end with tags | ||
| $this->assertSame('Unexpected end of file after tags on line 5', $e->getMessage()); | ||
| $this->assertSame('Unexpected end of file after tags on line: 5', $e->getMessage()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Slight change to the format for consistency with the other exceptions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Approved, just a tiny comment, feel free to ignore it
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should we throw a ParserException if we find steps after an examples table ? currently, the parser seems to allow mixing them AFAICT (unless the token type determination returns a different result for such case).
tests/Cucumber/CompatibilityTest.php
Outdated
| 'descriptions_with_comments.feature' => 'Examples table descriptions not supported', | ||
| 'extra_table_content.feature' => 'Table without right border triggers a ParserException', | ||
| 'incomplete_scenario_outline.feature' => 'Scenario and Scenario outline not yet synonyms', | ||
| 'padded_example.feature' => 'Scenario and Scenario outline not yet synonyms', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what about those 2 that were skipped for the same reason ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
They now fail for unrelated reasons.
incomplete_scenario_outline.featurebecause we throw if an Outline has no Examples table, but cucumber/gherkin does not (tracked in Scenario Outlines inconsistencies #153 and noted in the diff above).padded_example.featurebecause we don't trim table padding (specifically I think) the same as cucumber/gherkin - no issue for this yet (I plan to create one), and it has BC implications.
I'll update the reason descriptions in the testcase
Yeah, probably - it does seem that (as now) we don't detect / fail in that situation. I'll double check what cucumber/gherkin does. |
c8307f7 to
ead1ec8
Compare
We were not previously enforcing that an Examples: table can only be followed by more Examples: tables, comments, whitespace or the end of the Scenario / file. With this commit, that will now cause an UnexpectedParserNodeException. I have refactored the order of checking whether we can accept a particular node when parsing a scenario / outline body to make the logic easier. It also slightly changes the message of the exception for existing invalid cases so that they are more accurate about what is expected in the current state.
ead1ec8 to
2a43d41
Compare
|
I checked and I have added a test for that and updated the Parser to detect and throw in this situation. |
I suggest contributing such a test for bad data to the cucumber testsuite as well, to make it part of the shared tests in the future. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@acoulton looking great overall, just a small comment
src/Parser.php
Outdated
| * | ||
| * @throws UnexpectedTaggedNodeException if there is not a taggable node | ||
| */ | ||
| private function predictNextTaggedNodeType(): string |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This function actually does two things: validates that the next tagged node is correct and returns it. But the function name does not reflect the validation part, so when we call it and don't use the return value it looks weird. Also predict sounds a bit magic in my head 😆 . What about validateAndGetNextTaggedNodeType()?
|
@carlos-granados good catch, thanks. The method started off just looking ahead, and I named it |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All good now
In cucumber/gherkin, the
Scenario:andScenario Outline:keywords are interchangeable. Either may have (or not have) anExamples:table.We still need to differentiate between
OutlineNodeandScenarioNodein the parsed result, for BC and because Behat treats these differently at runtime.However, we can safely parse a
Scenario:that contains anExamples:table (which would have previously been invalid) to anOutlineNode- exactly as if the user had writtenScenario Outline:.This does require refactoring how we parse tags, because:
Therefore, we now scan ahead to predict which node the tags are attached to. This also allows us to throw a clearer exception if tags are found in unexpected places in the document.
Relates to #153