1 /* 2 * Copyright (C) 2018 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.android.compatibility.common.tradefed.result.suite; 17 18 import com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper; 19 import com.android.compatibility.common.util.DeviceInfo; 20 import com.android.compatibility.common.util.ResultHandler; 21 import com.android.compatibility.common.util.ResultUploader; 22 import com.android.tradefed.build.IBuildInfo; 23 import com.android.tradefed.config.IConfiguration; 24 import com.android.tradefed.config.IConfigurationReceiver; 25 import com.android.tradefed.config.Option; 26 import com.android.tradefed.config.OptionClass; 27 import com.android.tradefed.invoker.IInvocationContext; 28 import com.android.tradefed.log.LogUtil.CLog; 29 import com.android.tradefed.result.FileInputStreamSource; 30 import com.android.tradefed.result.ILogSaver; 31 import com.android.tradefed.result.ITestInvocationListener; 32 import com.android.tradefed.result.ITestSummaryListener; 33 import com.android.tradefed.result.InputStreamSource; 34 import com.android.tradefed.result.LogDataType; 35 import com.android.tradefed.result.LogFile; 36 import com.android.tradefed.result.LogFileSaver; 37 import com.android.tradefed.result.TestRunResult; 38 import com.android.tradefed.result.TestSummary; 39 import com.android.tradefed.result.suite.IFormatterGenerator; 40 import com.android.tradefed.result.suite.SuiteResultReporter; 41 import com.android.tradefed.result.suite.XmlFormattedGeneratorReporter; 42 import com.android.tradefed.util.FileUtil; 43 import com.android.tradefed.util.StreamUtil; 44 import com.android.tradefed.util.ZipUtil; 45 46 import java.io.File; 47 import java.io.FileInputStream; 48 import java.io.FileNotFoundException; 49 import java.io.FileOutputStream; 50 import java.io.IOException; 51 import java.io.InputStream; 52 import java.io.OutputStream; 53 import java.nio.file.Files; 54 import java.nio.file.Path; 55 import java.util.Collection; 56 import java.util.HashSet; 57 import java.util.LinkedHashMap; 58 import java.util.List; 59 import java.util.Map; 60 import java.util.Set; 61 62 import javax.xml.transform.Transformer; 63 import javax.xml.transform.TransformerException; 64 import javax.xml.transform.TransformerFactory; 65 import javax.xml.transform.stream.StreamResult; 66 import javax.xml.transform.stream.StreamSource; 67 68 /** 69 * Extension of {@link XmlFormattedGeneratorReporter} and {@link SuiteResultReporter} to handle 70 * Compatibility specific format and operations. 71 */ 72 @OptionClass(alias = "result-reporter") 73 public class CertificationSuiteResultReporter extends XmlFormattedGeneratorReporter 74 implements IConfigurationReceiver, ITestSummaryListener { 75 76 public static final String LATEST_LINK_NAME = "latest"; 77 public static final String SUMMARY_FILE = "invocation_summary.txt"; 78 public static final String FAILURE_REPORT_NAME = "test_result_failures_suite.html"; 79 public static final String FAILURE_XSL_FILE_NAME = "compatibility_failures.xsl"; 80 81 public static final String BUILD_FINGERPRINT = "build_fingerprint"; 82 83 @Option(name = "result-server", description = "Server to publish test results.") 84 private String mResultServer; 85 86 @Option( 87 name = "disable-result-posting", 88 description ="Disable result posting into report server." 89 ) 90 private boolean mDisableResultPosting = false; 91 92 @Option(name = "include-test-log-tags", description = "Include test log tags in report.") 93 private boolean mIncludeTestLogTags = false; 94 95 @Option(name = "use-log-saver", description = "Also saves generated result with log saver") 96 private boolean mUseLogSaver = false; 97 98 @Option(name = "compress-logs", description = "Whether logs will be saved with compression") 99 private boolean mCompressLogs = true; 100 101 public static final String INCLUDE_HTML_IN_ZIP = "html-in-zip"; 102 @Option(name = INCLUDE_HTML_IN_ZIP, 103 description = "Whether failure summary report is included in the zip fie.") 104 private boolean mIncludeHtml = false; 105 106 private CompatibilityBuildHelper mBuildHelper; 107 108 /** The directory containing the results */ 109 private File mResultDir = null; 110 /** The directory containing the logs */ 111 private File mLogDir = null; 112 113 private ResultUploader mUploader; 114 115 private LogFileSaver mTestLogSaver; 116 /** Invocation level Log saver to receive when files are logged */ 117 private ILogSaver mLogSaver; 118 /** Invocation level configuration */ 119 private IConfiguration mConfiguration = null; 120 121 private String mReferenceUrl; 122 123 private Map<String, String> mLoggedFiles; 124 125 private static final String[] RESULT_RESOURCES = { 126 "compatibility_result.css", 127 "compatibility_result.xsd", 128 "compatibility_result.xsl", 129 "logo.png" 130 }; 131 CertificationSuiteResultReporter()132 public CertificationSuiteResultReporter() { 133 super(); 134 mLoggedFiles = new LinkedHashMap<>(); 135 } 136 137 /** 138 * {@inheritDoc} 139 */ 140 @Override invocationStarted(IInvocationContext context)141 public final void invocationStarted(IInvocationContext context) { 142 super.invocationStarted(context); 143 144 if (mBuildHelper == null) { 145 mBuildHelper = new CompatibilityBuildHelper(getPrimaryBuildInfo()); 146 } 147 if (mResultDir == null) { 148 initializeResultDirectories(); 149 } 150 } 151 152 /** 153 * {@inheritDoc} 154 */ 155 @Override testLog(String name, LogDataType type, InputStreamSource stream)156 public void testLog(String name, LogDataType type, InputStreamSource stream) { 157 if (name.endsWith(DeviceInfo.FILE_SUFFIX)) { 158 // Handle device info file case 159 testLogDeviceInfo(name, stream); 160 return; 161 } 162 try { 163 File logFile = null; 164 if (mCompressLogs) { 165 try (InputStream inputStream = stream.createInputStream()) { 166 logFile = mTestLogSaver.saveAndGZipLogData(name, type, inputStream); 167 } 168 } else { 169 try (InputStream inputStream = stream.createInputStream()) { 170 logFile = mTestLogSaver.saveLogData(name, type, inputStream); 171 } 172 } 173 CLog.d("Saved logs for %s in %s", name, logFile.getAbsolutePath()); 174 } catch (IOException e) { 175 CLog.e("Failed to write log for %s", name); 176 CLog.e(e); 177 } 178 } 179 180 /** Write device-info files to the result, invoked only by the master result reporter */ testLogDeviceInfo(String name, InputStreamSource stream)181 private void testLogDeviceInfo(String name, InputStreamSource stream) { 182 try { 183 File ediDir = new File(mResultDir, DeviceInfo.RESULT_DIR_NAME); 184 ediDir.mkdirs(); 185 File ediFile = new File(ediDir, name); 186 if (!ediFile.exists()) { 187 // only write this file to the results if not already present 188 FileUtil.writeToFile(stream.createInputStream(), ediFile); 189 } 190 } catch (IOException e) { 191 CLog.w("Failed to write device info %s to result", name); 192 CLog.e(e); 193 } 194 } 195 196 /** 197 * {@inheritDoc} 198 */ 199 @Override testLogSaved(String dataName, LogDataType dataType, InputStreamSource dataStream, LogFile logFile)200 public void testLogSaved(String dataName, LogDataType dataType, InputStreamSource dataStream, 201 LogFile logFile) { 202 if (mIncludeTestLogTags) { 203 switch (dataType) { 204 case BUGREPORT: 205 case LOGCAT: 206 case PNG: 207 mLoggedFiles.put(dataName, logFile.getUrl()); 208 break; 209 default: 210 // Do nothing 211 break; 212 } 213 } 214 } 215 216 /** 217 * {@inheritDoc} 218 */ 219 @Override putSummary(List<TestSummary> summaries)220 public void putSummary(List<TestSummary> summaries) { 221 for (TestSummary summary : summaries) { 222 if (mReferenceUrl == null && summary.getSummary().getString() != null) { 223 mReferenceUrl = summary.getSummary().getString(); 224 } 225 } 226 } 227 228 /** 229 * {@inheritDoc} 230 */ 231 @Override setLogSaver(ILogSaver saver)232 public void setLogSaver(ILogSaver saver) { 233 mLogSaver = saver; 234 } 235 236 /** {@inheritDoc} */ 237 @Override setConfiguration(IConfiguration configuration)238 public void setConfiguration(IConfiguration configuration) { 239 mConfiguration = configuration; 240 } 241 242 /** 243 * Create directory structure where results and logs will be written. 244 */ initializeResultDirectories()245 private void initializeResultDirectories() { 246 CLog.d("Initializing result directory"); 247 // TODO: Clean up start time handling to avoid relying on buildinfo 248 getPrimaryBuildInfo().addBuildAttribute(CompatibilityBuildHelper.START_TIME_MS, 249 Long.toString(getStartTime())); 250 try { 251 mResultDir = mBuildHelper.getResultDir(); 252 if (mResultDir != null) { 253 mResultDir.mkdirs(); 254 } 255 } catch (FileNotFoundException e) { 256 throw new RuntimeException(e); 257 } 258 259 if (mResultDir == null) { 260 throw new RuntimeException("Result Directory was not created"); 261 } 262 if (!mResultDir.exists()) { 263 throw new RuntimeException("Result Directory was not created: " + 264 mResultDir.getAbsolutePath()); 265 } 266 267 CLog.d("Results Directory: %s", mResultDir.getAbsolutePath()); 268 269 mUploader = new ResultUploader(mResultServer, mBuildHelper.getSuiteName()); 270 try { 271 mLogDir = new File(mBuildHelper.getLogsDir(), 272 CompatibilityBuildHelper.getDirSuffix(getStartTime())); 273 } catch (FileNotFoundException e) { 274 CLog.e(e); 275 } 276 if (mLogDir != null && mLogDir.mkdirs()) { 277 CLog.d("Created log dir %s", mLogDir.getAbsolutePath()); 278 } 279 if (mLogDir == null || !mLogDir.exists()) { 280 throw new IllegalArgumentException(String.format("Could not create log dir %s", 281 mLogDir.getAbsolutePath())); 282 } 283 if (mTestLogSaver == null) { 284 mTestLogSaver = new LogFileSaver(mLogDir); 285 } 286 } 287 288 @Override createFormatter()289 public IFormatterGenerator createFormatter() { 290 return new CertificationResultXml(mBuildHelper.getSuiteName(), 291 mBuildHelper.getSuiteVersion(), 292 mBuildHelper.getSuitePlan(), 293 mBuildHelper.getSuiteBuild(), 294 mReferenceUrl, 295 getLogUrl()); 296 } 297 298 @Override preFormattingSetup(IFormatterGenerator formater)299 public void preFormattingSetup(IFormatterGenerator formater) { 300 super.preFormattingSetup(formater); 301 // Log the summary 302 TestSummary summary = getSummary(); 303 try { 304 File summaryFile = new File(mResultDir, SUMMARY_FILE); 305 FileUtil.writeToFile(summary.getSummary().toString(), summaryFile); 306 } catch (IOException e) { 307 CLog.e("Failed to save the summary."); 308 CLog.e(e); 309 } 310 311 copyDynamicConfigFiles(); 312 copyFormattingFiles(mResultDir, mBuildHelper.getSuiteName()); 313 } 314 315 @Override createResultDir()316 public File createResultDir() throws IOException { 317 return mResultDir; 318 } 319 320 @Override postFormattingStep(File resultDir, File reportFile)321 public void postFormattingStep(File resultDir, File reportFile) { 322 super.postFormattingStep(resultDir,reportFile); 323 324 createChecksum(resultDir, getRunResults(), 325 getPrimaryBuildInfo().getBuildAttributes().get(BUILD_FINGERPRINT)); 326 327 File failureReport = null; 328 if (mIncludeHtml) { 329 // Create the html report before the zip file. 330 failureReport = createFailureReport(reportFile); 331 } 332 File zippedResults = zipResults(mResultDir); 333 // TODO: calculate results checksum file 334 if (!mIncludeHtml) { 335 // Create failure report after zip file so extra data is not uploaded 336 failureReport = createFailureReport(reportFile); 337 } 338 try { 339 if (failureReport.exists()) { 340 CLog.i("Test Result: %s", failureReport.getCanonicalPath()); 341 } else { 342 CLog.i("Test Result: %s", reportFile.getCanonicalPath()); 343 } 344 Path latestLink = createLatestLinkDirectory(mResultDir.toPath()); 345 if (latestLink != null) { 346 CLog.i("Latest results link: " + latestLink.toAbsolutePath()); 347 } 348 349 latestLink = createLatestLinkDirectory(mLogDir.toPath()); 350 if (latestLink != null) { 351 CLog.i("Latest logs link: " + latestLink.toAbsolutePath()); 352 } 353 354 saveLog(reportFile, zippedResults); 355 } catch (IOException e) { 356 CLog.e("Error when handling the post processing of results file:"); 357 CLog.e(e); 358 } 359 360 uploadResult(reportFile); 361 } 362 363 /** 364 * Return the path in which log saver persists log files or null if 365 * logSaver is not enabled. 366 */ getLogUrl()367 private String getLogUrl() { 368 if (!mUseLogSaver || mLogSaver == null) { 369 return null; 370 } 371 372 return mLogSaver.getLogReportDir().getUrl(); 373 } 374 375 /** 376 * Update the "latest" symlink to the newest result directory. CTS specific. 377 */ createLatestLinkDirectory(Path directory)378 private Path createLatestLinkDirectory(Path directory) { 379 Path link = null; 380 381 Path parent = directory.getParent(); 382 383 if (parent != null) { 384 link = parent.resolve(LATEST_LINK_NAME); 385 try { 386 // if latest already exists, we have to remove it before creating 387 Files.deleteIfExists(link); 388 Files.createSymbolicLink(link, directory); 389 } catch (IOException ioe) { 390 CLog.e("Exception while attempting to create 'latest' link to: [%s]", 391 directory); 392 CLog.e(ioe); 393 return null; 394 } catch (UnsupportedOperationException uoe) { 395 CLog.e("Failed to create 'latest' symbolic link - unsupported operation"); 396 return null; 397 } 398 } 399 return link; 400 } 401 402 /** 403 * move the dynamic config files to the results directory 404 */ copyDynamicConfigFiles()405 private void copyDynamicConfigFiles() { 406 File configDir = new File(mResultDir, "config"); 407 if (!configDir.mkdir()) { 408 CLog.w("Failed to make dynamic config directory \"%s\" in the result", 409 configDir.getAbsolutePath()); 410 } 411 412 Set<String> uniqueModules = new HashSet<>(); 413 // Check each build of the invocation, in case of multi-device invocation. 414 for (IBuildInfo buildInfo : getInvocationContext().getBuildInfos()) { 415 CompatibilityBuildHelper helper = new CompatibilityBuildHelper(buildInfo); 416 Map<String, File> dcFiles = helper.getDynamicConfigFiles(); 417 for (String moduleName : dcFiles.keySet()) { 418 File srcFile = dcFiles.get(moduleName); 419 if (!uniqueModules.contains(moduleName)) { 420 // have not seen config for this module yet, copy into result 421 File destFile = new File(configDir, moduleName + ".dynamic"); 422 try { 423 FileUtil.copyFile(srcFile, destFile); 424 uniqueModules.add(moduleName); // Add to uniqueModules if copy succeeds 425 } catch (IOException e) { 426 CLog.w("Failure when copying config file \"%s\" to \"%s\" for module %s", 427 srcFile.getAbsolutePath(), destFile.getAbsolutePath(), moduleName); 428 CLog.e(e); 429 } 430 } 431 FileUtil.deleteFile(srcFile); 432 } 433 } 434 } 435 436 /** 437 * Copy the xml formatting files stored in this jar to the results directory. CTS specific. 438 * 439 * @param resultsDir 440 */ copyFormattingFiles(File resultsDir, String suiteName)441 private void copyFormattingFiles(File resultsDir, String suiteName) { 442 for (String resultFileName : RESULT_RESOURCES) { 443 InputStream configStream = CertificationResultXml.class.getResourceAsStream( 444 String.format("/report/%s-%s", suiteName, resultFileName)); 445 if (configStream == null) { 446 // If suite specific files are not available, fallback to common. 447 configStream = CertificationResultXml.class.getResourceAsStream( 448 String.format("/report/%s", resultFileName)); 449 } 450 if (configStream != null) { 451 File resultFile = new File(resultsDir, resultFileName); 452 try { 453 FileUtil.writeToFile(configStream, resultFile); 454 } catch (IOException e) { 455 CLog.w("Failed to write %s to file", resultFileName); 456 } 457 } else { 458 CLog.w("Failed to load %s from jar", resultFileName); 459 } 460 } 461 } 462 463 /** 464 * When enabled, save log data using log saver 465 */ saveLog(File resultFile, File zippedResults)466 private void saveLog(File resultFile, File zippedResults) throws IOException { 467 if (!mUseLogSaver) { 468 return; 469 } 470 471 FileInputStream fis = null; 472 LogFile logFile = null; 473 try { 474 fis = new FileInputStream(resultFile); 475 logFile = mLogSaver.saveLogData("log-result", LogDataType.XML, fis); 476 CLog.d("Result XML URL: %s", logFile.getUrl()); 477 logReportFiles(mConfiguration, resultFile, resultFile.getName(), LogDataType.XML); 478 } catch (IOException ioe) { 479 CLog.e("error saving XML with log saver"); 480 CLog.e(ioe); 481 } finally { 482 StreamUtil.close(fis); 483 } 484 // Save the full results folder. 485 if (zippedResults != null) { 486 FileInputStream zipResultStream = null; 487 try { 488 zipResultStream = new FileInputStream(zippedResults); 489 logFile = mLogSaver.saveLogData("results", LogDataType.ZIP, zipResultStream); 490 CLog.d("Result zip URL: %s", logFile.getUrl()); 491 logReportFiles(mConfiguration, zippedResults, "results", LogDataType.ZIP); 492 } finally { 493 StreamUtil.close(zipResultStream); 494 } 495 } 496 } 497 498 /** 499 * Zip the contents of the given results directory. CTS specific. 500 * 501 * @param resultsDir 502 */ zipResults(File resultsDir)503 private static File zipResults(File resultsDir) { 504 File zipResultFile = null; 505 try { 506 // create a file in parent directory, with same name as resultsDir 507 zipResultFile = new File(resultsDir.getParent(), String.format("%s.zip", 508 resultsDir.getName())); 509 ZipUtil.createZip(resultsDir, zipResultFile); 510 } catch (IOException e) { 511 CLog.w("Failed to create zip for %s", resultsDir.getName()); 512 } 513 return zipResultFile; 514 } 515 516 /** 517 * When enabled, upload the result to a server. CTS specific. 518 */ uploadResult(File resultFile)519 private void uploadResult(File resultFile) { 520 if (mResultServer != null && !mResultServer.trim().isEmpty() && !mDisableResultPosting) { 521 try { 522 CLog.d("Result Server: %d", mUploader.uploadResult(resultFile, mReferenceUrl)); 523 } catch (IOException ioe) { 524 CLog.e("IOException while uploading result."); 525 CLog.e(ioe); 526 } 527 } 528 } 529 530 /** 531 * Generate html report listing an failed tests. CTS specific. 532 */ createFailureReport(File inputXml)533 private File createFailureReport(File inputXml) { 534 File failureReport = new File(inputXml.getParentFile(), FAILURE_REPORT_NAME); 535 try (InputStream xslStream = ResultHandler.class.getResourceAsStream( 536 String.format("/report/%s", FAILURE_XSL_FILE_NAME)); 537 OutputStream outputStream = new FileOutputStream(failureReport)) { 538 539 Transformer transformer = TransformerFactory.newInstance().newTransformer( 540 new StreamSource(xslStream)); 541 transformer.transform(new StreamSource(inputXml), new StreamResult(outputStream)); 542 } catch (IOException | TransformerException ignored) { 543 CLog.e(ignored); 544 } 545 return failureReport; 546 } 547 548 /** 549 * Generates a checksum files based on the results. 550 */ createChecksum(File resultDir, Collection<TestRunResult> results, String buildFingerprint)551 private void createChecksum(File resultDir, Collection<TestRunResult> results, 552 String buildFingerprint) { 553 CertificationChecksumHelper.tryCreateChecksum(resultDir, results, buildFingerprint); 554 } 555 556 /** Re-log a result file to all reporters so they are aware of it. */ logReportFiles( IConfiguration configuration, File resultFile, String dataName, LogDataType type)557 private void logReportFiles( 558 IConfiguration configuration, File resultFile, String dataName, LogDataType type) { 559 if (configuration == null) { 560 return; 561 } 562 List<ITestInvocationListener> listeners = configuration.getTestInvocationListeners(); 563 try (FileInputStreamSource source = new FileInputStreamSource(resultFile)) { 564 for (ITestInvocationListener listener : listeners) { 565 if (listener.equals(this)) { 566 // Avoid logging agaisnt itself 567 continue; 568 } 569 listener.testLog(dataName, type, source); 570 } 571 } 572 } 573 } 574