diff --git a/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api b/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api index 546bfe3507..7f99a31afd 100644 --- a/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api +++ b/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api @@ -545,6 +545,15 @@ public final class kotlinx/coroutines/TimeoutKt { public static final fun withTimeoutOrNull-KLykuaI (JLkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } +public final class kotlinx/coroutines/YieldContext : kotlin/coroutines/AbstractCoroutineContextElement { + public static final field Key Lkotlinx/coroutines/YieldContext$Key; + public field dispatcherWasUnconfined Z + public fun ()V +} + +public final class kotlinx/coroutines/YieldContext$Key : kotlin/coroutines/CoroutineContext$Key { +} + public final class kotlinx/coroutines/YieldKt { public static final fun yield (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } diff --git a/kotlinx-coroutines-core/common/src/Unconfined.kt b/kotlinx-coroutines-core/common/src/Unconfined.kt index 24a401f702..5837ae83f3 100644 --- a/kotlinx-coroutines-core/common/src/Unconfined.kt +++ b/kotlinx-coroutines-core/common/src/Unconfined.kt @@ -38,6 +38,7 @@ internal object Unconfined : CoroutineDispatcher() { /** * Used to detect calls to [Unconfined.dispatch] from [yield] function. */ +@PublishedApi internal class YieldContext : AbstractCoroutineContextElement(Key) { companion object Key : CoroutineContext.Key diff --git a/kotlinx-coroutines-test/api/kotlinx-coroutines-test.api b/kotlinx-coroutines-test/api/kotlinx-coroutines-test.api index f60326b0eb..f024f2105f 100644 --- a/kotlinx-coroutines-test/api/kotlinx-coroutines-test.api +++ b/kotlinx-coroutines-test/api/kotlinx-coroutines-test.api @@ -40,6 +40,13 @@ public final class kotlinx/coroutines/test/TestCoroutineDispatcher : kotlinx/cor public fun toString ()Ljava/lang/String; } +public final class kotlinx/coroutines/test/TestCoroutineDispatchersKt { + public static final fun StandardTestDispatcher (Lkotlinx/coroutines/test/TestCoroutineScheduler;Ljava/lang/String;)Lkotlinx/coroutines/test/TestDispatcher; + public static synthetic fun StandardTestDispatcher$default (Lkotlinx/coroutines/test/TestCoroutineScheduler;Ljava/lang/String;ILjava/lang/Object;)Lkotlinx/coroutines/test/TestDispatcher; + public static final fun UnconfinedTestDispatcher (Lkotlinx/coroutines/test/TestCoroutineScheduler;Ljava/lang/String;)Lkotlinx/coroutines/test/TestDispatcher; + public static synthetic fun UnconfinedTestDispatcher$default (Lkotlinx/coroutines/test/TestCoroutineScheduler;Ljava/lang/String;ILjava/lang/Object;)Lkotlinx/coroutines/test/TestDispatcher; +} + public final class kotlinx/coroutines/test/TestCoroutineExceptionHandler : kotlin/coroutines/AbstractCoroutineContextElement, kotlinx/coroutines/CoroutineExceptionHandler, kotlinx/coroutines/test/UncaughtExceptionCaptor { public fun ()V public fun cleanupTestCoroutinesCaptor ()V @@ -69,6 +76,8 @@ public final class kotlinx/coroutines/test/TestCoroutineScopeKt { public static synthetic fun TestCoroutineScope$default (Lkotlin/coroutines/CoroutineContext;ILjava/lang/Object;)Lkotlinx/coroutines/test/TestCoroutineScope; public static final fun advanceTimeBy (Lkotlinx/coroutines/test/TestCoroutineScope;J)V public static final fun advanceUntilIdle (Lkotlinx/coroutines/test/TestCoroutineScope;)V + public static final fun createTestCoroutineScope (Lkotlin/coroutines/CoroutineContext;)Lkotlinx/coroutines/test/TestCoroutineScope; + public static synthetic fun createTestCoroutineScope$default (Lkotlin/coroutines/CoroutineContext;ILjava/lang/Object;)Lkotlinx/coroutines/test/TestCoroutineScope; public static final fun getCurrentTime (Lkotlinx/coroutines/test/TestCoroutineScope;)J public static final fun pauseDispatcher (Lkotlinx/coroutines/test/TestCoroutineScope;)V public static final fun pauseDispatcher (Lkotlinx/coroutines/test/TestCoroutineScope;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; @@ -77,7 +86,6 @@ public final class kotlinx/coroutines/test/TestCoroutineScopeKt { } public abstract class kotlinx/coroutines/test/TestDispatcher : kotlinx/coroutines/CoroutineDispatcher, kotlinx/coroutines/Delay { - public fun ()V public fun delay (JLkotlin/coroutines/Continuation;)Ljava/lang/Object; public abstract fun getScheduler ()Lkotlinx/coroutines/test/TestCoroutineScheduler; public fun invokeOnTimeout (JLjava/lang/Runnable;Lkotlin/coroutines/CoroutineContext;)Lkotlinx/coroutines/DisposableHandle; diff --git a/kotlinx-coroutines-test/common/src/DelayController.kt b/kotlinx-coroutines-test/common/src/DelayController.kt index eba12efc27..8b34b8a267 100644 --- a/kotlinx-coroutines-test/common/src/DelayController.kt +++ b/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 @@ -102,7 +103,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) /** @@ -111,7 +115,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() /** @@ -121,12 +128,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( @@ -178,7 +188,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." ) } diff --git a/kotlinx-coroutines-test/common/src/TestBuilders.kt b/kotlinx-coroutines-test/common/src/TestBuilders.kt index 941f864a3e..0d5013cb88 100644 --- a/kotlinx-coroutines-test/common/src/TestBuilders.kt +++ b/kotlinx-coroutines-test/common/src/TestBuilders.kt @@ -44,7 +44,7 @@ import kotlin.coroutines.* */ @Deprecated("Use `runTest` instead to support completing from other dispatchers.", level = DeprecationLevel.WARNING) public fun runBlockingTest(context: CoroutineContext = EmptyCoroutineContext, testBody: suspend TestCoroutineScope.() -> Unit) { - val scope = TestCoroutineScope(TestCoroutineDispatcher() + SupervisorJob() + context) + val scope = createTestCoroutineScope(TestCoroutineDispatcher() + SupervisorJob() + context) val scheduler = scope.testScheduler val deferred = scope.async { scope.testBody() @@ -197,10 +197,9 @@ public expect class TestResult * * [context] can be used to affect the environment of the code under test. Beside just being passed to the coroutine * scope created for the test, [context] also can be used to change how the test is executed. - * See the [TestCoroutineScope] constructor documentation for details. + * See the [createTestCoroutineScope] documentation for details. * - * @throws IllegalArgumentException if the [context] is invalid. See the [TestCoroutineScope] constructor docs for - * details. + * @throws IllegalArgumentException if the [context] is invalid. See the [createTestCoroutineScope] docs for details. */ @ExperimentalCoroutinesApi public fun runTest( @@ -210,7 +209,7 @@ public fun runTest( ): TestResult { if (context[RunningInRunTest] != null) throw IllegalStateException("Calls to `runTest` can't be nested. Please read the docs on `TestResult` for details.") - val testScope = TestBodyCoroutine(TestCoroutineScope(context + RunningInRunTest)) + val testScope = TestBodyCoroutine(createTestCoroutineScope(context + RunningInRunTest)) val scheduler = testScope.testScheduler return createTestResult { /** TODO: moving this [AbstractCoroutine.start] call outside [createTestResult] fails on Native with diff --git a/kotlinx-coroutines-test/common/src/TestCoroutineDispatcher.kt b/kotlinx-coroutines-test/common/src/TestCoroutineDispatcher.kt index 3537910ac5..31249ee6e4 100644 --- a/kotlinx-coroutines-test/common/src/TestCoroutineDispatcher.kt +++ b/kotlinx-coroutines-test/common/src/TestCoroutineDispatcher.kt @@ -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 { @@ -34,12 +36,6 @@ public class TestCoroutineDispatcher(public override val scheduler: TestCoroutin } } - /** @suppress */ - override fun processEvent(time: Long, marker: Any) { - check(marker is Runnable) - marker.run() - } - /** @suppress */ override fun dispatch(context: CoroutineContext, block: Runnable) { checkSchedulerInContext(scheduler, context) diff --git a/kotlinx-coroutines-test/common/src/TestCoroutineDispatchers.kt b/kotlinx-coroutines-test/common/src/TestCoroutineDispatchers.kt new file mode 100644 index 0000000000..6e18bf348e --- /dev/null +++ b/kotlinx-coroutines-test/common/src/TestCoroutineDispatchers.kt @@ -0,0 +1,129 @@ +/* + * 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.channels.* +import kotlinx.coroutines.flow.* +import kotlin.coroutines.* + +/** + * Creates an instance of an unconfined [TestDispatcher]. + * + * This dispatcher is similar to [Dispatchers.Unconfined]: the tasks that it executes are not confined to any particular + * thread and form an event loop; it's different in that it skips delays, as all [TestDispatcher]s do. + * + * Using this [TestDispatcher] can greatly simplify writing tests where it's not important which thread is used when and + * in which order the queued coroutines are executed. + * The typical use case for this is launching child coroutines that are resumed immediately, without going through a + * dispatch; this can be helpful for testing [Channel] and [StateFlow] usages. + * + * ``` + * @Test + * fun testUnconfinedDispatcher() = runTest { + * val values = mutableListOf() + * val stateFlow = MutableStateFlow(0) + * val job = launch(UnconfinedTestDispatcher(testScheduler)) { + * stateFlow.collect { + * values.add(it) + * } + * } + * stateFlow.value = 1 + * stateFlow.value = 2 + * stateFlow.value = 3 + * job.cancel() + * // each assignment will immediately resume the collecting child coroutine, + * // so no values will be skipped. + * assertEquals(listOf(0, 1, 2, 3), values) + * } + * ``` + * + * However, please be aware that, like [Dispatchers.Unconfined], this is a specific dispatcher with execution order + * guarantees that are unusual and not shared by most other dispatchers, so it can only be used reliably for testing + * functionality, not the specific order of actions. + * See [Dispatchers.Unconfined] for a discussion of the execution order guarantees. + * + * In order to support delay skipping, this dispatcher is linked to a [TestCoroutineScheduler], which is used to control + * the virtual time and can be shared among many test dispatchers. If no [scheduler] is passed as an argument, a new one + * is created. + * + * Additionally, [name] can be set to distinguish each dispatcher instance when debugging. + * + * @see StandardTestDispatcher for a more predictable [TestDispatcher]. + */ +@ExperimentalCoroutinesApi +@Suppress("FunctionName") +public fun UnconfinedTestDispatcher( + scheduler: TestCoroutineScheduler = TestCoroutineScheduler(), + name: String? = null +): TestDispatcher = UnconfinedTestDispatcherImpl(scheduler, name) + +private class UnconfinedTestDispatcherImpl( + override val scheduler: TestCoroutineScheduler, + private val name: String? = null +): TestDispatcher() { + + override fun isDispatchNeeded(context: CoroutineContext): Boolean = false + + @Suppress("INVISIBLE_MEMBER") + override fun dispatch(context: CoroutineContext, block: Runnable) { + checkSchedulerInContext(scheduler, context) + scheduler.sendDispatchEvent() + + /** copy-pasted from [kotlinx.coroutines.Unconfined.dispatch] */ + /** It can only be called by the [yield] function. See also code of [yield] function. */ + val yieldContext = context[YieldContext] + if (yieldContext !== null) { + // report to "yield" that it is an unconfined dispatcher and don't call "block.run()" + yieldContext.dispatcherWasUnconfined = true + return + } + throw UnsupportedOperationException( + "Function UnconfinedTestCoroutineDispatcher.dispatch can only be used by " + + "the yield function. If you wrap Unconfined dispatcher in your code, make sure you properly delegate " + + "isDispatchNeeded and dispatch calls." + ) + } + + override fun toString(): String = "${name ?: "UnconfinedTestDispatcher"}[scheduler=$scheduler]" +} + +/** + * Creates an instance of a [TestDispatcher] whose tasks are run inside calls to the [scheduler]. + * + * This [TestDispatcher] instance does not itself execute any of the tasks. Instead, it always sends them to its + * [scheduler], which can then be accessed via [TestCoroutineScheduler.runCurrent], + * [TestCoroutineScheduler.advanceUntilIdle], or [TestCoroutineScheduler.advanceTimeBy], which will then execute these + * tasks in a blocking manner. + * + * In practice, this means that [launch] or [async] blocks will not be entered immediately (unless they are + * parameterized with [CoroutineStart.UNDISPATCHED]), and one should either call [TestCoroutineScheduler.runCurrent] to + * run these pending tasks, which will block until there are no more tasks scheduled at this point in time, or, when + * inside [runTest], call [yield] to yield the (only) thread used by [runTest] to the newly-launched coroutines. + * + * If a [scheduler] is not passed as an argument, a new one is created. + * + * One can additionally pass a [name] in order to more easily distinguish this dispatcher during debugging. + * + * @see UnconfinedTestDispatcher for a dispatcher that is not confined to any particular thread. + */ +@Suppress("FunctionName") +public fun StandardTestDispatcher( + scheduler: TestCoroutineScheduler = TestCoroutineScheduler(), + name: String? = null +): TestDispatcher = StandardTestDispatcherImpl(scheduler, name) + +private class StandardTestDispatcherImpl( + override val scheduler: TestCoroutineScheduler = TestCoroutineScheduler(), + private val name: String? = null +): TestDispatcher() { + + override fun dispatch(context: CoroutineContext, block: Runnable) { + checkSchedulerInContext(scheduler, context) + scheduler.registerEvent(this, 0, block) { false } + } + + override fun toString(): String = "${name ?: "StandardTestDispatcher"}[scheduler=$scheduler]" +} diff --git a/kotlinx-coroutines-test/common/src/TestCoroutineScope.kt b/kotlinx-coroutines-test/common/src/TestCoroutineScope.kt index 17978df229..f60a97e088 100644 --- a/kotlinx-coroutines-test/common/src/TestCoroutineScope.kt +++ b/kotlinx-coroutines-test/common/src/TestCoroutineScope.kt @@ -44,8 +44,24 @@ private class TestCoroutineScopeImpl( private val initialJobs = coroutineContext.activeJobs() override fun cleanupTestCoroutines() { + val delayController = coroutineContext.delayController + val hasUnfinishedJobs = if (delayController != null) { + try { + delayController.cleanupTestCoroutines() + false + } catch (e: UncompletedCoroutinesError) { + true + } + } else { + testScheduler.runCurrent() + !testScheduler.isIdle() + } coroutineContext.uncaughtExceptionCaptor.cleanupTestCoroutinesCaptor() - coroutineContext.delayController?.cleanupTestCoroutines() + if (hasUnfinishedJobs) + throw UncompletedCoroutinesError( + "Unfinished coroutines during teardown. Ensure all coroutines are" + + " completed or cancelled by your test." + ) val jobs = coroutineContext.activeJobs() if ((jobs - initialJobs).isNotEmpty()) throw UncompletedCoroutinesError("Test finished with active jobs: $jobs") @@ -56,13 +72,29 @@ private fun CoroutineContext.activeJobs(): Set { return checkNotNull(this[Job]).children.filter { it.isActive }.toSet() } +/** + * A coroutine scope for launching test coroutines using [TestCoroutineDispatcher]. + * + * [createTestCoroutineScope] is a similar function that defaults to [StandardTestDispatcher]. + */ +@Deprecated("This constructs a `TestCoroutineScope` with a deprecated `CoroutineDispatcher` by default. " + + "Please use `createTestCoroutineScope` instead.", + ReplaceWith("createTestCoroutineScope(TestCoroutineDispatcher() + context)", + "kotlin.coroutines.EmptyCoroutineContext"), + level = DeprecationLevel.WARNING +) +public fun TestCoroutineScope(context: CoroutineContext = EmptyCoroutineContext): TestCoroutineScope { + val scheduler = context[TestCoroutineScheduler] ?: TestCoroutineScheduler() + return createTestCoroutineScope(TestCoroutineDispatcher(scheduler) + context) +} + /** * A coroutine scope for launching test coroutines. * * It ensures that all the test module machinery is properly initialized. * * If [context] doesn't define a [TestCoroutineScheduler] for orchestrating the virtual time used for delay-skipping, * a new one is created, unless a [TestDispatcher] is provided, in which case [TestDispatcher.scheduler] is used. - * * If [context] doesn't have a [ContinuationInterceptor], a [TestCoroutineDispatcher] is created. + * * If [context] doesn't have a [ContinuationInterceptor], a [StandardTestDispatcher] is created. * * If [context] does not provide a [CoroutineExceptionHandler], [TestCoroutineExceptionHandler] is created * automatically. * * If [context] provides a [Job], that job is used for the new scope; otherwise, a [CompletableJob] is created. @@ -73,9 +105,8 @@ private fun CoroutineContext.activeJobs(): Set { * @throws IllegalArgumentException if [context] has an [CoroutineExceptionHandler] that is not an * [UncaughtExceptionCaptor]. */ -@Suppress("FunctionName") @ExperimentalCoroutinesApi -public fun TestCoroutineScope(context: CoroutineContext = EmptyCoroutineContext): TestCoroutineScope { +public fun createTestCoroutineScope(context: CoroutineContext = EmptyCoroutineContext): TestCoroutineScope { val scheduler: TestCoroutineScheduler val dispatcher = when (val dispatcher = context[ContinuationInterceptor]) { is TestDispatcher -> { @@ -91,7 +122,7 @@ public fun TestCoroutineScope(context: CoroutineContext = EmptyCoroutineContext) } null -> { scheduler = context[TestCoroutineScheduler] ?: TestCoroutineScheduler() - TestCoroutineDispatcher(scheduler) + StandardTestDispatcher(scheduler) } else -> throw IllegalArgumentException("Dispatcher must implement TestDispatcher: $dispatcher") } @@ -159,7 +190,7 @@ public fun TestCoroutineScope.advanceTimeBy(delayTimeMillis: Long): Unit = * @see TestCoroutineScheduler.advanceUntilIdle */ @ExperimentalCoroutinesApi -public fun TestCoroutineScope.advanceUntilIdle(): Unit { +public fun TestCoroutineScope.advanceUntilIdle() { coroutineContext.delayController?.advanceUntilIdle() ?: testScheduler.advanceUntilIdle() } @@ -170,13 +201,14 @@ public fun TestCoroutineScope.advanceUntilIdle(): Unit { * @see TestCoroutineScheduler.runCurrent */ @ExperimentalCoroutinesApi -public fun TestCoroutineScope.runCurrent(): Unit { +public fun TestCoroutineScope.runCurrent() { coroutineContext.delayController?.runCurrent() ?: testScheduler.runCurrent() } @ExperimentalCoroutinesApi @Deprecated("The test coroutine scope isn't able to pause its dispatchers in the general case. " + - "Only `TestCoroutineDispatcher` supports pausing; pause it directly.", + "Only `TestCoroutineDispatcher` supports pausing; pause it directly, or use a dispatcher that is always " + + "\"paused\", like `StandardTestDispatcher`.", ReplaceWith("(this.coroutineContext[ContinuationInterceptor]!! as DelayController).pauseDispatcher(block)", "kotlin.coroutines.ContinuationInterceptor"), DeprecationLevel.WARNING) @@ -186,7 +218,8 @@ public suspend fun TestCoroutineScope.pauseDispatcher(block: suspend () -> Unit) @ExperimentalCoroutinesApi @Deprecated("The test coroutine scope isn't able to pause its dispatchers in the general case. " + - "Only `TestCoroutineDispatcher` supports pausing; pause it directly.", + "Only `TestCoroutineDispatcher` supports pausing; pause it directly, or use a dispatcher that is always " + + "\"paused\", like `StandardTestDispatcher`.", ReplaceWith("(this.coroutineContext[ContinuationInterceptor]!! as DelayController).pauseDispatcher()", "kotlin.coroutines.ContinuationInterceptor"), level = DeprecationLevel.WARNING) @@ -196,7 +229,8 @@ public fun TestCoroutineScope.pauseDispatcher() { @ExperimentalCoroutinesApi @Deprecated("The test coroutine scope isn't able to pause its dispatchers in the general case. " + - "Only `TestCoroutineDispatcher` supports pausing; pause it directly.", + "Only `TestCoroutineDispatcher` supports pausing; pause it directly, or use a dispatcher that is always " + + "\"paused\", like `StandardTestDispatcher`.", ReplaceWith("(this.coroutineContext[ContinuationInterceptor]!! as DelayController).resumeDispatcher()", "kotlin.coroutines.ContinuationInterceptor"), level = DeprecationLevel.WARNING) diff --git a/kotlinx-coroutines-test/common/src/TestDispatcher.kt b/kotlinx-coroutines-test/common/src/TestDispatcher.kt index b37f10bfda..c01e5b4d7b 100644 --- a/kotlinx-coroutines-test/common/src/TestDispatcher.kt +++ b/kotlinx-coroutines-test/common/src/TestDispatcher.kt @@ -12,13 +12,16 @@ import kotlin.jvm.* * A test dispatcher that can interface with a [TestCoroutineScheduler]. */ @ExperimentalCoroutinesApi -public abstract class TestDispatcher: CoroutineDispatcher(), Delay { +public sealed class TestDispatcher: CoroutineDispatcher(), Delay { /** The scheduler that this dispatcher is linked to. */ @ExperimentalCoroutinesApi public abstract val scheduler: TestCoroutineScheduler /** Notifies the dispatcher that it should process a single event marked with [marker] happening at time [time]. */ - internal abstract fun processEvent(time: Long, marker: Any) + internal fun processEvent(time: Long, marker: Any) { + check(marker is Runnable) + marker.run() + } /** @suppress */ override fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation) { diff --git a/kotlinx-coroutines-test/common/test/Helpers.kt b/kotlinx-coroutines-test/common/test/Helpers.kt index 8be3ea106a..68d9b6ee65 100644 --- a/kotlinx-coroutines-test/common/test/Helpers.kt +++ b/kotlinx-coroutines-test/common/test/Helpers.kt @@ -4,6 +4,7 @@ package kotlinx.coroutines.test +import kotlinx.atomicfu.* import kotlin.test.* import kotlin.time.* @@ -35,3 +36,32 @@ inline fun 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.void() { } diff --git a/kotlinx-coroutines-test/common/test/RunTestTest.kt b/kotlinx-coroutines-test/common/test/RunTestTest.kt index fbca2b05ac..623b5bf758 100644 --- a/kotlinx-coroutines-test/common/test/RunTestTest.kt +++ b/kotlinx-coroutines-test/common/test/RunTestTest.kt @@ -57,7 +57,7 @@ class RunTestTest { delay(2000) } val deferred = async { - val job = launch(TestCoroutineDispatcher(testScheduler)) { + val job = launch(StandardTestDispatcher(testScheduler)) { launch { delay(500) } @@ -156,7 +156,7 @@ class RunTestTest { @Test fun reproducer2405() = runTest { - val dispatcher = TestCoroutineDispatcher(testScheduler) + val dispatcher = StandardTestDispatcher(testScheduler) var collectedError = false withContext(dispatcher) { flow { emit(1) } diff --git a/kotlinx-coroutines-test/common/test/StandardTestDispatcherTest.kt b/kotlinx-coroutines-test/common/test/StandardTestDispatcherTest.kt new file mode 100644 index 0000000000..7e8a6ad158 --- /dev/null +++ b/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() + +} \ No newline at end of file diff --git a/kotlinx-coroutines-test/common/test/TestCoroutineSchedulerTest.kt b/kotlinx-coroutines-test/common/test/TestCoroutineSchedulerTest.kt index 45c02c09ad..5e5a91f6f7 100644 --- a/kotlinx-coroutines-test/common/test/TestCoroutineSchedulerTest.kt +++ b/kotlinx-coroutines-test/common/test/TestCoroutineSchedulerTest.kt @@ -13,7 +13,7 @@ class TestCoroutineSchedulerTest { @Test fun testContextElement() = runTest { assertFailsWith { - withContext(TestCoroutineDispatcher()) { + withContext(StandardTestDispatcher()) { } } } @@ -45,35 +45,42 @@ class TestCoroutineSchedulerTest { /** Tests that if [TestCoroutineScheduler.advanceTimeBy] encounters an arithmetic overflow, all the tasks scheduled * until the moment [Long.MAX_VALUE] get run. */ @Test - fun testAdvanceTimeByEnormousDelays() = runTest { - val initialDelay = 10L - delay(initialDelay) - assertEquals(initialDelay, currentTime) - var enteredInfinity = false - launch { - delay(Long.MAX_VALUE - 1) // delay(Long.MAX_VALUE) does nothing - assertEquals(Long.MAX_VALUE, currentTime) - enteredInfinity = true - } - var enteredNearInfinity = false - launch { - delay(Long.MAX_VALUE - initialDelay - 1) - assertEquals(Long.MAX_VALUE - 1, currentTime) - enteredNearInfinity = true + fun testAdvanceTimeByEnormousDelays() = forTestDispatchers { + assertRunsFast { + with (createTestCoroutineScope(it)) { + launch { + val initialDelay = 10L + delay(initialDelay) + assertEquals(initialDelay, currentTime) + var enteredInfinity = false + launch { + delay(Long.MAX_VALUE - 1) // delay(Long.MAX_VALUE) does nothing + assertEquals(Long.MAX_VALUE, currentTime) + enteredInfinity = true + } + var enteredNearInfinity = false + launch { + delay(Long.MAX_VALUE - initialDelay - 1) + assertEquals(Long.MAX_VALUE - 1, currentTime) + enteredNearInfinity = true + } + testScheduler.advanceTimeBy(Long.MAX_VALUE) + assertFalse(enteredInfinity) + assertTrue(enteredNearInfinity) + assertEquals(Long.MAX_VALUE, currentTime) + testScheduler.runCurrent() + assertTrue(enteredInfinity) + } + testScheduler.advanceUntilIdle() + } } - testScheduler.advanceTimeBy(Long.MAX_VALUE) - assertFalse(enteredInfinity) - assertTrue(enteredNearInfinity) - assertEquals(Long.MAX_VALUE, currentTime) - testScheduler.runCurrent() - assertTrue(enteredInfinity) } /** Tests the basic functionality of [TestCoroutineScheduler.advanceTimeBy]. */ @Test fun testAdvanceTimeBy() = assertRunsFast { val scheduler = TestCoroutineScheduler() - val scope = TestCoroutineScope(scheduler) + val scope = createTestCoroutineScope(scheduler) var stage = 1 scope.launch { delay(1_000) @@ -125,48 +132,52 @@ class TestCoroutineSchedulerTest { /** Tests that [TestCoroutineScheduler.runCurrent] will not run new tasks after the current time has advanced. */ @Test - fun testRunCurrentNotDrainingQueue() = assertRunsFast { - val scheduler = TestCoroutineScheduler() - val scope = TestCoroutineScope(scheduler) - var stage = 1 - scope.launch { - delay(SLOW) - launch { + fun testRunCurrentNotDrainingQueue() = forTestDispatchers { + assertRunsFast { + val scheduler = it.scheduler + val scope = createTestCoroutineScope(it) + var stage = 1 + scope.launch { delay(SLOW) - stage = 3 + launch { + delay(SLOW) + stage = 3 + } + scheduler.advanceTimeBy(SLOW) + stage = 2 } scheduler.advanceTimeBy(SLOW) - stage = 2 + assertEquals(1, stage) + scheduler.runCurrent() + assertEquals(2, stage) + scheduler.runCurrent() + assertEquals(3, stage) } - scheduler.advanceTimeBy(SLOW) - assertEquals(1, stage) - scheduler.runCurrent() - assertEquals(2, stage) - scheduler.runCurrent() - assertEquals(3, stage) } /** Tests that [TestCoroutineScheduler.advanceUntilIdle] doesn't hang when itself running in a scheduler task. */ @Test - fun testNestedAdvanceUntilIdle() = assertRunsFast { - val scheduler = TestCoroutineScheduler() - val scope = TestCoroutineScope(scheduler) - var executed = false - scope.launch { - launch { - delay(SLOW) - executed = true + fun testNestedAdvanceUntilIdle() = forTestDispatchers { + assertRunsFast { + val scheduler = it.scheduler + val scope = createTestCoroutineScope(it) + var executed = false + scope.launch { + launch { + delay(SLOW) + executed = true + } + scheduler.advanceUntilIdle() } scheduler.advanceUntilIdle() + assertTrue(executed) } - scheduler.advanceUntilIdle() - assertTrue(executed) } /** Tests [yield] scheduling tasks for future execution and not executing immediately. */ @Test - fun testYield() { - val scope = TestCoroutineScope() + fun testYield() = forTestDispatchers { + val scope = createTestCoroutineScope(it) var stage = 0 scope.launch { yield() @@ -183,6 +194,46 @@ class TestCoroutineSchedulerTest { scope.runCurrent() } + /** Tests that dispatching the delayed tasks is ordered by their waking times. */ + @Test + fun testDelaysPriority() = forTestDispatchers { + val scope = createTestCoroutineScope(it) + var lastMeasurement = 0L + fun checkTime(time: Long) { + assertTrue(lastMeasurement < time) + assertEquals(time, scope.currentTime) + lastMeasurement = scope.currentTime + } + scope.launch { + launch { + delay(100) + checkTime(100) + val deferred = async { + delay(70) + checkTime(170) + } + delay(1) + checkTime(101) + deferred.await() + delay(1) + checkTime(171) + } + launch { + delay(200) + checkTime(200) + } + launch { + delay(150) + checkTime(150) + delay(22) + checkTime(172) + } + delay(201) + } + scope.advanceUntilIdle() + checkTime(201) + } + private fun TestCoroutineScope.checkTimeout( timesOut: Boolean, timeoutMillis: Long = SLOW, block: suspend () -> Unit ) = assertRunsFast { @@ -206,8 +257,8 @@ class TestCoroutineSchedulerTest { /** Tests that timeouts get triggered. */ @Test - fun testSmallTimeouts() { - val scope = TestCoroutineScope() + fun testSmallTimeouts() = forTestDispatchers { + val scope = createTestCoroutineScope(it) scope.checkTimeout(true) { val half = SLOW / 2 delay(half) @@ -217,8 +268,8 @@ class TestCoroutineSchedulerTest { /** Tests that timeouts don't get triggered if the code finishes in time. */ @Test - fun testLargeTimeouts() { - val scope = TestCoroutineScope() + fun testLargeTimeouts() = forTestDispatchers { + val scope = createTestCoroutineScope(it) scope.checkTimeout(false) { val half = SLOW / 2 delay(half) @@ -228,8 +279,8 @@ class TestCoroutineSchedulerTest { /** Tests that timeouts get triggered if the code fails to finish in time asynchronously. */ @Test - fun testSmallAsynchronousTimeouts() { - val scope = TestCoroutineScope() + fun testSmallAsynchronousTimeouts() = forTestDispatchers { + val scope = createTestCoroutineScope(it) val deferred = CompletableDeferred() scope.launch { val half = SLOW / 2 @@ -244,8 +295,8 @@ class TestCoroutineSchedulerTest { /** Tests that timeouts don't get triggered if the code finishes in time, even if it does so asynchronously. */ @Test - fun testLargeAsynchronousTimeouts() { - val scope = TestCoroutineScope() + fun testLargeAsynchronousTimeouts() = forTestDispatchers { + val scope = createTestCoroutineScope(it) val deferred = CompletableDeferred() scope.launch { val half = SLOW / 2 @@ -257,4 +308,19 @@ class TestCoroutineSchedulerTest { deferred.await() } } + + private fun forTestDispatchers(block: (TestDispatcher) -> Unit): Unit = + @Suppress("DEPRECATION") + listOf( + TestCoroutineDispatcher(), + TestCoroutineDispatcher().also { it.pauseDispatcher() }, + StandardTestDispatcher(), + UnconfinedTestDispatcher() + ).forEach { + try { + block(it) + } catch (e: Throwable) { + throw RuntimeException("Test failed for dispatcher $it", e) + } + } } diff --git a/kotlinx-coroutines-test/common/test/TestCoroutineScopeTest.kt b/kotlinx-coroutines-test/common/test/TestCoroutineScopeTest.kt index b81eddbb5b..9002d2d8c3 100644 --- a/kotlinx-coroutines-test/common/test/TestCoroutineScopeTest.kt +++ b/kotlinx-coroutines-test/common/test/TestCoroutineScopeTest.kt @@ -14,7 +14,7 @@ class TestCoroutineScopeTest { fun testCreateThrowsOnInvalidArguments() { for (ctx in invalidContexts) { assertFailsWith { - TestCoroutineScope(ctx) + createTestCoroutineScope(ctx) } } } @@ -24,27 +24,27 @@ class TestCoroutineScopeTest { fun testCreateProvidesScheduler() { // Creates a new scheduler. run { - val scope = TestCoroutineScope() + val scope = createTestCoroutineScope() assertNotNull(scope.coroutineContext[TestCoroutineScheduler]) } // Reuses the scheduler that the dispatcher is linked to. run { - val dispatcher = TestCoroutineDispatcher() - val scope = TestCoroutineScope(dispatcher) + val dispatcher = StandardTestDispatcher() + val scope = createTestCoroutineScope(dispatcher) assertSame(dispatcher.scheduler, scope.coroutineContext[TestCoroutineScheduler]) } // Uses the scheduler passed to it. run { val scheduler = TestCoroutineScheduler() - val scope = TestCoroutineScope(scheduler) + val scope = createTestCoroutineScope(scheduler) assertSame(scheduler, scope.coroutineContext[TestCoroutineScheduler]) assertSame(scheduler, (scope.coroutineContext[ContinuationInterceptor] as TestDispatcher).scheduler) } // Doesn't touch the passed dispatcher and the scheduler if they match. run { val scheduler = TestCoroutineScheduler() - val dispatcher = TestCoroutineDispatcher(scheduler) - val scope = TestCoroutineScope(scheduler + dispatcher) + val dispatcher = StandardTestDispatcher(scheduler) + val scope = createTestCoroutineScope(scheduler + dispatcher) assertSame(scheduler, scope.coroutineContext[TestCoroutineScheduler]) assertSame(dispatcher, scope.coroutineContext[ContinuationInterceptor]) } @@ -53,7 +53,7 @@ class TestCoroutineScopeTest { /** Tests that the cleanup procedure throws if there were uncompleted delays by the end. */ @Test fun testPresentDelaysThrowing() { - val scope = TestCoroutineScope() + val scope = createTestCoroutineScope() var result = false scope.launch { delay(5) @@ -67,7 +67,7 @@ class TestCoroutineScopeTest { /** Tests that the cleanup procedure throws if there were active jobs by the end. */ @Test fun testActiveJobsThrowing() { - val scope = TestCoroutineScope() + val scope = createTestCoroutineScope() var result = false val deferred = CompletableDeferred() scope.launch { @@ -82,7 +82,7 @@ class TestCoroutineScopeTest { /** Tests that the cleanup procedure doesn't throw if it detects that the job is already cancelled. */ @Test fun testCancelledDelaysNotThrowing() { - val scope = TestCoroutineScope() + val scope = createTestCoroutineScope() var result = false val deferred = CompletableDeferred() val job = scope.launch { @@ -98,7 +98,7 @@ class TestCoroutineScopeTest { /** Tests that uncaught exceptions are thrown at the cleanup. */ @Test fun testThrowsUncaughtExceptionsOnCleanup() { - val scope = TestCoroutineScope() + val scope = createTestCoroutineScope() val exception = TestException("test") scope.launch { throw exception @@ -111,7 +111,7 @@ class TestCoroutineScopeTest { /** Tests that uncaught exceptions take priority over uncompleted jobs when throwing on cleanup. */ @Test fun testUncaughtExceptionsPrioritizedOnCleanup() { - val scope = TestCoroutineScope() + val scope = createTestCoroutineScope() val exception = TestException("test") scope.launch { throw exception @@ -127,7 +127,7 @@ class TestCoroutineScopeTest { companion object { internal val invalidContexts = listOf( Dispatchers.Default, // not a [TestDispatcher] - TestCoroutineDispatcher() + TestCoroutineScheduler(), // the dispatcher is not linked to the scheduler + StandardTestDispatcher() + TestCoroutineScheduler(), // the dispatcher is not linked to the scheduler CoroutineExceptionHandler { _, _ -> }, // not an `UncaughtExceptionCaptor` ) } diff --git a/kotlinx-coroutines-test/common/test/TestDispatchersTest.kt b/kotlinx-coroutines-test/common/test/TestDispatchersTest.kt index 3ec110ebcf..0c3cac71ae 100644 --- a/kotlinx-coroutines-test/common/test/TestDispatchersTest.kt +++ b/kotlinx-coroutines-test/common/test/TestDispatchersTest.kt @@ -3,24 +3,11 @@ */ package kotlinx.coroutines.test -import kotlinx.atomicfu.* import kotlinx.coroutines.* import kotlin.coroutines.* import kotlin.test.* -class TestDispatchersTest { - private val actionIndex = atomic(0) - private val finished = atomic(false) - - private fun expect(index: Int) { - val wasIndex = actionIndex.incrementAndGet() - check(index == wasIndex) { "Expecting action index $index but it is actually $wasIndex" } - } - - private fun finish(index: Int) { - expect(index) - check(!finished.getAndSet(true)) { "Should call 'finish(...)' at most once" } - } +class TestDispatchersTest: OrderedExecutionTestBase() { @BeforeTest fun setUp() { diff --git a/kotlinx-coroutines-test/common/test/UnconfinedTestDispatcherTest.kt b/kotlinx-coroutines-test/common/test/UnconfinedTestDispatcherTest.kt new file mode 100644 index 0000000000..3e1c48c717 --- /dev/null +++ b/kotlinx-coroutines-test/common/test/UnconfinedTestDispatcherTest.kt @@ -0,0 +1,137 @@ +/* + * 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.channels.* +import kotlinx.coroutines.flow.* +import kotlin.test.* + +class UnconfinedTestDispatcherTest { + + @Test + fun reproducer1742() { + class ObservableValue(initial: T) { + var value: T = initial + private set + + private val listeners = mutableListOf<(T) -> Unit>() + + fun set(value: T) { + this.value = value + listeners.forEach { it(value) } + } + + fun addListener(listener: (T) -> Unit) { + listeners.add(listener) + } + + fun removeListener(listener: (T) -> Unit) { + listeners.remove(listener) + } + } + + fun ObservableValue.observe(): Flow = + callbackFlow { + val listener = { value: T -> + if (!isClosedForSend) { + trySend(value) + } + } + addListener(listener) + listener(value) + awaitClose { removeListener(listener) } + } + + val intProvider = ObservableValue(0) + val stringProvider = ObservableValue("") + var data = Pair(0, "") + val scope = CoroutineScope(UnconfinedTestDispatcher()) + scope.launch { + combine( + intProvider.observe(), + stringProvider.observe() + ) { intValue, stringValue -> Pair(intValue, stringValue) } + .collect { pair -> + data = pair + } + } + + intProvider.set(1) + stringProvider.set("3") + intProvider.set(2) + intProvider.set(3) + + scope.cancel() + assertEquals(Pair(3, "3"), data) + } + + @Test + fun reproducer2082() = runTest { + val subject1 = MutableStateFlow(1) + val subject2 = MutableStateFlow("a") + val values = mutableListOf>() + + val job = launch(UnconfinedTestDispatcher(testScheduler)) { + combine(subject1, subject2) { intVal, strVal -> intVal to strVal } + .collect { + delay(10000) + values += it + } + } + + subject1.value = 2 + delay(10000) + subject2.value = "b" + delay(10000) + + subject1.value = 3 + delay(10000) + subject2.value = "c" + delay(10000) + delay(10000) + delay(1) + + job.cancel() + + assertEquals(listOf(Pair(1, "a"), Pair(2, "a"), Pair(2, "b"), Pair(3, "b"), Pair(3, "c")), values) + } + + @Test + fun reproducer2405() = createTestResult { + val dispatcher = UnconfinedTestDispatcher() + var collectedError = false + withContext(dispatcher) { + flow { emit(1) } + .combine( + flow { throw IllegalArgumentException() } + ) { int, string -> int.toString() + string } + .catch { emit("error") } + .collect { + assertEquals("error", it) + collectedError = true + } + } + assertTrue(collectedError) + } + + /** An example from the [UnconfinedTestDispatcher] documentation. */ + @Test + fun testUnconfinedDispatcher() = runTest { + val values = mutableListOf() + val stateFlow = MutableStateFlow(0) + val job = launch(UnconfinedTestDispatcher(testScheduler)) { + stateFlow.collect { + values.add(it) + } + } + stateFlow.value = 1 + stateFlow.value = 2 + stateFlow.value = 3 + job.cancel() + assertEquals(listOf(0, 1, 2, 3), values) + } + +} \ No newline at end of file diff --git a/kotlinx-coroutines-test/common/test/TestBuildersTest.kt b/kotlinx-coroutines-test/common/test/migration/TestBuildersTest.kt similarity index 99% rename from kotlinx-coroutines-test/common/test/TestBuildersTest.kt rename to kotlinx-coroutines-test/common/test/migration/TestBuildersTest.kt index 7fefaf78b5..6d49a01fa4 100644 --- a/kotlinx-coroutines-test/common/test/TestBuildersTest.kt +++ b/kotlinx-coroutines-test/common/test/migration/TestBuildersTest.kt @@ -8,6 +8,7 @@ import kotlinx.coroutines.* import kotlin.coroutines.* import kotlin.test.* +@Suppress("DEPRECATION") class TestBuildersTest { @Test diff --git a/kotlinx-coroutines-test/common/test/TestCoroutineDispatcherOrderTest.kt b/kotlinx-coroutines-test/common/test/migration/TestCoroutineDispatcherOrderTest.kt similarity index 70% rename from kotlinx-coroutines-test/common/test/TestCoroutineDispatcherOrderTest.kt rename to kotlinx-coroutines-test/common/test/migration/TestCoroutineDispatcherOrderTest.kt index e54ba21568..93fcd909cc 100644 --- a/kotlinx-coroutines-test/common/test/TestCoroutineDispatcherOrderTest.kt +++ b/kotlinx-coroutines-test/common/test/migration/TestCoroutineDispatcherOrderTest.kt @@ -8,20 +8,8 @@ import kotlinx.atomicfu.* import kotlinx.coroutines.* import kotlin.test.* -class TestCoroutineDispatcherOrderTest { - - private val actionIndex = atomic(0) - private val finished = atomic(false) - - private fun expect(index: Int) { - val wasIndex = actionIndex.incrementAndGet() - check(index == wasIndex) { "Expecting action index $index but it is actually $wasIndex" } - } - - private fun finish(index: Int) { - expect(index) - check(!finished.getAndSet(true)) { "Should call 'finish(...)' at most once" } - } +@Suppress("DEPRECATION") +class TestCoroutineDispatcherOrderTest: OrderedExecutionTestBase() { @Test fun testAdvanceTimeBy_progressesOnEachDelay() { diff --git a/kotlinx-coroutines-test/common/test/TestCoroutineDispatcherTest.kt b/kotlinx-coroutines-test/common/test/migration/TestCoroutineDispatcherTest.kt similarity index 98% rename from kotlinx-coroutines-test/common/test/TestCoroutineDispatcherTest.kt rename to kotlinx-coroutines-test/common/test/migration/TestCoroutineDispatcherTest.kt index f14b72632c..a78d923d34 100644 --- a/kotlinx-coroutines-test/common/test/TestCoroutineDispatcherTest.kt +++ b/kotlinx-coroutines-test/common/test/migration/TestCoroutineDispatcherTest.kt @@ -7,6 +7,7 @@ package kotlinx.coroutines.test import kotlinx.coroutines.* import kotlin.test.* +@Suppress("DEPRECATION") class TestCoroutineDispatcherTest { @Test fun whenDispatcherPaused_doesNotAutoProgressCurrent() { diff --git a/kotlinx-coroutines-test/common/test/TestRunBlockingOrderTest.kt b/kotlinx-coroutines-test/common/test/migration/TestRunBlockingOrderTest.kt similarity index 76% rename from kotlinx-coroutines-test/common/test/TestRunBlockingOrderTest.kt rename to kotlinx-coroutines-test/common/test/migration/TestRunBlockingOrderTest.kt index 5d94bd2866..32514d90e8 100644 --- a/kotlinx-coroutines-test/common/test/TestRunBlockingOrderTest.kt +++ b/kotlinx-coroutines-test/common/test/migration/TestRunBlockingOrderTest.kt @@ -4,24 +4,11 @@ package kotlinx.coroutines.test -import kotlinx.atomicfu.* import kotlinx.coroutines.* import kotlin.test.* -class TestRunBlockingOrderTest { - - private val actionIndex = atomic(0) - private val finished = atomic(false) - - private fun expect(index: Int) { - val wasIndex = actionIndex.incrementAndGet() - check(index == wasIndex) { "Expecting action index $index but it is actually $wasIndex" } - } - - private fun finish(index: Int) { - expect(index) - check(!finished.getAndSet(true)) { "Should call 'finish(...)' at most once" } - } +@Suppress("DEPRECATION") +class TestRunBlockingOrderTest: OrderedExecutionTestBase() { @Test fun testLaunchImmediate() = runBlockingTest { @@ -90,4 +77,4 @@ class TestRunBlockingOrderTest { } finish(2) } -} +} \ No newline at end of file diff --git a/kotlinx-coroutines-test/common/test/TestRunBlockingTest.kt b/kotlinx-coroutines-test/common/test/migration/TestRunBlockingTest.kt similarity index 99% rename from kotlinx-coroutines-test/common/test/TestRunBlockingTest.kt rename to kotlinx-coroutines-test/common/test/migration/TestRunBlockingTest.kt index 139229e610..66c06cf49f 100644 --- a/kotlinx-coroutines-test/common/test/TestRunBlockingTest.kt +++ b/kotlinx-coroutines-test/common/test/migration/TestRunBlockingTest.kt @@ -7,6 +7,7 @@ package kotlinx.coroutines.test import kotlinx.coroutines.* import kotlin.test.* +@Suppress("DEPRECATION") class TestRunBlockingTest { @Test diff --git a/kotlinx-coroutines-test/jvm/test/MultithreadingTest.kt b/kotlinx-coroutines-test/jvm/test/MultithreadingTest.kt index 6b0c071a56..da70bf5444 100644 --- a/kotlinx-coroutines-test/jvm/test/MultithreadingTest.kt +++ b/kotlinx-coroutines-test/jvm/test/MultithreadingTest.kt @@ -3,6 +3,7 @@ */ import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* import kotlinx.coroutines.test.* import kotlin.concurrent.* import kotlin.coroutines.* @@ -98,4 +99,15 @@ class MultithreadingTest { } } } + + /** Tests that [StandardTestDispatcher] is confined to the thread that interacts with the scheduler. */ + @Test + fun testStandardTestDispatcherIsConfined() = runTest { + val initialThread = Thread.currentThread() + withContext(Dispatchers.IO) { + val ioThread = Thread.currentThread() + assertNotSame(initialThread, ioThread) + } + assertEquals(initialThread, Thread.currentThread()) + } } diff --git a/kotlinx-coroutines-test/jvm/test/RunTestStressTest.kt b/kotlinx-coroutines-test/jvm/test/RunTestStressTest.kt new file mode 100644 index 0000000000..3edaa48fbd --- /dev/null +++ b/kotlinx-coroutines-test/jvm/test/RunTestStressTest.kt @@ -0,0 +1,26 @@ +/* + * 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 kotlin.concurrent.* +import kotlin.coroutines.* +import kotlin.test.* + +class RunTestStressTest { + /** Tests that notifications about asynchronous resumptions aren't lost. */ + @Test + fun testRunTestActivityNotificationsRace() { + val n = 1_000 * stressTestMultiplier + for (i in 0 until n) { + runTest { + suspendCancellableCoroutine { cont -> + thread { + cont.resume(Unit) + } + } + } + } + } +} \ No newline at end of file