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