Skip to content

Commit 437862e

Browse files
Merge pull request #130 from google/scenario-positions
Scenario positions
2 parents a0dd1cb + 2d82006 commit 437862e

File tree

9 files changed

+358
-82
lines changed

9 files changed

+358
-82
lines changed

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
<groupId>com.google.pdsl</groupId>
77
<artifactId>pdsl</artifactId>
8-
<version>1.9.0</version>
8+
<version>1.10.0</version>
99

1010
<name>pdsl</name>
1111
<url>http://www.github.com/google/polymorphicDSL</url>

src/main/java/com/pdsl/gherkin/DefaultGherkinTestSpecificationFactory.java

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@
44
import com.pdsl.exceptions.SentenceNotFoundException;
55
import com.pdsl.gherkin.filter.GherkinTagsVisitorImpl;
66
import com.pdsl.gherkin.models.GherkinBackground;
7+
import com.pdsl.gherkin.models.GherkinScenario;
78
import com.pdsl.gherkin.models.GherkinStep;
89
import com.pdsl.gherkin.specifications.GherkinTestSpecification;
910
import com.pdsl.gherkin.specifications.GherkinTestSpecificationFactory;
10-
import com.pdsl.gherkin.testcases.GherkinTestCaseSpecification;
1111
import com.pdsl.logging.AnsiTerminalColorHelper;
1212
import com.pdsl.runners.PdslTest;
1313
import com.pdsl.runners.RecognizedBy;
@@ -24,6 +24,7 @@
2424
import java.io.InputStream;
2525
import java.io.OutputStream;
2626
import java.net.URI;
27+
import java.net.URISyntaxException;
2728
import java.nio.charset.Charset;
2829
import java.util.ArrayList;
2930
import java.util.Collection;
@@ -230,7 +231,7 @@ public Optional<Collection<TestSpecification>> getTestSpecifications(Set<URI> te
230231
featureBuilder.withMetaData(
231232
new ByteArrayInputStream(featureMetaData.toByteArray()));
232233
} catch(IOException e){
233-
//TODO Y
234+
throw new IllegalStateException("There was an issue processing a feature file!", e);
234235
}
235236

236237
List<TestSpecification> pickles = getGherkinStepSpecificationScenarios(pickleJar.getScenarios(),
@@ -241,18 +242,38 @@ public Optional<Collection<TestSpecification>> getTestSpecifications(Set<URI> te
241242
pickles.addAll(transformRulesToTestSpecifications(pickleJar.getRules(), pickleJar.getLocation()));
242243
}
243244
featureBuilder.withChildTestSpecifications(pickles);
244-
featureTestSpecifications.add(new GherkinTestCaseSpecification(allTagsForTestCase, featureBuilder.build()));
245+
featureTestSpecifications.add(new GherkinTestSpecification(featureBuilder.build(), allTagsForTestCase));
245246
}
246247
return Optional.of(featureTestSpecifications);
247248
}
248249

