Skip to content

Add a CLI to convert message files #41

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 4 commits into from
Closed
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
102 changes: 102 additions & 0 deletions java/cli/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>io.cucumber</groupId>
<artifactId>junit-xml-formatter-parent</artifactId>
<version>0.5.1-SNAPSHOT</version>
</parent>

<artifactId>junit-xml-formatter-cli</artifactId>
<packaging>jar</packaging>
<name>JUnit XML Formatter CLI</name>
<description>CLI to render Cucumber Messages as JUnit XML</description>
<url>https://github.com/cucumber/junit-xml-formatter</url>

<properties>
<java.version>21</java.version>
<project.Automatic-Module-Name>io.cucumber.junitxmlformatter.cli</project.Automatic-Module-Name>
</properties>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.junit</groupId>
<artifactId>junit-bom</artifactId>
<version>5.11.3</version>
<type>pom</type>
<scope>import</scope>
</dependency>

<dependency>
<groupId>com.fasterxml.jackson</groupId>
<artifactId>jackson-bom</artifactId>
<version>2.18.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<dependencies>
<dependency>
<groupId>info.picocli</groupId>
<artifactId>picocli</artifactId>
<version>4.7.6</version>
</dependency>

<dependency>
<groupId>io.cucumber</groupId>
<artifactId>junit-xml-formatter</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>

<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jdk8</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.module</groupId>
<artifactId>jackson-module-parameter-names</artifactId>
</dependency>

<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.26.3</version>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifest>
<mainClass>io.cucumber.junitxmlformatter.cli.JunitXmlFormatter</mainClass>
</manifest>
</archive>
</configuration>
</plugin>
</plugins>
</build>
</project>
Original file line number Diff line number Diff line change
@@ -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<Integer> {
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);
}
}


}
Original file line number Diff line number Diff line change
@@ -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<String> getAttribute(Function<Package, String> function) {
return Optional.ofNullable(ManifestVersionProvider.class.getPackage()).map(function);
}
}
Loading
Loading