@@ -44,7 +44,7 @@ import kotlin.coroutines.*
44
44
*/
45
45
@Deprecated(" Use `runTest` instead to support completing from other dispatchers." , level = DeprecationLevel .WARNING )
46
46
public fun runBlockingTest (context : CoroutineContext = EmptyCoroutineContext , testBody : suspend TestCoroutineScope .() -> Unit ) {
47
- val scope = TestCoroutineScope (context)
47
+ val scope = TestCoroutineScope (TestCoroutineDispatcher () + SupervisorJob () + context)
48
48
val scheduler = scope.testScheduler
49
49
val deferred = scope.async {
50
50
scope.testBody()
@@ -141,6 +141,18 @@ public expect class TestResult
141
141
*
142
142
* ### Failures
143
143
*
144
+ * #### Test body failures
145
+ *
146
+ * If the test body finishes with an exception, then this exception will be thrown at the end of the test.
147
+ *
148
+ * #### Reported exceptions
149
+ *
150
+ * Exceptions reported to the test coroutine scope via [TestCoroutineScope.reportException] will be thrown at the end.
151
+ * By default, unless an explicit [TestExceptionHandler] is passed, this includes all unhandled exceptions. If the test
152
+ * body also fails, the reported exceptions are suppressed by it.
153
+ *
154
+ * #### Uncompleted coroutines
155
+ *
144
156
* This method requires that all coroutines launched inside [testBody] complete, or are cancelled. Otherwise, the test
145
157
* will be failed (which, on JVM and Native, means that [runTest] itself will throw [AssertionError],
146
158
* whereas on JS, the `Promise` will fail with it).
@@ -151,8 +163,6 @@ public expect class TestResult
151
163
* idle before throwing [AssertionError]. If some dispatcher linked to [TestCoroutineScheduler] receives a
152
164
* task during that time, the timer gets reset.
153
165
*
154
- * Unhandled exceptions thrown by coroutines in the test will be rethrown at the end of the test.
155
- *
156
166
* ### Configuration
157
167
*
158
168
* [context] can be used to affect the environment of the code under test. Beside just being passed to the coroutine
@@ -170,16 +180,18 @@ public fun runTest(
170
180
): TestResult {
171
181
if (context[RunningInRunTest ] != null )
172
182
throw IllegalStateException (" Calls to `runTest` can't be nested. Please read the docs on `TestResult` for details." )
173
- val testScope = TestCoroutineScope (context + RunningInRunTest ( ))
183
+ val testScope = TestBodyCoroutine < Unit >( TestCoroutineScope (context + RunningInRunTest ))
174
184
val scheduler = testScope.testScheduler
175
185
return createTestResult {
176
- val deferred = testScope.async {
177
- testScope.testBody()
186
+ /* * TODO: moving this [AbstractCoroutine.start] call outside [createTestResult] fails on Native with
187
+ * [TestCoroutineDispatcher], because the event loop is not started. */
188
+ testScope.start(CoroutineStart .DEFAULT , testScope) {
189
+ testBody()
178
190
}
179
191
var completed = false
180
192
while (! completed) {
181
193
scheduler.advanceUntilIdle()
182
- if (deferred .isCompleted) {
194
+ if (testScope .isCompleted) {
183
195
/* don't even enter `withTimeout`; this allows to use a timeout of zero to check that there are no
184
196
non-trivial dispatches. */
185
197
completed = true
@@ -188,7 +200,7 @@ public fun runTest(
188
200
try {
189
201
withTimeout(dispatchTimeoutMs) {
190
202
select<Unit > {
191
- deferred.onAwait {
203
+ testScope.onJoin {
192
204
completed = true
193
205
}
194
206
scheduler.onDispatchEvent {
@@ -205,7 +217,14 @@ public fun runTest(
205
217
throw UncompletedCoroutinesError (" The test coroutine was not completed after waiting for $dispatchTimeoutMs ms" )
206
218
}
207
219
}
208
- deferred.getCompletionExceptionOrNull()?.let {
220
+ testScope.getCompletionExceptionOrNull()?.let {
221
+ try {
222
+ testScope.cleanupTestCoroutines()
223
+ } catch (e: UncompletedCoroutinesError ) {
224
+ // it's normal that some jobs are not completed if the test body has failed, won't clutter the output
225
+ } catch (e: Throwable ) {
226
+ it.addSuppressed(e)
227
+ }
209
228
throw it
210
229
}
211
230
testScope.cleanupTestCoroutines()
@@ -222,7 +241,7 @@ internal expect fun createTestResult(testProcedure: suspend () -> Unit): TestRes
222
241
* Runs a test in a [TestCoroutineScope] based on this one.
223
242
*
224
243
* Calls [runTest] using a coroutine context from this [TestCoroutineScope]. The [TestCoroutineScope] used to run
225
- * [block] will be different from this one, but will reuse its [Job]; therefore, even if calling
244
+ * [block] will be different from this one, but will use its [Job] as a parent ; therefore, even if calling
226
245
* [TestCoroutineScope.cleanupTestCoroutines] on this scope were to complete its job, [runTest] won't complete it at the
227
246
* end of the test.
228
247
*
@@ -252,10 +271,24 @@ public fun TestDispatcher.runTest(
252
271
runTest(this , dispatchTimeoutMs, block)
253
272
254
273
/* * A coroutine context element indicating that the coroutine is running inside `runTest`. */
255
- private class RunningInRunTest : AbstractCoroutineContextElement (RunningInRunTest ), CoroutineContext.Element {
256
- companion object Key : CoroutineContext.Key<RunningInRunTest>
274
+ private object RunningInRunTest: CoroutineContext.Key<RunningInRunTest>, CoroutineContext.Element {
275
+ override val key: CoroutineContext .Key <* >
276
+ get() = this
277
+
278
+ override fun toString (): String = " RunningInRunTest"
257
279
}
258
280
259
281
/* * The default timeout to use when waiting for asynchronous completions of the coroutines managed by
260
282
* a [TestCoroutineScheduler]. */
261
283
private const val DEFAULT_DISPATCH_TIMEOUT_MS = 10_000L
284
+
285
+ private class TestBodyCoroutine <T >(
286
+ private val testScope : TestCoroutineScope ,
287
+ ) : AbstractCoroutine<T>(testScope.coroutineContext, initParentJob = true , active = true ), TestCoroutineScope,
288
+ UncaughtExceptionCaptor by testScope.coroutineContext.uncaughtExceptionCaptor
289
+ {
290
+ override val testScheduler get() = testScope.testScheduler
291
+
292
+ override fun cleanupTestCoroutines () = testScope.cleanupTestCoroutines()
293
+
294
+ }
0 commit comments