@@ -161,10 +161,11 @@ public fun runTest(
161
161
public fun TestScope.runTest (
162
162
dispatchTimeoutMs : Long = DEFAULT_DISPATCH_TIMEOUT_MS ,
163
163
testBody : suspend TestScope .() -> Unit
164
- ): TestResult = asSpecificImplementation().let {
165
- it.enter()
164
+ ): TestResult = with (asSpecificImplementation()) {
165
+ enter()
166
+ start(CoroutineStart .UNDISPATCHED , this , testBody)
166
167
createTestResult {
167
- runTestCoroutine(it , dispatchTimeoutMs, TestScopeImpl :: tryGetCompletionCause, testBody ) { it. leave() }
168
+ runTestCoroutine(this , dispatchTimeoutMs, { tryGetCompletionCause() } ) { leave() }
168
169
}
169
170
}
170
171
@@ -190,22 +191,18 @@ internal const val DEFAULT_DISPATCH_TIMEOUT_MS = 60_000L
190
191
* Run the [body][testBody] of the [test coroutine][coroutine], waiting for asynchronous completions for at most
191
192
* [dispatchTimeoutMs] milliseconds, and performing the [cleanup] procedure at the end.
192
193
*
194
+ * [tryGetCompletionCause] is the [JobSupport.completionCause], which is passed explicitly because it is protected.
195
+ *
193
196
* The [cleanup] procedure may either throw [UncompletedCoroutinesError] to denote that child coroutines were leaked, or
194
197
* return a list of uncaught exceptions that should be reported at the end of the test.
195
198
*/
196
- internal suspend fun < T : AbstractCoroutine < Unit >> runTestCoroutine (
197
- coroutine : T ,
199
+ internal suspend fun runTestCoroutine (
200
+ coroutine : AbstractCoroutine < Unit > ,
198
201
dispatchTimeoutMs : Long ,
199
- tryGetCompletionCause : T .() -> Throwable ? ,
200
- testBody : suspend T .() -> Unit ,
202
+ tryGetCompletionCause : () -> Throwable ? ,
201
203
cleanup : () -> List <Throwable >,
202
204
) {
203
205
val scheduler = coroutine.coroutineContext[TestCoroutineScheduler ]!!
204
- /* * TODO: moving this [AbstractCoroutine.start] call outside [createTestResult] fails on Native with
205
- * [TestCoroutineDispatcher], because the event loop is not started. */
206
- coroutine.start(CoroutineStart .UNDISPATCHED , coroutine) {
207
- testBody()
208
- }
209
206
var completed = false
210
207
while (! completed) {
211
208
scheduler.advanceUntilIdle()
@@ -223,32 +220,7 @@ internal suspend fun <T: AbstractCoroutine<Unit>> runTestCoroutine(
223
220
// we received knowledge that `scheduler` observed a dispatch event, so we reset the timeout
224
221
}
225
222
onTimeout(dispatchTimeoutMs) {
226
- try {
227
- cleanup()
228
- } catch (e: UncompletedCoroutinesError ) {
229
- // we expect these and will instead throw a more informative exception just below.
230
- emptyList()
231
- }.throwAll()
232
- var completing: Boolean
233
- val completionCause = try {
234
- coroutine.tryGetCompletionCause().also { completing = true }
235
- } catch (e: Throwable ) {
236
- completing = false
237
- null
238
- }
239
- var message = " After waiting for $dispatchTimeoutMs ms"
240
- if (! completing)
241
- message + = " , the test coroutine is not completing"
242
- val activeChildren = coroutine.children.filter { it.isActive }.toList()
243
- if (activeChildren.isNotEmpty())
244
- message + = " , there were active child jobs: $activeChildren "
245
- if (completing && activeChildren.isEmpty()) {
246
- // some sort of race condition? write something generic.
247
- message + = " , the test coroutine was not completed"
248
- }
249
- val error = UncompletedCoroutinesError (message)
250
- completionCause?.let { cause -> error.addSuppressed(cause) }
251
- throw error
223
+ handleTimeout(coroutine, dispatchTimeoutMs, tryGetCompletionCause, cleanup)
252
224
}
253
225
}
254
226
}
@@ -264,6 +236,41 @@ internal suspend fun <T: AbstractCoroutine<Unit>> runTestCoroutine(
264
236
cleanup().throwAll()
265
237
}
266
238
239
+ /* *
240
+ * Invoked on timeout in [runTest]. Almost always just builds a nice [UncompletedCoroutinesError] and throws it.
241
+ * However, sometimes it detects that the coroutine completed, in which case it returns normally.
242
+ */
243
+ private inline fun handleTimeout (
244
+ coroutine : AbstractCoroutine <Unit >,
245
+ dispatchTimeoutMs : Long ,
246
+ tryGetCompletionCause : () -> Throwable ? ,
247
+ cleanup : () -> List <Throwable >,
248
+ ) {
249
+ val uncaughtExceptions = try {
250
+ cleanup()
251
+ } catch (e: UncompletedCoroutinesError ) {
252
+ // we expect these and will instead throw a more informative exception.
253
+ emptyList()
254
+ }
255
+ val activeChildren = coroutine.children.filter { it.isActive }.toList()
256
+ val completionCause = if (coroutine.isCancelled) tryGetCompletionCause() else null
257
+ var message = " After waiting for $dispatchTimeoutMs ms"
258
+ if (completionCause == null )
259
+ message + = " , the test coroutine is not completing"
260
+ if (activeChildren.isNotEmpty())
261
+ message + = " , there were active child jobs: $activeChildren "
262
+ if (completionCause != null && activeChildren.isEmpty()) {
263
+ if (coroutine.isCompleted)
264
+ return
265
+ // TODO: can this really ever happen?
266
+ message + = " , the test coroutine was not completed"
267
+ }
268
+ val error = UncompletedCoroutinesError (message)
269
+ completionCause?.let { cause -> error.addSuppressed(cause) }
270
+ uncaughtExceptions.forEach { error.addSuppressed(it) }
271
+ throw error
272
+ }
273
+
267
274
internal fun List<Throwable>.throwAll () {
268
275
firstOrNull()?.apply {
269
276
drop(1 ).forEach { addSuppressed(it) }
0 commit comments