1 /* 2 * Copyright (C) 2022 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.ddmlib.Log.LogLevel; 20 import com.android.ddmlib.testrunner.TestResult.TestStatus; 21 import com.android.tradefed.config.Option; 22 import com.android.tradefed.config.OptionClass; 23 import com.android.tradefed.log.LogUtil.CLog; 24 import com.android.tradefed.metrics.proto.MetricMeasurement.Metric; 25 26 import com.google.common.annotations.VisibleForTesting; 27 28 import org.w3c.dom.CDATASection; 29 import org.w3c.dom.Document; 30 import org.w3c.dom.Element; 31 import org.w3c.dom.Node; 32 33 import java.io.IOException; 34 import java.io.OutputStream; 35 import java.io.UncheckedIOException; 36 import java.nio.file.FileSystem; 37 import java.nio.file.FileSystems; 38 import java.nio.file.Files; 39 import java.nio.file.Path; 40 import java.text.DateFormat; 41 import java.text.SimpleDateFormat; 42 import java.util.Date; 43 import java.util.HashMap; 44 import java.util.Locale; 45 import java.util.Map; 46 import java.util.TimeZone; 47 import java.util.TreeMap; 48 49 import javax.xml.parsers.DocumentBuilder; 50 import javax.xml.parsers.DocumentBuilderFactory; 51 import javax.xml.parsers.ParserConfigurationException; 52 import javax.xml.transform.OutputKeys; 53 import javax.xml.transform.Transformer; 54 import javax.xml.transform.TransformerException; 55 import javax.xml.transform.TransformerFactory; 56 import javax.xml.transform.dom.DOMSource; 57 import javax.xml.transform.stream.StreamResult; 58 59 /** 60 * A custom Tradefed reporter for Bazel XML result reporting. 61 * 62 * <p>This custom result reporter generates a test.xml file. The file contains detailed test case 63 * results and is written to the location provided in the Bazel XML_OUTPUT_FILE environment 64 * variable. The file is required for reporting detailed test results to AnTS via Bazel's BES 65 * protocol. The XML schema is based on the JUnit test result schema. See 66 * https://windyroad.com.au/dl/Open%20Source/JUnit.xsd for more details. 67 */ 68 @OptionClass(alias = "bazel-xml-result-reporter") 69 public final class BazelXmlResultReporter implements ITestInvocationListener { 70 private final FileSystem mFileSystem; 71 private TestRunResult mTestRunResult = new TestRunResult(); 72 73 // This is not a File object in order to use an in-memory FileSystem in tests. 74 // Using Path would have been more appropriate but Tradefed does not support 75 // option fields of that type. 76 @Option(name = "file", mandatory = true, description = "Bazel XML file") 77 private String mXmlFile; 78 79 @VisibleForTesting BazelXmlResultReporter(FileSystem fs)80 BazelXmlResultReporter(FileSystem fs) { 81 this.mFileSystem = fs; 82 } 83 BazelXmlResultReporter()84 public BazelXmlResultReporter() { 85 this(FileSystems.getDefault()); 86 } 87 88 @Override testRunStarted(String name, int numTests)89 public void testRunStarted(String name, int numTests) { 90 testRunStarted(name, numTests, 0); 91 } 92 93 @Override testRunStarted(String name, int numTests, int attemptNumber)94 public void testRunStarted(String name, int numTests, int attemptNumber) { 95 testRunStarted(name, numTests, attemptNumber, System.currentTimeMillis()); 96 } 97 98 @Override testRunStarted(String name, int numTests, int attemptNumber, long startTime)99 public void testRunStarted(String name, int numTests, int attemptNumber, long startTime) { 100 mTestRunResult.testRunStarted(name, numTests, startTime); 101 } 102 103 @Override testRunEnded(long elapsedTime, HashMap<String, Metric> runMetrics)104 public void testRunEnded(long elapsedTime, HashMap<String, Metric> runMetrics) { 105 mTestRunResult.testRunEnded(elapsedTime, runMetrics); 106 } 107 108 @Override testRunFailed(String errorMessage)109 public void testRunFailed(String errorMessage) { 110 mTestRunResult.testRunFailed(errorMessage); 111 } 112 113 @Override testRunFailed(FailureDescription failure)114 public void testRunFailed(FailureDescription failure) { 115 mTestRunResult.testRunFailed(failure); 116 } 117 118 @Override testRunStopped(long elapsedTime)119 public void testRunStopped(long elapsedTime) { 120 mTestRunResult.testRunStopped(elapsedTime); 121 } 122 123 @Override testStarted(TestDescription test)124 public void testStarted(TestDescription test) { 125 testStarted(test, System.currentTimeMillis()); 126 } 127 128 @Override testStarted(TestDescription test, long startTime)129 public void testStarted(TestDescription test, long startTime) { 130 mTestRunResult.testStarted(test, startTime); 131 } 132 133 @Override testEnded(TestDescription test, HashMap<String, Metric> testMetrics)134 public void testEnded(TestDescription test, HashMap<String, Metric> testMetrics) { 135 testEnded(test, System.currentTimeMillis(), testMetrics); 136 } 137 138 @Override testEnded(TestDescription test, long endTime, HashMap<String, Metric> testMetrics)139 public void testEnded(TestDescription test, long endTime, HashMap<String, Metric> testMetrics) { 140 mTestRunResult.testEnded(test, endTime, testMetrics); 141 } 142 143 @Override testFailed(TestDescription test, String trace)144 public void testFailed(TestDescription test, String trace) { 145 mTestRunResult.testFailed(test, trace); 146 } 147 148 @Override testFailed(TestDescription test, FailureDescription failure)149 public void testFailed(TestDescription test, FailureDescription failure) { 150 mTestRunResult.testFailed(test, failure); 151 } 152 153 @Override testAssumptionFailure(TestDescription test, String trace)154 public void testAssumptionFailure(TestDescription test, String trace) { 155 mTestRunResult.testAssumptionFailure(test, trace); 156 } 157 158 @Override testAssumptionFailure(TestDescription test, FailureDescription failure)159 public void testAssumptionFailure(TestDescription test, FailureDescription failure) { 160 mTestRunResult.testAssumptionFailure(test, failure); 161 } 162 163 @Override testIgnored(TestDescription test)164 public void testIgnored(TestDescription test) { 165 mTestRunResult.testIgnored(test); 166 } 167 168 @Override invocationEnded(long elapsedTime)169 public void invocationEnded(long elapsedTime) { 170 writeXmlFile(); 171 } 172 writeXmlFile()173 private void writeXmlFile() { 174 try (OutputStream os = createOutputStream(); ) { 175 DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); 176 DocumentBuilder builder = factory.newDocumentBuilder(); 177 Document doc = builder.newDocument(); 178 doc.setXmlStandalone(true); 179 // Pretty print XML file with indentation. 180 TransformerFactory transformerFactory = TransformerFactory.newInstance(); 181 Transformer transformer = transformerFactory.newTransformer(); 182 transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8"); 183 transformer.setOutputProperty(OutputKeys.INDENT, "yes"); 184 transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2"); 185 186 writeTestResult(doc, mTestRunResult); 187 188 DOMSource source = new DOMSource(doc); 189 StreamResult result = new StreamResult(os); 190 transformer.transform(source, result); 191 } catch (IOException e) { 192 throw new UncheckedIOException("Failed to write test.xml file", e); 193 } catch (TransformerException | ParserConfigurationException e) { 194 throw new RuntimeException("Failed to write test.xml file", e); 195 } 196 197 CLog.logAndDisplay(LogLevel.INFO, "Test XML file generated at %s.", mXmlFile); 198 } 199 createOutputStream()200 private OutputStream createOutputStream() throws IOException { 201 Path path = mFileSystem.getPath(mXmlFile); 202 Files.createDirectories(path.getParent()); 203 return Files.newOutputStream(path); 204 } 205 writeTestResult(Document doc, TestRunResult testRunResult)206 private void writeTestResult(Document doc, TestRunResult testRunResult) { 207 // There should be only one top-level testsuites element. 208 Element testSuites = writeTestSuites(doc, testRunResult); 209 doc.appendChild(testSuites); 210 211 Element testSuite = writeTestSuite(doc, testRunResult); 212 testSuites.appendChild(testSuite); 213 // We use a TreeMap to iterate over entries for deterministic output. 214 Map<TestDescription, TestResult> testResults = 215 new TreeMap<TestDescription, TestResult>(testRunResult.getTestResults()); 216 217 for (Map.Entry<TestDescription, TestResult> testEntry : testResults.entrySet()) { 218 if (testEntry.getValue().getStatus().equals(TestStatus.IGNORED)) { 219 continue; 220 } 221 testSuite.appendChild(writeTestCase(doc, testEntry.getKey(), testEntry.getValue())); 222 } 223 } 224 writeTestSuites(Document doc, TestRunResult testRunResult)225 private Element writeTestSuites(Document doc, TestRunResult testRunResult) { 226 Element testSuites = doc.createElementNS(null, "testsuites"); 227 228 writeStringAttribute(testSuites, "name", testRunResult.getName()); 229 writeTimestampAttribute(testSuites, "timestamp", testRunResult.getStartTime()); 230 231 return testSuites; 232 } 233 writeTestSuite(Document doc, TestRunResult testRunResult)234 private Element writeTestSuite(Document doc, TestRunResult testRunResult) { 235 Element testSuite = doc.createElementNS(null, "testsuite"); 236 237 writeStringAttribute(testSuite, "name", testRunResult.getName()); 238 writeTimestampAttribute(testSuite, "timestamp", testRunResult.getStartTime()); 239 240 writeIntAttribute(testSuite, "tests", testRunResult.getNumTests()); 241 writeIntAttribute( 242 testSuite, "failures", testRunResult.getNumTestsInState(TestStatus.FAILURE)); 243 // The tests were not run to completion because the tests decided that they should 244 // not be run(example: due to a failed assumption in a JUnit4-style tests). Some per-test 245 // setup or tear down may or may not have occurred for tests with this result. 246 writeIntAttribute( 247 testSuite, 248 "skipped", 249 testRunResult.getNumTestsInState(TestStatus.ASSUMPTION_FAILURE)); 250 // The tests were disabled with DISABLED_ (gUnit) or @Ignore (JUnit). 251 writeIntAttribute( 252 testSuite, "disabled", testRunResult.getNumTestsInState(TestStatus.IGNORED)); 253 254 writeDurationAttribute(testSuite, "time", testRunResult.getElapsedTime()); 255 256 return testSuite; 257 } 258 writeTestCase(Document doc, TestDescription description, TestResult result)259 private Element writeTestCase(Document doc, TestDescription description, TestResult result) { 260 TestStatus status = result.getStatus(); 261 Element testCase = doc.createElement("testcase"); 262 263 writeStringAttribute(testCase, "name", description.getTestName()); 264 writeStringAttribute(testCase, "classname", description.getClassName()); 265 writeDurationAttribute(testCase, "time", result.getEndTime() - result.getStartTime()); 266 267 writeStringAttribute(testCase, "status", "run"); 268 writeStringAttribute(testCase, "result", status.toString().toLowerCase()); 269 270 if (status.equals(TestStatus.FAILURE)) { 271 testCase.appendChild(writeStackTraceTag(doc, "failure", result.getStackTrace())); 272 } else if (status.equals(TestStatus.ASSUMPTION_FAILURE)) { 273 testCase.appendChild(writeStackTraceTag(doc, "skipped", result.getStackTrace())); 274 } 275 276 return testCase; 277 } 278 writeStackTraceTag(Document doc, String tag, String stackTrace)279 private static Node writeStackTraceTag(Document doc, String tag, String stackTrace) { 280 Element node = doc.createElement(tag); 281 CDATASection cdata = doc.createCDATASection(stackTrace); 282 node.appendChild(cdata); 283 return node; 284 } 285 writeStringAttribute( Element element, String attributeName, String attributeValue)286 private static void writeStringAttribute( 287 Element element, String attributeName, String attributeValue) { 288 element.setAttribute(attributeName, attributeValue); 289 } 290 writeIntAttribute( Element element, String attributeName, int attributeValue)291 private static void writeIntAttribute( 292 Element element, String attributeName, int attributeValue) { 293 element.setAttribute(attributeName, String.valueOf(attributeValue)); 294 } 295 writeTimestampAttribute( Element element, String attributeName, long timestampInMillis)296 private static void writeTimestampAttribute( 297 Element element, String attributeName, long timestampInMillis) { 298 element.setAttribute(attributeName, formatTimestamp(timestampInMillis)); 299 } 300 writeDurationAttribute(Element element, String attributeName, long millis)301 private static void writeDurationAttribute(Element element, String attributeName, long millis) { 302 element.setAttribute(attributeName, formatRunTime(millis)); 303 } 304 formatRunTime(Long runTimeInMillis)305 private static String formatRunTime(Long runTimeInMillis) { 306 return String.valueOf(runTimeInMillis / 1000.0D); 307 } 308 309 // Return an ISO 8601 combined date and time string for a specified timestamp. formatTimestamp(Long timestampInMillis)310 private static String formatTimestamp(Long timestampInMillis) { 311 DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US); 312 dateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); 313 return dateFormat.format(new Date(timestampInMillis)); 314 } 315 } 316