• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 // Copyright 2023 The Chromium Authors
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 package org.chromium.base.test.transit;
6 
7 import android.util.ArrayMap;
8 import android.util.Pair;
9 
10 import androidx.annotation.IntDef;
11 
12 import org.chromium.base.Log;
13 import org.chromium.base.ThreadUtils;
14 import org.chromium.base.TimeUtils;
15 import org.chromium.base.test.util.CriteriaHelper;
16 import org.chromium.base.test.util.CriteriaNotSatisfiedException;
17 
18 import java.lang.annotation.Retention;
19 import java.lang.annotation.RetentionPolicy;
20 import java.util.List;
21 import java.util.Map;
22 
23 /** Waits for multiple {@link ConditionWaitStatus}es, polling the {@link Condition}s in parallel. */
24 public class ConditionWaiter {
25 
26     /**
27      * The fulfillment status of a {@link Condition} being waited for.
28      *
29      * <p>Tracks the times at which the Condition was checked to provide information about how long
30      * it took to be fulfilled (or for long it was checked until it timed out).
31      *
32      * <p>Tracks and aggregates errors thrown during the Condition checking for user-friendly
33      * printing.
34      */
35     static class ConditionWaitStatus {
36 
37         private final Condition mCondition;
38         private final @ConditionOrigin int mOrigin;
39         private long mTimeStarted;
40         private long mTimeUnfulfilled;
41         private long mTimeFulfilled;
42         private ArrayMap<String, Integer> mErrors = new ArrayMap<>();
43 
44         /**
45          * Constructor.
46          *
47          * @param condition the {@link Condition} that this will hold the status for.
48          * @param origin the origin of the |condition|.
49          */
ConditionWaitStatus(Condition condition, @ConditionOrigin int origin)50         ConditionWaitStatus(Condition condition, @ConditionOrigin int origin) {
51             mCondition = condition;
52             mOrigin = origin;
53         }
54 
startTimer()55         private void startTimer() {
56             mTimeStarted = getNow();
57             mTimeUnfulfilled = mTimeStarted;
58         }
59 
update()60         private boolean update() {
61             try {
62                 boolean fulfilled;
63                 if (mCondition.isRunOnUiThread()) {
64                     // TODO(crbug.com/1489445): Post multiple checks in parallel, the UI thread will
65                     // run them sequentially.
66                     fulfilled = ThreadUtils.runOnUiThreadBlocking(mCondition::check);
67                 } else {
68                     fulfilled = mCondition.check();
69                 }
70 
71                 if (fulfilled) {
72                     reportFulfilledWait();
73                     return false;
74                 } else {
75                     reportUnfulfilledWait();
76                     return true;
77                 }
78             } catch (Exception e) {
79                 reportError(e.getMessage());
80                 return true;
81             }
82         }
83 
84         /**
85          * Report that the Condition being waited on is not fulfilled at this time.
86          *
87          * @throws IllegalStateException when the Condition is unfulfilled but it had previously
88          *     been fulfilled.
89          */
reportUnfulfilledWait()90         private void reportUnfulfilledWait() throws IllegalStateException {
91             if (isFulfilled()) {
92                 throw new IllegalStateException("Unfulfilled after already being fulfilled");
93             }
94 
95             mTimeUnfulfilled = getNow();
96         }
97 
98         /** Report that the Condition being waited on is fulfilled at this time. */
reportFulfilledWait()99         private void reportFulfilledWait() {
100             if (!isFulfilled()) {
101                 // isFulfilled() will return true after setting a non-zero time.
102                 mTimeFulfilled = getNow();
103             }
104         }
105 
106         /**
107          * Report that an error happened when checking the Condition.
108          *
109          * @param reason a String that will be printed as the reason; errors with the exact same
110          *     reason are aggregated.
111          */
reportError(String reason)112         private void reportError(String reason) {
113             int beforeCount = mErrors.getOrDefault(reason, 0);
114             mErrors.put(reason, beforeCount + 1);
115         }
116 
117         /**
118          * @return if the Condition is fulfilled.
119          */
isFulfilled()120         private boolean isFulfilled() {
121             return mTimeFulfilled > 0;
122         }
123 
124         /**
125          * @return how long the condition has been considered unfulfilled for.
126          *     <p>The Condition must be unfulfilled, or an assertion will be raised.
127          */
getTimeUnfulfilled()128         private long getTimeUnfulfilled() {
129             assert !isFulfilled();
130 
131             return mTimeUnfulfilled - mTimeStarted;
132         }
133 
134         /**
135          * @return how long the condition took to be fulfilled for the first time. The result is a
136          *     pair (lowerBound, upperBound), where the time it took is between these two numbers.
137          *     |lowerBound| is the last time at which the Condition was seen as unfulfilled and
138          *     |upperBound| is the first time at which the Condition was seen as fulfilled.
139          *     <p>The Condition must be fulfilled, or an assertion will be raised.
140          */
getTimeToFulfill()141         private Pair<Long, Long> getTimeToFulfill() {
142             assert isFulfilled();
143 
144             long minTimeToFulfill = mTimeUnfulfilled - mTimeStarted;
145             long maxTimeToFulfill = mTimeFulfilled - mTimeStarted;
146             return Pair.create(minTimeToFulfill, maxTimeToFulfill);
147         }
148 
149         /**
150          * @return an aggegation of the errors reported while checking a Condition or reporting its
151          *     status.
152          */
getErrors()153         private Map<String, Integer> getErrors() {
154             return mErrors;
155         }
156 
getNow()157         private static long getNow() {
158             long now = TimeUtils.currentTimeMillis();
159             assert now > 0;
160             return now;
161         }
162     }
163 
164     /** The maximum time to wait for a criteria to become valid. */
165     public static final long MAX_TIME_TO_POLL = 3000L;
166 
167     /** The polling interval to wait between checking for a satisfied criteria. */
168     public static final long POLLING_INTERVAL = 50;
169 
170     private static final String TAG = "Transit";
171 
172     /**
173      * Blocks waiting for multiple {@link ConditionWaitStatus}es, polling the {@link Condition}s in
174      * parallel and reporting their status to the {@link ConditionWaitStatus}es.
175      *
176      * <p>The timeout is |MAX_TIME_TO_POLL|.
177      *
178      * <p>TODO(crbug.com/1489462): Make the timeout configurable per transition.
179      *
180      * @param conditionStatuses the {@link ConditionWaitStatus}es to wait for.
181      * @throws AssertionError if not all {@link Condition}s are fulfilled before timing out.
182      */
waitFor(List<ConditionWaitStatus> conditionStatuses)183     public static void waitFor(List<ConditionWaitStatus> conditionStatuses) {
184         if (conditionStatuses.isEmpty()) {
185             Log.i(TAG, "No conditions to fulfill.");
186         }
187 
188         for (ConditionWaitStatus status : conditionStatuses) {
189             status.startTimer();
190         }
191 
192         Runnable checker =
193                 () -> {
194                     boolean anyCriteriaMissing = false;
195                     for (ConditionWaitStatus status : conditionStatuses) {
196                         anyCriteriaMissing |= status.update();
197                     }
198 
199                     if (anyCriteriaMissing) {
200                         throw buildWaitConditionsException(conditionStatuses);
201                     } else {
202                         Log.i(
203                                 TAG,
204                                 "Conditions fulfilled:\n%s",
205                                 createWaitConditionsSummary(conditionStatuses));
206                     }
207                 };
208 
209         CriteriaHelper.pollInstrumentationThread(checker, MAX_TIME_TO_POLL, POLLING_INTERVAL);
210     }
211 
buildWaitConditionsException( List<ConditionWaitStatus> conditionStatuses)212     private static CriteriaNotSatisfiedException buildWaitConditionsException(
213             List<ConditionWaitStatus> conditionStatuses) {
214         return new CriteriaNotSatisfiedException(
215                 "Did not meet all conditions:\n" + createWaitConditionsSummary(conditionStatuses));
216     }
217 
createWaitConditionsSummary(List<ConditionWaitStatus> conditionStatuses)218     private static String createWaitConditionsSummary(List<ConditionWaitStatus> conditionStatuses) {
219         StringBuilder detailsString = new StringBuilder();
220 
221         int i = 1;
222         for (ConditionWaitStatus conditionStatus : conditionStatuses) {
223             String conditionDescription = conditionStatus.mCondition.getDescription();
224 
225             String originString = "";
226             switch (conditionStatus.mOrigin) {
227                 case ConditionOrigin.ENTER:
228                     originString = "[ENTER]";
229                     break;
230                 case ConditionOrigin.EXIT:
231                     originString = "[EXIT ]";
232                     break;
233                 case ConditionOrigin.TRANSITION:
234                     originString = "[TRSTN]";
235                     break;
236             }
237 
238             Map<String, Integer> errors = conditionStatus.getErrors();
239             StringBuilder errorsString = new StringBuilder();
240             String statusString;
241             if (!errors.isEmpty()) {
242                 errorsString.append(" {errors: ");
243                 for (Map.Entry<String, Integer> e : errors.entrySet()) {
244                     String errorReason = e.getKey();
245                     Integer errorCount = e.getValue();
246                     errorsString.append(String.format("%s (%d errors);", errorReason, errorCount));
247                 }
248                 errorsString.append("}");
249                 statusString = "[ERR ]";
250             } else if (conditionStatus.isFulfilled()) {
251                 statusString = "[OK  ]";
252             } else {
253                 statusString = "[FAIL]";
254             }
255 
256             String fulfilledString;
257             if (conditionStatus.isFulfilled()) {
258                 Pair<Long, Long> timeToFulfill = conditionStatus.getTimeToFulfill();
259                 fulfilledString =
260                         String.format(
261                                 "{fulfilled after %d~%d ms}",
262                                 timeToFulfill.first, timeToFulfill.second);
263             } else {
264                 fulfilledString =
265                         String.format(
266                                 "{unfulfilled after %d ms}", conditionStatus.getTimeUnfulfilled());
267             }
268 
269             detailsString
270                     .append("    [")
271                     .append(i)
272                     .append("] ")
273                     .append(originString)
274                     .append(" ")
275                     .append(statusString)
276                     .append(" ")
277                     .append(conditionDescription)
278                     .append(" ")
279                     .append(fulfilledString);
280             if (errorsString.length() > 0) {
281                 detailsString.append(" ").append(errorsString);
282             }
283             detailsString.append('\n');
284             i++;
285         }
286         return detailsString.toString();
287     }
288 
289     /** The origin of a {@link Condition} (enter, exit, transition). */
290     @IntDef({
291         ConditionWaiter.ConditionOrigin.ENTER,
292         ConditionWaiter.ConditionOrigin.EXIT,
293         ConditionWaiter.ConditionOrigin.TRANSITION
294     })
295     @Retention(RetentionPolicy.SOURCE)
296     @interface ConditionOrigin {
297         int ENTER = 0;
298         int EXIT = 1;
299         int TRANSITION = 2;
300     }
301 }
302