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 */ 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 */ 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 */ 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 */ 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 */ 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 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 573 private static String nullToEmpty(String nullable) { 574 return nullable == null ? "" : nullable; 575 } 576 } 577