From 48de512af07ccca6ad98c43721579334eb819bb8 Mon Sep 17 00:00:00 2001 From: Sean McQuillan Date: Wed, 16 Oct 2019 16:36:30 -0700 Subject: [PATCH] Explore dispatched mode for TestCoroutineDispatcher. --- .../src/DelayController.kt | 84 ++++++++---------- kotlinx-coroutines-test/src/TestBuilders.kt | 49 ++++++----- .../src/TestCoroutineDispatcher.kt | 86 +++++-------------- .../test/RunBlockingTestOrderTest.kt | 18 ++-- .../TestCoroutineDispatcherOrderTest.kt | 56 ++++++++++++ .../obsolete/TestCoroutineDispatcherTest.kt | 27 +++--- .../test/obsolete/TestRunBlockingOrderTest.kt | 28 +++--- .../test/obsolete/TestRunBlockingTest.kt | 38 ++++---- 8 files changed, 194 insertions(+), 192 deletions(-) diff --git a/kotlinx-coroutines-test/src/DelayController.kt b/kotlinx-coroutines-test/src/DelayController.kt index 3f608e9e95..1c470f9c44 100644 --- a/kotlinx-coroutines-test/src/DelayController.kt +++ b/kotlinx-coroutines-test/src/DelayController.kt @@ -22,9 +22,6 @@ public interface DelayController { /** * Moves the Dispatcher's virtual clock forward by a specified amount of time. * - * The amount the clock is progressed may be larger than the requested `delayTimeMillis` if the code under test uses - * blocking coroutines. - * * The virtual clock time will advance once for each delay resumed until the next delay exceeds the requested * `delayTimeMills`. In the following test, the virtual time will progress by 2_000 then 1 to resume three different * calls to delay. @@ -60,7 +57,7 @@ public interface DelayController { /** * Immediately execute all pending tasks and advance the virtual clock-time to the last delay. * - * If new tasks are scheduled due to advancing virtual time, they will be executed before `advanceUntilIdle` + * If new tasks are scheduled while advancing virtual time, they will be executed before `advanceUntilIdle` * returns. * * @return the amount of delay-time that this Dispatcher's clock has been forwarded in milliseconds. @@ -87,63 +84,54 @@ public interface DelayController { public fun cleanupTestCoroutines() /** - * Run a block of code in a paused dispatcher. + * Suspends until at least one task may be executed by calling [runCurrent]. * - * By pausing the dispatcher any new coroutines will not execute immediately. After block executes, the dispatcher - * will resume auto-advancing. + * If this method returns normally, there must be at least one task that can be executed by calling [runCurrent]. It + * will return immediately if there is already a task in the queue for the current time. * - * This is useful when testing functions that start a coroutine. By pausing the dispatcher assertions or - * setup may be done between the time the coroutine is created and started. + * This is useful when another dispatcher is currently processing a coroutine and is expected to dispatch the result + * to this dispatcher. This most commonly happens due to calls to [withContext] or [Deferred.await]. * - * While in the paused block, the dispatcher will queue all dispatched coroutines and they will be resumed on - * whatever thread calls [advanceUntilIdle], [advanceTimeBy], or [runCurrent]. - */ - @ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0 - public suspend fun pauseDispatcher(block: suspend () -> Unit) - - /** - * Pause the dispatcher. + * Note: You should not need to call this function from inside [runBlockingTest] as it calls it implicitly, but it + * is required for thread coordination in in tests that don't use `runBlockingTest` and interact with multiple + * threads. * - * When paused, the dispatcher will not execute any coroutines automatically, and you must call [runCurrent] or - * [advanceTimeBy], or [advanceUntilIdle] to execute coroutines. + * ``` + * val otherDispatcher = // single threaded dispatcher * - * While paused, the dispatcher will queue all dispatched coroutines and they will be resumed on whatever thread - * calls [advanceUntilIdle], [advanceTimeBy], or [runCurrent]. - */ - @ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0 - public fun pauseDispatcher() - - /** - * Resume the dispatcher from a paused state. + * suspend fun switchingThreads() { + * withContext(otherDispatcher) { + * // this is not executing on TestCoroutineDispatcher + * database.query() + * } + * println("run me after waiting for withContext") + * } * - * Resumed dispatchers will automatically progress through all coroutines scheduled at the current time. To advance - * time and execute coroutines scheduled in the future use, one of [advanceTimeBy], - * or [advanceUntilIdle]. + * @Test whenSwitchingThreads_resultsBecomeAvailable() { + * val scope = TestCoroutineScope() * - * When the dispatcher is resumed, all execution be immediate in the thread that triggered it similar to - * [Dispatchers.Unconfined]. This means that the following code will not switch back from Dispatchers.IO after - * `withContext` + * val job = scope.launch { + * switchingThreads() + * } * - * ``` - * runBlockingTest { - * withContext(Dispatchers.IO) { doIo() } - * // runBlockingTest is still on Dispatchers.IO here + * scope.runCurrent() // run to withContext, which will dispatch on otherDispatcher + * runBlocking { + * // wait for otherDispatcher to return control of the coroutine to TestCoroutineDispatcher + * scope.waitForDispatcherBusy(2_000) // throws timeout exception if withContext doesn't finish in 2_000ms + * } + * scope.runCurrent() // run the dispatched task (everything after withContext) + * // job.isCompleted == true * } * ``` * - * For tests that need accurate threading behavior, [pauseDispatcher] will ensure that the following test dispatches - * on a controlled thread. + * Whenever possible, it is preferred to inject [TestCoroutineDispatcher] to the [withContext] call instead of + * calling `waitForDispatcherBusy` as it creates a single threaded test and avoids thread synchronization issues. * - * ``` - * runBlockingTest { - * pauseDispatcher() - * withContext(Dispatchers.IO) { doIo() } - * // runBlockingTest has returned to it's starting thread here - * } - * ``` + * Calling this method will never change the virtual-time. + * + * @param timeoutMills how long to wait for a task to be dispatched to this dispatcher. */ - @ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0 - public fun resumeDispatcher() + suspend fun waitForDispatcherBusy(timeoutMills: Long) } /** diff --git a/kotlinx-coroutines-test/src/TestBuilders.kt b/kotlinx-coroutines-test/src/TestBuilders.kt index 93349cd686..2ec1df0547 100644 --- a/kotlinx-coroutines-test/src/TestBuilders.kt +++ b/kotlinx-coroutines-test/src/TestBuilders.kt @@ -12,11 +12,14 @@ import kotlin.coroutines.* private const val DEFAULT_WAIT_FOR_OTHER_DISPATCHERS = 30_000L /** - * Executes a [testBody] inside an immediate execution dispatcher. + * Executes a [testBody] inside an dispatcher that gurantees controlled, repeatable execution. * - * 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. + * This is similar to [runBlocking] but it uses [TestCoroutineScope] to allow explict control over execution using the + * [DelayController] interface. When used for single-threaded testing, the ordering of execution is guranteed to be + * determistic (that means it always executes in the same order). + * + * When using for multi-threaded testing (e.g. calls to [withContext]), [runBlockingTest] will wait for the other + * dispatcher to return control then resume execution. * * ``` * @Test @@ -27,7 +30,7 @@ private const val DEFAULT_WAIT_FOR_OTHER_DISPATCHERS = 30_000L * delay(1_000) * }.await() * } - * + * advanceTimeBy(2_000) * deferred.await() // result available immediately * } * @@ -36,7 +39,8 @@ private const val DEFAULT_WAIT_FOR_OTHER_DISPATCHERS = 30_000L * This method requires that all coroutines launched inside [testBody] complete, or are cancelled, as part of the test * conditions. * - * Unhandled exceptions thrown by coroutines in the test will be re-thrown at the end of the test. + * Unhandled exceptions thrown by coroutines started in the [TestCoroutineScope] will be re-thrown at the end of the + * test. * * @throws UncompletedCoroutinesError If the [testBody] does not complete (or cancel) all coroutines that it launches * (including coroutines suspended on join/await). @@ -64,7 +68,7 @@ public fun runBlockingTest( localTestScope.testBody() } - val didTimeout = deferred.waitForCompletion(waitForOtherDispatchers, dispatcher, dispatcher as IdleWaiter) + val didTimeout = deferred.waitForCompletion(waitForOtherDispatchers, dispatcher) if (deferred.isCompleted) { deferred.getCompletionExceptionOrNull()?.let { @@ -87,14 +91,27 @@ public fun runBlockingTest( } } -private fun Deferred.waitForCompletion(wait: Long, delayController: DelayController, park: IdleWaiter): Boolean { +/** + * Convenience method for calling [runBlockingTest] on an existing [TestCoroutineScope]. + */ +// 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(waitForOtherDispatchers: Long = DEFAULT_WAIT_FOR_OTHER_DISPATCHERS, block: suspend TestCoroutineScope.() -> Unit) = runBlockingTest(coroutineContext, waitForOtherDispatchers, block) + +/** + * Convenience method for calling [runBlockingTest] on an existing [TestCoroutineDispatcher]. + */ +@ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0 +public fun TestCoroutineDispatcher.runBlockingTest(waitForOtherDispatchers: Long = DEFAULT_WAIT_FOR_OTHER_DISPATCHERS, block: suspend TestCoroutineScope.() -> Unit) = runBlockingTest(this, waitForOtherDispatchers, block) + +private fun Deferred.waitForCompletion(wait: Long, delayController: DelayController): Boolean { var didTimeout = false runBlocking { val unparkChannel = Channel(1) val job = launch { while(true) { - park.suspendUntilNextDispatch() + delayController.waitForDispatcherBusy(wait) unparkChannel.send(Unit) } } @@ -121,24 +138,10 @@ private fun CoroutineContext.activeJobs(): Set { return checkNotNull(this[Job]).children.filter { it.isActive }.toSet() } -/** - * Convenience method for calling [runBlockingTest] on an existing [TestCoroutineScope]. - */ -// 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(waitForOtherDispatchers: Long = DEFAULT_WAIT_FOR_OTHER_DISPATCHERS, block: suspend TestCoroutineScope.() -> Unit) = runBlockingTest(coroutineContext, waitForOtherDispatchers, block) - -/** - * Convenience method for calling [runBlockingTest] on an existing [TestCoroutineDispatcher]. - */ -@ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0 -public fun TestCoroutineDispatcher.runBlockingTest(waitForOtherDispatchers: Long = DEFAULT_WAIT_FOR_OTHER_DISPATCHERS, block: suspend TestCoroutineScope.() -> Unit) = runBlockingTest(this, waitForOtherDispatchers, block) - private fun CoroutineContext.checkArguments(): Pair { // TODO optimize it val dispatcher = get(ContinuationInterceptor).run { this?.let { require(this is DelayController) { "Dispatcher must implement DelayController: $this" } } - this?.let { require(this is IdleWaiter) { "Dispatcher must implement IdleWaiter" } } this ?: TestCoroutineDispatcher() } diff --git a/kotlinx-coroutines-test/src/TestCoroutineDispatcher.kt b/kotlinx-coroutines-test/src/TestCoroutineDispatcher.kt index ea7afef1d7..763fe8cbd5 100644 --- a/kotlinx-coroutines-test/src/TestCoroutineDispatcher.kt +++ b/kotlinx-coroutines-test/src/TestCoroutineDispatcher.kt @@ -14,33 +14,22 @@ import kotlin.coroutines.CoroutineContext import kotlin.math.max /** - * [CoroutineDispatcher] that performs both immediate and lazy execution of coroutines in tests + * [CoroutineDispatcher] that offers lazy, determistic, execution of coroutines in tests * and implements [DelayController] to control its virtual clock. * - * By default, [TestCoroutineDispatcher] is immediate. That means any tasks scheduled to be run without delay are - * immediately executed. If they were scheduled with a delay, the virtual clock-time must be advanced via one of the - * methods on [DelayController]. + * Any coroutines started via [launch] or [async] will not execute until a call to [DelayController.runCurrent] or the + * virtual clock-time has been advanced via one of the methods on [DelayController]. * - * When switched to lazy execution using [pauseDispatcher] any coroutines started via [launch] or [async] will - * not execute until a call to [DelayController.runCurrent] or the virtual clock-time has been advanced via one of the - * methods on [DelayController]. - * - * While in immediate mode [TestCoroutineDispatcher] behaves similar to [Dispatchers.Unconfined]. When resuming from - * another thread it will *not* switch threads. When in lazy mode, [TestCoroutineDispatcher] will enqueue all - * dispatches and whatever thread calls an [advanceUntilIdle], [advanceTimeBy], or [runCurrent] will continue execution. + * [TestCoroutineDispatcher] does not hold a thread, so if a coroutine switches to another thread via [withContext] or + * similar, this dispatcher will not automatically wait for the other thread to pass control back. Tests can wait for + * the other dispatcher by calling [DelayController.waitForDispatcherBusy], then call [DelayController.runCurrent] to + * run the dispatched task from the test thread. Tests that use [runBlockingTest] do not need to call + * [DelayController.waitForDispatcherBusy]. * * @see DelayController */ @ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0 -public class TestCoroutineDispatcher: CoroutineDispatcher(), Delay, DelayController, IdleWaiter { - private var dispatchImmediately = true - set(value) { - field = value - if (value) { - // there may already be tasks from setup code we need to run - advanceUntilIdle() - } - } +public class TestCoroutineDispatcher: CoroutineDispatcher(), Delay, DelayController { // The ordered queue for the runnable tasks. private val queue = ThreadSafeHeap() @@ -55,12 +44,7 @@ public class TestCoroutineDispatcher: CoroutineDispatcher(), Delay, DelayControl /** @suppress */ override fun dispatch(context: CoroutineContext, block: Runnable) { - if (dispatchImmediately) { - block.run() - unpark() - } else { - post(block) - } + post(block) } /** @suppress */ @@ -155,27 +139,6 @@ public class TestCoroutineDispatcher: CoroutineDispatcher(), Delay, DelayControl doActionsUntil(currentTime) } - /** @suppress */ - override suspend fun pauseDispatcher(block: suspend () -> Unit) { - val previous = dispatchImmediately - dispatchImmediately = false - try { - block() - } finally { - dispatchImmediately = previous - } - } - - /** @suppress */ - override fun pauseDispatcher() { - dispatchImmediately = false - } - - /** @suppress */ - override fun resumeDispatcher() { - dispatchImmediately = true - } - /** @suppress */ override fun cleanupTestCoroutines() { unpark() @@ -200,8 +163,16 @@ public class TestCoroutineDispatcher: CoroutineDispatcher(), Delay, DelayControl } } - override suspend fun suspendUntilNextDispatch() { - waitLock.receive() + override suspend fun waitForDispatcherBusy(timeoutMills: Long) { + withTimeout(timeoutMills) { + while (true) { + val nextTime = queue.peek()?.time + if (nextTime != null && nextTime <= currentTime) { + break + } + waitLock.receive() + } + } } private fun unpark() { @@ -239,20 +210,3 @@ private class TimedRunnable( override fun toString() = "TimedRunnable(time=$time, run=$runnable)" } - -/** - * Alternative implementations of [TestCoroutineDispatcher] must implement this interface in order to be supported by - * [runBlockingTest]. - * - * This interface allows external code to suspend itself until the next dispatch is received. This is similar to park in - * a normal event loop, but doesn't require that [TestCoroutineDispatcher] block a thread while parked. - */ -interface IdleWaiter { - /** - * Attempt to suspend until the next dispatch is received. - * - * This method may resume immediately if any dispatch was received since the last time it was called. This ensures - * that dispatches won't be dropped if they happen just before calling [suspendUntilNextDispatch]. - */ - public suspend fun suspendUntilNextDispatch() -} diff --git a/kotlinx-coroutines-test/test/RunBlockingTestOrderTest.kt b/kotlinx-coroutines-test/test/RunBlockingTestOrderTest.kt index 8cf3aa1b0c..0f736a94f3 100644 --- a/kotlinx-coroutines-test/test/RunBlockingTestOrderTest.kt +++ b/kotlinx-coroutines-test/test/RunBlockingTestOrderTest.kt @@ -16,16 +16,17 @@ class RunBlockingTestOrderTest : TestBase() { val timeout = Timeout.seconds(1) @Test - fun testImmediateExecution() = runBlockingTest { + fun testExecutionOrder() = runBlockingTest { expect(1) launch { expect(2) } + runCurrent() finish(3) } @Test - fun testImmediateNestedExecution() = runBlockingTest { + fun testNestedExecutionOrder() = runBlockingTest { expect(1) launch { expect(2) @@ -33,24 +34,26 @@ class RunBlockingTestOrderTest : TestBase() { expect(3) } } + runCurrent() finish(4) } @Test - fun testExecutionOrder() = runBlockingTest { + fun testComplexExecutionOrder() = runBlockingTest { expect(1) launch { expect(2) launch { - expect(3) + expect(4) yield() expect(6) } - expect(4) + expect(3) yield() - finish(7) + expect(5) } - expect(5) + runCurrent() + finish(7) } @Test @@ -62,6 +65,7 @@ class RunBlockingTestOrderTest : TestBase() { expect(4) 42 } + runCurrent() expect(3) assertEquals(42, result.await()) finish(5) diff --git a/kotlinx-coroutines-test/test/obsolete/TestCoroutineDispatcherOrderTest.kt b/kotlinx-coroutines-test/test/obsolete/TestCoroutineDispatcherOrderTest.kt index d3ebbb1808..945d4a28e8 100644 --- a/kotlinx-coroutines-test/test/obsolete/TestCoroutineDispatcherOrderTest.kt +++ b/kotlinx-coroutines-test/test/obsolete/TestCoroutineDispatcherOrderTest.kt @@ -29,6 +29,7 @@ class TestCoroutineDispatcherOrderTest : TestBase() { assertEquals(2_001, dispatcher.currentTime) expect(7) } + scope.runCurrent() expect(3) assertEquals(0, dispatcher.currentTime) dispatcher.advanceTimeBy(2_000) @@ -40,4 +41,59 @@ class TestCoroutineDispatcherOrderTest : TestBase() { scope.cleanupTestCoroutines() finish(9) } + + @Test + fun waitForDispatcherBusy_exitsImmediately() { + val subject = TestCoroutineDispatcher() + val scope = TestCoroutineScope(subject) + + scope.launch { + + } + + runBlocking { + assertRunsFast { + scope.waitForDispatcherBusy(1) + } + } + } + + @Test(expected = TimeoutCancellationException::class) + fun waitForDispatcherBusy_timetIfInFuture() { + val subject = TestCoroutineDispatcher() + val scope = TestCoroutineScope(subject) + + scope.launch { + delay(1_000) + } + + runBlocking { + assertRunsFast { + scope.runCurrent() + scope.waitForDispatcherBusy(1) + } + } + } + + @Test + fun waitForDispatcherBusy_resumesIfResumedByOtherThreadAsync() { + val subject = TestCoroutineDispatcher() + val scope = TestCoroutineScope(subject) + newFixedThreadPoolContext(1, "other pool").use { otherDispatcher -> + scope.launch { + delay(50) + withContext(otherDispatcher) { + delay(1) + } + } + + runBlocking { + scope.advanceTimeBy(50) + assertRunsFast { + scope.waitForDispatcherBusy(3_000) + } + } + + } + } } \ No newline at end of file diff --git a/kotlinx-coroutines-test/test/obsolete/TestCoroutineDispatcherTest.kt b/kotlinx-coroutines-test/test/obsolete/TestCoroutineDispatcherTest.kt index af2cac9626..9706a52aa3 100644 --- a/kotlinx-coroutines-test/test/obsolete/TestCoroutineDispatcherTest.kt +++ b/kotlinx-coroutines-test/test/obsolete/TestCoroutineDispatcherTest.kt @@ -27,7 +27,6 @@ class TestCoroutineDispatcherTest { fun whenStringCalled_itShowsQueuedJobs() { val subject = TestCoroutineDispatcher() val scope = TestCoroutineScope(subject) - scope.pauseDispatcher() scope.launch { delay(1_000) } @@ -41,7 +40,6 @@ class TestCoroutineDispatcherTest { @Test fun whenDispatcherPaused_doesntAutoProgressCurrent() { val subject = TestCoroutineDispatcher() - subject.pauseDispatcher() val scope = CoroutineScope(subject) var executed = 0 scope.launch { @@ -59,6 +57,7 @@ class TestCoroutineDispatcherTest { executed++ } + subject.runCurrent() assertEquals(1, executed) } @@ -80,7 +79,6 @@ class TestCoroutineDispatcherTest { @Test fun whenDispatcherPaused_thenResume_itDoesDispatchCurrent() { val subject = TestCoroutineDispatcher() - subject.pauseDispatcher() val scope = CoroutineScope(subject) var executed = 0 scope.launch { @@ -88,14 +86,13 @@ class TestCoroutineDispatcherTest { } assertEquals(0, executed) - subject.resumeDispatcher() + subject.runCurrent() assertEquals(1, executed) } @Test(expected = UncompletedCoroutinesError::class) fun whenDispatcherHasUncompletedCoroutines_itThrowsErrorInCleanup() { val subject = TestCoroutineDispatcher() - subject.pauseDispatcher() val scope = CoroutineScope(subject) scope.launch { delay(1_000) @@ -108,17 +105,20 @@ class TestCoroutineDispatcherTest { val currentThread = Thread.currentThread() val subject = TestCoroutineDispatcher() val scope = TestCoroutineScope(subject) - - val deferred = scope.async(Dispatchers.Default) { - withContext(subject) { - assertNotSame(currentThread, Thread.currentThread()) - 3 + newFixedThreadPoolContext(1, "other dispatcher").use { otherDispatcher -> + val deferred = scope.async(otherDispatcher) { + withContext(subject) { + assertSame(currentThread, Thread.currentThread()) + 3 + } } - } - runBlocking { // just to ensure the above code terminates - assertEquals(3, deferred.await()) + runBlocking { + scope.waitForDispatcherBusy(100) + } + scope.runCurrent() + runBlocking { withTimeout(100) { deferred.await() } } } } @@ -137,6 +137,7 @@ class TestCoroutineDispatcherTest { runBlocking { // just to ensure the above code terminates + scope.runCurrent() assertEquals(3, deferred.await()) } } diff --git a/kotlinx-coroutines-test/test/obsolete/TestRunBlockingOrderTest.kt b/kotlinx-coroutines-test/test/obsolete/TestRunBlockingOrderTest.kt index 52ba5f74c7..3867a29253 100644 --- a/kotlinx-coroutines-test/test/obsolete/TestRunBlockingOrderTest.kt +++ b/kotlinx-coroutines-test/test/obsolete/TestRunBlockingOrderTest.kt @@ -58,12 +58,11 @@ class TestRunBlockingOrderTest : TestBase() { val numbersFromOtherDispatcherWithDelays = flow { for(x in 1..max) { emit(x) } } .buffer(0) - .delayEach(1) + .onEach { delay(1) } .flowOn(otherDispatcher) otherDispatcher.use { runBlockingTest { - pauseDispatcher() numbersFromOtherDispatcherWithDelays.collect { value -> expect(value) } @@ -78,7 +77,7 @@ class TestRunBlockingOrderTest : TestBase() { val otherDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher() val numbersFromOtherDispatcherWithDelays = flow { emit(1) } - .delayEach(1) + .onEach { delay(1) } .buffer(0) .flowOn(otherDispatcher) @@ -87,13 +86,15 @@ class TestRunBlockingOrderTest : TestBase() { numbersFromOtherDispatcherWithDelays.collect { value -> expect(value) launch { - expect(2) + expect(3) } } - expect(3) + expect(2) + runCurrent() + expect(4) } } - finish(4) + finish(5) } @Test @@ -104,7 +105,7 @@ class TestRunBlockingOrderTest : TestBase() { val numbersFromOtherDispatcherWithDelays = flow { for(x in 1..max) { emit(x)} } .filter { it % 2 == 1 } - .delayEach(1) + .onEach { delay(1) } .buffer(0) .flowOn(otherDispatcher) @@ -158,7 +159,6 @@ class TestRunBlockingOrderTest : TestBase() { fun whenDispatcherPaused_runBlocking_dispatchesToTestThread() { val thread = Thread.currentThread() runBlockingTest { - pauseDispatcher() withContext(Dispatchers.IO) { expect(1) delay(1) @@ -170,23 +170,24 @@ class TestRunBlockingOrderTest : TestBase() { } @Test - fun whenDispatcherResumed_runBlocking_dispatchesImmediatelyOnIO() { + fun whenDispatcherResumed_runBlocking_dispatchesCorrectlyOnOtherDispatcher() { var thread: Thread? = null + val otherDispatcher = newFixedThreadPoolContext(1, "other dispatcher") runBlockingTest { - resumeDispatcher() - withContext(Dispatchers.IO) { + withContext(otherDispatcher) { expect(1) delay(1) expect(2) thread = Thread.currentThread() } - assertEquals(thread, Thread.currentThread()) + assertNotSame(thread, Thread.currentThread()) finish(3) } + otherDispatcher.close() } @Test - fun whenDispatcherRunning_doesntProgressDelays_inLaunchBody() { + fun doesntProgressDelays_inLaunchBody() { var state = 0 fun CoroutineScope.subject() = launch { state = 1 @@ -196,6 +197,7 @@ class TestRunBlockingOrderTest : TestBase() { runBlockingTest { subject() + runCurrent() assertEquals(1, state) advanceTimeBy(1000) assertEquals(2, state) diff --git a/kotlinx-coroutines-test/test/obsolete/TestRunBlockingTest.kt b/kotlinx-coroutines-test/test/obsolete/TestRunBlockingTest.kt index e4c113cce2..eb3239d594 100644 --- a/kotlinx-coroutines-test/test/obsolete/TestRunBlockingTest.kt +++ b/kotlinx-coroutines-test/test/obsolete/TestRunBlockingTest.kt @@ -29,41 +29,35 @@ class TestRunBlockingTest { @Test fun pauseDispatcher_disablesAutoAdvance_forCurrent() = runBlockingTest { var mutable = 0 - pauseDispatcher { - launch { - mutable++ - } - assertEquals(0, mutable) - runCurrent() - assertEquals(1, mutable) + launch { + mutable++ } + assertEquals(0, mutable) + runCurrent() + assertEquals(1, mutable) } @Test fun pauseDispatcher_disablesAutoAdvance_forDelay() = runBlockingTest { var mutable = 0 - pauseDispatcher { - launch { - mutable++ - delay(SLOW) - mutable++ - } - assertEquals(0, mutable) - runCurrent() - assertEquals(1, mutable) - advanceTimeBy(SLOW) - assertEquals(2, mutable) + launch { + mutable++ + delay(SLOW) + mutable++ } + assertEquals(0, mutable) + runCurrent() + assertEquals(1, mutable) + advanceTimeBy(SLOW) + assertEquals(2, mutable) } @Test fun pauseDispatcher_withDelay_resumesAfterPause() = runBlockingTest { var mutable = 0 assertRunsFast { - pauseDispatcher { - delay(1_000) - mutable++ - } + delay(1_000) + mutable++ } assertEquals(1, mutable) }