/*
* Copyright 2015-2017 the original author or authors.
*
* All rights reserved. This program and the accompanying materials are
* made available under the terms of the Eclipse Public License v1.0 which
* accompanies this distribution and is available at
*
* http://www.eclipse.org/legal/epl-v10.html
*/
package org.junit.platform.console.tasks;
import static java.text.MessageFormat.format;
import static java.time.format.DateTimeFormatter.ISO_LOCAL_DATE_TIME;
import static java.util.stream.Collectors.toList;
import static org.junit.platform.commons.util.ExceptionUtils.readStackTrace;
import static org.junit.platform.commons.util.StringUtils.isNotBlank;
import static org.junit.platform.console.tasks.XmlReportData.isFailure;
import static org.junit.platform.engine.TestExecutionResult.Status.FAILED;
import java.io.Writer;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.text.NumberFormat;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.Properties;
import java.util.TreeSet;
import javax.xml.stream.XMLOutputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamWriter;
import org.junit.platform.engine.TestExecutionResult;
import org.junit.platform.engine.reporting.ReportEntry;
import org.junit.platform.launcher.TestIdentifier;
/**
* @since 1.0
*/
class XmlReportWriter {
private final XmlReportData reportData;
XmlReportWriter(XmlReportData reportData) {
this.reportData = reportData;
}
void writeXmlReport(TestIdentifier testIdentifier, Writer out) throws XMLStreamException {
// @formatter:off
List<TestIdentifier> tests = reportData.getTestPlan().getDescendants(testIdentifier)
.stream()
.filter(TestIdentifier::isTest)
.collect(toList());
// @formatter:on
writeXmlReport(testIdentifier, tests, out);
}
private void writeXmlReport(TestIdentifier testIdentifier, List<TestIdentifier> tests, Writer out)
throws XMLStreamException {
XMLOutputFactory factory = XMLOutputFactory.newInstance();
XMLStreamWriter xmlWriter = factory.createXMLStreamWriter(out);
xmlWriter.writeStartDocument("UTF-8", "1.0");
newLine(xmlWriter);
writeTestsuite(testIdentifier, tests, xmlWriter);
xmlWriter.writeEndDocument();
xmlWriter.flush();
xmlWriter.close();
}
private void writeTestsuite(TestIdentifier testIdentifier, List<TestIdentifier> tests, XMLStreamWriter writer)
throws XMLStreamException {
// NumberFormat is not thread-safe. Thus, we instantiate it here and pass it to
// writeTestcase instead of using a constant
NumberFormat numberFormat = NumberFormat.getInstance(Locale.US);
writer.writeStartElement("testsuite");
writeSuiteAttributes(testIdentifier, tests, numberFormat, writer);
newLine(writer);
writeSystemProperties(writer);
for (TestIdentifier test : tests) {
writeTestcase(test, numberFormat, writer);
}
writeNonStandardAttributesToSystemOutElement(testIdentifier, writer);
writer.writeEndElement();
newLine(writer);
}
private void writeSuiteAttributes(TestIdentifier testIdentifier, List<TestIdentifier> tests,
NumberFormat numberFormat, XMLStreamWriter writer) throws XMLStreamException {
writer.writeAttribute("name", testIdentifier.getDisplayName());
writeTestCounts(tests, writer);
writer.writeAttribute("time", getTime(testIdentifier, numberFormat));
writer.writeAttribute("hostname", getHostname().orElse("<unknown host>"));
writer.writeAttribute("timestamp", ISO_LOCAL_DATE_TIME.format(getCurrentDateTime()));
}
private void writeTestCounts(List<TestIdentifier> tests, XMLStreamWriter writer) throws XMLStreamException {
TestCounts testCounts = TestCounts.from(reportData, tests);
writer.writeAttribute("tests", String.valueOf(testCounts.getTotal()));
writer.writeAttribute("skipped", String.valueOf(testCounts.getSkipped()));
writer.writeAttribute("failures", String.valueOf(testCounts.getFailures()));
writer.writeAttribute("errors", String.valueOf(testCounts.getErrors()));
}
private void writeSystemProperties(XMLStreamWriter writer) throws XMLStreamException {
writer.writeStartElement("properties");
newLine(writer);
Properties systemProperties = System.getProperties();
for (String propertyName : new TreeSet<>(systemProperties.stringPropertyNames())) {
writer.writeEmptyElement("property");
writer.writeAttribute("name", propertyName);
writer.writeAttribute("value", systemProperties.getProperty(propertyName));
newLine(writer);
}
writer.writeEndElement();
newLine(writer);
}
private void writeTestcase(TestIdentifier testIdentifier, NumberFormat numberFormat, XMLStreamWriter writer)
throws XMLStreamException {
writer.writeStartElement("testcase");
writer.writeAttribute("name", getName(testIdentifier));
writer.writeAttribute("classname", getClassName(testIdentifier));
writer.writeAttribute("time", getTime(testIdentifier, numberFormat));
newLine(writer);
writeSkippedOrErrorOrFailureElement(testIdentifier, writer);
writeReportEntriesToSystemOutElement(testIdentifier, writer);
writeNonStandardAttributesToSystemOutElement(testIdentifier, writer);
writer.writeEndElement();
newLine(writer);
}
private String getName(TestIdentifier testIdentifier) {
return testIdentifier.getLegacyReportingName();
}
private String getClassName(TestIdentifier testIdentifier) {
// @formatter:off
return reportData.getTestPlan().getParent(testIdentifier)
.map(TestIdentifier::getLegacyReportingName)
.orElse("<unrooted>");
// @formatter:on
}
private void writeSkippedOrErrorOrFailureElement(TestIdentifier testIdentifier, XMLStreamWriter writer)
throws XMLStreamException {
if (reportData.wasSkipped(testIdentifier)) {
writeSkippedElement(reportData.getSkipReason(testIdentifier), writer);
}
else {
Optional<TestExecutionResult> result = reportData.getResult(testIdentifier);
if (result.isPresent() && result.get().getStatus() == FAILED) {
writeErrorOrFailureElement(result.get().getThrowable(), writer);
}
}
}
private void writeSkippedElement(String reason, XMLStreamWriter writer) throws XMLStreamException {
if (isNotBlank(reason)) {
writer.writeStartElement("skipped");
writer.writeCData(reason);
writer.writeEndElement();
}
else {
writer.writeEmptyElement("skipped");
}
newLine(writer);
}
private void writeErrorOrFailureElement(Optional<Throwable> throwable, XMLStreamWriter writer)
throws XMLStreamException {
if (throwable.isPresent()) {
writer.writeStartElement(isFailure(throwable) ? "failure" : "error");
writeFailureAttributesAndContent(throwable.get(), writer);
writer.writeEndElement();
}
else {
writer.writeEmptyElement("error");
}
newLine(writer);
}
private void writeFailureAttributesAndContent(Throwable throwable, XMLStreamWriter writer)
throws XMLStreamException {
if (throwable.getMessage() != null) {
writer.writeAttribute("message", throwable.getMessage());
}
writer.writeAttribute("type", throwable.getClass().getName());
writer.writeCData(readStackTrace(throwable));
}
private void writeReportEntriesToSystemOutElement(TestIdentifier testIdentifier, XMLStreamWriter writer)
throws XMLStreamException {
List<ReportEntry> entries = reportData.getReportEntries(testIdentifier);
if (!entries.isEmpty()) {
writer.writeStartElement("system-out");
newLine(writer);
for (int i = 0; i < entries.size(); i++) {
writer.writeCharacters(buildReportEntryDescription(entries.get(i), i + 1));
}
writer.writeEndElement();
newLine(writer);
}
}
private String buildReportEntryDescription(ReportEntry reportEntry, int entryNumber) {
StringBuilder builder = new StringBuilder(format("Report Entry #{0} (timestamp: {1})\n", entryNumber,
ISO_LOCAL_DATE_TIME.format(reportEntry.getTimestamp())));
reportEntry.getKeyValuePairs().entrySet().forEach(
entry -> builder.append(format("\t- {0}: {1}\n", entry.getKey(), entry.getValue())));
return builder.toString();
}
private String getTime(TestIdentifier testIdentifier, NumberFormat numberFormat) {
return numberFormat.format(reportData.getDurationInSeconds(testIdentifier));
}
private Optional<String> getHostname() {
try {
return Optional.ofNullable(InetAddress.getLocalHost().getHostName());
}
catch (UnknownHostException e) {
return Optional.empty();
}
}
private LocalDateTime getCurrentDateTime() {
return LocalDateTime.now(reportData.getClock()).withNano(0);
}
private void writeNonStandardAttributesToSystemOutElement(TestIdentifier testIdentifier, XMLStreamWriter writer)
throws XMLStreamException {
String cData = "\nunique-id: " + testIdentifier.getUniqueId() //
+ "\ndisplay-name: " + testIdentifier.getDisplayName() + "\n";
writer.writeStartElement("system-out");
writer.writeCData(cData);
writer.writeEndElement();
newLine(writer);
}
private void newLine(XMLStreamWriter xmlWriter) throws XMLStreamException {
xmlWriter.writeCharacters("\n");
}
private static class TestCounts {
static TestCounts from(XmlReportData reportData, List<TestIdentifier> tests) {
TestCounts counts = new TestCounts(tests.size());
for (TestIdentifier test : tests) {
if (reportData.wasSkipped(test)) {
counts.skipped++;
}
else {
Optional<TestExecutionResult> result = reportData.getResult(test);
if (result.isPresent() && result.get().getStatus() == FAILED) {
if (isFailure(result.get().getThrowable())) {
counts.failures++;
}
else {
counts.errors++;
}
}
}
}
return counts;
}
private final long total;
private long skipped;
private long failures;
private long errors;
public TestCounts(long total) {
this.total = total;
}
public long getTotal() {
return total;
}
public long getSkipped() {
return skipped;
}
public long getFailures() {
return failures;
}
public long getErrors() {
return errors;
}
}
}