1 /*
2  * Copyright (C) 2009 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.tradefed.result;
18 
19 import com.android.tradefed.config.OptionClass;
20 import com.android.tradefed.log.LogUtil.CLog;
21 import com.android.tradefed.util.StreamUtil;
22 
23 import org.kxml2.io.KXmlSerializer;
24 
25 import java.io.ByteArrayInputStream;
26 import java.io.ByteArrayOutputStream;
27 import java.io.IOException;
28 import java.io.InputStream;
29 import java.text.SimpleDateFormat;
30 import java.util.Date;
31 import java.util.Map;
32 import java.util.TimeZone;
33 
34 /**
35  * Writes JUnit results to an XML files in a format consistent with
36  * Ant's XMLJUnitResultFormatter.
37  * <p/>
38  * Unlike Ant's formatter, this class does not report the execution time of
39  * tests.
40  * <p/>
41  * Collects all test info in memory, then dumps to file when invocation is complete.
42  * <p/>
43  * Ported from dalvik runner XmlReportPrinter.
44  * <p/>
45  * Result files will be stored in path constructed via [--output-file-path]/[build_id]
46  */
47 @OptionClass(alias = "xml")
48 public class XmlResultReporter extends CollectingTestListener implements ILogSaverListener {
49 
50     private static final String TEST_RESULT_FILE_PREFIX = "test_result_";
51 
52     private static final String TESTSUITE = "testsuite";
53     private static final String TESTCASE = "testcase";
54     private static final String ERROR = "error";
55     private static final String FAILURE = "failure";
56     private static final String IGNORED = "ignored";
57     private static final String ASSUMPTION_FAILURE = "assumption_failure";
58     private static final String ATTR_NAME = "name";
59     private static final String ATTR_TIME = "time";
60     private static final String ATTR_ERRORS = "errors";
61     private static final String ATTR_FAILURES = "failures";
62     private static final String ATTR_TESTS = "tests";
63     //private static final String ATTR_TYPE = "type";
64     //private static final String ATTR_MESSAGE = "message";
65     private static final String PROPERTIES = "properties";
66     private static final String ATTR_CLASSNAME = "classname";
67     private static final String TIMESTAMP = "timestamp";
68     private static final String HOSTNAME = "hostname";
69 
70     /** the XML namespace */
71     private static final String NS = null;
72 
73     private ILogSaver mLogSaver;
74 
75     /**
76      * {@inheritDoc}
77      */
78     @Override
invocationEnded(long elapsedTime)79     public void invocationEnded(long elapsedTime) {
80         super.invocationEnded(elapsedTime);
81         generateSummary(elapsedTime);
82     }
83 
84     @Override
testFailed(TestDescription test, String trace)85     public void testFailed(TestDescription test, String trace) {
86         super.testFailed(test, trace);
87         CLog.d("%s : %s", test, trace);
88     }
89 
90     /**
91      * Creates a report file and populates it with the report data from the completed tests.
92      */
generateSummary(long elapsedTime)93     private void generateSummary(long elapsedTime) {
94         String timestamp = getTimestamp();
95 
96         ByteArrayOutputStream outputStream = null;
97         InputStream inputStream = null;
98 
99         try {
100             outputStream = createOutputStream();
101             KXmlSerializer serializer = new KXmlSerializer();
102             serializer.setOutput(outputStream, "UTF-8");
103             serializer.startDocument("UTF-8", null);
104             serializer.setFeature(
105                     "http://xmlpull.org/v1/doc/features.html#indent-output", true);
106             // TODO: insert build info
107             printTestResults(serializer, timestamp, elapsedTime);
108             serializer.endDocument();
109 
110             inputStream = new ByteArrayInputStream(outputStream.toByteArray());
111             LogFile log = mLogSaver.saveLogData(TEST_RESULT_FILE_PREFIX, LogDataType.XML,
112                     inputStream);
113 
114             CLog.i(
115                     "XML test result file generated at %s. Total tests %d, " + "Failed %d",
116                     log.getPath(), getNumTotalTests(), getNumAllFailedTests());
117         } catch (IOException e) {
118             CLog.e("Failed to generate report data");
119             // TODO: consider throwing exception
120         } finally {
121             StreamUtil.close(outputStream);
122             StreamUtil.close(inputStream);
123         }
124     }
125 
126     /**
127      * Return the current timestamp as a {@link String}.
128      */
getTimestamp()129     String getTimestamp() {
130         SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
131         TimeZone gmt = TimeZone.getTimeZone("UTC");
132         dateFormat.setTimeZone(gmt);
133         dateFormat.setLenient(true);
134         String timestamp = dateFormat.format(new Date());
135         return timestamp;
136     }
137 
138     /**
139      * Creates the output stream to use for test results. Exposed for mocking.
140      */
createOutputStream()141     ByteArrayOutputStream createOutputStream() {
142         return new ByteArrayOutputStream();
143     }
144 
printTestResults(KXmlSerializer serializer, String timestamp, long elapsedTime)145     void printTestResults(KXmlSerializer serializer, String timestamp, long elapsedTime)
146             throws IOException {
147         serializer.startTag(NS, TESTSUITE);
148         serializer.attribute(NS, ATTR_NAME, getInvocationContext().getTestTag());
149         serializer.attribute(NS, ATTR_TESTS, Integer.toString(getNumTotalTests()));
150         serializer.attribute(
151                 NS, ATTR_FAILURES, Integer.toString(getNumTestsInState(TestStatus.FAILURE)));
152         serializer.attribute(NS, ATTR_ERRORS, "0");
153         serializer.attribute(NS, ATTR_TIME, Long.toString(elapsedTime));
154         serializer.attribute(NS, TIMESTAMP, timestamp);
155         serializer.attribute(NS, HOSTNAME, "localhost");
156         serializer.startTag(NS, PROPERTIES);
157         serializer.endTag(NS, PROPERTIES);
158 
159         for (TestRunResult runResult : getMergedTestRunResults()) {
160             // TODO: add test run summaries as TESTSUITES ?
161             Map<TestDescription, TestResult> testResults = runResult.getTestResults();
162             for (Map.Entry<TestDescription, TestResult> testEntry : testResults.entrySet()) {
163                 print(serializer, testEntry.getKey(), testEntry.getValue());
164             }
165         }
166 
167         serializer.endTag(NS, TESTSUITE);
168     }
169 
print(KXmlSerializer serializer, TestDescription testId, TestResult testResult)170     void print(KXmlSerializer serializer, TestDescription testId, TestResult testResult)
171             throws IOException {
172 
173         serializer.startTag(NS, TESTCASE);
174         serializer.attribute(NS, ATTR_NAME, testId.getTestName());
175         serializer.attribute(NS, ATTR_CLASSNAME, testId.getClassName());
176         serializer.attribute(NS, ATTR_TIME, "0");
177 
178         // TODO(b/322204420): Remove status downgrade and support SKIPPED in XML
179         com.android.ddmlib.testrunner.TestResult.TestStatus ddmlibStatus = testResult.getStatus();
180         TestStatus tfStatus = TestStatus.convertFromDdmlibType(ddmlibStatus);
181 
182         if (TestStatus.IGNORED.equals(tfStatus)) {
183             String result = IGNORED;
184             serializer.startTag(NS, result);
185             serializer.endTag(NS, result);
186         } else if (!TestStatus.PASSED.equals(tfStatus)) {
187             String result = ERROR;
188             if (TestStatus.FAILURE.equals(tfStatus)) {
189                 result = FAILURE;
190             } else if (TestStatus.ASSUMPTION_FAILURE.equals(tfStatus)) {
191                 result = ASSUMPTION_FAILURE;
192             }
193             serializer.startTag(NS, result);
194             // TODO: get message of stack trace ?
195 //            String msg = testResult.getStackTrace();
196 //            if (msg != null && msg.length() > 0) {
197 //                serializer.attribute(ns, ATTR_MESSAGE, msg);
198 //            }
199            // TODO: get class name of stackTrace exception
200             //serializer.attribute(ns, ATTR_TYPE, testId.getClassName());
201             String stackText = sanitize(testResult.getStackTrace());
202             serializer.text(stackText);
203             serializer.endTag(NS, result);
204         }
205 
206         serializer.endTag(NS, TESTCASE);
207      }
208 
209     /**
210      * Returns the text in a format that is safe for use in an XML document.
211      */
sanitize(String text)212     private String sanitize(String text) {
213         return text == null ? "" : text.replace("\0", "<\\0>");
214     }
215 
216     /**
217      * {@inheritDoc}
218      */
219     @Override
testLog(String dataName, LogDataType dataType, InputStreamSource dataStream)220     public void testLog(String dataName, LogDataType dataType, InputStreamSource dataStream) {
221         // Ignore
222     }
223 
224     /**
225      * {@inheritDoc}
226      */
227     @Override
testLogSaved(String dataName, LogDataType dataType, InputStreamSource dataStream, LogFile logFile)228     public void testLogSaved(String dataName, LogDataType dataType, InputStreamSource dataStream,
229             LogFile logFile) {
230         CLog.i("Saved %s log to %s", dataName, logFile.getPath());
231     }
232 
233     /**
234      * {@inheritDoc}
235      */
236     @Override
setLogSaver(ILogSaver logSaver)237     public void setLogSaver(ILogSaver logSaver) {
238         mLogSaver = logSaver;
239     }
240 }
241