1 /* 2 * Copyright (C) 2010 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.tradefed.result; 17 18 import com.android.tradefed.build.IBuildInfo; 19 import com.android.tradefed.config.Option; 20 import com.android.tradefed.invoker.IInvocationContext; 21 import com.android.tradefed.log.LogUtil.CLog; 22 import com.android.tradefed.metrics.proto.MetricMeasurement.Metric; 23 import com.android.tradefed.result.skipped.SkipReason; 24 import com.android.tradefed.retry.MergeStrategy; 25 import com.android.tradefed.util.IDisableable; 26 import com.android.tradefed.util.MultiMap; 27 28 import com.google.common.annotations.VisibleForTesting; 29 30 import java.util.ArrayList; 31 import java.util.Collection; 32 import java.util.Collections; 33 import java.util.HashMap; 34 import java.util.LinkedHashMap; 35 import java.util.LinkedList; 36 import java.util.List; 37 import java.util.Map; 38 import java.util.Map.Entry; 39 import java.util.concurrent.atomic.AtomicBoolean; 40 41 /** 42 * A {@link ITestInvocationListener} that will collect all test results. 43 * 44 * <p>Although the data structures used in this object are thread-safe, the {@link 45 * ITestInvocationListener} callbacks must be called in the correct order. 46 */ 47 public class CollectingTestListener 48 implements ITestInvocationListener, ILogSaverListener, IDisableable { 49 50 @Option( 51 name = "aggregate-metrics", 52 description = "attempt to add test metrics values for test runs with the same name.") 53 private boolean mIsAggregateMetrics = false; 54 55 @Option(name = "disable", description = "Whether or not to disable this reporter.") 56 private boolean mDisable = false; 57 58 /** Toggle the 'aggregate metrics' option */ setIsAggregrateMetrics(boolean aggregate)59 protected void setIsAggregrateMetrics(boolean aggregate) { 60 mIsAggregateMetrics = aggregate; 61 } 62 63 private IInvocationContext mContext; 64 private IBuildInfo mBuildInfo; 65 private Map<String, IInvocationContext> mModuleContextMap = new HashMap<>(); 66 // Use LinkedHashMap to provide consistent iterations over the keys. 67 private Map<String, List<TestRunResult>> mTestRunResultMap = 68 Collections.synchronizedMap(new LinkedHashMap<>()); 69 70 private IInvocationContext mCurrentModuleContext = null; 71 private TestRunResult mCurrentTestRunResult = new TestRunResult(); 72 /** True if the default initialized mCurrentTestRunResult has its original value. */ 73 private boolean mDefaultRun = true; 74 /** Track whether or not a test run is currently in progress */ 75 private boolean mRunInProgress = false; 76 77 private MultiMap<String, LogFile> mModuleLogFiles = new MultiMap<>(); 78 private MultiMap<String, LogFile> mNonAssociatedLogFiles = new MultiMap<>(); 79 80 // Tracks if mStatusCounts are accurate, or if they need to be recalculated 81 private AtomicBoolean mIsCountDirty = new AtomicBoolean(true); 82 // Tracks if the expected count is accurate, or if it needs to be recalculated. 83 private AtomicBoolean mIsExpectedCountDirty = new AtomicBoolean(true); 84 private int mExpectedCount = 0; 85 86 // Represents the merged test results. This should not be accessed directly since it's only 87 // calculated when needed. 88 private final List<TestRunResult> mMergedTestRunResults = new ArrayList<>(); 89 // Represents the number of tests in each TestStatus state of the merged test results. Indexed 90 // by TestStatus.ordinal() 91 private int[] mStatusCounts = new int[TestStatus.values().length]; 92 93 private MergeStrategy mStrategy = MergeStrategy.ONE_TESTCASE_PASS_IS_PASS; 94 95 /** Sets the {@link MergeStrategy} to use when merging results. */ setMergeStrategy(MergeStrategy strategy)96 public void setMergeStrategy(MergeStrategy strategy) { 97 mStrategy = strategy; 98 } 99 100 /** 101 * Return the primary build info that was reported via {@link 102 * #invocationStarted(IInvocationContext)}. Primary build is the build returned by the first 103 * build provider of the running configuration. Returns null if there is no context (no build to 104 * test case). 105 */ getPrimaryBuildInfo()106 public IBuildInfo getPrimaryBuildInfo() { 107 if (mContext == null) { 108 return null; 109 } else { 110 return mContext.getBuildInfos().get(0); 111 } 112 } 113 114 /** 115 * Return the invocation context that was reported via {@link 116 * #invocationStarted(IInvocationContext)} 117 */ getInvocationContext()118 public IInvocationContext getInvocationContext() { 119 return mContext; 120 } 121 122 /** 123 * Returns the build info. 124 * 125 * @deprecated rely on the {@link IBuildInfo} from {@link #getInvocationContext()}. 126 */ 127 @Deprecated getBuildInfo()128 public IBuildInfo getBuildInfo() { 129 return mBuildInfo; 130 } 131 132 /** 133 * Set the build info. Should only be used for testing. 134 * 135 * @deprecated Not necessary for testing anymore. 136 */ 137 @VisibleForTesting 138 @Deprecated setBuildInfo(IBuildInfo buildInfo)139 public void setBuildInfo(IBuildInfo buildInfo) { 140 mBuildInfo = buildInfo; 141 } 142 143 /** {@inheritDoc} */ 144 @Override invocationStarted(IInvocationContext context)145 public void invocationStarted(IInvocationContext context) { 146 mContext = context; 147 mBuildInfo = getPrimaryBuildInfo(); 148 } 149 150 /** {@inheritDoc} */ 151 @Override invocationEnded(long elapsedTime)152 public void invocationEnded(long elapsedTime) { 153 // ignore 154 } 155 156 @Override invocationSkipped(SkipReason reason)157 public void invocationSkipped(SkipReason reason) { 158 // ignore 159 } 160 161 /** {@inheritDoc} */ 162 @Override invocationFailed(Throwable cause)163 public void invocationFailed(Throwable cause) { 164 // ignore 165 } 166 167 @Override testModuleStarted(IInvocationContext moduleContext)168 public void testModuleStarted(IInvocationContext moduleContext) { 169 mCurrentModuleContext = moduleContext; 170 } 171 172 @Override testModuleEnded()173 public void testModuleEnded() { 174 mCurrentModuleContext = null; 175 } 176 177 /** {@inheritDoc} */ 178 @Override testRunStarted(String name, int numTests)179 public void testRunStarted(String name, int numTests) { 180 testRunStarted(name, numTests, 0); 181 } 182 183 /** {@inheritDoc} */ 184 @Override testRunStarted(String name, int numTests, int attemptNumber)185 public void testRunStarted(String name, int numTests, int attemptNumber) { 186 testRunStarted(name, numTests, attemptNumber, System.currentTimeMillis()); 187 } 188 189 /** {@inheritDoc} */ 190 @Override testRunStarted(String name, int numTests, int attemptNumber, long startTime)191 public void testRunStarted(String name, int numTests, int attemptNumber, long startTime) { 192 setCountDirty(); 193 // Only testRunStarted can affect the expected count. 194 mIsExpectedCountDirty.set(true); 195 196 // Associate the run name with the current module context 197 if (mCurrentModuleContext != null) { 198 mModuleContextMap.put(name, mCurrentModuleContext); 199 } 200 201 // Add the list of maps if the run doesn't exist 202 if (!mTestRunResultMap.containsKey(name)) { 203 mTestRunResultMap.put(name, new LinkedList<>()); 204 } 205 List<TestRunResult> results = mTestRunResultMap.get(name); 206 207 // Set the current test run result based on the attempt 208 if (attemptNumber < results.size()) { 209 if (results.get(attemptNumber) == null) { 210 throw new RuntimeException( 211 "Test run results should never be null in internal structure."); 212 } 213 } else if (attemptNumber == results.size()) { 214 // new run 215 TestRunResult result = getNewRunResult(); 216 results.add(result); 217 } else { 218 int size = results.size(); 219 for (int i = size; i < attemptNumber; i++) { 220 TestRunResult result = getNewRunResult(); 221 result.testRunStarted(name, numTests, startTime); 222 String errorMessage = 223 String.format( 224 "Run attempt %s of %s did not exists, but got attempt %s." 225 + " This is a placeholder for the missing attempt.", 226 i, name, attemptNumber); 227 result.testRunFailed(errorMessage); 228 result.testRunEnded(0L, new HashMap<String, Metric>()); 229 results.add(result); 230 } 231 // New current run 232 TestRunResult newResult = getNewRunResult(); 233 results.add(newResult); 234 } 235 mCurrentTestRunResult = results.get(attemptNumber); 236 237 mCurrentTestRunResult.testRunStarted(name, numTests, startTime); 238 mRunInProgress = true; 239 } 240 getNewRunResult()241 private TestRunResult getNewRunResult() { 242 TestRunResult result = new TestRunResult(); 243 if (mDefaultRun) { 244 result = mCurrentTestRunResult; 245 mDefaultRun = false; 246 } 247 result.setAggregateMetrics(mIsAggregateMetrics); 248 return result; 249 } 250 251 /** {@inheritDoc} */ 252 @Override testRunEnded(long elapsedTime, HashMap<String, Metric> runMetrics)253 public void testRunEnded(long elapsedTime, HashMap<String, Metric> runMetrics) { 254 setCountDirty(); 255 mCurrentTestRunResult.testRunEnded(elapsedTime, runMetrics); 256 mRunInProgress = false; 257 } 258 259 /** {@inheritDoc} */ 260 @Override testRunFailed(String errorMessage)261 public void testRunFailed(String errorMessage) { 262 setCountDirty(); 263 mCurrentTestRunResult.testRunFailed(errorMessage); 264 } 265 266 /** {@inheritDoc} */ 267 @Override testRunFailed(FailureDescription failure)268 public void testRunFailed(FailureDescription failure) { 269 setCountDirty(); 270 mCurrentTestRunResult.testRunFailed(failure); 271 } 272 273 /** {@inheritDoc} */ 274 @Override testRunStopped(long elapsedTime)275 public void testRunStopped(long elapsedTime) { 276 setCountDirty(); 277 mCurrentTestRunResult.testRunStopped(elapsedTime); 278 } 279 280 /** {@inheritDoc} */ 281 @Override testStarted(TestDescription test)282 public void testStarted(TestDescription test) { 283 testStarted(test, System.currentTimeMillis()); 284 } 285 286 /** {@inheritDoc} */ 287 @Override testStarted(TestDescription test, long startTime)288 public void testStarted(TestDescription test, long startTime) { 289 setCountDirty(); 290 mCurrentTestRunResult.testStarted(test, startTime); 291 } 292 293 /** {@inheritDoc} */ 294 @Override testEnded(TestDescription test, HashMap<String, Metric> testMetrics)295 public void testEnded(TestDescription test, HashMap<String, Metric> testMetrics) { 296 testEnded(test, System.currentTimeMillis(), testMetrics); 297 } 298 299 /** {@inheritDoc} */ 300 @Override testEnded(TestDescription test, long endTime, HashMap<String, Metric> testMetrics)301 public void testEnded(TestDescription test, long endTime, HashMap<String, Metric> testMetrics) { 302 setCountDirty(); 303 mCurrentTestRunResult.testEnded(test, endTime, testMetrics); 304 } 305 306 /** {@inheritDoc} */ 307 @Override testFailed(TestDescription test, String trace)308 public void testFailed(TestDescription test, String trace) { 309 setCountDirty(); 310 mCurrentTestRunResult.testFailed(test, trace); 311 } 312 313 @Override testFailed(TestDescription test, FailureDescription failure)314 public void testFailed(TestDescription test, FailureDescription failure) { 315 setCountDirty(); 316 mCurrentTestRunResult.testFailed(test, failure); 317 } 318 319 @Override testAssumptionFailure(TestDescription test, String trace)320 public void testAssumptionFailure(TestDescription test, String trace) { 321 setCountDirty(); 322 mCurrentTestRunResult.testAssumptionFailure(test, trace); 323 } 324 325 @Override testAssumptionFailure(TestDescription test, FailureDescription failure)326 public void testAssumptionFailure(TestDescription test, FailureDescription failure) { 327 setCountDirty(); 328 mCurrentTestRunResult.testAssumptionFailure(test, failure); 329 } 330 331 @Override testIgnored(TestDescription test)332 public void testIgnored(TestDescription test) { 333 setCountDirty(); 334 mCurrentTestRunResult.testIgnored(test); 335 } 336 337 @Override testSkipped(TestDescription test, SkipReason reason)338 public void testSkipped(TestDescription test, SkipReason reason) { 339 setCountDirty(); 340 mCurrentTestRunResult.testSkipped(test, reason); 341 } 342 343 /** {@inheritDoc} */ 344 @Override logAssociation(String dataName, LogFile logFile)345 public void logAssociation(String dataName, LogFile logFile) { 346 if (mRunInProgress) { 347 mCurrentTestRunResult.testLogSaved(dataName, logFile); 348 } else if (mCurrentModuleContext != null) { 349 mModuleLogFiles.put(dataName, logFile); 350 } else { 351 mNonAssociatedLogFiles.put(dataName, logFile); 352 } 353 } 354 355 /** 356 * Gets the results for the current test run. 357 * 358 * <p>Note the results may not be complete. It is recommended to test the value of {@link 359 * TestRunResult#isRunComplete()} and/or (@link TestRunResult#isRunFailure()} as appropriate 360 * before processing the results. 361 * 362 * @return the {@link TestRunResult} representing data collected during last test run 363 */ getCurrentRunResults()364 public TestRunResult getCurrentRunResults() { 365 return mCurrentTestRunResult; 366 } 367 368 /** Returns the total number of complete tests for all runs. */ getNumTotalTests()369 public int getNumTotalTests() { 370 computeMergedResults(); 371 int total = 0; 372 for (TestStatus s : TestStatus.values()) { 373 total += mStatusCounts[s.ordinal()]; 374 } 375 return total; 376 } 377 378 /** 379 * Returns the number of expected tests count. Could differ from {@link #getNumTotalTests()} if 380 * some tests did not run. 381 */ getExpectedTests()382 public synchronized int getExpectedTests() { 383 // If expected count is not dirty, no need to do anything 384 if (!mIsExpectedCountDirty.compareAndSet(true, false)) { 385 return mExpectedCount; 386 } 387 388 computeMergedResults(); 389 mExpectedCount = 0; 390 for (TestRunResult result : getMergedTestRunResults()) { 391 mExpectedCount += result.getExpectedTestCount(); 392 } 393 return mExpectedCount; 394 } 395 396 /** For compatibility with older status type */ getNumTestsInState( com.android.ddmlib.testrunner.TestResult.TestStatus ddmlibStatus)397 public int getNumTestsInState( 398 com.android.ddmlib.testrunner.TestResult.TestStatus ddmlibStatus) { 399 computeMergedResults(); 400 return mStatusCounts[TestStatus.convertFromDdmlibType(ddmlibStatus).ordinal()]; 401 } 402 403 /** Returns the number of tests in given state for this run. */ getNumTestsInState(TestStatus status)404 public int getNumTestsInState(TestStatus status) { 405 computeMergedResults(); 406 return mStatusCounts[status.ordinal()]; 407 } 408 409 /** Returns if the invocation had any failed or assumption failed tests. */ hasFailedTests()410 public boolean hasFailedTests() { 411 return getNumAllFailedTests() > 0; 412 } 413 414 /** Returns the total number of test runs in a failure state */ getNumAllFailedTestRuns()415 public int getNumAllFailedTestRuns() { 416 int count = 0; 417 for (TestRunResult result : getMergedTestRunResults()) { 418 if (result.isRunFailure()) { 419 count++; 420 } 421 } 422 return count; 423 } 424 425 /** 426 * Returns the total number of tests in a failure state (only failed, assumption failures do not 427 * count toward it). 428 */ getNumAllFailedTests()429 public int getNumAllFailedTests() { 430 return getNumTestsInState(TestStatus.FAILURE); 431 } 432 433 /** 434 * Return the merged collection of results for all runs across different attempts. 435 * 436 * <p>If there are multiple results, each test run is merged, with the latest test result 437 * overwriting test results of previous runs. Test runs are ordered by attempt number. 438 * 439 * <p>Metrics for the same attempt will be merged based on the preference set by {@code 440 * aggregate-metrics}. The final metrics will be the metrics of the last attempt. 441 */ getMergedTestRunResults()442 public List<TestRunResult> getMergedTestRunResults() { 443 computeMergedResults(); 444 return new ArrayList<>(mMergedTestRunResults); 445 } 446 447 /** 448 * Returns the results for all test runs. 449 * 450 * @deprecated Use {@link #getMergedTestRunResults()} 451 */ 452 @Deprecated getRunResults()453 public Collection<TestRunResult> getRunResults() { 454 return getMergedTestRunResults(); 455 } 456 457 /** 458 * Computes and stores the merged results and the total status counts since both operations are 459 * expensive. 460 */ computeMergedResults()461 private synchronized void computeMergedResults() { 462 // If not dirty, nothing to be done 463 if (!mIsCountDirty.compareAndSet(true, false)) { 464 return; 465 } 466 467 mMergedTestRunResults.clear(); 468 // Merge results 469 if (mTestRunResultMap.isEmpty() && mCurrentTestRunResult.isRunFailure()) { 470 // In case of early failure that is a bit untracked, still add it to the list to 471 // not loose it. 472 CLog.e( 473 "Early failure resulting in no testRunStart. Results might be inconsistent:" 474 + "\n%s", 475 mCurrentTestRunResult.getRunFailureMessage()); 476 mMergedTestRunResults.add(mCurrentTestRunResult); 477 } else { 478 for (Entry<String, List<TestRunResult>> results : mTestRunResultMap.entrySet()) { 479 TestRunResult res = TestRunResult.merge(results.getValue(), mStrategy); 480 if (res == null) { 481 // Merge can return null in case of results being empty. 482 CLog.w("No results for %s", results.getKey()); 483 } else { 484 mMergedTestRunResults.add(res); 485 } 486 } 487 } 488 // Reset counts 489 for (TestStatus s : TestStatus.values()) { 490 mStatusCounts[s.ordinal()] = 0; 491 } 492 493 // Calculate results 494 for (TestRunResult result : mMergedTestRunResults) { 495 for (TestStatus s : TestStatus.values()) { 496 mStatusCounts[s.ordinal()] += result.getNumTestsInState(s); 497 } 498 } 499 } 500 501 /** 502 * Keep dirty count as AtomicBoolean to ensure when accessed from another thread the state is 503 * consistent. 504 */ setCountDirty()505 private void setCountDirty() { 506 mIsCountDirty.set(true); 507 } 508 509 /** 510 * Return all the names for all the test runs. 511 * 512 * <p>These test runs may have run multiple times with different attempts. 513 */ getTestRunNames()514 public Collection<String> getTestRunNames() { 515 return new ArrayList<String>(mTestRunResultMap.keySet()); 516 } 517 518 /** 519 * Gets all the attempts for a {@link TestRunResult} of a given test run. 520 * 521 * @param testRunName The name given by {{@link #testRunStarted(String, int)}. 522 * @return All {@link TestRunResult} for a given test run, ordered by attempts. 523 */ getTestRunAttempts(String testRunName)524 public List<TestRunResult> getTestRunAttempts(String testRunName) { 525 return mTestRunResultMap.get(testRunName); 526 } 527 528 /** 529 * Gets all the results for a given attempt. 530 * 531 * @param attempt The attempt we want results for. 532 * @return All {@link TestRunResult} for a given attempt. 533 */ getTestRunForAttempts(int attempt)534 public List<TestRunResult> getTestRunForAttempts(int attempt) { 535 List<TestRunResult> allResultForAttempts = new ArrayList<>(); 536 for (Entry<String, List<TestRunResult>> runInfo : mTestRunResultMap.entrySet()) { 537 if (attempt < runInfo.getValue().size()) { 538 TestRunResult attemptRes = runInfo.getValue().get(attempt); 539 allResultForAttempts.add(attemptRes); 540 } 541 } 542 return allResultForAttempts; 543 } 544 545 /** 546 * Returns whether a given test run name has any results. 547 * 548 * @param testRunName The name given by {{@link #testRunStarted(String, int)}. 549 */ hasTestRunResultsForName(String testRunName)550 public boolean hasTestRunResultsForName(String testRunName) { 551 return mTestRunResultMap.containsKey(testRunName); 552 } 553 554 /** 555 * Returns the number of attempts for a given test run name. 556 * 557 * @param testRunName The name given by {{@link #testRunStarted(String, int)}. 558 */ getTestRunAttemptCount(String testRunName)559 public int getTestRunAttemptCount(String testRunName) { 560 List<TestRunResult> results = mTestRunResultMap.get(testRunName); 561 if (results == null) { 562 return 0; 563 } 564 return results.size(); 565 } 566 567 /** 568 * Return the {@link TestRunResult} for a single attempt. 569 * 570 * @param testRunName The name given by {{@link #testRunStarted(String, int)}. 571 * @param attempt The attempt id. 572 * @return The {@link TestRunResult} for the given name and attempt id or {@code null} if it 573 * does not exist. 574 */ getTestRunAtAttempt(String testRunName, int attempt)575 public TestRunResult getTestRunAtAttempt(String testRunName, int attempt) { 576 List<TestRunResult> results = mTestRunResultMap.get(testRunName); 577 if (results == null || attempt < 0 || attempt >= results.size()) { 578 return null; 579 } 580 581 return results.get(attempt); 582 } 583 584 /** 585 * Returns the {@link IInvocationContext} of the module associated with the results. 586 * 587 * @param testRunName The name given by {{@link #testRunStarted(String, int)}. 588 * @return The {@link IInvocationContext} of the module for a given test run name {@code null} 589 * if there are no results for that name. 590 */ getModuleContextForRunResult(String testRunName)591 public IInvocationContext getModuleContextForRunResult(String testRunName) { 592 return mModuleContextMap.get(testRunName); 593 } 594 595 /** Returns a copy of the map containing all the logged file associated with the module */ getModuleLogFiles()596 public MultiMap<String, LogFile> getModuleLogFiles() { 597 return new MultiMap<>(mModuleLogFiles); 598 } 599 600 /** 601 * Returns a copy of the map containing all the logged file not associated with a test run or a 602 * module. 603 */ getNonAssociatedLogFiles()604 public MultiMap<String, LogFile> getNonAssociatedLogFiles() { 605 return new MultiMap<>(mNonAssociatedLogFiles); 606 } 607 608 /** 609 * Allows to clear the results for a given run name. Should only be used in some cases like the 610 * aggregator of results. 611 */ clearResultsForName(String testRunName)612 protected final synchronized void clearResultsForName(String testRunName) { 613 setCountDirty(); 614 mTestRunResultMap.remove(testRunName); 615 } 616 617 /** Allows cleaning the module file so we avoid carrying them for too long. */ clearModuleLogFiles()618 protected final synchronized void clearModuleLogFiles() { 619 mModuleLogFiles = new MultiMap<>(); 620 } 621 622 @Override isDisabled()623 public boolean isDisabled() { 624 return mDisable; 625 } 626 627 @Override setDisable(boolean isDisabled)628 public void setDisable(boolean isDisabled) { 629 mDisable = isDisabled; 630 } 631 } 632