package org.keycloak.testsuite.util.junit; import org.apache.commons.configuration.PropertiesConfiguration; import org.jboss.logging.Logger; import org.junit.Ignore; import org.junit.runner.Description; import org.junit.runner.Result; import org.junit.runner.notification.Failure; import org.junit.runner.notification.RunListener; import org.w3c.dom.Document; import org.w3c.dom.Element; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import javax.xml.transform.OutputKeys; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerConfigurationException; import javax.xml.transform.TransformerException; import javax.xml.transform.TransformerFactory; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; import java.io.BufferedWriter; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStreamWriter; import java.io.Writer; import java.nio.charset.Charset; import java.util.HashMap; import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; /** * Aggregates jUnit test results into a single report - XML file. */ public class AggregateResultsReporter extends RunListener { private static final Logger LOGGER = Logger.getLogger(AggregateResultsReporter.class); private final Document xml; private final File reportFile; private final boolean working; private final AtomicInteger tests = new AtomicInteger(0); private final AtomicInteger errors = new AtomicInteger(0); private final AtomicInteger failures = new AtomicInteger(0); private final AtomicInteger ignored = new AtomicInteger(0); private final AtomicLong suiteStartTime = new AtomicLong(0L); private final AtomicReference<Element> testsuite = new AtomicReference<Element>(); private final Map<String, Long> testTimes = new HashMap<String, Long>(); public AggregateResultsReporter() { boolean working = true; Document xml = null; try { xml = createEmptyDocument(); } catch (ParserConfigurationException ex) { LOGGER.error("Failed to create XML DOM - reporting will not be done", ex); working = false; } File reportFile = null; try { reportFile = createReportFile(); } catch (Exception ex) { LOGGER.error("Failed to create log file - reporting will not be done", ex); working = false; } this.working = working; this.xml = xml; this.reportFile = reportFile; } private Document createEmptyDocument() throws ParserConfigurationException { DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); return builder.newDocument(); } private File createReportFile() throws Exception { String logDirPath = null; try { PropertiesConfiguration config = new PropertiesConfiguration(System.getProperty("testsuite.constants")); config.setThrowExceptionOnMissing(true); logDirPath = config.getString("log-dir"); } catch (Exception e) { logDirPath = System.getProperty("project.build.directory"); if (logDirPath == null) { throw new RuntimeException("Could not determine the path to the log directory."); } logDirPath += File.separator + "surefire-reports"; } final File logDir = new File(logDirPath); logDir.mkdirs(); final File reportFile = new File(logDir, "junit-single-report.xml").getAbsoluteFile(); reportFile.createNewFile(); return reportFile; } @Override public void testRunStarted(Description description) throws Exception { if (working) { suiteStartTime.set(System.currentTimeMillis()); Element testsuite = xml.createElement("testsuite"); if (description.getChildren().size() == 1) { testsuite.setAttribute("name", safeString(description.getChildren().get(0).getDisplayName())); } xml.appendChild(testsuite); this.testsuite.set(testsuite); writeXml(); } } @Override public void testStarted(Description description) throws Exception { if (working) { testTimes.put(description.getDisplayName(), System.currentTimeMillis()); } } @Override public void testFinished(Description description) throws Exception { if (working) { if (testTimes.containsKey(description.getDisplayName())) { testsuite.get().appendChild(createTestCase(description)); writeXml(); } } } @Override public void testAssumptionFailure(Failure failure) { if (working) { ignored.incrementAndGet(); Element testcase = createTestCase(failure.getDescription()); Element skipped = xml.createElement("skipped"); skipped.setAttribute("message", safeString(failure.getMessage())); testcase.appendChild(skipped); testsuite.get().appendChild(testcase); writeXml(); } } @Override public void testFailure(Failure failure) throws Exception { if (working) { if (failure.getDescription().getMethodName() == null) { // before class failed for (Description child : failure.getDescription().getChildren()) { // mark all methods failed testFailure(new Failure(child, failure.getException())); } } else { // normal failure Element testcase = createTestCase(failure.getDescription()); Element element; if (failure.getException() instanceof AssertionError) { failures.incrementAndGet(); element = xml.createElement("failure"); } else { errors.incrementAndGet(); element = xml.createElement("error"); } testcase.appendChild(element); element.setAttribute("type", safeString(failure.getException().getClass().getName())); element.setAttribute("message", safeString(failure.getMessage())); element.appendChild(xml.createCDATASection(safeString(failure.getTrace()))); testsuite.get().appendChild(testcase); writeXml(); } } } @Override public void testIgnored(Description description) throws Exception { if (working) { ignored.incrementAndGet(); Element testcase = createTestCase(description); Element skipped = xml.createElement("skipped"); skipped.setAttribute("message", safeString(description.getAnnotation(Ignore.class).value())); testcase.appendChild(skipped); testsuite.get().appendChild(testcase); writeXml(); } } @Override public void testRunFinished(Result result) throws Exception { if (working) { writeXml(); } } private void writeXml() { Element testsuite = this.testsuite.get(); testsuite.setAttribute("tests", Integer.toString(tests.get())); testsuite.setAttribute("errors", Integer.toString(errors.get())); testsuite.setAttribute("skipped", Integer.toString(ignored.get())); testsuite.setAttribute("failures", Integer.toString(failures.get())); testsuite.setAttribute("time", computeTestTime(suiteStartTime.get())); try { Writer writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(reportFile, false), Charset.forName("UTF-8"))); try { Transformer t = TransformerFactory.newInstance().newTransformer(); t.setOutputProperty(OutputKeys.INDENT, "yes"); t.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4"); t.transform(new DOMSource(xml), new StreamResult(writer)); } catch (TransformerConfigurationException ex) { LOGGER.error("Misconfigured transformer", ex); } catch (TransformerException ex) { LOGGER.error("Unable to save XML file", ex); } finally { writer.close(); } } catch (IOException ex) { LOGGER.warn("Unable to open report file", ex); } } private String computeTestTime(Long startTime) { if (startTime == null) { return "0"; } else { long amount = System.currentTimeMillis() - startTime; return String.format("%.3f", amount / 1000F); } } private Element createTestCase(Description description) { tests.incrementAndGet(); Element testcase = xml.createElement("testcase"); testcase.setAttribute("name", safeString(description.getMethodName())); testcase.setAttribute("classname", safeString(description.getClassName())); testcase.setAttribute("time", computeTestTime(testTimes.remove(description.getDisplayName()))); return testcase; } private String safeString(String input) { if (input == null) { return "null"; } return input // first remove color coding (all of it) .replaceAll("\u001b\\[\\d+m", "") // then remove control characters that are not whitespaces .replaceAll("[\\p{Cntrl}&&[^\\p{Space}]]", ""); } }