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.tradefed.result; 17 18 import com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper; 19 import com.android.compatibility.common.tradefed.testtype.retry.RetryFactoryTest; 20 import com.android.compatibility.common.tradefed.testtype.suite.CompatibilityTestSuite; 21 import com.android.compatibility.common.tradefed.util.FingerprintComparisonException; 22 import com.android.compatibility.common.tradefed.util.RetryType; 23 import com.android.compatibility.common.util.ChecksumReporter; 24 import com.android.compatibility.common.util.DeviceInfo; 25 import com.android.compatibility.common.util.ICaseResult; 26 import com.android.compatibility.common.util.IInvocationResult; 27 import com.android.compatibility.common.util.IModuleResult; 28 import com.android.compatibility.common.util.ITestResult; 29 import com.android.compatibility.common.util.InvocationResult; 30 import com.android.compatibility.common.util.InvocationResult.RunHistory; 31 import com.android.compatibility.common.util.MetricsStore; 32 import com.android.compatibility.common.util.ReportLog; 33 import com.android.compatibility.common.util.ResultHandler; 34 import com.android.compatibility.common.util.ResultUploader; 35 import com.android.compatibility.common.util.TestStatus; 36 import com.android.ddmlib.Log.LogLevel; 37 import com.android.tradefed.build.IBuildInfo; 38 import com.android.tradefed.config.IConfiguration; 39 import com.android.tradefed.config.IConfigurationReceiver; 40 import com.android.tradefed.config.Option; 41 import com.android.tradefed.config.Option.Importance; 42 import com.android.tradefed.config.OptionClass; 43 import com.android.tradefed.config.OptionCopier; 44 import com.android.tradefed.invoker.IInvocationContext; 45 import com.android.tradefed.log.LogUtil.CLog; 46 import com.android.tradefed.metrics.proto.MetricMeasurement.Metric; 47 import com.android.tradefed.result.FileInputStreamSource; 48 import com.android.tradefed.result.ILogSaver; 49 import com.android.tradefed.result.ILogSaverListener; 50 import com.android.tradefed.result.IShardableListener; 51 import com.android.tradefed.result.ITestInvocationListener; 52 import com.android.tradefed.result.ITestSummaryListener; 53 import com.android.tradefed.result.InputStreamSource; 54 import com.android.tradefed.result.LogDataType; 55 import com.android.tradefed.result.LogFile; 56 import com.android.tradefed.result.LogFileSaver; 57 import com.android.tradefed.result.TestDescription; 58 import com.android.tradefed.result.TestSummary; 59 import com.android.tradefed.result.suite.SuiteResultReporter; 60 import com.android.tradefed.util.FileUtil; 61 import com.android.tradefed.util.StreamUtil; 62 import com.android.tradefed.util.TimeUtil; 63 import com.android.tradefed.util.ZipUtil; 64 import com.android.tradefed.util.proto.TfMetricProtoUtil; 65 66 import com.google.common.annotations.VisibleForTesting; 67 import com.google.common.xml.XmlEscapers; 68 import com.google.gson.Gson; 69 70 import org.xmlpull.v1.XmlPullParserException; 71 72 import java.io.File; 73 import java.io.FileInputStream; 74 import java.io.FileNotFoundException; 75 import java.io.IOException; 76 import java.io.InputStream; 77 import java.nio.file.Files; 78 import java.nio.file.Path; 79 import java.util.Arrays; 80 import java.util.Collection; 81 import java.util.Collections; 82 import java.util.HashMap; 83 import java.util.HashSet; 84 import java.util.List; 85 import java.util.Map; 86 import java.util.Set; 87 import java.util.concurrent.CountDownLatch; 88 import java.util.concurrent.TimeUnit; 89 90 /** 91 * Collect test results for an entire invocation and output test results to disk. 92 */ 93 @OptionClass(alias="result-reporter") 94 public class ResultReporter implements ILogSaverListener, ITestInvocationListener, 95 ITestSummaryListener, IShardableListener, IConfigurationReceiver { 96 97 public static final String INCLUDE_HTML_IN_ZIP = "html-in-zip"; 98 private static final String UNKNOWN_DEVICE = "unknown_device"; 99 private static final String RESULT_KEY = "COMPATIBILITY_TEST_RESULT"; 100 private static final String CTS_PREFIX = "cts:"; 101 private static final String BUILD_INFO = CTS_PREFIX + "build_"; 102 private static final String LATEST_LINK_NAME = "latest"; 103 /** Used to get run history from the test result of last run. */ 104 private static final String RUN_HISTORY_KEY = "run_history"; 105 106 107 public static final String BUILD_BRAND = "build_brand"; 108 public static final String BUILD_DEVICE = "build_device"; 109 public static final String BUILD_FINGERPRINT = "build_fingerprint"; 110 public static final String BUILD_ID = "build_id"; 111 public static final String BUILD_MANUFACTURER = "build_manufacturer"; 112 public static final String BUILD_MODEL = "build_model"; 113 public static final String BUILD_PRODUCT = "build_product"; 114 public static final String BUILD_VERSION_RELEASE = "build_version_release"; 115 116 private static final List<String> NOT_RETRY_FILES = Arrays.asList( 117 ChecksumReporter.NAME, 118 ChecksumReporter.PREV_NAME, 119 ResultHandler.FAILURE_REPORT_NAME, 120 "diffs"); 121 122 @Option(name = RetryFactoryTest.RETRY_OPTION, 123 shortName = 'r', 124 description = "retry a previous session.", 125 importance = Importance.IF_UNSET) 126 private Integer mRetrySessionId = null; 127 128 @Option(name = RetryFactoryTest.RETRY_TYPE_OPTION, 129 description = "used with " + RetryFactoryTest.RETRY_OPTION 130 + ", retry tests of a certain status. Possible values include \"failed\", " 131 + "\"not_executed\", and \"custom\".", 132 importance = Importance.IF_UNSET) 133 private RetryType mRetryType = null; 134 135 @Option(name = "result-server", description = "Server to publish test results.") 136 private String mResultServer; 137 138 @Option(name = "disable-result-posting", description = "Disable result posting into report server.") 139 private boolean mDisableResultPosting = false; 140 141 @Option(name = "include-test-log-tags", description = "Include test log tags in report.") 142 private boolean mIncludeTestLogTags = false; 143 144 @Option(name = "use-log-saver", description = "Also saves generated result with log saver") 145 private boolean mUseLogSaver = false; 146 147 @Option(name = "compress-logs", description = "Whether logs will be saved with compression") 148 private boolean mCompressLogs = true; 149 150 @Option(name = INCLUDE_HTML_IN_ZIP, 151 description = "Whether failure summary report is included in the zip fie.") 152 private boolean mIncludeHtml = false; 153 154 @Option( 155 name = "result-attribute", 156 description = 157 "Extra key-value pairs to be added as attributes and corresponding" 158 + "values of the \"Result\" tag in the result XML.") 159 private Map<String, String> mResultAttributes = new HashMap<String, String>(); 160 161 private CompatibilityBuildHelper mBuildHelper; 162 private File mResultDir = null; 163 private File mLogDir = null; 164 private ResultUploader mUploader; 165 private String mReferenceUrl; 166 private ILogSaver mLogSaver; 167 private int invocationEndedCount = 0; 168 private CountDownLatch mFinalized = null; 169 170 protected IInvocationResult mResult = new InvocationResult(); 171 private IModuleResult mCurrentModuleResult; 172 private ICaseResult mCurrentCaseResult; 173 private ITestResult mCurrentResult; 174 private String mDeviceSerial = UNKNOWN_DEVICE; 175 private Set<String> mMainDeviceSerials = new HashSet<>(); 176 private Set<IBuildInfo> mMainBuildInfos = new HashSet<>(); 177 // Whether or not we failed the fingerprint check 178 private boolean mFingerprintFailure = false; 179 180 // mCurrentTestNum and mTotalTestsInModule track the progress within the module 181 // Note that this count is not necessarily equal to the count of tests contained 182 // in mCurrentModuleResult because of how special cases like ignored tests are reported. 183 private int mCurrentTestNum; 184 private int mTotalTestsInModule; 185 186 // Whether modules can be marked done for this invocation. Initialized in invocationStarted() 187 // Visible for unit testing 188 protected boolean mCanMarkDone; 189 // Whether the current test run has failed. If true, we will not mark the current module done 190 protected boolean mTestRunFailed; 191 // Whether the current module has previously been marked done 192 private boolean mModuleWasDone; 193 194 // Nullable. If null, "this" is considered the primary and must handle 195 // result aggregation and reporting. When not null, it should forward events to the primary 196 private final ResultReporter mPrimaryResultReporter; 197 198 private LogFileSaver mTestLogSaver; 199 200 // Elapsed time from invocation started to ended. 201 private long mElapsedTime; 202 203 /** Invocation level configuration */ 204 private IConfiguration mConfiguration = null; 205 206 /** 207 * Default constructor. 208 */ ResultReporter()209 public ResultReporter() { 210 this(null); 211 mFinalized = new CountDownLatch(1); 212 } 213 214 /** 215 * Construct a shard ResultReporter that forwards module results to the mPrimaryResultReporter. 216 */ ResultReporter(ResultReporter primaryResultReporter)217 public ResultReporter(ResultReporter primaryResultReporter) { 218 mPrimaryResultReporter = primaryResultReporter; 219 } 220 221 /** {@inheritDoc} */ 222 @Override setConfiguration(IConfiguration configuration)223 public void setConfiguration(IConfiguration configuration) { 224 mConfiguration = configuration; 225 } 226 227 /** 228 * {@inheritDoc} 229 */ 230 @Override invocationStarted(IInvocationContext context)231 public void invocationStarted(IInvocationContext context) { 232 IBuildInfo primaryBuild = context.getBuildInfos().get(0); 233 synchronized(this) { 234 if (mBuildHelper == null) { 235 mBuildHelper = new CompatibilityBuildHelper(primaryBuild); 236 } 237 if (mDeviceSerial == null && primaryBuild.getDeviceSerial() != null) { 238 mDeviceSerial = primaryBuild.getDeviceSerial(); 239 } 240 mCanMarkDone = canMarkDone(mBuildHelper.getRecentCommandLineArgs()); 241 } 242 243 if (isShardResultReporter()) { 244 // Shard ResultReporters forward invocationStarted to the mPrimaryResultReporter 245 mPrimaryResultReporter.invocationStarted(context); 246 return; 247 } 248 249 // NOTE: Everything after this line only applies to the primary ResultReporter. 250 251 synchronized (this) { 252 if (primaryBuild.getDeviceSerial() != null) { 253 // The primary ResultReporter collects all device serials being used 254 // for the current implementation. 255 mMainDeviceSerials.add(primaryBuild.getDeviceSerial()); 256 } 257 258 // The primary ResultReporter collects all buildInfos. 259 mMainBuildInfos.add(primaryBuild); 260 261 if (mResultDir == null) { 262 // For the non-sharding case, invocationStarted is only called once, 263 // but for the sharding case, this might be called multiple times. 264 // Logic used to initialize the result directory should not be 265 // invoked twice during the same invocation. 266 initializeResultDirectories(); 267 } 268 } 269 } 270 271 /** 272 * Create directory structure where results and logs will be written. 273 */ initializeResultDirectories()274 private void initializeResultDirectories() { 275 debug("Initializing result directory"); 276 277 try { 278 // Initialize the result directory. Either a new directory or reusing 279 // an existing session. 280 if (mRetrySessionId != null) { 281 // Overwrite the mResult with the test results of the previous session 282 mResult = ResultHandler.findResult(mBuildHelper.getResultsDir(), mRetrySessionId); 283 } 284 mResult.setStartTime(mBuildHelper.getStartTime()); 285 mResultDir = mBuildHelper.getResultDir(); 286 if (mResultDir != null) { 287 mResultDir.mkdirs(); 288 } 289 } catch (FileNotFoundException e) { 290 throw new RuntimeException(e); 291 } 292 293 if (mResultDir == null) { 294 throw new RuntimeException("Result Directory was not created"); 295 } 296 if (!mResultDir.exists()) { 297 throw new RuntimeException("Result Directory was not created: " + 298 mResultDir.getAbsolutePath()); 299 } 300 301 debug("Results Directory: %s", mResultDir.getAbsolutePath()); 302 303 mUploader = new ResultUploader(mResultServer, mBuildHelper.getSuiteName()); 304 try { 305 mLogDir = new File(mBuildHelper.getLogsDir(), 306 CompatibilityBuildHelper.getDirSuffix(mBuildHelper.getStartTime())); 307 } catch (FileNotFoundException e) { 308 CLog.e(e); 309 } 310 if (mLogDir != null && mLogDir.mkdirs()) { 311 debug("Created log dir %s", mLogDir.getAbsolutePath()); 312 } 313 if (mLogDir == null || !mLogDir.exists()) { 314 throw new IllegalArgumentException(String.format("Could not create log dir %s", 315 mLogDir.getAbsolutePath())); 316 } 317 if (mTestLogSaver == null) { 318 mTestLogSaver = new LogFileSaver(mLogDir); 319 } 320 } 321 322 /** 323 * {@inheritDoc} 324 */ 325 @Override testRunStarted(String id, int numTests)326 public void testRunStarted(String id, int numTests) { 327 if (mCurrentModuleResult != null && mCurrentModuleResult.getId().equals(id) 328 && mCurrentModuleResult.isDone()) { 329 // Modules run with JarHostTest treat each test class as a separate module, 330 // resulting in additional unexpected test runs. 331 // This case exists only for N 332 mTotalTestsInModule += numTests; 333 } else { 334 // Handle non-JarHostTest case 335 mCurrentModuleResult = mResult.getOrCreateModule(id); 336 mModuleWasDone = mCurrentModuleResult.isDone(); 337 mTestRunFailed = false; 338 if (!mModuleWasDone) { 339 // we only want to update testRun variables if the IModuleResult is not yet done 340 // otherwise leave testRun variables alone so isDone evaluates to true. 341 if (mCurrentModuleResult.getExpectedTestRuns() == 0) { 342 mCurrentModuleResult.setExpectedTestRuns(TestRunHandler.getTestRuns( 343 mBuildHelper, mCurrentModuleResult.getId())); 344 } 345 mCurrentModuleResult.addTestRun(); 346 } 347 // Reset counters 348 mTotalTestsInModule = numTests; 349 mCurrentTestNum = 0; 350 } 351 mCurrentModuleResult.inProgress(true); 352 } 353 354 /** 355 * {@inheritDoc} 356 */ 357 @Override testStarted(TestDescription test)358 public void testStarted(TestDescription test) { 359 mCurrentCaseResult = mCurrentModuleResult.getOrCreateResult(test.getClassName()); 360 mCurrentResult = mCurrentCaseResult.getOrCreateResult(test.getTestName().trim()); 361 if (mCurrentResult.isRetry()) { 362 mCurrentResult.reset(); // clear result status for this invocation 363 } 364 mCurrentTestNum++; 365 } 366 367 /** 368 * {@inheritDoc} 369 */ 370 @Override testEnded(TestDescription test, HashMap<String, Metric> metrics)371 public void testEnded(TestDescription test, HashMap<String, Metric> metrics) { 372 if (mCurrentResult.getResultStatus() == TestStatus.FAIL) { 373 // Test has previously failed. 374 return; 375 } 376 // device test can have performance results in test metrics 377 Metric perfResult = metrics.get(RESULT_KEY); 378 ReportLog report = null; 379 if (perfResult != null) { 380 try { 381 report = ReportLog.parse(perfResult.getMeasurements().getSingleString()); 382 } catch (XmlPullParserException | IOException e) { 383 e.printStackTrace(); 384 } 385 } else { 386 // host test should be checked into MetricsStore. 387 report = MetricsStore.removeResult(mBuildHelper.getBuildInfo(), 388 mCurrentModuleResult.getAbi(), test.toString()); 389 } 390 if (mCurrentResult.getResultStatus() == null) { 391 // Only claim that we passed when we're certain our result was 392 // not any other state. 393 mCurrentResult.passed(report); 394 } 395 } 396 397 /** 398 * {@inheritDoc} 399 */ 400 @Override testIgnored(TestDescription test)401 public void testIgnored(TestDescription test) { 402 mCurrentResult.skipped(); 403 } 404 405 /** 406 * {@inheritDoc} 407 */ 408 @Override testFailed(TestDescription test, String trace)409 public void testFailed(TestDescription test, String trace) { 410 mCurrentResult.failed(sanitizeXmlContent(trace)); 411 } 412 413 /** 414 * {@inheritDoc} 415 */ 416 @Override testAssumptionFailure(TestDescription test, String trace)417 public void testAssumptionFailure(TestDescription test, String trace) { 418 mCurrentResult.skipped(); 419 } 420 421 /** 422 * {@inheritDoc} 423 */ 424 @Override testRunStopped(long elapsedTime)425 public void testRunStopped(long elapsedTime) { 426 // ignore 427 } 428 429 /** 430 * {@inheritDoc} 431 */ 432 @Override testRunEnded(long elapsedTime, Map<String, String> metrics)433 public void testRunEnded(long elapsedTime, Map<String, String> metrics) { 434 testRunEnded(elapsedTime, TfMetricProtoUtil.upgradeConvert(metrics)); 435 } 436 437 /** 438 * {@inheritDoc} 439 */ 440 @Override testRunEnded(long elapsedTime, HashMap<String, Metric> metrics)441 public void testRunEnded(long elapsedTime, HashMap<String, Metric> metrics) { 442 mCurrentModuleResult.inProgress(false); 443 mCurrentModuleResult.addRuntime(elapsedTime); 444 if (!mModuleWasDone && mCanMarkDone) { 445 if (mTestRunFailed) { 446 // set done to false for test run failures 447 mCurrentModuleResult.setDone(false); 448 } else { 449 // Only mark module done if: 450 // - status of the invocation allows it (mCanMarkDone), and 451 // - module has not already been marked done, and 452 // - no test run failure has been detected 453 mCurrentModuleResult.setDone(mCurrentTestNum >= mTotalTestsInModule); 454 } 455 } 456 if (isShardResultReporter()) { 457 // Forward module results to the primary. 458 mPrimaryResultReporter.mergeModuleResult(mCurrentModuleResult); 459 mCurrentModuleResult.resetTestRuns(); 460 mCurrentModuleResult.resetRuntime(); 461 } 462 } 463 464 /** 465 * Directly add a module result. Note: this method is meant to be used by 466 * a shard ResultReporter. 467 */ mergeModuleResult(IModuleResult moduleResult)468 private void mergeModuleResult(IModuleResult moduleResult) { 469 // This merges the results in moduleResult to any existing results already 470 // contained in mResult. This is useful for retries and allows the final 471 // report from a retry to contain all test results. 472 synchronized(this) { 473 mResult.mergeModuleResult(moduleResult); 474 } 475 } 476 477 /** 478 * {@inheritDoc} 479 */ 480 @Override testRunFailed(String errorMessage)481 public void testRunFailed(String errorMessage) { 482 mTestRunFailed = true; 483 mCurrentModuleResult.setFailed(); 484 } 485 486 /** 487 * {@inheritDoc} 488 */ 489 @Override getSummary()490 public TestSummary getSummary() { 491 // ignore 492 return null; 493 } 494 495 /** 496 * {@inheritDoc} 497 */ 498 @Override putSummary(List<TestSummary> summaries)499 public void putSummary(List<TestSummary> summaries) { 500 for (TestSummary summary : summaries) { 501 // If one summary is from SuiteResultReporter, log it as an extra file. 502 if (SuiteResultReporter.SUITE_REPORTER_SOURCE.equals(summary.getSource())) { 503 File summaryFile = null; 504 try { 505 summaryFile = FileUtil.createTempFile("summary", ".txt"); 506 FileUtil.writeToFile(summary.getSummary().getString(), summaryFile); 507 try (InputStreamSource stream = new FileInputStreamSource(summaryFile)) { 508 testLog("summary", LogDataType.TEXT, stream); 509 } 510 } catch (IOException e) { 511 CLog.e(e); 512 } finally { 513 FileUtil.deleteFile(summaryFile); 514 } 515 } else if (mReferenceUrl == null && summary.getSummary().getString() != null) { 516 mReferenceUrl = summary.getSummary().getString(); 517 } 518 } 519 } 520 521 /** 522 * {@inheritDoc} 523 */ 524 @Override invocationEnded(long elapsedTime)525 public void invocationEnded(long elapsedTime) { 526 if (isShardResultReporter()) { 527 // Shard ResultReporters report 528 mPrimaryResultReporter.invocationEnded(elapsedTime); 529 return; 530 } 531 532 // NOTE: Everything after this line only applies to the primary ResultReporter. 533 534 synchronized (this) { 535 // The mPrimaryResultReporter tracks the progress of all invocations across 536 // shard ResultReporters. Writing results should not proceed until all 537 // ResultReporters have completed. 538 if (++invocationEndedCount < mMainBuildInfos.size()) { 539 return; 540 } 541 mElapsedTime = elapsedTime; 542 finalizeResults(); 543 mFinalized.countDown(); 544 } 545 } 546 547 /** 548 * Returns whether a report creation should be skipped. 549 */ shouldSkipReportCreation()550 protected boolean shouldSkipReportCreation() { 551 // This value is always false here for backwards compatibility. 552 // Extended classes have the option to override this. 553 return false; 554 } 555 finalizeResults()556 private void finalizeResults() { 557 if (mFingerprintFailure) { 558 CLog.w("Failed the fingerprint check. Skip result reporting."); 559 return; 560 } 561 // Add all device serials into the result to be serialized 562 for (String deviceSerial : mMainDeviceSerials) { 563 mResult.addDeviceSerial(deviceSerial); 564 } 565 566 addDeviceBuildInfoToResult(); 567 568 Set<String> allExpectedModules = new HashSet<>(); 569 for (IBuildInfo buildInfo : mMainBuildInfos) { 570 for (Map.Entry<String, String> entry : buildInfo.getBuildAttributes().entrySet()) { 571 String key = entry.getKey(); 572 String value = entry.getValue(); 573 if (key.equals(CompatibilityBuildHelper.MODULE_IDS) && value.length() > 0) { 574 Collections.addAll(allExpectedModules, value.split(",")); 575 } 576 } 577 } 578 579 // Include a record in the report of all expected modules ids, even if they weren't 580 // executed. 581 for (String moduleId : allExpectedModules) { 582 mResult.getOrCreateModule(moduleId); 583 } 584 585 String moduleProgress = String.format("%d of %d", 586 mResult.getModuleCompleteCount(), mResult.getModules().size()); 587 588 589 if (shouldSkipReportCreation()) { 590 return; 591 } 592 593 // Get run history from the test result of last run and add the run history of the current 594 // run to it. 595 // TODO(b/137973382): avoid casting by move the method to interface level. 596 Collection<RunHistory> runHistories = ((InvocationResult) mResult).getRunHistories(); 597 String runHistoryJSON = mResult.getInvocationInfo().get(RUN_HISTORY_KEY); 598 Gson gson = new Gson(); 599 if (runHistoryJSON != null) { 600 RunHistory[] runHistoryArray = gson.fromJson(runHistoryJSON, RunHistory[].class); 601 Collections.addAll(runHistories, runHistoryArray); 602 } 603 RunHistory newRun = new RunHistory(); 604 newRun.startTime = mResult.getStartTime(); 605 newRun.endTime = newRun.startTime + mElapsedTime; 606 runHistories.add(newRun); 607 mResult.addInvocationInfo(RUN_HISTORY_KEY, gson.toJson(runHistories)); 608 609 try { 610 // Zip the full test results directory. 611 copyDynamicConfigFiles(); 612 copyFormattingFiles(mResultDir, mBuildHelper.getSuiteName()); 613 614 File resultFile = generateResultXmlFile(); 615 if (mRetrySessionId != null) { 616 copyRetryFiles(ResultHandler.getResultDirectory( 617 mBuildHelper.getResultsDir(), mRetrySessionId), mResultDir); 618 } 619 File failureReport = null; 620 if (mIncludeHtml) { 621 // Create the html report before the zip file. 622 failureReport = ResultHandler.createFailureReport(resultFile); 623 } 624 File zippedResults = zipResults(mResultDir); 625 if (!mIncludeHtml) { 626 // Create failure report after zip file so extra data is not uploaded 627 failureReport = ResultHandler.createFailureReport(resultFile); 628 } 629 if (failureReport != null && failureReport.exists()) { 630 info("Test Result: %s", failureReport.getCanonicalPath()); 631 } else { 632 info("Test Result: %s", resultFile.getCanonicalPath()); 633 } 634 info("Test Logs: %s", mLogDir.getCanonicalPath()); 635 debug("Full Result: %s", zippedResults.getCanonicalPath()); 636 637 Path latestLink = createLatestLinkDirectory(mResultDir.toPath()); 638 if (latestLink != null) { 639 info("Latest results link: " + latestLink.toAbsolutePath()); 640 } 641 642 latestLink = createLatestLinkDirectory(mLogDir.toPath()); 643 if (latestLink != null) { 644 info("Latest logs link: " + latestLink.toAbsolutePath()); 645 } 646 647 saveLog(resultFile, zippedResults); 648 649 uploadResult(resultFile); 650 651 } catch (IOException | XmlPullParserException e) { 652 CLog.e("[%s] Exception while saving result XML.", mDeviceSerial); 653 CLog.e(e); 654 } 655 // print the run results last. 656 info("Invocation finished in %s. PASSED: %d, FAILED: %d, MODULES: %s", 657 TimeUtil.formatElapsedTime(mElapsedTime), 658 mResult.countResults(TestStatus.PASS), 659 mResult.countResults(TestStatus.FAIL), 660 moduleProgress); 661 } 662 createLatestLinkDirectory(Path directory)663 private Path createLatestLinkDirectory(Path directory) { 664 Path link = null; 665 666 Path parent = directory.getParent(); 667 668 if (parent != null) { 669 link = parent.resolve(LATEST_LINK_NAME); 670 try { 671 // if latest already exists, we have to remove it before creating 672 Files.deleteIfExists(link); 673 Files.createSymbolicLink(link, directory); 674 } catch (IOException ioe) { 675 CLog.e("Exception while attempting to create 'latest' link to: [%s]", 676 directory); 677 CLog.e(ioe); 678 return null; 679 } catch (UnsupportedOperationException uoe) { 680 CLog.e("Failed to create 'latest' symbolic link - unsupported operation"); 681 return null; 682 } 683 } 684 return link; 685 } 686 687 /** 688 * {@inheritDoc} 689 */ 690 @Override invocationFailed(Throwable cause)691 public void invocationFailed(Throwable cause) { 692 warn("Invocation failed: %s", cause); 693 InvocationFailureHandler.setFailed(mBuildHelper, cause); 694 if (cause instanceof FingerprintComparisonException) { 695 mFingerprintFailure = true; 696 } 697 } 698 699 /** 700 * {@inheritDoc} 701 */ 702 @Override testLog(String name, LogDataType type, InputStreamSource stream)703 public void testLog(String name, LogDataType type, InputStreamSource stream) { 704 // This is safe to be invoked on either the primary or a shard ResultReporter 705 if (isShardResultReporter()) { 706 // Shard ResultReporters forward testLog to the mPrimaryResultReporter 707 mPrimaryResultReporter.testLog(name, type, stream); 708 return; 709 } 710 if (name.endsWith(DeviceInfo.FILE_SUFFIX)) { 711 // Handle device info file case 712 testLogDeviceInfo(name, stream); 713 } else { 714 // Handle default case 715 try { 716 File logFile = null; 717 if (mCompressLogs) { 718 try (InputStream inputStream = stream.createInputStream()) { 719 logFile = mTestLogSaver.saveAndGZipLogData(name, type, inputStream); 720 } 721 } else { 722 try (InputStream inputStream = stream.createInputStream()) { 723 logFile = mTestLogSaver.saveLogData(name, type, inputStream); 724 } 725 } 726 debug("Saved logs for %s in %s", name, logFile.getAbsolutePath()); 727 } catch (IOException e) { 728 warn("Failed to write log for %s", name); 729 CLog.e(e); 730 } 731 } 732 } 733 734 /* Write device-info files to the result, invoked only by the primary result reporter */ testLogDeviceInfo(String name, InputStreamSource stream)735 private void testLogDeviceInfo(String name, InputStreamSource stream) { 736 try { 737 File ediDir = new File(mResultDir, DeviceInfo.RESULT_DIR_NAME); 738 ediDir.mkdirs(); 739 File ediFile = new File(ediDir, name); 740 if (!ediFile.exists()) { 741 // only write this file to the results if not already present 742 FileUtil.writeToFile(stream.createInputStream(), ediFile); 743 } 744 } catch (IOException e) { 745 warn("Failed to write device info %s to result", name); 746 CLog.e(e); 747 } 748 } 749 750 /** 751 * {@inheritDoc} 752 */ 753 @Override testLogSaved(String dataName, LogDataType dataType, InputStreamSource dataStream, LogFile logFile)754 public void testLogSaved(String dataName, LogDataType dataType, InputStreamSource dataStream, 755 LogFile logFile) { 756 // This is safe to be invoked on either the primary or a shard ResultReporter 757 if (mIncludeTestLogTags 758 && mCurrentResult != null 759 && dataName.startsWith(mCurrentResult.getFullName())) { 760 761 if (dataType == LogDataType.BUGREPORT) { 762 mCurrentResult.setBugReport(logFile.getUrl()); 763 } else if (dataType == LogDataType.LOGCAT) { 764 mCurrentResult.setLog(logFile.getUrl()); 765 } else if (dataType == LogDataType.PNG) { 766 mCurrentResult.setScreenshot(logFile.getUrl()); 767 } 768 } 769 } 770 771 /** 772 * {@inheritDoc} 773 */ 774 @Override setLogSaver(ILogSaver saver)775 public void setLogSaver(ILogSaver saver) { 776 // This is safe to be invoked on either the primary or a shard ResultReporter 777 mLogSaver = saver; 778 } 779 780 /** 781 * When enabled, save log data using log saver 782 */ saveLog(File resultFile, File zippedResults)783 private void saveLog(File resultFile, File zippedResults) throws IOException { 784 if (!mUseLogSaver) { 785 return; 786 } 787 788 FileInputStream fis = null; 789 LogFile logFile = null; 790 try { 791 fis = new FileInputStream(resultFile); 792 logFile = mLogSaver.saveLogData("log-result", LogDataType.XML, fis); 793 debug("Result XML URL: %s", logFile.getUrl()); 794 logReportFiles(mConfiguration, resultFile, resultFile.getName(), LogDataType.XML); 795 } catch (IOException ioe) { 796 CLog.e("[%s] error saving XML with log saver", mDeviceSerial); 797 CLog.e(ioe); 798 } finally { 799 StreamUtil.close(fis); 800 } 801 // Save the full results folder. 802 if (zippedResults != null) { 803 FileInputStream zipResultStream = null; 804 try { 805 zipResultStream = new FileInputStream(zippedResults); 806 logFile = mLogSaver.saveLogData("results", LogDataType.ZIP, zipResultStream); 807 debug("Result zip URL: %s", logFile.getUrl()); 808 logReportFiles( 809 mConfiguration, zippedResults, "results", LogDataType.ZIP); 810 } finally { 811 StreamUtil.close(zipResultStream); 812 } 813 } 814 } 815 816 /** 817 * Return the path in which log saver persists log files or null if 818 * logSaver is not enabled. 819 */ getLogUrl()820 private String getLogUrl() { 821 if (!mUseLogSaver || mLogSaver == null) { 822 return null; 823 } 824 825 return mLogSaver.getLogReportDir().getUrl(); 826 } 827 828 @Override clone()829 public IShardableListener clone() { 830 ResultReporter clone = new ResultReporter(this); 831 OptionCopier.copyOptionsNoThrow(this, clone); 832 return clone; 833 } 834 835 /** 836 * Create results file compatible with CTSv2 (xml) report format. 837 */ generateResultXmlFile()838 protected File generateResultXmlFile() 839 throws IOException, XmlPullParserException { 840 return ResultHandler.writeResults( 841 mBuildHelper.getSuiteName(), 842 mBuildHelper.getSuiteVersion(), 843 getSuitePlan(mBuildHelper), 844 mBuildHelper.getSuiteBuild(), 845 mResult, 846 mResultDir, 847 mResult.getStartTime(), 848 mElapsedTime + mResult.getStartTime(), 849 mReferenceUrl, 850 getLogUrl(), 851 mBuildHelper.getCommandLineArgs(), 852 mResultAttributes); 853 } 854 855 /** 856 * Add build info collected from the device attributes to the results. 857 */ addDeviceBuildInfoToResult()858 protected void addDeviceBuildInfoToResult() { 859 // Add all build info to the result to be serialized 860 Map<String, String> buildProperties = mapBuildInfo(); 861 addBuildInfoToResult(buildProperties, mResult); 862 } 863 864 /** 865 * Override specific build properties so the report will be associated with the 866 * build fingerprint being certified. 867 */ addDeviceBuildInfoToResult(String buildFingerprintOverride, String manufactureOverride, String modelOverride)868 protected void addDeviceBuildInfoToResult(String buildFingerprintOverride, 869 String manufactureOverride, String modelOverride) { 870 871 Map<String, String> buildProperties = mapBuildInfo(); 872 873 // Extract and override values from build fingerprint. 874 // Build fingerprint format: brand/product/device:version/build_id/tags 875 String fingerprintPrefix = buildFingerprintOverride.split(":")[0]; 876 String fingerprintTail = buildFingerprintOverride.split(":")[1]; 877 String buildIdOverride = fingerprintTail.split("/")[1]; 878 buildProperties.put(BUILD_ID, buildIdOverride); 879 String brandOverride = fingerprintPrefix.split("/")[0]; 880 buildProperties.put(BUILD_BRAND, brandOverride); 881 String deviceOverride = fingerprintPrefix.split("/")[2]; 882 buildProperties.put(BUILD_DEVICE, deviceOverride); 883 String productOverride = fingerprintPrefix.split("/")[1]; 884 buildProperties.put(BUILD_PRODUCT, productOverride); 885 String versionOverride = fingerprintTail.split("/")[0]; 886 buildProperties.put(BUILD_VERSION_RELEASE, versionOverride); 887 buildProperties.put(BUILD_FINGERPRINT, buildFingerprintOverride); 888 buildProperties.put(BUILD_MANUFACTURER, manufactureOverride); 889 buildProperties.put(BUILD_MODEL, modelOverride); 890 891 // Add modified values to results. 892 addBuildInfoToResult(buildProperties, mResult); 893 mResult.setBuildFingerprint(buildFingerprintOverride); 894 } 895 /** Aggregate build info from member device info. */ mapBuildInfo()896 protected Map<String, String> mapBuildInfo() { 897 Map<String, String> buildProperties = new HashMap<>(); 898 for (IBuildInfo buildInfo : mMainBuildInfos) { 899 for (Map.Entry<String, String> entry : buildInfo.getBuildAttributes().entrySet()) { 900 String key = entry.getKey(); 901 String value = entry.getValue(); 902 if (key.startsWith(BUILD_INFO)) { 903 buildProperties.put(key.substring(CTS_PREFIX.length()), value); 904 } 905 } 906 } 907 return buildProperties; 908 } 909 910 /** 911 * Add build info to results. 912 * @param buildProperties Build info to add. 913 */ addBuildInfoToResult(Map<String, String> buildProperties, IInvocationResult invocationResult)914 protected static void addBuildInfoToResult(Map<String, String> buildProperties, 915 IInvocationResult invocationResult) { 916 buildProperties.entrySet().stream().forEach(entry -> 917 invocationResult.addInvocationInfo(entry.getKey(), entry.getValue())); 918 } 919 920 /** 921 * Get the suite plan. This protected method was created for overrides. 922 * Extending classes can decide on the content of the output's suite_plan field. 923 * 924 * @param mBuildHelper Helper that contains build information. 925 * @return string Suite plan to use. 926 */ getSuitePlan(CompatibilityBuildHelper mBuildHelper)927 protected String getSuitePlan(CompatibilityBuildHelper mBuildHelper) { 928 return mBuildHelper.getSuitePlan(); 929 } 930 931 /** 932 * Return true if this instance is a shard ResultReporter and should propagate certain events to 933 * the primary. 934 */ isShardResultReporter()935 private boolean isShardResultReporter() { 936 return mPrimaryResultReporter != null; 937 } 938 939 /** 940 * When enabled, upload the result to a server. 941 */ uploadResult(File resultFile)942 private void uploadResult(File resultFile) { 943 if (mResultServer != null && !mResultServer.trim().isEmpty() && !mDisableResultPosting) { 944 try { 945 debug("Result Server: %d", mUploader.uploadResult(resultFile, mReferenceUrl)); 946 } catch (IOException ioe) { 947 CLog.e("[%s] IOException while uploading result.", mDeviceSerial); 948 CLog.e(ioe); 949 } 950 } 951 } 952 953 /** 954 * Returns whether it is safe to mark modules as "done", given the invocation command-line 955 * arguments. Returns true unless this is a retry and specific filtering techniques are applied 956 * on the command-line, such as: 957 * --retry-type failed 958 * --include-filter 959 * --exclude-filter 960 * -t/--test 961 * --subplan 962 */ canMarkDone(String args)963 private boolean canMarkDone(String args) { 964 if (mRetrySessionId == null) { 965 return true; // always allow modules to be marked done if not retry 966 } 967 return !(RetryType.FAILED.equals(mRetryType) 968 || RetryType.CUSTOM.equals(mRetryType) 969 || args.contains(CompatibilityTestSuite.INCLUDE_FILTER_OPTION) 970 || args.contains(CompatibilityTestSuite.EXCLUDE_FILTER_OPTION) 971 || args.contains(CompatibilityTestSuite.SUBPLAN_OPTION) 972 || args.matches(String.format(".* (-%s|--%s) .*", 973 CompatibilityTestSuite.TEST_OPTION_SHORT_NAME, CompatibilityTestSuite.TEST_OPTION))); 974 } 975 976 /** 977 * Copy the xml formatting files stored in this jar to the results directory 978 * 979 * @param resultsDir 980 */ copyFormattingFiles(File resultsDir, String suiteName)981 static void copyFormattingFiles(File resultsDir, String suiteName) { 982 for (String resultFileName : ResultHandler.RESULT_RESOURCES) { 983 InputStream configStream = ResultHandler.class.getResourceAsStream( 984 String.format("/report/%s-%s", suiteName, resultFileName)); 985 if (configStream == null) { 986 // If suite specific files are not available, fallback to common. 987 configStream = ResultHandler.class.getResourceAsStream( 988 String.format("/report/%s", resultFileName)); 989 } 990 if (configStream != null) { 991 File resultFile = new File(resultsDir, resultFileName); 992 try { 993 FileUtil.writeToFile(configStream, resultFile); 994 } catch (IOException e) { 995 warn("Failed to write %s to file", resultFileName); 996 } 997 } else { 998 warn("Failed to load %s from jar", resultFileName); 999 } 1000 } 1001 } 1002 1003 /** 1004 * move the dynamic config files to the results directory 1005 */ copyDynamicConfigFiles()1006 private void copyDynamicConfigFiles() { 1007 File configDir = new File(mResultDir, "config"); 1008 if (!configDir.mkdir()) { 1009 warn("Failed to make dynamic config directory \"%s\" in the result", 1010 configDir.getAbsolutePath()); 1011 } 1012 1013 Set<String> uniqueModules = new HashSet<>(); 1014 for (IBuildInfo buildInfo : mMainBuildInfos) { 1015 CompatibilityBuildHelper helper = new CompatibilityBuildHelper(buildInfo); 1016 Map<String, File> dcFiles = helper.getDynamicConfigFiles(); 1017 for (String moduleName : dcFiles.keySet()) { 1018 File srcFile = dcFiles.get(moduleName); 1019 if (!uniqueModules.contains(moduleName)) { 1020 // have not seen config for this module yet, copy into result 1021 File destFile = new File(configDir, moduleName + ".dynamic"); 1022 try { 1023 FileUtil.copyFile(srcFile, destFile); 1024 uniqueModules.add(moduleName); // Add to uniqueModules if copy succeeds 1025 } catch (IOException e) { 1026 warn("Failure when copying config file \"%s\" to \"%s\" for module %s", 1027 srcFile.getAbsolutePath(), destFile.getAbsolutePath(), moduleName); 1028 CLog.e(e); 1029 } 1030 } 1031 FileUtil.deleteFile(srcFile); 1032 } 1033 } 1034 } 1035 1036 /** 1037 * Recursively copy any other files found in the previous session's result directory to the 1038 * new result directory, so long as they don't already exist. For example, a "screenshots" 1039 * directory generated in a previous session by a passing test will not be generated on retry 1040 * unless copied from the old result directory. 1041 * 1042 * @param oldDir 1043 * @param newDir 1044 */ copyRetryFiles(File oldDir, File newDir)1045 static void copyRetryFiles(File oldDir, File newDir) { 1046 File[] oldChildren = oldDir.listFiles(); 1047 for (File oldChild : oldChildren) { 1048 if (NOT_RETRY_FILES.contains(oldChild.getName())) { 1049 continue; // do not copy this file/directory or its children 1050 } 1051 File newChild = new File(newDir, oldChild.getName()); 1052 if (!newChild.exists()) { 1053 // If this old file or directory doesn't exist in new dir, simply copy it 1054 try { 1055 if (oldChild.isDirectory()) { 1056 FileUtil.recursiveCopy(oldChild, newChild); 1057 } else { 1058 FileUtil.copyFile(oldChild, newChild); 1059 } 1060 } catch (IOException e) { 1061 warn("Failed to copy file \"%s\" from previous session", oldChild.getName()); 1062 } 1063 } else if (oldChild.isDirectory() && newChild.isDirectory()) { 1064 // If both children exist as directories, make sure the children of the old child 1065 // directory exist in the new child directory. 1066 copyRetryFiles(oldChild, newChild); 1067 } 1068 } 1069 } 1070 1071 /** 1072 * Zip the contents of the given results directory. 1073 * 1074 * @param resultsDir 1075 */ zipResults(File resultsDir)1076 private static File zipResults(File resultsDir) { 1077 File zipResultFile = null; 1078 try { 1079 // create a file in parent directory, with same name as resultsDir 1080 zipResultFile = new File(resultsDir.getParent(), String.format("%s.zip", 1081 resultsDir.getName())); 1082 ZipUtil.createZip(resultsDir, zipResultFile); 1083 } catch (IOException e) { 1084 warn("Failed to create zip for %s", resultsDir.getName()); 1085 } 1086 return zipResultFile; 1087 } 1088 1089 /** 1090 * Log info to the console. 1091 */ info(String format, Object... args)1092 private static void info(String format, Object... args) { 1093 log(LogLevel.INFO, format, args); 1094 } 1095 1096 /** 1097 * Log debug to the console. 1098 */ debug(String format, Object... args)1099 private static void debug(String format, Object... args) { 1100 log(LogLevel.DEBUG, format, args); 1101 } 1102 1103 /** 1104 * Log a warning to the console. 1105 */ warn(String format, Object... args)1106 private static void warn(String format, Object... args) { 1107 log(LogLevel.WARN, format, args); 1108 } 1109 1110 /** 1111 * Log a message to the console 1112 */ log(LogLevel level, String format, Object... args)1113 private static void log(LogLevel level, String format, Object... args) { 1114 CLog.logAndDisplay(level, format, args); 1115 } 1116 1117 /** 1118 * For testing purpose. 1119 */ 1120 @VisibleForTesting getResult()1121 public IInvocationResult getResult() { 1122 return mResult; 1123 } 1124 1125 /** 1126 * Returns true if the reporter is finalized before the end of the timeout. False otherwise. 1127 */ 1128 @VisibleForTesting waitForFinalized(long timeout, TimeUnit unit)1129 public boolean waitForFinalized(long timeout, TimeUnit unit) throws InterruptedException { 1130 return mFinalized.await(timeout, unit); 1131 } 1132 sanitizeXmlContent(String s)1133 private static String sanitizeXmlContent(String s) { 1134 return XmlEscapers.xmlContentEscaper().escape(s); 1135 } 1136 1137 /** Re-log a result file to all reporters so they are aware of it. */ logReportFiles( IConfiguration configuration, File resultFile, String dataName, LogDataType type)1138 private void logReportFiles( 1139 IConfiguration configuration, File resultFile, String dataName, LogDataType type) { 1140 if (configuration == null) { 1141 return; 1142 } 1143 List<ITestInvocationListener> listeners = configuration.getTestInvocationListeners(); 1144 try (FileInputStreamSource source = new FileInputStreamSource(resultFile)) { 1145 for (ITestInvocationListener listener : listeners) { 1146 if (listener.equals(this)) { 1147 // Avoid logging agaisnt itself 1148 continue; 1149 } 1150 listener.testLog(dataName, type, source); 1151 } 1152 } 1153 } 1154 } 1155