From 15cfe0fde5f3a0dd6bc9497783b3445195765d07 Mon Sep 17 00:00:00 2001 From: "M.P. Korstanje" Date: Thu, 17 Jul 2025 14:14:16 +0200 Subject: [PATCH 1/3] Message based rerun formatter --- cucumber-bom/pom.xml | 2 +- .../cucumber/core/plugin/RerunFormatter.java | 99 ++++++++++++------- 2 files changed, 63 insertions(+), 38 deletions(-) diff --git a/cucumber-bom/pom.xml b/cucumber-bom/pom.xml index 168d68414c..cc487d3378 100644 --- a/cucumber-bom/pom.xml +++ b/cucumber-bom/pom.xml @@ -20,7 +20,7 @@ 0.8.0 28.0.0 0.3.0 - 13.5.0 + 13.5.1-SNAPSHOT 6.1.2 0.4.0 diff --git a/cucumber-core/src/main/java/io/cucumber/core/plugin/RerunFormatter.java b/cucumber-core/src/main/java/io/cucumber/core/plugin/RerunFormatter.java index e3063116f3..2395472b75 100644 --- a/cucumber-core/src/main/java/io/cucumber/core/plugin/RerunFormatter.java +++ b/cucumber-core/src/main/java/io/cucumber/core/plugin/RerunFormatter.java @@ -1,22 +1,28 @@ package io.cucumber.core.plugin; import io.cucumber.core.feature.FeatureWithLines; +import io.cucumber.messages.types.Envelope; +import io.cucumber.messages.types.TestCaseFinished; +import io.cucumber.messages.types.TestStepResult; +import io.cucumber.messages.types.TestStepResultStatus; import io.cucumber.plugin.ConcurrentEventListener; import io.cucumber.plugin.event.EventPublisher; -import io.cucumber.plugin.event.TestCase; -import io.cucumber.plugin.event.TestCaseFinished; -import io.cucumber.plugin.event.TestRunFinished; +import io.cucumber.query.Query; import java.io.File; import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; import java.net.URI; import java.net.URISyntaxException; -import java.util.ArrayList; -import java.util.Collection; +import java.nio.charset.StandardCharsets; +import java.util.HashSet; import java.util.LinkedHashMap; import java.util.Map; +import java.util.Set; import static io.cucumber.core.feature.FeatureWithLines.create; +import static java.util.Objects.requireNonNull; /** * Formatter for reporting all failed test cases and print their locations @@ -24,42 +30,19 @@ */ public final class RerunFormatter implements ConcurrentEventListener { - private final UTF8PrintWriter out; - private final Map> featureAndFailedLinesMapping = new LinkedHashMap<>(); + private final PrintWriter writer; + private final Map> featureAndFailedLinesMapping = new LinkedHashMap<>(); + private final Query query = new Query(); public RerunFormatter(OutputStream out) { - this.out = new UTF8PrintWriter(out); + this.writer = createPrintWriter(out); } - @Override - public void setEventPublisher(EventPublisher publisher) { - publisher.registerHandlerFor(TestCaseFinished.class, this::handleTestCaseFinished); - publisher.registerHandlerFor(TestRunFinished.class, event -> finishReport()); - } - - private void handleTestCaseFinished(TestCaseFinished event) { - if (!event.getResult().getStatus().isOk()) { - recordTestFailed(event.getTestCase()); - } - } - - private void finishReport() { - for (Map.Entry> entry : featureAndFailedLinesMapping.entrySet()) { - FeatureWithLines featureWithLines = create(relativize(entry.getKey()), entry.getValue()); - out.println(featureWithLines.toString()); - } - - out.close(); - } - - private void recordTestFailed(TestCase testCase) { - URI uri = testCase.getUri(); - Collection failedTestCaseLines = getFailedTestCaseLines(uri); - failedTestCaseLines.add(testCase.getLocation().getLine()); - } - - private Collection getFailedTestCaseLines(URI uri) { - return featureAndFailedLinesMapping.computeIfAbsent(uri, k -> new ArrayList<>()); + private static PrintWriter createPrintWriter(OutputStream out) { + return new PrintWriter( + new OutputStreamWriter( + requireNonNull(out), + StandardCharsets.UTF_8)); } static URI relativize(URI uri) { @@ -79,4 +62,46 @@ static URI relativize(URI uri) { throw new IllegalArgumentException(e.getMessage(), e); } } + + @Override + public void setEventPublisher(EventPublisher publisher) { + publisher.registerHandlerFor(Envelope.class, event -> { + query.update(event); + event.getTestCaseFinished().ifPresent(this::handleTestCaseFinished); + event.getTestRunFinished().ifPresent(testRunFinished -> finishReport()); + }); + } + + private void handleTestCaseFinished(TestCaseFinished event) { + TestStepResultStatus testStepResultStatus = query.findMostSevereTestStepResultBy(event) + .map(TestStepResult::getStatus) + // By definition + .orElse(TestStepResultStatus.PASSED); + + if (testStepResultStatus == TestStepResultStatus.PASSED + || testStepResultStatus == TestStepResultStatus.SKIPPED) { + return; + } + + query.findPickleBy(event).ifPresent(pickle -> { + Set lines = featureAndFailedLinesMapping + .computeIfAbsent(pickle.getUri(), s -> new HashSet<>()); + query.findLocationOf(pickle).ifPresent(location -> { + // TODO: Messages are silly + lines.add((int) (long) location.getLine()); + }); + }); + } + + private void finishReport() { + for (Map.Entry> entry : featureAndFailedLinesMapping.entrySet()) { + String key = entry.getKey(); + // TODO: Should these be relative? + FeatureWithLines featureWithLines = create(relativize(URI.create(key)), entry.getValue()); + writer.println(featureWithLines); + } + + writer.close(); + } + } From cf3dc374bbca5eb2eeb9f86b10ef70c27dde6c62 Mon Sep 17 00:00:00 2001 From: "M.P. Korstanje" Date: Thu, 17 Jul 2025 19:21:47 +0200 Subject: [PATCH 2/3] Simplify with better messages --- cucumber-bom/pom.xml | 4 +- .../cucumber/core/plugin/RerunFormatter.java | 105 +++++++++++++++--- .../cucumber/core/plugin/TeamCityPlugin.java | 2 +- 3 files changed, 91 insertions(+), 20 deletions(-) diff --git a/cucumber-bom/pom.xml b/cucumber-bom/pom.xml index cc487d3378..33fbae112e 100644 --- a/cucumber-bom/pom.xml +++ b/cucumber-bom/pom.xml @@ -15,10 +15,10 @@ 10.0.1 18.0.1 - 33.0.0 + 33.0.1-SNAPSHOT 21.13.0 0.8.0 - 28.0.0 + 28.0.1-SNAPSHOT 0.3.0 13.5.1-SNAPSHOT 6.1.2 diff --git a/cucumber-core/src/main/java/io/cucumber/core/plugin/RerunFormatter.java b/cucumber-core/src/main/java/io/cucumber/core/plugin/RerunFormatter.java index 2395472b75..581d1e2f89 100644 --- a/cucumber-core/src/main/java/io/cucumber/core/plugin/RerunFormatter.java +++ b/cucumber-core/src/main/java/io/cucumber/core/plugin/RerunFormatter.java @@ -2,12 +2,15 @@ import io.cucumber.core.feature.FeatureWithLines; import io.cucumber.messages.types.Envelope; +import io.cucumber.messages.types.Pickle; +import io.cucumber.messages.types.TestCase; import io.cucumber.messages.types.TestCaseFinished; -import io.cucumber.messages.types.TestStepResult; +import io.cucumber.messages.types.TestCaseStarted; +import io.cucumber.messages.types.TestRunFinished; +import io.cucumber.messages.types.TestStepFinished; import io.cucumber.messages.types.TestStepResultStatus; import io.cucumber.plugin.ConcurrentEventListener; import io.cucumber.plugin.event.EventPublisher; -import io.cucumber.query.Query; import java.io.File; import java.io.OutputStream; @@ -16,12 +19,21 @@ import java.net.URI; import java.net.URISyntaxException; import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; import java.util.HashSet; -import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; import static io.cucumber.core.feature.FeatureWithLines.create; +import static io.cucumber.messages.types.TestStepResultStatus.PASSED; +import static io.cucumber.messages.types.TestStepResultStatus.SKIPPED; +import static java.util.Collections.emptyList; +import static java.util.Comparator.comparing; import static java.util.Objects.requireNonNull; /** @@ -30,9 +42,9 @@ */ public final class RerunFormatter implements ConcurrentEventListener { - private final PrintWriter writer; - private final Map> featureAndFailedLinesMapping = new LinkedHashMap<>(); private final Query query = new Query(); + private final Map> featureAndFailedLinesMapping = new HashMap<>(); + private final PrintWriter writer; public RerunFormatter(OutputStream out) { this.writer = createPrintWriter(out); @@ -68,32 +80,30 @@ public void setEventPublisher(EventPublisher publisher) { publisher.registerHandlerFor(Envelope.class, event -> { query.update(event); event.getTestCaseFinished().ifPresent(this::handleTestCaseFinished); - event.getTestRunFinished().ifPresent(testRunFinished -> finishReport()); + event.getTestRunFinished().ifPresent(this::handleTestRunFinished); }); } + private void handleTestCaseFinished(TestCaseFinished event) { - TestStepResultStatus testStepResultStatus = query.findMostSevereTestStepResultBy(event) - .map(TestStepResult::getStatus) + TestStepResultStatus status = query.findMostSevereTestStepResultBy(event) // By definition - .orElse(TestStepResultStatus.PASSED); - - if (testStepResultStatus == TestStepResultStatus.PASSED - || testStepResultStatus == TestStepResultStatus.SKIPPED) { + .orElse(PASSED); + if (status == PASSED || status == SKIPPED) { return; } - query.findPickleBy(event).ifPresent(pickle -> { - Set lines = featureAndFailedLinesMapping - .computeIfAbsent(pickle.getUri(), s -> new HashSet<>()); - query.findLocationOf(pickle).ifPresent(location -> { + // Adds the entire feature for rerunning + Set lines = featureAndFailedLinesMapping.computeIfAbsent(pickle.getUri(), s -> new HashSet<>()); + pickle.getLocation().ifPresent(location -> { + // Adds the specific scenarios // TODO: Messages are silly lines.add((int) (long) location.getLine()); }); }); } - private void finishReport() { + private void handleTestRunFinished(TestRunFinished testRunFinished) { for (Map.Entry> entry : featureAndFailedLinesMapping.entrySet()) { String key = entry.getKey(); // TODO: Should these be relative? @@ -104,4 +114,65 @@ private void finishReport() { writer.close(); } + /** + * Miniaturized version of Cucumber Query. + *

+ * The rerun plugin only needs a few things. + */ + private static class Query { + + private final Map testCaseById = new HashMap<>(); + private final Map> testStepsResultStatusByTestCaseStartedId = new HashMap<>(); + private final Map testCaseStartedById = new HashMap<>(); + private final Map pickleById = new HashMap<>(); + + void update(Envelope envelope) { + envelope.getPickle().ifPresent(this::updatePickle); + envelope.getTestCase().ifPresent(this::updateTestCase); + envelope.getTestCaseStarted().ifPresent(this::updateTestCaseStarted); + envelope.getTestStepFinished().ifPresent(this::updateTestStepFinished); + } + + private void updatePickle(Pickle event) { + pickleById.put(event.getId(), event); + } + + private void updateTestCase(TestCase event) { + testCaseById.put(event.getId(), event); + } + + private void updateTestCaseStarted(TestCaseStarted testCaseStarted) { + testCaseStartedById.put(testCaseStarted.getId(), testCaseStarted); + } + + private void updateTestStepFinished(TestStepFinished event) { + String testCaseStartedId = event.getTestCaseStartedId(); + testStepsResultStatusByTestCaseStartedId.computeIfAbsent(testCaseStartedId, s -> new ArrayList<>()) + .add(event.getTestStepResult().getStatus()); + } + + public Optional findMostSevereTestStepResultBy(TestCaseFinished testCaseFinished) { + List statuses = testStepsResultStatusByTestCaseStartedId + .getOrDefault(testCaseFinished.getTestCaseStartedId(), emptyList()); + if (statuses.isEmpty()) { + return Optional.empty(); + } + return Optional.of(Collections.max(statuses, comparing(Enum::ordinal))); + } + + public Optional findPickleBy(TestCaseFinished testCaseFinished) { + String testCaseStartedId = testCaseFinished.getTestCaseStartedId(); + TestCaseStarted testCaseStarted = testCaseStartedById.get(testCaseStartedId); + if (testCaseStarted == null) { + return Optional.empty(); + } + TestCase testCase = testCaseById.get(testCaseStarted.getTestCaseId()); + if (testCase == null) { + return Optional.empty(); + } + return Optional.ofNullable(pickleById.get(testCase.getPickleId())); + } + + } + } diff --git a/cucumber-core/src/main/java/io/cucumber/core/plugin/TeamCityPlugin.java b/cucumber-core/src/main/java/io/cucumber/core/plugin/TeamCityPlugin.java index 24d32eb422..85ac95c626 100644 --- a/cucumber-core/src/main/java/io/cucumber/core/plugin/TeamCityPlugin.java +++ b/cucumber-core/src/main/java/io/cucumber/core/plugin/TeamCityPlugin.java @@ -379,7 +379,7 @@ private static String getHookName(Hook hook) { } private Optional findSnippets(Pickle pickle) { - return query.findLocationOf(pickle) + return pickle.getLocation() .map(location -> { URI uri = URI.create(pickle.getUri()); List suggestionForTestCase = suggestions.stream() From fde01500e74bcf9109d2455256b61f5b2663b47b Mon Sep 17 00:00:00 2001 From: "M.P. Korstanje" Date: Thu, 17 Jul 2025 19:25:34 +0200 Subject: [PATCH 3/3] Simplify with better messages --- .../src/main/java/io/cucumber/core/plugin/RerunFormatter.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cucumber-core/src/main/java/io/cucumber/core/plugin/RerunFormatter.java b/cucumber-core/src/main/java/io/cucumber/core/plugin/RerunFormatter.java index 581d1e2f89..8944f28c9b 100644 --- a/cucumber-core/src/main/java/io/cucumber/core/plugin/RerunFormatter.java +++ b/cucumber-core/src/main/java/io/cucumber/core/plugin/RerunFormatter.java @@ -37,8 +37,7 @@ import static java.util.Objects.requireNonNull; /** - * Formatter for reporting all failed test cases and print their locations - * Failed means: results that make the exit code non-zero. + * Formatter for reporting all failed test cases and print their locations. */ public final class RerunFormatter implements ConcurrentEventListener {