1 /* 2 * Copyright (C) 2025 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.retry; 17 18 import com.android.tradefed.invoker.logger.InvocationMetricLogger; 19 import com.android.tradefed.invoker.logger.InvocationMetricLogger.InvocationMetricKey; 20 import com.android.tradefed.log.LogUtil.CLog; 21 import com.android.tradefed.result.TestDescription; 22 import com.android.tradefed.result.TestResult; 23 import com.android.tradefed.result.TestRunResult; 24 import com.android.tradefed.result.TestStatus; 25 import java.util.HashSet; 26 import java.util.LinkedHashMap; 27 import java.util.List; 28 import java.util.Map; 29 import java.util.Set; 30 31 /** 32 * A helper class to track the retry attempts for each test case and determine if a test run should 33 * be retried. 34 * 35 * <p>This class is used by {@link BaseRetryDecision} to determine which tests should be retried and 36 * which tests should be excluded from the next attempt. 37 * 38 * <p>When processing failed test runs, {@link BaseRetryDecision} calls {@link #recordTestRun} 39 * to record the results of the previous test run, and uses {@link #shouldRetry} to determine if 40 * the test run should be retried. If the test run should be retried, {@link BaseRetryDecision} will 41 * use the information from {@link #getExcludedTests} to exclude tests that have already passed or 42 * have reached the maximum number of attempts. 43 */ 44 class RetryTracker { 45 /** Track the number of attempts for each test case. */ 46 private Map<TestDescription, Integer> mTestAttemptCounter = new LinkedHashMap<>(); 47 48 /** Track the tests that have finished retrying. */ 49 private Set<TestDescription> mFinishedRetries = new HashSet<>(); 50 51 /** Track the tests to be excluded on the upcoming attempt. */ 52 private Set<TestDescription> mExcludedTests = new HashSet<>(); 53 54 /** Whether or not the test run failed (or encountered a fatal run failure). */ 55 private boolean mHasRunFailure; 56 private boolean mHasFatalRunFailure; 57 58 /** Whether we've ever seen a passing test run (at the module level) */ 59 private boolean mHasRunEverPassed; 60 61 /** The maximum number of empty retries. */ 62 private static final int MAX_EMPTY_RETRIES = 1; 63 64 /** The number of test runs that finished with no tests to retry. */ 65 private int mEmptyRetries = 0; 66 67 /** The number of the attempt that was just executed. */ 68 private int mAttemptJustExecuted; 69 70 /** The maximum number of attempts for each test case. */ 71 private int mMaxTestCaseAttempts; 72 73 final static private Set<TestStatus> RETRIABLE_STATUSES = 74 Set.of(TestStatus.FAILURE, TestStatus.SKIPPED); 75 76 /** 77 * Creates a new RetryTracker for the given module. 78 * 79 * @param testCaseAttempts The maximum number of attempts for the test cases. 80 */ RetryTracker(int testCaseAttempts)81 public RetryTracker(int testCaseAttempts) { 82 mMaxTestCaseAttempts = testCaseAttempts; 83 } 84 85 /** Returns the set of tests that passed or have finished retrying. */ getExcludedTests()86 public Set<TestDescription> getExcludedTests() { 87 return mExcludedTests; 88 } 89 90 /** 91 * Record a test run that was just executed. 92 * 93 * @param runs The results of the latest attempt. 94 * @param attemptJustExecuted The number of the attempt that was just executed. 95 * @param skipList The list of tests that should not be retried. 96 */ recordTestRun(List<TestRunResult> runs, int attemptJustExecuted, Set<String> skipList)97 public void recordTestRun(List<TestRunResult> runs, int attemptJustExecuted, Set<String> skipList) { 98 mAttemptJustExecuted = attemptJustExecuted; 99 100 // Only keep the tests that failed in the previous run. 101 mHasRunFailure = false; 102 mHasFatalRunFailure = false; 103 for (var run : runs) { 104 if (run.isRunFailure()) { 105 mHasRunFailure = true; 106 if (!run.getRunFailureDescription().isRetriable()) { 107 mHasFatalRunFailure = true; 108 } 109 } 110 111 run.getTestResults().forEach((testCase, testResult) -> { 112 recordTestCase(testCase, testResult, skipList); 113 }); 114 115 } 116 117 if (!mHasRunFailure) { 118 // Record whether we've ever seen a passing test run. 119 mHasRunEverPassed = true; 120 } 121 122 mExcludedTests.clear(); 123 mExcludedTests.addAll(mFinishedRetries); 124 125 for (var run : runs) { 126 run.getTestResults().forEach((testCase, testResult) -> { 127 if (!isRetriable(testCase, testResult, skipList)) { 128 mExcludedTests.add(testCase); 129 } 130 }); 131 } 132 133 // Record an empty retry. 134 if (mTestAttemptCounter.isEmpty() && shouldRetry()) { 135 mEmptyRetries++; 136 } 137 } 138 139 /** 140 * Returns true if the test run should be retried. 141 */ shouldRetry()142 public boolean shouldRetry() { 143 if (mHasFatalRunFailure) { 144 CLog.d("Not retrying due to fatal run failure."); 145 return false; 146 } 147 if (!mTestAttemptCounter.isEmpty()) { 148 CLog.d("Retrying because there are tests that have not finished retrying."); 149 return true; 150 } 151 if (!mHasRunFailure) { 152 CLog.d("Not retrying because there are no tests to retry and module passed."); 153 return false; 154 } 155 if (mHasRunEverPassed) { 156 // If the only problem is a module error and it's passed before, we can skip retries. 157 CLog.d("Not retrying because there are no tests to retry and module passed before."); 158 return false; 159 } 160 if (mEmptyRetries > MAX_EMPTY_RETRIES && mAttemptJustExecuted >= mMaxTestCaseAttempts) { 161 CLog.d("Not retrying because we hit empty retry limit: %d/%d", mEmptyRetries, 162 MAX_EMPTY_RETRIES); 163 return false; 164 } 165 CLog.d("Retrying because the module failed."); 166 return true; 167 } 168 169 /** 170 * Returns true if the test case can ever be retried. 171 */ isRetriable(TestDescription test, TestResult result, Set<String> skipList)172 private boolean isRetriable(TestDescription test, TestResult result, Set<String> skipList) { 173 // Don't retry passed tests. 174 if (!RETRIABLE_STATUSES.contains(result.getResultStatus())) { 175 return false; 176 } 177 178 // Don't retry tests that failed with a non-retriable failure (e.g. timeouts). 179 var failureDescription = result.getFailure(); 180 if (failureDescription != null && !failureDescription.isRetriable()) { 181 return false; 182 } 183 184 // Exclude tests that are finished retrying. 185 int attempts = mTestAttemptCounter.getOrDefault(test, 0); 186 if (attempts >= mMaxTestCaseAttempts || mFinishedRetries.contains(test)) { 187 return false; 188 } 189 190 // Exclude tests that are in the skip-retry-list. 191 if (skipList.contains(test.toString())) { 192 InvocationMetricLogger.addInvocationMetrics( 193 InvocationMetricKey.RETRY_TEST_SKIPPED_COUNT, 1); 194 CLog.d("Skip retry of %s, it's in skip-retry-list.", test.toString()); 195 return false; 196 } 197 198 return true; 199 } 200 201 /** 202 * Record a test case that was just executed. 203 * 204 * @param test The test case that was just executed. 205 * @param result The result of the test case. 206 * @param skipList The list of tests that should not be retried. 207 */ recordTestCase(TestDescription test, TestResult result, Set<String> skipList)208 private void recordTestCase(TestDescription test, TestResult result, Set<String> skipList) { 209 if (TestStatus.SKIPPED.equals(result.getResultStatus())) { 210 mTestAttemptCounter.putIfAbsent(test, 0); 211 return; 212 } 213 214 // Increment the attempt count for the test. 215 mTestAttemptCounter.put(test, mTestAttemptCounter.getOrDefault(test, 0) + 1); 216 217 // Record unretriable tests so we don't retry them again (or miscalculate 218 // mSmallestAttemptCount). 219 if (!isRetriable(test, result, skipList)) { 220 mTestAttemptCounter.remove(test); 221 mFinishedRetries.add(test); 222 } 223 } 224 }