From ea3bb60ed9fdf1f10abd0e46112d226aee3f94c8 Mon Sep 17 00:00:00 2001 From: "M.P. Korstanje" Date: Fri, 5 Apr 2024 15:29:45 +0200 Subject: [PATCH] Use Cucumber Query to query messages (#31) Extracted the Query object to https://github.com/cucumber/query/tree/main --- CHANGELOG.md | 2 + java/pom.xml | 5 + .../junitxmlformatter/GherkinAstNodes.java | 63 ----- .../io/cucumber/junitxmlformatter/Query.java | 262 ------------------ .../junitxmlformatter/XmlReportData.java | 58 ++-- .../junitxmlformatter/XmlReportWriter.java | 4 +- 6 files changed, 27 insertions(+), 367 deletions(-) delete mode 100644 java/src/main/java/io/cucumber/junitxmlformatter/GherkinAstNodes.java delete mode 100644 java/src/main/java/io/cucumber/junitxmlformatter/Query.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ab9a41..96c3f6d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Changed +- Extracted common code to [Cucumber Query](https://github.com/cucumber/query/tree/main) ([#31](https://github.com/cucumber/cucumber-junit-xml-formatter/pull/31), M.P. Korstanje) ## [0.3.0] - 2024-03-23 ### Added diff --git a/java/pom.xml b/java/pom.xml index 26e3495..583fd54 100644 --- a/java/pom.xml +++ b/java/pom.xml @@ -53,6 +53,11 @@ messages [24.0.0,25.0.0) + + io.cucumber + query + [12.1.1,13.0.0) + com.fasterxml.jackson.core diff --git a/java/src/main/java/io/cucumber/junitxmlformatter/GherkinAstNodes.java b/java/src/main/java/io/cucumber/junitxmlformatter/GherkinAstNodes.java deleted file mode 100644 index b4e56e5..0000000 --- a/java/src/main/java/io/cucumber/junitxmlformatter/GherkinAstNodes.java +++ /dev/null @@ -1,63 +0,0 @@ -package io.cucumber.junitxmlformatter; - -import io.cucumber.messages.types.Examples; -import io.cucumber.messages.types.Feature; -import io.cucumber.messages.types.Rule; -import io.cucumber.messages.types.Scenario; -import io.cucumber.messages.types.TableRow; - -import java.util.Optional; - -import static java.util.Objects.requireNonNull; - -class GherkinAstNodes { - private final Feature feature; - private final Rule rule; - private final Scenario scenario; - private final Examples examples; - private final TableRow example; - private final Integer examplesIndex; - private final Integer exampleIndex; - - public GherkinAstNodes(Feature feature, Rule rule, Scenario scenario) { - this(feature, rule, scenario, null, null, null, null); - } - - public GherkinAstNodes(Feature feature, Rule rule, Scenario scenario, Integer examplesIndex, Examples examples, Integer exampleIndex, TableRow example) { - this.feature = requireNonNull(feature); - this.rule = rule; - this.scenario = requireNonNull(scenario); - this.examplesIndex = examplesIndex; - this.examples = examples; - this.exampleIndex = exampleIndex; - this.example = example; - } - - public Feature feature() { - return feature; - } - - public Optional rule() { - return Optional.ofNullable(rule); - } - - public Scenario scenario() { - return scenario; - } - - public Optional examples() { - return Optional.ofNullable(examples); - } - - public Optional example() { - return Optional.ofNullable(example); - } - - public Optional examplesIndex() { - return Optional.ofNullable(examplesIndex); - } - - public Optional exampleIndex() { - return Optional.ofNullable(exampleIndex); - } -} diff --git a/java/src/main/java/io/cucumber/junitxmlformatter/Query.java b/java/src/main/java/io/cucumber/junitxmlformatter/Query.java deleted file mode 100644 index be1e090..0000000 --- a/java/src/main/java/io/cucumber/junitxmlformatter/Query.java +++ /dev/null @@ -1,262 +0,0 @@ -package io.cucumber.junitxmlformatter; - -import io.cucumber.messages.Convertor; -import io.cucumber.messages.types.Envelope; -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.PickleStep; -import io.cucumber.messages.types.Rule; -import io.cucumber.messages.types.Scenario; -import io.cucumber.messages.types.Step; -import io.cucumber.messages.types.TableRow; -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.TestRunStarted; -import io.cucumber.messages.types.TestStep; -import io.cucumber.messages.types.TestStepFinished; -import io.cucumber.messages.types.TestStepResult; -import io.cucumber.messages.types.Timestamp; - -import java.time.Duration; -import java.util.AbstractMap.SimpleEntry; -import java.util.ArrayList; -import java.util.Comparator; -import java.util.Deque; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentLinkedDeque; -import java.util.function.BiFunction; - -import static java.util.Collections.emptyList; -import static java.util.Comparator.comparing; -import static java.util.Comparator.nullsFirst; -import static java.util.Objects.requireNonNull; -import static java.util.Optional.ofNullable; -import static java.util.stream.Collectors.toList; - -/** - * Given one Cucumber Message, find another. - *

- * This class is effectively a simple in memory database. It can be updated in - * real time through the {@link #update(Envelope)} method. Queries can be made - * while the test run is incomplete - and this will of-course return incomplete - * results. - *

- * It is safe to query and update concurrently. - * - * @see Cucumber Messages - Message Overview - */ -class Query { - private final Comparator testStepResultComparator = nullsFirst(comparing(o -> o.getStatus().ordinal())); - private final Deque testCaseStarted = new ConcurrentLinkedDeque<>(); - private final Map testCaseFinishedByTestCaseStartedId = new ConcurrentHashMap<>(); - private final Map> testStepsFinishedByTestCaseStartedId = new ConcurrentHashMap<>(); - private final Map pickleById = new ConcurrentHashMap<>(); - private final Map testCaseById = new ConcurrentHashMap<>(); - private final Map stepById = new ConcurrentHashMap<>(); - private final Map testStepById = new ConcurrentHashMap<>(); - private final Map pickleStepById = new ConcurrentHashMap<>(); - private final Map gherkinAstNodesById = new ConcurrentHashMap<>(); - private TestRunStarted testRunStarted; - private TestRunFinished testRunFinished; - - public List findAllTestCaseStarted() { - // Concurrency - return new ArrayList<>(testCaseStarted); - } - - public Optional findGherkinAstNodesBy(Pickle pickle) { - requireNonNull(pickle); - List astNodeIds = pickle.getAstNodeIds(); - String pickleAstNodeId = astNodeIds.get(astNodeIds.size() - 1); - return Optional.ofNullable(gherkinAstNodesById.get(pickleAstNodeId)); - } - - public Optional findGherkinAstNodesBy(TestCaseStarted testCaseStarted) { - return findPickleBy(testCaseStarted) - .flatMap(this::findGherkinAstNodesBy); - } - - public Optional findMostSevereTestStepResultStatusBy(TestCaseStarted testCaseStarted) { - requireNonNull(testCaseStarted); - return findTestStepsFinishedBy(testCaseStarted) - .stream() - .map(TestStepFinished::getTestStepResult) - .max(testStepResultComparator); - } - - public Optional findPickleBy(TestCaseStarted testCaseStarted) { - requireNonNull(testCaseStarted); - return findTestCaseBy(testCaseStarted) - .map(TestCase::getPickleId) - .map(pickleById::get); - } - - public Optional findPickleStepBy(TestStep testStep) { - requireNonNull(testCaseStarted); - return testStep.getPickleStepId() - .map(pickleStepById::get); - } - - public Optional findStepBy(PickleStep pickleStep) { - requireNonNull(pickleStep); - String stepId = pickleStep.getAstNodeIds().get(0); - return ofNullable(stepById.get(stepId)); - } - - public Optional findTestCaseBy(TestCaseStarted testCaseStarted) { - requireNonNull(testCaseStarted); - return ofNullable(testCaseById.get(testCaseStarted.getTestCaseId())); - } - - public Optional findTestCaseDurationBy(TestCaseStarted testCaseStarted) { - requireNonNull(testCaseStarted); - Timestamp started = testCaseStarted.getTimestamp(); - return findTestCaseFinishedBy(testCaseStarted) - .map(TestCaseFinished::getTimestamp) - .map(finished -> Duration.between( - Convertor.toInstant(started), - Convertor.toInstant(finished) - )); - } - - public Optional findTestCaseFinishedBy(TestCaseStarted testCaseStarted) { - requireNonNull(testCaseStarted); - return ofNullable(testCaseFinishedByTestCaseStartedId.get(testCaseStarted.getId())); - } - - public Optional findTestRunDuration() { - if (testRunStarted == null || testRunFinished == null) { - return Optional.empty(); - } - Duration between = Duration.between( - Convertor.toInstant(testRunStarted.getTimestamp()), - Convertor.toInstant(testRunFinished.getTimestamp()) - ); - return Optional.of(between); - } - - public Optional findTestRunFinished() { - return ofNullable(testRunFinished); - } - - public Optional findTestRunStarted() { - return ofNullable(testRunStarted); - } - - public List> findTestStepAndTestStepFinishedBy(TestCaseStarted testCaseStarted) { - return findTestStepsFinishedBy(testCaseStarted).stream() - .map(testStepFinished -> findTestStepBy(testStepFinished).map(testStep -> new SimpleEntry<>(testStep, testStepFinished))) - .filter(Optional::isPresent) - .map(Optional::get) - .collect(toList()); - } - - public Optional findTestStepBy(TestStepFinished testStepFinished) { - requireNonNull(testStepFinished); - return ofNullable(testStepById.get(testStepFinished.getTestStepId())); - } - - public List findTestStepsFinishedBy(TestCaseStarted testCaseStarted) { - requireNonNull(testCaseStarted); - List testStepsFinished = testStepsFinishedByTestCaseStartedId. - getOrDefault(testCaseStarted.getId(), emptyList()); - // Concurrency - return new ArrayList<>(testStepsFinished); - } - - public void update(Envelope envelope) { - envelope.getTestRunStarted().ifPresent(this::updateTestRunStarted); - envelope.getTestRunFinished().ifPresent(this::updateTestRunFinished); - envelope.getTestCaseStarted().ifPresent(this::updateTestCaseStarted); - envelope.getTestCaseFinished().ifPresent(this::updateTestCaseFinished); - envelope.getTestStepFinished().ifPresent(this::updateTestStepFinished); - envelope.getGherkinDocument().ifPresent(this::updateGherkinDocument); - envelope.getPickle().ifPresent(this::updatePickle); - envelope.getTestCase().ifPresent(this::updateTestCase); - } - - private void updateTestCaseStarted(TestCaseStarted testCaseStarted) { - this.testCaseStarted.add(testCaseStarted); - } - - private void updateTestCase(TestCase event) { - this.testCaseById.put(event.getId(), event); - event.getTestSteps().forEach(testStep -> testStepById.put(testStep.getId(), testStep)); - } - - private void updatePickle(Pickle event) { - this.pickleById.put(event.getId(), event); - event.getSteps().forEach(pickleStep -> pickleStepById.put(pickleStep.getId(), pickleStep)); - } - - private void updateGherkinDocument(GherkinDocument gherkinDocument) { - gherkinDocument.getFeature().ifPresent(this::updateFeature); - } - - private void updateFeature(Feature feature) { - feature.getChildren() - .forEach(featureChild -> { - featureChild.getBackground().ifPresent(background -> updateSteps(background.getSteps())); - featureChild.getScenario().ifPresent(scenario -> updateScenario(feature, null, scenario)); - featureChild.getRule().ifPresent(rule -> rule.getChildren().forEach(ruleChild -> { - ruleChild.getBackground().ifPresent(background -> updateSteps(background.getSteps())); - ruleChild.getScenario().ifPresent(scenario -> updateScenario(feature, rule, scenario)); - })); - }); - } - - private void updateSteps(List steps) { - steps.forEach(step -> stepById.put(step.getId(), step)); - } - - private void updateTestStepFinished(TestStepFinished event) { - this.testStepsFinishedByTestCaseStartedId.compute(event.getTestCaseStartedId(), updateList(event)); - } - - private void updateTestCaseFinished(TestCaseFinished event) { - this.testCaseFinishedByTestCaseStartedId.put(event.getTestCaseStartedId(), event); - } - - private void updateTestRunFinished(TestRunFinished event) { - this.testRunFinished = event; - } - - private void updateTestRunStarted(TestRunStarted event) { - this.testRunStarted = event; - } - - private void updateScenario(Feature feature, Rule rule, Scenario scenario) { - this.gherkinAstNodesById.put(scenario.getId(), new GherkinAstNodes(feature, rule, scenario)); - updateSteps(scenario.getSteps()); - - List examples = scenario.getExamples(); - for (int examplesIndex = 0; examplesIndex < examples.size(); examplesIndex++) { - Examples currentExamples = examples.get(examplesIndex); - List tableRows = currentExamples.getTableBody(); - for (int exampleIndex = 0; exampleIndex < tableRows.size(); exampleIndex++) { - TableRow currentExample = tableRows.get(exampleIndex); - gherkinAstNodesById.put(currentExample.getId(), new GherkinAstNodes(feature, rule, scenario, examplesIndex, currentExamples, exampleIndex, currentExample)); - } - } - } - - private BiFunction, List> updateList(E element) { - return (key, existing) -> { - if (existing != null) { - existing.add(element); - return existing; - } - List list = new ArrayList<>(); - list.add(element); - return list; - }; - } - -} diff --git a/java/src/main/java/io/cucumber/junitxmlformatter/XmlReportData.java b/java/src/main/java/io/cucumber/junitxmlformatter/XmlReportData.java index ae419bb..a58b76b 100644 --- a/java/src/main/java/io/cucumber/junitxmlformatter/XmlReportData.java +++ b/java/src/main/java/io/cucumber/junitxmlformatter/XmlReportData.java @@ -1,28 +1,29 @@ package io.cucumber.junitxmlformatter; import io.cucumber.messages.types.Envelope; -import io.cucumber.messages.types.Examples; import io.cucumber.messages.types.Feature; import io.cucumber.messages.types.Pickle; import io.cucumber.messages.types.PickleStep; -import io.cucumber.messages.types.Rule; import io.cucumber.messages.types.Step; import io.cucumber.messages.types.TestCaseStarted; import io.cucumber.messages.types.TestStep; import io.cucumber.messages.types.TestStepFinished; import io.cucumber.messages.types.TestStepResult; import io.cucumber.messages.types.TestStepResultStatus; +import io.cucumber.query.NamingStrategy; +import io.cucumber.query.Query; import java.time.Duration; import java.util.AbstractMap.SimpleEntry; -import java.util.ArrayList; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Map.Entry; import java.util.Optional; -import java.util.stream.Collectors; import static io.cucumber.messages.types.TestStepResultStatus.PASSED; +import static io.cucumber.query.NamingStrategy.FeatureName.EXCLUDE; +import static io.cucumber.query.NamingStrategy.Strategy.LONG; import static java.util.concurrent.TimeUnit.SECONDS; import static java.util.function.Function.identity; import static java.util.stream.Collectors.counting; @@ -32,6 +33,10 @@ class XmlReportData { private final Query query = new Query(); + private final NamingStrategy namingStrategy = NamingStrategy + .strategy(LONG) + .featureName(EXCLUDE) + .build(); private static final long MILLIS_PER_SECOND = SECONDS.toMillis(1L); @@ -54,7 +59,7 @@ void collect(Envelope envelope) { Map getTestCaseStatusCounts() { // @formatter:off return query.findAllTestCaseStarted().stream() - .map(query::findMostSevereTestStepResultStatusBy) + .map(query::findMostSevereTestStepResulBy) .filter(Optional::isPresent) .map(Optional::get) .map(TestStepResult::getStatus) @@ -70,50 +75,23 @@ String getPickleName(TestCaseStarted testCaseStarted) { Pickle pickle = query.findPickleBy(testCaseStarted) .orElseThrow(() -> new IllegalStateException("No pickle for " + testCaseStarted.getId())); - return query.findGherkinAstNodesBy(pickle) - .map(XmlReportData::getPickleName) - .orElse(pickle.getName()); - } - - private static String getPickleName(GherkinAstNodes elements) { - List pieces = new ArrayList<>(); - - elements.rule().map(Rule::getName).ifPresent(pieces::add); - - pieces.add(elements.scenario().getName()); - - elements.examples().map(Examples::getName).ifPresent(pieces::add); - - String examplesPrefix = elements.examplesIndex() - .map(examplesIndex -> examplesIndex + 1) - .map(examplesIndex -> examplesIndex + ".") - .orElse(""); - - elements.exampleIndex() - .map(exampleIndex -> exampleIndex + 1) - .map(exampleSuffix -> "Example #" + examplesPrefix + exampleSuffix) - .ifPresent(pieces::add); - - return pieces.stream() - .filter(s -> !s.isEmpty()) - .collect(Collectors.joining(" - ")); + return query.findNameOf(pickle, namingStrategy); } public String getFeatureName(TestCaseStarted testCaseStarted) { - return query.findGherkinAstNodesBy(testCaseStarted) - .map(GherkinAstNodes::feature) + return query.findFeatureBy(testCaseStarted) .map(Feature::getName) .orElseThrow(() -> new IllegalStateException("No feature for " + testCaseStarted)); } - List> getStepsAndResult(TestCaseStarted testCaseStarted) { - return query.findTestStepAndTestStepFinishedBy(testCaseStarted) + List> getStepsAndResult(TestCaseStarted testCaseStarted) { + return query.findTestStepFinishedAndTestStepBy(testCaseStarted) .stream() // Exclude hooks - .filter(entry -> entry.getKey().getPickleStepId().isPresent()) + .filter(entry -> entry.getValue().getPickleStepId().isPresent()) .map(testStep -> { - String key = renderTestStepText(testStep.getKey()); - String value = renderTestStepResult(testStep.getValue()); + String key = renderTestStepText(testStep.getValue()); + String value = renderTestStepResult(testStep.getKey()); return new SimpleEntry<>(key, value); }) .collect(toList()); @@ -152,7 +130,7 @@ List getAllTestCaseStarted() { private static final TestStepResult SCENARIO_WITH_NO_STEPS = new TestStepResult(ZERO_DURATION, null, PASSED, null); TestStepResult getTestCaseStatus(TestCaseStarted testCaseStarted) { - return query.findMostSevereTestStepResultStatusBy(testCaseStarted) + return query.findMostSevereTestStepResulBy(testCaseStarted) .orElse(SCENARIO_WITH_NO_STEPS); } diff --git a/java/src/main/java/io/cucumber/junitxmlformatter/XmlReportWriter.java b/java/src/main/java/io/cucumber/junitxmlformatter/XmlReportWriter.java index ae3db98..0c9db7e 100644 --- a/java/src/main/java/io/cucumber/junitxmlformatter/XmlReportWriter.java +++ b/java/src/main/java/io/cucumber/junitxmlformatter/XmlReportWriter.java @@ -31,12 +31,12 @@ void writeXmlReport(Writer out) throws XMLStreamException { EscapingXmlStreamWriter writer = new EscapingXmlStreamWriter(factory.createXMLStreamWriter(out)); writer.writeStartDocument("UTF-8", "1.0"); writer.newLine(); - writeTestsuite(data, writer); + writeTestsuite(writer); writer.writeEndDocument(); writer.flush(); } - private void writeTestsuite(XmlReportData data, EscapingXmlStreamWriter writer) throws XMLStreamException { + private void writeTestsuite(EscapingXmlStreamWriter writer) throws XMLStreamException { writer.writeStartElement("testsuite"); writeSuiteAttributes(writer); writer.newLine();