Skip to content

Commit 4a23c96

Browse files
committed
[Core] Fix JUnit report xml attributes
Cucumber JUnits xml format doesn't pass XSD validation in the Jenkins XUnit plugin because it is missing the errors attribute. Additionally time should be presented using numbers with both decimal and thousands separators. See: https://maven.apache.org/surefire/maven-surefire-plugin/xsd/surefire-test-report.xsd Closes #1777
1 parent 8ee4880 commit 4a23c96

File tree

6 files changed

+47
-55
lines changed

6 files changed

+47
-55
lines changed

core/src/main/java/io/cucumber/core/plugin/JUnitFormatter.java

+30-38
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,20 @@
11
package io.cucumber.core.plugin;
22

3-
import io.cucumber.plugin.event.PickleStepTestStep;
3+
import io.cucumber.core.exception.CucumberException;
4+
import io.cucumber.plugin.EventListener;
5+
import io.cucumber.plugin.StrictAware;
46
import io.cucumber.plugin.event.EventPublisher;
7+
import io.cucumber.plugin.event.PickleStepTestStep;
58
import io.cucumber.plugin.event.Result;
69
import io.cucumber.plugin.event.Status;
710
import io.cucumber.plugin.event.TestCaseFinished;
811
import io.cucumber.plugin.event.TestCaseStarted;
912
import io.cucumber.plugin.event.TestRunFinished;
13+
import io.cucumber.plugin.event.TestRunStarted;
1014
import io.cucumber.plugin.event.TestSourceRead;
1115
import io.cucumber.plugin.event.TestStepFinished;
12-
import io.cucumber.core.exception.CucumberException;
13-
14-
import io.cucumber.plugin.EventListener;
15-
import io.cucumber.plugin.StrictAware;
1616
import org.w3c.dom.Document;
1717
import org.w3c.dom.Element;
18-
import org.w3c.dom.NodeList;
1918

2019
import javax.xml.XMLConstants;
2120
import javax.xml.parsers.DocumentBuilderFactory;
@@ -26,10 +25,6 @@
2625
import javax.xml.transform.TransformerFactory;
2726
import javax.xml.transform.dom.DOMSource;
2827
import javax.xml.transform.stream.StreamResult;
29-
30-
import static java.util.Locale.ROOT;
31-
import static java.util.concurrent.TimeUnit.SECONDS;
32-
3328
import java.io.Closeable;
3429
import java.io.IOException;
3530
import java.io.PrintWriter;
@@ -38,13 +33,18 @@
3833
import java.net.URL;
3934
import java.text.DecimalFormat;
4035
import java.text.NumberFormat;
36+
import java.time.Duration;
37+
import java.time.Instant;
4138
import java.util.ArrayList;
4239
import java.util.List;
4340
import java.util.Locale;
4441

