• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 }