• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2022 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 distributed under the
11  * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
12  * KIND, either express or implied. See the License for the specific language governing
13  * permissions and limitations under the License.
14  */
15 package android.platform.uiautomator_helpers
16 
17 import android.os.SystemClock.sleep
18 import android.os.SystemClock.uptimeMillis
19 import android.os.Trace
20 import android.platform.uiautomator_helpers.TracingUtils.trace
21 import android.platform.uiautomator_helpers.WaitUtils.LoggerImpl.Companion.withEventualLogging
22 import android.util.Log
23 import java.io.Closeable
24 import java.time.Duration
25 import java.time.Instant.now
26 
27 /**
28  * Collection of utilities to ensure a certain conditions is met.
29  *
30  * Those are meant to make tests more understandable from perfetto traces, and less flaky.
31  */
32 object WaitUtils {
33     private val DEFAULT_DEADLINE = Duration.ofSeconds(10)
34     private val POLLING_WAIT = Duration.ofMillis(100)
35     private val DEFAULT_SETTLE_TIME = Duration.ofSeconds(3)
36     private const val TAG = "WaitUtils"
37     private const val VERBOSE = true
38 
39     /**
40      * Ensures that [condition] succeeds within [timeout], or fails with [errorProvider] message.
41      *
42      * This also logs with atrace each iteration, and its entire execution. Those traces are then
43      * visible in perfetto. Note that logs are output only after the end of the method, all
44      * together.
45      *
46      * Example of usage:
47      * ```
48      * ensureThat("screen is on") { uiDevice.isScreenOn }
49      * ```
50      */
51     @JvmStatic
52     @JvmOverloads
ensureThatnull53     fun ensureThat(
54         description: String? = null,
55         timeout: Duration = DEFAULT_DEADLINE,
56         errorProvider: (() -> String)? = null,
57         ignoreFailure: Boolean = false,
58         ignoreException: Boolean = false,
59         condition: () -> Boolean,
60     ) {
61         val traceName =
62             if (description != null) {
63                 "Ensuring $description"
64             } else {
65                 "ensure"
66             }
67         val errorProvider =
68             errorProvider
69                 ?: { "Error ensuring that \"$description\" within ${timeout.toMillis()}ms" }
70         trace(traceName) {
71             val startTime = uptimeMillis()
72             val timeoutMs = timeout.toMillis()
73             Log.d(TAG, "Starting $traceName")
74             withEventualLogging(logTimeDelta = true) {
75                 log(traceName)
76                 var i = 1
77                 while (uptimeMillis() < startTime + timeoutMs) {
78                     trace("iteration $i") {
79                         try {
80                             if (condition()) {
81                                 log("[#$i] Condition true")
82                                 return
83                             }
84                         } catch (t: Throwable) {
85                             log("[#$i] Condition failing with exception")
86                             if (!ignoreException) {
87                                 throw RuntimeException("[#$i] iteration failed.", t)
88                             }
89                         }
90 
91                         log("[#$i] Condition false, might retry.")
92                         sleep(POLLING_WAIT.toMillis())
93                         i++
94                     }
95                 }
96                 log("[#$i] Condition has always been false. Failing.")
97                 if (ignoreFailure) {
98                     Log.w(TAG, "Ignoring ensureThat failure: ${errorProvider()}")
99                 } else {
100                     throw FailedEnsureException(errorProvider())
101                 }
102             }
103         }
104     }
105 
106     /**
107      * Same as [waitForNullableValueToSettle], but assumes that [supplier] return value is non-null.
108      */
109     @JvmStatic
110     @JvmOverloads
waitForValueToSettlenull111     fun <T> waitForValueToSettle(
112         description: String? = null,
113         minimumSettleTime: Duration = DEFAULT_SETTLE_TIME,
114         timeout: Duration = DEFAULT_DEADLINE,
115         errorProvider: () -> String =
116             defaultWaitForSettleError(minimumSettleTime, description, timeout),
117         supplier: () -> T,
118     ): T {
119         return waitForNullableValueToSettle(
120             description,
121             minimumSettleTime,
122             timeout,
123             errorProvider,
124             supplier
125         )
126             ?: error(errorProvider())
127     }
128 
129     /**
130      * Waits for [supplier] to return the same value for at least [minimumSettleTime].
131      *
132      * If the value changes, the timer gets restarted. Fails when reaching [timeoutMs]. The minimum
133      * running time of this method is [minimumSettleTime], in case the value is stable since the
134      * beginning.
135      *
136      * Fails if [supplier] throws an exception.
137      *
138      * Outputs atraces visible with perfetto.
139      *
140      * Example of usage:
141      * ```
142      * val screenOn = waitForValueToSettle("Screen on") { uiDevice.isScreenOn }
143      * ```
144      *
145      * Note: Prefer using [waitForValueToSettle] when [supplier] doesn't return a null value.
146      *
147      * @return the settled value. Throws if it doesn't settle.
148      */
149     @JvmStatic
150     @JvmOverloads
waitForNullableValueToSettlenull151     fun <T> waitForNullableValueToSettle(
152         description: String? = null,
153         minimumSettleTime: Duration = DEFAULT_SETTLE_TIME,
154         timeout: Duration = DEFAULT_DEADLINE,
155         errorProvider: () -> String =
156             defaultWaitForSettleError(minimumSettleTime, description, timeout),
157         supplier: () -> T?,
158     ): T? {
159         val prefix =
160             if (description != null) {
161                 "Waiting for \"$description\" to settle"
162             } else {
163                 "waitForValueToSettle"
164             }
165         val traceName =
166             prefix +
167                 " (settleTime=${minimumSettleTime.toMillis()}ms, deadline=${timeout.toMillis()}ms)"
168         trace(traceName) {
169             Log.d(TAG, "Starting $traceName")
170             withEventualLogging(logTimeDelta = true) {
171                 log(traceName)
172 
173                 val startTime = now()
174                 var settledSince = startTime
175                 var previousValue: T? = null
176                 var previousValueSet = false
177                 while (now().isBefore(startTime + timeout)) {
178                     val newValue =
179                         try {
180                             supplier()
181                         } catch (t: Throwable) {
182                             if (previousValueSet) {
183                                 Trace.endSection()
184                             }
185                             log("Supplier has thrown an exception")
186                             throw RuntimeException(t)
187                         }
188                     val currentTime = now()
189                     if (previousValue != newValue || !previousValueSet) {
190                         log("value changed to $newValue")
191                         settledSince = currentTime
192                         if (previousValueSet) {
193                             Trace.endSection()
194                         }
195                         TracingUtils.beginSectionSafe("New value: $newValue")
196                         previousValue = newValue
197                         previousValueSet = true
198                     } else if (now().isAfter(settledSince + minimumSettleTime)) {
199                         log("Got settled value. Returning \"$previousValue\"")
200                         Trace.endSection() // previousValue is guaranteed to be non-null.
201                         return previousValue
202                     }
203                     sleep(POLLING_WAIT.toMillis())
204                 }
205                 if (previousValueSet) {
206                     Trace.endSection()
207                 }
208                 error(errorProvider())
209             }
210         }
211     }
212 
defaultWaitForSettleErrornull213     private fun defaultWaitForSettleError(
214         minimumSettleTime: Duration,
215         description: String?,
216         timeout: Duration
217     ): () -> String {
218         return {
219             "Error getting settled (${minimumSettleTime.toMillis()}) " +
220                 "value for \"$description\" within ${timeout.toMillis()}."
221         }
222     }
223 
224     /**
225      * Waits for [supplier] to return a non-null value within [timeout].
226      *
227      * Returns null after the timeout finished.
228      */
waitForNullablenull229     fun <T> waitForNullable(
230         description: String,
231         timeout: Duration = DEFAULT_DEADLINE,
232         checker: (T?) -> Boolean = { it != null },
233         supplier: () -> T?,
234     ): T? {
235         var result: T? = null
236 
<lambda>null237         ensureThat("Waiting for \"$description\"", timeout, ignoreFailure = true) {
238             result = supplier()
239             checker(result)
240         }
241         return result
242     }
243 
244     /** Wraps [waitForNullable] using the default checker, and allowing kotlin supplier syntax. */
waitForNullablenull245     fun <T> waitForNullable(
246         description: String,
247         timeout: Duration = DEFAULT_DEADLINE,
248         supplier: () -> T?,
249     ): T? = waitForNullable(description, timeout, checker = { it != null }, supplier)
250 
251     /**
252      * Waits for [supplier] to return a not null and not empty list within [timeout].
253      *
254      * Returns the not-empty list as soon as it's received, or an empty list once reached the
255      * timeout.
256      */
waitForPossibleEmptynull257     fun <T> waitForPossibleEmpty(
258         description: String,
259         timeout: Duration = DEFAULT_DEADLINE,
260         supplier: () -> List<T>?
261     ): List<T> =
262         waitForNullable(description, timeout, { !it.isNullOrEmpty() }, supplier) ?: emptyList()
263 
264     /**
265      * Waits for [supplier] to return a non-null value within [timeout].
266      *
267      * Throws an exception with [errorProvider] provided message if [supplier] failed to produce a
268      * non-null value within [timeout].
269      */
waitFornull270     fun <T> waitFor(
271         description: String,
272         timeout: Duration = DEFAULT_DEADLINE,
273         errorProvider: () -> String = {
274             "Didn't get a non-null value for \"$description\" within ${timeout.toMillis()}ms"
275         },
276         supplier: () -> T?
277     ): T = waitForNullable(description, timeout, supplier) ?: error(errorProvider())
278 
279     /** Generic logging interface. */
280     private interface Logger {
lognull281         fun log(s: String)
282     }
283 
284     /** Logs all messages when closed. */
285     private class LoggerImpl private constructor(private val logTimeDelta: Boolean) :
286         Closeable, Logger {
287         private val logs = mutableListOf<String>()
288         private val startTime = uptimeMillis()
289 
290         companion object {
291             /** Executes [block] and prints all logs at the end. */
292             inline fun <T> withEventualLogging(
293                 logTimeDelta: Boolean = false,
294                 block: Logger.() -> T
295             ): T = LoggerImpl(logTimeDelta).use { it.block() }
296         }
297 
298         override fun log(s: String) {
299             logs += if (logTimeDelta) "+${uptimeMillis() - startTime}ms $s" else s
300         }
301 
302         override fun close() {
303             if (VERBOSE) {
304                 Log.d(TAG, logs.joinToString("\n"))
305             }
306         }
307     }
308 }
309 
310 /** Exception thrown when [WaitUtils.ensureThat] fails. */
311 class FailedEnsureException(message: String? = null) : IllegalStateException(message)
312