Skip to content

kotlinx-coroutines-test cleanup #1144

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 25, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
4 changes: 2 additions & 2 deletions kotlinx-coroutines-test/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -274,8 +274,8 @@ important to ensure that [cleanupTestCoroutines][TestCoroutineScope.cleanupTestC

```kotlin
class TestClass {
val testScope = TestCoroutineScope()
lateinit var subject: Subject = null
private val testScope = TestCoroutineScope()
private lateinit var subject: Subject = null

@Before
fun setup() {
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")
}
}