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