xref: /aosp_15_r20/external/kotlinx.coroutines/test-utils/common/src/TestBase.common.kt (revision 7a7160fed73afa6648ef8aa100d4a336fe921d9a)

<lambda>null1 @file:Suppress("unused")
2 package kotlinx.coroutines.testing
3 
4 import kotlinx.atomicfu.*
5 import kotlinx.coroutines.flow.*
6 import kotlinx.coroutines.*
7 import kotlinx.coroutines.internal.*
8 import kotlin.coroutines.*
9 import kotlin.test.*
10 import kotlin.time.*
11 import kotlin.time.Duration.Companion.seconds
12 
13 /**
14  * The number of milliseconds that is sure not to pass [assertRunsFast].
15  */
16 const val SLOW = 100_000L
17 
18 /**
19  * Asserts that a block completed within [timeout].
20  */
21 inline fun <T> assertRunsFast(timeout: Duration, block: () -> T): T {
22     val result: T
23     val elapsed = TimeSource.Monotonic.measureTime { result = block() }
24     assertTrue("Should complete in $timeout, but took $elapsed") { elapsed < timeout }
25     return result
26 }
27 
28 /**
29  * Asserts that a block completed within two seconds.
30  */
assertRunsFastnull31 inline fun <T> assertRunsFast(block: () -> T): T = assertRunsFast(2.seconds, block)
32 
33 /**
34  * Whether the tests should trace their calls to `expect` and `finish` with `println`.
35  * `false` by default. On the JVM, can be set to `true` by setting the `test.verbose` system property.
36  */
37 expect val VERBOSE: Boolean
38 
39 interface OrderedExecution {
40     /** Expect the next action to be [index] in order. */
41     fun expect(index: Int)
42 
43     /** Expect this action to be final, with the given [index]. */
44     fun finish(index: Int)
45 
46     /** * Asserts that this line is never executed. */
47     fun expectUnreached()
48 
49     /**
50      * Checks that [finish] was called.
51      *
52      * By default, it is allowed to not call [finish] if [expect] was not called.
53      * This is useful for tests that don't check the ordering of events.
54      * When [allowNotUsingExpect] is set to `false`, it is an error to not call [finish] in any case.
55      */
56     fun checkFinishCall(allowNotUsingExpect: Boolean = true)
57 
58     class Impl : OrderedExecution {
59         private val actionIndex = atomic(0)
60 
61         override fun expect(index: Int) {
62             val wasIndex = actionIndex.incrementAndGet()
63             if (VERBOSE) println("expect($index), wasIndex=$wasIndex")
64             check(index == wasIndex) {
65                 if (wasIndex < 0) "Expecting action index $index but it is actually finished"
66                 else "Expecting action index $index but it is actually $wasIndex"
67             }
68         }
69 
70         override fun finish(index: Int) {
71             val wasIndex = actionIndex.getAndSet(Int.MIN_VALUE) + 1
72             if (VERBOSE) println("finish($index), wasIndex=${if (wasIndex < 0) "finished" else wasIndex}")
73             check(index == wasIndex) {
74                 if (wasIndex < 0) "Finished more than once"
75                 else "Finishing with action index $index but it is actually $wasIndex"
76             }
77         }
78 
79         override fun expectUnreached() {
80             error("Should not be reached, ${
81                 actionIndex.value.let {
82                     when {
83                         it < 0 -> "already finished"
84                         it == 0 -> "'expect' was not called yet"
85                         else -> "the last executed action was $it"
86                     }
87                 }
88             }")
89         }
90 
91         override fun checkFinishCall(allowNotUsingExpect: Boolean) {
92             actionIndex.value.let {
93                 assertTrue(
94                     it < 0 || allowNotUsingExpect && it == 0,
95                     "Expected `finish(${actionIndex.value + 1})` to be called, but the test finished"
96                 )
97             }
98         }
99     }
100 }
101 
102 interface ErrorCatching {
103     /**
104      * Returns `true` if errors were logged in the test.
105      */
hasErrornull106     fun hasError(): Boolean
107 
108     /**
109      * Directly reports an error to the test catching facilities.
110      */
111     fun reportError(error: Throwable)
112 
113     class Impl : ErrorCatching {
114 
115         private val errors = mutableListOf<Throwable>()
116         private val lock = SynchronizedObject()
117         private var closed = false
118 
119         override fun hasError(): Boolean = synchronized(lock) {
120             errors.isNotEmpty()
121         }
122 
123         override fun reportError(error: Throwable) {
124             synchronized(lock) {
125                 if (closed) {
126                     lastResortReportException(error)
127                 } else {
128                     errors.add(error)
129                 }
130             }
131         }
132 
133         fun close() {
134             synchronized(lock) {
135                 if (closed) {
136                     lastResortReportException(IllegalStateException("ErrorCatching closed more than once"))
137                 }
138                 closed = true
139                 errors.firstOrNull()?.let {
140                     for (error in errors.drop(1))
141                         it.addSuppressed(error)
142                     throw it
143                 }
144             }
145         }
146     }
147 }
148 
149 /**
150  * Reports an error *somehow* so that it doesn't get completely forgotten.
151  */
lastResortReportExceptionnull152 internal expect fun lastResortReportException(error: Throwable)
153 
154 /**
155  * Throws [IllegalStateException] when `value` is false, like `check` in stdlib, but also ensures that the
156  * test will not complete successfully even if this exception is consumed somewhere in the test.
157  */
158 public inline fun ErrorCatching.check(value: Boolean, lazyMessage: () -> Any) {
159     if (!value) error(lazyMessage())
160 }
161 
162 /**
163  * Throws [IllegalStateException], like `error` in stdlib, but also ensures that the test will not
164  * complete successfully even if this exception is consumed somewhere in the test.
165  */
ErrorCatchingnull166 fun ErrorCatching.error(message: Any, cause: Throwable? = null): Nothing {
167     throw IllegalStateException(message.toString(), cause).also {
168         reportError(it)
169     }
170 }
171 
172 /**
173  * A class inheriting from which allows to check the execution order inside tests.
174  *
175  * @see TestBase
176  */
177 open class OrderedExecutionTestBase : OrderedExecution
178 {
179     // TODO: move to by-delegation when [reset] is no longer needed.
180     private var orderedExecutionDelegate = OrderedExecution.Impl()
181 
182     @AfterTest
checkFinishednull183     fun checkFinished() { orderedExecutionDelegate.checkFinishCall() }
184 
185     /** Resets counter and finish flag. Workaround for parametrized tests absence in common */
resetnull186     public fun reset() {
187         orderedExecutionDelegate.checkFinishCall()
188         orderedExecutionDelegate = OrderedExecution.Impl()
189     }
190 
expectnull191     override fun expect(index: Int) = orderedExecutionDelegate.expect(index)
192 
193     override fun finish(index: Int) = orderedExecutionDelegate.finish(index)
194 
195     override fun expectUnreached() = orderedExecutionDelegate.expectUnreached()
196 
197     override fun checkFinishCall(allowNotUsingExpect: Boolean) =
198         orderedExecutionDelegate.checkFinishCall(allowNotUsingExpect)
199 }
200 
201 fun <T> T.void() {}
202 
203 @OptionalExpectation
204 expect annotation class NoJs()
205 
206 @OptionalExpectation
207 expect annotation class NoNative()
208 
209 expect val isStressTest: Boolean
210 expect val stressTestMultiplier: Int
211 expect val stressTestMultiplierSqrt: Int
212 
213 /**
214  * The result of a multiplatform asynchronous test.
215  * Aliases into Unit on K/JVM and K/N, and into Promise on K/JS.
216  */
217 @Suppress("NO_ACTUAL_FOR_EXPECT")
218 public expect class TestResult
219 
220 public expect open class TestBase(): OrderedExecutionTestBase, ErrorCatching {
printlnnull221     public fun println(message: Any?)
222 
223     public fun runTest(
224         expected: ((Throwable) -> Boolean)? = null,
225         unhandled: List<(Throwable) -> Boolean> = emptyList(),
226         block: suspend CoroutineScope.() -> Unit
227     ): TestResult
228 }
229 
230 public suspend inline fun hang(onCancellation: () -> Unit) {
231     try {
232         suspendCancellableCoroutine<Unit> { }
233     } finally {
234         onCancellation()
235     }
236 }
237 
assertFailsWithnull238 suspend inline fun <reified T : Throwable> assertFailsWith(flow: Flow<*>) = assertFailsWith<T> { flow.collect() }
239 
accnull240 public suspend fun Flow<Int>.sum() = fold(0) { acc, value -> acc + value }
accnull241 public suspend fun Flow<Long>.longSum() = fold(0L) { acc, value -> acc + value }
242 
243 // data is added to avoid stacktrace recovery because CopyableThrowable is not accessible from common modules
244 public class TestException(message: String? = null, private val data: Any? = null) : Throwable(message)
245 public class TestException1(message: String? = null, private val data: Any? = null) : Throwable(message)
246 public class TestException2(message: String? = null, private val data: Any? = null) : Throwable(message)
247 public class TestException3(message: String? = null, private val data: Any? = null) : Throwable(message)
248 public class TestCancellationException(message: String? = null, private val data: Any? = null) :
249     CancellationException(message)
250 
251 public class TestRuntimeException(message: String? = null, private val data: Any? = null) : RuntimeException(message)
252 public class RecoverableTestException(message: String? = null) : RuntimeException(message)
253 public class RecoverableTestCancellationException(message: String? = null) : CancellationException(message)
254 
wrapperDispatchernull255 public fun wrapperDispatcher(context: CoroutineContext): CoroutineContext {
256     val dispatcher = context[ContinuationInterceptor] as CoroutineDispatcher
257     return object : CoroutineDispatcher() {
258         override fun isDispatchNeeded(context: CoroutineContext): Boolean =
259             dispatcher.isDispatchNeeded(context)
260 
261         override fun dispatch(context: CoroutineContext, block: Runnable) =
262             dispatcher.dispatch(context, block)
263     }
264 }
265 
wrapperDispatchernull266 public suspend fun wrapperDispatcher(): CoroutineContext = wrapperDispatcher(coroutineContext)
267 class BadClass {
268     override fun equals(other: Any?): Boolean = error("equals")
269     override fun hashCode(): Int = error("hashCode")
270     override fun toString(): String = error("toString")
271 }
272 
273 public expect val isJavaAndWindows: Boolean
274 
275 public expect val isNative: Boolean
276 
277 /*
278  * In common tests we emulate parameterized tests
279  * by iterating over parameters space in the single @Test method.
280  * This kind of tests is too slow for JS and does not fit into
281  * the default Mocha timeout, so we're using this flag to bail-out
282  * and run such tests only on JVM and K/N.
283  */
284 public expect val isBoundByJsTestTimeout: Boolean
285