Skip to content

Commit 9e5f9ab

Browse files
committed
kotlinx-coroutines-test cleanup
* Yield support in test dispatcher * Allow partial override of coroutine context * Code style fixes
1 parent c9867b2 commit 9e5f9ab

File tree

6 files changed

+77
-33
lines changed

6 files changed

+77
-33
lines changed

binary-compatibility-validator/reference-public-api/kotlinx-coroutines-test.txt

+1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ public final class kotlinx/coroutines/test/TestCoroutineDispatcher : kotlinx/cor
2323
public fun cleanupTestCoroutines ()V
2424
public fun delay (JLkotlin/coroutines/Continuation;)Ljava/lang/Object;
2525
public fun dispatch (Lkotlin/coroutines/CoroutineContext;Ljava/lang/Runnable;)V
26+
public fun dispatchYield (Lkotlin/coroutines/CoroutineContext;Ljava/lang/Runnable;)V
2627
public fun getCurrentTime ()J
2728
public fun invokeOnTimeout (JLjava/lang/Runnable;)Lkotlinx/coroutines/DisposableHandle;
2829
public fun pauseDispatcher ()V

kotlinx-coroutines-test/src/TestBuilders.kt

+15-27
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import kotlin.coroutines.*
1313
* This is similar to [runBlocking] but it will immediately progress past delays and into [launch] and [async] blocks.
1414
* You can use this to write tests that execute in the presence of calls to [delay] without causing your test to take
1515
* extra time.
16-
**
16+
*
1717
* ```
1818
* @Test
1919
* fun exampleTest() = runBlockingTest {
@@ -37,17 +37,14 @@ import kotlin.coroutines.*
3737
* @throws UncompletedCoroutinesError If the [testBody] does not complete (or cancel) all coroutines that it launches
3838
* (including coroutines suspended on join/await).
3939
*
40-
* @param context An optional context that MUST contain a [DelayController] and/or [TestCoroutineExceptionHandler]
40+
* @param context additional context elements. If [context] contains [CoroutineDispatcher] or [CoroutineExceptionHandler],
41+
* then they must implement [DelayController] and [TestCoroutineExceptionHandler] respectively.
4142
* @param testBody The code of the unit-test.
4243
*/
4344
@ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0
44-
public fun runBlockingTest(context: CoroutineContext? = null, testBody: suspend TestCoroutineScope.() -> Unit) {
45+
public fun runBlockingTest(context: CoroutineContext = EmptyCoroutineContext, testBody: suspend TestCoroutineScope.() -> Unit) {
4546
val (safeContext, dispatcher) = context.checkArguments()
46-
// smart cast dispatcher to expose interface
47-
dispatcher as DelayController
48-
4947
val startingJobs = safeContext.activeJobs()
50-
5148
val scope = TestCoroutineScope(safeContext)
5249
val deferred = scope.async {
5350
scope.testBody()
@@ -72,37 +69,28 @@ private fun CoroutineContext.activeJobs(): Set<Job> {
7269
*/
7370
// todo: need documentation on how this extension is supposed to be used
7471
@ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0
75-
public fun TestCoroutineScope.runBlockingTest(block: suspend TestCoroutineScope.() -> Unit) {
76-
runBlockingTest(coroutineContext, block)
77-
}
72+
public fun TestCoroutineScope.runBlockingTest(block: suspend TestCoroutineScope.() -> Unit) = runBlockingTest(coroutineContext, block)
7873

7974
/**
8075
* Convenience method for calling [runBlockingTest] on an existing [TestCoroutineDispatcher].
8176
*/
8277
@ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0
83-
public fun TestCoroutineDispatcher.runBlockingTest(block: suspend TestCoroutineScope.() -> Unit) {
84-
runBlockingTest(this, block)
85-
}
78+
public fun TestCoroutineDispatcher.runBlockingTest(block: suspend TestCoroutineScope.() -> Unit) = runBlockingTest(this, block)
8679

87-
private fun CoroutineContext?.checkArguments(): Pair<CoroutineContext, ContinuationInterceptor> {
88-
var safeContext = this ?: TestCoroutineExceptionHandler() + TestCoroutineDispatcher()
89-
90-
val dispatcher = safeContext[ContinuationInterceptor].run {
91-
this?.let {
92-
require(this is DelayController) { "Dispatcher must implement DelayController" }
93-
}
80+
private fun CoroutineContext.checkArguments(): Pair<CoroutineContext, DelayController> {
81+
// TODO optimize it
82+
val dispatcher = get(ContinuationInterceptor).run {
83+
this?.let { require(this is DelayController) { "Dispatcher must implement DelayController: $this" } }
9484
this ?: TestCoroutineDispatcher()
9585
}
9686

97-
val exceptionHandler = safeContext[CoroutineExceptionHandler].run {
87+
val exceptionHandler = get(CoroutineExceptionHandler).run {
9888
this?.let {
99-
require(this is UncaughtExceptionCaptor) { "coroutineExceptionHandler must implement UncaughtExceptionCaptor" }
89+
require(this is UncaughtExceptionCaptor) { "coroutineExceptionHandler must implement UncaughtExceptionCaptor: $this" }
10090
}
10191
this ?: TestCoroutineExceptionHandler()
10292
}
10393

104-
val job = safeContext[Job] ?: SupervisorJob()
105-
106-
safeContext = safeContext + dispatcher + exceptionHandler + job
107-
return Pair(safeContext, dispatcher)
108-
}
94+
val job = get(Job) ?: SupervisorJob()
95+
return Pair(this + dispatcher + exceptionHandler + job, dispatcher as DelayController)
96+
}

kotlinx-coroutines-test/src/TestCoroutineDispatcher.kt

+18
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ public class TestCoroutineDispatcher: CoroutineDispatcher(), Delay, DelayControl
136136
// Storing time in nanoseconds internally.
137137
private val _time = atomic(0L)
138138

139+
/** @suppress */
139140
override fun dispatch(context: CoroutineContext, block: Runnable) {
140141
if (dispatchImmediately) {
141142
block.run()
@@ -144,10 +145,18 @@ public class TestCoroutineDispatcher: CoroutineDispatcher(), Delay, DelayControl
144145
}
145146
}
146147

148+
/** @suppress */
149+
@InternalCoroutinesApi
150+
override fun dispatchYield(context: CoroutineContext, block: Runnable) {
151+
post(block)
152+
}
153+
154+
/** @suppress */
147155
override fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation<Unit>) {
148156
postDelayed(CancellableContinuationRunnable(continuation) { resumeUndispatched(Unit) }, timeMillis)
149157
}
150158

159+
/** @suppress */
151160
override fun invokeOnTimeout(timeMillis: Long, block: Runnable): DisposableHandle {
152161
val node = postDelayed(block, timeMillis)
153162
return object : DisposableHandle {
@@ -157,6 +166,7 @@ public class TestCoroutineDispatcher: CoroutineDispatcher(), Delay, DelayControl
157166
}
158167
}
159168

169+
/** @suppress */
160170
override fun toString(): String {
161171
return "TestCoroutineDispatcher[currentTime=${currentTime}ms, queued=${queue.size}]"
162172
}
@@ -186,8 +196,10 @@ public class TestCoroutineDispatcher: CoroutineDispatcher(), Delay, DelayControl
186196
}
187197
}
188198

199+
/** @suppress */
189200
override val currentTime get() = _time.value
190201

202+
/** @suppress */
191203
override fun advanceTimeBy(delayTimeMillis: Long): Long {
192204
val oldTime = currentTime
193205
advanceUntilTime(oldTime + delayTimeMillis)
@@ -204,6 +216,7 @@ public class TestCoroutineDispatcher: CoroutineDispatcher(), Delay, DelayControl
204216
_time.update { currentValue -> max(currentValue, targetTime) }
205217
}
206218

219+
/** @suppress */
207220
override fun advanceUntilIdle(): Long {
208221
val oldTime = currentTime
209222
while(!queue.isEmpty) {
@@ -214,8 +227,10 @@ public class TestCoroutineDispatcher: CoroutineDispatcher(), Delay, DelayControl
214227
return currentTime - oldTime
215228
}
216229

230+
/** @suppress */
217231
override fun runCurrent() = doActionsUntil(currentTime)
218232

233+
/** @suppress */
219234
override suspend fun pauseDispatcher(block: suspend () -> Unit) {
220235
val previous = dispatchImmediately
221236
dispatchImmediately = false
@@ -226,14 +241,17 @@ public class TestCoroutineDispatcher: CoroutineDispatcher(), Delay, DelayControl
226241
}
227242
}
228243

244+
/** @suppress */
229245
override fun pauseDispatcher() {
230246
dispatchImmediately = false
231247
}
232248

249+
/** @suppress */
233250
override fun resumeDispatcher() {
234251
dispatchImmediately = true
235252
}
236253

254+
/** @suppress */
237255
override fun cleanupTestCoroutines() {
238256
// process any pending cancellations or completions, but don't advance time
239257
doActionsUntil(currentTime)

kotlinx-coroutines-test/src/TestCoroutineExceptionHandler.kt

+5-3
Original file line numberDiff line numberDiff line change
@@ -35,20 +35,22 @@ public interface UncaughtExceptionCaptor {
3535
* An exception handler that captures uncaught exceptions in tests.
3636
*/
3737
@ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0
38-
public class TestCoroutineExceptionHandler:
38+
public class TestCoroutineExceptionHandler :
3939
AbstractCoroutineContextElement(CoroutineExceptionHandler), UncaughtExceptionCaptor, CoroutineExceptionHandler
4040
{
41+
private val _exceptions = mutableListOf<Throwable>()
42+
4143
override fun handleException(context: CoroutineContext, exception: Throwable) {
4244
synchronized(_exceptions) {
4345
_exceptions += exception
4446
}
4547
}
4648

47-
private val _exceptions = mutableListOf<Throwable>()
48-
49+
/** @suppress **/
4950
override val uncaughtExceptions
5051
get() = synchronized(_exceptions) { _exceptions.toList() }
5152

53+
/** @suppress **/
5254
override fun cleanupTestCoroutines() {
5355
synchronized(_exceptions) {
5456
val exception = _exceptions.firstOrNull() ?: return

kotlinx-coroutines-test/test/TestRunBlockingOrderTest.kt

+12
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package kotlinx.coroutines.test
66

77
import kotlinx.coroutines.*
88
import org.junit.*
9+
import kotlin.coroutines.*
910

1011
class TestRunBlockingOrderTest : TestBase() {
1112
@Test
@@ -17,6 +18,17 @@ class TestRunBlockingOrderTest : TestBase() {
1718
finish(3)
1819
}
1920

21+
@Test
22+
fun testYield() = runBlockingTest {
23+
expect(1)
24+
launch {
25+
expect(2)
26+
yield()
27+
finish(4)
28+
}
29+
expect(3)
30+
}
31+
2032
@Test
2133
fun testLaunchWithDelayCompletes() = runBlockingTest {
2234
expect(1)

kotlinx-coroutines-test/test/TestRunBlockingTest.kt

+26-3
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@
55
package kotlinx.coroutines.test
66

77
import kotlinx.coroutines.*
8-
import org.junit.Assert.*
9-
import org.junit.Test
108
import kotlin.coroutines.*
119
import kotlin.test.*
1210

@@ -371,4 +369,29 @@ class TestRunBlockingTest {
371369
runCurrent()
372370
assertEquals(true, result.isSuccess)
373371
}
374-
}
372+
373+
374+
private val exceptionHandler = TestCoroutineExceptionHandler()
375+
376+
@Test
377+
fun testPartialContextOverride() = runBlockingTest(CoroutineName("named")) {
378+
assertEquals(CoroutineName("named"), coroutineContext[CoroutineName])
379+
assertNotNull(coroutineContext[CoroutineExceptionHandler])
380+
assertNotSame(coroutineContext[CoroutineExceptionHandler], exceptionHandler)
381+
}
382+
383+
@Test(expected = IllegalArgumentException::class)
384+
fun testPartialDispatcherOverride() = runBlockingTest(Dispatchers.Unconfined) {
385+
fail("Unreached")
386+
}
387+
388+
@Test
389+
fun testOverrideExceptionHandler() = runBlockingTest(exceptionHandler) {
390+
assertSame(coroutineContext[CoroutineExceptionHandler], exceptionHandler)
391+
}
392+
393+
@Test(expected = IllegalArgumentException::class)
394+
fun testOverrideExceptionHandlerError() = runBlockingTest(CoroutineExceptionHandler { _, _ -> }) {
395+
fail("Unreached")
396+
}
397+
}

0 commit comments

Comments
 (0)