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.annotations.VisibleForTesting; 19 import com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper; 20 import com.android.compatibility.common.util.DeviceInfo; 21 import com.android.tradefed.build.IBuildInfo; 22 import com.android.tradefed.config.IConfiguration; 23 import com.android.tradefed.config.Option; 24 import com.android.tradefed.config.OptionClass; 25 import com.android.tradefed.invoker.IInvocationContext; 26 import com.android.tradefed.invoker.ShardListener; 27 import com.android.tradefed.log.LogUtil.CLog; 28 import com.android.tradefed.result.ILogSaver; 29 import com.android.tradefed.result.ITestInvocationListener; 30 import com.android.tradefed.result.ITestSummaryListener; 31 import com.android.tradefed.result.InputStreamSource; 32 import com.android.tradefed.result.LogDataType; 33 import com.android.tradefed.result.LogFile; 34 import com.android.tradefed.result.LogFileSaver; 35 import com.android.tradefed.result.SnapshotInputStreamSource; 36 import com.android.tradefed.result.TestRunResult; 37 import com.android.tradefed.result.TestSummary; 38 import com.android.tradefed.result.suite.IFormatterGenerator; 39 import com.android.tradefed.result.suite.SuiteResultReporter; 40 import com.android.tradefed.result.suite.XmlFormattedGeneratorReporter; 41 import com.android.tradefed.util.FileUtil; 42 43 import java.io.File; 44 import java.io.FileNotFoundException; 45 import java.io.IOException; 46 import java.io.InputStream; 47 import java.nio.file.Files; 48 import java.nio.file.Path; 49 import java.util.Collection; 50 import java.util.HashMap; 51 import java.util.HashSet; 52 import java.util.LinkedHashMap; 53 import java.util.List; 54 import java.util.Map; 55 import java.util.Set; 56 57 /** 58 * Extension of {@link XmlFormattedGeneratorReporter} and {@link SuiteResultReporter} to handle 59 * Compatibility specific format and operations. 60 */ 61 @OptionClass(alias = "result-reporter") 62 public class CertificationSuiteResultReporter extends XmlFormattedGeneratorReporter 63 implements ITestSummaryListener { 64 65 // The known existing variant of suites. 66 // Adding a new variant requires approval from Android Partner team and Test Harness team. 67 private enum SuiteVariant { 68 CTS_ON_GSI("CTS_ON_GSI", "cts-on-gsi"); 69 70 private final String mReportDisplayName; 71 private final String mConfigName; 72 SuiteVariant(String reportName, String configName)73 private SuiteVariant(String reportName, String configName) { 74 mReportDisplayName = reportName; 75 mConfigName = configName; 76 } 77 getReportDisplayName()78 public String getReportDisplayName() { 79 return mReportDisplayName; 80 } 81 getConfigName()82 public String getConfigName() { 83 return mConfigName; 84 } 85 } 86 87 public static final String LATEST_LINK_NAME = "latest"; 88 public static final String SUMMARY_FILE = "invocation_summary.txt"; 89 90 public static final String BUILD_FINGERPRINT = "cts:build_fingerprint"; 91 92 @Option(name = "result-server", description = "Server to publish test results.") 93 @Deprecated 94 private String mResultServer; 95 96 @Option( 97 name = "disable-result-posting", 98 description = "Disable result posting into report server.") 99 @Deprecated 100 private boolean mDisableResultPosting = false; 101 102 @Option(name = "include-test-log-tags", description = "Include test log tags in report.") 103 private boolean mIncludeTestLogTags = false; 104 105 @Option(name = "use-log-saver", description = "Also saves generated result with log saver") 106 private boolean mUseLogSaver = false; 107 108 @Option(name = "compress-logs", description = "Whether logs will be saved with compression") 109 private boolean mCompressLogs = true; 110 111 public static final String INCLUDE_HTML_IN_ZIP = "html-in-zip"; 112 113 @Option( 114 name = INCLUDE_HTML_IN_ZIP, 115 description = "Whether failure summary report is included in the zip fie.") 116 @Deprecated 117 private boolean mIncludeHtml = false; 118 119 @Option( 120 name = "result-attribute", 121 description = 122 "Extra key-value pairs to be added as attributes and corresponding values " 123 + "of the \"Result\" tag in the result XML.") 124 private Map<String, String> mResultAttributes = new HashMap<String, String>(); 125 126 // Should be removed for the S release. 127 @Option( 128 name = "cts-on-gsi-variant", 129 description = 130 "Workaround for the R release to ensure the CTS-on-GSI report can be parsed " 131 + "by the APFE.") 132 private boolean mCtsOnGsiVariant = false; 133 134 private CompatibilityBuildHelper mBuildHelper; 135 136 /** The directory containing the results */ 137 private File mResultDir = null; 138 /** The directory containing the logs */ 139 private File mLogDir = null; 140 141 /** LogFileSaver to copy the file to the CTS results folder */ 142 private LogFileSaver mTestLogSaver; 143 144 private Map<LogFile, InputStreamSource> mPreInvocationLogs = new HashMap<>(); 145 /** Invocation level Log saver to receive when files are logged */ 146 private ILogSaver mLogSaver; 147 148 private String mReferenceUrl; 149 150 private Map<String, String> mLoggedFiles; 151 152 private static final String[] RESULT_RESOURCES = { 153 "compatibility_result.css", 154 "compatibility_result.xsl", 155 "logo.png" 156 }; 157 CertificationSuiteResultReporter()158 public CertificationSuiteResultReporter() { 159 super(); 160 mLoggedFiles = new LinkedHashMap<>(); 161 } 162 163 /** 164 * {@inheritDoc} 165 */ 166 @Override invocationStarted(IInvocationContext context)167 public final void invocationStarted(IInvocationContext context) { 168 super.invocationStarted(context); 169 170 if (mBuildHelper == null) { 171 mBuildHelper = createBuildHelper(); 172 } 173 if (mResultDir == null) { 174 initializeResultDirectories(); 175 } 176 } 177 178 @VisibleForTesting createBuildHelper()179 CompatibilityBuildHelper createBuildHelper() { 180 return new CompatibilityBuildHelper(getPrimaryBuildInfo()); 181 } 182 183 /** 184 * {@inheritDoc} 185 */ 186 @Override testLog(String name, LogDataType type, InputStreamSource stream)187 public void testLog(String name, LogDataType type, InputStreamSource stream) { 188 if (name.endsWith(DeviceInfo.FILE_SUFFIX)) { 189 // Handle device info file case 190 testLogDeviceInfo(name, stream); 191 return; 192 } 193 if (mTestLogSaver == null) { 194 LogFile info = new LogFile(name, null, type); 195 mPreInvocationLogs.put( 196 info, new SnapshotInputStreamSource(name, stream.createInputStream())); 197 return; 198 } 199 try { 200 File logFile = null; 201 if (mCompressLogs) { 202 try (InputStream inputStream = stream.createInputStream()) { 203 logFile = mTestLogSaver.saveAndGZipLogData(name, type, inputStream); 204 } 205 } else { 206 try (InputStream inputStream = stream.createInputStream()) { 207 logFile = mTestLogSaver.saveLogData(name, type, inputStream); 208 } 209 } 210 CLog.d("Saved logs for %s in %s", name, logFile.getAbsolutePath()); 211 } catch (IOException e) { 212 CLog.e("Failed to write log for %s", name); 213 CLog.e(e); 214 } 215 } 216 217 /** Write device-info files to the result */ testLogDeviceInfo(String name, InputStreamSource stream)218 private void testLogDeviceInfo(String name, InputStreamSource stream) { 219 try { 220 File ediDir = new File(mResultDir, DeviceInfo.RESULT_DIR_NAME); 221 ediDir.mkdirs(); 222 File ediFile = new File(ediDir, name); 223 if (!ediFile.exists()) { 224 // only write this file to the results if not already present 225 FileUtil.writeToFile(stream.createInputStream(), ediFile); 226 } 227 } catch (IOException e) { 228 CLog.w("Failed to write device info %s to result", name); 229 CLog.e(e); 230 } 231 } 232 233 /** 234 * {@inheritDoc} 235 */ 236 @Override testLogSaved(String dataName, LogDataType dataType, InputStreamSource dataStream, LogFile logFile)237 public void testLogSaved(String dataName, LogDataType dataType, InputStreamSource dataStream, 238 LogFile logFile) { 239 if (mIncludeTestLogTags) { 240 switch (dataType) { 241 case BUGREPORT: 242 case LOGCAT: 243 case PNG: 244 mLoggedFiles.put(dataName, logFile.getUrl()); 245 break; 246 default: 247 // Do nothing 248 break; 249 } 250 } 251 } 252 253 /** 254 * {@inheritDoc} 255 */ 256 @Override putSummary(List<TestSummary> summaries)257 public void putSummary(List<TestSummary> summaries) { 258 for (TestSummary summary : summaries) { 259 if (mReferenceUrl == null && summary.getSummary().getString() != null) { 260 mReferenceUrl = summary.getSummary().getString(); 261 } 262 } 263 } 264 265 /** 266 * {@inheritDoc} 267 */ 268 @Override setLogSaver(ILogSaver saver)269 public void setLogSaver(ILogSaver saver) { 270 mLogSaver = saver; 271 } 272 273 /** 274 * Create directory structure where results and logs will be written. 275 */ initializeResultDirectories()276 private void initializeResultDirectories() { 277 CLog.d("Initializing result directory"); 278 try { 279 mResultDir = mBuildHelper.getResultDir(); 280 if (mResultDir != null) { 281 mResultDir.mkdirs(); 282 } 283 } catch (FileNotFoundException e) { 284 throw new RuntimeException(e); 285 } 286 287 if (mResultDir == null) { 288 throw new RuntimeException("Result Directory was not created"); 289 } 290 if (!mResultDir.exists()) { 291 throw new RuntimeException("Result Directory was not created: " + 292 mResultDir.getAbsolutePath()); 293 } 294 295 CLog.d("Results Directory: %s", mResultDir.getAbsolutePath()); 296 297 try { 298 mLogDir = mBuildHelper.getInvocationLogDir(); 299 } catch (FileNotFoundException e) { 300 CLog.e(e); 301 } 302 if (mLogDir != null && mLogDir.mkdirs()) { 303 CLog.d("Created log dir %s", mLogDir.getAbsolutePath()); 304 } 305 if (mLogDir == null || !mLogDir.exists()) { 306 throw new IllegalArgumentException(String.format("Could not create log dir %s", 307 mLogDir.getAbsolutePath())); 308 } 309 // During sharding, we reach here before invocationStarted is called so the log_saver will 310 // be null at that point. 311 if (mTestLogSaver == null) { 312 mTestLogSaver = new LogFileSaver(mLogDir); 313 // Log all the early logs from before init. 314 for (LogFile earlyLog : mPreInvocationLogs.keySet()) { 315 try (InputStreamSource source = mPreInvocationLogs.get(earlyLog)) { 316 testLog(earlyLog.getPath(), earlyLog.getType(), source); 317 } 318 } 319 mPreInvocationLogs.clear(); 320 } 321 } 322 323 @Override createFormatter()324 public IFormatterGenerator createFormatter() { 325 return new CertificationResultXml( 326 mBuildHelper.getSuiteName(), 327 mBuildHelper.getSuiteVersion(), 328 createSuiteVariant(), 329 mBuildHelper.getSuitePlan(), 330 mBuildHelper.getSuiteBuild(), 331 mReferenceUrl, 332 getLogUrl(), 333 mResultAttributes); 334 } 335 336 @Override preFormattingSetup(IFormatterGenerator formater)337 public void preFormattingSetup(IFormatterGenerator formater) { 338 super.preFormattingSetup(formater); 339 // Log the summary 340 TestSummary summary = getSummary(); 341 try { 342 File summaryFile = new File(mResultDir, SUMMARY_FILE); 343 FileUtil.writeToFile(summary.getSummary().toString(), summaryFile); 344 } catch (IOException e) { 345 CLog.e("Failed to save the summary."); 346 CLog.e(e); 347 } 348 349 copyDynamicConfigFiles(); 350 copyFormattingFiles(mResultDir, mBuildHelper.getSuiteName()); 351 } 352 353 @Override createResultDir()354 public File createResultDir() throws IOException { 355 return mResultDir; 356 } 357 358 @Override postFormattingStep(File resultDir, File reportFile)359 public void postFormattingStep(File resultDir, File reportFile) { 360 super.postFormattingStep(resultDir,reportFile); 361 362 createChecksum( 363 resultDir, 364 getMergedTestRunResults(), 365 getPrimaryBuildInfo().getBuildAttributes().get(BUILD_FINGERPRINT)); 366 367 Path latestLink = createLatestLinkDirectory(mResultDir.toPath()); 368 if (latestLink != null) { 369 CLog.i("Latest results link: " + latestLink.toAbsolutePath()); 370 } 371 372 latestLink = createLatestLinkDirectory(mLogDir.toPath()); 373 if (latestLink != null) { 374 CLog.i("Latest logs link: " + latestLink.toAbsolutePath()); 375 } 376 377 for (ITestInvocationListener resultReporter : 378 getConfiguration().getTestInvocationListeners()) { 379 if (resultReporter instanceof CertificationReportCreator) { 380 ((CertificationReportCreator) resultReporter).setReportFile(reportFile); 381 } 382 if (resultReporter instanceof ShardListener) { 383 for (ITestInvocationListener subListener : ((ShardListener) resultReporter).getUnderlyingResultReporter()) { 384 if (subListener instanceof CertificationReportCreator) { 385 ((CertificationReportCreator) subListener).setReportFile(reportFile); 386 } 387 } 388 } 389 } 390 } 391 392 /** 393 * Return the path in which log saver persists log files or null if 394 * logSaver is not enabled. 395 */ getLogUrl()396 private String getLogUrl() { 397 if (!mUseLogSaver || mLogSaver == null) { 398 return null; 399 } 400 401 return mLogSaver.getLogReportDir().getUrl(); 402 } 403 404 /** 405 * Update the "latest" symlink to the newest result directory. CTS specific. 406 */ createLatestLinkDirectory(Path directory)407 private Path createLatestLinkDirectory(Path directory) { 408 Path link = null; 409 410 Path parent = directory.getParent(); 411 412 if (parent != null) { 413 link = parent.resolve(LATEST_LINK_NAME); 414 try { 415 // if latest already exists, we have to remove it before creating 416 Files.deleteIfExists(link); 417 Files.createSymbolicLink(link, directory); 418 } catch (IOException ioe) { 419 CLog.e("Exception while attempting to create 'latest' link to: [%s]", 420 directory); 421 CLog.e(ioe); 422 return null; 423 } catch (UnsupportedOperationException uoe) { 424 CLog.e("Failed to create 'latest' symbolic link - unsupported operation"); 425 return null; 426 } 427 } 428 return link; 429 } 430 431 /** 432 * move the dynamic config files to the results directory 433 */ copyDynamicConfigFiles()434 private void copyDynamicConfigFiles() { 435 File configDir = new File(mResultDir, "config"); 436 if (!configDir.exists() && !configDir.mkdir()) { 437 CLog.w( 438 "Failed to make dynamic config directory \"%s\" in the result.", 439 configDir.getAbsolutePath()); 440 } 441 442 Set<String> uniqueModules = new HashSet<>(); 443 // Check each build of the invocation, in case of multi-device invocation. 444 for (IBuildInfo buildInfo : getInvocationContext().getBuildInfos()) { 445 CompatibilityBuildHelper helper = new CompatibilityBuildHelper(buildInfo); 446 Map<String, File> dcFiles = helper.getDynamicConfigFiles(); 447 for (String moduleName : dcFiles.keySet()) { 448 File srcFile = dcFiles.get(moduleName); 449 if (!uniqueModules.contains(moduleName)) { 450 // have not seen config for this module yet, copy into result 451 File destFile = new File(configDir, moduleName + ".dynamic"); 452 if (destFile.exists()) { 453 continue; 454 } 455 try { 456 FileUtil.copyFile(srcFile, destFile); 457 uniqueModules.add(moduleName); // Add to uniqueModules if copy succeeds 458 } catch (IOException e) { 459 CLog.w("Failure when copying config file \"%s\" to \"%s\" for module %s", 460 srcFile.getAbsolutePath(), destFile.getAbsolutePath(), moduleName); 461 CLog.e(e); 462 } 463 } 464 FileUtil.deleteFile(srcFile); 465 } 466 } 467 } 468 469 /** 470 * Copy the xml formatting files stored in this jar to the results directory. CTS specific. 471 * 472 * @param resultsDir 473 */ copyFormattingFiles(File resultsDir, String suiteName)474 private void copyFormattingFiles(File resultsDir, String suiteName) { 475 for (String resultFileName : RESULT_RESOURCES) { 476 InputStream configStream = CertificationResultXml.class.getResourceAsStream( 477 String.format("/report/%s-%s", suiteName, resultFileName)); 478 if (configStream == null) { 479 // If suite specific files are not available, fallback to common. 480 configStream = CertificationResultXml.class.getResourceAsStream( 481 String.format("/report/%s", resultFileName)); 482 } 483 if (configStream != null) { 484 File resultFile = new File(resultsDir, resultFileName); 485 try { 486 FileUtil.writeToFile(configStream, resultFile); 487 } catch (IOException e) { 488 CLog.w("Failed to write %s to file", resultFileName); 489 } 490 } else { 491 CLog.w("Failed to load %s from jar", resultFileName); 492 } 493 } 494 } 495 496 /** 497 * Generates a checksum files based on the results. 498 */ createChecksum(File resultDir, Collection<TestRunResult> results, String buildFingerprint)499 private void createChecksum(File resultDir, Collection<TestRunResult> results, 500 String buildFingerprint) { 501 CertificationChecksumHelper.tryCreateChecksum(resultDir, results, buildFingerprint); 502 } 503 createSuiteVariant()504 private String createSuiteVariant() { 505 IConfiguration currentConfig = getConfiguration(); 506 String commandLine = currentConfig.getCommandLine(); 507 for (SuiteVariant var : SuiteVariant.values()) { 508 if (commandLine.startsWith(var.getConfigName() + " ") 509 || commandLine.equals(var.getConfigName())) { 510 return var.getReportDisplayName(); 511 } 512 } 513 return null; 514 } 515 } 516