1 /* 2 * Copyright (C) 2017 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.junitxml; 18 19 import org.junit.Ignore; 20 import org.junit.runner.Description; 21 import org.junit.runner.notification.Failure; 22 import org.junit.runner.notification.RunListener; 23 import org.w3c.dom.Document; 24 import org.w3c.dom.Element; 25 import org.w3c.dom.Text; 26 27 import java.io.BufferedWriter; 28 import java.io.IOException; 29 import java.io.OutputStream; 30 import java.io.OutputStreamWriter; 31 import java.io.Writer; 32 import java.net.InetAddress; 33 import java.net.UnknownHostException; 34 import java.text.SimpleDateFormat; 35 import java.util.Date; 36 import java.util.HashSet; 37 import java.util.Hashtable; 38 import java.util.Properties; 39 import java.util.Set; 40 41 import javax.xml.parsers.DocumentBuilder; 42 import javax.xml.parsers.DocumentBuilderFactory; 43 import javax.xml.transform.OutputKeys; 44 import javax.xml.transform.Source; 45 import javax.xml.transform.Transformer; 46 import javax.xml.transform.TransformerException; 47 import javax.xml.transform.TransformerFactory; 48 import javax.xml.transform.dom.DOMSource; 49 import javax.xml.transform.stream.StreamResult; 50 51 /** 52 * {@link RunListener} to write JUnit4 test results to XML in a format adapted from the schema used 53 * by Ant in {@code org.apache.tools.ant.taskdefs.optional.junit.XMLJUnitResultFormatter}. 54 */ 55 public class XmlRunListener extends RunListener implements XmlConstants { 56 57 private static final double ONE_SECOND = 1000.0; 58 59 private static final String TESTCASE_NAME_UNKNOWN = "unknown"; 60 61 private Document mDocument; 62 63 private Element mRootElement; 64 65 private final Hashtable<Description, Element> mTestElements = new Hashtable<>(); 66 67 private final Set<Description> mFailedTests = new HashSet<>(); 68 69 private final Set<Description> mErrorTests = new HashSet<>(); 70 71 private final Set<Description> mSkippedTests = new HashSet<>(); 72 73 private final Set<Description> mIgnoredTests = new HashSet<>(); 74 75 private final Hashtable<Description, Long> mTestStarts = new Hashtable<>(); 76 77 private OutputStream mOutputStream; 78 79 private long mStartTime; 80 getDocumentBuilder()81 private static DocumentBuilder getDocumentBuilder() { 82 try { 83 return DocumentBuilderFactory.newInstance().newDocumentBuilder(); 84 } catch (final Exception exc) { 85 throw new ExceptionInInitializerError(exc); 86 } 87 } 88 XmlRunListener(OutputStream out, String suiteName)89 public XmlRunListener(OutputStream out, String suiteName) { 90 mDocument = getDocumentBuilder().newDocument(); 91 mRootElement = mDocument.createElement(ELEMENT_TESTSUITE); 92 mOutputStream = out; 93 startTestSuite(suiteName); 94 } 95 startTestSuite(String suiteName)96 private void startTestSuite(String suiteName) { 97 mRootElement.setAttribute(ATTR_TESTSUITE_NAME, suiteName); 98 99 SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"); 100 String timestamp = simpleDateFormat.format(new Date()); 101 mRootElement.setAttribute(ATTR_TESTSUITE_TIME, timestamp); 102 103 mRootElement.setAttribute(ATTR_TESTSUITE_HOSTNAME, getHostname()); 104 105 Element propsElement = mDocument.createElement(ELEMENT_PROPERTIES); 106 mRootElement.appendChild(propsElement); 107 mStartTime = System.currentTimeMillis(); 108 final Properties props = System.getProperties(); 109 if (props != null) { 110 for (Object name : props.keySet()) { 111 Element propElement = mDocument.createElement(ELEMENT_PROPERTY); 112 propElement.setAttribute(ATTR_PROPERTY_NAME, (String) name); 113 propElement.setAttribute(ATTR_PROPERTY_VALUE, props.getProperty((String) name)); 114 propsElement.appendChild(propElement); 115 } 116 } 117 } 118 getHostname()119 private String getHostname() { 120 String hostname = "localhost"; 121 try { 122 InetAddress localHost = InetAddress.getLocalHost(); 123 if (localHost != null) { 124 hostname = localHost.getHostName(); 125 } 126 } catch (UnknownHostException e) { 127 // fall back to default 'localhost' 128 } 129 return hostname; 130 } 131 endTestSuite()132 public void endTestSuite() throws IOException { 133 mRootElement.setAttribute(ATTR_TESTSUITE_TESTS, "" + mTestStarts.size()); 134 mRootElement.setAttribute(ATTR_TESTSUITE_FAILURES, "" + mFailedTests.size()); 135 mRootElement.setAttribute(ATTR_TESTSUITE_ERRORS, "" + mErrorTests.size()); 136 mRootElement.setAttribute(ATTR_TESTSUITE_SKIPPED, "" + mSkippedTests.size()); 137 138 mRootElement.setAttribute( 139 ATTR_TESTSUITE_TIME, 140 "" + ((System.currentTimeMillis() - mStartTime) / ONE_SECOND)); 141 if (mOutputStream != null) { 142 Writer writer = null; 143 try { 144 writer = new BufferedWriter(new OutputStreamWriter(mOutputStream, "UTF8")); 145 146 Transformer transformer; 147 148 transformer = TransformerFactory.newInstance().newTransformer(); 149 javax.xml.transform.Result output = new StreamResult(writer); 150 Source input = new DOMSource(mRootElement); 151 transformer.setOutputProperty(OutputKeys.INDENT, "yes"); 152 transformer.transform(input, output); 153 154 } catch (final IOException | TransformerException exc) { 155 throw new IOException("Unable to write log file", exc); 156 } finally { 157 if (writer != null) { 158 try { 159 writer.flush(); 160 } catch (final IOException ex) { 161 // ignore 162 } 163 if (mOutputStream != System.out && mOutputStream != System.err) { 164 writer.close(); 165 } 166 } 167 } 168 } 169 } 170 171 @SuppressWarnings("ThrowableResultOfMethodCallIgnored") 172 @Override testFailure(Failure failure)173 public void testFailure(Failure failure) throws Exception { 174 Description description = failure.getDescription(); 175 testFinished(description); 176 177 if (failure.getException() instanceof AssertionError) { 178 formatError(ELEMENT_FAILURE, failure); 179 mFailedTests.add(description); 180 } else { 181 formatError(ELEMENT_ERROR, failure); 182 mErrorTests.add(description); 183 } 184 } 185 formatError(String type, Failure failure)186 private void formatError(String type, Failure failure) throws Exception { 187 final Element failureOrError = mDocument.createElement(type); 188 Element currentTest; 189 if (failure.getDescription() != null) { 190 currentTest = mTestElements.get(failure.getDescription()); 191 } else { 192 currentTest = mRootElement; 193 } 194 195 currentTest.appendChild(failureOrError); 196 197 final String message = failure.getMessage(); 198 if (message != null && message.length() > 0) { 199 failureOrError.setAttribute(ATTR_FAILURE_MESSAGE, message); 200 } 201 failureOrError.setAttribute( 202 ATTR_FAILURE_TYPE, 203 failure.getDescription().getClassName()); 204 205 final String stackTrace = failure.getTrace(); 206 final Text trace = mDocument.createTextNode(stackTrace); 207 failureOrError.appendChild(trace); 208 } 209 210 @Override testFinished(Description description)211 public void testFinished(Description description) throws Exception { 212 if (!mTestStarts.containsKey(description)) { 213 testStarted(description); 214 } 215 216 Element currentTest; 217 if (!mFailedTests.contains(description) && !mErrorTests.contains(description) 218 && !mSkippedTests.contains(description) && !mIgnoredTests 219 .contains(description)) { 220 currentTest = mDocument.createElement(ELEMENT_TESTCASE); 221 final String methodName = description.getMethodName(); 222 currentTest.setAttribute( 223 ATTR_TESTCASE_NAME, 224 methodName == null ? TESTCASE_NAME_UNKNOWN : methodName); 225 // a TestSuite can contain Tests from multiple classes, 226 // even tests with the same name - disambiguate them. 227 currentTest.setAttribute(ATTR_TESTCASE_CLASSNAME, description.getClassName()); 228 mRootElement.appendChild(currentTest); 229 mTestElements.put(description, currentTest); 230 231 } else { 232 currentTest = mTestElements.get(description); 233 } 234 235 final long l = mTestStarts.get(description); 236 currentTest.setAttribute( 237 ATTR_TESTCASE_TIME, "" + ((System.currentTimeMillis() - l) / ONE_SECOND)); 238 } 239 240 @Override testStarted(Description description)241 public void testStarted(Description description) throws Exception { 242 mTestStarts.put(description, System.currentTimeMillis()); 243 } 244 245 @Override testIgnored(Description description)246 public void testIgnored(Description description) throws Exception { 247 Ignore ignoreAnnotation = description.getAnnotation(Ignore.class); 248 formatSkip(description, ignoreAnnotation != null ? ignoreAnnotation.value() : null); 249 mIgnoredTests.add(description); 250 } 251 252 @Override testAssumptionFailure(Failure failure)253 public void testAssumptionFailure(Failure failure) { 254 try { 255 formatSkip(failure.getDescription(), failure.getMessage()); 256 } catch (Exception e) { 257 e.printStackTrace(); 258 } 259 mSkippedTests.add(failure.getDescription()); 260 } 261 formatSkip(Description description, String message)262 private void formatSkip(Description description, String message) throws Exception { 263 testFinished(description); 264 265 final Element skippedElement = mDocument.createElement(ELEMENT_SKIPPED); 266 if (message != null) { 267 skippedElement.setAttribute(ATTR_SKIPPED_MESSAGE, message); 268 } 269 270 Element currentTest = mTestElements.get(description); 271 currentTest.appendChild(skippedElement); 272 } 273 } 274