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 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.safetycenter.testing 18 19 import android.util.Log 20 import androidx.test.platform.app.InstrumentationRegistry 21 import java.time.Duration 22 import kotlinx.coroutines.DEBUG_PROPERTY_NAME 23 import kotlinx.coroutines.DEBUG_PROPERTY_VALUE_AUTO 24 import kotlinx.coroutines.DEBUG_PROPERTY_VALUE_ON 25 import kotlinx.coroutines.TimeoutCancellationException 26 import kotlinx.coroutines.delay 27 import kotlinx.coroutines.runBlocking 28 import kotlinx.coroutines.withTimeout 29 import kotlinx.coroutines.withTimeoutOrNull 30 31 /** A class that facilitates interacting with coroutines. */ 32 object Coroutines { 33 34 /** 35 * The timeout of a test case, typically varies depending on whether the test is running 36 * locally, on pre-submit or post-submit. 37 */ 38 val TEST_TIMEOUT: Duration 39 get() = 40 Duration.ofMillis( 41 InstrumentationRegistry.getArguments().getString("timeout_msec", "60000").toLong() 42 ) 43 44 /** A long timeout, to be used for actions that are expected to complete. */ 45 val TIMEOUT_LONG: Duration 46 get() = TEST_TIMEOUT.dividedBy(2) 47 48 /** A short timeout, to be used for actions that are expected not to complete. */ 49 val TIMEOUT_SHORT: Duration = Duration.ofSeconds(1) 50 51 /** Shorthand for [runBlocking] combined with [withTimeout]. */ runBlockingWithTimeoutnull52 fun <T> runBlockingWithTimeout(timeout: Duration = TIMEOUT_LONG, block: suspend () -> T): T = 53 runBlocking { 54 withTimeout(timeout.toMillis()) { block() } 55 } 56 57 /** Shorthand for [runBlocking] combined with [withTimeoutOrNull] */ runBlockingWithTimeoutOrNullnull58 fun <T> runBlockingWithTimeoutOrNull( 59 timeout: Duration = TIMEOUT_LONG, 60 block: suspend () -> T, 61 ): T? = runBlocking { withTimeoutOrNull(timeout.toMillis()) { block() } } 62 63 /** Check a condition using coroutines with a timeout. */ waitForWithTimeoutnull64 fun waitForWithTimeout( 65 timeout: Duration = TIMEOUT_LONG, 66 checkPeriod: Duration = CHECK_PERIOD, 67 condition: () -> Boolean, 68 ) { 69 runBlockingWithTimeout(timeout) { waitFor(checkPeriod, condition) } 70 } 71 72 /** Check an assertion passes, with a timeout if it does not. */ assertWithTimeoutnull73 fun assertWithTimeout( 74 timeout: Duration = TIMEOUT_LONG, 75 checkPeriod: Duration = CHECK_PERIOD, 76 assertion: () -> Unit, 77 ) { 78 try { 79 runBlockingWithTimeout(timeout) { assertThatWaiting(checkPeriod, assertion) } 80 } catch (ex: TimeoutCancellationException) { 81 // Rerun the assertion to generate a meaningful error message that isn't just "timeout" 82 assertion() 83 } 84 } 85 86 /** Retries a [fallibleAction] until no errors are thrown or a timeout occurs. */ waitForSuccessWithTimeoutnull87 fun waitForSuccessWithTimeout( 88 timeout: Duration = TIMEOUT_LONG, 89 checkPeriod: Duration = CHECK_PERIOD, 90 fallibleAction: () -> Unit, 91 ) { 92 waitForWithTimeout(timeout, checkPeriod) { 93 try { 94 fallibleAction() 95 true 96 } catch (ex: Throwable) { 97 Log.w(TAG, "Encountered failure, retrying until timeout: $ex") 98 false 99 } 100 } 101 } 102 103 /** 104 * Enables debug mode for coroutines, in particular this enables stack traces in case of 105 * failures. 106 */ enableDebuggingnull107 fun enableDebugging() { 108 System.setProperty(DEBUG_PROPERTY_NAME, DEBUG_PROPERTY_VALUE_ON) 109 } 110 111 /** Resets the debug mode to its original state. */ resetDebuggingnull112 fun resetDebugging() { 113 System.setProperty(DEBUG_PROPERTY_NAME, debugMode) 114 } 115 116 /** Check a condition using coroutines. */ waitFornull117 private suspend fun waitFor(checkPeriod: Duration = CHECK_PERIOD, condition: () -> Boolean) { 118 while (!condition()) { 119 delay(checkPeriod.toMillis()) 120 } 121 } 122 123 /** Check an assertion passes using coroutines. */ assertThatWaitingnull124 private suspend fun assertThatWaiting( 125 checkPeriod: Duration = CHECK_PERIOD, 126 assertion: () -> Unit, 127 ) { 128 while (true) { 129 try { 130 assertion() 131 break 132 } catch (ex: AssertionError) { 133 delay(checkPeriod.toMillis()) 134 } 135 } 136 } 137 138 private const val TAG: String = "Coroutines" 139 140 /** A medium period, to be used for conditions that are expected to change. */ 141 private val CHECK_PERIOD: Duration = Duration.ofMillis(250) 142 143 private val debugMode: String? = 144 System.getProperty(DEBUG_PROPERTY_NAME, DEBUG_PROPERTY_VALUE_AUTO) 145 } 146