42+
import static java.util.Locale.ROOT;
43+
import static java.util.concurrent.TimeUnit.SECONDS;
44+
4545
public final class JUnitFormatter implements EventListener, StrictAware {
4646

47-
private static final long NANOS_PER_SECONDS = SECONDS.toNanos(1L);
47+
private static final long MILLIS_PER_SECOND = SECONDS.toMillis(1L);
4848
private final Writer writer;
4949
private final Document document;
5050
private final Element rootElement;
@@ -55,6 +55,7 @@ public final class JUnitFormatter implements EventListener, StrictAware {
5555
private String currentFeatureFile = null;
5656
private String previousTestCaseName;
5757
private int exampleNumber;
58+
private Instant started;
5859

5960
@SuppressWarnings("WeakerAccess") // Used by plugin factory
6061
public JUnitFormatter(URL writer) throws IOException {
@@ -72,13 +73,24 @@ private static String getUniqueTestNameForScenarioExample(String testCaseName, i
7273
return testCaseName + (testCaseName.contains(" ") ? " " : "_") + exampleNumber;
7374
}
7475

76+
private static String calculateTotalDurationString(Duration result) {
77+
DecimalFormat numberFormat = (DecimalFormat) NumberFormat.getNumberInstance(Locale.US);
78+
double duration = (double) result.toMillis() / MILLIS_PER_SECOND;
79+
return numberFormat.format(duration);
80+
}
81+
7582
@Override
7683
public void setEventPublisher(EventPublisher publisher) {
84+
publisher.registerHandlerFor(TestRunStarted.class, this::handleTestRunStarted);
7785
publisher.registerHandlerFor(TestSourceRead.class, this::handleTestSourceRead);
7886
publisher.registerHandlerFor(TestCaseStarted.class, this::handleTestCaseStarted);
7987
publisher.registerHandlerFor(TestCaseFinished.class, this::handleTestCaseFinished);
8088
publisher.registerHandlerFor(TestStepFinished.class, this::handleTestStepFinished);
81-
publisher.registerHandlerFor(TestRunFinished.class, event -> finishReport());
89+
publisher.registerHandlerFor(TestRunFinished.class, this::handleTestRunFinished);
90+
}
91+
92+
private void handleTestRunStarted(TestRunStarted event) {
93+
this.started = event.getInstant();
8294
}
8395

8496
@Override
@@ -119,13 +131,15 @@ private void handleTestCaseFinished(TestCaseFinished event) {
119131
}
120132
}
121133

122-
private void finishReport() {
134+
private void handleTestRunFinished(TestRunFinished event) {
123135
try {
136+
Instant finished = event.getInstant();
124137
// set up a transformer
125138
rootElement.setAttribute("name", JUnitFormatter.class.getName());
126139
rootElement.setAttribute("failures", String.valueOf(rootElement.getElementsByTagName("failure").getLength()));
127140
rootElement.setAttribute("skipped", String.valueOf(rootElement.getElementsByTagName("skipped").getLength()));
128-
rootElement.setAttribute("time", getTotalDuration(rootElement.getElementsByTagName("testcase")));
141+
rootElement.setAttribute("errors", "0");
142+
rootElement.setAttribute("time", calculateTotalDurationString(Duration.between(started, finished)));
129143

130144
TransformerFactory factory = TransformerFactory.newInstance();
131145
factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
@@ -148,22 +162,6 @@ private void closeQuietly(Closeable out) {
148162
}
149163
}
150164

151-
private String getTotalDuration(NodeList testCaseNodes) {
152-
double totalDurationSecondsForAllTimes = 0.0d;
153-
for (int i = 0; i < testCaseNodes.getLength(); i++) {
154-
try {
155-
double testCaseTime =
156-
Double.parseDouble(testCaseNodes.item(i).getAttributes().getNamedItem("time").getNodeValue());
157-
totalDurationSecondsForAllTimes += testCaseTime;
158-
} catch (NumberFormatException | NullPointerException e) {
159-
throw new CucumberException(e);
160-
}
161-
}
162-
DecimalFormat nfmt = (DecimalFormat) NumberFormat.getNumberInstance(Locale.US);
163-
nfmt.applyPattern("0.######");
164-
return nfmt.format(totalDurationSecondsForAllTimes);
165-
}
166-
167165
private void increaseAttributeValue(Element element, String attribute) {
168166
int value = 0;
169167
if (element.hasAttribute(attribute)) {
@@ -203,7 +201,7 @@ private String calculateElementName(io.cucumber.plugin.event.TestCase testCase)
203201
}
204202

205203
void addTestCaseElement(Document doc, Element tc, Result result) {
206-
tc.setAttribute("time", calculateTotalDurationString(result));
204+
tc.setAttribute("time", calculateTotalDurationString(result.getDuration()));
207205

208206
StringBuilder sb = new StringBuilder();
209207
addStepAndResultListing(sb);
@@ -229,20 +227,14 @@ void addTestCaseElement(Document doc, Element tc, Result result) {
229227
}
230228

231229
void handleEmptyTestCase(Document doc, Element tc, Result result) {
232-
tc.setAttribute("time", calculateTotalDurationString(result));
230+
tc.setAttribute("time", calculateTotalDurationString(result.getDuration()));
233231

234232
String resultType = strict ? "failure" : "skipped";
235233
Element child = createElementWithMessage(doc, new StringBuilder(), resultType, "The scenario has no steps");
236234

237235
tc.appendChild(child);
238236
}
239237

240-
private String calculateTotalDurationString(Result result) {
241-
DecimalFormat numberFormat = (DecimalFormat) NumberFormat.getNumberInstance(Locale.US);
242-
numberFormat.applyPattern("0.######");
243-
return numberFormat.format(((double) result.getDuration().toNanos() / NANOS_PER_SECONDS));
244-
}
245-
246238
private void addStepAndResultListing(StringBuilder sb) {
247239
for (int i = 0; i < steps.size(); i++) {
248240
int length = sb.length();

core/src/test/java/io/cucumber/core/plugin/JUnitFormatterTest.java

+13-13
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ void should_format_passed_scenario() throws Throwable {
8686
String formatterOutput = runFeaturesWithFormatter();
8787

8888
String expected = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n" +
89-
"<testsuite failures=\"0\" name=\"io.cucumber.core.plugin.JUnitFormatter\" skipped=\"0\" tests=\"1\" time=\"0.003\">\n" +
89+
"<testsuite failures=\"0\" name=\"io.cucumber.core.plugin.JUnitFormatter\" skipped=\"0\" errors=\"0\" tests=\"1\" time=\"0.003\">\n" +
9090
" <testcase classname=\"feature name\" name=\"scenario name\" time=\"0.003\">\n" +
9191
" <system-out><![CDATA[" +
9292
"Given first step............................................................passed\n" +
@@ -111,13 +111,13 @@ void should_format_skipped_scenario() throws Throwable {
111111
stepsToResult.put("first step", result("skipped", exception));
112112
stepsToResult.put("second step", result("skipped"));
113113
stepsToResult.put("third step", result("skipped"));
114-
stepDuration = Duration.ofMillis(1L);
114+
stepDuration = Duration.ofNanos(1_000_001L);
115115

116116
String formatterOutput = runFeaturesWithFormatter();
117117

118118
String stackTrace = getStackTrace(exception);
119119
String expected = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n" +
120-
"<testsuite failures=\"0\" name=\"io.cucumber.core.plugin.JUnitFormatter\" skipped=\"1\" tests=\"1\" time=\"0.003\">\n" +
120+
"<testsuite failures=\"0\" name=\"io.cucumber.core.plugin.JUnitFormatter\" skipped=\"1\" errors=\"0\" tests=\"1\" time=\"0.003\">\n" +
121121
" <testcase classname=\"feature name\" name=\"scenario name\" time=\"0.003\">\n" +
122122
" <skipped message=\"" + stackTrace.replace("\n\t", "&#10;&#9;").replaceAll("\r", "&#13;") + "\"><![CDATA[" +
123123
"Given first step............................................................skipped\n" +
@@ -149,7 +149,7 @@ void should_format_pending_scenario() throws Throwable {
149149
String formatterOutput = runFeaturesWithFormatter();
150150

151151
String expected = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n" +
152-
"<testsuite failures=\"0\" name=\"io.cucumber.core.plugin.JUnitFormatter\" skipped=\"1\" tests=\"1\" time=\"0.003\">\n" +
152+
"<testsuite failures=\"0\" name=\"io.cucumber.core.plugin.JUnitFormatter\" skipped=\"1\" errors=\"0\" tests=\"1\" time=\"0.003\">\n" +
153153
" <testcase classname=\"feature name\" name=\"scenario name\" time=\"0.003\">\n" +
154154
" <skipped><![CDATA[" +
155155
"Given first step............................................................pending\n" +
@@ -178,7 +178,7 @@ void should_format_failed_scenario() throws Throwable {
178178
String formatterOutput = runFeaturesWithFormatter();
179179

180180
String expected = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n" +
181-
"<testsuite failures=\"1\" name=\"io.cucumber.core.plugin.JUnitFormatter\" skipped=\"0\" tests=\"1\" time=\"0.003\">\n" +
181+
"<testsuite failures=\"1\" name=\"io.cucumber.core.plugin.JUnitFormatter\" skipped=\"0\" errors=\"0\" tests=\"1\" time=\"0.003\">\n" +
182182
" <testcase classname=\"feature name\" name=\"scenario name\" time=\"0.003\">\n" +
183183
" <failure message=\"the stack trace\"><![CDATA[" +
184184
"Given first step............................................................passed\n" +
@@ -211,7 +211,7 @@ void should_handle_failure_in_before_hook() throws Throwable {
211211
String formatterOutput = runFeaturesWithFormatter();
212212

213213
String expected = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n" +
214-
"<testsuite failures=\"1\" name=\"io.cucumber.core.plugin.JUnitFormatter\" skipped=\"0\" tests=\"1\" time=\"0.004\">\n" +
214+
"<testsuite failures=\"1\" name=\"io.cucumber.core.plugin.JUnitFormatter\" skipped=\"0\" errors=\"0\" tests=\"1\" time=\"0.004\">\n" +
215215
" <testcase classname=\"feature name\" name=\"scenario name\" time=\"0.004\">\n" +
216216
" <failure message=\"the stack trace\"><![CDATA[" +
217217
"Given first step............................................................skipped\n" +
@@ -244,7 +244,7 @@ void should_handle_pending_in_before_hook() throws Throwable {
244244
String formatterOutput = runFeaturesWithFormatter();
245245

246246
String expected = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n" +
247-
"<testsuite failures=\"0\" name=\"io.cucumber.core.plugin.JUnitFormatter\" skipped=\"1\" tests=\"1\" time=\"0.004\">\n" +
247+
"<testsuite failures=\"0\" name=\"io.cucumber.core.plugin.JUnitFormatter\" skipped=\"1\" errors=\"0\" tests=\"1\" time=\"0.004\">\n" +
248248
" <testcase classname=\"feature name\" name=\"scenario name\" time=\"0.004\">\n" +
249249
" <skipped><![CDATA[" +
250250
"Given first step............................................................skipped\n" +
@@ -275,7 +275,7 @@ void should_handle_failure_in_before_hook_with_background() throws Throwable {
275275
String formatterOutput = runFeaturesWithFormatter();
276276

277277
String expected = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n" +
278-
"<testsuite failures=\"1\" name=\"io.cucumber.core.plugin.JUnitFormatter\" skipped=\"0\" tests=\"1\" time=\"0.004\">\n" +
278+
"<testsuite failures=\"1\" name=\"io.cucumber.core.plugin.JUnitFormatter\" skipped=\"0\" errors=\"0\" tests=\"1\" time=\"0.004\">\n" +
279279
" <testcase classname=\"feature name\" name=\"scenario name\" time=\"0.004\">\n" +
280280
" <failure message=\"the stack trace\"><![CDATA[" +
281281
"Given first step............................................................skipped\n" +
@@ -308,7 +308,7 @@ void should_handle_failure_in_after_hook() throws Throwable {
308308
String formatterOutput = runFeaturesWithFormatter();
309309

310310
String expected = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n" +
311-
"<testsuite failures=\"1\" name=\"io.cucumber.core.plugin.JUnitFormatter\" skipped=\"0\" tests=\"1\" time=\"0.004\">\n" +
311+
"<testsuite failures=\"1\" name=\"io.cucumber.core.plugin.JUnitFormatter\" skipped=\"0\" errors=\"0\" tests=\"1\" time=\"0.004\">\n" +
312312
" <testcase classname=\"feature name\" name=\"scenario name\" time=\"0.004\">\n" +
313313
" <failure message=\"the stack trace\"><![CDATA[" +
314314
"Given first step............................................................passed\n" +
@@ -340,7 +340,7 @@ void should_accumulate_time_from_steps_and_hooks() throws Throwable {
340340
String formatterOutput = runFeaturesWithFormatter();
341341

342342
String expected = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n" +
343-
"<testsuite failures=\"0\" name=\"io.cucumber.core.plugin.JUnitFormatter\" skipped=\"0\" tests=\"1\" time=\"0.004\">\n" +
343+
"<testsuite failures=\"0\" name=\"io.cucumber.core.plugin.JUnitFormatter\" skipped=\"0\" errors=\"0\" tests=\"1\" time=\"0.004\">\n" +
344344
" <testcase classname=\"feature name\" name=\"scenario name\" time=\"0.004\">\n" +
345345
" <system-out><![CDATA[" +
346346
"* first step................................................................passed\n" +
@@ -373,7 +373,7 @@ void should_format_scenario_outlines() throws Throwable {
373373
String formatterOutput = runFeaturesWithFormatter();
374374

375375
String expected = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n" +
376-
"<testsuite failures=\"0\" name=\"io.cucumber.core.plugin.JUnitFormatter\" skipped=\"0\" tests=\"2\" time=\"0.006\">\n" +
376+
"<testsuite failures=\"0\" name=\"io.cucumber.core.plugin.JUnitFormatter\" skipped=\"0\" errors=\"0\" tests=\"2\" time=\"0.006\">\n" +
377377
" <testcase classname=\"feature name\" name=\"outline_name\" time=\"0.003\">\n" +
378378
" <system-out><![CDATA[" +
379379
"Given first step \"a\"........................................................passed\n" +
@@ -420,7 +420,7 @@ void should_format_scenario_outlines_with_multiple_examples() throws Throwable {
420420
String formatterOutput = runFeaturesWithFormatter();
421421

422422
String expected = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n" +
423-
"<testsuite failures=\"0\" name=\"io.cucumber.core.plugin.JUnitFormatter\" skipped=\"0\" tests=\"4\" time=\"0.012\">\n" +
423+
"<testsuite failures=\"0\" name=\"io.cucumber.core.plugin.JUnitFormatter\" skipped=\"0\" errors=\"0\" tests=\"4\" time=\"0.012\">\n" +
424424
" <testcase classname=\"feature name\" name=\"outline name\" time=\"0.003\">\n" +
425425
" <system-out><![CDATA[" +
426426
"Given first step \"a\"........................................................passed\n" +
@@ -475,7 +475,7 @@ void should_format_scenario_outlines_with_arguments_in_name() throws Throwable {
475475
String formatterOutput = runFeaturesWithFormatter();
476476

477477
String expected = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n" +
478-
"<testsuite failures=\"0\" name=\"io.cucumber.core.plugin.JUnitFormatter\" skipped=\"0\" tests=\"2\" time=\"0.006\">\n" +
478+
"<testsuite failures=\"0\" name=\"io.cucumber.core.plugin.JUnitFormatter\" skipped=\"0\" errors=\"0\" tests=\"2\" time=\"0.006\">\n" +
479479
" <testcase classname=\"feature name\" name=\"outline name a\" time=\"0.003\">\n" +
480480
" <system-out><![CDATA[" +
481481
"Given first step \"a\"........................................................passed\n" +

core/src/test/resources/io/cucumber/core/plugin/JUnitFormatterTest_1.report.xml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
2-
<testsuite tests="2" failures="0" skipped="2" time="0" name="io.cucumber.core.plugin.JUnitFormatter">
2+
<testsuite tests="2" failures="0" skipped="2" errors="0" time="0" name="io.cucumber.core.plugin.JUnitFormatter">
33
<testcase classname="Feature_1" name="Scenario_1" time="0">
44
<skipped><![CDATA[Given step_1................................................................undefined
55
When step_2.................................................................undefined

core/src/test/resources/io/cucumber/core/plugin/JUnitFormatterTest_1_strict.report.xml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
2-
<testsuite failures="2" name="io.cucumber.core.plugin.JUnitFormatter" skipped="0" tests="2" time="0">
2+
<testsuite failures="2" name="io.cucumber.core.plugin.JUnitFormatter" skipped="0" errors="0" tests="2" time="0">
33
<testcase classname="Feature_1" name="Scenario_1" time="0">
44
<failure message="The scenario has pending or undefined step(s)"><![CDATA[Given step_1................................................................undefined
55
When step_2.................................................................undefined

core/src/test/resources/io/cucumber/core/plugin/JUnitFormatterTest_2.report.xml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
2-
<testsuite tests="2" failures="0" skipped="2" time="0" name="io.cucumber.core.plugin.JUnitFormatter">
2+
<testsuite tests="2" failures="0" skipped="2" errors="0" time="0" name="io.cucumber.core.plugin.JUnitFormatter">
33
<testcase classname="Feature_2" name="Scenario_1" time="0">
44
<skipped><![CDATA[Given bg_1..................................................................undefined
55
When bg_2...................................................................undefined

core/src/test/resources/io/cucumber/core/plugin/JUnitFormatterTest_3.report.xml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
2-
<testsuite failures="0" tests="4" skipped="4" time="0" name="io.cucumber.core.plugin.JUnitFormatter">
2+
<testsuite failures="0" tests="4" skipped="4" errors="0" time="0" name="io.cucumber.core.plugin.JUnitFormatter">
33
<testcase classname="Feature_3" name="Scenario_1" time="0">
44
<skipped><![CDATA[Given bg_1..................................................................undefined
55
When bg_2...................................................................undefined

0 commit comments

Comments
 (0)