Skip to content

Commit dfbfc76

Browse files
committed
Add TestNG XML
1 parent 2e21126 commit dfbfc76

File tree

11 files changed

+326
-5
lines changed

11 files changed

+326
-5
lines changed

java/.gitignore

Lines changed: 0 additions & 2 deletions
This file was deleted.
File renamed without changes.

java/pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,11 @@
6161
<artifactId>junit-xml-formatter</artifactId>
6262
<version>0.5.0</version>
6363
</dependency>
64+
<dependency>
65+
<groupId>io.cucumber</groupId>
66+
<artifactId>testng-xml-formatter</artifactId>
67+
<version>0.2.0</version>
68+
</dependency>
6469
<dependency>
6570
<groupId>com.fasterxml.jackson.core</groupId>
6671
<artifactId>jackson-databind</artifactId>

java/src/main/java/io/cucumber/messages/cli/MessagesCli.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99
description = "Work with Cucumber messages",
1010
versionProvider = ManifestVersionProvider.class,
1111
subcommands = {
12-
JunitXmlCommand.class
12+
JunitXmlCommand.class,
13+
TestngXmlCommand.class
1314
}
1415
)
1516
final class MessagesCli {
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package io.cucumber.messages.cli;
2+
3+
import io.cucumber.junitxmlformatter.MessagesToJunitXmlWriter;
4+
import io.cucumber.messages.NdjsonToMessageIterable;
5+
import io.cucumber.query.NamingStrategy.ExampleName;
6+
import io.cucumber.testngxmlformatter.MessagesToTestngXmlWriter;
7+
import picocli.CommandLine.Command;
8+
import picocli.CommandLine.Model.CommandSpec;
9+
import picocli.CommandLine.Option;
10+
import picocli.CommandLine.Parameters;
11+
import picocli.CommandLine.Spec;
12+
13+
import java.io.IOException;
14+
import java.nio.file.Path;
15+
import java.util.concurrent.Callable;
16+
17+
@Command(
18+
name = "testng-xml",
19+
description = "Converts Cucumber messages to JUnit XML",
20+
mixinStandardHelpOptions = true
21+
)
22+
class TestngXmlCommand implements Callable<Integer> {
23+
24+
@Spec
25+
private CommandSpec spec;
26+
27+
@Parameters(
28+
index = "0",
29+
paramLabel = "file",
30+
description = "The input file containing Cucumber messages. " +
31+
"Use - to read from the standard input."
32+
)
33+
private Path source;
34+
35+
@Option(
36+
names = {"-o", "--output"},
37+
arity = "0..1",
38+
paramLabel = "file",
39+
description = "The output file containing JUnit XML. " +
40+
"If file is a directory, a new file be " +
41+
"created by taking the name of the input file and " +
42+
"replacing the suffix with '.xml'. If the file is omitted " +
43+
"the current working directory is used."
44+
)
45+
private Path output;
46+
47+
@Option(
48+
names = {"-e", "--example-naming-strategy"},
49+
paramLabel = "strategy",
50+
description = "How to name examples. Valid values: ${COMPLETION-CANDIDATES}",
51+
defaultValue = "NUMBER_AND_PICKLE_IF_PARAMETERIZED"
52+
)
53+
private ExampleName exampleNameStrategy;
54+
55+
private static String xml(String fileName, int counter) {
56+
return fileName + "." + counter + ".xml";
57+
}
58+
59+
@Override
60+
public Integer call() throws IOException {
61+
var options = new CommonOptions(spec, source, output, TestngXmlCommand::xml);
62+
63+
try (var envelopes = new NdjsonToMessageIterable(options.sourceInputStream(), Jackson.deserializer());
64+
var writer = new MessagesToTestngXmlWriter(exampleNameStrategy, options.outputPrintWriter())
65+
) {
66+
for (var envelope : envelopes) {
67+
writer.write(envelope);
68+
}
69+
}
70+
return 0;
71+
}
72+
73+
74+
}

java/src/test/java/io/cucumber/messages/cli/JunitXmlCommandTest.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
class JunitXmlCommandTest {
2626

2727
static final Path minimalFeatureNdjson = Paths.get("../testdata/minimal.feature.ndjson");
28-
static final Path minimalFeatureXml = Paths.get("../testdata/minimal.feature.xml");
28+
static final Path minimalFeatureXml = Paths.get("../testdata/junit/minimal.feature.xml");
2929

3030
final ByteArrayOutputStream stdOut = new ByteArrayOutputStream();
3131
final StringWriter stdErr = new StringWriter();
@@ -131,8 +131,10 @@ void failsToWriteToReadOnlyOutputFile() throws IOException {
131131
}
132132

133133
@Test
134-
void writesFileToCurrentWorkingDirectory() {
134+
void writesFileToCurrentWorkingDirectory() throws IOException {
135135
var destination = Paths.get("minimal.feature.xml");
136+
Files.deleteIfExists(destination);
137+
136138
var exitCode = cmd.execute("junit-xml", "../testdata/minimal.feature.ndjson", "--output");
137139
assertAll(
138140
() -> assertThat(exitCode).isZero(),
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
package io.cucumber.messages.cli;
2+
3+
import org.junit.jupiter.api.AfterEach;
4+
import org.junit.jupiter.api.BeforeEach;
5+
import org.junit.jupiter.api.Test;
6+
import org.junit.jupiter.api.io.TempDir;
7+
import picocli.CommandLine;
8+
9+
import java.io.ByteArrayOutputStream;
10+
import java.io.IOException;
11+
import java.io.InputStream;
12+
import java.io.PrintStream;
13+
import java.io.PrintWriter;
14+
import java.io.StringWriter;
15+
import java.nio.file.Files;
16+
import java.nio.file.Path;
17+
import java.nio.file.Paths;
18+
19+
import static java.nio.charset.StandardCharsets.UTF_8;
20+
import static java.nio.file.Files.newInputStream;
21+
import static java.nio.file.Files.readString;
22+
import static org.assertj.core.api.Assertions.assertThat;
23+
import static org.junit.jupiter.api.Assertions.assertAll;
24+
25+
class TestngXmlCommandTest {
26+
27+
static final Path minimalFeatureNdjson = Paths.get("../testdata/minimal.feature.ndjson");
28+
static final Path minimalFeatureXml = Paths.get("../testdata/testng/minimal.feature.xml");
29+
30+
final ByteArrayOutputStream stdOut = new ByteArrayOutputStream();
31+
final StringWriter stdErr = new StringWriter();
32+
CommandLine cmd;
33+
InputStream originalSystemIn;
34+
PrintStream originalSystemOut;
35+
36+
@TempDir
37+
Path tmp;
38+
39+
@BeforeEach
40+
void setup() {
41+
cmd = MessagesCli.createCommandLine();
42+
// TODO: Use mocking, but has wrong type. Ask pico CLI for mock with PrintStream.
43+
originalSystemIn = System.in;
44+
originalSystemOut = System.out;
45+
System.setOut(new PrintStream(stdOut));
46+
cmd.setErr(new PrintWriter(stdErr));
47+
}
48+
49+
@AfterEach
50+
void cleanup() {
51+
System.setIn(originalSystemIn);
52+
System.setOut(originalSystemOut);
53+
// Helps with debugging
54+
System.out.println(stdOut.toString(UTF_8));
55+
System.out.println(stdErr);
56+
}
57+
58+
@Test
59+
void help() {
60+
int exitCode = cmd.execute("testng-xml", "--help");
61+
assertThat(exitCode).isZero();
62+
}
63+
64+
@Test
65+
void writeToSystemOut() {
66+
var exitCode = cmd.execute("testng-xml", "../testdata/minimal.feature.ndjson");
67+
assertAll(
68+
() -> assertThat(exitCode).isZero(),
69+
() -> assertThat(stdOut.toString())
70+
.isEqualTo(readString(minimalFeatureXml))
71+
);
72+
}
73+
74+
@Test
75+
void failsToReadNonExistingFile() {
76+
var exitCode = cmd.execute("testng-xml", "../testdata/no-such.feature.ndjson");
77+
assertAll(
78+
() -> assertThat(exitCode).isEqualTo(2),
79+
() -> assertThat(stdErr.toString())
80+
.contains("Invalid argument, could not read '../testdata/no-such.feature.ndjson'")
81+
);
82+
}
83+
84+
@Test
85+
void readsFromSystemIn() throws IOException {
86+
System.setIn(newInputStream(minimalFeatureNdjson));
87+
var exitCode = cmd.execute("testng-xml", "-");
88+
assertAll(
89+
() -> assertThat(exitCode).isZero(),
90+
() -> assertThat(stdOut.toString())
91+
.isEqualTo(readString(minimalFeatureXml))
92+
);
93+
}
94+
95+
@Test
96+
void writesToOutputFile() {
97+
var destination = tmp.resolve("minimal.feature.xml");
98+
var exitCode = cmd.execute("testng-xml", "../testdata/minimal.feature.ndjson", "--output", destination.toString());
99+
assertAll(
100+
() -> assertThat(exitCode).isZero(),
101+
() -> assertThat(readString(destination))
102+
.isEqualTo(readString(minimalFeatureXml))
103+
);
104+
}
105+
106+
@Test
107+
void doesNotOverwriteWhenWritingToDirectory() {
108+
var exitCode1 = cmd.execute("testng-xml", "../testdata/minimal.feature.ndjson", "--output", tmp.toString());
109+
var exitCode2 = cmd.execute("testng-xml", "../testdata/minimal.feature.ndjson", "--output", tmp.toString());
110+
assertAll(
111+
() -> assertThat(exitCode1).isZero(),
112+
() -> assertThat(tmp.resolve("minimal.feature.xml")).exists(),
113+
() -> assertThat(exitCode2).isZero(),
114+
() -> assertThat(tmp.resolve("minimal.feature.1.xml")).exists()
115+
);
116+
}
117+
118+
@Test
119+
void failsToWriteToReadOnlyOutputFile() throws IOException {
120+
var destination = Files.createFile(tmp.resolve("minimal.feature.xml"));
121+
var isReadOnly = destination.toFile().setReadOnly();
122+
assertThat(isReadOnly).isTrue();
123+
124+
var exitCode = cmd.execute("testng-xml", "../testdata/minimal.feature.ndjson", "--output", destination.toString());
125+
assertAll(
126+
() -> assertThat(exitCode).isEqualTo(2),
127+
() -> assertThat(stdErr.toString())
128+
.contains("Invalid value '%s' for option '--output': Could not write to '%s'"
129+
.formatted(destination, destination))
130+
);
131+
}
132+
133+
@Test
134+
void writesFileToCurrentWorkingDirectory() throws IOException {
135+
var destination = Paths.get("minimal.feature.xml");
136+
Files.deleteIfExists(destination);
137+
138+
var exitCode = cmd.execute("testng-xml", "../testdata/minimal.feature.ndjson", "--output");
139+
assertAll(
140+
() -> assertThat(exitCode).isZero(),
141+
() -> assertThat(readString(destination))
142+
.isEqualTo(readString(minimalFeatureXml))
143+
);
144+
}
145+
146+
@Test
147+
void canNotGuessFileNameWhenReadingFromSystemIn() throws IOException {
148+
System.setIn(newInputStream(minimalFeatureNdjson));
149+
var exitCode = cmd.execute("testng-xml", "-", "--output");
150+
assertAll(
151+
() -> assertThat(exitCode).isEqualTo(2),
152+
() -> assertThat(stdErr.toString())
153+
.contains("Invalid value '' for option '--output': When reading from standard input, output can not be a directory")
154+
);
155+
}
156+
157+
@Test
158+
void useExampleNamingStrategy() {
159+
var exitCode = cmd.execute("testng-xml", "../testdata/examples-tables.feature.ndjson", "--example-naming-strategy", "NUMBER");
160+
assertAll(
161+
() -> assertThat(exitCode).isZero(),
162+
() -> assertThat(stdOut.toString())
163+
.contains("name=\"Eating cucumbers with &lt;friends&gt; friends - #1.1\"")
164+
);
165+
}
166+
167+
}

testdata/junit/minimal.feature.xml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<testsuite name="Cucumber" time="0.005" tests="1" skipped="0" failures="0" errors="0">
3+
<testcase classname="minimal" name="cukes" time="0.003">
4+
<system-out><![CDATA[
5+
Given I have 42 cukes in my belly...........................................passed
6+
]]></system-out>
7+
</testcase>
8+
</testsuite>
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<testng-results failed="4" passed="5" skipped="0" total="9">
3+
<suite name="Cucumber" duration-ms="73">
4+
<test name="Cucumber" duration-ms="73">
5+
<class name="Examples Tables">
6+
<test-method name="Eating cucumbers - These are passing - #1.1" status="PASS" duration-ms="7" started-at="1970-01-01T00:00:00.001Z" finished-at="1970-01-01T00:00:00.008Z"/>
7+
<test-method name="Eating cucumbers - These are passing - #1.2" status="PASS" duration-ms="7" started-at="1970-01-01T00:00:00.009Z" finished-at="1970-01-01T00:00:00.016Z"/>
8+
<test-method name="Eating cucumbers - These are failing - #2.1" status="FAIL" duration-ms="7" started-at="1970-01-01T00:00:00.017Z" finished-at="1970-01-01T00:00:00.024Z">
9+
<exception class="AssertionError">
10+
<message><![CDATA[
11+
Given there are 12 cucumbers................................................passed
12+
When I eat 20 cucumbers.....................................................passed
13+
Then I should have 0 cucumbers..............................................failed
14+
]]></message>
15+
</exception>
16+
</test-method>
17+
<test-method name="Eating cucumbers - These are failing - #2.2" status="FAIL" duration-ms="7" started-at="1970-01-01T00:00:00.025Z" finished-at="1970-01-01T00:00:00.032Z">
18+
<exception class="AssertionError">
19+
<message><![CDATA[
20+
Given there are 0 cucumbers.................................................passed
21+
When I eat 1 cucumbers......................................................passed
22+
Then I should have 0 cucumbers..............................................failed
23+
]]></message>
24+
</exception>
25+
</test-method>
26+
<test-method name="Eating cucumbers - These are undefined because the value is not an {int} - #3.1" status="FAIL" duration-ms="7" started-at="1970-01-01T00:00:00.033Z" finished-at="1970-01-01T00:00:00.040Z">
27+
<exception class="The scenario has undefined step(s)">
28+
<message><![CDATA[
29+
Given there are 12 cucumbers................................................passed
30+
When I eat banana cucumbers.................................................undefined
31+
Then I should have 12 cucumbers.............................................skipped
32+
]]></message>
33+
<full-stacktrace>
34+
<![CDATA[The scenario has undefined step(s)]]>
35+
</full-stacktrace>
36+
</exception>
37+
</test-method>
38+
<test-method name="Eating cucumbers - These are undefined because the value is not an {int} - #3.2" status="FAIL" duration-ms="7" started-at="1970-01-01T00:00:00.041Z" finished-at="1970-01-01T00:00:00.048Z">
39+
<exception class="The scenario has undefined step(s)">
40+
<message><![CDATA[
41+
Given there are 0 cucumbers.................................................passed
42+
When I eat 1 cucumbers......................................................passed
43+
Then I should have apple cucumbers..........................................undefined
44+
]]></message>
45+
<full-stacktrace>
46+
<![CDATA[The scenario has undefined step(s)]]>
47+
</full-stacktrace>
48+
</exception>
49+
</test-method>
50+
<test-method name="Eating cucumbers with &lt;friends&gt; friends - #1.1: Eating cucumbers with 11 friends" status="PASS" duration-ms="7" started-at="1970-01-01T00:00:00.049Z" finished-at="1970-01-01T00:00:00.056Z"/>
51+
<test-method name="Eating cucumbers with &lt;friends&gt; friends - #1.2: Eating cucumbers with 1 friends" status="PASS" duration-ms="7" started-at="1970-01-01T00:00:00.057Z" finished-at="1970-01-01T00:00:00.064Z"/>
52+
<test-method name="Eating cucumbers with &lt;friends&gt; friends - #1.3: Eating cucumbers with 0 friends" status="PASS" duration-ms="7" started-at="1970-01-01T00:00:00.065Z" finished-at="1970-01-01T00:00:00.072Z"/>
53+
</class>
54+
</test>
55+
</suite>
56+
</testng-results>

0 commit comments

Comments
 (0)