scenarioPosition;
private final int lineNumber;
public GherkinScenario(Builder builder) {
@@ -22,8 +23,118 @@ public GherkinScenario(Builder builder) {
this.examples = builder.examples.isEmpty() ? Optional.empty()
: Optional.of(builder.examples);
this.lineNumber = builder.lineNumber;
+ this.scenarioPosition = builder.scenarioPosition;
}
+ /**
+ * A specification of the hierarchical position this scenario appeared in with relation to other scenarios.
+ *
+ * The ruleIndex specifies which rule the scenario was found. [e.g. which rule it was found in]
+ * If the test was not nested in a rule the value will be 0
+ *
+ * The ordinal specifies which order this test showed up in relation to the others in the same rule index.
+ *
+ * The testIndex specifies is the test was derived from an examples table. If it was, it is the
+ * nth row it appeared in starting from one. If it was not in an examples table the number will be 0.
+ *
+ * This encoding CANNOT guarantee whether an arbitrary scenario was declared before another as it is possible
+ * to intersperse rule nodes between root level scenarios. If you want to know the prceise order the test
+ * was declared in the original source file use #getLineNumber() and compare using the
+ * {@see com.pdsl.testcases.DefaultTestCase.PdslTestCaseComparator}
+ *
+ * For example:
+ *
+ *
+ * {@code
+ * Feature:
+ * Scenario:
+ * # First part is "0" because it is a root node. Last part is "0" because it is not in a table.
+ * Then this group ordinal is 0.1.0
+ * Scenario:
+ * Then this group ordinal is 0.2.0 # The second test in the root, se we increment to 2.
+ * Scenario:
+ * Then this group ordinal is
+ * Examples:
+ * |ORDINAL|
+ * | 0.3.1 | # Second test in the root, so we increment the second value to 2
+ * | 0.3.2 | # Increment last digit as it comes from the same group
+ * | 0.3.3 |
+ *
+ * Rule: First rule (1)
+ * Scenario:
+ * Then this group ordinal is 1.1.0
+ * Scenario:
+ * Then this group ordinal is 1.2.0
+ * Scenario:
+ * Then this group oridinal is
+ * Examples:
+ * |ORDINAL|
+ * | 1.3.1 |
+ * | 1.3.2 |
+ *
+ * # Multi-tables continue from the index used in the last table
+ * Examples:
+ * |ORDINAL|
+ * | 1.3.3 |
+ * | 1.3.4
+ *
+ * Scenario:
+ * Then this group ordinal is 0.4.0 # Note we're back at root and continue from the last testPosition
+ *
+ * Rule: Second rule (2)
+ * Scenario:
+ * Then this group ordinal is 2.1.0
+ * }
+ *
+ *
+ * @param ruleIndex the nth rule this scenario was derived from, 0 if not in a rule
+ * @param ordinal the nth position of this scenario relative to others in the same depth
+ * @param testIndex 0 if not derived from an examples table, otherwise the nth row starting from 1
+ */
+ public record ScenarioPosition(int ruleIndex, int ordinal, int testIndex) implements Comparable {
+
+ public static String RULE_INDEX= "ruleIndex";
+ public static String ORDINAL = "ordinal";
+ public static String TABLE_INDEX = "tableIndex";
+ private static final ScenarioPositionComparator SINGLETON = new ScenarioPositionComparator();
+
+ private static class ScenarioPositionComparator implements Comparator {
+ @Override
+ public int compare(ScenarioPosition p1, ScenarioPosition p2) {
+ if (p1.ruleIndex != p2.ruleIndex) {
+ return Integer.compare(p1.ruleIndex, p2.ruleIndex);
+ }
+ if (p1.ordinal != p2.ordinal) {
+ return Integer.compare(p1.ordinal, p2.ordinal);
+ }
+ return Integer.compare(p1.testIndex, p2.testIndex);
+ }
+ }
+
+ @Override
+ public int compareTo(ScenarioPosition scenarioPosition) {
+ return SINGLETON.compare(this, scenarioPosition);
+ }
+
+ public static Optional from(URI uri) {
+ Map params = Arrays.stream(uri.getQuery().split("&"))
+ .map(param -> param.split("="))
+ .filter(arr -> arr.length == 2)
+ .collect(Collectors.toMap(arr -> arr[0], arr -> arr[1]));
+ try {
+ int ruleIndex = Integer.parseInt(params.get(RULE_INDEX));
+ int ordinal = Integer.parseInt(params.get(ORDINAL));
+ int tableIndex = Integer.parseInt(params.get(TABLE_INDEX));
+ return Optional.of(new ScenarioPosition(ruleIndex, ordinal, tableIndex));
+ } catch(RuntimeException e) {
+ return Optional.empty();
+ }
+ }
+ }
+
+
+ public Optional getScenarioPositition() { return scenarioPosition; }
+
public Optional> getTags() {
return tags;
}
@@ -55,11 +166,17 @@ public static class Builder {
private String longDescription = "";
private Optional> stepsList = Optional.empty();
private int lineNumber = -1;
+ private Optional scenarioPosition = Optional.empty();
public GherkinScenario build() {
return new GherkinScenario(this);
}
+ public Builder withScenarioPosition(ScenarioPosition scenarioPosition) {
+ this.scenarioPosition = Optional.ofNullable(scenarioPosition);
+ return this;
+ }
+
public Builder addExamples(GherkinExamplesTable examples) {
this.examples.add(examples);
return this;
diff --git a/src/main/java/com/pdsl/gherkin/testcases/GherkinTestCaseSpecification.java b/src/main/java/com/pdsl/gherkin/testcases/GherkinTestCaseSpecification.java
deleted file mode 100644
index 93046d6..0000000
--- a/src/main/java/com/pdsl/gherkin/testcases/GherkinTestCaseSpecification.java
+++ /dev/null
@@ -1,61 +0,0 @@
-package com.pdsl.gherkin.testcases;
-
-import com.pdsl.specifications.DefaultTestSpecification;
-import com.pdsl.specifications.FilteredPhrase;
-import com.pdsl.specifications.TaggedTestSpecification;
-import com.pdsl.specifications.TestSpecification;
-
-import java.io.InputStream;
-import java.net.URI;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Optional;
-import java.util.Set;
-
-public class GherkinTestCaseSpecification implements TaggedTestSpecification {
-
- private final Set tags;
- private final TestSpecification testSpecification;
-
- public GherkinTestCaseSpecification(Set tags, TestSpecification testSpecification) {
- this.testSpecification = testSpecification;
- this.tags = tags;
- }
-
- public GherkinTestCaseSpecification(List childFeatures, URI originalTestResource) {
- this.testSpecification = new DefaultTestSpecification.Builder("Gherkin Test Container", originalTestResource)
- .withChildTestSpecifications(new ArrayList<>(childFeatures))
- .build();
- this.tags = Set.of();
- }
-
- @Override
- public Set getTags() {
- return tags;
- }
-
- @Override
- public Optional getMetaData() {
- return testSpecification.getMetaData();
- }
-
- @Override
- public Optional> nestedTestSpecifications() {
- return testSpecification.nestedTestSpecifications();
- }
-
- @Override
- public String getName() {
- return testSpecification.getName();
- }
-
- @Override
- public Optional> getFilteredPhrases() {
- return testSpecification.getFilteredPhrases();
- }
-
- @Override
- public URI getOriginalTestResource() {
- return testSpecification.getOriginalTestResource();
- }
-}
diff --git a/src/main/java/com/pdsl/testcases/DefaultPdslTestCase.java b/src/main/java/com/pdsl/testcases/DefaultPdslTestCase.java
index ec274e6..2b602ad 100644
--- a/src/main/java/com/pdsl/testcases/DefaultPdslTestCase.java
+++ b/src/main/java/com/pdsl/testcases/DefaultPdslTestCase.java
@@ -110,12 +110,12 @@ public List getFilteredPhrases() {
public static class PdslTestCaseComparator implements Comparator {
private static final int NUMBER_INDEX = "line=".length();
+
@Override
public int compare(TestCase source1, TestCase source2) {
- int compareUris = source1.getOriginalSource().getRawSchemeSpecificPart().compareTo(
- source2.getOriginalSource().getRawSchemeSpecificPart());
+ int compareUris = source1.getOriginalSource().getPath().compareTo(
+ source2.getOriginalSource().getPath());
// If the scenarios came from the same file have the most recent scenario first via line number
-
if (compareUris == 0) {
String fragment1 = source1.getOriginalSource().getFragment();
String fragment2 = source2.getOriginalSource().getFragment();
diff --git a/src/test/java/com/pdsl/api/ScenarioPositionTest.java b/src/test/java/com/pdsl/api/ScenarioPositionTest.java
new file mode 100644
index 0000000..4ed8b5c
--- /dev/null
+++ b/src/test/java/com/pdsl/api/ScenarioPositionTest.java
@@ -0,0 +1,122 @@
+package com.pdsl.api;
+
+import com.pdsl.executors.DefaultPolymorphicDslTestExecutor;
+import com.pdsl.executors.PolymorphicDslTestExecutor;
+import com.pdsl.gherkin.DefaultGherkinTestSpecificationFactory;
+import com.pdsl.gherkin.models.GherkinScenario;
+import com.pdsl.grammars.AllGrammarsLexer;
+import com.pdsl.grammars.AllGrammarsParser;
+import com.pdsl.reports.DefaultTestResult;
+import com.pdsl.reports.PolymorphicDslTestRunResults;
+import com.pdsl.reports.proto.TechnicalReportData;
+import com.pdsl.specifications.TestSpecification;
+import com.pdsl.testcases.PreorderTestCaseFactory;
+import com.pdsl.testcases.TestCase;
+import com.pdsl.transformers.DefaultPolymorphicDslPhraseFilter;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+import java.io.OutputStream;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.util.*;
+
+import static com.google.common.truth.Truth.assertThat;
+
+public class ScenarioPositionTest {
+
+
+
+ private static final DefaultGherkinTestSpecificationFactory specificationFactory = new DefaultGherkinTestSpecificationFactory.Builder(
+ new DefaultPolymorphicDslPhraseFilter(AllGrammarsParser.class, AllGrammarsLexer.class)).build();
+ private static final PolymorphicDslTestExecutor executor = DefaultPolymorphicDslTestExecutor
+ .ofWithoutDuplicateFiltering(List.of());
+ private static final PreorderTestCaseFactory testCaseFactory = new PreorderTestCaseFactory();
+
+ private static URI getURI(URL url) throws URISyntaxException {
+ return Objects.requireNonNull(url).toURI();
+ }
+
+ @Test
+ public void veryLongResultsLocationInformation_groupedProperlyInNavigableSet() throws URISyntaxException {
+ // ARRANGE
+ final URI complexBackground = getURI(getClass().getClassLoader()
+ .getResource("testdata/good/very_long.feature"));
+ Collection specs = specificationFactory.getTestSpecifications(Set.of(complexBackground))
+ .orElseThrow();
+ Collection testCases = testCaseFactory.processTestSpecification(specs);
+ PolymorphicDslTestRunResults results = new PolymorphicDslTestRunResults(List.of(Mockito.mock(OutputStream.class)), "API");
+
+ // ACT
+ testCases.forEach(tc ->
+ results.addTestResult(new DefaultTestResult(1, tc, TechnicalReportData.Status.PASSED)));
+ List positions = results.getTestResults().stream()
+ .map(r -> r.getTestCase().getOriginalSource())
+ .sorted(new Comparator(){
+
+ @Override
+ public int compare(URI u1, URI u2) {
+ return u1.getFragment().split("=")[1].compareTo(u2.getFragment().split("=")[1]);
+ }
+ })
+ .map(GherkinScenario.ScenarioPosition::from)
+ .map(Optional::orElseThrow)
+ .toList();
+ // ASSERT
+
+ for (int i=1; i <= positions.size(); i++) {
+ GherkinScenario.ScenarioPosition position = positions.get(i - 1);
+ assertThat(position.ruleIndex()).isEqualTo(0);
+ assertThat(position.ordinal()).isEqualTo(i);
+ assertThat(position.testIndex()).isEqualTo(0);
+ }
+
+ }
+
+ @Test
+ public void featureTreeStructure_groupedProperlyInNavigableSet() throws URISyntaxException {
+ // ARRANGE
+ final URI complexBackground = getURI(getClass().getClassLoader()
+ .getResource("framework_specifications/features/ScenarioPositionData.feature"));
+ Collection specs = specificationFactory.getTestSpecifications(Set.of(complexBackground))
+ .orElseThrow();
+ Collection testCases = testCaseFactory.processTestSpecification(specs);
+ PolymorphicDslTestRunResults results = new PolymorphicDslTestRunResults(List.of(Mockito.mock(OutputStream.class)), "API");
+ testCases.forEach(tc ->
+ results.addTestResult(new DefaultTestResult(1, tc, TechnicalReportData.Status.PASSED)));
+ List actualPositions = results.getTestResults().stream()
+ .map(r -> r.getTestCase().getOriginalSource())
+ .sorted(new Comparator(){
+
+ @Override
+ public int compare(URI u1, URI u2) {
+ return u1.getFragment().split("=")[1].compareTo(u2.getFragment().split("=")[1]);
+ }
+ })
+ .map(GherkinScenario.ScenarioPosition::from)
+ .map(Optional::orElseThrow)
+ .toList();
+ // ASSERT
+ List expectedPositions = List.of(
+ new GherkinScenario.ScenarioPosition(0,1,0),
+ new GherkinScenario.ScenarioPosition(0,2,0),
+ new GherkinScenario.ScenarioPosition(0,3,1),
+ new GherkinScenario.ScenarioPosition(0,3,2),
+ new GherkinScenario.ScenarioPosition(0,4,0),
+ new GherkinScenario.ScenarioPosition(0,5,1),
+ new GherkinScenario.ScenarioPosition(0,5,2),
+ new GherkinScenario.ScenarioPosition(0,5,3),
+ new GherkinScenario.ScenarioPosition(0,5,4),
+ new GherkinScenario.ScenarioPosition(1,1,0),
+ new GherkinScenario.ScenarioPosition(2,1,1),
+ new GherkinScenario.ScenarioPosition(2,1,2)
+ );
+
+ assertThat(actualPositions.size()).isEqualTo(expectedPositions.size());
+ for (int i=0; i < expectedPositions.size(); i++) {
+ assertThat(actualPositions.get(i)).isEqualTo(expectedPositions.get(i));
+ }
+ }
+
+}
diff --git a/src/test/resources/framework_specifications/features/ScenarioPositionData.feature b/src/test/resources/framework_specifications/features/ScenarioPositionData.feature
new file mode 100644
index 0000000..3eea23a
--- /dev/null
+++ b/src/test/resources/framework_specifications/features/ScenarioPositionData.feature
@@ -0,0 +1,56 @@
+Feature: Scenario Locations
+ Some test frameworks behave better when the test cases are grouped together based on how they originally showed
+ up in the feature file. Some PDSL implementations support a "ScenarioPostion" which provide:
+ - Rule Index: The nth rule it was assotiated with [0 if not part of any rule]
+ - Ordinal: The nth test case in the rule index [Always greater than or equal to 1]
+ - Table Index: The nth test case from examples tables [0 if there was no table]
+
+ The position is notated as ..
+ Examples of proper usage of this notation are shown in the below tests.
+
+
+ Scenario:
+ Then the position is 0.1.0
+
+ Scenario:
+ Then the position is 0.2.0
+
+ Scenario Outline: :
+ Then the position is
+
+ Examples:
+ |POSITION|
+ | 0.3.1 |
+ | 0.3.2 |
+
+ Scenario: Position counter continues from the same depth
+ Then the position is 0.4.0
+
+ Scenario Outline:
+ Then the postion is
+
+ Examples:
+ |POSITION|
+ | 0.5.1 |
+ | 0.5.2 |
+
+ Examples:
+ |POSITION|
+ | 0.5.3 |
+ | 0.5.4 |
+
+
+ Rule:
+ Scenario:
+ Then the position is 1.1.0
+
+ Rule:
+ Scenario:
+ Then the position is
+ Examples:
+ |POSITION|
+ | 2.1.1 |
+
+ Examples:
+ | POSITION |
+ | 2.1.2 |