1 /*
2  * Copyright (C) 2018 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 package com.android.tradefed.result.suite;
17 
18 import com.android.annotations.VisibleForTesting;
19 import com.android.tradefed.build.IBuildInfo;
20 import com.android.tradefed.invoker.IInvocationContext;
21 import com.android.tradefed.invoker.InvocationContext;
22 import com.android.tradefed.log.LogUtil.CLog;
23 import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
24 import com.android.tradefed.result.FailureDescription;
25 import com.android.tradefed.result.LogDataType;
26 import com.android.tradefed.result.LogFile;
27 import com.android.tradefed.result.TestDescription;
28 import com.android.tradefed.result.TestResult;
29 import com.android.tradefed.result.TestRunResult;
30 import com.android.tradefed.result.TestStatus;
31 import com.android.tradefed.result.error.ErrorIdentifier;
32 import com.android.tradefed.testtype.Abi;
33 import com.android.tradefed.testtype.IAbi;
34 import com.android.tradefed.util.AbiUtils;
35 import com.android.tradefed.util.StreamUtil;
36 import com.android.tradefed.util.proto.TfMetricProtoUtil;
37 
38 import com.google.common.base.Strings;
39 import com.google.common.xml.XmlEscapers;
40 import com.google.gson.Gson;
41 
42 import org.xmlpull.v1.XmlPullParser;
43 import org.xmlpull.v1.XmlPullParserException;
44 import org.xmlpull.v1.XmlPullParserFactory;
45 import org.xmlpull.v1.XmlSerializer;
46 
47 import java.io.File;
48 import java.io.FileOutputStream;
49 import java.io.FileReader;
50 import java.io.IOException;
51 import java.io.OutputStream;
52 import java.net.InetAddress;
53 import java.net.UnknownHostException;
54 import java.text.SimpleDateFormat;
55 import java.util.ArrayList;
56 import java.util.Arrays;
57 import java.util.Collection;
58 import java.util.Collections;
59 import java.util.Comparator;
60 import java.util.Date;
61 import java.util.HashMap;
62 import java.util.LinkedHashMap;
63 import java.util.LinkedHashSet;
64 import java.util.List;
65 import java.util.Map;
66 import java.util.Map.Entry;
67 import java.util.Set;
68 
69 /**
70  * Utility class to save a suite run as an XML. TODO: Remove all the special Compatibility Test
71  * format work around to get the same format.
72  */
73 public class XmlSuiteResultFormatter implements IFormatterGenerator {
74 
75     // The maximum size of a stack trace saved in the report.
76     private static final int STACK_TRACE_MAX_SIZE = 1024 * 1024;
77 
78     private static final String ENCODING = "UTF-8";
79     private static final String TYPE = "org.kxml2.io.KXmlParser,org.kxml2.io.KXmlSerializer";
80     public static final String NS = null;
81 
82     public static final String TEST_RESULT_FILE_NAME = "test_result.xml";
83 
84     // XML constants
85     private static final String ABI_ATTR = "abi";
86     private static final String BUGREPORT_TAG = "BugReport";
87     private static final String BUILD_TAG = "Build";
88     private static final String CASE_TAG = "TestCase";
89     private static final String COMMAND_LINE_ARGS = "command_line_args";
90     private static final String DEVICES_ATTR = "devices";
91     private static final String DEVICE_KERNEL_INFO_ATTR = "device_kernel_info";
92     private static final String DONE_ATTR = "done";
93     private static final String END_DISPLAY_TIME_ATTR = "end_display";
94     private static final String END_TIME_ATTR = "end";
95     private static final String FAILED_ATTR = "failed";
96     private static final String FAILURE_TAG = "Failure";
97     private static final String HOST_NAME_ATTR = "host_name";
98     private static final String JAVA_VENDOR_ATTR = "java_vendor";
99     private static final String JAVA_VERSION_ATTR = "java_version";
100     private static final String LOGCAT_TAG = "Logcat";
101 
102     private static final String METRIC_TAG = "Metric";
103     private static final String METRIC_KEY = "key";
104 
105     private static final String MESSAGE_ATTR = "message";
106     private static final String MODULE_TAG = "Module";
107     private static final String MODULES_DONE_ATTR = "modules_done";
108     private static final String MODULES_TOTAL_ATTR = "modules_total";
109     private static final String MODULES_NOT_DONE_REASON = "Reason";
110     private static final String NAME_ATTR = "name";
111     private static final String OS_ARCH_ATTR = "os_arch";
112     private static final String OS_NAME_ATTR = "os_name";
113     private static final String OS_VERSION_ATTR = "os_version";
114     private static final String PASS_ATTR = "pass";
115 
116     private static final String RESULT_ATTR = "result";
117     private static final String RESULT_TAG = "Result";
118     private static final String RUN_HISTORY = "run_history";
119     private static final String RUN_HISTORY_TAG = "RunHistory";
120     private static final String RUN_TAG = "Run";
121     private static final String RUNTIME_ATTR = "runtime";
122     private static final String SCREENSHOT_TAG = "Screenshot";
123     private static final String SKIPPED_ATTR = "skipped";
124     private static final String STACK_TAG = "StackTrace";
125     private static final String ERROR_NAME_ATTR = "error_name";
126     private static final String ERROR_CODE_ATTR = "error_code";
127     private static final String START_DISPLAY_TIME_ATTR = "start_display";
128     private static final String START_TIME_ATTR = "start";
129 
130     private static final String SUMMARY_TAG = "Summary";
131     private static final String SYSTEM_IMG_INFO_ATTR = "system_img_info";
132     private static final String TEST_TAG = "Test";
133     private static final String TOTAL_TESTS_ATTR = "total_tests";
134     private static final String VENDOR_IMG_INFO_ATTR = "vendor_img_info";
135 
136     private static final String LOG_FILE_NAME_ATTR = "file_name";
137 
138     /** Helper object for JSON conversion. */
139     public static final class RunHistory {
140         public long startTime;
141         public long endTime;
142         public long passedTests;
143         public long failedTests;
144         public String commandLineArgs;
145         public String hostName;
146     }
147 
148     /**
149      * Allows to add some attributes to the <Result> tag via {@code serializer.attribute}.
150      *
151      * @param serializer The object that serializes an XML suite result.
152      */
addSuiteAttributes(XmlSerializer serializer)153     public void addSuiteAttributes(XmlSerializer serializer)
154             throws IllegalArgumentException, IllegalStateException, IOException {
155         // Default implementation does nothing
156     }
157 
158     /**
159      * Reverse operation from {@link #addSuiteAttributes(XmlSerializer)}.
160      *
161      * @param parser The parser where to read the attributes from.
162      * @param context The {@link IInvocationContext} where to put the attributes.
163      * @throws XmlPullParserException When XmlPullParser fails.
164      */
parseSuiteAttributes(XmlPullParser parser, IInvocationContext context)165     public void parseSuiteAttributes(XmlPullParser parser, IInvocationContext context)
166             throws XmlPullParserException {
167         // Default implementation does nothing
168     }
169 
170     /**
171      * Allows to add some attributes to the <Build> tag via {@code serializer.attribute}.
172      *
173      * @param serializer The object that serializes an XML suite result.
174      * @param holder An object that contains information to be written to the suite result.
175      */
addBuildInfoAttributes(XmlSerializer serializer, SuiteResultHolder holder)176     public void addBuildInfoAttributes(XmlSerializer serializer, SuiteResultHolder holder)
177             throws IllegalArgumentException, IllegalStateException, IOException {
178         // Default implementation does nothing
179     }
180 
181     /**
182      * Reverse operation from {@link #addBuildInfoAttributes(XmlSerializer, SuiteResultHolder)}.
183      *
184      * @param parser The parser where to read the attributes from.
185      * @param context The {@link IInvocationContext} where to put the attributes.
186      * @throws XmlPullParserException When XmlPullParser fails.
187      */
parseBuildInfoAttributes(XmlPullParser parser, IInvocationContext context)188     public void parseBuildInfoAttributes(XmlPullParser parser, IInvocationContext context)
189             throws XmlPullParserException {
190         // Default implementation does nothing
191     }
192 
193     /**
194      * Write the invocation results in an xml format.
195      *
196      * @param holder a {@link SuiteResultHolder} holding all the info required for the xml
197      * @param resultDir the result directory {@link File} where to put the results.
198      * @return a {@link File} pointing to the xml output file.
199      */
200     @Override
writeResults(SuiteResultHolder holder, File resultDir)201     public File writeResults(SuiteResultHolder holder, File resultDir) throws IOException {
202         File resultFile = new File(resultDir, TEST_RESULT_FILE_NAME);
203         OutputStream stream = new FileOutputStream(resultFile);
204         XmlSerializer serializer = null;
205         try {
206             serializer = XmlPullParserFactory.newInstance(TYPE, null).newSerializer();
207         } catch (XmlPullParserException e) {
208             StreamUtil.close(stream);
209             throw new IOException(e);
210         }
211         serializer.setOutput(stream, ENCODING);
212         serializer.startDocument(ENCODING, false);
213         serializer.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
214         serializer.processingInstruction(
215                 "xml-stylesheet type=\"text/xsl\" href=\"compatibility_result.xsl\"");
216         serializer.startTag(NS, RESULT_TAG);
217         serializer.attribute(NS, START_TIME_ATTR, String.valueOf(holder.startTime));
218         serializer.attribute(NS, END_TIME_ATTR, String.valueOf(holder.endTime));
219         serializer.attribute(NS, START_DISPLAY_TIME_ATTR, toReadableDateString(holder.startTime));
220         serializer.attribute(NS, END_DISPLAY_TIME_ATTR, toReadableDateString(holder.endTime));
221         serializer.attribute(
222                 NS,
223                 COMMAND_LINE_ARGS,
224                 Strings.nullToEmpty(
225                         holder.context.getAttributes().getUniqueMap().get(COMMAND_LINE_ARGS)));
226 
227         addSuiteAttributes(serializer);
228 
229         // Device Info
230         Map<Integer, List<String>> serialsShards = holder.context.getShardsSerials();
231         String deviceList = "";
232         if (serialsShards.isEmpty()) {
233             deviceList = String.join(",", holder.context.getSerials());
234         } else {
235             Set<String> subSet = new LinkedHashSet<>();
236             for (List<String> list : serialsShards.values()) {
237                 subSet.addAll(list);
238             }
239             deviceList = String.join(",", subSet);
240         }
241         serializer.attribute(NS, DEVICES_ATTR, deviceList);
242 
243         // Host Info
244         String hostName = "";
245         try {
246             hostName = InetAddress.getLocalHost().getHostName();
247         } catch (UnknownHostException ignored) {
248         }
249         serializer.attribute(NS, HOST_NAME_ATTR, hostName);
250         serializer.attribute(NS, OS_NAME_ATTR, System.getProperty("os.name"));
251         serializer.attribute(NS, OS_VERSION_ATTR, System.getProperty("os.version"));
252         serializer.attribute(NS, OS_ARCH_ATTR, System.getProperty("os.arch"));
253         serializer.attribute(NS, JAVA_VENDOR_ATTR, System.getProperty("java.vendor"));
254         serializer.attribute(NS, JAVA_VERSION_ATTR, System.getProperty("java.version"));
255 
256         // Build Info
257         serializer.startTag(NS, BUILD_TAG);
258         for (String key : holder.context.getAttributes().keySet()) {
259             serializer.attribute(
260                     NS,
261                     sanitizeAttributesKey(key),
262                     String.join(",", holder.context.getAttributes().get(key)));
263         }
264         if (!holder.context.getBuildInfos().isEmpty()) {
265             IBuildInfo buildInfo = holder.context.getBuildInfos().get(0);
266             addBuildInfoAttributesIfNotNull(serializer, buildInfo, DEVICE_KERNEL_INFO_ATTR);
267             addBuildInfoAttributesIfNotNull(serializer, buildInfo, SYSTEM_IMG_INFO_ATTR);
268             addBuildInfoAttributesIfNotNull(serializer, buildInfo, VENDOR_IMG_INFO_ATTR);
269         }
270         addBuildInfoAttributes(serializer, holder);
271         serializer.endTag(NS, BUILD_TAG);
272 
273         // Run History
274         String runHistoryJson = holder.context.getAttributes().getUniqueMap().get(RUN_HISTORY);
275         if (runHistoryJson != null) {
276             serializer.startTag(NS, RUN_HISTORY_TAG);
277             Gson gson = new Gson();
278             RunHistory[] runHistories = gson.fromJson(runHistoryJson, RunHistory[].class);
279             for (RunHistory runHistory : runHistories) {
280                 serializer.startTag(NS, RUN_TAG);
281                 serializer.attribute(NS, START_TIME_ATTR, String.valueOf(runHistory.startTime));
282                 serializer.attribute(NS, END_TIME_ATTR, String.valueOf(runHistory.endTime));
283                 serializer.attribute(NS, PASS_ATTR, Long.toString(runHistory.passedTests));
284                 serializer.attribute(NS, FAILED_ATTR, Long.toString(runHistory.failedTests));
285                 serializer.attribute(NS, COMMAND_LINE_ARGS, runHistory.commandLineArgs);
286                 serializer.attribute(NS, HOST_NAME_ATTR, runHistory.hostName);
287                 serializer.endTag(NS, RUN_TAG);
288             }
289             serializer.endTag(NS, RUN_HISTORY_TAG);
290         }
291 
292         // Summary
293         serializer.startTag(NS, SUMMARY_TAG);
294         serializer.attribute(NS, PASS_ATTR, Long.toString(holder.passedTests));
295         serializer.attribute(NS, FAILED_ATTR, Long.toString(holder.failedTests));
296         serializer.attribute(NS, MODULES_DONE_ATTR, Integer.toString(holder.completeModules));
297         serializer.attribute(NS, MODULES_TOTAL_ATTR, Integer.toString(holder.totalModules));
298         serializer.endTag(NS, SUMMARY_TAG);
299 
300         List<TestRunResult> sortedModuleList = sortModules(holder.runResults, holder.modulesAbi);
301         // Results
302         for (TestRunResult module : sortedModuleList) {
303             serializer.startTag(NS, MODULE_TAG);
304             // To be compatible of CTS strip the abi from the module name when available.
305             if (holder.modulesAbi.get(module.getName()) != null) {
306                 String moduleAbi = holder.modulesAbi.get(module.getName()).getName();
307                 String moduleNameStripped = module.getName().replace(moduleAbi + " ", "");
308                 serializer.attribute(NS, NAME_ATTR, moduleNameStripped);
309                 serializer.attribute(NS, ABI_ATTR, moduleAbi);
310             } else {
311                 serializer.attribute(NS, NAME_ATTR, module.getName());
312             }
313             serializer.attribute(NS, RUNTIME_ATTR, String.valueOf(module.getElapsedTime()));
314             boolean isDone = module.isRunComplete() && !module.isRunFailure();
315 
316             serializer.attribute(NS, DONE_ATTR, Boolean.toString(isDone));
317             serializer.attribute(
318                     NS, PASS_ATTR, Integer.toString(module.getNumTestsInState(TestStatus.PASSED)));
319             serializer.attribute(NS, TOTAL_TESTS_ATTR, Integer.toString(module.getNumTests()));
320 
321             if (!isDone) {
322                 String message = module.getRunFailureMessage();
323                 if (message == null) {
324                     message = "Run was incomplete. Some tests might not have finished.";
325                 }
326                 FailureDescription failureDescription = module.getRunFailureDescription();
327                 serializer.startTag(NS, MODULES_NOT_DONE_REASON);
328                 serializer.attribute(NS, MESSAGE_ATTR, sanitizeXmlContent(message));
329                 if (failureDescription != null && failureDescription.getErrorIdentifier() != null) {
330                     serializer.attribute(
331                             NS, ERROR_NAME_ATTR, failureDescription.getErrorIdentifier().name());
332                     serializer.attribute(
333                             NS,
334                             ERROR_CODE_ATTR,
335                             Long.toString(failureDescription.getErrorIdentifier().code()));
336                 }
337                 serializer.endTag(NS, MODULES_NOT_DONE_REASON);
338             }
339             serializeTestCases(serializer, module.getTestResults());
340             serializer.endTag(NS, MODULE_TAG);
341         }
342         serializer.endDocument();
343         return resultFile;
344     }
345 
serializeTestCases( XmlSerializer serializer, Map<TestDescription, TestResult> results)346     private void serializeTestCases(
347             XmlSerializer serializer, Map<TestDescription, TestResult> results)
348             throws IllegalArgumentException, IllegalStateException, IOException {
349         // We reformat into the same format as the ResultHandler from CTS to be compatible for now.
350         Map<String, Map<String, TestResult>> format = new LinkedHashMap<>();
351         for (Entry<TestDescription, TestResult> cr : results.entrySet()) {
352             if (format.get(cr.getKey().getClassName()) == null) {
353                 format.put(cr.getKey().getClassName(), new LinkedHashMap<>());
354             }
355             Map<String, TestResult> methodResult = format.get(cr.getKey().getClassName());
356             methodResult.put(cr.getKey().getTestName(), cr.getValue());
357         }
358 
359         for (String className : format.keySet()) {
360             serializer.startTag(NS, CASE_TAG);
361             serializer.attribute(NS, NAME_ATTR, className);
362             for (Entry<String, TestResult> individualResult : format.get(className).entrySet()) {
363                 TestStatus status = individualResult.getValue().getResultStatus();
364                 // TODO(b/322204420): Report skipped to XML and support parsing it
365                 if (TestStatus.SKIPPED.equals(status)) {
366                     continue;
367                 }
368                 if (status == null) {
369                     continue; // test was not executed, don't report
370                 }
371                 serializer.startTag(NS, TEST_TAG);
372                 serializer.attribute(
373                         NS, RESULT_ATTR, TestStatus.convertToCompatibilityString(status));
374                 serializer.attribute(NS, NAME_ATTR, individualResult.getKey());
375                 if (TestStatus.IGNORED.equals(status)) {
376                     serializer.attribute(NS, SKIPPED_ATTR, Boolean.toString(true));
377                 }
378 
379                 handleTestFailure(serializer, individualResult);
380 
381                 HandleLoggedFiles(serializer, individualResult);
382 
383                 for (Entry<String, String> metric :
384                         TfMetricProtoUtil.compatibleConvert(
385                                         individualResult.getValue().getProtoMetrics())
386                                 .entrySet()) {
387                     serializer.startTag(NS, METRIC_TAG);
388                     serializer.attribute(NS, METRIC_KEY, metric.getKey());
389                     serializer.text(sanitizeXmlContent(metric.getValue()));
390                     serializer.endTag(NS, METRIC_TAG);
391                 }
392                 serializer.endTag(NS, TEST_TAG);
393             }
394             serializer.endTag(NS, CASE_TAG);
395         }
396     }
397 
handleTestFailure(XmlSerializer serializer, Entry<String, TestResult> testResult)398     private void handleTestFailure(XmlSerializer serializer, Entry<String, TestResult> testResult)
399             throws IllegalArgumentException, IllegalStateException, IOException {
400         final String fullStack = testResult.getValue().getStackTrace();
401         if (fullStack != null) {
402             String message;
403             int index = fullStack.indexOf('\n');
404             if (index < 0) {
405                 // Trace is a single line, just set the message to be the same as the stacktrace.
406                 message = fullStack;
407             } else {
408                 message = fullStack.substring(0, index);
409             }
410             ErrorIdentifier errorIdentifier =
411                     testResult.getValue().getFailure().getErrorIdentifier();
412             String truncatedStackTrace = getTruncatedStackTrace(fullStack, testResult.getKey());
413             serializer.startTag(NS, FAILURE_TAG);
414 
415             serializer.attribute(NS, MESSAGE_ATTR, sanitizeXmlContent(message));
416             if (errorIdentifier != null) {
417                 serializer.attribute(NS, ERROR_NAME_ATTR, errorIdentifier.name());
418                 serializer.attribute(NS, ERROR_CODE_ATTR, Long.toString(errorIdentifier.code()));
419             }
420             serializer.startTag(NS, STACK_TAG);
421             serializer.text(sanitizeXmlContent(truncatedStackTrace));
422             serializer.endTag(NS, STACK_TAG);
423 
424             serializer.endTag(NS, FAILURE_TAG);
425         }
426     }
427 
428     /** Truncates the full stack trace with maximum {@link STACK_TRACE_MAX_SIZE} characters. */
getTruncatedStackTrace(String fullStackTrace, String testCaseName)429     private static String getTruncatedStackTrace(String fullStackTrace, String testCaseName) {
430         if (fullStackTrace == null) {
431             return null;
432         }
433         if (fullStackTrace.length() > STACK_TRACE_MAX_SIZE) {
434             CLog.i(
435                     "The stack trace for test case %s contains %d characters, and has been"
436                             + " truncated to %d characters in %s.",
437                     testCaseName,
438                     fullStackTrace.length(),
439                     STACK_TRACE_MAX_SIZE,
440                     TEST_RESULT_FILE_NAME);
441             return fullStackTrace.substring(0, STACK_TRACE_MAX_SIZE);
442         }
443         return fullStackTrace;
444     }
445 
446     /** Add files captured on test failures. */
HandleLoggedFiles( XmlSerializer serializer, Entry<String, TestResult> testResult)447     private static void HandleLoggedFiles(
448             XmlSerializer serializer, Entry<String, TestResult> testResult)
449             throws IllegalArgumentException, IllegalStateException, IOException {
450         Map<String, LogFile> loggedFiles = testResult.getValue().getLoggedFiles();
451         if (loggedFiles == null || loggedFiles.isEmpty()) {
452             return;
453         }
454         for (String key : loggedFiles.keySet()) {
455             switch (loggedFiles.get(key).getType()) {
456                 case BUGREPORT:
457                     addLogIfNotNull(serializer, BUGREPORT_TAG, key, loggedFiles.get(key).getUrl());
458                     break;
459                 case LOGCAT:
460                     addLogIfNotNull(serializer, LOGCAT_TAG, key, loggedFiles.get(key).getUrl());
461                     break;
462                 case PNG:
463                 case JPEG:
464                     addLogIfNotNull(serializer, SCREENSHOT_TAG, key, loggedFiles.get(key).getUrl());
465                     break;
466                 default:
467                     break;
468             }
469         }
470     }
471 
addLogIfNotNull( XmlSerializer serializer, String tag, String key, String text)472     private static void addLogIfNotNull(
473             XmlSerializer serializer, String tag, String key, String text)
474             throws IllegalArgumentException, IllegalStateException, IOException {
475         if (text == null) {
476             CLog.d("Text for tag '%s' and key '%s' is null. skipping it.", tag, key);
477             return;
478         }
479         serializer.startTag(NS, tag);
480         serializer.attribute(NS, LOG_FILE_NAME_ATTR, key);
481         serializer.text(text);
482         serializer.endTag(NS, tag);
483     }
484 
485     /**
486      * Return the given time as a {@link String} suitable for displaying.
487      *
488      * <p>Example: Fri Aug 20 15:13:03 PDT 2010
489      *
490      * @param time the epoch time in ms since midnight Jan 1, 1970
491      */
toReadableDateString(long time)492     private static String toReadableDateString(long time) {
493         SimpleDateFormat dateFormat = new SimpleDateFormat("EEE MMM dd HH:mm:ss zzz yyyy");
494         return dateFormat.format(new Date(time));
495     }
496 
497     /** {@inheritDoc} */
498     @Override
parseResults(File resultDir, boolean shallow)499     public SuiteResultHolder parseResults(File resultDir, boolean shallow) throws IOException {
500         File resultFile = new File(resultDir, TEST_RESULT_FILE_NAME);
501         if (!resultFile.exists()) {
502             CLog.e("Could not find %s for loading the results.", resultFile.getAbsolutePath());
503             return null;
504         }
505         SuiteResultHolder invocation = new SuiteResultHolder();
506         IInvocationContext context = new InvocationContext();
507         try {
508             XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
509             XmlPullParser parser = factory.newPullParser();
510             parser.setInput(new FileReader(resultFile));
511 
512             parser.nextTag();
513             parser.require(XmlPullParser.START_TAG, NS, RESULT_TAG);
514             invocation.startTime = Long.valueOf(parser.getAttributeValue(NS, START_TIME_ATTR));
515             invocation.endTime = Long.valueOf(parser.getAttributeValue(NS, END_TIME_ATTR));
516             invocation.hostName = parser.getAttributeValue(NS, HOST_NAME_ATTR);
517             context.addInvocationAttribute(
518                     COMMAND_LINE_ARGS, parser.getAttributeValue(NS, COMMAND_LINE_ARGS));
519             parseSuiteAttributes(parser, context);
520 
521             String deviceList = parser.getAttributeValue(NS, DEVICES_ATTR);
522             int i = 0;
523             // TODO: Fix to correctly handle the number of device per shard.
524             for (String device : deviceList.split(",")) {
525                 context.addSerialsFromShard(i, Arrays.asList(device));
526                 i++;
527             }
528 
529             parser.nextTag();
530             parser.require(XmlPullParser.START_TAG, NS, BUILD_TAG);
531 
532             for (int index = 0; index < parser.getAttributeCount(); index++) {
533                 String key = parser.getAttributeName(index);
534                 String value = parser.getAttributeValue(NS, key);
535                 // TODO: Handle list of values that are comma separated.
536                 context.addInvocationAttribute(key, value);
537             }
538             parseBuildInfoAttributes(parser, context);
539 
540             parser.nextTag();
541             parser.require(XmlPullParser.END_TAG, NS, BUILD_TAG);
542 
543             parser.nextTag();
544             boolean hasRunHistoryTag = true;
545             try {
546                 parser.require(XmlPullParser.START_TAG, NS, RUN_HISTORY_TAG);
547             } catch (XmlPullParserException e) {
548                 hasRunHistoryTag = false;
549             }
550             if (hasRunHistoryTag) {
551                 handleRunHistoryLevel(parser);
552             }
553 
554             parser.require(XmlPullParser.START_TAG, NS, SUMMARY_TAG);
555 
556             invocation.completeModules =
557                     Integer.parseInt(parser.getAttributeValue(NS, MODULES_DONE_ATTR));
558             invocation.totalModules =
559                     Integer.parseInt(parser.getAttributeValue(NS, MODULES_TOTAL_ATTR));
560             invocation.passedTests = Integer.parseInt(parser.getAttributeValue(NS, PASS_ATTR));
561             invocation.failedTests = Integer.parseInt(parser.getAttributeValue(NS, FAILED_ATTR));
562 
563             parser.nextTag();
564             parser.require(XmlPullParser.END_TAG, NS, SUMMARY_TAG);
565 
566             if (!shallow) {
567                 Collection<TestRunResult> results = new ArrayList<>();
568                 Map<String, IAbi> moduleAbis = new HashMap<>();
569                 // Module level information parsing
570                 handleModuleLevel(parser, results, moduleAbis);
571                 parser.require(XmlPullParser.END_TAG, NS, RESULT_TAG);
572                 invocation.runResults = results;
573                 invocation.modulesAbi = moduleAbis;
574             }
575         } catch (XmlPullParserException e) {
576             CLog.e(e);
577             return null;
578         }
579 
580         invocation.context = context;
581         return invocation;
582     }
583 
584     /** Sort the list of results based on their name without abi primarily then secondly on abi. */
585     @VisibleForTesting
sortModules( Collection<TestRunResult> results, Map<String, IAbi> moduleAbis)586     List<TestRunResult> sortModules(
587             Collection<TestRunResult> results, Map<String, IAbi> moduleAbis) {
588         List<TestRunResult> sortedList = new ArrayList<>(results);
589         Collections.sort(
590                 sortedList,
591                 new Comparator<TestRunResult>() {
592                     @Override
593                     public int compare(TestRunResult o1, TestRunResult o2) {
594                         String module1NameStripped = o1.getName();
595                         String module1Abi = "";
596                         if (moduleAbis.get(module1NameStripped) != null) {
597                             module1Abi = moduleAbis.get(module1NameStripped).getName();
598                             module1NameStripped = module1NameStripped.replace(module1Abi + " ", "");
599                         }
600 
601                         String module2NameStripped = o2.getName();
602                         String module2Abi = "";
603                         if (moduleAbis.get(module2NameStripped) != null) {
604                             module2Abi = moduleAbis.get(module2NameStripped).getName();
605                             module2NameStripped = module2NameStripped.replace(module2Abi + " ", "");
606                         }
607                         int res = module1NameStripped.compareTo(module2NameStripped);
608                         if (res != 0) {
609                             return res;
610                         }
611                         // Use the Abi as discriminant to always sort abi in the same order.
612                         return module1Abi.compareTo(module2Abi);
613                     }
614                 });
615         return sortedList;
616     }
617 
618     /** Handle the parsing and replay of all run history information. */
handleRunHistoryLevel(XmlPullParser parser)619     private void handleRunHistoryLevel(XmlPullParser parser)
620             throws IOException, XmlPullParserException {
621         while (parser.nextTag() == XmlPullParser.START_TAG) {
622             parser.require(XmlPullParser.START_TAG, NS, RUN_TAG);
623             parser.nextTag();
624             parser.require(XmlPullParser.END_TAG, NS, RUN_TAG);
625         }
626         parser.require(XmlPullParser.END_TAG, NS, RUN_HISTORY_TAG);
627         parser.nextTag();
628     }
629 
630     /**
631      * Handle the parsing and replay of all the information inside a module (class, method,
632      * failures).
633      */
handleModuleLevel( XmlPullParser parser, Collection<TestRunResult> results, Map<String, IAbi> moduleAbis)634     private void handleModuleLevel(
635             XmlPullParser parser, Collection<TestRunResult> results, Map<String, IAbi> moduleAbis)
636             throws IOException, XmlPullParserException {
637         while (parser.nextTag() == XmlPullParser.START_TAG) {
638             parser.require(XmlPullParser.START_TAG, NS, MODULE_TAG);
639             TestRunResult module = new TestRunResult();
640             results.add(module);
641             String name = parser.getAttributeValue(NS, NAME_ATTR);
642             String abi = parser.getAttributeValue(NS, ABI_ATTR);
643             String moduleId = name;
644             if (abi != null) {
645                 moduleId = AbiUtils.createId(abi, name);
646                 moduleAbis.put(moduleId, new Abi(abi, AbiUtils.getBitness(abi)));
647             }
648             long moduleElapsedTime = Long.parseLong(parser.getAttributeValue(NS, RUNTIME_ATTR));
649             boolean moduleDone = Boolean.parseBoolean(parser.getAttributeValue(NS, DONE_ATTR));
650             int totalTests = Integer.parseInt(parser.getAttributeValue(NS, TOTAL_TESTS_ATTR));
651             module.testRunStarted(moduleId, totalTests);
652             // TestCase level information parsing
653             while (parser.nextTag() == XmlPullParser.START_TAG) {
654                 // If a reason for not done exists, handle it.
655                 if (parser.getName().equals(MODULES_NOT_DONE_REASON)) {
656                     parser.require(XmlPullParser.START_TAG, NS, MODULES_NOT_DONE_REASON);
657                     parser.nextTag();
658                     parser.require(XmlPullParser.END_TAG, NS, MODULES_NOT_DONE_REASON);
659                     continue;
660                 }
661                 parser.require(XmlPullParser.START_TAG, NS, CASE_TAG);
662                 String className = parser.getAttributeValue(NS, NAME_ATTR);
663                 // Test level information parsing
664                 handleTestCaseLevel(parser, module, className);
665                 parser.require(XmlPullParser.END_TAG, NS, CASE_TAG);
666             }
667             module.testRunEnded(moduleElapsedTime, new HashMap<String, Metric>());
668             module.setRunComplete(moduleDone);
669             parser.require(XmlPullParser.END_TAG, NS, MODULE_TAG);
670         }
671     }
672 
673     /** Parse and replay all the individual test cases level (method) informations. */
handleTestCaseLevel( XmlPullParser parser, TestRunResult currentModule, String className)674     private void handleTestCaseLevel(
675             XmlPullParser parser, TestRunResult currentModule, String className)
676             throws IOException, XmlPullParserException {
677         while (parser.nextTag() == XmlPullParser.START_TAG) {
678             parser.require(XmlPullParser.START_TAG, NS, TEST_TAG);
679             String methodName = parser.getAttributeValue(NS, NAME_ATTR);
680             TestStatus status =
681                     TestStatus.convertFromCompatibilityString(
682                             parser.getAttributeValue(NS, RESULT_ATTR));
683             TestDescription description = new TestDescription(className, methodName);
684             currentModule.testStarted(description);
685             if (TestStatus.IGNORED.equals(status)) {
686                 currentModule.testIgnored(description);
687             }
688             HashMap<String, Metric> metrics = new HashMap<String, Metric>();
689             while (parser.nextTag() == XmlPullParser.START_TAG) { // Failure level
690                 if (parser.getName().equals(FAILURE_TAG)) {
691                     String failure = parser.getAttributeValue(NS, MESSAGE_ATTR);
692                     if (parser.nextTag() == XmlPullParser.START_TAG) {
693                         parser.require(XmlPullParser.START_TAG, NS, STACK_TAG);
694                         failure = parser.nextText();
695                         parser.require(XmlPullParser.END_TAG, NS, STACK_TAG);
696                     }
697                     if (TestStatus.FAILURE.equals(status)) {
698                         currentModule.testFailed(description, failure);
699                     } else if (TestStatus.ASSUMPTION_FAILURE.equals(status)) {
700                         currentModule.testAssumptionFailure(description, failure);
701                     }
702                     parser.nextTag();
703                     parser.require(XmlPullParser.END_TAG, NS, FAILURE_TAG);
704                 }
705                 parseLoggedFiles(parser, currentModule);
706                 metrics.putAll(parseMetrics(parser));
707             }
708             currentModule.testEnded(description, metrics);
709             parser.require(XmlPullParser.END_TAG, NS, TEST_TAG);
710         }
711     }
712 
713     /** Add files captured on test failures. */
parseLoggedFiles(XmlPullParser parser, TestRunResult currentModule)714     private static void parseLoggedFiles(XmlPullParser parser, TestRunResult currentModule)
715             throws XmlPullParserException, IOException {
716         if (parser.getName().equals(BUGREPORT_TAG)) {
717             parseSingleFiles(parser, currentModule, BUGREPORT_TAG, LogDataType.BUGREPORTZ);
718         } else if (parser.getName().equals(LOGCAT_TAG)) {
719             parseSingleFiles(parser, currentModule, LOGCAT_TAG, LogDataType.LOGCAT);
720         } else if (parser.getName().equals(SCREENSHOT_TAG)) {
721             parseSingleFiles(parser, currentModule, SCREENSHOT_TAG, LogDataType.PNG);
722         }
723     }
724 
parseSingleFiles( XmlPullParser parser, TestRunResult currentModule, String tagName, LogDataType type)725     private static void parseSingleFiles(
726             XmlPullParser parser, TestRunResult currentModule, String tagName, LogDataType type)
727             throws XmlPullParserException, IOException {
728         String name = parser.getAttributeValue(NS, LOG_FILE_NAME_ATTR);
729         String logFileUrl = parser.nextText();
730         currentModule.testLogSaved(name, new LogFile(logFileUrl, logFileUrl, type));
731         parser.require(XmlPullParser.END_TAG, NS, tagName);
732     }
733 
parseMetrics(XmlPullParser parser)734     private static HashMap<String, Metric> parseMetrics(XmlPullParser parser)
735             throws XmlPullParserException, IOException {
736         HashMap<String, Metric> metrics = new HashMap<>();
737         if (parser.getName().equals(METRIC_TAG)) {
738             parser.require(XmlPullParser.START_TAG, NS, METRIC_TAG);
739             for (int index = 0; index < parser.getAttributeCount(); index++) {
740                 String key = parser.getAttributeValue(index);
741                 String value = parser.nextText();
742                 metrics.put(key, TfMetricProtoUtil.stringToMetric(value));
743             }
744             parser.require(XmlPullParser.END_TAG, NS, METRIC_TAG);
745         }
746         return metrics;
747     }
748 
749     @VisibleForTesting
sanitizeXmlContent(String s)750     protected String sanitizeXmlContent(String s) {
751         return XmlEscapers.xmlContentEscaper().escape(s);
752     }
753 
sanitizeAttributesKey(String attribute)754     private static String sanitizeAttributesKey(String attribute) {
755         return attribute.replace(":", "_");
756     }
757 
addBuildInfoAttributesIfNotNull( XmlSerializer serializer, IBuildInfo buildInfo, String attributeName)758     private static void addBuildInfoAttributesIfNotNull(
759             XmlSerializer serializer, IBuildInfo buildInfo, String attributeName)
760             throws IOException {
761         String attributeValue = buildInfo.getBuildAttributes().get(attributeName);
762         if (attributeValue != null) {
763             serializer.attribute(NS, attributeName, attributeValue);
764         }
765     }
766 }
767