Skip to content

Commit 177e5f1

Browse files
committed
Copy TestExceptionHandler if already owned
1 parent 6c6299e commit 177e5f1

File tree

2 files changed

+25
-11
lines changed

2 files changed

+25
-11
lines changed

kotlinx-coroutines-test/common/src/TestCoroutineScope.kt

+3-1
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,8 @@ public fun createTestCoroutineScope(context: CoroutineContext = EmptyCoroutineCo
177177
}
178178
val exceptionHandler = context[CoroutineExceptionHandler]
179179
?: TestExceptionHandler { _, throwable -> reportException(throwable) }
180+
val handlerOwner = Any()
181+
val linkedHandler = (exceptionHandler as? TestExceptionHandlerContextElement)?.claimOwnershipOrCopy(handlerOwner)
180182
val job: Job
181183
val ownJob: CompletableJob?
182184
if (context[Job] == null) {
@@ -187,7 +189,7 @@ public fun createTestCoroutineScope(context: CoroutineContext = EmptyCoroutineCo
187189
job = context[Job]!!
188190
}
189191
return TestCoroutineScopeImpl(context + scheduler + dispatcher + exceptionHandler + job, ownJob)
190-
.also { (exceptionHandler as? TestExceptionHandlerContextElement)?.tryRegisterTestCoroutineScope(it) }
192+
.also { linkedHandler?.registerTestCoroutineScope(handlerOwner, it) }
191193
}
192194

193195
private inline val CoroutineContext.delayController: DelayController?

kotlinx-coroutines-test/common/src/TestExceptionHandler.kt

+22-10
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,10 @@ import kotlin.coroutines.*
1919
*
2020
* If [linkedScope] is `null`, the [CoroutineExceptionHandler] returned from this function has special behavior when
2121
* passed to [createTestCoroutineScope]: the newly-created scope is linked to this handler. If [linkedScope] is not
22-
* null, then the resulting [CoroutineExceptionHandler] will be linked to it, and passing it to [TestCoroutineScope]
23-
* will not lead to it re-linking.
22+
* null, then the resulting [CoroutineExceptionHandler] will be linked to it.
23+
*
24+
* Passing an already-linked instance to [TestCoroutineScope] will lead to it making its own copy with the same
25+
* [handler].
2426
*/
2527
public fun TestExceptionHandler(
2628
linkedScope: TestCoroutineScope? = null,
@@ -30,22 +32,32 @@ public fun TestExceptionHandler(
3032
/** The [CoroutineExceptionHandler] corresponding to the given [handler]. */
3133
internal class TestExceptionHandlerContextElement(
3234
private val handler: TestCoroutineScope.(CoroutineContext, Throwable) -> Unit,
33-
private var testCoroutineScope: TestCoroutineScope?
35+
private var testCoroutineScope: TestCoroutineScope?,
36+
private var owner: Any? = null
3437
): AbstractCoroutineContextElement(CoroutineExceptionHandler), CoroutineExceptionHandler
3538
{
3639
private val lock = SynchronizedObject()
3740

41+
/**
42+
* Claims ownership of this [TestExceptionHandler], or returns its copy, owned and not linked to anything.
43+
*/
44+
fun claimOwnershipOrCopy(owner: Any): TestExceptionHandlerContextElement = synchronized(lock) {
45+
if (this.owner == null && testCoroutineScope == null) {
46+
this.owner = owner
47+
this
48+
} else {
49+
TestExceptionHandlerContextElement(handler, null, owner)
50+
}
51+
}
52+
3853
/**
3954
* Links a [TestCoroutineScope] to this, unless there's already one linked.
4055
*/
41-
fun tryRegisterTestCoroutineScope(scope: TestCoroutineScope): Boolean =
56+
fun registerTestCoroutineScope(owner: Any, scope: TestCoroutineScope) =
4257
synchronized(lock) {
43-
if (testCoroutineScope != null) {
44-
false
45-
} else {
46-
testCoroutineScope = scope
47-
true
48-
}
58+
check(this.owner === owner && testCoroutineScope == null)
59+
testCoroutineScope = scope
60+
this.owner = null
4961
}
5062

5163
override fun handleException(context: CoroutineContext, exception: Throwable) {

0 commit comments

Comments
 (0)