@@ -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. Additionally,
147
+ * to prevent child coroutines getting stuck, the whole scope will be cancelled in this case.
148
+ *
149
+ * #### Reported exceptions
150
+ *
151
+ * Exceptions reported to the test coroutine scope via [TestCoroutineScope.reportException] will be thrown at the end.
152
+ * By default (without passing an explicit [TestExceptionHandler]), this includes all unhandled exceptions.
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 [UncompletedCoroutinesError],
146
158
* whereas on JS, the `Promise` will fail with it).
@@ -151,8 +163,6 @@ public expect class TestResult
151
163
* idle before throwing [UncompletedCoroutinesError]. 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
@@ -177,7 +187,23 @@ public fun runTest(
177
187
}
178
188
var completed = false
179
189
while (! completed) {
180
- scheduler.advanceUntilIdle()
190
+ while (scheduler.tryRunNextTask()) {
191
+ if (deferred.isCompleted && deferred.getCompletionExceptionOrNull() != null && testScope.isActive) {
192
+ /* *
193
+ * Here, we already know how the test will finish: it will throw
194
+ * [Deferred.getCompletionExceptionOrNull]. Therefore, we won't care if there are uncompleted jobs,
195
+ * and may as well just exit right here. However, in order to lower the surprise factor, we
196
+ * cancel the child jobs here and wait for them to finish instead of dropping them: there could be
197
+ * some cleanup procedures involved, and not having finalizers run could mean leaking resources.
198
+ *
199
+ * Another approach to take if this turns out not to be enough and some child jobs still fail is to
200
+ * only make at most a fixed number of [TestCoroutineScheduler.tryRunNextTask] once we detect the
201
+ * failure with which the test will finish. This has the downside that there is still some
202
+ * negligible risk of not running the finalizers.
203
+ */
204
+ testScope.cancel()
205
+ }
206
+ }
181
207
if (deferred.isCompleted) {
182
208
/* don't even enter `withTimeout`; this allows to use a timeout of zero to check that there are no
183
209
non-trivial dispatches. */
0 commit comments