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