Skip to content

Commit

Permalink
kotlinx-coroutines-test cleanup
Browse files Browse the repository at this point in the history
    * Yield support in test dispatcher
    * Allow partial override of coroutine context
    * Code style fixes
  • Loading branch information
qwwdfsad committed Apr 25, 2019
1 parent c9867b2 commit 9e5f9ab
Show file tree
Hide file tree
Showing 6 changed files with 77 additions and 33 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ public final class kotlinx/coroutines/test/TestCoroutineDispatcher : kotlinx/cor
public fun cleanupTestCoroutines ()V
public fun delay (JLkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun dispatch (Lkotlin/coroutines/CoroutineContext;Ljava/lang/Runnable;)V
public fun dispatchYield (Lkotlin/coroutines/CoroutineContext;Ljava/lang/Runnable;)V
public fun getCurrentTime ()J
public fun invokeOnTimeout (JLjava/lang/Runnable;)Lkotlinx/coroutines/DisposableHandle;
public fun pauseDispatcher ()V
Expand Down
42 changes: 15 additions & 27 deletions kotlinx-coroutines-test/src/TestBuilders.kt
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import kotlin.coroutines.*
* This is similar to [runBlocking] but it will immediately progress past delays and into [launch] and [async] blocks.
* You can use this to write tests that execute in the presence of calls to [delay] without causing your test to take
* extra time.
**
*
* ```
* @Test
* fun exampleTest() = runBlockingTest {
Expand All @@ -37,17 +37,14 @@ import kotlin.coroutines.*
* @throws UncompletedCoroutinesError If the [testBody] does not complete (or cancel) all coroutines that it launches
* (including coroutines suspended on join/await).
*
* @param context An optional context that MUST contain a [DelayController] and/or [TestCoroutineExceptionHandler]
* @param context additional context elements. If [context] contains [CoroutineDispatcher] or [CoroutineExceptionHandler],
* then they must implement [DelayController] and [TestCoroutineExceptionHandler] respectively.
* @param testBody The code of the unit-test.
*/
@ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0
public fun runBlockingTest(context: CoroutineContext? = null, testBody: suspend TestCoroutineScope.() -> Unit) {
public fun runBlockingTest(context: CoroutineContext = EmptyCoroutineContext, testBody: suspend TestCoroutineScope.() -> Unit) {
val (safeContext, dispatcher) = context.checkArguments()
// smart cast dispatcher to expose interface
dispatcher as DelayController

val startingJobs = safeContext.activeJobs()

val scope = TestCoroutineScope(safeContext)
val deferred = scope.async {
scope.testBody()
Expand All @@ -72,37 +69,28 @@ private fun CoroutineContext.activeJobs(): Set<Job> {
*/
// todo: need documentation on how this extension is supposed to be used
@ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0
public fun TestCoroutineScope.runBlockingTest(block: suspend TestCoroutineScope.() -> Unit) {
runBlockingTest(coroutineContext, block)
}
public fun TestCoroutineScope.runBlockingTest(block: suspend TestCoroutineScope.() -> Unit) = runBlockingTest(coroutineContext, block)

/**
* Convenience method for calling [runBlockingTest] on an existing [TestCoroutineDispatcher].
*/
@ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0
public fun TestCoroutineDispatcher.runBlockingTest(block: suspend TestCoroutineScope.() -> Unit) {
runBlockingTest(this, block)
}
public fun TestCoroutineDispatcher.runBlockingTest(block: suspend TestCoroutineScope.() -> Unit) = runBlockingTest(this, block)

private fun CoroutineContext?.checkArguments(): Pair<CoroutineContext, ContinuationInterceptor> {
var safeContext = this ?: TestCoroutineExceptionHandler() + TestCoroutineDispatcher()

val dispatcher = safeContext[ContinuationInterceptor].run {
this?.let {
require(this is DelayController) { "Dispatcher must implement DelayController" }
}
private fun CoroutineContext.checkArguments(): Pair<CoroutineContext, DelayController> {
// TODO optimize it
val dispatcher = get(ContinuationInterceptor).run {
this?.let { require(this is DelayController) { "Dispatcher must implement DelayController: $this" } }
this ?: TestCoroutineDispatcher()
}

val exceptionHandler = safeContext[CoroutineExceptionHandler].run {
val exceptionHandler = get(CoroutineExceptionHandler).run {
this?.let {
require(this is UncaughtExceptionCaptor) { "coroutineExceptionHandler must implement UncaughtExceptionCaptor" }
require(this is UncaughtExceptionCaptor) { "coroutineExceptionHandler must implement UncaughtExceptionCaptor: $this" }
}
this ?: TestCoroutineExceptionHandler()
}

val job = safeContext[Job] ?: SupervisorJob()

safeContext = safeContext + dispatcher + exceptionHandler + job
return Pair(safeContext, dispatcher)
}
val job = get(Job) ?: SupervisorJob()
return Pair(this + dispatcher + exceptionHandler + job, dispatcher as DelayController)
}
18 changes: 18 additions & 0 deletions kotlinx-coroutines-test/src/TestCoroutineDispatcher.kt
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ public class TestCoroutineDispatcher: CoroutineDispatcher(), Delay, DelayControl
// Storing time in nanoseconds internally.
private val _time = atomic(0L)

/** @suppress */
override fun dispatch(context: CoroutineContext, block: Runnable) {
if (dispatchImmediately) {
block.run()
Expand All @@ -144,10 +145,18 @@ public class TestCoroutineDispatcher: CoroutineDispatcher(), Delay, DelayControl
}
}

/** @suppress */
@InternalCoroutinesApi
override fun dispatchYield(context: CoroutineContext, block: Runnable) {
post(block)
}

/** @suppress */
override fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation<Unit>) {
postDelayed(CancellableContinuationRunnable(continuation) { resumeUndispatched(Unit) }, timeMillis)
}

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

/** @suppress */
override fun toString(): String {
return "TestCoroutineDispatcher[currentTime=${currentTime}ms, queued=${queue.size}]"
}
Expand Down Expand Up @@ -186,8 +196,10 @@ public class TestCoroutineDispatcher: CoroutineDispatcher(), Delay, DelayControl
}
}

/** @suppress */
override val currentTime get() = _time.value

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

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

/** @suppress */
override fun runCurrent() = doActionsUntil(currentTime)

/** @suppress */
override suspend fun pauseDispatcher(block: suspend () -> Unit) {
val previous = dispatchImmediately
dispatchImmediately = false
Expand All @@ -226,14 +241,17 @@ public class TestCoroutineDispatcher: CoroutineDispatcher(), Delay, DelayControl
}
}

/** @suppress */
override fun pauseDispatcher() {
dispatchImmediately = false
}

/** @suppress */
override fun resumeDispatcher() {
dispatchImmediately = true
}

/** @suppress */
override fun cleanupTestCoroutines() {
// process any pending cancellations or completions, but don't advance time
doActionsUntil(currentTime)
Expand Down
8 changes: 5 additions & 3 deletions kotlinx-coroutines-test/src/TestCoroutineExceptionHandler.kt
Original file line number Diff line number Diff line change
Expand Up @@ -35,20 +35,22 @@ public interface UncaughtExceptionCaptor {
* An exception handler that captures uncaught exceptions in tests.
*/
@ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0
public class TestCoroutineExceptionHandler:
public class TestCoroutineExceptionHandler :
AbstractCoroutineContextElement(CoroutineExceptionHandler), UncaughtExceptionCaptor, CoroutineExceptionHandler
{
private val _exceptions = mutableListOf<Throwable>()

override fun handleException(context: CoroutineContext, exception: Throwable) {
synchronized(_exceptions) {
_exceptions += exception
}
}

private val _exceptions = mutableListOf<Throwable>()

/** @suppress **/
override val uncaughtExceptions
get() = synchronized(_exceptions) { _exceptions.toList() }

/** @suppress **/
override fun cleanupTestCoroutines() {
synchronized(_exceptions) {
val exception = _exceptions.firstOrNull() ?: return
Expand Down
12 changes: 12 additions & 0 deletions kotlinx-coroutines-test/test/TestRunBlockingOrderTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package kotlinx.coroutines.test

import kotlinx.coroutines.*
import org.junit.*
import kotlin.coroutines.*

class TestRunBlockingOrderTest : TestBase() {
@Test
Expand All @@ -17,6 +18,17 @@ class TestRunBlockingOrderTest : TestBase() {
finish(3)
}

@Test
fun testYield() = runBlockingTest {
expect(1)
launch {
expect(2)
yield()
finish(4)
}
expect(3)
}

@Test
fun testLaunchWithDelayCompletes() = runBlockingTest {
expect(1)
Expand Down
29 changes: 26 additions & 3 deletions kotlinx-coroutines-test/test/TestRunBlockingTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@
package kotlinx.coroutines.test

import kotlinx.coroutines.*
import org.junit.Assert.*
import org.junit.Test
import kotlin.coroutines.*
import kotlin.test.*

Expand Down Expand Up @@ -371,4 +369,29 @@ class TestRunBlockingTest {
runCurrent()
assertEquals(true, result.isSuccess)
}
}


private val exceptionHandler = TestCoroutineExceptionHandler()

@Test
fun testPartialContextOverride() = runBlockingTest(CoroutineName("named")) {
assertEquals(CoroutineName("named"), coroutineContext[CoroutineName])
assertNotNull(coroutineContext[CoroutineExceptionHandler])
assertNotSame(coroutineContext[CoroutineExceptionHandler], exceptionHandler)
}

@Test(expected = IllegalArgumentException::class)
fun testPartialDispatcherOverride() = runBlockingTest(Dispatchers.Unconfined) {
fail("Unreached")
}

@Test
fun testOverrideExceptionHandler() = runBlockingTest(exceptionHandler) {
assertSame(coroutineContext[CoroutineExceptionHandler], exceptionHandler)
}

@Test(expected = IllegalArgumentException::class)
fun testOverrideExceptionHandlerError() = runBlockingTest(CoroutineExceptionHandler { _, _ -> }) {
fail("Unreached")
}
}

0 comments on commit 9e5f9ab

Please sign in to comment.