@@ -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
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()) {
@@ -113,6 +115,11 @@ private class TestCoroutineScopeImpl (
113
115
}
114
116
}
115
117
118
+ internal class ExceptionReportAfterCleanup (cause : Throwable ): IllegalStateException(
119
+ " Attempting to report an uncaught exception after the test coroutine scope was already cleaned up" ,
120
+ cause
121
+ )
122
+
116
123
private fun CoroutineContext.activeJobs (): Set <Job > {
117
124
return checkNotNull(this [Job ]).children.filter { it.isActive }.toSet()
118
125
}
@@ -173,10 +180,23 @@ public fun createTestCoroutineScope(context: CoroutineContext = EmptyCoroutineCo
173
180
}
174
181
else -> throw IllegalArgumentException (" Dispatcher must implement TestDispatcher: $dispatcher " )
175
182
}
176
- val exceptionHandler = context[ CoroutineExceptionHandler ]
177
- ? : TestExceptionHandler { _, throwable -> reportException(throwable) }
183
+ val linkedHandler : TestExceptionHandlerContextElement ?
184
+ val exceptionHandler : CoroutineExceptionHandler
178
185
val handlerOwner = Any ()
179
- val linkedHandler = (exceptionHandler as ? TestExceptionHandlerContextElement )?.claimOwnershipOrCopy(handlerOwner)
186
+ when (val exceptionHandlerInCtx = context[CoroutineExceptionHandler ]) {
187
+ null -> {
188
+ linkedHandler = TestExceptionHandlerContextElement (
189
+ { _, throwable -> reportException(throwable) },
190
+ null ,
191
+ handlerOwner)
192
+ exceptionHandler = linkedHandler
193
+ }
194
+ else -> {
195
+ linkedHandler = (exceptionHandlerInCtx as ? TestExceptionHandlerContextElement
196
+ )?.claimOwnershipOrCopy(handlerOwner)
197
+ exceptionHandler = linkedHandler ? : exceptionHandlerInCtx
198
+ }
199
+ }
180
200
val job: Job
181
201
val ownJob: CompletableJob ?
182
202
if (context[Job ] == null ) {
0 commit comments