Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added
- Support configurable testsuite name, testcase classname, and testcase name. ([#105](https://github.com/cucumber/junit-xml-formatter/pull/105))

## [0.9.0] - 2025-09-11
### Changed
- Update dependency cucumber/query to 14.0.1
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import java.io.OutputStreamWriter;
import java.nio.charset.StandardCharsets;

import static io.cucumber.query.NamingStrategy.ExampleName.NUMBER_AND_PICKLE_IF_PARAMETERIZED;
import static io.cucumber.query.NamingStrategy.FeatureName.EXCLUDE;
import static io.cucumber.query.NamingStrategy.Strategy.LONG;
import static java.util.Objects.requireNonNull;
Expand All @@ -22,24 +23,75 @@
*/
public final class MessagesToJunitXmlWriter implements AutoCloseable {

private static final String DEFAULT_TEST_SUITE_NAME = "Cucumber";
private final OutputStreamWriter out;
private final XmlReportData data;
private boolean streamClosed = false;

public MessagesToJunitXmlWriter(OutputStream out) {
this(NamingStrategy.ExampleName.NUMBER_AND_PICKLE_IF_PARAMETERIZED, out);
this("Cucumber", null, createNamingStrategy(NUMBER_AND_PICKLE_IF_PARAMETERIZED), out);
}

@Deprecated
public MessagesToJunitXmlWriter(NamingStrategy.ExampleName exampleNameStrategy, OutputStream out) {
this(createNamingStrategy(requireNonNull(exampleNameStrategy)), out);
this("Cucumber", null, createNamingStrategy(requireNonNull(exampleNameStrategy)), out);
}

public static Builder builder() {
return new Builder();
}

public static class Builder {

private String testSuiteName = DEFAULT_TEST_SUITE_NAME;
private String testClassName;
private NamingStrategy testNamingStrategy = NamingStrategy.strategy(LONG)
.featureName(EXCLUDE)
.exampleName(NUMBER_AND_PICKLE_IF_PARAMETERIZED)
.build();

private Builder() {

}

/**
* Sets the value for the {@code <testsuite name="..." .../>} attribute. Defaults to {@value DEFAULT_TEST_SUITE_NAME}.
*/
public Builder testSuiteName(String testSuiteName) {
this.testSuiteName = requireNonNull(testSuiteName);
return this;
}

/**
* Sets the value for the {@code <testcase classname="..." .../>} attribute. Defaults to the name of the
* feature.
*/
public Builder testClassName(String testClassName) {
this.testClassName = testClassName;
return this;
}

/**
* Set the naming strategy used for the {@code <testcase name="...".../> attribute}. Defaults to the
* {@link NamingStrategy.Strategy#LONG} strategy with {@link NamingStrategy.FeatureName#EXCLUDE} and
* {@link NamingStrategy.ExampleName#NUMBER_AND_PICKLE_IF_PARAMETERIZED}.
*/
public Builder testNamingStrategy(NamingStrategy namingStrategy) {
this.testNamingStrategy = requireNonNull(namingStrategy);
return this;
}

public MessagesToJunitXmlWriter build(OutputStream out) {
return new MessagesToJunitXmlWriter(testSuiteName, testClassName, testNamingStrategy, requireNonNull(out));
}
}

private static NamingStrategy createNamingStrategy(NamingStrategy.ExampleName exampleName) {
return NamingStrategy.strategy(LONG).featureName(EXCLUDE).exampleName(exampleName).build();
return NamingStrategy.strategy(NamingStrategy.Strategy.LONG).featureName(NamingStrategy.FeatureName.EXCLUDE).exampleName(exampleName).build();
}

private MessagesToJunitXmlWriter(NamingStrategy namingStrategy, OutputStream out) {
this.data = new XmlReportData(namingStrategy);
private MessagesToJunitXmlWriter(String testSuiteName, String testClassName, NamingStrategy testNamingStrategy, OutputStream out) {
this.data = new XmlReportData(testSuiteName, testClassName, testNamingStrategy);
this.out = new OutputStreamWriter(
requireNonNull(out),
StandardCharsets.UTF_8
Expand Down
31 changes: 21 additions & 10 deletions java/src/main/java/io/cucumber/junitxmlformatter/XmlReportData.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,21 +28,25 @@
import static io.cucumber.messages.types.TestStepResultStatus.PASSED;
import static io.cucumber.query.Repository.RepositoryFeature.INCLUDE_GHERKIN_DOCUMENTS;
import static java.time.format.DateTimeFormatter.ISO_INSTANT;
import static java.util.Objects.requireNonNull;
import static java.util.concurrent.TimeUnit.SECONDS;
import static java.util.stream.Collectors.toList;

class XmlReportData {
private static final long MILLIS_PER_SECOND = SECONDS.toMillis(1L);

private final Repository repository = Repository.builder()
.feature(INCLUDE_GHERKIN_DOCUMENTS, true)
.build();
private final Query query = new Query(repository);
private final NamingStrategy namingStrategy;

private static final long MILLIS_PER_SECOND = SECONDS.toMillis(1L);

XmlReportData(NamingStrategy namingStrategy) {
this.namingStrategy = namingStrategy;
private final String testSuiteName;
private final String testClassName;
private final NamingStrategy testNamingStrategy;

XmlReportData(String testSuiteName, String testClassName, NamingStrategy testNamingStrategy) {
this.testSuiteName = requireNonNull(testSuiteName);
this.testClassName = testClassName;
this.testNamingStrategy = requireNonNull(testNamingStrategy);
}

void collect(Envelope envelope) {
Expand Down Expand Up @@ -74,20 +78,27 @@ private Pickle getPickle(TestCaseStarted testCaseStarted) {
.orElseThrow(() -> new IllegalStateException("No pickle for " + testCaseStarted.getId()));
}

String getPickleName(TestCaseStarted testCaseStarted) {
String getTestName(TestCaseStarted testCaseStarted) {
Pickle pickle = getPickle(testCaseStarted);
return query.findLineageBy(pickle)
.map(lineage -> namingStrategy.reduce(lineage, pickle))
.map(lineage -> testNamingStrategy.reduce(lineage, pickle))
.orElseGet(pickle::getName);
}

String getFeatureName(TestCaseStarted testCaseStarted) {
String getTestClassName(TestCaseStarted testCaseStarted) {
if (testClassName != null) {
return testClassName;
}
return query.findLineageBy(testCaseStarted)
.flatMap(Lineage::feature)
.map(Feature::getName)
.orElseGet(() -> this.getPickle(testCaseStarted).getUri());
}

String getTestSuiteName() {
return testSuiteName;
}

List<Entry<String, String>> getStepsAndResult(TestCaseStarted testCaseStarted) {
return query.findTestStepFinishedAndTestStepBy(testCaseStarted)
.stream()
Expand Down Expand Up @@ -138,7 +149,7 @@ TestStepResult getTestCaseStatus(TestCaseStarted testCaseStarted) {
.orElse(SCENARIO_WITH_NO_STEPS);
}

public Optional<String> getTestRunStartedAt() {
Optional<String> getTestRunStartedAt() {
return query.findTestRunStarted()
.map(TestRunStarted::getTimestamp)
.map(Convertor::toInstant)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ private void writeTestsuite(EscapingXmlStreamWriter writer) throws XMLStreamExce
}

private void writeSuiteAttributes(EscapingXmlStreamWriter writer) throws XMLStreamException {
writer.writeAttribute("name", "Cucumber");
writer.writeAttribute("name", data.getTestSuiteName());
writer.writeAttribute("time", String.valueOf(data.getSuiteDurationInSeconds()));

Map<TestStepResultStatus, Long> counts = data.getTestCaseStatusCounts();
Expand Down Expand Up @@ -85,8 +85,8 @@ private void writeTestcase(EscapingXmlStreamWriter writer, TestCaseStarted testC
}

private void writeTestCaseAttributes(EscapingXmlStreamWriter writer, TestCaseStarted testCaseStarted) throws XMLStreamException {
writer.writeAttribute("classname", data.getFeatureName(testCaseStarted));
writer.writeAttribute("name", data.getPickleName(testCaseStarted));
writer.writeAttribute("classname", data.getTestClassName(testCaseStarted));
writer.writeAttribute("name", data.getTestName(testCaseStarted));
writer.writeAttribute("time", String.valueOf(data.getDurationInSeconds(testCaseStarted)));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import io.cucumber.messages.NdjsonToMessageIterable;
import io.cucumber.messages.types.Envelope;
import io.cucumber.query.NamingStrategy;
import io.cucumber.query.NamingStrategy.Strategy;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.params.ParameterizedTest;
Expand All @@ -24,23 +26,44 @@
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static io.cucumber.junitxmlformatter.Jackson.OBJECT_MAPPER;
import static io.cucumber.query.NamingStrategy.Strategy.LONG;
import static io.cucumber.query.NamingStrategy.strategy;
import static org.xmlunit.assertj.XmlAssert.assertThat;

class MessagesToJunitXmlWriterAcceptanceTest {
private static final NdjsonToMessageIterable.Deserializer deserializer = (json) -> OBJECT_MAPPER.readValue(json, Envelope.class);

static List<TestCase> acceptance() throws IOException {
List<TestCase> testCases = new ArrayList<>();

try (Stream<Path> paths = Files.list(Paths.get("../testdata/src"))) {
return paths
paths
.filter(path -> path.getFileName().toString().endsWith(".ndjson"))
.map(TestCase::new)
.map(source -> new TestCase(
source,
"default",
MessagesToJunitXmlWriter.builder()
)
)
.sorted(Comparator.comparing(testCase -> testCase.source))
.collect(Collectors.toList());
.forEach(testCases::add);
}

testCases.add(
new TestCase(
Paths.get("../testdata/src/examples-tables.ndjson"),
"custom",
MessagesToJunitXmlWriter.builder()
.testSuiteName("Cucumber Suite")
.testClassName("Cucumber Class")
.testNamingStrategy(strategy(LONG).build())
)
);

return testCases;
}

@ParameterizedTest
Expand Down Expand Up @@ -94,7 +117,7 @@ void updateExpectedFiles(TestCase testCase) throws IOException {
private static <T extends OutputStream> T writeJunitXmlReport(TestCase testCase, T out) throws IOException {
try (InputStream in = Files.newInputStream(testCase.source)) {
try (NdjsonToMessageIterable envelopes = new NdjsonToMessageIterable(in, deserializer)) {
try (MessagesToJunitXmlWriter writer = new MessagesToJunitXmlWriter(out)) {
try (MessagesToJunitXmlWriter writer = testCase.getBuilder().build(out)) {
for (Envelope envelope : envelopes) {
writer.write(envelope);
}
Expand All @@ -109,17 +132,25 @@ static class TestCase {
private final Path expected;

private final String name;
private final MessagesToJunitXmlWriter.Builder builder;
private final String strategyName;

TestCase(Path source) {
TestCase(Path source, String namingStrategyName, MessagesToJunitXmlWriter.Builder builder) {
this.source = source;
String fileName = source.getFileName().toString();
this.name = fileName.substring(0, fileName.lastIndexOf(".ndjson"));
this.expected = source.getParent().resolve(name + ".xml");
this.expected = source.getParent().resolve(name + "." + namingStrategyName + ".xml");
this.builder = builder;
this.strategyName = namingStrategyName;
}

MessagesToJunitXmlWriter.Builder getBuilder() {
return builder;
}

@Override
public String toString() {
return name;
return name + " -> " + strategyName;
}

@Override
Expand Down
7 changes: 0 additions & 7 deletions javascript/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading