From 29199b66db3f33e6ffc801f98bba3a17ca282dc1 Mon Sep 17 00:00:00 2001 From: "M.P. Korstanje" Date: Thu, 8 May 2025 23:02:59 +0200 Subject: [PATCH 01/24] Add Lineage to public API --- .../main/java/io/cucumber/query/Lineage.java | 23 ++++---- .../io/cucumber/query/LineageCollector.java | 45 ---------------- .../io/cucumber/query/LineageReducer.java | 51 ++++++++++++++++-- .../query/LineageReducerAscending.java | 45 ++++++++++++++++ .../query/LineageReducerDescending.java | 34 ++++++------ .../io/cucumber/query/NamingCollector.java | 3 +- .../main/java/io/cucumber/query/Query.java | 52 ++++++++++--------- 7 files changed, 148 insertions(+), 105 deletions(-) delete mode 100644 java/src/main/java/io/cucumber/query/LineageCollector.java create mode 100644 java/src/main/java/io/cucumber/query/LineageReducerAscending.java diff --git a/java/src/main/java/io/cucumber/query/Lineage.java b/java/src/main/java/io/cucumber/query/Lineage.java index 967eec10..37b1e8c3 100644 --- a/java/src/main/java/io/cucumber/query/Lineage.java +++ b/java/src/main/java/io/cucumber/query/Lineage.java @@ -9,7 +9,6 @@ import java.util.Objects; import java.util.Optional; -import java.util.function.Supplier; import static java.util.Objects.requireNonNull; @@ -21,7 +20,7 @@ * * @see LineageReducer */ -class Lineage { +public final class Lineage { private final GherkinDocument document; private final Feature feature; @@ -67,42 +66,38 @@ private Lineage(GherkinDocument document, Feature feature, Rule rule, Scenario s this.exampleIndex = exampleIndex; } - GherkinDocument document() { + public GherkinDocument document() { return document; } - Optional feature() { + public Optional feature() { return Optional.ofNullable(feature); } - Optional rule() { + public Optional rule() { return Optional.ofNullable(rule); } - Optional scenario() { + public Optional scenario() { return Optional.ofNullable(scenario); } - Optional examples() { + public Optional examples() { return Optional.ofNullable(examples); } - Optional example() { + public Optional example() { return Optional.ofNullable(example); } - Optional examplesIndex() { + public Optional examplesIndex() { return Optional.ofNullable(examplesIndex); } - Optional exampleIndex() { + public Optional exampleIndex() { return Optional.ofNullable(exampleIndex); } - LineageReducer reduce(Supplier> collector) { - return new LineageReducerDescending(collector); - } - @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/java/src/main/java/io/cucumber/query/LineageCollector.java b/java/src/main/java/io/cucumber/query/LineageCollector.java deleted file mode 100644 index 19a7553a..00000000 --- a/java/src/main/java/io/cucumber/query/LineageCollector.java +++ /dev/null @@ -1,45 +0,0 @@ -package io.cucumber.query; - -import io.cucumber.messages.types.Examples; -import io.cucumber.messages.types.Feature; -import io.cucumber.messages.types.GherkinDocument; -import io.cucumber.messages.types.Pickle; -import io.cucumber.messages.types.Rule; -import io.cucumber.messages.types.Scenario; -import io.cucumber.messages.types.TableRow; - -/** - * Collect the {@link Lineage} of a - * {@linkplain io.cucumber.messages.types.GherkinDocument GherkinDocument element} - * or {@link Pickle} and reduce it to a single result. - * - * @param the type reduced to. - */ -interface LineageCollector { - default void add(GherkinDocument document) { - - } - - default void add(Feature feature) { - - } - - default void add(Rule rule) { - - } - - default void add(Scenario scenario) { - - } - - default void add(Examples examples, int index) { - } - - default void add(TableRow example, int index) { - } - - default void add(Pickle pickle) { - } - - T finish(); -} diff --git a/java/src/main/java/io/cucumber/query/LineageReducer.java b/java/src/main/java/io/cucumber/query/LineageReducer.java index 96a3cf66..af28d3aa 100644 --- a/java/src/main/java/io/cucumber/query/LineageReducer.java +++ b/java/src/main/java/io/cucumber/query/LineageReducer.java @@ -1,11 +1,15 @@ package io.cucumber.query; +import io.cucumber.messages.types.Examples; +import io.cucumber.messages.types.Feature; +import io.cucumber.messages.types.GherkinDocument; import io.cucumber.messages.types.Pickle; +import io.cucumber.messages.types.Rule; +import io.cucumber.messages.types.Scenario; +import io.cucumber.messages.types.TableRow; import java.util.function.Supplier; -import static java.util.Objects.requireNonNull; - /** * Visit the {@link Lineage} of a {@linkplain io.cucumber.messages.types.GherkinDocument GherkinDocument element} * or {@link Pickle} and reduce it. @@ -17,14 +21,53 @@ * * @param the type reduced to. */ -interface LineageReducer { +public interface LineageReducer { - static LineageReducer descending(Supplier> collector) { + static LineageReducer descending(Supplier> collector) { return new LineageReducerDescending<>(collector); } + + static LineageReducer ascending(Supplier> collector) { + return new LineageReducerAscending<>(collector); + } T reduce(Lineage lineage); T reduce(Lineage lineage, Pickle pickle); + /** + * Collect the {@link Lineage} of a + * {@linkplain io.cucumber.messages.types.GherkinDocument GherkinDocument element} + * or {@link Pickle} and reduce it to a single result. + * + * @param the type reduced to. + */ + interface Collector { + default void add(GherkinDocument document) { + + } + + default void add(Feature feature) { + + } + + default void add(Rule rule) { + + } + + default void add(Scenario scenario) { + + } + + default void add(Examples examples, int index) { + } + + default void add(TableRow example, int index) { + } + + default void add(Pickle pickle) { + } + + T finish(); + } } diff --git a/java/src/main/java/io/cucumber/query/LineageReducerAscending.java b/java/src/main/java/io/cucumber/query/LineageReducerAscending.java new file mode 100644 index 00000000..0e0314bc --- /dev/null +++ b/java/src/main/java/io/cucumber/query/LineageReducerAscending.java @@ -0,0 +1,45 @@ +package io.cucumber.query; + +import io.cucumber.messages.types.Pickle; + +import java.util.function.Supplier; + +import static java.util.Objects.requireNonNull; + +/** + * Reduces the lineage of a Gherkin document element in ascending order. + * + * @param type to which the lineage is reduced. + */ +class LineageReducerAscending implements LineageReducer { + + private final Supplier> collectorSupplier; + + LineageReducerAscending(Supplier> collectorSupplier) { + this.collectorSupplier = requireNonNull(collectorSupplier); + } + + @Override + public T reduce(Lineage lineage) { + Collector collector = collectorSupplier.get(); + reduceAddLineage(collector, lineage); + return collector.finish(); + } + + @Override + public T reduce(Lineage lineage, Pickle pickle) { + Collector collector = collectorSupplier.get(); + collector.add(pickle); + reduceAddLineage(collector, lineage); + return collector.finish(); + } + + private static void reduceAddLineage(Collector reducer, Lineage lineage) { + lineage.example().ifPresent(example -> reducer.add(example, lineage.exampleIndex().orElse(0))); + lineage.examples().ifPresent(examples -> reducer.add(examples, lineage.examplesIndex().orElse(0))); + lineage.scenario().ifPresent(reducer::add); + lineage.rule().ifPresent(reducer::add); + lineage.feature().ifPresent(reducer::add); + reducer.add(lineage.document()); + } +} diff --git a/java/src/main/java/io/cucumber/query/LineageReducerDescending.java b/java/src/main/java/io/cucumber/query/LineageReducerDescending.java index 7a089150..759d0d92 100644 --- a/java/src/main/java/io/cucumber/query/LineageReducerDescending.java +++ b/java/src/main/java/io/cucumber/query/LineageReducerDescending.java @@ -13,33 +13,33 @@ */ class LineageReducerDescending implements LineageReducer { - private final Supplier> reducerSupplier; + private final Supplier> collectorSupplier; - LineageReducerDescending(Supplier> reducerSupplier) { - this.reducerSupplier = requireNonNull(reducerSupplier); + LineageReducerDescending(Supplier> collectorSupplier) { + this.collectorSupplier = requireNonNull(collectorSupplier); } @Override public T reduce(Lineage lineage) { - LineageCollector reducer = reducerSupplier.get(); - reduceAddLineage(reducer, lineage); - return reducer.finish(); + Collector collector = collectorSupplier.get(); + reduceAddLineage(collector, lineage); + return collector.finish(); } @Override public T reduce(Lineage lineage, Pickle pickle) { - LineageCollector reducer = reducerSupplier.get(); - reduceAddLineage(reducer, lineage); - reducer.add(pickle); - return reducer.finish(); + Collector collector = collectorSupplier.get(); + reduceAddLineage(collector, lineage); + collector.add(pickle); + return collector.finish(); } - private static void reduceAddLineage(LineageCollector reducer, Lineage lineage) { - reducer.add(lineage.document()); - lineage.feature().ifPresent(reducer::add); - lineage.rule().ifPresent(reducer::add); - lineage.scenario().ifPresent(reducer::add); - lineage.examples().ifPresent(examples -> reducer.add(examples, lineage.examplesIndex().orElse(0))); - lineage.example().ifPresent(example -> reducer.add(example, lineage.exampleIndex().orElse(0))); + private static void reduceAddLineage(Collector collector, Lineage lineage) { + collector.add(lineage.document()); + lineage.feature().ifPresent(collector::add); + lineage.rule().ifPresent(collector::add); + lineage.scenario().ifPresent(collector::add); + lineage.examples().ifPresent(examples -> collector.add(examples, lineage.examplesIndex().orElse(0))); + lineage.example().ifPresent(example -> collector.add(example, lineage.exampleIndex().orElse(0))); } } diff --git a/java/src/main/java/io/cucumber/query/NamingCollector.java b/java/src/main/java/io/cucumber/query/NamingCollector.java index 1d70e53d..236fb463 100644 --- a/java/src/main/java/io/cucumber/query/NamingCollector.java +++ b/java/src/main/java/io/cucumber/query/NamingCollector.java @@ -6,6 +6,7 @@ import io.cucumber.messages.types.Rule; import io.cucumber.messages.types.Scenario; import io.cucumber.messages.types.TableRow; +import io.cucumber.query.LineageReducer.Collector; import io.cucumber.query.NamingStrategy.ExampleName; import io.cucumber.query.NamingStrategy.FeatureName; import io.cucumber.query.NamingStrategy.Strategy; @@ -24,7 +25,7 @@ * * @see NamingStrategy */ -class NamingCollector implements LineageCollector { +class NamingCollector implements Collector { private final Deque parts = new ArrayDeque<>(); private final CharSequence delimiter = " - "; diff --git a/java/src/main/java/io/cucumber/query/Query.java b/java/src/main/java/io/cucumber/query/Query.java index 8141cb37..a79b1007 100644 --- a/java/src/main/java/io/cucumber/query/Query.java +++ b/java/src/main/java/io/cucumber/query/Query.java @@ -37,6 +37,7 @@ import static java.util.Collections.emptyList; import static java.util.Comparator.comparing; +import static java.util.Comparator.naturalOrder; import static java.util.Comparator.nullsFirst; import static java.util.Objects.requireNonNull; import static java.util.Optional.ofNullable; @@ -115,14 +116,17 @@ public List findAllTestCaseStarted() { public Map, List> findAllTestCaseStartedGroupedByFeature() { return findAllTestCaseStarted() .stream() - .map(testCaseStarted1 -> { - Optional astNodes = findLineageBy(testCaseStarted1); - return new SimpleEntry<>(astNodes, testCaseStarted1); + .map(testCaseStarted -> { + Optional astNodes = findLineageBy(testCaseStarted); + return new SimpleEntry<>(astNodes, testCaseStarted); }) // Sort entries by gherkin document URI for consistent ordering - .sorted(nullsFirst(comparing(entry1 -> entry1.getKey() - .flatMap(nodes -> nodes.document().getUri()) - .orElse(null)))) + .sorted(comparing( + entry -> entry.getKey() + .flatMap(nodes -> nodes.document().getUri()) + .orElse(null), + nullsFirst(naturalOrder()) + )) .map(entry -> { // Unpack the now sorted entries Optional feature = entry.getKey().flatMap(Lineage::feature); @@ -225,53 +229,53 @@ private static Supplier createElementWasNotPartOfThisQ return () -> new IllegalArgumentException("Element was not part of this query object"); } - Optional reduceLinageOf(GherkinDocument element, LineageReducer reducer) { + public Optional reduceLinageOf(GherkinDocument element, LineageReducer reducer) { requireNonNull(element); requireNonNull(reducer); return findLineageBy(element) .map(reducer::reduce); } - Optional reduceLinageOf(Feature element, LineageReducer reducer) { + public Optional reduceLinageOf(Feature element, LineageReducer reducer) { requireNonNull(element); requireNonNull(reducer); return findLineageBy(element) .map(reducer::reduce); } - Optional reduceLinageOf(Rule element, LineageReducer reducer) { + public Optional reduceLinageOf(Rule element, LineageReducer reducer) { requireNonNull(element); requireNonNull(reducer); return findLineageBy(element) .map(reducer::reduce); } - Optional reduceLinageOf(Scenario element, LineageReducer reducer) { + public Optional reduceLinageOf(Scenario element, LineageReducer reducer) { requireNonNull(element); requireNonNull(reducer); return findLineageBy(element) .map(reducer::reduce); } - Optional reduceLinageOf(Examples element, LineageReducer reducer) { + public Optional reduceLinageOf(Examples element, LineageReducer reducer) { requireNonNull(element); requireNonNull(reducer); return findLineageBy(element) .map(reducer::reduce); } - Optional reduceLinageOf(TableRow element, LineageReducer reducer) { + public Optional reduceLinageOf(TableRow element, LineageReducer reducer) { requireNonNull(element); requireNonNull(reducer); return findLineageBy(element) .map(reducer::reduce); } - Optional reduceLinageOf(Pickle element, LineageReducer reducer) { - requireNonNull(element); + public Optional reduceLinageOf(Pickle pickle, LineageReducer reducer) { + requireNonNull(pickle); requireNonNull(reducer); - return findLineageBy(element) - .map(lineage -> reducer.reduce(lineage, element)); + return findLineageBy(pickle) + .map(lineage -> reducer.reduce(lineage, pickle)); } public Optional findPickleBy(TestCaseStarted testCaseStarted) { @@ -368,44 +372,44 @@ public void update(Envelope envelope) { envelope.getAttachment().ifPresent(this::updateAttachment); } - private Optional findLineageBy(GherkinDocument element) { + public Optional findLineageBy(GherkinDocument element) { requireNonNull(element); return Optional.ofNullable(lineageById.get(element.getUri())); } - private Optional findLineageBy(Feature element) { + public Optional findLineageBy(Feature element) { requireNonNull(element); return Optional.ofNullable(lineageById.get(element)); } - private Optional findLineageBy(Rule element) { + public Optional findLineageBy(Rule element) { requireNonNull(element); return Optional.ofNullable(lineageById.get(element.getId())); } - private Optional findLineageBy(Scenario element) { + public Optional findLineageBy(Scenario element) { requireNonNull(element); return Optional.ofNullable(lineageById.get(element.getId())); } - private Optional findLineageBy(Examples element) { + public Optional findLineageBy(Examples element) { requireNonNull(element); return Optional.ofNullable(lineageById.get(element.getId())); } - private Optional findLineageBy(TableRow element) { + public Optional findLineageBy(TableRow element) { requireNonNull(element); return Optional.ofNullable(lineageById.get(element.getId())); } - private Optional findLineageBy(Pickle pickle) { + public Optional findLineageBy(Pickle pickle) { requireNonNull(pickle); List astNodeIds = pickle.getAstNodeIds(); String pickleAstNodeId = astNodeIds.get(astNodeIds.size() - 1); return Optional.ofNullable(lineageById.get(pickleAstNodeId)); } - private Optional findLineageBy(TestCaseStarted testCaseStarted) { + public Optional findLineageBy(TestCaseStarted testCaseStarted) { return findPickleBy(testCaseStarted) .flatMap(this::findLineageBy); } From 15176ed3e51b0026ee642515c96d6eb4d2536628 Mon Sep 17 00:00:00 2001 From: "M.P. Korstanje" Date: Sun, 1 Jun 2025 21:24:56 +0200 Subject: [PATCH 02/24] Add more queries --- .../query/FirstLocationCollector.java | 65 +++++++++++++++++++ .../io/cucumber/query/NamingCollector.java | 3 +- .../main/java/io/cucumber/query/Query.java | 61 +++++++++++++++-- .../java/io/cucumber/query/QueryTest.java | 8 +-- 4 files changed, 127 insertions(+), 10 deletions(-) create mode 100644 java/src/main/java/io/cucumber/query/FirstLocationCollector.java diff --git a/java/src/main/java/io/cucumber/query/FirstLocationCollector.java b/java/src/main/java/io/cucumber/query/FirstLocationCollector.java new file mode 100644 index 00000000..7cb06807 --- /dev/null +++ b/java/src/main/java/io/cucumber/query/FirstLocationCollector.java @@ -0,0 +1,65 @@ +package io.cucumber.query; + +import io.cucumber.messages.types.Examples; +import io.cucumber.messages.types.Feature; +import io.cucumber.messages.types.Location; +import io.cucumber.messages.types.Pickle; +import io.cucumber.messages.types.Rule; +import io.cucumber.messages.types.Scenario; +import io.cucumber.messages.types.TableRow; +import io.cucumber.query.LineageReducer.Collector; + +import java.util.function.Supplier; + +/** + * Finds the first (depending on + * {@link LineageReducer#descending(Supplier)} or + * {@link LineageReducer#ascending(Supplier)}) location of an element in + * a lineage. + * + * @see NamingStrategy + */ +class FirstLocationCollector implements Collector { + + private Location location; + + @Override + public void add(Feature feature) { + if (location == null) { + location = feature.getLocation(); + } + } + + @Override + public void add(Rule rule) { + if (location == null) { + location = rule.getLocation(); + } + } + + @Override + public void add(Scenario scenario) { + if (location == null) { + location = scenario.getLocation(); + } + } + + @Override + public void add(Examples examples, int index) { + if (location == null) { + location = examples.getLocation(); + } + } + + @Override + public void add(TableRow example, int index) { + if (location == null) { + location = example.getLocation(); + } + } + + @Override + public Location finish() { + return location; + } +} diff --git a/java/src/main/java/io/cucumber/query/NamingCollector.java b/java/src/main/java/io/cucumber/query/NamingCollector.java index 236fb463..1fc037f1 100644 --- a/java/src/main/java/io/cucumber/query/NamingCollector.java +++ b/java/src/main/java/io/cucumber/query/NamingCollector.java @@ -27,7 +27,8 @@ */ class NamingCollector implements Collector { - private final Deque parts = new ArrayDeque<>(); + // There are at most 5 levels to a feature file. + private final Deque parts = new ArrayDeque<>(5); private final CharSequence delimiter = " - "; private final Strategy strategy; private final FeatureName featureName; diff --git a/java/src/main/java/io/cucumber/query/Query.java b/java/src/main/java/io/cucumber/query/Query.java index a79b1007..33c25ed4 100644 --- a/java/src/main/java/io/cucumber/query/Query.java +++ b/java/src/main/java/io/cucumber/query/Query.java @@ -7,6 +7,7 @@ import io.cucumber.messages.types.Feature; import io.cucumber.messages.types.GherkinDocument; import io.cucumber.messages.types.Hook; +import io.cucumber.messages.types.Location; import io.cucumber.messages.types.Meta; import io.cucumber.messages.types.Pickle; import io.cucumber.messages.types.PickleStep; @@ -23,6 +24,7 @@ import io.cucumber.messages.types.TestStepFinished; import io.cucumber.messages.types.TestStepResult; import io.cucumber.messages.types.TestStepResultStatus; +import io.cucumber.messages.types.TestStepStarted; import io.cucumber.messages.types.Timestamp; import java.time.Duration; @@ -30,11 +32,11 @@ import java.util.AbstractMap.SimpleEntry; import java.util.Map.Entry; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentLinkedDeque; import java.util.function.BiFunction; import java.util.function.Supplier; import java.util.stream.Collectors; +import static io.cucumber.query.LineageReducer.ascending; import static java.util.Collections.emptyList; import static java.util.Comparator.comparing; import static java.util.Comparator.naturalOrder; @@ -63,7 +65,7 @@ public final class Query { private static final Map ZEROES_BY_TEST_STEP_RESULT_STATUSES = Arrays.stream(TestStepResultStatus.values()) .collect(Collectors.toMap(identity(), (s) -> 0L)); private final Comparator testStepResultComparator = nullsFirst(comparing(o -> o.getStatus().ordinal())); - private final Deque testCaseStarted = new ConcurrentLinkedDeque<>(); + private final Map testCaseStartedById = new ConcurrentHashMap<>(); private final Map testCaseFinishedByTestCaseStartedId = new ConcurrentHashMap<>(); private final Map> testStepsFinishedByTestCaseStartedId = new ConcurrentHashMap<>(); private final Map pickleById = new ConcurrentHashMap<>(); @@ -106,12 +108,32 @@ public List findAllPickleSteps() { } public List findAllTestCaseStarted() { - return this.testCaseStarted.stream() - .filter(testCaseStarted1 -> !findTestCaseFinishedBy(testCaseStarted1) + return this.testCaseStartedById.values().stream() + .sorted(comparing(TestCaseStarted::getTimestamp, timestampComparator)) + .filter(element -> !findTestCaseFinishedBy(element) .filter(TestCaseFinished::getWillBeRetried) .isPresent()) .collect(toList()); } + + // TODO: Move to Messages, make comparable? + private final Comparator timestampComparator = (a, b) -> { + long x = a.getSeconds(); + long y = b.getSeconds(); + int cmp; + if (x < y) + return -1; + if(y > x) + return 1; + + long x1 = a.getNanos(); + long y1 = b.getNanos(); + if (x1 < y1) + return -1; + if(y1 > x1) + return 1; + return 0; + }; public Map, List> findAllTestCaseStartedGroupedByFeature() { return findAllTestCaseStarted() @@ -277,6 +299,10 @@ public Optional reduceLinageOf(Pickle pickle, LineageReducer reducer) return findLineageBy(pickle) .map(lineage -> reducer.reduce(lineage, pickle)); } + + public Optional findLocationOf(Pickle pickle) { + return reduceLinageOf(pickle, ascending(FirstLocationCollector::new)); + } public Optional findPickleBy(TestCaseStarted testCaseStarted) { requireNonNull(testCaseStarted); @@ -285,6 +311,13 @@ public Optional findPickleBy(TestCaseStarted testCaseStarted) { .map(pickleById::get); } + public Optional findPickleBy(TestStepStarted testStepStarted) { + requireNonNull(testStepStarted); + return findTestCaseBy(testStepStarted) + .map(TestCase::getPickleId) + .map(pickleById::get); + } + public Optional findPickleStepBy(TestStep testStep) { requireNonNull(testStep); return testStep.getPickleStepId() @@ -302,6 +335,12 @@ public Optional findTestCaseBy(TestCaseStarted testCaseStarted) { return ofNullable(testCaseById.get(testCaseStarted.getTestCaseId())); } + public Optional findTestCaseBy(TestStepStarted testStepStarted) { + requireNonNull(testStepStarted); + return findTestCaseStartedBy(testStepStarted) + .flatMap(this::findTestCaseBy); + } + public Optional findTestCaseDurationBy(TestCaseStarted testCaseStarted) { requireNonNull(testCaseStarted); Timestamp started = testCaseStarted.getTimestamp(); @@ -313,6 +352,12 @@ public Optional findTestCaseDurationBy(TestCaseStarted testCaseStarted )); } + public Optional findTestCaseStartedBy(TestStepStarted testStepStarted) { + requireNonNull(testStepStarted); + String testCaseStartedId = testStepStarted.getTestCaseStartedId(); + return ofNullable(testCaseStartedById.get(testCaseStartedId)); + } + public Optional findTestCaseFinishedBy(TestCaseStarted testCaseStarted) { requireNonNull(testCaseStarted); return ofNullable(testCaseFinishedByTestCaseStartedId.get(testCaseStarted.getId())); @@ -337,6 +382,11 @@ public Optional findTestRunStarted() { return ofNullable(testRunStarted); } + public Optional findTestStepBy(TestStepStarted testStepStarted) { + requireNonNull(testStepStarted); + return ofNullable(testStepById.get(testStepStarted.getTestStepId())); + } + public Optional findTestStepBy(TestStepFinished testStepFinished) { requireNonNull(testStepFinished); return ofNullable(testStepById.get(testStepFinished.getTestStepId())); @@ -424,7 +474,7 @@ private void updateHook(Hook hook) { } private void updateTestCaseStarted(TestCaseStarted testCaseStarted) { - this.testCaseStarted.add(testCaseStarted); + this.testCaseStartedById.put(testCaseStarted.getId(), testCaseStarted); } private void updateTestCase(TestCase event) { @@ -493,4 +543,5 @@ private BiFunction, List> updateList(E element) { return list; }; } + } diff --git a/java/src/test/java/io/cucumber/query/QueryTest.java b/java/src/test/java/io/cucumber/query/QueryTest.java index 5cd21dc1..6ff1e864 100644 --- a/java/src/test/java/io/cucumber/query/QueryTest.java +++ b/java/src/test/java/io/cucumber/query/QueryTest.java @@ -16,10 +16,10 @@ class QueryTest { final Query query = new Query(); @Test - void retainsInsertionOrderForTestCaseStarted() { - TestCaseStarted a = new TestCaseStarted(0L, randomId(), randomId(), "main", new Timestamp(0L, 0L)); - TestCaseStarted b = new TestCaseStarted(0L, randomId(), randomId(), "main", new Timestamp(0L, 0L)); - TestCaseStarted c = new TestCaseStarted(0L, randomId(), randomId(), "main", new Timestamp(0L, 0L)); + void retainsTimestampOrderForTestCaseStarted() { + TestCaseStarted a = new TestCaseStarted(0L, randomId(), randomId(), "main", new Timestamp(1L, 0L)); + TestCaseStarted b = new TestCaseStarted(0L, randomId(), randomId(), "main", new Timestamp(2L, 0L)); + TestCaseStarted c = new TestCaseStarted(0L, randomId(), randomId(), "main", new Timestamp(3L, 0L)); Stream.of(a, b, c) .map(Envelope::of) From becb779e6eccc516cff1b52d2579d44eeea9c553 Mon Sep 17 00:00:00 2001 From: "M.P. Korstanje" Date: Thu, 5 Jun 2025 16:20:26 +0200 Subject: [PATCH 03/24] Add test --- .../query/FirstLocationCollector.java | 65 ------------------- .../main/java/io/cucumber/query/Query.java | 7 +- .../cucumber/query/QueryAcceptanceTest.java | 4 ++ .../attachments.feature.query-results.json | 46 +++++++++++++ testdata/empty.feature.query-results.json | 6 ++ ...examples-tables.feature.query-results.json | 38 +++++++++++ testdata/hooks.feature.query-results.json | 22 +++++++ testdata/minimal.feature.query-results.json | 6 ++ testdata/rules.feature.query-results.json | 14 ++++ 9 files changed, 142 insertions(+), 66 deletions(-) delete mode 100644 java/src/main/java/io/cucumber/query/FirstLocationCollector.java diff --git a/java/src/main/java/io/cucumber/query/FirstLocationCollector.java b/java/src/main/java/io/cucumber/query/FirstLocationCollector.java deleted file mode 100644 index 7cb06807..00000000 --- a/java/src/main/java/io/cucumber/query/FirstLocationCollector.java +++ /dev/null @@ -1,65 +0,0 @@ -package io.cucumber.query; - -import io.cucumber.messages.types.Examples; -import io.cucumber.messages.types.Feature; -import io.cucumber.messages.types.Location; -import io.cucumber.messages.types.Pickle; -import io.cucumber.messages.types.Rule; -import io.cucumber.messages.types.Scenario; -import io.cucumber.messages.types.TableRow; -import io.cucumber.query.LineageReducer.Collector; - -import java.util.function.Supplier; - -/** - * Finds the first (depending on - * {@link LineageReducer#descending(Supplier)} or - * {@link LineageReducer#ascending(Supplier)}) location of an element in - * a lineage. - * - * @see NamingStrategy - */ -class FirstLocationCollector implements Collector { - - private Location location; - - @Override - public void add(Feature feature) { - if (location == null) { - location = feature.getLocation(); - } - } - - @Override - public void add(Rule rule) { - if (location == null) { - location = rule.getLocation(); - } - } - - @Override - public void add(Scenario scenario) { - if (location == null) { - location = scenario.getLocation(); - } - } - - @Override - public void add(Examples examples, int index) { - if (location == null) { - location = examples.getLocation(); - } - } - - @Override - public void add(TableRow example, int index) { - if (location == null) { - location = example.getLocation(); - } - } - - @Override - public Location finish() { - return location; - } -} diff --git a/java/src/main/java/io/cucumber/query/Query.java b/java/src/main/java/io/cucumber/query/Query.java index 33c25ed4..5e8ec33e 100644 --- a/java/src/main/java/io/cucumber/query/Query.java +++ b/java/src/main/java/io/cucumber/query/Query.java @@ -301,7 +301,12 @@ public Optional reduceLinageOf(Pickle pickle, LineageReducer reducer) } public Optional findLocationOf(Pickle pickle) { - return reduceLinageOf(pickle, ascending(FirstLocationCollector::new)); + return findLineageBy(pickle).flatMap(lineage -> { + if (lineage.example().isPresent()) { + return lineage.example().map(TableRow::getLocation); + } + return lineage.scenario().map(Scenario::getLocation); + }); } public Optional findPickleBy(TestCaseStarted testCaseStarted) { diff --git a/java/src/test/java/io/cucumber/query/QueryAcceptanceTest.java b/java/src/test/java/io/cucumber/query/QueryAcceptanceTest.java index bd0ed49c..3b371411 100644 --- a/java/src/test/java/io/cucumber/query/QueryAcceptanceTest.java +++ b/java/src/test/java/io/cucumber/query/QueryAcceptanceTest.java @@ -137,6 +137,10 @@ private static Map createQueryResults(Query query) { .map(hook -> hook.map(Hook::getId)) .filter(Optional::isPresent) .collect(toList())); + results.put("findLocationBy", query.findAllPickles().stream() + .map(query::findLocationOf) + .filter(Optional::isPresent) + .collect(toList())); results.put("findMeta", query.findMeta().map(meta -> meta.getImplementation().getName())); results.put("findMostSevereTestStepResultBy", query.findAllTestCaseStarted().stream() .map(query::findMostSevereTestStepResultBy) diff --git a/testdata/attachments.feature.query-results.json b/testdata/attachments.feature.query-results.json index cc659157..7e76b43e 100644 --- a/testdata/attachments.feature.query-results.json +++ b/testdata/attachments.feature.query-results.json @@ -96,6 +96,52 @@ "Attachments", "Attachments" ], + "findLocationBy" : [ + { + "line" : 12, + "column" : 3 + }, + { + "line" : 18, + "column" : 3 + }, + { + "line" : 21, + "column" : 3 + }, + { + "line" : 24, + "column" : 3 + }, + { + "line" : 30, + "column" : 3 + }, + { + "line" : 33, + "column" : 3 + }, + { + "line" : 36, + "column" : 3 + }, + { + "line" : 44, + "column" : 7 + }, + { + "line" : 45, + "column" : 7 + }, + { + "line" : 47, + "column" : 3 + }, + { + "line" : 50, + "column" : 3 + } + ], "findMeta" : "fake-cucumber", "findMostSevereTestStepResultBy" : [ "PASSED", diff --git a/testdata/empty.feature.query-results.json b/testdata/empty.feature.query-results.json index 1b4b992a..ce23311b 100644 --- a/testdata/empty.feature.query-results.json +++ b/testdata/empty.feature.query-results.json @@ -24,6 +24,12 @@ "findFeatureBy" : [ "Empty Scenarios" ], + "findLocationBy" : [ + { + "line" : 7, + "column" : 3 + } + ], "findMeta" : "fake-cucumber", "findMostSevereTestStepResultBy" : [ null diff --git a/testdata/examples-tables.feature.query-results.json b/testdata/examples-tables.feature.query-results.json index cd6532d2..7ff75b6f 100644 --- a/testdata/examples-tables.feature.query-results.json +++ b/testdata/examples-tables.feature.query-results.json @@ -40,6 +40,44 @@ "Examples Tables", "Examples Tables" ], + "findLocationBy" : [ + { + "line" : 19, + "column" : 7 + }, + { + "line" : 20, + "column" : 7 + }, + { + "line" : 25, + "column" : 7 + }, + { + "line" : 26, + "column" : 7 + }, + { + "line" : 31, + "column" : 7 + }, + { + "line" : 32, + "column" : 7 + }, + { + "line" : 41, + "column" : 7 + }, + { + "line" : 42, + "column" : 7 + }, + { + "line" : 43, + "column" : 7 + } + ], "findMeta" : "fake-cucumber", "findMostSevereTestStepResultBy" : [ "PASSED", diff --git a/testdata/hooks.feature.query-results.json b/testdata/hooks.feature.query-results.json index 1616156c..326b9f45 100644 --- a/testdata/hooks.feature.query-results.json +++ b/testdata/hooks.feature.query-results.json @@ -36,6 +36,28 @@ "0", "3" ], + "findLocationBy" : [ + { + "line" : 6, + "column" : 3 + }, + { + "line" : 9, + "column" : 3 + }, + { + "line" : 12, + "column" : 3 + }, + { + "line" : 16, + "column" : 3 + }, + { + "line" : 20, + "column" : 3 + } + ], "findMeta" : "fake-cucumber", "findMostSevereTestStepResultBy" : [ "PASSED", diff --git a/testdata/minimal.feature.query-results.json b/testdata/minimal.feature.query-results.json index 5f5cc3aa..5bb54508 100644 --- a/testdata/minimal.feature.query-results.json +++ b/testdata/minimal.feature.query-results.json @@ -24,6 +24,12 @@ "findFeatureBy" : [ "minimal" ], + "findLocationBy" : [ + { + "line" : 9, + "column" : 3 + } + ], "findMeta" : "fake-cucumber", "findMostSevereTestStepResultBy" : [ "PASSED" diff --git a/testdata/rules.feature.query-results.json b/testdata/rules.feature.query-results.json index bd6f8706..b0c4fdb4 100644 --- a/testdata/rules.feature.query-results.json +++ b/testdata/rules.feature.query-results.json @@ -28,6 +28,20 @@ "Usage of a `Rule`", "Usage of a `Rule`" ], + "findLocationBy" : [ + { + "line" : 9, + "column" : 5 + }, + { + "line" : 16, + "column" : 5 + }, + { + "line" : 25, + "column" : 5 + } + ], "findMeta" : "fake-cucumber", "findMostSevereTestStepResultBy" : [ "PASSED", From 5f4fd722ba161599dd2f45f4543952bdcf833010 Mon Sep 17 00:00:00 2001 From: "M.P. Korstanje" Date: Thu, 5 Jun 2025 16:38:02 +0200 Subject: [PATCH 04/24] Extract comparator --- .../main/java/io/cucumber/query/Query.java | 22 +--------- .../cucumber/query/TimestampComparator.java | 30 +++++++++++++ .../query/TimestampComparatorTest.java | 42 +++++++++++++++++++ 3 files changed, 73 insertions(+), 21 deletions(-) create mode 100644 java/src/main/java/io/cucumber/query/TimestampComparator.java create mode 100644 java/src/test/java/io/cucumber/query/TimestampComparatorTest.java diff --git a/java/src/main/java/io/cucumber/query/Query.java b/java/src/main/java/io/cucumber/query/Query.java index 5e8ec33e..213f2710 100644 --- a/java/src/main/java/io/cucumber/query/Query.java +++ b/java/src/main/java/io/cucumber/query/Query.java @@ -36,7 +36,6 @@ import java.util.function.Supplier; import java.util.stream.Collectors; -import static io.cucumber.query.LineageReducer.ascending; import static java.util.Collections.emptyList; import static java.util.Comparator.comparing; import static java.util.Comparator.naturalOrder; @@ -109,31 +108,12 @@ public List findAllPickleSteps() { public List findAllTestCaseStarted() { return this.testCaseStartedById.values().stream() - .sorted(comparing(TestCaseStarted::getTimestamp, timestampComparator)) + .sorted(comparing(TestCaseStarted::getTimestamp, new TimestampComparator())) .filter(element -> !findTestCaseFinishedBy(element) .filter(TestCaseFinished::getWillBeRetried) .isPresent()) .collect(toList()); } - - // TODO: Move to Messages, make comparable? - private final Comparator timestampComparator = (a, b) -> { - long x = a.getSeconds(); - long y = b.getSeconds(); - int cmp; - if (x < y) - return -1; - if(y > x) - return 1; - - long x1 = a.getNanos(); - long y1 = b.getNanos(); - if (x1 < y1) - return -1; - if(y1 > x1) - return 1; - return 0; - }; public Map, List> findAllTestCaseStartedGroupedByFeature() { return findAllTestCaseStarted() diff --git a/java/src/main/java/io/cucumber/query/TimestampComparator.java b/java/src/main/java/io/cucumber/query/TimestampComparator.java new file mode 100644 index 00000000..29ed2794 --- /dev/null +++ b/java/src/main/java/io/cucumber/query/TimestampComparator.java @@ -0,0 +1,30 @@ +package io.cucumber.query; + +import io.cucumber.messages.types.Timestamp; + +import java.util.Comparator; + +class TimestampComparator implements Comparator { + @Override + public int compare(Timestamp a, Timestamp b) { + long sa = a.getSeconds(); + long sb = b.getSeconds(); + + if (sa < sb) { + return -1; + } else if (sb < sa) { + return 1; + } + + long na = a.getNanos(); + long nb = b.getNanos(); + + if (na < nb) { + return -1; + } else if (nb < na) { + return 1; + } + + return 0; + } +} diff --git a/java/src/test/java/io/cucumber/query/TimestampComparatorTest.java b/java/src/test/java/io/cucumber/query/TimestampComparatorTest.java new file mode 100644 index 00000000..d48e6e99 --- /dev/null +++ b/java/src/test/java/io/cucumber/query/TimestampComparatorTest.java @@ -0,0 +1,42 @@ +package io.cucumber.query; + +import io.cucumber.messages.types.Timestamp; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class TimestampComparatorTest { + + private final TimestampComparator comparator = new TimestampComparator(); + + @Test + void identity(){ + Timestamp a = new Timestamp(1L, 1L); + Timestamp b = new Timestamp(1L, 1L); + + assertThat(comparator.compare(a, b)).isEqualTo(0); + assertThat(comparator.compare(b, a)).isEqualTo(0); + } + + @Test + void onSeconds(){ + Timestamp a = new Timestamp(1L, 1L); + Timestamp b = new Timestamp(2L, 2L); + assertThat(comparator.compare(a, b)).isEqualTo(-1); + assertThat(comparator.compare(b, a)).isEqualTo(1); + } + + @Test + void onNanoSeconds(){ + Timestamp a = new Timestamp(1L, 1L); + Timestamp b1 = new Timestamp(1L, 0L); + Timestamp b2 = new Timestamp(1L, 2L); + + assertThat(comparator.compare(a, b1)).isEqualTo(1); + assertThat(comparator.compare(b1, a)).isEqualTo(-1); + + assertThat(comparator.compare(a, b2)).isEqualTo(-1); + assertThat(comparator.compare(b2, a)).isEqualTo(1); + + } +} From 1b18f3f0c5eb47adf71ab9377b6b6f58051cc085 Mon Sep 17 00:00:00 2001 From: "M.P. Korstanje" Date: Thu, 5 Jun 2025 16:42:15 +0200 Subject: [PATCH 05/24] Update testdata --- testdata/attachments.feature.query-results.json | 12 ++---------- testdata/hooks.feature.query-results.json | 14 +++----------- 2 files changed, 5 insertions(+), 21 deletions(-) diff --git a/testdata/attachments.feature.query-results.json b/testdata/attachments.feature.query-results.json index 7e76b43e..e7e357be 100644 --- a/testdata/attachments.feature.query-results.json +++ b/testdata/attachments.feature.query-results.json @@ -126,19 +126,11 @@ "column" : 3 }, { - "line" : 44, - "column" : 7 - }, - { - "line" : 45, - "column" : 7 - }, - { - "line" : 47, + "line" : 39, "column" : 3 }, { - "line" : 50, + "line" : 42, "column" : 3 } ], diff --git a/testdata/hooks.feature.query-results.json b/testdata/hooks.feature.query-results.json index 326b9f45..b8607c2e 100644 --- a/testdata/hooks.feature.query-results.json +++ b/testdata/hooks.feature.query-results.json @@ -38,23 +38,15 @@ ], "findLocationBy" : [ { - "line" : 6, + "line" : 4, "column" : 3 }, { - "line" : 9, + "line" : 7, "column" : 3 }, { - "line" : 12, - "column" : 3 - }, - { - "line" : 16, - "column" : 3 - }, - { - "line" : 20, + "line" : 10, "column" : 3 } ], From 27643ddcadf416641f883ea52a8aa67252b74d8f Mon Sep 17 00:00:00 2001 From: "M.P. Korstanje" Date: Thu, 5 Jun 2025 17:59:13 +0200 Subject: [PATCH 06/24] Update testdata --- CHANGELOG.md | 2 + .../main/java/io/cucumber/query/Query.java | 83 ++++++------------- .../cucumber/query/QueryAcceptanceTest.java | 8 +- .../attachments.feature.query-results.json | 13 ++- ...examples-tables.feature.query-results.json | 31 ++++++- testdata/hooks.feature.query-results.json | 13 ++- testdata/minimal.feature.query-results.json | 5 +- testdata/rules.feature.query-results.json | 16 +++- 8 files changed, 109 insertions(+), 62 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e3eaf40..b79df45e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). ## [Unreleased] +### Added +- [Java] Make `Lineage` API public and add `Query.findLineageBy` methods ## [13.2.0] - 2025-02-02 ### Changed diff --git a/java/src/main/java/io/cucumber/query/Query.java b/java/src/main/java/io/cucumber/query/Query.java index 213f2710..fd86b622 100644 --- a/java/src/main/java/io/cucumber/query/Query.java +++ b/java/src/main/java/io/cucumber/query/Query.java @@ -67,6 +67,7 @@ public final class Query { private final Map testCaseStartedById = new ConcurrentHashMap<>(); private final Map testCaseFinishedByTestCaseStartedId = new ConcurrentHashMap<>(); private final Map> testStepsFinishedByTestCaseStartedId = new ConcurrentHashMap<>(); + private final Map> testStepsStartedByTestCaseStartedId = new ConcurrentHashMap<>(); private final Map pickleById = new ConcurrentHashMap<>(); private final Map testCaseById = new ConcurrentHashMap<>(); private final Map stepById = new ConcurrentHashMap<>(); @@ -181,49 +182,56 @@ public Optional findMostSevereTestStepResultBy(TestCaseStarted t public String findNameOf(GherkinDocument element, NamingStrategy namingStrategy) { requireNonNull(element); requireNonNull(namingStrategy); - return reduceLinageOf(element, namingStrategy) + return findLineageBy(element) + .map(namingStrategy::reduce) .orElseThrow(createElementWasNotPartOfThisQueryObject()); } public String findNameOf(Feature element, NamingStrategy namingStrategy) { requireNonNull(element); requireNonNull(namingStrategy); - return reduceLinageOf(element, namingStrategy) + return findLineageBy(element) + .map(namingStrategy::reduce) .orElseThrow(createElementWasNotPartOfThisQueryObject()); } public String findNameOf(Rule element, NamingStrategy namingStrategy) { requireNonNull(element); requireNonNull(namingStrategy); - return reduceLinageOf(element, namingStrategy) + return findLineageBy(element) + .map(namingStrategy::reduce) .orElseThrow(createElementWasNotPartOfThisQueryObject()); } public String findNameOf(Scenario element, NamingStrategy namingStrategy) { requireNonNull(element); requireNonNull(namingStrategy); - return reduceLinageOf(element, namingStrategy) + return findLineageBy(element) + .map(namingStrategy::reduce) .orElseThrow(createElementWasNotPartOfThisQueryObject()); } public String findNameOf(Examples element, NamingStrategy namingStrategy) { requireNonNull(element); requireNonNull(namingStrategy); - return reduceLinageOf(element, namingStrategy) + return findLineageBy(element) + .map(namingStrategy::reduce) .orElseThrow(createElementWasNotPartOfThisQueryObject()); } public String findNameOf(TableRow element, NamingStrategy namingStrategy) { requireNonNull(element); requireNonNull(namingStrategy); - return reduceLinageOf(element, namingStrategy) + return findLineageBy(element) + .map(namingStrategy::reduce) .orElseThrow(createElementWasNotPartOfThisQueryObject()); } public String findNameOf(Pickle element, NamingStrategy namingStrategy) { requireNonNull(element); requireNonNull(namingStrategy); - return reduceLinageOf(element, namingStrategy) + return findLineageBy(element) + .map(lineage -> namingStrategy.reduce(lineage, element)) .orElseGet(element::getName); } @@ -231,55 +239,6 @@ private static Supplier createElementWasNotPartOfThisQ return () -> new IllegalArgumentException("Element was not part of this query object"); } - public Optional reduceLinageOf(GherkinDocument element, LineageReducer reducer) { - requireNonNull(element); - requireNonNull(reducer); - return findLineageBy(element) - .map(reducer::reduce); - } - - public Optional reduceLinageOf(Feature element, LineageReducer reducer) { - requireNonNull(element); - requireNonNull(reducer); - return findLineageBy(element) - .map(reducer::reduce); - } - - public Optional reduceLinageOf(Rule element, LineageReducer reducer) { - requireNonNull(element); - requireNonNull(reducer); - return findLineageBy(element) - .map(reducer::reduce); - } - - public Optional reduceLinageOf(Scenario element, LineageReducer reducer) { - requireNonNull(element); - requireNonNull(reducer); - return findLineageBy(element) - .map(reducer::reduce); - } - - public Optional reduceLinageOf(Examples element, LineageReducer reducer) { - requireNonNull(element); - requireNonNull(reducer); - return findLineageBy(element) - .map(reducer::reduce); - } - - public Optional reduceLinageOf(TableRow element, LineageReducer reducer) { - requireNonNull(element); - requireNonNull(reducer); - return findLineageBy(element) - .map(reducer::reduce); - } - - public Optional reduceLinageOf(Pickle pickle, LineageReducer reducer) { - requireNonNull(pickle); - requireNonNull(reducer); - return findLineageBy(pickle) - .map(lineage -> reducer.reduce(lineage, pickle)); - } - public Optional findLocationOf(Pickle pickle) { return findLineageBy(pickle).flatMap(lineage -> { if (lineage.example().isPresent()) { @@ -377,6 +336,14 @@ public Optional findTestStepBy(TestStepFinished testStepFinished) { return ofNullable(testStepById.get(testStepFinished.getTestStepId())); } + public List findTestStepsStartedBy(TestCaseStarted testCaseStarted) { + requireNonNull(testCaseStarted); + List testStepsFinished = testStepsStartedByTestCaseStartedId. + getOrDefault(testCaseStarted.getId(), emptyList()); + // Concurrency + return new ArrayList<>(testStepsFinished); + } + public List findTestStepsFinishedBy(TestCaseStarted testCaseStarted) { requireNonNull(testCaseStarted); List testStepsFinished = testStepsFinishedByTestCaseStartedId. @@ -399,6 +366,7 @@ public void update(Envelope envelope) { envelope.getTestRunFinished().ifPresent(this::updateTestRunFinished); envelope.getTestCaseStarted().ifPresent(this::updateTestCaseStarted); envelope.getTestCaseFinished().ifPresent(this::updateTestCaseFinished); + envelope.getTestStepFinished().ifPresent(this::updateTestStepStarted); envelope.getTestStepFinished().ifPresent(this::updateTestStepFinished); envelope.getGherkinDocument().ifPresent(this::updateGherkinDocument); envelope.getPickle().ifPresent(this::updatePickle); @@ -489,6 +457,9 @@ private void updateFeature(Feature feature) { }); } + private void updateTestStepStarted(TestStepFinished event) { + this.testStepsStartedByTestCaseStartedId.compute(event.getTestCaseStartedId(), updateList(event)); + } private void updateTestStepFinished(TestStepFinished event) { this.testStepsFinishedByTestCaseStartedId.compute(event.getTestCaseStartedId(), updateList(event)); } diff --git a/java/src/test/java/io/cucumber/query/QueryAcceptanceTest.java b/java/src/test/java/io/cucumber/query/QueryAcceptanceTest.java index 3b371411..2e355503 100644 --- a/java/src/test/java/io/cucumber/query/QueryAcceptanceTest.java +++ b/java/src/test/java/io/cucumber/query/QueryAcceptanceTest.java @@ -195,7 +195,13 @@ private static Map createQueryResults(Query query) { .map(Convertor::toMessage)); results.put("findTestRunFinished", query.findTestRunFinished()); results.put("findTestRunStarted", query.findTestRunStarted()); - results.put("findTestStepBy", query.findAllTestCaseStarted().stream() + results.put("findTestStepByTestStepStarted", query.findAllTestCaseStarted().stream() + .map(query::findTestStepsStartedBy) + .flatMap(Collection::stream) + .map(query::findTestStepBy) + .map(testStep -> testStep.map(TestStep::getId)) + .collect(toList())); + results.put("findTestStepByTestStepFinished", query.findAllTestCaseStarted().stream() .map(query::findTestStepsFinishedBy) .flatMap(Collection::stream) .map(query::findTestStepBy) diff --git a/testdata/attachments.feature.query-results.json b/testdata/attachments.feature.query-results.json index e7e357be..98d768a6 100644 --- a/testdata/attachments.feature.query-results.json +++ b/testdata/attachments.feature.query-results.json @@ -315,7 +315,18 @@ }, "id" : "45" }, - "findTestStepBy" : [ + "findTestStepByTestStepStarted" : [ + "46", + "48", + "50", + "52", + "54", + "56", + "58", + "60", + "62" + ], + "findTestStepByTestStepFinished" : [ "46", "48", "50", diff --git a/testdata/examples-tables.feature.query-results.json b/testdata/examples-tables.feature.query-results.json index 7ff75b6f..9b5f8814 100644 --- a/testdata/examples-tables.feature.query-results.json +++ b/testdata/examples-tables.feature.query-results.json @@ -295,7 +295,36 @@ }, "id" : "69" }, - "findTestStepBy" : [ + "findTestStepByTestStepStarted" : [ + "70", + "71", + "72", + "74", + "75", + "76", + "78", + "79", + "80", + "82", + "83", + "84", + "86", + "87", + "88", + "90", + "91", + "92", + "94", + "95", + "96", + "98", + "99", + "100", + "102", + "103", + "104" + ], + "findTestStepByTestStepFinished" : [ "70", "71", "72", diff --git a/testdata/hooks.feature.query-results.json b/testdata/hooks.feature.query-results.json index b8607c2e..dfedcdbf 100644 --- a/testdata/hooks.feature.query-results.json +++ b/testdata/hooks.feature.query-results.json @@ -141,7 +141,18 @@ }, "id" : "16" }, - "findTestStepBy" : [ + "findTestStepByTestStepStarted" : [ + "17", + "18", + "19", + "21", + "22", + "23", + "25", + "26", + "27" + ], + "findTestStepByTestStepFinished" : [ "17", "18", "19", diff --git a/testdata/minimal.feature.query-results.json b/testdata/minimal.feature.query-results.json index 5bb54508..64768f03 100644 --- a/testdata/minimal.feature.query-results.json +++ b/testdata/minimal.feature.query-results.json @@ -91,7 +91,10 @@ }, "id" : "5" }, - "findTestStepBy" : [ + "findTestStepByTestStepStarted" : [ + "6" + ], + "findTestStepByTestStepFinished" : [ "6" ], "findTestStepsFinishedBy" : [ diff --git a/testdata/rules.feature.query-results.json b/testdata/rules.feature.query-results.json index b0c4fdb4..90b71d80 100644 --- a/testdata/rules.feature.query-results.json +++ b/testdata/rules.feature.query-results.json @@ -151,7 +151,21 @@ }, "id" : "39" }, - "findTestStepBy" : [ + "findTestStepByTestStepStarted" : [ + "40", + "41", + "42", + "43", + "45", + "46", + "47", + "48", + "50", + "51", + "52", + "53" + ], + "findTestStepByTestStepFinished" : [ "40", "41", "42", From 394b8bcc9051fdc9f2a5c5c2287e548a076069c4 Mon Sep 17 00:00:00 2001 From: "M.P. Korstanje" Date: Thu, 5 Jun 2025 18:01:40 +0200 Subject: [PATCH 07/24] Update testdata --- java/src/main/java/io/cucumber/query/Query.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/java/src/main/java/io/cucumber/query/Query.java b/java/src/main/java/io/cucumber/query/Query.java index fd86b622..8d2d293c 100644 --- a/java/src/main/java/io/cucumber/query/Query.java +++ b/java/src/main/java/io/cucumber/query/Query.java @@ -67,7 +67,7 @@ public final class Query { private final Map testCaseStartedById = new ConcurrentHashMap<>(); private final Map testCaseFinishedByTestCaseStartedId = new ConcurrentHashMap<>(); private final Map> testStepsFinishedByTestCaseStartedId = new ConcurrentHashMap<>(); - private final Map> testStepsStartedByTestCaseStartedId = new ConcurrentHashMap<>(); + private final Map> testStepsStartedByTestCaseStartedId = new ConcurrentHashMap<>(); private final Map pickleById = new ConcurrentHashMap<>(); private final Map testCaseById = new ConcurrentHashMap<>(); private final Map stepById = new ConcurrentHashMap<>(); @@ -336,9 +336,9 @@ public Optional findTestStepBy(TestStepFinished testStepFinished) { return ofNullable(testStepById.get(testStepFinished.getTestStepId())); } - public List findTestStepsStartedBy(TestCaseStarted testCaseStarted) { + public List findTestStepsStartedBy(TestCaseStarted testCaseStarted) { requireNonNull(testCaseStarted); - List testStepsFinished = testStepsStartedByTestCaseStartedId. + List testStepsFinished = testStepsStartedByTestCaseStartedId. getOrDefault(testCaseStarted.getId(), emptyList()); // Concurrency return new ArrayList<>(testStepsFinished); @@ -366,7 +366,7 @@ public void update(Envelope envelope) { envelope.getTestRunFinished().ifPresent(this::updateTestRunFinished); envelope.getTestCaseStarted().ifPresent(this::updateTestCaseStarted); envelope.getTestCaseFinished().ifPresent(this::updateTestCaseFinished); - envelope.getTestStepFinished().ifPresent(this::updateTestStepStarted); + envelope.getTestStepStarted().ifPresent(this::updateTestStepStarted); envelope.getTestStepFinished().ifPresent(this::updateTestStepFinished); envelope.getGherkinDocument().ifPresent(this::updateGherkinDocument); envelope.getPickle().ifPresent(this::updatePickle); @@ -457,7 +457,7 @@ private void updateFeature(Feature feature) { }); } - private void updateTestStepStarted(TestStepFinished event) { + private void updateTestStepStarted(TestStepStarted event) { this.testStepsStartedByTestCaseStartedId.compute(event.getTestCaseStartedId(), updateList(event)); } private void updateTestStepFinished(TestStepFinished event) { From d4b17e1023a333000a9de8ac34bfa365f003cc56 Mon Sep 17 00:00:00 2001 From: "M.P. Korstanje" Date: Thu, 5 Jun 2025 18:11:09 +0200 Subject: [PATCH 08/24] Update CHANGELOG --- CHANGELOG.md | 6 ++++- .../main/java/io/cucumber/query/Query.java | 23 +++++++++++-------- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b79df45e..8a71e25d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,11 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## [Unreleased] ### Added -- [Java] Make `Lineage` API public and add `Query.findLineageBy` methods +- [Java] Make `Lineage` API and `Query.findLineageBy` public +- [Java] Add `Query.findPickleBy(TestStepStarted)`, `.findTestCaseBy(TestStepStarted)`, `.findTestCaseStartedBy(TestStepStarted)`, `.findTestStepBy(TestStepStarted)`, `.findTestStepsStartedBy(TestCaseStarted)` methods + +### Fixed +- [Java] `Query.findAllTestCaseStarted` orders events by `timestamp` and `id`. ## [13.2.0] - 2025-02-02 ### Changed diff --git a/java/src/main/java/io/cucumber/query/Query.java b/java/src/main/java/io/cucumber/query/Query.java index 8d2d293c..adf65897 100644 --- a/java/src/main/java/io/cucumber/query/Query.java +++ b/java/src/main/java/io/cucumber/query/Query.java @@ -109,7 +109,9 @@ public List findAllPickleSteps() { public List findAllTestCaseStarted() { return this.testCaseStartedById.values().stream() - .sorted(comparing(TestCaseStarted::getTimestamp, new TimestampComparator())) + .sorted(comparing(TestCaseStarted::getTimestamp, new TimestampComparator()) + // tie-breaker for stability + .thenComparing(TestCaseStarted::getId)) .filter(element -> !findTestCaseFinishedBy(element) .filter(TestCaseFinished::getWillBeRetried) .isPresent()) @@ -240,12 +242,12 @@ private static Supplier createElementWasNotPartOfThisQ } public Optional findLocationOf(Pickle pickle) { - return findLineageBy(pickle).flatMap(lineage -> { - if (lineage.example().isPresent()) { - return lineage.example().map(TableRow::getLocation); - } - return lineage.scenario().map(Scenario::getLocation); - }); + return findLineageBy(pickle).flatMap(lineage -> { + if (lineage.example().isPresent()) { + return lineage.example().map(TableRow::getLocation); + } + return lineage.scenario().map(Scenario::getLocation); + }); } public Optional findPickleBy(TestCaseStarted testCaseStarted) { @@ -330,7 +332,7 @@ public Optional findTestStepBy(TestStepStarted testStepStarted) { requireNonNull(testStepStarted); return ofNullable(testStepById.get(testStepStarted.getTestStepId())); } - + public Optional findTestStepBy(TestStepFinished testStepFinished) { requireNonNull(testStepFinished); return ofNullable(testStepById.get(testStepFinished.getTestStepId())); @@ -338,10 +340,10 @@ public Optional findTestStepBy(TestStepFinished testStepFinished) { public List findTestStepsStartedBy(TestCaseStarted testCaseStarted) { requireNonNull(testCaseStarted); - List testStepsFinished = testStepsStartedByTestCaseStartedId. + List testStepsStarted = testStepsStartedByTestCaseStartedId. getOrDefault(testCaseStarted.getId(), emptyList()); // Concurrency - return new ArrayList<>(testStepsFinished); + return new ArrayList<>(testStepsStarted); } public List findTestStepsFinishedBy(TestCaseStarted testCaseStarted) { @@ -460,6 +462,7 @@ private void updateFeature(Feature feature) { private void updateTestStepStarted(TestStepStarted event) { this.testStepsStartedByTestCaseStartedId.compute(event.getTestCaseStartedId(), updateList(event)); } + private void updateTestStepFinished(TestStepFinished event) { this.testStepsFinishedByTestCaseStartedId.compute(event.getTestCaseStartedId(), updateList(event)); } From b3b944834e18bb1f5eebe6d5548219f53279af52 Mon Sep 17 00:00:00 2001 From: "M.P. Korstanje" Date: Fri, 6 Jun 2025 20:49:21 +0200 Subject: [PATCH 09/24] Id is tiebreaker --- java/src/test/java/io/cucumber/query/QueryTest.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/java/src/test/java/io/cucumber/query/QueryTest.java b/java/src/test/java/io/cucumber/query/QueryTest.java index 6ff1e864..37bcdf55 100644 --- a/java/src/test/java/io/cucumber/query/QueryTest.java +++ b/java/src/test/java/io/cucumber/query/QueryTest.java @@ -27,6 +27,18 @@ void retainsTimestampOrderForTestCaseStarted() { assertThat(query.findAllTestCaseStarted()).containsExactly(a, b, c); } + @Test + void idIsTieOrderTieBreaker() { + TestCaseStarted a = new TestCaseStarted(0L, "2", randomId(), "main", new Timestamp(1L, 0L)); + TestCaseStarted b = new TestCaseStarted(0L, "1", randomId(), "main", new Timestamp(1L, 0L)); + TestCaseStarted c = new TestCaseStarted(0L, "0", randomId(), "main", new Timestamp(1L, 0L)); + + Stream.of(a, b, c) + .map(Envelope::of) + .forEach(query::update); + + assertThat(query.findAllTestCaseStarted()).containsExactly(c, b, a); + } @Test void omitsTestCaseStartedIfFinishedAndWillBeRetried() { From bc8eaf1215af2eb2a823a439902a59134dafbc57 Mon Sep 17 00:00:00 2001 From: "M.P. Korstanje" Date: Fri, 6 Jun 2025 20:52:34 +0200 Subject: [PATCH 10/24] Update CHANGELOG --- CHANGELOG.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a71e25d..81757c59 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,8 +7,12 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## [Unreleased] ### Added -- [Java] Make `Lineage` API and `Query.findLineageBy` public -- [Java] Add `Query.findPickleBy(TestStepStarted)`, `.findTestCaseBy(TestStepStarted)`, `.findTestCaseStartedBy(TestStepStarted)`, `.findTestStepBy(TestStepStarted)`, `.findTestStepsStartedBy(TestCaseStarted)` methods +- Make `Lineage` APIs public ([#76](https://github.com/cucumber/query/pull/76)) +- New method `findPickleBy(TestStepStarted)` ([#76](https://github.com/cucumber/query/pull/76)) +- New method `findTestCaseBy(TestStepStarted)` ([#76](https://github.com/cucumber/query/pull/76)) +- New method `findTestCaseStartedBy(TestStepStarted)` ([#76](https://github.com/cucumber/query/pull/76)) +- New method `findTestStepBy(TestStepStarted)` ([#76](https://github.com/cucumber/query/pull/76)) +- New method `findTestStepsStartedBy(TestCaseStarted)` ([#76](https://github.com/cucumber/query/pull/76)) ### Fixed - [Java] `Query.findAllTestCaseStarted` orders events by `timestamp` and `id`. From eaf67f5b58ce7a3f225d01b836aefe2a1fcaad66 Mon Sep 17 00:00:00 2001 From: "M.P. Korstanje" Date: Fri, 6 Jun 2025 20:57:33 +0200 Subject: [PATCH 11/24] Update CHANGELOG --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 81757c59..40930e46 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## [Unreleased] ### Added -- Make `Lineage` APIs public ([#76](https://github.com/cucumber/query/pull/76)) +- Made `Lineage` APIs public ([#76](https://github.com/cucumber/query/pull/76)) - New method `findPickleBy(TestStepStarted)` ([#76](https://github.com/cucumber/query/pull/76)) - New method `findTestCaseBy(TestStepStarted)` ([#76](https://github.com/cucumber/query/pull/76)) - New method `findTestCaseStartedBy(TestStepStarted)` ([#76](https://github.com/cucumber/query/pull/76)) From 09b965581a2512fbe996a5ae61da33f7413a7184 Mon Sep 17 00:00:00 2001 From: "M.P. Korstanje" Date: Sat, 7 Jun 2025 00:05:36 +0200 Subject: [PATCH 12/24] Update README --- README.md | 35 ----------------------------------- 1 file changed, 35 deletions(-) diff --git a/README.md b/README.md index 9182d34a..43edfcd4 100644 --- a/README.md +++ b/README.md @@ -38,38 +38,3 @@ is rendering the [GherkinDocument](../cucumber-messages/messages.md#io.cucumber. The Query library makes this easy by providing a function to look up the status of a step, a scenario or an entire file. - -## API - -| Query function | .NET | Go | Java | Ruby | TypeScript | -|-----------------------------------------------------------------------------------------------|------|-----|------|------|------------| -| `getStepResults(uri: string, lineNumber: number): messages.ITestResult[]` | | | | | ✓ | -| `getScenarioResults(uri: string, lineNumber: number): messages.ITestResult[]` | | | | | ✓ | -| `getDocumentResults(uri: string): messages.ITestResult[]` | | | | | ✓ | -| `getStepMatchArguments(uri: string, lineNumber: number): messages.IStepMatchArgument[]` | | | | | ✓ | -| `getGherkinStep(gherkinStepId: string): messages.GherkinDocument.Feature.IStep` | | | | | ✓ | -| `countMostSevereTestStepResultStatus(): Map` | | | ✓ | | ✓ | -| `countTestCasesStarted(): int` | | | ✓ | | ✓ | -| `findAllPickles(): List` | | | ✓ | | ✓ | -| `findAllPickleSteps(): List` | | | ✓ | | ✓ | -| `findAllTestCaseStarted(): List` | | | ✓ | | ✓ | -| `findAllTestCaseStartedGroupedByFeature(): Map, List>` | | | ✓ | | ✓ | -| `findAllTestSteps(): List` | | | ✓ | | ✓ | -| `findAttachmentsBy(TestStepFinished): List` | | | ✓ | | ✓ | -| `findFeatureBy(TestCaseStarted): Optional` | | | ✓ | | ✓ | -| `findHookBy(TestStep): Optional` | | | ✓ | | ✓ | -| `findMeta(): Optional` | | | ✓ | | ✓ | -| `findMostSevereTestStepResulBy(TestCaseStarted): Optional` | | | ✓ | | ✓ | -| `findNameOf(Pickle, NamingStrategy): String` | | | ✓ | | ✓ | -| `findPickleBy(TestCaseStarted): Optional` | | | ✓ | | ✓ | -| `findPickleStepBy(TestStep testStep): Optional` | | | ✓ | | ✓ | -| `findStepBy(PickleStep pickleStep): Optional` | | | ✓ | | ✓ | -| `findTestCaseBy(TestCaseStarted): Optional` | | | ✓ | | ✓ | -| `findTestCaseDurationBy(TestCaseStarted): Optional` | | | ✓ | | ✓ | -| `findTestCaseFinishedBy(TestCaseStarted): Optional` | | | ✓ | | ✓ | -| `findTestRunDuration(): Optional` | | | ✓ | | ✓ | -| `findTestRunFinished(): Optional` | | | ✓ | | ✓ | -| `findTestRunStarted(): Optional` | | | ✓ | | ✓ | -| `findTestStepBy(TestStepFinished): Optional` | | | ✓ | | ✓ | -| `findTestStepsFinishedBy(TestCaseStarted): List` | | | ✓ | | ✓ | -| `findTestStepFinishedAndTestStepBy(TestCaseStarted): List>` | | | ✓ | | ✓ | From cf4ca2c034a7c98e0b9311d24584cc053ab49426 Mon Sep 17 00:00:00 2001 From: "M.P. Korstanje" Date: Sat, 7 Jun 2025 00:58:48 +0200 Subject: [PATCH 13/24] Update README --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 43edfcd4..f8da7152 100644 --- a/README.md +++ b/README.md @@ -33,8 +33,8 @@ primary and foreign keys. Consumers of these messages may want to *query* the messages for certain information. For example, [cucumber-react](https://github.com/cucumber/cucumber-react) needs to know the status of -a [Step](../cucumber-messages/messages.md#io.cucumber.messages.GherkinDocument.Feature.Step) as it -is rendering the [GherkinDocument](../cucumber-messages/messages.md#io.cucumber.messages.GherkinDocument) +a [Step](https://github.com/cucumber/messages/blob/main/messages.md#step) as it +is rendering the [GherkinDocument](https://github.com/cucumber/messages/blob/main/messages.md#gherkindocument) The Query library makes this easy by providing a function to look up the status of a step, a scenario or an entire file. From 28e8ad88cf084601390742bc75e16549512b795b Mon Sep 17 00:00:00 2001 From: David Goss Date: Sat, 7 Jun 2025 09:10:43 +0100 Subject: [PATCH 14/24] empty impls of new js methods --- javascript/src/Query.ts | 40 ++++++++++++++++++++++++++++++++-------- 1 file changed, 32 insertions(+), 8 deletions(-) diff --git a/javascript/src/Query.ts b/javascript/src/Query.ts index b6be0369..b244b244 100644 --- a/javascript/src/Query.ts +++ b/javascript/src/Query.ts @@ -5,7 +5,7 @@ import { Feature, getWorstTestStepResult, GherkinDocument, - Hook, + Hook, Location, Meta, Pickle, PickleStep, @@ -20,7 +20,7 @@ import { TestStep, TestStepFinished, TestStepResult, - TestStepResultStatus, + TestStepResultStatus, TestStepStarted, TimeConversion, } from '@cucumber/messages' import { ArrayMultimap } from '@teppeis/multimaps' @@ -478,8 +478,16 @@ export default class Query { return lineage ? namingStrategy.reduce(lineage, pickle) : pickle.name } - public findPickleBy(testCaseStarted: TestCaseStarted): Pickle | undefined { - const testCase = this.findTestCaseBy(testCaseStarted) + public findLocationOf(pickle: Pickle): Location | undefined { + return undefined + } + + public findPickleBy(element: TestCaseStarted | TestStepStarted): Pickle | undefined { + if ('testCaseStartedId' in element) { + // TODO implement lookup by TestStepStarted + return undefined + } + const testCase = this.findTestCaseBy(element) assert.ok(testCase, 'Expected to find TestCase from TestCaseStarted') return this.pickleById.get(testCase.pickleId) } @@ -497,8 +505,12 @@ export default class Query { return this.stepById.get(astNodeId) } - public findTestCaseBy(testCaseStarted: TestCaseStarted): TestCase | undefined { - return this.testCaseById.get(testCaseStarted.testCaseId) + public findTestCaseBy(element: TestCaseStarted | TestStepStarted): TestCase | undefined { + if ('testCaseStartedId' in element) { + // TODO implement lookup by TestStepStarted + return undefined + } + return this.testCaseById.get(element.testCaseId) } public findTestCaseDurationBy(testCaseStarted: TestCaseStarted): Duration | undefined { @@ -512,6 +524,11 @@ export default class Query { ) } + public findTestCaseStartedBy(testStepStarted: TestStepStarted): TestCaseStarted | undefined { + // TODO implement + return undefined + } + public findTestCaseFinishedBy(testCaseStarted: TestCaseStarted): TestCaseFinished | undefined { return this.testCaseFinishedByTestCaseStartedId.get(testCaseStarted.id) } @@ -534,8 +551,15 @@ export default class Query { return this.testRunStarted } - public findTestStepBy(testStepFinished: TestStepFinished): TestStep | undefined { - return this.testStepById.get(testStepFinished.testStepId) + public findTestStepBy(element: TestStepStarted | TestStepFinished): TestStep | undefined { + return this.testStepById.get(element.testStepId) + } + + public findTestStepsStartedBy( + testCaseStarted: TestCaseStarted + ): ReadonlyArray { + // TODO implement + return [] } public findTestStepsFinishedBy( From 5c5d1e80510fd151e5db95915b778a6ca0a5cff7 Mon Sep 17 00:00:00 2001 From: David Goss Date: Sat, 7 Jun 2025 09:18:48 +0100 Subject: [PATCH 15/24] update acceptance test plumbing --- javascript/src/acceptance.spec.ts | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/javascript/src/acceptance.spec.ts b/javascript/src/acceptance.spec.ts index 04cd73dc..65d65a63 100644 --- a/javascript/src/acceptance.spec.ts +++ b/javascript/src/acceptance.spec.ts @@ -143,6 +143,8 @@ describe('Acceptance Tests', async () => { ) ), }, + findLocationOf: query.findAllPickles() + .map((pickle) => query.findLocationOf(pickle)), findPickleBy: query .findAllTestCaseStarted() .map((testCaseStarted) => query.findPickleBy(testCaseStarted)) @@ -170,7 +172,12 @@ describe('Acceptance Tests', async () => { findTestRunDuration: query.findTestRunDuration(), findTestRunFinished: query.findTestRunFinished(), findTestRunStarted: query.findTestRunStarted(), - findTestStepBy: query + findTestStepByTestStepStarted: query + .findAllTestCaseStarted() + .flatMap((testCaseStarted) => query.findTestStepsStartedBy(testCaseStarted)) + .map((testStepStarted) => query.findTestStepBy(testStepStarted)) + .map((testStep) => testStep?.id), + findTestStepByTestStepFinished: query .findAllTestCaseStarted() .flatMap((testCaseStarted) => query.findTestStepsFinishedBy(testCaseStarted)) .map((testStepFinished) => query.findTestStepBy(testStepFinished)) @@ -212,6 +219,7 @@ interface ResultsFixture { short: Array shortPickleName: Array } + findLocationOf: Array findHookBy: Array findPickleBy: Array findPickleStepBy: Array @@ -222,7 +230,8 @@ interface ResultsFixture { findTestRunDuration: Duration findTestRunFinished: TestRunFinished findTestRunStarted: TestRunStarted - findTestStepBy: Array + findTestStepByTestStepStarted: Array + findTestStepByTestStepFinished: Array findTestStepsFinishedBy: Array> findTestStepFinishedAndTestStepBy: Array<[string, string]> } @@ -246,7 +255,8 @@ const defaults: Partial = { findTestCaseBy: [], findTestCaseDurationBy: [], findTestCaseFinishedBy: [], - findTestStepBy: [], + findTestStepByTestStepStarted: [], + findTestStepByTestStepFinished: [], findTestStepsFinishedBy: [], findTestStepFinishedAndTestStepBy: [], } From 85b81e8f386117c05721c78dabbde96255aaef21 Mon Sep 17 00:00:00 2001 From: David Goss Date: Sat, 7 Jun 2025 09:44:34 +0100 Subject: [PATCH 16/24] change name in testdata files --- java/src/test/java/io/cucumber/query/QueryAcceptanceTest.java | 2 +- testdata/attachments.feature.query-results.json | 2 +- testdata/empty.feature.query-results.json | 2 +- testdata/examples-tables.feature.query-results.json | 2 +- testdata/hooks.feature.query-results.json | 2 +- testdata/minimal.feature.query-results.json | 2 +- testdata/rules.feature.query-results.json | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/java/src/test/java/io/cucumber/query/QueryAcceptanceTest.java b/java/src/test/java/io/cucumber/query/QueryAcceptanceTest.java index 2e355503..dc54e685 100644 --- a/java/src/test/java/io/cucumber/query/QueryAcceptanceTest.java +++ b/java/src/test/java/io/cucumber/query/QueryAcceptanceTest.java @@ -137,7 +137,7 @@ private static Map createQueryResults(Query query) { .map(hook -> hook.map(Hook::getId)) .filter(Optional::isPresent) .collect(toList())); - results.put("findLocationBy", query.findAllPickles().stream() + results.put("findLocationOf", query.findAllPickles().stream() .map(query::findLocationOf) .filter(Optional::isPresent) .collect(toList())); diff --git a/testdata/attachments.feature.query-results.json b/testdata/attachments.feature.query-results.json index 98d768a6..988c1074 100644 --- a/testdata/attachments.feature.query-results.json +++ b/testdata/attachments.feature.query-results.json @@ -96,7 +96,7 @@ "Attachments", "Attachments" ], - "findLocationBy" : [ + "findLocationOf" : [ { "line" : 12, "column" : 3 diff --git a/testdata/empty.feature.query-results.json b/testdata/empty.feature.query-results.json index ce23311b..645774a5 100644 --- a/testdata/empty.feature.query-results.json +++ b/testdata/empty.feature.query-results.json @@ -24,7 +24,7 @@ "findFeatureBy" : [ "Empty Scenarios" ], - "findLocationBy" : [ + "findLocationOf" : [ { "line" : 7, "column" : 3 diff --git a/testdata/examples-tables.feature.query-results.json b/testdata/examples-tables.feature.query-results.json index 9b5f8814..8380693f 100644 --- a/testdata/examples-tables.feature.query-results.json +++ b/testdata/examples-tables.feature.query-results.json @@ -40,7 +40,7 @@ "Examples Tables", "Examples Tables" ], - "findLocationBy" : [ + "findLocationOf" : [ { "line" : 19, "column" : 7 diff --git a/testdata/hooks.feature.query-results.json b/testdata/hooks.feature.query-results.json index dfedcdbf..8c4fb304 100644 --- a/testdata/hooks.feature.query-results.json +++ b/testdata/hooks.feature.query-results.json @@ -36,7 +36,7 @@ "0", "3" ], - "findLocationBy" : [ + "findLocationOf" : [ { "line" : 4, "column" : 3 diff --git a/testdata/minimal.feature.query-results.json b/testdata/minimal.feature.query-results.json index 64768f03..c4892efa 100644 --- a/testdata/minimal.feature.query-results.json +++ b/testdata/minimal.feature.query-results.json @@ -24,7 +24,7 @@ "findFeatureBy" : [ "minimal" ], - "findLocationBy" : [ + "findLocationOf" : [ { "line" : 9, "column" : 3 diff --git a/testdata/rules.feature.query-results.json b/testdata/rules.feature.query-results.json index 90b71d80..a49b48ed 100644 --- a/testdata/rules.feature.query-results.json +++ b/testdata/rules.feature.query-results.json @@ -28,7 +28,7 @@ "Usage of a `Rule`", "Usage of a `Rule`" ], - "findLocationBy" : [ + "findLocationOf" : [ { "line" : 9, "column" : 5 From 412ef9a892d9d7d0b0a65987ba22a9e7de0f8983 Mon Sep 17 00:00:00 2001 From: David Goss Date: Sat, 7 Jun 2025 09:53:38 +0100 Subject: [PATCH 17/24] add new real implementations --- javascript/src/Query.ts | 41 ++++++++++++++++++++++++++--------------- 1 file changed, 26 insertions(+), 15 deletions(-) diff --git a/javascript/src/Query.ts b/javascript/src/Query.ts index b244b244..b29ce943 100644 --- a/javascript/src/Query.ts +++ b/javascript/src/Query.ts @@ -53,6 +53,7 @@ export default class Query { private testRunStarted: TestRunStarted private testRunFinished: TestRunFinished private readonly testCaseStarted: Array = [] + private readonly testCaseStartedById: Map = new Map() private readonly lineageById: Map = new Map() private readonly stepById: Map = new Map() private readonly pickleById: Map = new Map() @@ -60,6 +61,8 @@ export default class Query { private readonly testCaseById: Map = new Map() private readonly testStepById: Map = new Map() private readonly testCaseFinishedByTestCaseStartedId: Map = new Map() + private readonly testStepStartedByTestCaseStartedId: ArrayMultimap = + new ArrayMultimap() private readonly testStepFinishedByTestCaseStartedId: ArrayMultimap = new ArrayMultimap() private readonly attachmentsByTestCaseStartedId: ArrayMultimap = @@ -87,6 +90,9 @@ export default class Query { if (envelope.testCaseStarted) { this.updateTestCaseStarted(envelope.testCaseStarted) } + if (envelope.testStepStarted) { + this.updateTestStepStarted(envelope.testStepStarted) + } if (envelope.attachment) { this.updateAttachment(envelope.attachment) } @@ -196,6 +202,7 @@ export default class Query { private updateTestCaseStarted(testCaseStarted: TestCaseStarted) { this.testCaseStarted.push(testCaseStarted) + this.testCaseStartedById.set(testCaseStarted.id, testCaseStarted) /* when a test case attempt starts besides the first one, clear all existing results @@ -211,6 +218,13 @@ export default class Query { } } + private updateTestStepStarted(testStepStarted: TestStepStarted) { + this.testStepStartedByTestCaseStartedId.put( + testStepStarted.testCaseStartedId, + testStepStarted + ) + } + private updateAttachment(attachment: Attachment) { if (attachment.testStepId) { this.attachmentsByTestStepId.put(attachment.testStepId, attachment) @@ -479,14 +493,14 @@ export default class Query { } public findLocationOf(pickle: Pickle): Location | undefined { - return undefined + const lineage = this.findLineageBy(pickle) + if (lineage?.example) { + return lineage.example.location + } + return lineage?.scenario?.location } public findPickleBy(element: TestCaseStarted | TestStepStarted): Pickle | undefined { - if ('testCaseStartedId' in element) { - // TODO implement lookup by TestStepStarted - return undefined - } const testCase = this.findTestCaseBy(element) assert.ok(testCase, 'Expected to find TestCase from TestCaseStarted') return this.pickleById.get(testCase.pickleId) @@ -506,11 +520,9 @@ export default class Query { } public findTestCaseBy(element: TestCaseStarted | TestStepStarted): TestCase | undefined { - if ('testCaseStartedId' in element) { - // TODO implement lookup by TestStepStarted - return undefined - } - return this.testCaseById.get(element.testCaseId) + const testCaseStarted = 'testCaseStartedId' in element ? this.findTestCaseStartedBy(element) : element + assert.ok(testCaseStarted, 'Expected to find TestCaseStarted by TestStepStarted') + return this.testCaseById.get(testCaseStarted.testCaseId) } public findTestCaseDurationBy(testCaseStarted: TestCaseStarted): Duration | undefined { @@ -525,8 +537,7 @@ export default class Query { } public findTestCaseStartedBy(testStepStarted: TestStepStarted): TestCaseStarted | undefined { - // TODO implement - return undefined + return this.testCaseStartedById.get(testStepStarted.testCaseStartedId) } public findTestCaseFinishedBy(testCaseStarted: TestCaseStarted): TestCaseFinished | undefined { @@ -558,8 +569,8 @@ export default class Query { public findTestStepsStartedBy( testCaseStarted: TestCaseStarted ): ReadonlyArray { - // TODO implement - return [] + // multimaps `get` implements `getOrDefault([])` behaviour internally + return [...this.testStepStartedByTestCaseStartedId.get(testCaseStarted.id)] } public findTestStepsFinishedBy( @@ -581,7 +592,7 @@ export default class Query { }) } - private findLineageBy(element: Pickle | TestCaseStarted) { + private findLineageBy(element: Pickle | TestCaseStarted): Lineage | undefined { const pickle = 'testCaseId' in element ? this.findPickleBy(element) : element const deepestAstNodeId = pickle.astNodeIds.at(-1) assert.ok(deepestAstNodeId, 'Expected Pickle to have at least one astNodeId') From 0a004e2a8d487e37b0437bdf93d82f59b7aa5997 Mon Sep 17 00:00:00 2001 From: David Goss Date: Sat, 7 Jun 2025 09:59:47 +0100 Subject: [PATCH 18/24] formatting --- javascript/src/Query.ts | 18 ++++++++---------- javascript/src/acceptance.spec.ts | 3 +-- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/javascript/src/Query.ts b/javascript/src/Query.ts index b29ce943..631d044f 100644 --- a/javascript/src/Query.ts +++ b/javascript/src/Query.ts @@ -5,7 +5,8 @@ import { Feature, getWorstTestStepResult, GherkinDocument, - Hook, Location, + Hook, + Location, Meta, Pickle, PickleStep, @@ -20,7 +21,8 @@ import { TestStep, TestStepFinished, TestStepResult, - TestStepResultStatus, TestStepStarted, + TestStepResultStatus, + TestStepStarted, TimeConversion, } from '@cucumber/messages' import { ArrayMultimap } from '@teppeis/multimaps' @@ -219,10 +221,7 @@ export default class Query { } private updateTestStepStarted(testStepStarted: TestStepStarted) { - this.testStepStartedByTestCaseStartedId.put( - testStepStarted.testCaseStartedId, - testStepStarted - ) + this.testStepStartedByTestCaseStartedId.put(testStepStarted.testCaseStartedId, testStepStarted) } private updateAttachment(attachment: Attachment) { @@ -520,7 +519,8 @@ export default class Query { } public findTestCaseBy(element: TestCaseStarted | TestStepStarted): TestCase | undefined { - const testCaseStarted = 'testCaseStartedId' in element ? this.findTestCaseStartedBy(element) : element + const testCaseStarted = + 'testCaseStartedId' in element ? this.findTestCaseStartedBy(element) : element assert.ok(testCaseStarted, 'Expected to find TestCaseStarted by TestStepStarted') return this.testCaseById.get(testCaseStarted.testCaseId) } @@ -566,9 +566,7 @@ export default class Query { return this.testStepById.get(element.testStepId) } - public findTestStepsStartedBy( - testCaseStarted: TestCaseStarted - ): ReadonlyArray { + public findTestStepsStartedBy(testCaseStarted: TestCaseStarted): ReadonlyArray { // multimaps `get` implements `getOrDefault([])` behaviour internally return [...this.testStepStartedByTestCaseStartedId.get(testCaseStarted.id)] } diff --git a/javascript/src/acceptance.spec.ts b/javascript/src/acceptance.spec.ts index 65d65a63..9820d814 100644 --- a/javascript/src/acceptance.spec.ts +++ b/javascript/src/acceptance.spec.ts @@ -143,8 +143,7 @@ describe('Acceptance Tests', async () => { ) ), }, - findLocationOf: query.findAllPickles() - .map((pickle) => query.findLocationOf(pickle)), + findLocationOf: query.findAllPickles().map((pickle) => query.findLocationOf(pickle)), findPickleBy: query .findAllTestCaseStarted() .map((testCaseStarted) => query.findPickleBy(testCaseStarted)) From 4ad96cd69d69431ddafeeb926fa7457ad073fc47 Mon Sep 17 00:00:00 2001 From: David Goss Date: Sat, 7 Jun 2025 11:11:47 +0100 Subject: [PATCH 19/24] make findLineageBy public --- javascript/src/Query.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/javascript/src/Query.ts b/javascript/src/Query.ts index 631d044f..158c9f66 100644 --- a/javascript/src/Query.ts +++ b/javascript/src/Query.ts @@ -590,7 +590,7 @@ export default class Query { }) } - private findLineageBy(element: Pickle | TestCaseStarted): Lineage | undefined { + public findLineageBy(element: Pickle | TestCaseStarted): Lineage | undefined { const pickle = 'testCaseId' in element ? this.findPickleBy(element) : element const deepestAstNodeId = pickle.astNodeIds.at(-1) assert.ok(deepestAstNodeId, 'Expected Pickle to have at least one astNodeId') From 9ba0f88e37441eb49c5d970c5cfb00b54d361d37 Mon Sep 17 00:00:00 2001 From: David Goss Date: Sat, 7 Jun 2025 11:47:48 +0100 Subject: [PATCH 20/24] use map for findAll, rework sorting --- javascript/package-lock.json | 27 ++++++++++++++- javascript/package.json | 4 ++- javascript/src/Query.ts | 62 ++++++++++++++++++++-------------- javascript/src/helpers.spec.ts | 35 ------------------- javascript/src/helpers.ts | 33 ++---------------- 5 files changed, 68 insertions(+), 93 deletions(-) delete mode 100644 javascript/src/helpers.spec.ts diff --git a/javascript/package-lock.json b/javascript/package-lock.json index deb828f8..79d3d056 100644 --- a/javascript/package-lock.json +++ b/javascript/package-lock.json @@ -9,7 +9,8 @@ "version": "13.2.0", "license": "MIT", "dependencies": { - "@teppeis/multimaps": "3.0.0" + "@teppeis/multimaps": "3.0.0", + "lodash.sortby": "^4.7.0" }, "devDependencies": { "@cucumber/compatibility-kit": "^18.0.0", @@ -21,6 +22,7 @@ "@eslint/compat": "^1.2.7", "@eslint/eslintrc": "^3.3.0", "@eslint/js": "^9.21.0", + "@types/lodash.sortby": "^4.7.9", "@types/mocha": "10.0.10", "@types/node": "22.15.29", "@typescript-eslint/eslint-plugin": "^8.24.1", @@ -685,6 +687,23 @@ "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", "dev": true }, + "node_modules/@types/lodash": { + "version": "4.17.17", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.17.tgz", + "integrity": "sha512-RRVJ+J3J+WmyOTqnz3PiBLA501eKwXl2noseKOrNo/6+XEHjTAxO4xHvxQB6QuNm+s4WRbn6rSiap8+EA+ykFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/lodash.sortby": { + "version": "4.7.9", + "resolved": "https://registry.npmjs.org/@types/lodash.sortby/-/lodash.sortby-4.7.9.tgz", + "integrity": "sha512-PDmjHnOlndLS59GofH0pnxIs+n9i4CWeXGErSB5JyNFHu2cmvW6mQOaUKjG8EDPkni14IgF8NsRW8bKvFzTm9A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/lodash": "*" + } + }, "node_modules/@types/mocha": { "version": "10.0.10", "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.10.tgz", @@ -3406,6 +3425,12 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, + "node_modules/lodash.sortby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", + "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==", + "license": "MIT" + }, "node_modules/log-symbols": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", diff --git a/javascript/package.json b/javascript/package.json index edf35093..b68c0414 100644 --- a/javascript/package.json +++ b/javascript/package.json @@ -35,6 +35,7 @@ "@eslint/compat": "^1.2.7", "@eslint/eslintrc": "^3.3.0", "@eslint/js": "^9.21.0", + "@types/lodash.sortby": "^4.7.9", "@types/mocha": "10.0.10", "@types/node": "22.15.29", "@typescript-eslint/eslint-plugin": "^8.24.1", @@ -59,7 +60,8 @@ "@cucumber/messages": "*" }, "dependencies": { - "@teppeis/multimaps": "3.0.0" + "@teppeis/multimaps": "3.0.0", + "lodash.sortby": "^4.7.0" }, "overrides": { "@cucumber/messages": "27.2.0" diff --git a/javascript/src/Query.ts b/javascript/src/Query.ts index 158c9f66..ccda94dc 100644 --- a/javascript/src/Query.ts +++ b/javascript/src/Query.ts @@ -26,8 +26,9 @@ import { TimeConversion, } from '@cucumber/messages' import { ArrayMultimap } from '@teppeis/multimaps' +import sortBy from 'lodash.sortby' -import { assert, comparatorBy, comparatorById, comparatorByStatus } from './helpers' +import { assert, statusOrdinal } from './helpers' import { Lineage, NamingStrategy } from './Lineage' export default class Query { @@ -54,7 +55,6 @@ export default class Query { private meta: Meta private testRunStarted: TestRunStarted private testRunFinished: TestRunFinished - private readonly testCaseStarted: Array = [] private readonly testCaseStartedById: Map = new Map() private readonly lineageById: Map = new Map() private readonly stepById: Map = new Map() @@ -203,7 +203,6 @@ export default class Query { } private updateTestCaseStarted(testCaseStarted: TestCaseStarted) { - this.testCaseStarted.push(testCaseStarted) this.testCaseStartedById.set(testCaseStarted.id, testCaseStarted) /* @@ -404,10 +403,12 @@ export default class Query { [TestStepResultStatus.UNKNOWN]: 0, } for (const testCaseStarted of this.findAllTestCaseStarted()) { - const mostSevereResult = this.findTestStepFinishedAndTestStepBy(testCaseStarted) - .map(([testStepFinished]) => testStepFinished.testStepResult) - .sort(comparatorByStatus) - .at(-1) + const mostSevereResult = sortBy( + this.findTestStepFinishedAndTestStepBy(testCaseStarted).map( + ([testStepFinished]) => testStepFinished.testStepResult + ), + [(testStepResult) => statusOrdinal(testStepResult.status)] + ).at(-1) if (mostSevereResult) { result[mostSevereResult.status]++ } @@ -421,20 +422,27 @@ export default class Query { public findAllPickles(): ReadonlyArray { const pickles = [...this.pickleById.values()] - return pickles.sort(comparatorById) + return sortBy(pickles, 'id') } public findAllPickleSteps(): ReadonlyArray { const pickleSteps = [...this.pickleStepById.values()] - return pickleSteps.sort(comparatorById) + return sortBy(pickleSteps, 'id') } public findAllTestCaseStarted(): ReadonlyArray { - return this.testCaseStarted.filter((testCaseStarted) => { - const testCaseFinished = this.testCaseFinishedByTestCaseStartedId.get(testCaseStarted.id) - // only include if not yet finished OR won't be retried - return !testCaseFinished?.willBeRetried - }) + return sortBy( + [...this.testCaseStartedById.values()].filter((testCaseStarted) => { + const testCaseFinished = this.testCaseFinishedByTestCaseStartedId.get(testCaseStarted.id) + // only include if not yet finished OR won't be retried + return !testCaseFinished?.willBeRetried + }), + [ + (testCaseStarted) => + TimeConversion.timestampToMillisecondsSinceEpoch(testCaseStarted.timestamp), + 'id', + ] + ) } public findAllTestCaseStartedGroupedByFeature(): Map< @@ -442,18 +450,20 @@ export default class Query { ReadonlyArray > { const results = new Map() - this.findAllTestCaseStarted() - .map((testCaseStarted) => [this.findLineageBy(testCaseStarted), testCaseStarted] as const) - .sort(([a], [b]) => comparatorBy(a.gherkinDocument, b.gherkinDocument, 'uri')) - .forEach(([{ feature }, testCaseStarted]) => { - results.set(feature, [...(results.get(feature) ?? []), testCaseStarted]) - }) + sortBy( + this.findAllTestCaseStarted().map( + (testCaseStarted) => [this.findLineageBy(testCaseStarted), testCaseStarted] as const + ), + [([lineage]) => lineage.gherkinDocument.uri] + ).forEach(([{ feature }, testCaseStarted]) => { + results.set(feature, [...(results.get(feature) ?? []), testCaseStarted]) + }) return results } public findAllTestSteps(): ReadonlyArray { const testSteps = [...this.testStepById.values()] - return testSteps.sort(comparatorById) + return sortBy(testSteps, 'id') } public findAttachmentsBy(testStepFinished: TestStepFinished): ReadonlyArray { @@ -480,10 +490,12 @@ export default class Query { public findMostSevereTestStepResultBy( testCaseStarted: TestCaseStarted ): TestStepResult | undefined { - return this.findTestStepFinishedAndTestStepBy(testCaseStarted) - .map(([testStepFinished]) => testStepFinished.testStepResult) - .sort(comparatorByStatus) - .at(-1) + return sortBy( + this.findTestStepFinishedAndTestStepBy(testCaseStarted).map( + ([testStepFinished]) => testStepFinished.testStepResult + ), + [(testStepResult) => statusOrdinal(testStepResult.status)] + ).at(-1) } public findNameOf(pickle: Pickle, namingStrategy: NamingStrategy): string { diff --git a/javascript/src/helpers.spec.ts b/javascript/src/helpers.spec.ts deleted file mode 100644 index 89cc5917..00000000 --- a/javascript/src/helpers.spec.ts +++ /dev/null @@ -1,35 +0,0 @@ -import assert from 'node:assert' - -import { TestStepResult, TestStepResultStatus } from '@cucumber/messages' - -import { comparatorByStatus } from './helpers' - -describe('comparatorByStatus', () => { - function resultWithStatus(status: TestStepResultStatus) { - return { status } as TestStepResult - } - - it('puts the more severe status after the less severe', () => { - assert.strictEqual( - comparatorByStatus( - resultWithStatus(TestStepResultStatus.PASSED), - resultWithStatus(TestStepResultStatus.FAILED) - ), - -1 - ) - assert.strictEqual( - comparatorByStatus( - resultWithStatus(TestStepResultStatus.FAILED), - resultWithStatus(TestStepResultStatus.PASSED) - ), - 1 - ) - assert.strictEqual( - comparatorByStatus( - resultWithStatus(TestStepResultStatus.FAILED), - resultWithStatus(TestStepResultStatus.FAILED) - ), - 0 - ) - }) -}) diff --git a/javascript/src/helpers.ts b/javascript/src/helpers.ts index 5cfe6dd4..52c0f23c 100644 --- a/javascript/src/helpers.ts +++ b/javascript/src/helpers.ts @@ -1,35 +1,6 @@ -import { TestStepResult, TestStepResultStatus } from '@cucumber/messages' +import { TestStepResultStatus } from '@cucumber/messages' -interface WithId { - id: string | number -} - -export function comparatorById(a: WithId, b: WithId) { - return comparatorBy(a, b, 'id') -} - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function comparatorBy(a: any, b: any, key: string) { - if (a[key] < b[key]) { - return -1 - } - if (a[key] > b[key]) { - return 1 - } - return 0 -} - -export function comparatorByStatus(a: TestStepResult, b: TestStepResult) { - if (ordinal(a.status) < ordinal(b.status)) { - return -1 - } - if (ordinal(a.status) > ordinal(b.status)) { - return 1 - } - return 0 -} - -function ordinal(status: TestStepResultStatus) { +export function statusOrdinal(status: TestStepResultStatus) { return [ TestStepResultStatus.UNKNOWN, TestStepResultStatus.PASSED, From 1871d6a6f5b819d4e4c352dc9c9fabb6317f5214 Mon Sep 17 00:00:00 2001 From: David Goss Date: Sat, 7 Jun 2025 11:50:29 +0100 Subject: [PATCH 21/24] update changelog again --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 40930e46..b60f635e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,7 +15,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - New method `findTestStepsStartedBy(TestCaseStarted)` ([#76](https://github.com/cucumber/query/pull/76)) ### Fixed -- [Java] `Query.findAllTestCaseStarted` orders events by `timestamp` and `id`. +- `Query.findAllTestCaseStarted` orders events by `timestamp` and `id` ([#76](https://github.com/cucumber/query/pull/76)) ## [13.2.0] - 2025-02-02 ### Changed From a199d25f926c63ee0dc7712cd6ca12dce601a014 Mon Sep 17 00:00:00 2001 From: David Goss Date: Sat, 7 Jun 2025 11:51:23 +0100 Subject: [PATCH 22/24] fix readme link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f8da7152..4a33b541 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ using `id` fields. It's a bit similar to rows in a relational database, with primary and foreign keys. Consumers of these messages may want to *query* the messages for certain information. -For example, [cucumber-react](https://github.com/cucumber/cucumber-react) needs to know the status of +For example, [@cucumber/react-components](https://github.com/cucumber/react-components) needs to know the status of a [Step](https://github.com/cucumber/messages/blob/main/messages.md#step) as it is rendering the [GherkinDocument](https://github.com/cucumber/messages/blob/main/messages.md#gherkindocument) From a61857ca75c53bd2d2a5e47142b1e553a9b5edea Mon Sep 17 00:00:00 2001 From: David Goss Date: Sat, 7 Jun 2025 11:58:41 +0100 Subject: [PATCH 23/24] use sortBy properly --- javascript/src/Query.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/javascript/src/Query.ts b/javascript/src/Query.ts index ccda94dc..265b9c94 100644 --- a/javascript/src/Query.ts +++ b/javascript/src/Query.ts @@ -422,12 +422,12 @@ export default class Query { public findAllPickles(): ReadonlyArray { const pickles = [...this.pickleById.values()] - return sortBy(pickles, 'id') + return sortBy(pickles, ['id']) } public findAllPickleSteps(): ReadonlyArray { const pickleSteps = [...this.pickleStepById.values()] - return sortBy(pickleSteps, 'id') + return sortBy(pickleSteps, ['id']) } public findAllTestCaseStarted(): ReadonlyArray { @@ -463,7 +463,7 @@ export default class Query { public findAllTestSteps(): ReadonlyArray { const testSteps = [...this.testStepById.values()] - return sortBy(testSteps, 'id') + return sortBy(testSteps, ['id']) } public findAttachmentsBy(testStepFinished: TestStepFinished): ReadonlyArray { From 846c6e00db89e249166a5a623f4a2e163cc87fb6 Mon Sep 17 00:00:00 2001 From: David Goss Date: Sat, 7 Jun 2025 12:11:56 +0100 Subject: [PATCH 24/24] test for test case ordering --- javascript/src/Query.spec.ts | 76 ++++++++++++++++++++++++++++++++++++ javascript/src/Query.ts | 12 +++--- 2 files changed, 83 insertions(+), 5 deletions(-) diff --git a/javascript/src/Query.spec.ts b/javascript/src/Query.spec.ts index f1ecb404..a29641b2 100644 --- a/javascript/src/Query.spec.ts +++ b/javascript/src/Query.spec.ts @@ -12,6 +12,7 @@ import { import { GherkinStreams } from '@cucumber/gherkin-streams' import { Query as GherkinQuery } from '@cucumber/gherkin-utils' import * as messages from '@cucumber/messages' +import { TestCaseStarted } from '@cucumber/messages' import { pipeline, Readable, Writable } from 'stream' import { promisify } from 'util' @@ -27,6 +28,81 @@ describe('Query', () => { cucumberQuery = new Query() }) + describe('#findAllTestCaseStarted', () => { + it('retains timestamp order', () => { + const testCasesStarted: TestCaseStarted[] = [ + { + id: '1', + testCaseId: '1', + attempt: 0, + timestamp: { + seconds: 1, + nanos: 1, + }, + }, + { + id: '2', + testCaseId: '2', + attempt: 0, + timestamp: { + seconds: 2, + nanos: 1, + }, + }, + { + id: '3', + testCaseId: '3', + attempt: 0, + timestamp: { + seconds: 2, + nanos: 3, + }, + }, + ] + + testCasesStarted + .map((testCaseStarted) => ({ testCaseStarted })) + .reverse() + .forEach((envelope) => { + cucumberQuery.update(envelope) + }) + + assert.deepStrictEqual(cucumberQuery.findAllTestCaseStarted(), testCasesStarted) + }) + + it('uses id as tie breaker', () => { + const testCasesStarted: TestCaseStarted[] = [ + { + id: '1', + testCaseId: '1', + attempt: 0, + timestamp: { + seconds: 1, + nanos: 1, + }, + }, + { + id: '2', + testCaseId: '2', + attempt: 0, + timestamp: { + seconds: 1, + nanos: 1, + }, + }, + ] + + testCasesStarted + .map((testCaseStarted) => ({ testCaseStarted })) + .reverse() + .forEach((envelope) => { + cucumberQuery.update(envelope) + }) + + assert.deepStrictEqual(cucumberQuery.findAllTestCaseStarted(), testCasesStarted) + }) + }) + describe('#getPickleStepTestStepResults(pickleStepIds)', () => { it('returns a single UNKNOWN when the list is empty', () => { const results = cucumberQuery.getPickleTestStepResults([]) diff --git a/javascript/src/Query.ts b/javascript/src/Query.ts index 265b9c94..0d704b19 100644 --- a/javascript/src/Query.ts +++ b/javascript/src/Query.ts @@ -211,11 +211,13 @@ export default class Query { (applies to legacy pickle-oriented query methods only) */ const testCase = this.testCaseById.get(testCaseStarted.testCaseId) - this.testStepResultByPickleId.delete(testCase.pickleId) - for (const testStep of testCase.testSteps) { - this.testStepResultsByPickleStepId.delete(testStep.pickleStepId) - this.testStepResultsbyTestStepId.delete(testStep.id) - this.attachmentsByTestStepId.delete(testStep.id) + if (testCase) { + this.testStepResultByPickleId.delete(testCase.pickleId) + for (const testStep of testCase.testSteps) { + this.testStepResultsByPickleStepId.delete(testStep.pickleStepId) + this.testStepResultsbyTestStepId.delete(testStep.id) + this.attachmentsByTestStepId.delete(testStep.id) + } } }