diff --git a/java/cli/pom.xml b/java/cli/pom.xml new file mode 100644 index 0000000..345fe9d --- /dev/null +++ b/java/cli/pom.xml @@ -0,0 +1,102 @@ + + + 4.0.0 + + + io.cucumber + junit-xml-formatter-parent + 0.5.1-SNAPSHOT + + + junit-xml-formatter-cli + jar + JUnit XML Formatter CLI + CLI to render Cucumber Messages as JUnit XML + https://github.com/cucumber/junit-xml-formatter + + + 21 + io.cucumber.junitxmlformatter.cli + + + + + + org.junit + junit-bom + 5.11.3 + pom + import + + + + com.fasterxml.jackson + jackson-bom + 2.18.0 + pom + import + + + + + + + info.picocli + picocli + 4.7.6 + + + + io.cucumber + junit-xml-formatter + + + com.fasterxml.jackson.core + jackson-databind + + + + com.fasterxml.jackson.datatype + jackson-datatype-jdk8 + + + com.fasterxml.jackson.module + jackson-module-parameter-names + + + + org.assertj + assertj-core + 3.26.3 + test + + + + org.junit.jupiter + junit-jupiter-engine + test + + + + org.junit.jupiter + junit-jupiter-params + test + + + + + + + org.apache.maven.plugins + maven-jar-plugin + + + + io.cucumber.junitxmlformatter.cli.JunitXmlFormatter + + + + + + + diff --git a/java/cli/src/main/java/io/cucumber/junitxmlformatter/cli/JunitXmlFormatter.java b/java/cli/src/main/java/io/cucumber/junitxmlformatter/cli/JunitXmlFormatter.java new file mode 100644 index 0000000..2faee9b --- /dev/null +++ b/java/cli/src/main/java/io/cucumber/junitxmlformatter/cli/JunitXmlFormatter.java @@ -0,0 +1,179 @@ +package io.cucumber.junitxmlformatter.cli; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.cfg.ConstructorDetector; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; +import com.fasterxml.jackson.module.paramnames.ParameterNamesModule; +import io.cucumber.junitxmlformatter.MessagesToJunitXmlWriter; +import io.cucumber.messages.NdjsonToMessageIterable; +import io.cucumber.messages.NdjsonToMessageIterable.Deserializer; +import io.cucumber.messages.types.Envelope; +import io.cucumber.query.NamingStrategy.ExampleName; +import picocli.CommandLine; +import picocli.CommandLine.Command; +import picocli.CommandLine.Model.CommandSpec; +import picocli.CommandLine.Option; +import picocli.CommandLine.ParameterException; +import picocli.CommandLine.Parameters; +import picocli.CommandLine.Spec; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.concurrent.Callable; + +import static io.cucumber.junitxmlformatter.cli.JunitXmlFormatter.*; +import static java.nio.file.Files.newOutputStream; +import static java.nio.file.StandardOpenOption.CREATE; +import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING; + +@Command( + name = JUNIT_XML_FORMATTER_NAME, + mixinStandardHelpOptions = true, + header = "Converts Cucumber messages to JUnit XML", + versionProvider = ManifestVersionProvider.class +) +class JunitXmlFormatter implements Callable { + static final String JUNIT_XML_FORMATTER_NAME = "junit-xml-formatter"; + + @Spec + private CommandSpec spec; + + @Parameters( + index = "0", + paramLabel = "file", + description = "The input file containing Cucumber messages. " + + "Use - to read from the standard input." + ) + private Path source; + + @Option( + names = {"-o", "--output"}, + arity = "0..1", + paramLabel = "file", + description = "The output file containing JUnit XML. " + + "If file is a directory, a new file be " + + "created by taking the name of the input file and " + + "replacing the suffix with '.xml'. If the file is omitted " + + "the current working directory is used." + ) + private Path output; + + @Option( + names = {"-e","--example-naming-strategy"}, + paramLabel = "strategy", + description = "How to name examples. Valid values: ${COMPLETION-CANDIDATES}", + defaultValue = "NUMBER_AND_PICKLE_IF_PARAMETERIZED" + ) + private ExampleName exampleNameStrategy; + + @Override + public Integer call() throws IOException { + if (isSourceSystemIn()) { + if (isDestinationDirectory()) { + throw new ParameterException( + spec.commandLine(), + ("Invalid value '%s' for option '--output': When " + + "reading from standard input, output can not " + + "be a directory").formatted(output) + ); + } + } + + try (var envelopes = new NdjsonToMessageIterable(sourceInputStream(), deserializer()); + var writer = new MessagesToJunitXmlWriter(exampleNameStrategy, outputPrintWriter()) + ) { + for (var envelope : envelopes) { + // TODO: What if exception while writing? + writer.write(envelope); + } + } + return 0; + } + + public static void main(String... args) { + var exitCode = new CommandLine(new JunitXmlFormatter()).execute(args); + System.exit(exitCode); + } + + private boolean isSourceSystemIn() { + return source.getFileName().toString().equals("-"); + } + + private boolean isDestinationDirectory() { + return output != null && Files.isDirectory(output); + } + + private static Deserializer deserializer() { + var jsonMapper = JsonMapper.builder() + .addModule(new Jdk8Module()) + .addModule(new ParameterNamesModule(JsonCreator.Mode.PROPERTIES)) + .constructorDetector(ConstructorDetector.USE_PROPERTIES_BASED) + .enable(DeserializationFeature.READ_ENUMS_USING_TO_STRING) + .enable(DeserializationFeature.USE_LONG_FOR_INTS) + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) + .build(); + return json -> jsonMapper.readValue(json, Envelope.class); + } + + private PrintWriter outputPrintWriter() { + if (output == null) { + return spec.commandLine().getOut(); + } + Path path = outputPath(); + try { + return new PrintWriter( + new OutputStreamWriter( + newOutputStream(path, CREATE, TRUNCATE_EXISTING), + StandardCharsets.UTF_8 + ) + ); + } catch (IOException e) { + throw new ParameterException( + spec.commandLine(), + ("Invalid value '%s' for option '--output': Could not " + + "write to '%s'" + ).formatted(output, path), e); + } + } + + private Path outputPath() { + if (!isDestinationDirectory()) { + return output; + } + + // Given a directory, decide on a file name + var fileName = source.getFileName().toString(); + var index = fileName.lastIndexOf("."); + if (index >= 0) { + fileName = fileName.substring(0, index); + } + var candidate = output.resolve(fileName + ".xml"); + + // Avoid overwriting existing files when we decided the file name. + var counter = 1; + while(Files.exists(candidate)) { + candidate = output.resolve(fileName + "." + counter + ".xml"); + } + return candidate; + } + + private InputStream sourceInputStream() { + if (isSourceSystemIn()) { + return System.in; + } + try { + return Files.newInputStream(source); + } catch (IOException e) { + throw new ParameterException(spec.commandLine(), "Invalid argument, could not read '%s'".formatted(source), e); + } + } + + +} diff --git a/java/cli/src/main/java/io/cucumber/junitxmlformatter/cli/ManifestVersionProvider.java b/java/cli/src/main/java/io/cucumber/junitxmlformatter/cli/ManifestVersionProvider.java new file mode 100644 index 0000000..02991a9 --- /dev/null +++ b/java/cli/src/main/java/io/cucumber/junitxmlformatter/cli/ManifestVersionProvider.java @@ -0,0 +1,21 @@ +package io.cucumber.junitxmlformatter.cli; + +import picocli.CommandLine.IVersionProvider; + +import java.util.Optional; +import java.util.function.Function; + +import static io.cucumber.junitxmlformatter.cli.JunitXmlFormatter.JUNIT_XML_FORMATTER_NAME; + +public class ManifestVersionProvider implements IVersionProvider { + + @Override + public String[] getVersion() { + var version = getAttribute(Package::getImplementationVersion).orElse("DEVELOPMENT"); + return new String[]{JUNIT_XML_FORMATTER_NAME + " " + version}; + } + + private static Optional getAttribute(Function function) { + return Optional.ofNullable(ManifestVersionProvider.class.getPackage()).map(function); + } +} diff --git a/java/cli/src/test/java/io/cucumber/junitxmlformatter/cli/JunitXmlFormatterTest.java b/java/cli/src/test/java/io/cucumber/junitxmlformatter/cli/JunitXmlFormatterTest.java new file mode 100644 index 0000000..2835215 --- /dev/null +++ b/java/cli/src/test/java/io/cucumber/junitxmlformatter/cli/JunitXmlFormatterTest.java @@ -0,0 +1,167 @@ +package io.cucumber.junitxmlformatter.cli; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import picocli.CommandLine; + +import java.io.IOException; +import java.io.InputStream; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +import static java.nio.file.Files.newInputStream; +import static java.nio.file.Files.readString; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +class JunitXmlFormatterTest { + + static final Path minimalFeatureNdjson = Paths.get("../../testdata/minimal.feature.ndjson"); + static final Path minimalFeatureXml = Paths.get("../../testdata/minimal.feature.xml"); + + final StringWriter stdOut = new StringWriter(); + final StringWriter stdErr = new StringWriter(); + final CommandLine cmd = new CommandLine(new JunitXmlFormatter()); + InputStream originalSystemIn; + + @TempDir + Path tmp; + + @BeforeEach + void setup() { + originalSystemIn = System.in; + cmd.setOut(new PrintWriter(stdOut)); + cmd.setErr(new PrintWriter(stdErr)); + } + + @AfterEach + void cleanup() { + System.setIn(originalSystemIn); + // Helps with debugging + System.out.println(stdOut); + System.out.println(stdErr); + } + + @Test + void help() { + cmd.printVersionHelp(new PrintWriter(stdOut)); + cmd.execute("--help"); + } + + @Test + void version() { + var exitCode = cmd.execute("--version"); + assertAll( + () -> assertThat(exitCode).isZero(), + () -> assertThat(stdOut.toString()) + .isEqualToIgnoringNewLines("junit-xml-formatter DEVELOPMENT") + ); + } + + @Test + void writeToSystemOut() { + var exitCode = cmd.execute("../../testdata/minimal.feature.ndjson"); + assertAll( + () -> assertThat(exitCode).isZero(), + () -> assertThat(stdOut.toString()) + .isEqualTo(readString(minimalFeatureXml)) + ); + } + + @Test + void failsToReadNonExistingFile() { + var exitCode = cmd.execute("../../testdata/no-such.feature.ndjson"); + assertAll( + () -> assertThat(exitCode).isEqualTo(2), + () -> assertThat(stdErr.toString()) + .contains("Invalid argument, could not read '../../testdata/no-such.feature.ndjson'") + ); + } + + @Test + void readsFromSystemIn() throws IOException { + System.setIn(newInputStream(minimalFeatureNdjson)); + var exitCode = cmd.execute("-"); + assertAll( + () -> assertThat(exitCode).isZero(), + () -> assertThat(stdOut.toString()) + .isEqualTo(readString(minimalFeatureXml)) + ); + } + + @Test + void writesToOutputFile() { + var destination = tmp.resolve("minimal.feature.xml"); + var exitCode = cmd.execute("../../testdata/minimal.feature.ndjson", "--output", destination.toString()); + assertAll( + () -> assertThat(exitCode).isZero(), + () -> assertThat(readString(destination)) + .isEqualTo(readString(minimalFeatureXml)) + ); + } + + @Test + void doesNotOverwriteWhenWritingToDirectory() { + var exitCode1 = cmd.execute("../../testdata/minimal.feature.ndjson", "--output", tmp.toString()); + var exitCode2 = cmd.execute("../../testdata/minimal.feature.ndjson", "--output", tmp.toString()); + assertAll( + () -> assertThat(exitCode1).isZero(), + () -> assertThat(tmp.resolve("minimal.feature.xml")).exists(), + () -> assertThat(exitCode2).isZero(), + () -> assertThat(tmp.resolve("minimal.feature.1.xml")).exists() + ); + } + + @Test + void failsToWriteToForbiddenOutputFile() throws IOException { + var destination = Files.createFile(tmp.resolve("minimal.feature.xml")); + var isReadOnly = destination.toFile().setReadOnly(); + assertThat(isReadOnly).isTrue(); + + var exitCode = cmd.execute("../../testdata/minimal.feature.ndjson", "--output", destination.toString()); + assertAll( + () -> assertThat(exitCode).isEqualTo(2), + () -> assertThat(stdErr.toString()) + .contains("Invalid value '%s' for option '--output': Could not write to '%s'" + .formatted(destination, destination)) + ); + } + + @Test + void writesFileToCurrentWorkingDirectory() { + var destination = Paths.get("minimal.feature.xml"); + var exitCode = cmd.execute("../../testdata/minimal.feature.ndjson", "--output"); + assertAll( + () -> assertThat(exitCode).isZero(), + () -> assertThat(readString(destination)) + .isEqualTo(readString(minimalFeatureXml)) + ); + } + + @Test + void canNotGuessFileNameWhenReadingFromSystemIn() throws IOException { + System.setIn(newInputStream(minimalFeatureNdjson)); + var exitCode = cmd.execute("-", "--output"); + assertAll( + () -> assertThat(exitCode).isEqualTo(2), + () -> assertThat(stdErr.toString()) + .contains("Invalid value '' for option '--output': When reading from standard input, output can not be a directory") + ); + } + + @Test + void useExampleNamingStrategy() { + var exitCode = cmd.execute("../../testdata/examples-tables.feature.ndjson", "--example-naming-strategy", "NUMBER"); + assertAll( + () -> assertThat(exitCode).isZero(), + () -> assertThat(stdOut.toString()) + .contains("name=\"Eating cucumbers with <friends> friends - #1.1\"") + ); + } + +} diff --git a/java/lib/pom.xml b/java/lib/pom.xml new file mode 100644 index 0000000..5191352 --- /dev/null +++ b/java/lib/pom.xml @@ -0,0 +1,104 @@ + + + 4.0.0 + + + io.cucumber + junit-xml-formatter-parent + 0.5.1-SNAPSHOT + + + junit-xml-formatter + jar + JUnit XML Formatter + Renders Cucumber Messages as JUnit XML + https://github.com/cucumber/junit-xml-formatter + + + io.cucumber.junitxmlformatter + + + + + + org.junit + junit-bom + 5.11.3 + pom + import + + + + com.fasterxml.jackson + jackson-bom + 2.18.0 + pom + import + + + + + + + io.cucumber + messages + [24.0.0,27.0.0) + + + io.cucumber + query + [12.2.0,13.0.0) + + + + com.fasterxml.jackson.core + jackson-databind + test + + + + com.fasterxml.jackson.datatype + jackson-datatype-jdk8 + test + + + + com.fasterxml.jackson.module + jackson-module-parameter-names + test + + + + org.hamcrest + hamcrest + 3.0 + test + + + + org.assertj + assertj-core + 3.26.3 + test + + + + org.xmlunit + xmlunit-assertj + 2.10.0 + test + + + + org.junit.jupiter + junit-jupiter-engine + test + + + + org.junit.jupiter + junit-jupiter-params + test + + + diff --git a/java/src/main/java/io/cucumber/junitxmlformatter/EscapingXmlStreamWriter.java b/java/lib/src/main/java/io/cucumber/junitxmlformatter/EscapingXmlStreamWriter.java similarity index 100% rename from java/src/main/java/io/cucumber/junitxmlformatter/EscapingXmlStreamWriter.java rename to java/lib/src/main/java/io/cucumber/junitxmlformatter/EscapingXmlStreamWriter.java diff --git a/java/src/main/java/io/cucumber/junitxmlformatter/MessagesToJunitXmlWriter.java b/java/lib/src/main/java/io/cucumber/junitxmlformatter/MessagesToJunitXmlWriter.java similarity index 86% rename from java/src/main/java/io/cucumber/junitxmlformatter/MessagesToJunitXmlWriter.java rename to java/lib/src/main/java/io/cucumber/junitxmlformatter/MessagesToJunitXmlWriter.java index 68df541..c5fc203 100644 --- a/java/src/main/java/io/cucumber/junitxmlformatter/MessagesToJunitXmlWriter.java +++ b/java/lib/src/main/java/io/cucumber/junitxmlformatter/MessagesToJunitXmlWriter.java @@ -7,6 +7,7 @@ import java.io.IOException; import java.io.OutputStream; import java.io.OutputStreamWriter; +import java.io.Writer; import java.nio.charset.StandardCharsets; import static io.cucumber.query.NamingStrategy.FeatureName.EXCLUDE; @@ -22,7 +23,7 @@ */ public class MessagesToJunitXmlWriter implements AutoCloseable { - private final OutputStreamWriter out; + private final Writer out; private final XmlReportData data; private boolean streamClosed = false; @@ -34,18 +35,25 @@ public MessagesToJunitXmlWriter(NamingStrategy.ExampleName exampleNameStrategy, this(createNamingStrategy(requireNonNull(exampleNameStrategy)), out); } + public MessagesToJunitXmlWriter(NamingStrategy.ExampleName exampleNameStrategy, Writer out) { + this(createNamingStrategy(exampleNameStrategy), out); + } + + private static NamingStrategy createNamingStrategy(NamingStrategy.ExampleName exampleName) { return NamingStrategy.strategy(LONG).featureName(EXCLUDE).exampleName(exampleName).build(); } private MessagesToJunitXmlWriter(NamingStrategy namingStrategy, OutputStream out) { - this.data = new XmlReportData(namingStrategy); - this.out = new OutputStreamWriter( - requireNonNull(out), - StandardCharsets.UTF_8 + this(namingStrategy, new OutputStreamWriter(requireNonNull(out), StandardCharsets.UTF_8) ); } + private MessagesToJunitXmlWriter(NamingStrategy namingStrategy, Writer out) { + this.data = new XmlReportData(namingStrategy); + this.out = out; + } + /** * Writes a cucumber message to the xml output. * diff --git a/java/src/main/java/io/cucumber/junitxmlformatter/XmlReportData.java b/java/lib/src/main/java/io/cucumber/junitxmlformatter/XmlReportData.java similarity index 100% rename from java/src/main/java/io/cucumber/junitxmlformatter/XmlReportData.java rename to java/lib/src/main/java/io/cucumber/junitxmlformatter/XmlReportData.java diff --git a/java/src/main/java/io/cucumber/junitxmlformatter/XmlReportWriter.java b/java/lib/src/main/java/io/cucumber/junitxmlformatter/XmlReportWriter.java similarity index 100% rename from java/src/main/java/io/cucumber/junitxmlformatter/XmlReportWriter.java rename to java/lib/src/main/java/io/cucumber/junitxmlformatter/XmlReportWriter.java diff --git a/java/src/test/java/io/cucumber/junitxmlformatter/EscapingXmlStreamWriterTest.java b/java/lib/src/test/java/io/cucumber/junitxmlformatter/EscapingXmlStreamWriterTest.java similarity index 100% rename from java/src/test/java/io/cucumber/junitxmlformatter/EscapingXmlStreamWriterTest.java rename to java/lib/src/test/java/io/cucumber/junitxmlformatter/EscapingXmlStreamWriterTest.java diff --git a/java/src/test/java/io/cucumber/junitxmlformatter/Jackson.java b/java/lib/src/test/java/io/cucumber/junitxmlformatter/Jackson.java similarity index 100% rename from java/src/test/java/io/cucumber/junitxmlformatter/Jackson.java rename to java/lib/src/test/java/io/cucumber/junitxmlformatter/Jackson.java diff --git a/java/src/test/java/io/cucumber/junitxmlformatter/MessagesToJunitXmlWriterAcceptanceTest.java b/java/lib/src/test/java/io/cucumber/junitxmlformatter/MessagesToJunitXmlWriterAcceptanceTest.java similarity index 95% rename from java/src/test/java/io/cucumber/junitxmlformatter/MessagesToJunitXmlWriterAcceptanceTest.java rename to java/lib/src/test/java/io/cucumber/junitxmlformatter/MessagesToJunitXmlWriterAcceptanceTest.java index 89e98ab..f341f6a 100644 --- a/java/src/test/java/io/cucumber/junitxmlformatter/MessagesToJunitXmlWriterAcceptanceTest.java +++ b/java/lib/src/test/java/io/cucumber/junitxmlformatter/MessagesToJunitXmlWriterAcceptanceTest.java @@ -34,7 +34,7 @@ class MessagesToJunitXmlWriterAcceptanceTest { private static final NdjsonToMessageIterable.Deserializer deserializer = (json) -> OBJECT_MAPPER.readValue(json, Envelope.class); static List acceptance() throws IOException { - try (Stream paths = Files.list(Paths.get("../testdata"))) { + try (Stream paths = Files.list(Paths.get("../../testdata"))) { return paths .filter(path -> path.getFileName().toString().endsWith(".ndjson")) .map(TestCase::new) @@ -57,7 +57,7 @@ void test(TestCase testCase) throws IOException { void validateAgainstJenkins(TestCase testCase) throws IOException { ByteArrayOutputStream bytes = writeJunitXmlReport(testCase, new ByteArrayOutputStream()); Source actual = Input.fromByteArray(bytes.toByteArray()).build(); - Source jenkinsSchema = Input.fromPath(Paths.get("../jenkins-junit.xsd")).build(); + Source jenkinsSchema = Input.fromPath(Paths.get("../../jenkins-junit.xsd")).build(); assertThat(actual).isValidAgainst(jenkinsSchema); } @@ -75,7 +75,7 @@ void validateAgainstJenkins(TestCase testCase) throws IOException { void validateAgainstSurefire(TestCase testCase) throws IOException { ByteArrayOutputStream bytes = writeJunitXmlReport(testCase, new ByteArrayOutputStream()); Source actual = Input.fromByteArray(bytes.toByteArray()).build(); - Source surefireSchema = Input.fromPath(Paths.get("../surefire-test-report-3.0.xsd")).build(); + Source surefireSchema = Input.fromPath(Paths.get("../../surefire-test-report-3.0.xsd")).build(); if (!testCasesWithMissingException.contains(testCase.name)) { assertThat(actual).isValidAgainst(surefireSchema); return; diff --git a/java/src/test/java/io/cucumber/junitxmlformatter/MessagesToJunitXmlWriterTest.java b/java/lib/src/test/java/io/cucumber/junitxmlformatter/MessagesToJunitXmlWriterTest.java similarity index 100% rename from java/src/test/java/io/cucumber/junitxmlformatter/MessagesToJunitXmlWriterTest.java rename to java/lib/src/test/java/io/cucumber/junitxmlformatter/MessagesToJunitXmlWriterTest.java diff --git a/java/pom.xml b/java/pom.xml index 4b8ad27..9fac4b4 100644 --- a/java/pom.xml +++ b/java/pom.xml @@ -1,5 +1,6 @@ - + 4.0.0 @@ -8,18 +9,15 @@ 4.2.0 - junit-xml-formatter + junit-xml-formatter-parent 0.5.1-SNAPSHOT - jar - JUnit XML Formatter - Renders Cucumber Messages as JUnit XML + pom + JUnit XML Formatter Parent https://github.com/cucumber/junit-xml-formatter - io.cucumber.junitxmlformatter 1719064372 - scm:git:git://github.com/cucumber/junit-xml-formatter.git scm:git:git@github.com:cucumber/junit-xml-formatter.git @@ -27,87 +25,23 @@ HEAD + + lib + cli + + - org.junit - junit-bom - 5.11.3 - pom - import + io.cucumber + junit-xml-formatter + 0.5.1-SNAPSHOT - - com.fasterxml.jackson - jackson-bom - 2.18.0 - pom - import + io.cucumber + junit-xml-formatter-cli + 0.5.1-SNAPSHOT - - - - io.cucumber - messages - [24.0.0,27.0.0) - - - io.cucumber - query - [12.2.0,13.0.0) - - - - com.fasterxml.jackson.core - jackson-databind - test - - - - com.fasterxml.jackson.datatype - jackson-datatype-jdk8 - test - - - - com.fasterxml.jackson.module - jackson-module-parameter-names - test - - - - org.hamcrest - hamcrest - 3.0 - test - - - - org.assertj - assertj-core - 3.26.3 - test - - - - org.xmlunit - xmlunit-assertj - 2.10.0 - test - - - - org.junit.jupiter - junit-jupiter-engine - test - - - - org.junit.jupiter - junit-jupiter-params - test - -