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