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