@@ -303,67 +303,70 @@ 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
+ yield ()
311
312
testBody()
312
313
}
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" )) {
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
- // no more tasks, we should suspend until there are some more
327
- testScheduler.receiveDispatchEvent()
328
- }
329
- }
330
- }
331
314
var timeoutError: Throwable ? = null
315
+ var cancellationException: CancellationException ? = null
332
316
try {
333
317
withTimeout(timeout) {
334
- it.join()
335
- workRunner.cancelAndJoin()
318
+ coroutineContext.job.invokeOnCompletion(onCancelling = true ) { exception ->
319
+ if (exception is TimeoutCancellationException ) {
320
+ dumpCoroutines()
321
+ val activeChildren = scope.children.filter(Job ::isActive).toList()
322
+ val completionCause = if (scope.isCancelled) scope.tryGetCompletionCause() else null
323
+ var message = " After waiting for $timeout "
324
+ if (completionCause == null )
325
+ message + = " , the test coroutine is not completing"
326
+ if (activeChildren.isNotEmpty())
327
+ message + = " , there were active child jobs: $activeChildren "
328
+ if (completionCause != null && activeChildren.isEmpty()) {
329
+ message + = if (scope.isCompleted)
330
+ " , the test coroutine completed"
331
+ else
332
+ " , the test coroutine was not completed"
333
+ }
334
+ timeoutError = UncompletedCoroutinesError (message)
335
+ cancellationException = CancellationException (" The test timed out" )
336
+ (scope as Job ).cancel(cancellationException!! )
337
+ }
338
+ }
339
+ while (scope.isActive) {
340
+ val executedSomething = testScheduler.tryRunNextTaskUnless { ! scope.isActive }
341
+ if (executedSomething) {
342
+ /* * yield to check for cancellation. On JS, we can't use [ensureActive] here, as the cancellation
343
+ * procedure needs a chance to run concurrently. */
344
+ yield ()
345
+ } else {
346
+ // no tasks, we should suspend until there are some more or the test coroutine is completed
347
+ select {
348
+ scope.onJoin {
349
+ // do nothing, just exit the select and the `while` loop.
350
+ }
351
+ testScheduler.onDispatchEvent {
352
+ // do nothing, just exit the select and run the next iteration
353
+ }
354
+ }
355
+ }
356
+ }
336
357
}
337
358
} 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
359
// 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()
360
+ scope .join()
361
+ val completion = scope .getCompletionExceptionOrNull()
358
362
if (completion != null && completion != = cancellationException) {
359
- timeoutError.addSuppressed(completion)
363
+ timeoutError!! .addSuppressed(completion)
360
364
}
361
- workRunner.cancelAndJoin()
362
365
} finally {
363
366
backgroundScope.cancel()
364
367
testScheduler.advanceUntilIdleOr { false }
365
- val uncaughtExceptions = it .leave()
366
- throwAll(timeoutError ? : it .getCompletionExceptionOrNull(), uncaughtExceptions)
368
+ val uncaughtExceptions = scope .leave()
369
+ throwAll(timeoutError ? : scope .getCompletionExceptionOrNull(), uncaughtExceptions)
367
370
}
368
371
}
369
372
}
0 commit comments