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