Skip to content

Commit 219372f

Browse files
authored
Improve runTest timeout messages (#4184)
* 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 dbe1dda commit 219372f

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,15 @@ 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+
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"
350353
}
351354
timeoutError = UncompletedCoroutinesError(message)
352355
cancellationException = CancellationException("The test timed out")
@@ -603,3 +606,11 @@ public fun TestScope.runTestLegacy(
603606
marker: Int,
604607
unused2: Any?,
605608
): 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

Comments
 (0)