diff --git a/cucumber-bom/pom.xml b/cucumber-bom/pom.xml index 168d68414c..33fbae112e 100644 --- a/cucumber-bom/pom.xml +++ b/cucumber-bom/pom.xml @@ -15,12 +15,12 @@ 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.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..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 @@ -1,65 +1,59 @@ package io.cucumber.core.plugin; 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.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.plugin.event.TestCase; -import io.cucumber.plugin.event.TestCaseFinished; -import io.cucumber.plugin.event.TestRunFinished; 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.nio.charset.StandardCharsets; import java.util.ArrayList; -import java.util.Collection; -import java.util.LinkedHashMap; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +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; /** - * 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 { - private final UTF8PrintWriter out; - 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.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 +73,105 @@ 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(this::handleTestRunFinished); + }); + } + + + private void handleTestCaseFinished(TestCaseFinished event) { + TestStepResultStatus status = query.findMostSevereTestStepResultBy(event) + // By definition + .orElse(PASSED); + if (status == PASSED || status == SKIPPED) { + return; + } + query.findPickleBy(event).ifPresent(pickle -> { + // 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 handleTestRunFinished(TestRunFinished testRunFinished) { + 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(); + } + + /** + * 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()