• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
3  */
4 @file:JvmName("TestBuildersKt")
5 @file:JvmMultifileClass
6 
7 package kotlinx.coroutines.test
8 
9 import kotlinx.coroutines.*
10 import kotlinx.coroutines.flow.*
11 import kotlinx.coroutines.selects.*
12 import kotlin.coroutines.*
13 import kotlin.jvm.*
14 import kotlin.time.*
15 import kotlin.time.Duration.Companion.milliseconds
16 import kotlin.time.Duration.Companion.seconds
17 import kotlinx.coroutines.internal.*
18 
19 /**
20  * A test result.
21  *
22  * * On JVM and Native, this resolves to [Unit], representing the fact that tests are run in a blocking manner on these
23  *   platforms: a call to a function returning a [TestResult] will simply execute the test inside it.
24  * * On JS, this is a `Promise`, which reflects the fact that the test-running function does not wait for a test to
25  *   finish. The JS test frameworks typically support returning `Promise` from a test and will correctly handle it.
26  *
27  * Because of the behavior on JS, extra care must be taken when writing multiplatform tests to avoid losing test errors:
28  * * Don't do anything after running the functions returning a [TestResult]. On JS, this code will execute *before* the
29  *   test finishes.
30  * * As a corollary, don't run functions returning a [TestResult] more than once per test. The only valid thing to do
31  *   with a [TestResult] is to immediately `return` it from a test.
32  * * Don't nest functions returning a [TestResult].
33  */
34 @Suppress("NO_ACTUAL_FOR_EXPECT")
35 public expect class TestResult
36 
37 /**
38  * Executes [testBody] as a test in a new coroutine, returning [TestResult].
39  *
40  * On JVM and Native, this function behaves similarly to `runBlocking`, with the difference that the code that it runs
41  * will skip delays. This allows to use [delay] in tests without causing them to take more time than necessary.
42  * On JS, this function creates a `Promise` that executes the test body with the delay-skipping behavior.
43  *
44  * ```
45  * @Test
46  * fun exampleTest() = runTest {
47  *     val deferred = async {
48  *         delay(1.seconds)
49  *         async {
50  *             delay(1.seconds)
51  *         }.await()
52  *     }
53  *
54  *     deferred.await() // result available immediately
55  * }
56  * ```
57  *
58  * The platform difference entails that, in order to use this function correctly in common code, one must always
59  * immediately return the produced [TestResult] from the test method, without doing anything else afterwards. See
60  * [TestResult] for details on this.
61  *
62  * The test is run on a single thread, unless other [CoroutineDispatcher] are used for child coroutines.
63  * Because of this, child coroutines are not executed in parallel to the test body.
64  * In order for the spawned-off asynchronous code to actually be executed, one must either [yield] or suspend the
65  * test body some other way, or use commands that control scheduling (see [TestCoroutineScheduler]).
66  *
67  * ```
68  * @Test
69  * fun exampleWaitingForAsyncTasks1() = runTest {
70  *     // 1
71  *     val job = launch {
72  *         // 3
73  *     }
74  *     // 2
75  *     job.join() // the main test coroutine suspends here, so the child is executed
76  *     // 4
77  * }
78  *
79  * @Test
80  * fun exampleWaitingForAsyncTasks2() = runTest {
81  *     // 1
82  *     launch {
83  *         // 3
84  *     }
85  *     // 2
86  *     testScheduler.advanceUntilIdle() // runs the tasks until their queue is empty
87  *     // 4
88  * }
89  * ```
90  *
91  * ### Task scheduling
92  *
93  * Delay skipping is achieved by using virtual time.
94  * If [Dispatchers.Main] is set to a [TestDispatcher] via [Dispatchers.setMain] before the test,
95  * then its [TestCoroutineScheduler] is used;
96  * otherwise, a new one is automatically created (or taken from [context] in some way) and can be used to control
97  * the virtual time, advancing it, running the tasks scheduled at a specific time etc.
98  * The scheduler can be accessed via [TestScope.testScheduler].
99  *
100  * Delays in code that runs inside dispatchers that don't use a [TestCoroutineScheduler] don't get skipped:
101  * ```
102  * @Test
103  * fun exampleTest() = runTest {
104  *     val elapsed = TimeSource.Monotonic.measureTime {
105  *         val deferred = async {
106  *             delay(1.seconds) // will be skipped
107  *             withContext(Dispatchers.Default) {
108  *                 delay(5.seconds) // Dispatchers.Default doesn't know about TestCoroutineScheduler
109  *             }
110  *         }
111  *         deferred.await()
112  *     }
113  *     println(elapsed) // about five seconds
114  * }
115  * ```
116  *
117  * ### Failures
118  *
119  * #### Test body failures
120  *
121  * If the created coroutine completes with an exception, then this exception will be thrown at the end of the test.
122  *
123  * #### Timing out
124  *
125  * There's a built-in timeout of 10 seconds for the test body. If the test body doesn't complete within this time,
126  * then the test fails with an [AssertionError]. The timeout can be changed by setting the [timeout] parameter.
127  *
128  * On timeout, the test body is cancelled so that the test finishes. If the code inside the test body does not
129  * respond to cancellation, the timeout will not be able to make the test execution stop.
130  * In that case, the test will hang despite the attempt to terminate it.
131  *
132  * On the JVM, if `DebugProbes` from the `kotlinx-coroutines-debug` module are installed, the current dump of the
133  * coroutines' stack is printed to the console on timeout before the test body is cancelled.
134  *
135  * #### Reported exceptions
136  *
137  * Unhandled exceptions will be thrown at the end of the test.
138  * If uncaught exceptions happen after the test finishes, they are propagated in a platform-specific manner:
139  * see [handleCoroutineException] for details.
140  * If the test coroutine completes with an exception, the unhandled exceptions are suppressed by it.
141  *
142  * #### Uncompleted coroutines
143  *
144  * Otherwise, the test will hang until all the coroutines launched inside [testBody] complete.
145  * This may be an issue when there are some coroutines that are not supposed to complete, like infinite loops that
146  * perform some background work and are supposed to outlive the test.
147  * In that case, [TestScope.backgroundScope] can be used to launch such coroutines.
148  * They will be cancelled automatically when the test finishes.
149  *
150  * ### Configuration
151  *
152  * [context] can be used to affect the environment of the code under test. Beside just being passed to the coroutine
153  * scope created for the test, [context] also can be used to change how the test is executed.
154  * See the [TestScope] constructor function documentation for details.
155  *
156  * @throws IllegalArgumentException if the [context] is invalid. See the [TestScope] constructor docs for details.
157  */
158 public fun runTest(
159     context: CoroutineContext = EmptyCoroutineContext,
160     timeout: Duration = DEFAULT_TIMEOUT,
161     testBody: suspend TestScope.() -> Unit
162 ): TestResult {
163     check(context[RunningInRunTest] == null) {
164         "Calls to `runTest` can't be nested. Please read the docs on `TestResult` for details."
165     }
166     return TestScope(context + RunningInRunTest).runTest(timeout, testBody)
167 }
168 
169 /**
170  * Executes [testBody] as a test in a new coroutine, returning [TestResult].
171  *
172  * On JVM and Native, this function behaves similarly to `runBlocking`, with the difference that the code that it runs
173  * will skip delays. This allows to use [delay] in without causing the tests to take more time than necessary.
174  * On JS, this function creates a `Promise` that executes the test body with the delay-skipping behavior.
175  *
176  * ```
177  * @Test
178  * fun exampleTest() = runTest {
179  *     val deferred = async {
180  *         delay(1.seconds)
181  *         async {
182  *             delay(1.seconds)
183  *         }.await()
184  *     }
185  *
186  *     deferred.await() // result available immediately
187  * }
188  * ```
189  *
190  * The platform difference entails that, in order to use this function correctly in common code, one must always
191  * immediately return the produced [TestResult] from the test method, without doing anything else afterwards. See
192  * [TestResult] for details on this.
193  *
194  * The test is run in a single thread, unless other [CoroutineDispatcher] are used for child coroutines.
195  * Because of this, child coroutines are not executed in parallel to the test body.
196  * In order for the spawned-off asynchronous code to actually be executed, one must either [yield] or suspend the
197  * test body some other way, or use commands that control scheduling (see [TestCoroutineScheduler]).
198  *
199  * ```
200  * @Test
201  * fun exampleWaitingForAsyncTasks1() = runTest {
202  *     // 1
203  *     val job = launch {
204  *         // 3
205  *     }
206  *     // 2
207  *     job.join() // the main test coroutine suspends here, so the child is executed
208  *     // 4
209  * }
210  *
211  * @Test
212  * fun exampleWaitingForAsyncTasks2() = runTest {
213  *     // 1
214  *     launch {
215  *         // 3
216  *     }
217  *     // 2
218  *     advanceUntilIdle() // runs the tasks until their queue is empty
219  *     // 4
220  * }
221  * ```
222  *
223  * ### Task scheduling
224  *
225  * Delay-skipping is achieved by using virtual time.
226  * If [Dispatchers.Main] is set to a [TestDispatcher] via [Dispatchers.setMain] before the test,
227  * then its [TestCoroutineScheduler] is used;
228  * otherwise, a new one is automatically created (or taken from [context] in some way) and can be used to control
229  * the virtual time, advancing it, running the tasks scheduled at a specific time etc.
230  * Some convenience methods are available on [TestScope] to control the scheduler.
231  *
232  * Delays in code that runs inside dispatchers that don't use a [TestCoroutineScheduler] don't get skipped:
233  * ```
234  * @Test
235  * fun exampleTest() = runTest {
236  *     val elapsed = TimeSource.Monotonic.measureTime {
237  *         val deferred = async {
238  *             delay(1.seconds) // will be skipped
239  *             withContext(Dispatchers.Default) {
240  *                 delay(5.seconds) // Dispatchers.Default doesn't know about TestCoroutineScheduler
241  *             }
242  *         }
243  *         deferred.await()
244  *     }
245  *     println(elapsed) // about five seconds
246  * }
247  * ```
248  *
249  * ### Failures
250  *
251  * #### Test body failures
252  *
253  * If the created coroutine completes with an exception, then this exception will be thrown at the end of the test.
254  *
255  * #### Reported exceptions
256  *
257  * Unhandled exceptions will be thrown at the end of the test.
258  * If the uncaught exceptions happen after the test finishes, the error is propagated in a platform-specific manner.
259  * If the test coroutine completes with an exception, the unhandled exceptions are suppressed by it.
260  *
261  * #### Uncompleted coroutines
262  *
263  * This method requires that, after the test coroutine has completed, all the other coroutines launched inside
264  * [testBody] also complete, or are cancelled.
265  * Otherwise, the test will be failed (which, on JVM and Native, means that [runTest] itself will throw
266  * [AssertionError], whereas on JS, the `Promise` will fail with it).
267  *
268  * In the general case, if there are active jobs, it's impossible to detect if they are going to complete eventually due
269  * to the asynchronous nature of coroutines. In order to prevent tests hanging in this scenario, [runTest] will wait
270  * for [dispatchTimeoutMs] from the moment when [TestCoroutineScheduler] becomes
271  * idle before throwing [AssertionError]. If some dispatcher linked to [TestCoroutineScheduler] receives a
272  * task during that time, the timer gets reset.
273  *
274  * ### Configuration
275  *
276  * [context] can be used to affect the environment of the code under test. Beside just being passed to the coroutine
277  * scope created for the test, [context] also can be used to change how the test is executed.
278  * See the [TestScope] constructor function documentation for details.
279  *
280  * @throws IllegalArgumentException if the [context] is invalid. See the [TestScope] constructor docs for details.
281  */
282 @Deprecated(
283     "Define a total timeout for the whole test instead of using dispatchTimeoutMs. " +
284         "Warning: the proposed replacement is not identical as it uses 'dispatchTimeoutMs' as the timeout for the whole test!",
285     ReplaceWith("runTest(context, timeout = dispatchTimeoutMs.milliseconds, testBody)",
286         "kotlin.time.Duration.Companion.milliseconds"),
287     DeprecationLevel.WARNING
288 ) // Warning since 1.7.0, was experimental in 1.6.x
runTestnull289 public fun runTest(
290     context: CoroutineContext = EmptyCoroutineContext,
291     dispatchTimeoutMs: Long,
292     testBody: suspend TestScope.() -> Unit
293 ): TestResult {
294     if (context[RunningInRunTest] != null)
295         throw IllegalStateException("Calls to `runTest` can't be nested. Please read the docs on `TestResult` for details.")
296     @Suppress("DEPRECATION")
297     return TestScope(context + RunningInRunTest).runTest(dispatchTimeoutMs = dispatchTimeoutMs, testBody)
298 }
299 
300 /**
301  * Performs [runTest] on an existing [TestScope]. See the documentation for [runTest] for details.
302  */
runTestnull303 public fun TestScope.runTest(
304     timeout: Duration = DEFAULT_TIMEOUT,
305     testBody: suspend TestScope.() -> Unit
306 ): TestResult = asSpecificImplementation().let { scope ->
307     scope.enter()
308     createTestResult {
309         /** TODO: moving this [AbstractCoroutine.start] call outside [createTestResult] fails on JS. */
310         scope.start(CoroutineStart.UNDISPATCHED, scope) {
311             /* we're using `UNDISPATCHED` to avoid the event loop, but we do want to set up the timeout machinery
312             before any code executes, so we have to park here. */
313             yield()
314             testBody()
315         }
316         var timeoutError: Throwable? = null
317         var cancellationException: CancellationException? = null
318         val workRunner = launch(CoroutineName("kotlinx.coroutines.test runner")) {
319             while (true) {
320                 val executedSomething = testScheduler.tryRunNextTaskUnless { !isActive }
321                 if (executedSomething) {
322                     /** yield to check for cancellation. On JS, we can't use [ensureActive] here, as the cancellation
323                      * procedure needs a chance to run concurrently. */
324                     yield()
325                 } else {
326                     // waiting for the next task to be scheduled, or for the test runner to be cancelled
327                     testScheduler.receiveDispatchEvent()
328                 }
329             }
330         }
331         try {
332             withTimeout(timeout) {
333                 coroutineContext.job.invokeOnCompletion(onCancelling = true) { exception ->
334                     if (exception is TimeoutCancellationException) {
335                         dumpCoroutines()
336                         val activeChildren = scope.children.filter(Job::isActive).toList()
337                         val completionCause = if (scope.isCancelled) scope.tryGetCompletionCause() else null
338                         var message = "After waiting for $timeout"
339                         if (completionCause == null)
340                             message += ", the test coroutine is not completing"
341                         if (activeChildren.isNotEmpty())
342                             message += ", there were active child jobs: $activeChildren"
343                         if (completionCause != null && activeChildren.isEmpty()) {
344                             message += if (scope.isCompleted)
345                                 ", the test coroutine completed"
346                             else
347                                 ", the test coroutine was not completed"
348                         }
349                         timeoutError = UncompletedCoroutinesError(message)
350                         cancellationException = CancellationException("The test timed out")
351                         (scope as Job).cancel(cancellationException!!)
352                     }
353                 }
354                 scope.join()
355                 workRunner.cancelAndJoin()
356             }
357         } catch (_: TimeoutCancellationException) {
358             scope.join()
359             val completion = scope.getCompletionExceptionOrNull()
360             if (completion != null && completion !== cancellationException) {
361                 timeoutError!!.addSuppressed(completion)
362             }
363             workRunner.cancelAndJoin()
364         } finally {
365             backgroundScope.cancel()
366             testScheduler.advanceUntilIdleOr { false }
367             val uncaughtExceptions = scope.leave()
368             throwAll(timeoutError ?: scope.getCompletionExceptionOrNull(), uncaughtExceptions)
369         }
370     }
371 }
372 
373 /**
374  * Performs [runTest] on an existing [TestScope].
375  *
376  * In the general case, if there are active jobs, it's impossible to detect if they are going to complete eventually due
377  * to the asynchronous nature of coroutines. In order to prevent tests hanging in this scenario, [runTest] will wait
378  * for [dispatchTimeoutMs] from the moment when [TestCoroutineScheduler] becomes
379  * idle before throwing [AssertionError]. If some dispatcher linked to [TestCoroutineScheduler] receives a
380  * task during that time, the timer gets reset.
381  */
382 @Deprecated(
383     "Define a total timeout for the whole test instead of using dispatchTimeoutMs. " +
384         "Warning: the proposed replacement is not identical as it uses 'dispatchTimeoutMs' as the timeout for the whole test!",
385     ReplaceWith("this.runTest(timeout = dispatchTimeoutMs.milliseconds, testBody)",
386         "kotlin.time.Duration.Companion.milliseconds"),
387     DeprecationLevel.WARNING
388 ) // Warning since 1.7.0, was experimental in 1.6.x
runTestnull389 public fun TestScope.runTest(
390     dispatchTimeoutMs: Long,
391     testBody: suspend TestScope.() -> Unit
392 ): TestResult = asSpecificImplementation().let {
393     it.enter()
394     @Suppress("DEPRECATION")
395     createTestResult {
396         runTestCoroutineLegacy(it, dispatchTimeoutMs.milliseconds, TestScopeImpl::tryGetCompletionCause, testBody) {
397             backgroundScope.cancel()
398             testScheduler.advanceUntilIdleOr { false }
399             it.legacyLeave()
400         }
401     }
402 }
403 
404 /**
405  * Runs [testProcedure], creating a [TestResult].
406  */
407 @Suppress("NO_ACTUAL_FOR_EXPECT") // actually suppresses `TestResult`
createTestResultnull408 internal expect fun createTestResult(testProcedure: suspend CoroutineScope.() -> Unit): TestResult
409 
410 /** A coroutine context element indicating that the coroutine is running inside `runTest`. */
411 internal object RunningInRunTest : CoroutineContext.Key<RunningInRunTest>, CoroutineContext.Element {
412     override val key: CoroutineContext.Key<*>
413         get() = this
414 
415     override fun toString(): String = "RunningInRunTest"
416 }
417 
418 /** The default timeout to use when waiting for asynchronous completions of the coroutines managed by
419  * a [TestCoroutineScheduler]. */
420 internal const val DEFAULT_DISPATCH_TIMEOUT_MS = 60_000L
421 
422 /**
423  * The default timeout to use when running a test.
424  */
425 internal val DEFAULT_TIMEOUT = 10.seconds
426 
427 /**
428  * Run the [body][testBody] of the [test coroutine][coroutine], waiting for asynchronous completions for at most
429  * [dispatchTimeout] and performing the [cleanup] procedure at the end.
430  *
431  * [tryGetCompletionCause] is the [JobSupport.completionCause], which is passed explicitly because it is protected.
432  *
433  * The [cleanup] procedure may either throw [UncompletedCoroutinesError] to denote that child coroutines were leaked, or
434  * return a list of uncaught exceptions that should be reported at the end of the test.
435  */
436 @Deprecated("Used for support of legacy behavior")
runTestCoroutineLegacynull437 internal suspend fun <T : AbstractCoroutine<Unit>> CoroutineScope.runTestCoroutineLegacy(
438     coroutine: T,
439     dispatchTimeout: Duration,
440     tryGetCompletionCause: T.() -> Throwable?,
441     testBody: suspend T.() -> Unit,
442     cleanup: () -> List<Throwable>,
443 ) {
444     val scheduler = coroutine.coroutineContext[TestCoroutineScheduler]!!
445     /** TODO: moving this [AbstractCoroutine.start] call outside [createTestResult] fails on JS. */
446     coroutine.start(CoroutineStart.UNDISPATCHED, coroutine) {
447         testBody()
448     }
449     /**
450      * This is the legacy behavior, kept for now for compatibility only.
451      *
452      * The general procedure here is as follows:
453      * 1. Try running the work that the scheduler knows about, both background and foreground.
454      *
455      * 2. Wait until we run out of foreground work to do. This could mean one of the following:
456      *    * The main coroutine is already completed. This is checked separately; then we leave the procedure.
457      *    * It's switched to another dispatcher that doesn't know about the [TestCoroutineScheduler].
458      *    * Generally, it's waiting for something external (like a network request, or just an arbitrary callback).
459      *    * The test simply hanged.
460      *    * The main coroutine is waiting for some background work.
461      *
462      * 3. We await progress from things that are not the code under test:
463      *    the background work that the scheduler knows about, the external callbacks,
464      *    the work on dispatchers not linked to the scheduler, etc.
465      *
466      *    When we observe that the code under test can proceed, we go to step 1 again.
467      *    If there is no activity for [dispatchTimeoutMs] milliseconds, we consider the test to have hanged.
468      *
469      *    The background work is not running on a dedicated thread.
470      *    Instead, the test thread itself is used, by spawning a separate coroutine.
471      */
472     var completed = false
473     while (!completed) {
474         scheduler.advanceUntilIdle()
475         if (coroutine.isCompleted) {
476             /* don't even enter `withTimeout`; this allows to use a timeout of zero to check that there are no
477            non-trivial dispatches. */
478             completed = true
479             continue
480         }
481         // in case progress depends on some background work, we need to keep spinning it.
482         val backgroundWorkRunner = launch(CoroutineName("background work runner")) {
483             while (true) {
484                 val executedSomething = scheduler.tryRunNextTaskUnless { !isActive }
485                 if (executedSomething) {
486                     // yield so that the `select` below has a chance to finish successfully or time out
487                     yield()
488                 } else {
489                     // no more tasks, we should suspend until there are some more.
490                     // this doesn't interfere with the `select` below, because different channels are used.
491                     scheduler.receiveDispatchEvent()
492                 }
493             }
494         }
495         try {
496             select<Unit> {
497                 coroutine.onJoin {
498                     // observe that someone completed the test coroutine and leave without waiting for the timeout
499                     completed = true
500                 }
501                 scheduler.onDispatchEventForeground {
502                     // we received knowledge that `scheduler` observed a dispatch event, so we reset the timeout
503                 }
504                 onTimeout(dispatchTimeout) {
505                     throw handleTimeout(coroutine, dispatchTimeout, tryGetCompletionCause, cleanup)
506                 }
507             }
508         } finally {
509             backgroundWorkRunner.cancelAndJoin()
510         }
511     }
512     coroutine.getCompletionExceptionOrNull()?.let { exception ->
513         val exceptions = try {
514             cleanup()
515         } catch (e: UncompletedCoroutinesError) {
516             // it's normal that some jobs are not completed if the test body has failed, won't clutter the output
517             emptyList()
518         }
519         throwAll(exception, exceptions)
520     }
521     throwAll(null, cleanup())
522 }
523 
524 /**
525  * Invoked on timeout in [runTest]. Just builds a nice [UncompletedCoroutinesError] and returns it.
526  */
handleTimeoutnull527 private inline fun <T : AbstractCoroutine<Unit>> handleTimeout(
528     coroutine: T,
529     dispatchTimeout: Duration,
530     tryGetCompletionCause: T.() -> Throwable?,
531     cleanup: () -> List<Throwable>,
532 ): AssertionError {
533     val uncaughtExceptions = try {
534         cleanup()
535     } catch (e: UncompletedCoroutinesError) {
536         // we expect these and will instead throw a more informative exception.
537         emptyList()
538     }
539     val activeChildren = coroutine.children.filter { it.isActive }.toList()
540     val completionCause = if (coroutine.isCancelled) coroutine.tryGetCompletionCause() else null
541     var message = "After waiting for $dispatchTimeout"
542     if (completionCause == null)
543         message += ", the test coroutine is not completing"
544     if (activeChildren.isNotEmpty())
545         message += ", there were active child jobs: $activeChildren"
546     if (completionCause != null && activeChildren.isEmpty()) {
547         message += if (coroutine.isCompleted)
548             ", the test coroutine completed"
549         else
550             ", the test coroutine was not completed"
551     }
552     val error = UncompletedCoroutinesError(message)
553     completionCause?.let { cause -> error.addSuppressed(cause) }
554     uncaughtExceptions.forEach { error.addSuppressed(it) }
555     return error
556 }
557 
throwAllnull558 internal fun throwAll(head: Throwable?, other: List<Throwable>) {
559     if (head != null) {
560         other.forEach { head.addSuppressed(it) }
561         throw head
562     } else {
563         with(other) {
564             firstOrNull()?.apply {
565                 drop(1).forEach { addSuppressed(it) }
566                 throw this
567             }
568         }
569     }
570 }
571 
dumpCoroutinesnull572 internal expect fun dumpCoroutines()
573 
574 @Deprecated(
575     "This is for binary compatibility with the `runTest` overload that existed at some point",
576     level = DeprecationLevel.HIDDEN
577 )
578 @JvmName("runTest\$default")
579 @Suppress("DEPRECATION", "UNUSED_PARAMETER")
580 public fun TestScope.runTestLegacy(
581     dispatchTimeoutMs: Long,
582     testBody: suspend TestScope.() -> Unit,
583     marker: Int,
584     unused2: Any?,
585 ): TestResult = runTest(dispatchTimeoutMs = if (marker and 1 != 0) dispatchTimeoutMs else 60_000L, testBody)
586