@@ -69,9 +69,7 @@ private class TestCoroutineScopeImpl(
69
69
override fun reportException (throwable : Throwable ) {
70
70
synchronized(lock) {
71
71
if (cleanedUp)
72
- throw IllegalStateException (
73
- " Attempting to report an uncaught exception after the test coroutine scope was already cleaned up" ,
74
- throwable)
72
+ throw ExceptionReportAfterCleanup (throwable)
75
73
exceptions.add(throwable)
76
74
}
77
75
}
@@ -83,6 +81,17 @@ private class TestCoroutineScopeImpl(
83
81
private val initialJobs = coroutineContext.activeJobs()
84
82
85
83
override fun cleanupTestCoroutines () {
84
+ (coroutineContext[CoroutineExceptionHandler ] as ? UncaughtExceptionCaptor )?.cleanupTestCoroutines()
85
+ val delayController = coroutineContext.delayController
86
+ var hasUncompletedJobs = false
87
+ if (delayController != null ) {
88
+ delayController.cleanupTestCoroutines()
89
+ } else {
90
+ testScheduler.runCurrent()
91
+ if (! testScheduler.isIdle()) {
92
+ hasUncompletedJobs = true
93
+ }
94
+ }
86
95
synchronized(lock) {
87
96
if (cleanedUp)
88
97
throw IllegalStateException (" Attempting to clean up a test coroutine scope more than once." )
@@ -92,18 +101,11 @@ private class TestCoroutineScopeImpl(
92
101
drop(1 ).forEach { it.printStackTrace() }
93
102
singleOrNull()?.let { throw it }
94
103
}
95
- (coroutineContext[CoroutineExceptionHandler ] as ? UncaughtExceptionCaptor )?.cleanupTestCoroutines()
96
- val delayController = coroutineContext.delayController
97
- if (delayController != null ) {
98
- delayController.cleanupTestCoroutines()
99
- } else {
100
- testScheduler.runCurrent()
101
- if (! testScheduler.isIdle()) {
102
- throw UncompletedCoroutinesError (
103
- " Unfinished coroutines during teardown. Ensure all coroutines are" +
104
- " completed or cancelled by your test."
105
- )
106
- }
104
+ if (hasUncompletedJobs) {
105
+ throw UncompletedCoroutinesError (
106
+ " Unfinished coroutines during teardown. Ensure all coroutines are" +
107
+ " completed or cancelled by your test."
108
+ )
107
109
}
108
110
val jobs = coroutineContext.activeJobs()
109
111
if ((jobs - initialJobs).isNotEmpty()) {
@@ -115,6 +117,11 @@ private class TestCoroutineScopeImpl(
115
117
}
116
118
}
117
119
120
+ internal class ExceptionReportAfterCleanup (cause : Throwable ): IllegalStateException(
121
+ " Attempting to report an uncaught exception after the test coroutine scope was already cleaned up" ,
122
+ cause
123
+ )
124
+
118
125
private fun CoroutineContext.activeJobs (): Set <Job > {
119
126
return checkNotNull(this [Job ]).children.filter { it.isActive }.toSet()
120
127
}
@@ -175,10 +182,23 @@ public fun createTestCoroutineScope(context: CoroutineContext = EmptyCoroutineCo
175
182
}
176
183
else -> throw IllegalArgumentException (" Dispatcher must implement TestDispatcher: $dispatcher " )
177
184
}
178
- val exceptionHandler = context[ CoroutineExceptionHandler ]
179
- ? : TestExceptionHandler { _, throwable -> reportException(throwable) }
185
+ val linkedHandler : TestExceptionHandlerContextElement ?
186
+ val exceptionHandler : CoroutineExceptionHandler
180
187
val handlerOwner = Any ()
181
- val linkedHandler = (exceptionHandler as ? TestExceptionHandlerContextElement )?.claimOwnershipOrCopy(handlerOwner)
188
+ when (val exceptionHandlerInCtx = context[CoroutineExceptionHandler ]) {
189
+ null -> {
190
+ linkedHandler = TestExceptionHandlerContextElement (
191
+ { _, throwable -> reportException(throwable) },
192
+ null ,
193
+ handlerOwner)
194
+ exceptionHandler = linkedHandler
195
+ }
196
+ else -> {
197
+ linkedHandler = (exceptionHandlerInCtx as ? TestExceptionHandlerContextElement
198
+ )?.claimOwnershipOrCopy(handlerOwner)
199
+ exceptionHandler = linkedHandler ? : exceptionHandlerInCtx
200
+ }
201
+ }
182
202
val job: Job
183
203
val ownJob: CompletableJob ?
184
204
if (context[Job ] == null ) {
0 commit comments