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.delay
26 import kotlinx.coroutines.runBlocking
27 import kotlinx.coroutines.withTimeout
28 import kotlinx.coroutines.withTimeoutOrNull
29 
30 /** A class that facilitates interacting with coroutines. */
31 object Coroutines {
32 
33     /**
34      * The timeout of a test case, typically varies depending on whether the test is running
35      * locally, on pre-submit or post-submit.
36      */
37     val TEST_TIMEOUT: Duration
38         get() =
39             Duration.ofMillis(
40                 InstrumentationRegistry.getArguments().getString("timeout_msec", "60000").toLong()
41             )
42 
43     /** A long timeout, to be used for actions that are expected to complete. */
44     val TIMEOUT_LONG: Duration
45         get() = TEST_TIMEOUT.dividedBy(2)
46 
47     /** A short timeout, to be used for actions that are expected not to complete. */
48     val TIMEOUT_SHORT: Duration = Duration.ofSeconds(1)
49 
50     /** Shorthand for [runBlocking] combined with [withTimeout]. */
runBlockingWithTimeoutnull51     fun <T> runBlockingWithTimeout(timeout: Duration = TIMEOUT_LONG, block: suspend () -> T): T =
52         runBlocking {
53             withTimeout(timeout.toMillis()) { block() }
54         }
55 
56     /** Shorthand for [runBlocking] combined with [withTimeoutOrNull] */
runBlockingWithTimeoutOrNullnull57     fun <T> runBlockingWithTimeoutOrNull(
58         timeout: Duration = TIMEOUT_LONG,
59         block: suspend () -> T
60     ): T? = runBlocking { withTimeoutOrNull(timeout.toMillis()) { block() } }
61 
62     /** Check a condition using coroutines with a timeout. */
waitForWithTimeoutnull63     fun waitForWithTimeout(
64         timeout: Duration = TIMEOUT_LONG,
65         checkPeriod: Duration = CHECK_PERIOD,
66         condition: () -> Boolean
67     ) {
68         runBlockingWithTimeout(timeout) { waitFor(checkPeriod, condition) }
69     }
70 
71     /** Retries a [fallibleAction] until no errors are thrown or a timeout occurs. */
waitForSuccessWithTimeoutnull72     fun waitForSuccessWithTimeout(
73         timeout: Duration = TIMEOUT_LONG,
74         checkPeriod: Duration = CHECK_PERIOD,
75         fallibleAction: () -> Unit
76     ) {
77         waitForWithTimeout(timeout, checkPeriod) {
78             try {
79                 fallibleAction()
80                 true
81             } catch (ex: Throwable) {
82                 Log.w(TAG, "Encountered failure, retrying until timeout: $ex")
83                 false
84             }
85         }
86     }
87 
88     /**
89      * Enables debug mode for coroutines, in particular this enables stack traces in case of
90      * failures.
91      */
enableDebuggingnull92     fun enableDebugging() {
93         System.setProperty(DEBUG_PROPERTY_NAME, DEBUG_PROPERTY_VALUE_ON)
94     }
95 
96     /** Resets the debug mode to its original state. */
resetDebuggingnull97     fun resetDebugging() {
98         System.setProperty(DEBUG_PROPERTY_NAME, debugMode)
99     }
100 
101     /** Check a condition using coroutines. */
waitFornull102     private suspend fun waitFor(checkPeriod: Duration = CHECK_PERIOD, condition: () -> Boolean) {
103         while (!condition()) {
104             delay(checkPeriod.toMillis())
105         }
106     }
107 
108     private const val TAG: String = "Coroutines"
109 
110     /** A medium period, to be used for conditions that are expected to change. */
111     private val CHECK_PERIOD: Duration = Duration.ofMillis(250)
112 
113     private val debugMode: String? =
114         System.getProperty(DEBUG_PROPERTY_NAME, DEBUG_PROPERTY_VALUE_AUTO)
115 }
116