• 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
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