Skip to content

Commit

Permalink
Fixes
Browse files Browse the repository at this point in the history
  • Loading branch information
dkhalanskyjb committed Oct 27, 2021
1 parent 51950b5 commit 015b06c
Show file tree
Hide file tree
Showing 15 changed files with 246 additions and 116 deletions.
20 changes: 15 additions & 5 deletions kotlinx-coroutines-test/common/src/DelayController.kt
@@ -1,6 +1,7 @@
/*
* Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/
@file:Suppress("DEPRECATION")

package kotlinx.coroutines.test

Expand Down Expand Up @@ -100,7 +101,10 @@ public interface DelayController {
* 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.
*/
@ExperimentalCoroutinesApi
@Deprecated(
"Please use a dispatcher that is paused by default, like `StandardTestDispatcher`.",
level = DeprecationLevel.WARNING
)
public suspend fun pauseDispatcher(block: suspend () -> Unit)

/**
Expand All @@ -109,7 +113,10 @@ public interface DelayController {
* When paused, the dispatcher will not execute any coroutines automatically, and you must call [runCurrent] or
* [advanceTimeBy], or [advanceUntilIdle] to execute coroutines.
*/
@ExperimentalCoroutinesApi
@Deprecated(
"Please use a dispatcher that is paused by default, like `StandardTestDispatcher`.",
level = DeprecationLevel.WARNING
)
public fun pauseDispatcher()

/**
Expand All @@ -119,12 +126,15 @@ public interface DelayController {
* time and execute coroutines scheduled in the future use, one of [advanceTimeBy],
* or [advanceUntilIdle].
*/
@ExperimentalCoroutinesApi
@Deprecated(
"Please use a dispatcher that is paused by default, like `StandardTestDispatcher`.",
level = DeprecationLevel.WARNING
)
public fun resumeDispatcher()
}

