• 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         condition: () -> Boolean,
59     ) {
60         val traceName =
61             if (description != null) {
62                 "Ensuring $description"
63             } else {
64                 "ensure"
65             }
66         val errorProvider =
67             errorProvider
68                 ?: { "Error ensuring that \"$description\" within ${timeout.toMillis()}ms" }
69         trace(traceName) {
70             val startTime = uptimeMillis()
71             val timeoutMs = timeout.toMillis()
72             Log.d(TAG, "Starting $traceName")
73             withEventualLogging(logTimeDelta = true) {
74                 log(traceName)
75                 var i = 1
76                 while (uptimeMillis() < startTime + timeoutMs) {
77                     trace("iteration $i") {
78                         try {
79                             if (condition()) {
80                                 log("[#$i] Condition true")
81                                 return
82                             }
83                         } catch (t: Throwable) {
84                             log("[#$i] Condition failing with exception")
85                             throw RuntimeException("[#$i] iteration failed.", t)
86                         }
87 
88                         log("[#$i] Condition false, might retry.")
89                         sleep(POLLING_WAIT.toMillis())
90                         i++
91                     }
92                 }
93                 log("[#$i] Condition has always been false. Failing.")
94                 if (ignoreFailure) {
95                     Log.w(TAG, "Ignoring ensureThat failure: ${errorProvider()}")
96                 } else {
97                     throw FailedEnsureException(errorProvider())
98                 }
99             }
100         }
101     }
102 
103     /**
104      * Same as [waitForNullableValueToSettle], but assumes that [supplier] return value is non-null.
105      */
106     @JvmStatic
107     @JvmOverloads
waitForValueToSettlenull108     fun <T> waitForValueToSettle(
109         description: String? = null,
110         minimumSettleTime: Duration = DEFAULT_SETTLE_TIME,
111         timeout: Duration = DEFAULT_DEADLINE,
112         errorProvider: () -> String =
113             defaultWaitForSettleError(minimumSettleTime, description, timeout),
114         supplier: () -> T,
115     ): T {
116         return waitForNullableValueToSettle(
117             description,
118             minimumSettleTime,
119             timeout,
120             errorProvider,
121             supplier
122         )
123             ?: error(errorProvider())
124     }
125 
126     /**
127      * Waits for [supplier] to return the same value for at least [minimumSettleTime].
128      *
129      * If the value changes, the timer gets restarted. Fails when reaching [timeoutMs]. The minimum
130      * running time of this method is [minimumSettleTime], in case the value is stable since the
131      * beginning.
132      *
133      * Fails if [supplier] throws an exception.
134      *
135      * Outputs atraces visible with perfetto.
136      *
137      * Example of usage:
138      * ```
139      * val screenOn = waitForValueToSettle("Screen on") { uiDevice.isScreenOn }
140      * ```
141      *
142      * Note: Prefer using [waitForValueToSettle] when [supplier] doesn't return a null value.
143      *
144      * @return the settled value. Throws if it doesn't settle.
145      */
146     @JvmStatic
147     @JvmOverloads
waitForNullableValueToSettlenull148     fun <T> waitForNullableValueToSettle(
149         description: String? = null,
150         minimumSettleTime: Duration = DEFAULT_SETTLE_TIME,
151         timeout: Duration = DEFAULT_DEADLINE,
152         errorProvider: () -> String =
153             defaultWaitForSettleError(minimumSettleTime, description, timeout),
154         supplier: () -> T?,
155     ): T? {
156         val prefix =
157             if (description != null) {
158                 "Waiting for \"$description\" to settle"
159             } else {
160                 "waitForValueToSettle"
161             }
162         val traceName =
163             prefix +
164                 " (settleTime=${minimumSettleTime.toMillis()}ms, deadline=${timeout.toMillis()}ms)"
165         trace(traceName) {
166             Log.d(TAG, "Starting $traceName")
167             withEventualLogging(logTimeDelta = true) {
168                 log(traceName)
169 
170                 val startTime = now()
171                 var settledSince = startTime
172                 var previousValue: T? = null
173                 var previousValueSet = false
174                 while (now().isBefore(startTime + timeout)) {
175                     val newValue =
176                         try {
177                             supplier()
178                         } catch (t: Throwable) {
179                             if (previousValueSet) {
180                                 Trace.endSection()
181                             }
182                             log("Supplier has thrown an exception")
183                             throw RuntimeException(t)
184                         }
185                     val currentTime = now()
186                     if (previousValue != newValue || !previousValueSet) {
187                         log("value changed to $newValue")
188                         settledSince = currentTime
189                         if (previousValueSet) {
190                             Trace.endSection()
191                         }
192                         TracingUtils.beginSectionSafe("New value: $newValue")
193                         previousValue = newValue
194                         previousValueSet = true
195                     } else if (now().isAfter(settledSince + minimumSettleTime)) {
196                         log("Got settled value. Returning \"$previousValue\"")
197                         Trace.endSection() // previousValue is guaranteed to be non-null.
198                         return previousValue
199                     }
200                     sleep(POLLING_WAIT.toMillis())
201                 }
202                 if (previousValueSet) {
203                     Trace.endSection()
204                 }
205                 error(errorProvider())
206             }
207         }
208     }
209 
defaultWaitForSettleErrornull210     private fun defaultWaitForSettleError(
211         minimumSettleTime: Duration,
212         description: String?,
213         timeout: Duration
214     ): () -> String {
215         return {
216             "Error getting settled (${minimumSettleTime.toMillis()}) " +
217                 "value for \"$description\" within ${timeout.toMillis()}."
218         }
219     }
220 
221     /**
222      * Waits for [supplier] to return a non-null value within [timeout].
223      *
224      * Returns null after the timeout finished.
225      */
waitForNullablenull226     fun <T> waitForNullable(
227         description: String,
228         timeout: Duration = DEFAULT_DEADLINE,
229         supplier: () -> T?
230     ): T? {
231         var result: T? = null
232 
233         ensureThat("Waiting for \"$description\"", timeout, ignoreFailure = true) {
234             result = supplier()
235             result != null
236         }
237         return result
238     }
239 
240     /**
241      * Waits for [supplier] to return a non-null value within [timeout].
242      *
243      * Throws an exception with [errorProvider] provided message if [supplier] failed to produce a
244      * non-null value within [timeout].
245      */
waitFornull246     fun <T> waitFor(
247         description: String,
248         timeout: Duration = DEFAULT_DEADLINE,
249         errorProvider: () -> String = {
250             "Didn't get a non-null value for \"$description\" within ${timeout.toMillis()}ms"
251         },
252         supplier: () -> T?
253     ): T = waitForNullable(description, timeout, supplier) ?: error(errorProvider())
254 
255     /** Generic logging interface. */
256     private interface Logger {
lognull257         fun log(s: String)
258     }
259 
260     /** Logs all messages when closed. */
261     private class LoggerImpl private constructor(private val logTimeDelta: Boolean) :
262         Closeable, Logger {
263         private val logs = mutableListOf<String>()
264         private val startTime = uptimeMillis()
265 
266         companion object {
267             /** Executes [block] and prints all logs at the end. */
268             inline fun <T> withEventualLogging(
269                 logTimeDelta: Boolean = false,
270                 block: Logger.() -> T
271             ): T = LoggerImpl(logTimeDelta).use { it.block() }
272         }
273 
274         override fun log(s: String) {
275             logs += if (logTimeDelta) "+${uptimeMillis() - startTime}ms $s" else s
276         }
277 
278         override fun close() {
279             if (VERBOSE) {
280                 Log.d(TAG, logs.joinToString("\n"))
281             }
282         }
283     }
284 }
285 
286 /** Exception thrown when [WaitUtils.ensureThat] fails. */
287 class FailedEnsureException(message: String? = null) : IllegalStateException(message)
288