249250
private List<GherkinTestSpecification> transformScenariosToTestSpecifications(List<PickleJar.PickleJarScenario> scenarios, Set<String> parentTags, URI originalSourceLocation) {
250251
List<GherkinTestSpecification> gherkinTestSpecifications = new ArrayList<>();
251252
for (PickleJar.PickleJarScenario pickleJarScenario : scenarios) {
253+
var position = pickleJarScenario.getScenarioPosition();
254+
URI processedUri = null;
255+
try {
256+
processedUri = new URI(
257+
originalSourceLocation.getScheme(),
258+
originalSourceLocation.getRawUserInfo(), // Use raw to preserve encoding
259+
originalSourceLocation.getHost(),
260+
originalSourceLocation.getPort(),
261+
originalSourceLocation.getRawPath(), // Use raw to preserve encoding
262+
// Provide the line number using the rfc 5147 standard for 'text/plain'
263+
// Also provide positional data as params so that test frameworks can group them together
264+
// Preserve existing fragment
265+
String.format("%s=%d&%s=%d&%s=%d",
266+
GherkinScenario.ScenarioPosition.RULE_INDEX, position.ruleIndex(),
267+
GherkinScenario.ScenarioPosition.ORDINAL, position.ordinal(),
268+
GherkinScenario.ScenarioPosition.TABLE_INDEX, position.testIndex()), // Use the query string to provide position information
269+
String.format("line=%d", pickleJarScenario.getLineNumber()));
270+
} catch (URISyntaxException e) {
271+
processedUri = originalSourceLocation;
272+
}
252273
DefaultTestSpecification.Builder topLevelScenario = new DefaultTestSpecification.Builder(
253-
pickleJarScenario.getTitleWithSubstitutions(),
254-
// Provide the line number using the rfc 5147 standard for 'text/plain'
255-
originalSourceLocation.resolve("#line=" + pickleJarScenario.getLineNumber()));
274+
pickleJarScenario.getTitleWithSubstitutions(), processedUri
275+
276+
);
256277
// Provide metadata
257278
topLevelScenario.withMetaData(new ByteArrayInputStream(extractMetaData(pickleJarScenario).toByteArray()));
258279
// Process step body
@@ -316,7 +337,9 @@ private Optional<TestSpecification> processStepBody(String title, List<String> s
316337

317338
private List<TestSpecification> transformRulesToTestSpecifications(List<PickleJar.PickleJarRule> rules, URI originalSourceLocation) {
318339
List<TestSpecification> testSpecifications = new ArrayList<>();
340+
int ruleIndex = 0;
319341
for (PickleJar.PickleJarRule rule : rules) {
342+
ruleIndex++;
320343
DefaultTestSpecification.Builder ruleBuilder = new DefaultTestSpecification.Builder(rule.getTitle(),originalSourceLocation);
321344
ByteArrayOutputStream ruleMetaData = new ByteArrayOutputStream();
322345
if (rule.getLongDescription().isPresent()) {
@@ -327,7 +350,7 @@ private List<TestSpecification> transformRulesToTestSpecifications(List<PickleJa
327350
// Nest the scenarios in a background TestSpecification
328351
GherkinBackground bg = rule.getBackground().get();
329352
addBytesWithCorrectEncoding(ruleMetaData, getBackgroundText(bg));
330-
logger.debug(String.format("%sRule Background%s in %s", AnsiTerminalColorHelper.CYAN, AnsiTerminalColorHelper.RESET, rule.getTitle()));
353+
logger.debug("{}Rule Background{} in {}", AnsiTerminalColorHelper.CYAN, AnsiTerminalColorHelper.RESET, rule.getTitle());
331354
Optional<List<FilteredPhrase>> filteredBackgroundStepBody = processStepBodyContent(bg.getSteps().orElseThrow());
332355
filteredBackgroundStepBody.ifPresent(ruleBuilder::withTestPhrases);
333356
}

src/main/java/com/pdsl/gherkin/PickleJar.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import com.google.common.base.Preconditions;
44
import com.pdsl.gherkin.models.GherkinBackground;
5+
import com.pdsl.gherkin.models.GherkinScenario;
56

67
import java.net.URI;
78
import java.util.*;
@@ -201,16 +202,23 @@ static class PickleJarScenario {
201202
private final List<String> stepsWithParameterSubstitutionsIfNeeded;
202203
private Optional<Set<String>> tags;
203204
private final int lineNumber;
205+
private final GherkinScenario.ScenarioPosition scenarioPosition;
206+
204207
private PickleJarScenario(Builder builder) {
205208
this.tags = builder.tags;
206209
this.longDescription = builder.longDescription;
207210
this.tags = builder.tags;
208211
this.lineNumber = builder.lineNumber;
209212
this.scenarioTitleWithParameterSubstitutionsIfNeeded = builder.titleWithSubstitutions;
210213
this.stepsWithParameterSubstitutionsIfNeeded = builder.stepsWithSubstitutions;
214+
this.scenarioPosition = builder.scenarioPosition.orElseThrow();
211215
}
212216

213217
public int getLineNumber() { return lineNumber; }
218+
219+
public GherkinScenario.ScenarioPosition getScenarioPosition() {
220+
return scenarioPosition;
221+
}
214222
public Optional<Set<String>> getTags() {
215223
return tags;
216224
}
@@ -233,6 +241,7 @@ public static class Builder {
233241
private Optional<Set<String>> tags = Optional.empty();
234242
private Optional<String> longDescription = Optional.empty();
235243
private int lineNumber = -1;
244+
private Optional<GherkinScenario.ScenarioPosition> scenarioPosition;
236245

237246
public Builder(String titleWithSubstitutions, List<String> stepsWithSubstitutions) {
238247
this.titleWithSubstitutions = titleWithSubstitutions;
@@ -248,6 +257,11 @@ public Builder withLineNumber(int lineNumber) {
248257
return this;
249258
}
250259

260+
public Builder withScenarioPosition(int depth, int ordinal, int tableIndex) {
261+
this.scenarioPosition = Optional.of(new GherkinScenario.ScenarioPosition(depth, ordinal, tableIndex));
262+
return this;
263+
}
264+
251265
public Builder withLongDescription(String longDescription) {
252266
this.longDescription = Optional.ofNullable(longDescription);
253267
return this;

src/main/java/com/pdsl/gherkin/PickleJarFactory.java

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ public List<PickleJar> getPickleJars(Set<URI> testResources) {
8484
// Top level scenarios
8585
if (feature.getOptionalGherkinScenarios().isPresent()) {
8686
List<PickleJar.PickleJarScenario> topLevelPickles =
87-
convertScenariosToPickleJarScenarios(feature.getOptionalGherkinScenarios().get());
87+
convertScenariosToPickleJarScenarios(feature.getOptionalGherkinScenarios().get(), 0);
8888
pickleJarBuilder.withFeatureLevelScenarios(topLevelPickles);
8989
}
9090
// Add all rules
@@ -97,15 +97,18 @@ public List<PickleJar> getPickleJars(Set<URI> testResources) {
9797
}
9898

9999

100-
private List<PickleJar.PickleJarScenario> convertScenariosToPickleJarScenarios(List<GherkinScenario> scenarios) {
100+
private List<PickleJar.PickleJarScenario> convertScenariosToPickleJarScenarios(List<GherkinScenario> scenarios, int depth) {
101+
int nextOrdinal = 0;
101102
List<PickleJar.PickleJarScenario> pickleJarScenarios = new ArrayList<>();
102103
for (GherkinScenario scenario : scenarios) {
104+
nextOrdinal++;
103105
// If the scenario has an examples table the tags will need to be combined with the scenario level
104106
Set<String> tags = new HashSet<>();
105107
if (scenario.getTags().isPresent()) {
106108
tags.addAll(processTags(scenario.getTags().get()));
107109
}
108110
if (scenario.getExamples().isPresent()) {
111+
int tableIndex = 1;
109112
for (GherkinExamplesTable table : scenario.getExamples().get()) {
110113
Set<String> tableTags = new HashSet<>(tags);
111114
for (Map<String, GherkinExamplesTable.CellOfExamplesTable> substitutions : table.getRowsWithCell()) {
@@ -120,7 +123,8 @@ private List<PickleJar.PickleJarScenario> convertScenariosToPickleJarScenarios(L
120123
scenario.getTitle().orElseThrow().getStringWithSubstitutions(substitutionsAsStrings),
121124
substitutedSteps)
122125
// All parameters should have the same line number, so just get the number from the first one
123-
.withLineNumber(substitutions.values().stream().findFirst().orElseThrow().lineNumber());
126+
.withLineNumber(substitutions.values().stream().findFirst().orElseThrow().lineNumber())
127+
.withScenarioPosition(depth, nextOrdinal, tableIndex++);
124128
if (scenario.getLongDescription().isPresent()) {
125129
builder.withLongDescription(scenario.getLongDescription().get().getStringWithSubstitutions(substitutionsAsStrings));
126130
}
@@ -147,7 +151,8 @@ private List<PickleJar.PickleJarScenario> convertScenariosToPickleJarScenarios(L
147151
PickleJar.PickleJarScenario.Builder builder = new PickleJar.PickleJarScenario.Builder(
148152
scenario.getTitle().orElseThrow().getRawString(),
149153
stepBody)
150-
.withLineNumber(scenario.getLineNumber());
154+
.withLineNumber(scenario.getLineNumber())
155+
.withScenarioPosition(depth,nextOrdinal, 0);
151156
if (!tags.isEmpty()) {
152157
builder.withTags(processTags(tags));
153158
}
@@ -163,9 +168,9 @@ private List<PickleJar.PickleJarScenario> convertScenariosToPickleJarScenarios(L
163168

164169
private List<PickleJar.PickleJarRule> convertRulesToPickles(List<GherkinRule> rules) {
165170
List<PickleJar.PickleJarRule> pickleJarRules = new ArrayList<>();
166-
for (GherkinRule rule : rules) {
167-
168-
List<PickleJar.PickleJarScenario> scenarios = convertScenariosToPickleJarScenarios(rule.getScenarios().orElseThrow());
171+
for (int i=0; i < rules.size(); i++) {
172+
GherkinRule rule = rules.get(i);
173+
List<PickleJar.PickleJarScenario> scenarios = convertScenariosToPickleJarScenarios(rule.getScenarios().orElseThrow(), i+1);
169174
PickleJar.PickleJarRule.Builder builder = new PickleJar.PickleJarRule.Builder(rule.getTitle().orElseThrow(), scenarios);
170175
if (rule.getBackground().isPresent()) {
171176
builder.withBackground(rule.getBackground().get());

src/main/java/com/pdsl/gherkin/models/GherkinScenario.java

Lines changed: 120 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
package com.pdsl.gherkin.models;
22

3-
import java.util.ArrayList;
4-
import java.util.List;
5-
import java.util.Optional;
3+
import java.net.URI;
4+
import java.util.*;
5+
import java.util.stream.Collectors;
66

77
public class GherkinScenario {
88
private final Optional<List<String>> tags;
99
private final Optional<GherkinString> title;
1010
private final Optional<GherkinString> longDescription;
1111
private final Optional<List<GherkinStep>> stepsList;
1212
private final Optional<List<GherkinExamplesTable>> examples;
13+
private final Optional<ScenarioPosition> scenarioPosition;
1314
private final int lineNumber;
1415

1516
public GherkinScenario(Builder builder) {
@@ -22,8 +23,118 @@ public GherkinScenario(Builder builder) {
2223
this.examples = builder.examples.isEmpty() ? Optional.empty()
2324
: Optional.of(builder.examples);
2425
this.lineNumber = builder.lineNumber;
26+
this.scenarioPosition = builder.scenarioPosition;
2527
}
2628

29+
/**
30+
* A specification of the hierarchical position this scenario appeared in with relation to other scenarios.
31+
* <p>
32+
* The ruleIndex specifies which rule the scenario was found. [e.g. which rule it was found in]
33+
* If the test was not nested in a rule the value will be 0
34+
* <p>
35+
* The ordinal specifies which order this test showed up in relation to the others in the same rule index.
36+
* <p>
37+
* The testIndex specifies is the test was derived from an examples table. If it was, it is the
38+
* nth row it appeared in starting from one. If it was not in an examples table the number will be 0.
39+
* <p>
40+
* This encoding CANNOT guarantee whether an arbitrary scenario was declared before another as it is possible
41+
* to intersperse rule nodes between root level scenarios. If you want to know the prceise order the test
42+
* was declared in the original source file use #getLineNumber() and compare using the
43+
* {@see com.pdsl.testcases.DefaultTestCase.PdslTestCaseComparator}
44+
* <p>
45+
* For example:
46+
* <p>
47+
* <pre>
48+
* {@code
49+
* Feature:
50+
* Scenario:
51+
* # First part is "0" because it is a root node. Last part is "0" because it is not in a table.
52+
* Then this group ordinal is 0.1.0
53+
* Scenario:
54+
* Then this group ordinal is 0.2.0 # The second test in the root, se we increment to 2.
55+
* Scenario:
56+
* Then this group ordinal is <ORDINAL>
57+
* Examples:
58+
* |ORDINAL|
59+
* | 0.3.1 | # Second test in the root, so we increment the second value to 2
60+
* | 0.3.2 | # Increment last digit as it comes from the same group
61+
* | 0.3.3 |
62+
*
63+
* Rule: First rule (1)
64+
* Scenario:
65+
* Then this group ordinal is 1.1.0
66+
* Scenario:
67+
* Then this group ordinal is 1.2.0
68+
* Scenario:
69+
* Then this group oridinal is <ORDINAL>
70+
* Examples:
71+
* |ORDINAL|
72+
* | 1.3.1 |
73+
* | 1.3.2 |
74+
*
75+
* # Multi-tables continue from the index used in the last table
76+
* Examples:
77+
* |ORDINAL|
78+
* | 1.3.3 |
79+
* | 1.3.4
80+
*
81+
* Scenario:
82+
* Then this group ordinal is 0.4.0 # Note we're back at root and continue from the last testPosition
83+
*
84+
* Rule: Second rule (2)
85+
* Scenario:
86+
* Then this group ordinal is 2.1.0
87+
* }
88+
* </pre>
89+
*
90+
* @param ruleIndex the nth rule this scenario was derived from, 0 if not in a rule
91+
* @param ordinal the nth position of this scenario relative to others in the same depth
92+
* @param testIndex 0 if not derived from an examples table, otherwise the nth row starting from 1
93+
*/
94+
public record ScenarioPosition(int ruleIndex, int ordinal, int testIndex) implements Comparable<ScenarioPosition> {
95+
96+
public static String RULE_INDEX= "ruleIndex";
97+
public static String ORDINAL = "ordinal";
98+
public static String TABLE_INDEX = "tableIndex";
99+
private static final ScenarioPositionComparator SINGLETON = new ScenarioPositionComparator();
100+
101+
private static class ScenarioPositionComparator implements Comparator<ScenarioPosition> {
102+
@Override
103+
public int compare(ScenarioPosition p1, ScenarioPosition p2) {
104+
if (p1.ruleIndex != p2.ruleIndex) {
105+
return Integer.compare(p1.ruleIndex, p2.ruleIndex);
106+
}
107+
if (p1.ordinal != p2.ordinal) {
108+
return Integer.compare(p1.ordinal, p2.ordinal);
109+
}
110+
return Integer.compare(p1.testIndex, p2.testIndex);
111+
}
112+
}
113+
114+
@Override
115+
public int compareTo(ScenarioPosition scenarioPosition) {
116+
return SINGLETON.compare(this, scenarioPosition);
117+
}
118+
119+
public static Optional<ScenarioPosition> from(URI uri) {
120+
Map<String, String> params = Arrays.stream(uri.getQuery().split("&"))
121+
.map(param -> param.split("="))
122+
.filter(arr -> arr.length == 2)
123+
.collect(Collectors.toMap(arr -> arr[0], arr -> arr[1]));
124+
try {
125+
int ruleIndex = Integer.parseInt(params.get(RULE_INDEX));
126+
int ordinal = Integer.parseInt(params.get(ORDINAL));
127+
int tableIndex = Integer.parseInt(params.get(TABLE_INDEX));
128+
return Optional.of(new ScenarioPosition(ruleIndex, ordinal, tableIndex));
129+
} catch(RuntimeException e) {
130+
return Optional.empty();
131+
}
132+
}
133+
}
134+
135+
136+
public Optional<ScenarioPosition> getScenarioPositition() { return scenarioPosition; }
137+
27138
public Optional<List<String>> getTags() {
28139
return tags;
29140
}
@@ -55,11 +166,17 @@ public static class Builder {
55166
private String longDescription = "";
56167
private Optional<List<GherkinStep>> stepsList = Optional.empty();
57168
private int lineNumber = -1;
169+
private Optional<ScenarioPosition> scenarioPosition = Optional.empty();
58170

59171
public GherkinScenario build() {
60172
return new GherkinScenario(this);
61173
}
62174

175+
public Builder withScenarioPosition(ScenarioPosition scenarioPosition) {
176+
this.scenarioPosition = Optional.ofNullable(scenarioPosition);
177+
return this;
178+
}
179+
63180
public Builder addExamples(GherkinExamplesTable examples) {
64181
this.examples.add(examples);
65182
return this;

0 commit comments

Comments
 (0)