Skip to content

Commit 78080a1

Browse files
committed
Improvements
1 parent a6ac216 commit 78080a1

File tree

4 files changed

+322
-44
lines changed

4 files changed

+322
-44
lines changed

java/cli/pom.xml

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
<url>https://github.com/cucumber/junit-xml-formatter</url>
1616

1717
<properties>
18+
<java.version>21</java.version>
1819
<project.Automatic-Module-Name>io.cucumber.junitxmlformatter.cli</project.Automatic-Module-Name>
1920
</properties>
2021

@@ -85,22 +86,6 @@
8586

8687
<build>
8788
<plugins>
88-
<plugin>
89-
<groupId>org.apache.maven.plugins</groupId>
90-
<artifactId>maven-compiler-plugin</artifactId>
91-
<configuration>
92-
<annotationProcessorPaths>
93-
<path>
94-
<groupId>info.picocli</groupId>
95-
<artifactId>picocli-codegen</artifactId>
96-
<version>4.7.6</version>
97-
</path>
98-
</annotationProcessorPaths>
99-
<compilerArgs>
100-
<arg>-Aproject=${project.groupId}/${project.artifactId}</arg>
101-
</compilerArgs>
102-
</configuration>
103-
</plugin>
10489
<plugin>
10590
<groupId>org.apache.maven.plugins</groupId>
10691
<artifactId>maven-jar-plugin</artifactId>

java/cli/src/main/java/io/cucumber/junitxmlformatter/cli/JunitXmlFormatter.java

Lines changed: 133 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -10,66 +10,171 @@
1010
import io.cucumber.messages.NdjsonToMessageIterable;
1111
import io.cucumber.messages.NdjsonToMessageIterable.Deserializer;
1212
import io.cucumber.messages.types.Envelope;
13-
import io.cucumber.query.NamingStrategy;
13+
import io.cucumber.query.NamingStrategy.ExampleName;
1414
import picocli.CommandLine;
1515
import picocli.CommandLine.Command;
1616
import picocli.CommandLine.Model.CommandSpec;
17+
import picocli.CommandLine.Option;
18+
import picocli.CommandLine.ParameterException;
1719
import picocli.CommandLine.Parameters;
1820
import picocli.CommandLine.Spec;
1921

22+
import java.io.IOException;
2023
import java.io.InputStream;
21-
import java.io.Writer;
24+
import java.io.OutputStreamWriter;
25+
import java.io.PrintWriter;
26+
import java.nio.charset.StandardCharsets;
2227
import java.nio.file.Files;
2328
import java.nio.file.Path;
2429
import java.util.concurrent.Callable;
2530

26-
import static io.cucumber.query.NamingStrategy.ExampleName.NUMBER_AND_PICKLE_IF_PARAMETERIZED;
31+
import static io.cucumber.junitxmlformatter.cli.JunitXmlFormatter.*;
32+
import static java.nio.file.Files.newOutputStream;
33+
import static java.nio.file.StandardOpenOption.CREATE;
34+
import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING;
2735

