3
3
4
4
package kotlinx.coroutines.test
5
5
6
+ import kotlinx.atomicfu.atomic
6
7
import kotlinx.coroutines.*
7
- import kotlinx.coroutines.flow.*
8
8
import kotlinx.coroutines.selects.*
9
9
import kotlin.coroutines.*
10
10
import kotlin.jvm.*
@@ -308,12 +308,17 @@ public fun TestScope.runTest(
308
308
): TestResult = asSpecificImplementation().let { scope ->
309
309
scope.enter()
310
310
createTestResult {
311
+ val testBodyFinished = AtomicBoolean (false )
311
312
/* * TODO: moving this [AbstractCoroutine.start] call outside [createTestResult] fails on JS. */
312
313
scope.start(CoroutineStart .UNDISPATCHED , scope) {
313
314
/* we're using `UNDISPATCHED` to avoid the event loop, but we do want to set up the timeout machinery
314
315
before any code executes, so we have to park here. */
315
316
yield ()
316
- testBody()
317
+ try {
318
+ testBody()
319
+ } finally {
320
+ testBodyFinished.value = true
321
+ }
317
322
}
318
323
var timeoutError: Throwable ? = null
319
324
var cancellationException: CancellationException ? = null
@@ -336,17 +341,15 @@ public fun TestScope.runTest(
336
341
if (exception is TimeoutCancellationException ) {
337
342
dumpCoroutines()
338
343
val activeChildren = scope.children.filter(Job ::isActive).toList()
339
- val completionCause = if (scope.isCancelled) scope.tryGetCompletionCause() else null
340
- var message = " After waiting for $timeout "
341
- if (completionCause == null )
342
- message + = " , the test coroutine is not completing"
343
- if (activeChildren.isNotEmpty())
344
- message + = " , there were active child jobs: $activeChildren "
345
- if (completionCause != null && activeChildren.isEmpty()) {
346
- message + = if (scope.isCompleted)
347
- " , the test coroutine completed"
348
- else
349
- " , the test coroutine was not completed"
344
+ val message = " After waiting for $timeout , " + when {
345
+ testBodyFinished.value && activeChildren.isNotEmpty() ->
346
+ " there were active child jobs: $activeChildren . " +
347
+ " Use `TestScope.backgroundScope` " +
348
+ " to launch the coroutines that need to be cancelled when the test body finishes"
349
+ testBodyFinished.value ->
350
+ " the test completed, but only after the timeout"
351
+ else ->
352
+ " the test body did not run to completion"
350
353
}
351
354
timeoutError = UncompletedCoroutinesError (message)
352
355
cancellationException = CancellationException (" The test timed out" )
@@ -603,3 +606,11 @@ public fun TestScope.runTestLegacy(
603
606
marker : Int ,
604
607
unused2 : Any? ,
605
608
): TestResult = runTest(dispatchTimeoutMs = if (marker and 1 != 0 ) dispatchTimeoutMs else 60_000L , testBody)
609
+
610
+ // Remove after https://youtrack.jetbrains.com/issue/KT-62423/
611
+ private class AtomicBoolean (initial : Boolean ) {
612
+ private val container = atomic(initial)
613
+ var value: Boolean
614
+ get() = container.value
615
+ set(value: Boolean ) { container.value = value }
616
+ }
0 commit comments