@@ -303,67 +303,69 @@ public fun runTest(
303
303
public fun TestScope.runTest (
304
304
timeout : Duration = DEFAULT_TIMEOUT ,
305
305
testBody : suspend TestScope .() -> Unit
306
- ): TestResult = asSpecificImplementation().let {
307
- it .enter()
306
+ ): TestResult = asSpecificImplementation().let { scope ->
307
+ scope .enter()
308
308
createTestResult {
309
309
/* * TODO: moving this [AbstractCoroutine.start] call outside [createTestResult] fails on JS. */
310
- it.start(CoroutineStart .UNDISPATCHED , it) {
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 ()
311
314
testBody()
312
315
}
313
- /* *
314
- * We run the tasks in the test coroutine using [Dispatchers.Default]. On JS, this does nothing particularly,
315
- * but on the JVM and Native, this means that the timeout can be processed even while the test runner is busy
316
- * doing some synchronous work.
317
- */
318
- val workRunner = launch(Dispatchers .Default + CoroutineName (" kotlinx.coroutines.test runner" )) {
316
+ var timeoutError: Throwable ? = null
317
+ var cancellationException: CancellationException ? = null
318
+ val workRunner = launch(CoroutineName (" kotlinx.coroutines.test runner" )) {
319
319
while (true ) {
320
320
val executedSomething = testScheduler.tryRunNextTaskUnless { ! isActive }
321
321
if (executedSomething) {
322
322
/* * yield to check for cancellation. On JS, we can't use [ensureActive] here, as the cancellation
323
323
* procedure needs a chance to run concurrently. */
324
324
yield ()
325
325
} else {
326
- // no more tasks, we should suspend until there are some more
326
+ // waiting for the next task to be scheduled, or for the test runner to be cancelled
327
327
testScheduler.receiveDispatchEvent()
328
328
}
329
329
}
330
330
}
331
- var timeoutError: Throwable ? = null
332
331
try {
333
332
withTimeout(timeout) {
334
- it.join()
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()
335
355
workRunner.cancelAndJoin()
336
356
}
337
357
} catch (_: TimeoutCancellationException ) {
338
- val activeChildren = it.children.filter(Job ::isActive).toList()
339
- val completionCause = if (it.isCancelled) it.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 (it.isCompleted)
347
- " , the test coroutine completed"
348
- else
349
- " , the test coroutine was not completed"
350
- }
351
- timeoutError = UncompletedCoroutinesError (message)
352
- dumpCoroutines()
353
- val cancellationException = CancellationException (" The test timed out" )
354
- (it as Job ).cancel(cancellationException)
355
- // we can't abandon the work we're doing, so if it hanged, we'll still hang, despite the timeout.
356
- it.join()
357
- val completion = it.getCompletionExceptionOrNull()
358
+ scope.join()
359
+ val completion = scope.getCompletionExceptionOrNull()
358
360
if (completion != null && completion != = cancellationException) {
359
- timeoutError.addSuppressed(completion)
361
+ timeoutError!! .addSuppressed(completion)
360
362
}
361
363
workRunner.cancelAndJoin()
362
364
} finally {
363
365
backgroundScope.cancel()
364
366
testScheduler.advanceUntilIdleOr { false }
365
- val uncaughtExceptions = it .leave()
366
- throwAll(timeoutError ? : it .getCompletionExceptionOrNull(), uncaughtExceptions)
367
+ val uncaughtExceptions = scope .leave()
368
+ throwAll(timeoutError ? : scope .getCompletionExceptionOrNull(), uncaughtExceptions)
367
369
}
368
370
}
369
371
}
0 commit comments