2836
// TODO: Read version from manifest?
2937
@Command(
30-
name = "junit-xml-formatter",
38+
name = JUNIT_XML_FORMATTER_NAME,
3139
mixinStandardHelpOptions = true,
32-
description = "Converts Cucumber messages to JUnit XML"
40+
header = "Converts Cucumber messages to JUnit XML",
41+
versionProvider = ManifestVersionProvider.class
3342
)
3443
class JunitXmlFormatter implements Callable<Integer> {
44+
static final String JUNIT_XML_FORMATTER_NAME = "junit-xml-formatter";
45+
3546
@Spec
3647
private CommandSpec spec;
3748

38-
@Parameters(index = "0", description = "The source file containing Cucumber messages")
49+
@Parameters(
50+
index = "0",
51+
paramLabel = "file",
52+
description = "The input file containing Cucumber messages. " +
53+
"Use - to read from the standard input."
54+
)
3955
private Path source;
4056

57+
@Option(
58+
names = {"-o", "--output"},
59+
arity = "0..1",
60+
paramLabel = "file",
61+
description = "The output file containing JUnit XML. " +
62+
"If file is a directory, a new file be " +
63+
"created by taking the name of the input file and " +
64+
"replacing the suffix with '.xml'. If the file is omitted " +
65+
"the current working directory is used."
66+
)
67+
private Path output;
68+
69+
@Option(
70+
names = {"-e","--example-naming-strategy"},
71+
paramLabel = "strategy",
72+
description = "How to name examples. Valid values: ${COMPLETION-CANDIDATES}",
73+
defaultValue = "NUMBER_AND_PICKLE_IF_PARAMETERIZED"
74+
)
75+
private ExampleName exampleNameStrategy;
76+
4177
@Override
42-
public Integer call() throws Exception {
43-
JsonMapper jsonMapper = JsonMapper.builder()
78+
public Integer call() throws IOException {
79+
if (isSourceSystemIn()) {
80+
if (isDestinationDirectory()) {
81+
throw new ParameterException(
82+
spec.commandLine(),
83+
("Invalid value '%s' for option '--output': When " +
84+
"reading from standard input, output can not " +
85+
"be a directory").formatted(output)
86+
);
87+
}
88+
}
89+
90+
try (var envelopes = new NdjsonToMessageIterable(sourceInputStream(), deserializer());
91+
var writer = new MessagesToJunitXmlWriter(exampleNameStrategy, outputPrintWriter())
92+
) {
93+
for (var envelope : envelopes) {
94+
// TODO: What if exception while writing?
95+
writer.write(envelope);
96+
}
97+
}
98+
return 0;
99+
}
100+
101+
public static void main(String... args) {
102+
var exitCode = new CommandLine(new JunitXmlFormatter()).execute(args);
103+
System.exit(exitCode);
104+
}
105+
106+
private boolean isSourceSystemIn() {
107+
return source.getFileName().toString().equals("-");
108+
}
109+
110+
private boolean isDestinationDirectory() {
111+
return output != null && Files.isDirectory(output);
112+
}
113+
114+
private static Deserializer deserializer() {
115+
var jsonMapper = JsonMapper.builder()
44116
.addModule(new Jdk8Module())
45117
.addModule(new ParameterNamesModule(JsonCreator.Mode.PROPERTIES))
46118
.constructorDetector(ConstructorDetector.USE_PROPERTIES_BASED)
47119
.enable(DeserializationFeature.READ_ENUMS_USING_TO_STRING)
48120
.enable(DeserializationFeature.USE_LONG_FOR_INTS)
49121
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
50122
.build();
51-
Deserializer deserializer = json -> jsonMapper.readValue(json, Envelope.class);
52-
53-
// TODO: Use the CLI options.
54-
NamingStrategy.ExampleName exampleNameStrategy = NUMBER_AND_PICKLE_IF_PARAMETERIZED;
55-
// TODO: Read from standard in, or read/write from files.
56-
Writer out = spec.commandLine().getOut();
57-
58-
try (InputStream in = Files.newInputStream(source)) {
59-
try (NdjsonToMessageIterable envelopes = new NdjsonToMessageIterable(in, deserializer)) {
60-
try (MessagesToJunitXmlWriter writer = new MessagesToJunitXmlWriter(exampleNameStrategy, out)) {
61-
for (Envelope envelope : envelopes) {
62-
// TODO: What if exception?
63-
writer.write(envelope);
64-
}
65-
}
66-
}
123+
return json -> jsonMapper.readValue(json, Envelope.class);
124+
}
125+
126+
private PrintWriter outputPrintWriter() {
127+
if (output == null) {
128+
return spec.commandLine().getOut();
129+
}
130+
Path path = outputPath();
131+
try {
132+
return new PrintWriter(
133+
new OutputStreamWriter(
134+
newOutputStream(path, CREATE, TRUNCATE_EXISTING),
135+
StandardCharsets.UTF_8
136+
)
137+
);
138+
} catch (IOException e) {
139+
throw new ParameterException(
140+
spec.commandLine(),
141+
("Invalid value '%s' for option '--output': Could not " +
142+
"write to '%s'"
143+
).formatted(output, path), e);
67144
}
68-
return 0;
69145
}
70146

71-
public static void main(String... args) {
72-
int exitCode = new CommandLine(new JunitXmlFormatter()).execute(args);
73-
System.exit(exitCode);
147+
private Path outputPath() {
148+
if (!isDestinationDirectory()) {
149+
return output;
150+
}
151+
152+
// Given a directory, decide on a file name
153+
var fileName = source.getFileName().toString();
154+
var index = fileName.lastIndexOf(".");
155+
if (index >= 0) {
156+
fileName = fileName.substring(0, index);
157+
}
158+
var candidate = output.resolve(fileName + ".xml");
159+
160+
// Avoid overwriting existing files when we decided the file name.
161+
var counter = 1;
162+
while(Files.exists(candidate)) {
163+
candidate = output.resolve(fileName + "." + counter + ".xml");
164+
}
165+
return candidate;
74166
}
167+
168+
private InputStream sourceInputStream() {
169+
if (isSourceSystemIn()) {
170+
return System.in;
171+
}
172+
try {
173+
return Files.newInputStream(source);
174+
} catch (IOException e) {
175+
throw new ParameterException(spec.commandLine(), "Invalid argument, could not read '%s'".formatted(source), e);
176+
}
177+
}
178+
179+
75180
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package io.cucumber.junitxmlformatter.cli;
2+
3+
import picocli.CommandLine.IVersionProvider;
4+
5+
import java.util.Optional;
6+
import java.util.function.Function;
7+
8+
import static io.cucumber.junitxmlformatter.cli.JunitXmlFormatter.JUNIT_XML_FORMATTER_NAME;
9+
10+
public class ManifestVersionProvider implements IVersionProvider {
11+
12+
@Override
13+
public String[] getVersion() {
14+
var version = getAttribute(Package::getImplementationVersion).orElse("DEVELOPMENT");
15+
return new String[]{JUNIT_XML_FORMATTER_NAME + " " + version};
16+
}
17+
18+
private static Optional<String> getAttribute(Function<Package, String> function) {
19+
return Optional.ofNullable(ManifestVersionProvider.class.getPackage()).map(function);
20+
}
21+
}

0 commit comments

Comments
 (0)