internal interface SchedulerAsDelayController: DelayController {
public val scheduler: TestCoroutineScheduler
val scheduler: TestCoroutineScheduler

/** @suppress */
@Deprecated("This property delegates to the test scheduler, which may cause confusing behavior unless made explicit.",
Expand Down Expand Up @@ -167,7 +177,7 @@ internal interface SchedulerAsDelayController: DelayController {
scheduler.runCurrent()
if (!scheduler.isIdle()) {
throw UncompletedCoroutinesError(
"Unfinished coroutines during teardown. Ensure all coroutines are" +
"Unfinished coroutines during tear-down. Ensure all coroutines are" +
" completed or cancelled by your test."
)
}
Expand Down
Expand Up @@ -21,7 +21,9 @@ import kotlin.coroutines.*
*
* @see DelayController
*/
@ExperimentalCoroutinesApi
@Deprecated("The execution order of `TestCoroutineDispatcher` can be confusing, and the mechanism of " +
"pausing is typically misunderstood. Please use `StandardTestDispatcher` or `UnconfinedTestDispatcher` instead.",
level = DeprecationLevel.WARNING)
public class TestCoroutineDispatcher(public override val scheduler: TestCoroutineScheduler = TestCoroutineScheduler()):
TestDispatcher(), Delay, SchedulerAsDelayController
{
Expand Down
Expand Up @@ -86,8 +86,7 @@ public class UnconfinedTestDispatcher(
*
* One can additionally pass a [name] in order to more easily distinguish this dispatcher during debugging.
*
* @see UnconfinedTestDispatcher for a dispatcher that immediately enters [launch] and [async] blocks and is not
* confined to any particular thread.
* @see UnconfinedTestDispatcher for a dispatcher that is not confined to any particular thread.
*/
@ExperimentalCoroutinesApi
public class StandardTestDispatcher(
Expand Down
3 changes: 2 additions & 1 deletion kotlinx-coroutines-test/common/src/TestCoroutineScope.kt
Expand Up @@ -46,12 +46,13 @@ private class TestCoroutineScopeImpl(

override fun cleanupTestCoroutines() {
try {
coroutineContext.uncaughtExceptionCaptor.cleanupTestCoroutinesCaptor()
val delayController = coroutineContext.delayController
if (delayController != null) {
delayController.cleanupTestCoroutines()
coroutineContext.uncaughtExceptionCaptor.cleanupTestCoroutinesCaptor()
} else {
testScheduler.runCurrent()
coroutineContext.uncaughtExceptionCaptor.cleanupTestCoroutinesCaptor()
if (!testScheduler.isIdle()) {
throw UncompletedCoroutinesError(
"Unfinished coroutines during teardown. Ensure all coroutines are" +
Expand Down
30 changes: 30 additions & 0 deletions kotlinx-coroutines-test/common/test/Helpers.kt
Expand Up @@ -4,6 +4,7 @@

package kotlinx.coroutines.test

import kotlinx.atomicfu.*
import kotlin.test.*
import kotlin.time.*

Expand Down Expand Up @@ -35,3 +36,32 @@ inline fun <T> assertRunsFast(block: () -> T): T = assertRunsFast(Duration.secon
expect fun testResultMap(block: (() -> Unit) -> Unit, test: () -> TestResult): TestResult

class TestException(message: String? = null): Exception(message)

/**
* A class inheriting from which allows to check the execution order inside tests.
*
* @see TestBase
*/
open class OrderedExecutionTestBase {
private val actionIndex = atomic(0)
private val finished = atomic(false)

/** Expect the next action to be [index] in order. */
protected fun expect(index: Int) {
val wasIndex = actionIndex.incrementAndGet()
check(index == wasIndex) { "Expecting action index $index but it is actually $wasIndex" }
}

/** Expect this action to be final, with the given [index]. */
protected fun finish(index: Int) {
expect(index)
check(!finished.getAndSet(true)) { "Should call 'finish(...)' at most once" }
}

@AfterTest
fun ensureFinishCalls() {
assertTrue(finished.value || actionIndex.value == 0, "Expected `finish` to be called")
}
}

internal fun <T> T.void() { }
2 changes: 1 addition & 1 deletion kotlinx-coroutines-test/common/test/RunTestTest.kt
Expand Up @@ -57,7 +57,7 @@ class RunTestTest {
delay(2000)
}
val deferred = async {
val job = launch(TestCoroutineDispatcher(testScheduler)) {
val job = launch(StandardTestDispatcher(testScheduler)) {
launch {
delay(500)
}
Expand Down
57 changes: 57 additions & 0 deletions kotlinx-coroutines-test/common/test/StandardTestDispatcherTest.kt
@@ -0,0 +1,57 @@
/*
* Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/

package kotlinx.coroutines.test

import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import kotlin.test.*

class StandardTestDispatcherTest: OrderedExecutionTestBase() {

private val scope = createTestCoroutineScope(StandardTestDispatcher())

@AfterTest
fun cleanup() = scope.cleanupTestCoroutines()

/** Tests that the [StandardTestDispatcher] follows an execution order similar to `runBlocking`. */
@Test
fun testFlowsNotSkippingValues() = scope.launch {
// https://github.com/Kotlin/kotlinx.coroutines/issues/1626#issuecomment-554632852
val list = flowOf(1).onStart { emit(0) }
.combine(flowOf("A")) { int, str -> "$str$int" }
.toList()
assertEquals(list, listOf("A0", "A1"))
}.void()

/** Tests that each [launch] gets dispatched. */
@Test
fun testLaunchDispatched() = scope.launch {
expect(1)
launch {
expect(3)
}
finish(2)
}.void()

/** Tests that dispatching is done in a predictable order and [yield] puts this task at the end of the queue. */
@Test
fun testYield() = scope.launch {
expect(1)
scope.launch {
expect(3)
yield()
expect(6)
}
scope.launch {
expect(4)
yield()
finish(7)
}
expect(2)
yield()
expect(5)
}.void()

}

0 comments on commit 015b06c

Please sign in to comment.