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