1 /*
2  * Copyright (C) 2015 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.compatibility.common.util;
17 
18 import com.android.compatibility.common.util.ChecksumReporter.ChecksumValidationException;
19 
20 import com.google.common.base.Strings;
21 
22 import org.xmlpull.v1.XmlPullParser;
23 import org.xmlpull.v1.XmlPullParserException;
24 import org.xmlpull.v1.XmlPullParserFactory;
25 import org.xmlpull.v1.XmlSerializer;
26 
27 import java.io.File;
28 import java.io.FileOutputStream;
29 import java.io.FileReader;
30 import java.io.IOException;
31 import java.io.InputStream;
32 import java.io.OutputStream;
33 import java.net.InetAddress;
34 import java.net.UnknownHostException;
35 import java.nio.file.FileSystems;
36 import java.nio.file.Files;
37 import java.nio.file.Path;
38 import java.text.SimpleDateFormat;
39 import java.util.ArrayList;
40 import java.util.Collection;
41 import java.util.Collections;
42 import java.util.Date;
43 import java.util.List;
44 import java.util.Locale;
45 import java.util.Map;
46 import java.util.Map.Entry;
47 import java.util.Set;
48 
49 import javax.xml.transform.Transformer;
50 import javax.xml.transform.TransformerException;
51 import javax.xml.transform.TransformerFactory;
52 import javax.xml.transform.stream.StreamResult;
53 import javax.xml.transform.stream.StreamSource;
54 
55 /**
56  * Handles conversion of results to/from files.
57  *
58  * @deprecated b/170495912 Please avoid any change in the schema which would force updates in
59  *     classes that currently handle the XML generation for *TS.
60  */
61 @Deprecated
62 public class ResultHandler {
63 
64     private static final String ENCODING = "UTF-8";
65     private static final String TYPE = "org.kxml2.io.KXmlParser,org.kxml2.io.KXmlSerializer";
66     private static final String NS = null;
67     private static final String RESULT_FILE_VERSION = "5.0";
68     public static final String TEST_RESULT_FILE_NAME = "test_result.xml";
69     public static final String FAILURE_REPORT_NAME = "test_result_failures.html";
70     private static final String FAILURE_XSL_FILE_NAME = "compatibility_failures.xsl";
71 
72     public static final String[] RESULT_RESOURCES = {
73         "compatibility_result.css",
74         "compatibility_result.xsd",
75         "compatibility_result.xsl",
76         "logo.png"
77     };
78 
79     // XML constants
80     private static final String ABI_ATTR = "abi";
81     private static final String BUGREPORT_TAG = "BugReport";
82     private static final String BUILD_FINGERPRINT = "build_fingerprint";
83     private static final String BUILD_FINGERPRINT_UNALTERED = "build_fingerprint_unaltered";
84     private static final String BUILD_ID = "build_id";
85     private static final String BUILD_PRODUCT = "build_product";
86     private static final String BUILD_TAG = "Build";
87     private static final String CASE_TAG = "TestCase";
88     private static final String COMMAND_LINE_ARGS = "command_line_args";
89     private static final String DEVICES_ATTR = "devices";
90     private static final String DONE_ATTR = "done";
91     private static final String END_DISPLAY_TIME_ATTR = "end_display";
92     private static final String END_TIME_ATTR = "end";
93     private static final String FAILED_ATTR = "failed";
94     private static final String FAILURE_TAG = "Failure";
95     private static final String HOST_NAME_ATTR = "host_name";
96     private static final String JAVA_VENDOR_ATTR = "java_vendor";
97     private static final String JAVA_VERSION_ATTR = "java_version";
98     private static final String LOGCAT_TAG = "Logcat";
99     private static final String LOG_URL_ATTR = "log_url";
100     private static final String MESSAGE_ATTR = "message";
101     private static final String MODULE_TAG = "Module";
102     private static final String MODULES_DONE_ATTR = "modules_done";
103     private static final String MODULES_TOTAL_ATTR = "modules_total";
104     private static final String MODULES_NOT_DONE_REASON = "Reason";
105     private static final String NAME_ATTR = "name";
106     private static final String OS_ARCH_ATTR = "os_arch";
107     private static final String OS_NAME_ATTR = "os_name";
108     private static final String OS_VERSION_ATTR = "os_version";
109     private static final String PASS_ATTR = "pass";
110     private static final String REPORT_VERSION_ATTR = "report_version";
111     private static final String REFERENCE_URL_ATTR = "reference_url";
112     private static final String RESULT_ATTR = "result";
113     private static final String RESULT_TAG = "Result";
114     private static final String RUNTIME_ATTR = "runtime";
115     private static final String RUN_HISTORY_ATTR = "run_history";
116     private static final String RUN_HISTORY_TAG = "RunHistory";
117     private static final String RUN_TAG = "Run";
118     private static final String SCREENSHOT_TAG = "Screenshot";
119     private static final String SKIPPED_ATTR = "skipped";
120     private static final String STACK_TAG = "StackTrace";
121     private static final String START_DISPLAY_TIME_ATTR = "start_display";
122     private static final String START_TIME_ATTR = "start";
123     private static final String SUITE_NAME_ATTR = "suite_name";
124     private static final String SUITE_PLAN_ATTR = "suite_plan";
125     private static final String SUITE_VERSION_ATTR = "suite_version";
126     private static final String SUITE_BUILD_ATTR = "suite_build_number";
127     private static final String SUMMARY_TAG = "Summary";
128     private static final String METRIC_TAG = "Metric";
129     private static final String TEST_TAG = "Test";
130 
131     private static final String LATEST_RESULT_DIR = "latest";
132 
133     /**
134      * Returns IInvocationResults that can be queried for general reporting information, but that
135      * do not store underlying module data. Useful for summarizing invocation history.
136      * @param resultsDir
137      */
getLightResults(File resultsDir)138     public static List<IInvocationResult> getLightResults(File resultsDir) {
139         List<IInvocationResult> results = new ArrayList<>();
140         List<File> files = getResultDirectories(resultsDir);
141         for (File resultDir : files) {
142             if (LATEST_RESULT_DIR.equals(resultDir.getName())) {
143                 continue;
144             }
145             IInvocationResult result = getResultFromDir(resultDir, false);
146             if (result != null) {
147                 results.add(new LightInvocationResult(result));
148                 result = null; // ensure all references are removed to free memory
149             }
150         }
151         // Sort the table entries on each entry's timestamp.
152         Collections.sort(results,  (result1, result2) -> Long.compare(
153                 result1.getStartTime(),
154                 result2.getStartTime()));
155         return results;
156     }
157 
158     /**
159      * @param resultDir
160      * @return an IInvocationResult for this result, or null upon error
161      */
getResultFromDir(File resultDir)162     public static IInvocationResult getResultFromDir(File resultDir) {
163         return getResultFromDir(resultDir, false);
164     }
165 
166     /**
167      * @param resultDir
168      * @param useChecksum
169      * @return an IInvocationResult for this result, or null upon error
170      */
getResultFromDir(File resultDir, Boolean useChecksum)171     public static IInvocationResult getResultFromDir(File resultDir, Boolean useChecksum) {
172         File resultFile = null;
173         try {
174             resultFile = new File(resultDir, TEST_RESULT_FILE_NAME);
175             if (!resultFile.exists()) {
176                 return null;
177             }
178             Boolean invocationUseChecksum = useChecksum;
179             IInvocationResult invocation = new InvocationResult();
180             invocation.setRetryDirectory(resultDir);
181             ChecksumReporter checksumReporter = null;
182             if (invocationUseChecksum) {
183                 try {
184                     checksumReporter = ChecksumReporter.load(resultDir);
185                     invocation.setRetryChecksumStatus(RetryChecksumStatus.RetryWithChecksum);
186                 } catch (ChecksumValidationException e) {
187                     // Unable to read checksum form previous execution
188                     invocation.setRetryChecksumStatus(RetryChecksumStatus.RetryWithoutChecksum);
189                     invocationUseChecksum = false;
190                 }
191             }
192             XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
193             XmlPullParser parser = factory.newPullParser();
194             parser.setInput(new FileReader(resultFile));
195 
196             parser.nextTag();
197             parser.require(XmlPullParser.START_TAG, NS, RESULT_TAG);
198             invocation.setStartTime(Long.valueOf(
199                     parser.getAttributeValue(NS, START_TIME_ATTR)));
200             invocation.setTestPlan(parser.getAttributeValue(NS, SUITE_PLAN_ATTR));
201             invocation.setCommandLineArgs(parser.getAttributeValue(NS, COMMAND_LINE_ARGS));
202             String deviceList = parser.getAttributeValue(NS, DEVICES_ATTR);
203             for (String device : deviceList.split(",")) {
204                 invocation.addDeviceSerial(device);
205             }
206 
207             parser.nextTag();
208             parser.require(XmlPullParser.START_TAG, NS, BUILD_TAG);
209             invocation.addInvocationInfo(BUILD_ID, parser.getAttributeValue(NS, BUILD_ID));
210             invocation.addInvocationInfo(BUILD_PRODUCT, parser.getAttributeValue(NS,
211                     BUILD_PRODUCT));
212             String runHistoryValue = parser.getAttributeValue(NS, RUN_HISTORY_ATTR);
213             if (runHistoryValue != null) {
214                 invocation.addInvocationInfo(RUN_HISTORY_ATTR, runHistoryValue);
215             }
216 
217             // The build fingerprint needs to reflect the true fingerprint of the device under test,
218             // ignoring potential overrides made by test suites (namely STS) for APFE build
219             // association.
220             String reportFingerprint = parser.getAttributeValue(NS, BUILD_FINGERPRINT);
221             String unalteredFingerprint = parser.getAttributeValue(NS, BUILD_FINGERPRINT_UNALTERED);
222             Boolean fingerprintWasAltered = !Strings.isNullOrEmpty(unalteredFingerprint);
223             invocation.setBuildFingerprint(fingerprintWasAltered ? unalteredFingerprint :
224                 reportFingerprint );
225 
226             // TODO(stuartscott): may want to reload these incase the retry was done with
227             // --skip-device-info flag
228             parser.nextTag();
229             parser.require(XmlPullParser.END_TAG, NS, BUILD_TAG);
230 
231             // Parse RunHistory tag.
232             parser.nextTag();
233             boolean hasRunHistoryTag = true;
234             try {
235                 parser.require(XmlPullParser.START_TAG, NS, RUN_HISTORY_TAG);
236             } catch (XmlPullParserException e) {
237                 hasRunHistoryTag = false;
238             }
239             if (hasRunHistoryTag) {
240                 parseRunHistory(parser);
241             }
242 
243             parser.require(XmlPullParser.START_TAG, NS, SUMMARY_TAG);
244             parser.nextTag();
245             parser.require(XmlPullParser.END_TAG, NS, SUMMARY_TAG);
246             while (parser.nextTag() == XmlPullParser.START_TAG) {
247                 parser.require(XmlPullParser.START_TAG, NS, MODULE_TAG);
248                 String name = parser.getAttributeValue(NS, NAME_ATTR);
249                 String abi = parser.getAttributeValue(NS, ABI_ATTR);
250                 String moduleId = AbiUtils.createId(abi, name);
251                 boolean done = Boolean.parseBoolean(parser.getAttributeValue(NS, DONE_ATTR));
252                 IModuleResult module = invocation.getOrCreateModule(moduleId);
253                 module.initializeDone(done);
254                 long runtime = Long.parseLong(parser.getAttributeValue(NS, RUNTIME_ATTR));
255                 module.addRuntime(runtime);
256                 while (parser.nextTag() == XmlPullParser.START_TAG) {
257                     // If a reason for not done exists, handle it.
258                     if (parser.getName().equals(MODULES_NOT_DONE_REASON)) {
259                         parser.require(XmlPullParser.START_TAG, NS, MODULES_NOT_DONE_REASON);
260                         parser.nextTag();
261                         parser.require(XmlPullParser.END_TAG, NS, MODULES_NOT_DONE_REASON);
262                         continue;
263                     }
264                     parser.require(XmlPullParser.START_TAG, NS, CASE_TAG);
265                     String caseName = parser.getAttributeValue(NS, NAME_ATTR);
266                     ICaseResult testCase = module.getOrCreateResult(caseName);
267                     while (parser.nextTag() == XmlPullParser.START_TAG) {
268                         parser.require(XmlPullParser.START_TAG, NS, TEST_TAG);
269                         String testName = parser.getAttributeValue(NS, NAME_ATTR);
270                         ITestResult test = testCase.getOrCreateResult(testName);
271                         String result = parser.getAttributeValue(NS, RESULT_ATTR);
272                         String skipped = parser.getAttributeValue(NS, SKIPPED_ATTR);
273                         if (skipped != null && Boolean.parseBoolean(skipped)) {
274                             // mark test passed and skipped
275                             test.skipped();
276                         } else {
277                             // only apply result status directly if test was not skipped
278                             test.setResultStatus(TestStatus.getStatus(result));
279                         }
280                         test.setRetry(true);
281                         while (parser.nextTag() == XmlPullParser.START_TAG) {
282                             if (parser.getName().equals(FAILURE_TAG)) {
283                                 test.setMessage(parser.getAttributeValue(NS, MESSAGE_ATTR));
284                                 if (parser.nextTag() == XmlPullParser.START_TAG) {
285                                     parser.require(XmlPullParser.START_TAG, NS, STACK_TAG);
286                                     test.setStackTrace(parser.nextText());
287                                     parser.require(XmlPullParser.END_TAG, NS, STACK_TAG);
288                                     parser.nextTag();
289                                 }
290                                 parser.require(XmlPullParser.END_TAG, NS, FAILURE_TAG);
291                             } else if (parser.getName().equals(BUGREPORT_TAG)) {
292                                 test.setBugReport(parser.nextText());
293                                 parser.require(XmlPullParser.END_TAG, NS, BUGREPORT_TAG);
294                             } else if (parser.getName().equals(LOGCAT_TAG)) {
295                                 test.setLog(parser.nextText());
296                                 parser.require(XmlPullParser.END_TAG, NS, LOGCAT_TAG);
297                             } else if (parser.getName().equals(SCREENSHOT_TAG)) {
298                                 test.setScreenshot(parser.nextText());
299                                 parser.require(XmlPullParser.END_TAG, NS, SCREENSHOT_TAG);
300                             } else if (SUMMARY_TAG.equals(parser.getName())) {
301                                 test.setReportLog(ReportLog.parse(parser));
302                             } else if (METRIC_TAG.equals(parser.getName())) {
303                                 // Ignore the new format in the old parser.
304                                 parser.nextText();
305                                 parser.require(XmlPullParser.END_TAG, NS, METRIC_TAG);
306                             } else if (RUN_HISTORY_TAG.equals(parser.getName())) {
307                                 // Ignore the test result history since it only exists in
308                                 // CTS Verifier, which will not use parsing feature.
309                                 skipCurrentTag(parser);
310                             } else {
311                                 parser.nextTag();
312                             }
313                         }
314                         parser.require(XmlPullParser.END_TAG, NS, TEST_TAG);
315                         // If the fingerprint was altered, then checksum against the fingerprint
316                         // originally reported
317                         Boolean checksumMismatch = invocationUseChecksum &&
318                              !checksumReporter.containsTestResult(test, module, reportFingerprint)
319                              && (fingerprintWasAltered ? !checksumReporter.containsTestResult(
320                                  test, module, unalteredFingerprint) : true);
321                         if (checksumMismatch) {
322                             test.removeResult();
323                         }
324                     }
325                     parser.require(XmlPullParser.END_TAG, NS, CASE_TAG);
326                 }
327                 parser.require(XmlPullParser.END_TAG, NS, MODULE_TAG);
328                 // If the fingerprint was altered, then checksum against the fingerprint
329                 // originally reported
330                 Boolean checksumMismatch = invocationUseChecksum &&
331                      !checksumReporter.containsModuleResult(module, reportFingerprint) &&
332                      (fingerprintWasAltered ? !checksumReporter.containsModuleResult(
333                          module, unalteredFingerprint) : true);
334                 if (checksumMismatch) {
335                     module.initializeDone(false);
336                 }
337             }
338             parser.require(XmlPullParser.END_TAG, NS, RESULT_TAG);
339             return invocation;
340         } catch (XmlPullParserException | IOException e) {
341             System.out.println(
342                     String.format("Exception when trying to load %s",
343                             resultFile.getAbsolutePath()));
344             e.printStackTrace();
345             return null;
346         }
347     }
348 
349     /** Parse and replay all run history information. */
parseRunHistory(XmlPullParser parser)350     private static void parseRunHistory(XmlPullParser parser)
351             throws IOException, XmlPullParserException {
352         while (parser.nextTag() == XmlPullParser.START_TAG) {
353             parser.require(XmlPullParser.START_TAG, NS, RUN_TAG);
354             parser.nextTag();
355             parser.require(XmlPullParser.END_TAG, NS, RUN_TAG);
356         }
357         parser.require(XmlPullParser.END_TAG, NS, RUN_HISTORY_TAG);
358         parser.nextTag();
359     }
360 
361     /** Skip the current XML tags. */
skipCurrentTag(XmlPullParser parser)362     private static void skipCurrentTag(XmlPullParser parser)
363             throws XmlPullParserException, IOException {
364         int depth = 1;
365         while (depth != 0) {
366             switch (parser.next()) {
367                 case XmlPullParser.END_TAG:
368                     depth--;
369                     break;
370                 case XmlPullParser.START_TAG:
371                     depth++;
372                     break;
373             }
374         }
375     }
376 
377     /**
378      * @param result
379      * @param resultDir
380      * @param startTime
381      * @param referenceUrl A nullable string that can contain a URL to a related data
382      * @param logUrl A nullable string that can contain a URL to related log files
383      * @param commandLineArgs A string containing the arguments to the run command
384      * @param resultAttributes Extra key-value pairs to be added as attributes and corresponding
385      *     values into the result XML file
386      * @return The result file created.
387      * @throws IOException
388      * @throws XmlPullParserException
389      */
writeResults( String suiteName, String suiteVersion, String suitePlan, String suiteBuild, IInvocationResult result, File resultDir, long startTime, long endTime, String referenceUrl, String logUrl, String commandLineArgs, Map<String, String> resultAttributes)390     public static File writeResults(
391             String suiteName,
392             String suiteVersion,
393             String suitePlan,
394             String suiteBuild,
395             IInvocationResult result,
396             File resultDir,
397             long startTime,
398             long endTime,
399             String referenceUrl,
400             String logUrl,
401             String commandLineArgs,
402             Map<String, String> resultAttributes)
403             throws IOException, XmlPullParserException {
404         int passed = result.countResults(TestStatus.PASS);
405         int failed = result.countResults(TestStatus.FAIL);
406         File resultFile = new File(resultDir, TEST_RESULT_FILE_NAME);
407         OutputStream stream = new FileOutputStream(resultFile);
408         XmlSerializer serializer = XmlPullParserFactory.newInstance(TYPE, null).newSerializer();
409         serializer.setOutput(stream, ENCODING);
410         serializer.startDocument(ENCODING, false);
411         serializer.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
412         serializer.processingInstruction(
413                 "xml-stylesheet type=\"text/xsl\" href=\"compatibility_result.xsl\"");
414         serializer.startTag(NS, RESULT_TAG);
415         serializer.attribute(NS, START_TIME_ATTR, String.valueOf(startTime));
416         serializer.attribute(NS, END_TIME_ATTR, String.valueOf(endTime));
417         serializer.attribute(NS, START_DISPLAY_TIME_ATTR, toReadableDateString(startTime));
418         serializer.attribute(NS, END_DISPLAY_TIME_ATTR, toReadableDateString(endTime));
419 
420         serializer.attribute(NS, SUITE_NAME_ATTR, suiteName);
421         serializer.attribute(NS, SUITE_VERSION_ATTR, suiteVersion);
422         serializer.attribute(NS, SUITE_PLAN_ATTR, suitePlan);
423         serializer.attribute(NS, SUITE_BUILD_ATTR, suiteBuild);
424         serializer.attribute(NS, REPORT_VERSION_ATTR, RESULT_FILE_VERSION);
425         serializer.attribute(NS, COMMAND_LINE_ARGS, nullToEmpty(commandLineArgs));
426 
427         if (resultAttributes != null) {
428             for (Entry<String, String> entry : resultAttributes.entrySet()) {
429                 serializer.attribute(NS, entry.getKey(), entry.getValue());
430             }
431         }
432 
433         if (referenceUrl != null) {
434             serializer.attribute(NS, REFERENCE_URL_ATTR, referenceUrl);
435         }
436 
437         if (logUrl != null) {
438             serializer.attribute(NS, LOG_URL_ATTR, logUrl);
439         }
440 
441         // Device Info
442         Set<String> devices = result.getDeviceSerials();
443         StringBuilder deviceList = new StringBuilder();
444         boolean first = true;
445         for (String device : devices) {
446             if (first) {
447                 first = false;
448             } else {
449                 deviceList.append(",");
450             }
451             deviceList.append(device);
452         }
453         serializer.attribute(NS, DEVICES_ATTR, deviceList.toString());
454 
455         // Host Info
456         String hostName = "";
457         try {
458             hostName = InetAddress.getLocalHost().getHostName();
459         } catch (UnknownHostException ignored) {}
460         serializer.attribute(NS, HOST_NAME_ATTR, hostName);
461         serializer.attribute(NS, OS_NAME_ATTR, System.getProperty("os.name"));
462         serializer.attribute(NS, OS_VERSION_ATTR, System.getProperty("os.version"));
463         serializer.attribute(NS, OS_ARCH_ATTR, System.getProperty("os.arch"));
464         serializer.attribute(NS, JAVA_VENDOR_ATTR, System.getProperty("java.vendor"));
465         serializer.attribute(NS, JAVA_VERSION_ATTR, System.getProperty("java.version"));
466 
467         // Build Info
468         serializer.startTag(NS, BUILD_TAG);
469         for (Entry<String, String> entry : result.getInvocationInfo().entrySet()) {
470             serializer.attribute(NS, entry.getKey(), entry.getValue());
471             if (Strings.isNullOrEmpty(result.getBuildFingerprint()) &&
472                 entry.getKey().equals(BUILD_FINGERPRINT)) {
473                 result.setBuildFingerprint(entry.getValue());
474             }
475         }
476         serializer.endTag(NS, BUILD_TAG);
477 
478         // Run history - this contains a list of start and end times of previous runs. More
479         // information may be added in the future.
480         Collection<InvocationResult.RunHistory> runHistories =
481                 ((InvocationResult) result).getRunHistories();
482         if (!runHistories.isEmpty()) {
483             serializer.startTag(NS, RUN_HISTORY_TAG);
484             for (InvocationResult.RunHistory runHistory : runHistories) {
485                 serializer.startTag(NS, RUN_TAG);
486                 serializer.attribute(NS, START_TIME_ATTR, String.valueOf(runHistory.startTime));
487                 serializer.attribute(NS, END_TIME_ATTR, String.valueOf(runHistory.endTime));
488                 serializer.endTag(NS, RUN_TAG);
489             }
490             serializer.endTag(NS, RUN_HISTORY_TAG);
491         }
492 
493         // Summary
494         serializer.startTag(NS, SUMMARY_TAG);
495         serializer.attribute(NS, PASS_ATTR, Integer.toString(passed));
496         serializer.attribute(NS, FAILED_ATTR, Integer.toString(failed));
497         serializer.attribute(NS, MODULES_DONE_ATTR,
498                 Integer.toString(result.getModuleCompleteCount()));
499         serializer.attribute(NS, MODULES_TOTAL_ATTR,
500                 Integer.toString(result.getModules().size()));
501         serializer.endTag(NS, SUMMARY_TAG);
502 
503         // Results
504         for (IModuleResult module : result.getModules()) {
505             serializer.startTag(NS, MODULE_TAG);
506             serializer.attribute(NS, NAME_ATTR, module.getName());
507             serializer.attribute(NS, ABI_ATTR, module.getAbi());
508             serializer.attribute(NS, RUNTIME_ATTR, String.valueOf(module.getRuntime()));
509             serializer.attribute(NS, DONE_ATTR, Boolean.toString(module.isDone()));
510             serializer.attribute(NS, PASS_ATTR,
511                     Integer.toString(module.countResults(TestStatus.PASS)));
512             for (ICaseResult cr : module.getResults()) {
513                 serializer.startTag(NS, CASE_TAG);
514                 serializer.attribute(NS, NAME_ATTR, cr.getName());
515                 for (ITestResult r : cr.getResults()) {
516                     TestStatus status = r.getResultStatus();
517                     if (status == null) {
518                         continue; // test was not executed, don't report
519                     }
520                     serializer.startTag(NS, TEST_TAG);
521                     serializer.attribute(NS, RESULT_ATTR, status.getValue());
522                     serializer.attribute(NS, NAME_ATTR, r.getName());
523                     if (r.isSkipped()) {
524                         serializer.attribute(NS, SKIPPED_ATTR, Boolean.toString(true));
525                     }
526                     String message = r.getMessage();
527                     if (message != null) {
528                         serializer.startTag(NS, FAILURE_TAG);
529                         serializer.attribute(NS, MESSAGE_ATTR, message);
530                         String stackTrace = r.getStackTrace();
531                         if (stackTrace != null) {
532                             serializer.startTag(NS, STACK_TAG);
533                             serializer.text(stackTrace);
534                             serializer.endTag(NS, STACK_TAG);
535                         }
536                         serializer.endTag(NS, FAILURE_TAG);
537                     }
538                     String bugreport = r.getBugReport();
539                     if (bugreport != null) {
540                         serializer.startTag(NS, BUGREPORT_TAG);
541                         serializer.text(bugreport);
542                         serializer.endTag(NS, BUGREPORT_TAG);
543                     }
544                     String logcat = r.getLog();
545                     if (logcat != null) {
546                         serializer.startTag(NS, LOGCAT_TAG);
547                         serializer.text(logcat);
548                         serializer.endTag(NS, LOGCAT_TAG);
549                     }
550                     String screenshot = r.getScreenshot();
551                     if (screenshot != null) {
552                         serializer.startTag(NS, SCREENSHOT_TAG);
553                         serializer.text(screenshot);
554                         serializer.endTag(NS, SCREENSHOT_TAG);
555                     }
556                     ReportLog report = r.getReportLog();
557                     if (report != null) {
558                         ReportLog.serialize(serializer, report);
559                     }
560 
561                     // Test result history contains a list of execution time for each test item.
562                     List<TestResultHistory> testResultHistories = r.getTestResultHistories();
563                     if (testResultHistories != null) {
564                         for (TestResultHistory resultHistory : testResultHistories) {
565                             TestResultHistory.serialize(serializer, resultHistory, r.getName());
566                         }
567                     }
568 
569                     serializer.endTag(NS, TEST_TAG);
570                 }
571                 serializer.endTag(NS, CASE_TAG);
572             }
573             serializer.endTag(NS, MODULE_TAG);
574         }
575         serializer.endDocument();
576         createChecksum(resultDir, result);
577         return resultFile;
578     }
579 
580     /**
581      * Generate html report listing an failed tests
582      */
createFailureReport(File inputXml)583     public static File createFailureReport(File inputXml) {
584         File failureReport = new File(inputXml.getParentFile(), FAILURE_REPORT_NAME);
585         try (InputStream xslStream = ResultHandler.class.getResourceAsStream(
586                 String.format("/report/%s", FAILURE_XSL_FILE_NAME));
587              OutputStream outputStream = new FileOutputStream(failureReport)) {
588 
589             Transformer transformer = TransformerFactory.newInstance().newTransformer(
590                     new StreamSource(xslStream));
591             transformer.transform(new StreamSource(inputXml), new StreamResult(outputStream));
592         } catch (IOException | TransformerException ignored) { }
593         return failureReport;
594     }
595 
createChecksum(File resultDir, IInvocationResult invocationResult)596     private static void createChecksum(File resultDir, IInvocationResult invocationResult) {
597         RetryChecksumStatus retryStatus = invocationResult.getRetryChecksumStatus();
598         switch (retryStatus) {
599             case NotRetry: case RetryWithChecksum:
600                 // Do not disrupt the process if there is a problem generating checksum.
601                 boolean unused = ChecksumReporter.tryCreateChecksum(resultDir, invocationResult);
602                 break;
603             case RetryWithoutChecksum:
604                 // If the previous run has an invalid checksum file,
605                 // copy it into current results folder for future troubleshooting
606                 File retryDirectory = invocationResult.getRetryDirectory();
607                 Path retryChecksum = FileSystems.getDefault().getPath(
608                         retryDirectory.getAbsolutePath(), ChecksumReporter.NAME);
609                 if (!retryChecksum.toFile().exists()) {
610                     // if no checksum file, check for a copy from a previous retry
611                     retryChecksum = FileSystems.getDefault().getPath(
612                             retryDirectory.getAbsolutePath(), ChecksumReporter.PREV_NAME);
613                 }
614 
615                 if (retryChecksum.toFile().exists()) {
616                     File checksumCopy = new File(resultDir, ChecksumReporter.PREV_NAME);
617                     try (FileOutputStream stream = new FileOutputStream(checksumCopy)) {
618                         Files.copy(retryChecksum, stream);
619                     } catch (IOException e) {
620                         // Do not disrupt the process if there is a problem copying checksum
621                     }
622                 }
623         }
624     }
625 
626 
627     /**
628      * Find the IInvocationResult for the given sessionId.
629      */
findResult(File resultsDir, Integer sessionId)630     public static IInvocationResult findResult(File resultsDir, Integer sessionId) {
631         return findResult(resultsDir, sessionId, true);
632     }
633 
634     /**
635      * Find the IInvocationResult for the given sessionId.
636      */
findResult( File resultsDir, Integer sessionId, Boolean useChecksum)637     private static IInvocationResult findResult(
638             File resultsDir, Integer sessionId, Boolean useChecksum) {
639         if (sessionId < 0) {
640             throw new IllegalArgumentException(
641                 String.format("Invalid session id [%d] ", sessionId));
642         }
643         File resultDir = getResultDirectory(resultsDir, sessionId);
644         IInvocationResult result = getResultFromDir(resultDir, useChecksum);
645         if (result == null) {
646             throw new RuntimeException(String.format("Could not find session [%d]", sessionId));
647         }
648         return result;
649     }
650 
651     /**
652      * Get the result directory for the given sessionId.
653      */
getResultDirectory(File resultsDir, Integer sessionId)654     public static File getResultDirectory(File resultsDir, Integer sessionId) {
655         if (sessionId < 0) {
656             throw new IllegalArgumentException(
657                 String.format("Invalid session id [%d] ", sessionId));
658         }
659         List<File> allResultDirs = getResultDirectories(resultsDir);
660         if (sessionId >= allResultDirs.size()) {
661             throw new IllegalArgumentException(String.format("Invalid session id [%d], results " +
662                     "directory (%s) contains only %d results",
663                     sessionId, resultsDir.getAbsolutePath(), allResultDirs.size()));
664         }
665         return allResultDirs.get(sessionId);
666     }
667 
668     /**
669      * Get a list of child directories that contain test invocation results
670      * @param resultsDir the root test result directory
671      * @return the list of {@link File} results directory.
672      */
getResultDirectories(File resultsDir)673     public static List<File> getResultDirectories(File resultsDir) {
674         List<File> directoryList = new ArrayList<>();
675         File[] files = resultsDir.listFiles();
676         if (files == null || files.length == 0) {
677             // No results, just return the empty list
678             return directoryList;
679         }
680         for (File resultDir : files) {
681             if (!resultDir.isDirectory()) {
682                 continue;
683             }
684             // Only include if it contain results file
685             File resultFile = new File(resultDir, TEST_RESULT_FILE_NAME);
686             if (!resultFile.exists()) {
687                 continue;
688             }
689             directoryList.add(resultDir);
690         }
691         Collections.sort(directoryList, (d1, d2) -> d1.getName().compareTo(d2.getName()));
692         return directoryList;
693     }
694 
695     /**
696      * Return the given time as a {@link String} suitable for displaying.
697      * <p/>
698      * Example: Fri Aug 20 15:13:03 PDT 2010
699      *
700      * @param time the epoch time in ms since midnight Jan 1, 1970
701      */
toReadableDateString(long time)702     static String toReadableDateString(long time) {
703     SimpleDateFormat dateFormat =
704         new SimpleDateFormat("EEE MMM dd HH:mm:ss zzz yyyy", Locale.ENGLISH);
705         return dateFormat.format(new Date(time));
706     }
707 
708     /**
709      * When nullable is null, return an empty string. Otherwise, return the value in nullable.
710      */
nullToEmpty(String nullable)711     private static String nullToEmpty(String nullable) {
712         return nullable == null ? "" : nullable;
713     }
714 }
715