Skip to content

Commit bc6bd8b

Browse files
committed
Improve runTest timeout messages
* Clearly separate the cases when the test body finished and was just waiting for the children from the cases where the test body itself kept running; * When the test only awaits the child coroutines, mention `TestScope.backgroundScope` Fixes #4182
1 parent 5e03ca2 commit bc6bd8b

File tree

1 file changed

+24
-13
lines changed

1 file changed

+24
-13
lines changed

kotlinx-coroutines-test/common/src/TestBuilders.kt

+24-13
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33

44
package kotlinx.coroutines.test
55

6+
import kotlinx.atomicfu.atomic
67
import kotlinx.coroutines.*
7-
import kotlinx.coroutines.flow.*
88
import kotlinx.coroutines.selects.*
99
import kotlin.coroutines.*
1010
import kotlin.jvm.*
@@ -308,12 +308,17 @@ public fun TestScope.runTest(
308308
): TestResult = asSpecificImplementation().let { scope ->
309309
scope.enter()
310310
createTestResult {
311+
val testBodyFinished = AtomicBoolean(false)
311312
/** TODO: moving this [AbstractCoroutine.start] call outside [createTestResult] fails on JS. */
312313
scope.start(CoroutineStart.UNDISPATCHED, scope) {
313314
/* we're using `UNDISPATCHED` to avoid the event loop, but we do want to set up the timeout machinery
314315
before any code executes, so we have to park here. */
315316
yield()
316-
testBody()
317+
try {
318+
testBody()
319+
} finally {
320+
testBodyFinished.value = true
321+
}
317322
}
318323
var timeoutError: Throwable? = null
319324
var cancellationException: CancellationException? = null
@@ -336,17 +341,16 @@ public fun TestScope.runTest(
336341
if (exception is TimeoutCancellationException) {
337342
dumpCoroutines()
338343
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+
var message = "After waiting for $timeout, "
345+
message += when {
346+
testBodyFinished.value && activeChildren.isNotEmpty() ->
347+
"there were active child jobs: $activeChildren. " +
348+
"Use `TestScope.backgroundScope` " +
349+
"to launch the coroutines that need to be cancelled when the test body finishes"
350+
testBodyFinished.value ->
351+
"the test completed, but only after the timeout"
352+
else ->
353+
"the test body did not run to completion"
350354
}
351355
timeoutError = UncompletedCoroutinesError(message)
352356
cancellationException = CancellationException("The test timed out")
@@ -603,3 +607,10 @@ public fun TestScope.runTestLegacy(
603607
marker: Int,
604608
unused2: Any?,
605609
): TestResult = runTest(dispatchTimeoutMs = if (marker and 1 != 0) dispatchTimeoutMs else 60_000L, testBody)
610+
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

Comments
 (0)