Skip to content

Commit 3382706

Browse files
committed
Merge cucumber#818, "Implement TestNG-compatible XML formatter".
Also update History.md.
2 parents b5b43dc + 2fe6292 commit 3382706

File tree

9 files changed

+886
-154
lines changed

9 files changed

+886
-154
lines changed

History.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
## [1.2.3-SNAPSHOT](https://github.com/cucumber/cucumber-jvm/compare/v1.2.2...master) (In Git)
22

3+
* [Core] Implement TestNG-compatible XML formatter ([#818](https://github.com/cucumber/cucumber-jvm/pull/818), [#621](https://github.com/cucumber/cucumber-jvm/pull/621) Dimitry Berezhony, Björn Rasmusson)
34
* `DataTable.diff(List)` gives proper error message when the `List` argument is empty (Aslak Hellesøy)
45
* Execute no scenarios when the rerun file is empty ([#840](https://github.com/cucumber/cucumber-jvm/issues/840) Björn Rasmusson)
56
* Snippets for quoted arguments changed from `(.*?)` to `([^\"]*)` (which is how it was before 1.1.6). See [cucumber/cucumber#663](https://github.com/cucumber/cucumber/pull/663) (Aslak Hellesøy)

core/src/main/java/cucumber/runtime/formatter/PluginFactory.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ public class PluginFactory {
4646
private static final Map<String, Class> PLUGIN_CLASSES = new HashMap<String, Class>() {{
4747
put("null", NullFormatter.class);
4848
put("junit", JUnitFormatter.class);
49+
put("testng", TestNGFormatter.class);
4950
put("html", HTMLFormatter.class);
5051
put("pretty", CucumberPrettyFormatter.class);
5152
put("progress", ProgressFormatter.class);
Lines changed: 331 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,331 @@
1+
package cucumber.runtime.formatter;
2+
3+
import cucumber.runtime.CucumberException;
4+
import cucumber.runtime.io.URLOutputStream;
5+
import cucumber.runtime.io.UTF8OutputStreamWriter;
6+
import gherkin.formatter.Formatter;
7+
import gherkin.formatter.Reporter;
8+
import gherkin.formatter.model.Background;
9+
import gherkin.formatter.model.Examples;
10+
import gherkin.formatter.model.Feature;
11+
import gherkin.formatter.model.Match;
12+
import gherkin.formatter.model.Result;
13+
import gherkin.formatter.model.Scenario;
14+
import gherkin.formatter.model.ScenarioOutline;
15+
import gherkin.formatter.model.Step;
16+
import org.w3c.dom.Document;
17+
import org.w3c.dom.Element;
18+
import org.w3c.dom.NamedNodeMap;
19+
import org.w3c.dom.Node;
20+
import org.w3c.dom.NodeList;
21+
22+
import javax.xml.parsers.DocumentBuilderFactory;
23+
import javax.xml.parsers.ParserConfigurationException;
24+
import javax.xml.transform.OutputKeys;
25+
import javax.xml.transform.Transformer;
26+
import javax.xml.transform.TransformerException;
27+
import javax.xml.transform.TransformerFactory;
28+
import javax.xml.transform.dom.DOMSource;
29+
import javax.xml.transform.stream.StreamResult;
30+
import java.io.IOException;
31+
import java.io.PrintWriter;
32+
import java.io.StringWriter;
33+
import java.io.Writer;
34+
import java.net.URL;
35+
import java.text.SimpleDateFormat;
36+
import java.util.ArrayList;
37+
import java.util.Date;
38+
import java.util.List;
39+
40+
class TestNGFormatter implements Formatter, Reporter, StrictAware {
41+
42+
private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
43+
private final Writer writer;
44+
private final Document document;
45+
private final Element results;
46+
private final Element suite;
47+
private final Element test;
48+
private Element clazz;
49+
private Element root;
50+
private TestMethod testMethod;
51+
52+
public TestNGFormatter(URL url) throws IOException {
53+
this.writer = new UTF8OutputStreamWriter(new URLOutputStream(url));
54+
TestMethod.treatSkippedAsFailure = false;
55+
try {
56+
document = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();
57+
results = document.createElement("testng-results");
58+
suite = document.createElement("suite");
59+
test = document.createElement("test");
60+
suite.appendChild(test);
61+
results.appendChild(suite);
62+
document.appendChild(results);
63+
} catch (ParserConfigurationException e) {
64+
throw new CucumberException("Error initializing DocumentBuilder.", e);
65+
}
66+
}
67+
68+
@Override
69+
public void syntaxError(String state, String event, List<String> legalEvents, String uri, Integer line) {
70+
}
71+
72+
@Override
73+
public void setStrict(boolean strict) {
74+
TestMethod.treatSkippedAsFailure = strict;
75+
}
76+
77+
@Override
78+
public void uri(String uri) {
79+
}
80+
81+
@Override
82+
public void feature(Feature feature) {
83+
TestMethod.feature = feature;
84+
TestMethod.previousScenarioOutlineName = "";
85+
TestMethod.exampleNumber = 1;
86+
clazz = document.createElement("class");
87+
clazz.setAttribute("name", feature.getName());
88+
test.appendChild(clazz);
89+
}
90+
91+
@Override
92+
public void scenarioOutline(ScenarioOutline scenarioOutline) {
93+
testMethod = new TestMethod(null);
94+
}
95+
96+
@Override
97+
public void examples(Examples examples) {
98+
}
99+
100+
@Override
101+
public void startOfScenarioLifeCycle(Scenario scenario) {
102+
root = document.createElement("test-method");
103+
clazz.appendChild(root);
104+
testMethod = new TestMethod(scenario);
105+
testMethod.start(root);
106+
}
107+
108+
@Override
109+
public void before(Match match, Result result) {
110+
testMethod.hooks.add(result);
111+
}
112+
113+
@Override
114+
public void background(Background background) {
115+
}
116+
117+
@Override
118+
public void scenario(Scenario scenario) {
119+
}
120+
121+
@Override
122+
public void step(Step step) {
123+
testMethod.steps.add(step);
124+
}
125+
126+
@Override
127+
public void match(Match match) {
128+
}
129+
130+
@Override
131+
public void result(Result result) {
132+
testMethod.results.add(result);
133+
}
134+
135+
@Override
136+
public void embedding(String mimeType, byte[] data) {
137+
}
138+
139+
@Override
140+
public void write(String text) {
141+
}
142+
143+
@Override
144+
public void after(Match match, Result result) {
145+
testMethod.hooks.add(result);
146+
}
147+
148+
@Override
149+
public void endOfScenarioLifeCycle(Scenario scenario) {
150+
testMethod.finish(document, root);
151+
}
152+
153+
@Override
154+
public void eof() {
155+
}
156+
157+
@Override
158+
public void done() {
159+
try {
160+
results.setAttribute("total", String.valueOf(getElementsCountByAttribute(suite, "status", ".*")));
161+
results.setAttribute("passed", String.valueOf(getElementsCountByAttribute(suite, "status", "PASS")));
162+
results.setAttribute("failed", String.valueOf(getElementsCountByAttribute(suite, "status", "FAIL")));
163+
results.setAttribute("skipped", String.valueOf(getElementsCountByAttribute(suite, "status", "SKIP")));
164+
suite.setAttribute("name", TestNGFormatter.class.getName());
165+
suite.setAttribute("duration-ms", getTotalDuration(suite.getElementsByTagName("test-method")));
166+
test.setAttribute("name", TestNGFormatter.class.getName());
167+
test.setAttribute("duration-ms", getTotalDuration(suite.getElementsByTagName("test-method")));
168+
169+
Transformer transformer = TransformerFactory.newInstance().newTransformer();
170+
transformer.setOutputProperty(OutputKeys.INDENT, "yes");
171+
StreamResult streamResult = new StreamResult(writer);
172+
DOMSource domSource = new DOMSource(document);
173+
transformer.transform(domSource, streamResult);
174+
} catch (TransformerException e) {
175+
throw new CucumberException("Error transforming report.", e);
176+
}
177+
}
178+
179+
@Override
180+
public void close() {
181+
}
182+
183+
private int getElementsCountByAttribute(Node node, String attributeName, String attributeValue) {
184+
int count = 0;
185+
186+
for (int i = 0; i < node.getChildNodes().getLength(); i++) {
187+
count += getElementsCountByAttribute(node.getChildNodes().item(i), attributeName, attributeValue);
188+
}
189+
190+
NamedNodeMap attributes = node.getAttributes();
191+
if (attributes != null) {
192+
Node namedItem = attributes.getNamedItem(attributeName);
193+
if (namedItem != null && namedItem.getNodeValue().matches(attributeValue)) {
194+
count++;
195+
}
196+
}
197+
198+
return count;
199+
}
200+
201+
private String getTotalDuration(NodeList testCaseNodes) {
202+
long totalDuration = 0;
203+
for (int i = 0; i < testCaseNodes.getLength(); i++) {
204+
try {
205+
String duration = testCaseNodes.item(i).getAttributes().getNamedItem("duration-ms").getNodeValue();
206+
totalDuration += Long.parseLong(duration);
207+
} catch (NumberFormatException e) {
208+
throw new CucumberException(e);
209+
} catch (NullPointerException e) {
210+
throw new CucumberException(e);
211+
}
212+
}
213+
return String.valueOf(totalDuration);
214+
}
215+
216+
private static class TestMethod {
217+
218+
static Feature feature;
219+
static boolean treatSkippedAsFailure = false;
220+
static String previousScenarioOutlineName;
221+
static int exampleNumber;
222+
final List<Step> steps = new ArrayList<Step>();
223+
final List<Result> results = new ArrayList<Result>();
224+
final List<Result> hooks = new ArrayList<Result>();
225+
final Scenario scenario;
226+
227+
private TestMethod(Scenario scenario) {
228+
this.scenario = scenario;
229+
}
230+
231+
private void start(Element element) {
232+
element.setAttribute("name", calculateElementName(scenario));
233+
element.setAttribute("started-at", DATE_FORMAT.format(new Date()));
234+
}
235+
236+
private String calculateElementName(Scenario scenario) {
237+
String scenarioName = scenario.getName();
238+
if (scenario.getKeyword().equals("Scenario Outline") && scenarioName.equals(previousScenarioOutlineName)) {
239+
return scenarioName + "_" + ++exampleNumber;
240+
} else {
241+
previousScenarioOutlineName = scenario.getKeyword().equals("Scenario Outline") ? scenarioName : "";
242+
exampleNumber = 1;
243+
return scenarioName;
244+
}
245+
}
246+
247+
public void finish(Document doc, Element element) {
248+
element.setAttribute("duration-ms", calculateTotalDurationString());
249+
element.setAttribute("finished-at", DATE_FORMAT.format(new Date()));
250+
StringBuilder stringBuilder = new StringBuilder();
251+
addStepAndResultListing(stringBuilder);
252+
Result skipped = null;
253+
Result failed = null;
254+
for (Result result : results) {
255+
if ("failed".equals(result.getStatus())) {
256+
failed = result;
257+
}
258+
if ("undefined".equals(result.getStatus()) || "pending".equals(result.getStatus())) {
259+
skipped = result;
260+
}
261+
}
262+
for (Result result : hooks) {
263+
if (failed == null && "failed".equals(result.getStatus())) {
264+
failed = result;
265+
}
266+
}
267+
if (failed != null) {
268+
element.setAttribute("status", "FAIL");
269+
StringWriter stringWriter = new StringWriter();
270+
failed.getError().printStackTrace(new PrintWriter(stringWriter));
271+
Element exception = createException(doc, failed.getError().getClass().getName(), stringBuilder.toString(), stringWriter.toString());
272+
element.appendChild(exception);
273+
} else if (skipped != null) {
274+
if (treatSkippedAsFailure) {
275+
element.setAttribute("status", "FAIL");
276+
Element exception = createException(doc, "The scenario has pending or undefined step(s)", stringBuilder.toString(), "The scenario has pending or undefined step(s)");
277+
element.appendChild(exception);
278+
} else {
279+
element.setAttribute("status", "SKIP");
280+
}
281+
} else {
282+
element.setAttribute("status", "PASS");
283+
}
284+
}
285+
286+
private String calculateTotalDurationString() {
287+
long totalDurationNanos = 0;
288+
for (Result r : results) {
289+
totalDurationNanos += r.getDuration() == null ? 0 : r.getDuration();
290+
}
291+
for (Result r : hooks) {
292+
totalDurationNanos += r.getDuration() == null ? 0 : r.getDuration();
293+
}
294+
return String.valueOf(totalDurationNanos / 1000000);
295+
}
296+
297+
private void addStepAndResultListing(StringBuilder sb) {
298+
for (int i = 0; i < steps.size(); i++) {
299+
int length = sb.length();
300+
String resultStatus = "not executed";
301+
if (i < results.size()) {
302+
resultStatus = results.get(i).getStatus();
303+
}
304+
sb.append(steps.get(i).getKeyword());
305+
sb.append(steps.get(i).getName());
306+
do {
307+
sb.append(".");
308+
} while (sb.length() - length < 76);
309+
sb.append(resultStatus);
310+
sb.append("\n");
311+
}
312+
}
313+
314+
private Element createException(Document doc, String clazz, String message, String stacktrace) {
315+
Element exceptionElement = doc.createElement("exception");
316+
exceptionElement.setAttribute("class", clazz);
317+
318+
if (message != null) {
319+
Element messageElement = doc.createElement("message");
320+
messageElement.appendChild(doc.createCDATASection(message));
321+
exceptionElement.appendChild(messageElement);
322+
}
323+
324+
Element stacktraceElement = doc.createElement("full-stacktrace");
325+
stacktraceElement.appendChild(doc.createCDATASection(stacktrace));
326+
exceptionElement.appendChild(stacktraceElement);
327+
328+
return exceptionElement;
329+
}
330+
}
331+
}

core/src/main/resources/cucumber/api/cli/USAGE.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ Options:
55
-g, --glue PATH Where glue code (step definitions and hooks) is loaded from.
66
-p, --plugin PLUGIN[:PATH_OR_URL] Register a plugin.
77
Built-in PLUGIN types: junit, html, pretty, progress, json, usage,
8-
rerun. PLUGIN can also be a fully qualified class name, allowing
9-
registration of 3rd party plugins.
8+
rerun, testng. PLUGIN can also be a fully qualified class name,
9+
allowing registration of 3rd party plugins.
1010
-f, --format FORMAT[:PATH_OR_URL] Deprecated. Use --plugin instead.
1111
-t, --tags TAG_EXPRESSION Only run scenarios tagged with tags matching TAG_EXPRESSION.
1212
-n, --name REGEXP Only run scenarios whose names match REGEXP.

0 commit comments

Comments
 (0)