blob: c7ef9cc8e7bc3398549e9c9423233165d079c644 [file] [log] [blame]
/*
* Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/
@file:Suppress("DEPRECATION")
@file:JvmName("TestBuildersKt")
@file:JvmMultifileClass
package kotlinx.coroutines.test
import kotlinx.coroutines.*
import kotlin.coroutines.*
import kotlin.jvm.*
import kotlin.time.Duration.Companion.milliseconds
/**
* Executes a [testBody] inside an immediate execution dispatcher.
*
* This method is deprecated in favor of [runTest]. Please see the
* [migration guide](https://github.com/Kotlin/kotlinx.coroutines/blob/master/kotlinx-coroutines-test/MIGRATION.md)
* for an instruction on how to update the code for the new API.
*
* 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 {
* val deferred = async {
* delay(1_000)
* async {
* delay(1_000)
* }.await()
* }
*
* deferred.await() // result available immediately
* }
*
* ```
*
* 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.
*
* @throws AssertionError If the [testBody] does not complete (or cancel) all coroutines that it launches
* (including coroutines suspended on join/await).
*
* @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.
*/
@Deprecated(
"Use `runTest` instead to support completing from other dispatchers. " +
"Please see the migration guide for details: " +
"https://github.com/Kotlin/kotlinx.coroutines/blob/master/kotlinx-coroutines-test/MIGRATION.md",
level = DeprecationLevel.WARNING
)
// Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0
public fun runBlockingTest(
context: CoroutineContext = EmptyCoroutineContext,
testBody: suspend TestCoroutineScope.() -> Unit
) {
val scope = createTestCoroutineScope(TestCoroutineDispatcher() + SupervisorJob() + context)
val scheduler = scope.testScheduler
val deferred = scope.async {
scope.testBody()
}
scheduler.advanceUntilIdle()
deferred.getCompletionExceptionOrNull()?.let {
throw it
}
scope.cleanupTestCoroutines()
}
/**
* A version of [runBlockingTest] that works with [TestScope].
*/
@Deprecated("Use `runTest` instead to support completing from other dispatchers.", level = DeprecationLevel.WARNING)
// Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0
public fun runBlockingTestOnTestScope(
context: CoroutineContext = EmptyCoroutineContext,
testBody: suspend TestScope.() -> Unit
) {
val completeContext = TestCoroutineDispatcher() + SupervisorJob() + context
val startJobs = completeContext.activeJobs()
val scope = TestScope(completeContext).asSpecificImplementation()
scope.enter()
scope.start(CoroutineStart.UNDISPATCHED, scope) {
scope.testBody()
}
scope.testScheduler.advanceUntilIdle()
val throwable = try {
scope.getCompletionExceptionOrNull()
} catch (e: IllegalStateException) {
null // the deferred was not completed yet; `scope.legacyLeave()` should complain then about unfinished jobs
}
scope.backgroundScope.cancel()
scope.testScheduler.advanceUntilIdleOr { false }
throwable?.let {
val exceptions = try {
scope.legacyLeave()
} catch (e: UncompletedCoroutinesError) {
listOf()
}
throwAll(it, exceptions)
return
}
throwAll(null, scope.legacyLeave())
val jobs = completeContext.activeJobs() - startJobs
if (jobs.isNotEmpty())
throw UncompletedCoroutinesError("Some jobs were not completed at the end of the test: $jobs")
}
/**
* Convenience method for calling [runBlockingTest] on an existing [TestCoroutineScope].
*
* This method is deprecated in favor of [runTest], whereas [TestCoroutineScope] is deprecated in favor of [TestScope].
* Please see the
* [migration guide](https://github.com/Kotlin/kotlinx.coroutines/blob/master/kotlinx-coroutines-test/MIGRATION.md)
* for an instruction on how to update the code for the new API.
*/
@Deprecated(
"Use `runTest` instead to support completing from other dispatchers. " +
"Please see the migration guide for details: " +
"https://github.com/Kotlin/kotlinx.coroutines/blob/master/kotlinx-coroutines-test/MIGRATION.md",
level = DeprecationLevel.WARNING
)
// Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0
public fun TestCoroutineScope.runBlockingTest(block: suspend TestCoroutineScope.() -> Unit): Unit =
runBlockingTest(coroutineContext, block)
/**
* Convenience method for calling [runBlockingTestOnTestScope] on an existing [TestScope].
*/
@Deprecated("Use `runTest` instead to support completing from other dispatchers.", level = DeprecationLevel.WARNING)
// Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0
public fun TestScope.runBlockingTest(block: suspend TestScope.() -> Unit): Unit =
runBlockingTestOnTestScope(coroutineContext, block)
/**
* Convenience method for calling [runBlockingTest] on an existing [TestCoroutineDispatcher].
*
* This method is deprecated in favor of [runTest], whereas [TestCoroutineScope] is deprecated in favor of [TestScope].
* Please see the
* [migration guide](https://github.com/Kotlin/kotlinx.coroutines/blob/master/kotlinx-coroutines-test/MIGRATION.md)
* for an instruction on how to update the code for the new API.
*/
@Deprecated(
"Use `runTest` instead to support completing from other dispatchers. " +
"Please see the migration guide for details: " +
"https://github.com/Kotlin/kotlinx.coroutines/blob/master/kotlinx-coroutines-test/MIGRATION.md",
level = DeprecationLevel.WARNING
)
// Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0
public fun TestCoroutineDispatcher.runBlockingTest(block: suspend TestCoroutineScope.() -> Unit): Unit =
runBlockingTest(this, block)
/**
* This is an overload of [runTest] that works with [TestCoroutineScope].
*/
@ExperimentalCoroutinesApi
@Deprecated("Use `runTest` instead.", level = DeprecationLevel.WARNING)
// Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0
public fun runTestWithLegacyScope(
context: CoroutineContext = EmptyCoroutineContext,
dispatchTimeoutMs: Long = DEFAULT_DISPATCH_TIMEOUT_MS,
testBody: suspend TestCoroutineScope.() -> Unit
) {
if (context[RunningInRunTest] != null)
throw IllegalStateException("Calls to `runTest` can't be nested. Please read the docs on `TestResult` for details.")
val testScope = TestBodyCoroutine(createTestCoroutineScope(context + RunningInRunTest))
return createTestResult {
runTestCoroutineLegacy(
testScope,
dispatchTimeoutMs.milliseconds,
TestBodyCoroutine::tryGetCompletionCause,
testBody
) {
try {
testScope.cleanup()
emptyList()
} catch (e: UncompletedCoroutinesError) {
throw e
} catch (e: Throwable) {
listOf(e)
}
}
}
}
/**
* Runs a test in a [TestCoroutineScope] based on this one.
*
* Calls [runTest] using a coroutine context from this [TestCoroutineScope]. The [TestCoroutineScope] used to run the
* [block] will be different from this one, but will use its [Job] as a parent.
*
* Since this function returns [TestResult], in order to work correctly on the JS, its result must be returned
* immediately from the test body. See the docs for [TestResult] for details.
*/
@ExperimentalCoroutinesApi
@Deprecated("Use `TestScope.runTest` instead.", level = DeprecationLevel.WARNING)
// Since 1.6.0, ERROR in 1.7.0 and removed as experimental in 1.8.0
public fun TestCoroutineScope.runTest(
dispatchTimeoutMs: Long = DEFAULT_DISPATCH_TIMEOUT_MS,
block: suspend TestCoroutineScope.() -> Unit
): TestResult = runTestWithLegacyScope(coroutineContext, dispatchTimeoutMs, block)
private class TestBodyCoroutine(
private val testScope: TestCoroutineScope,
) : AbstractCoroutine<Unit>(testScope.coroutineContext, initParentJob = true, active = true), TestCoroutineScope {
override val testScheduler get() = testScope.testScheduler
@Deprecated(
"This deprecation is to prevent accidentally calling `cleanupTestCoroutines` in our own code.",
ReplaceWith("this.cleanup()"),
DeprecationLevel.ERROR
)
override fun cleanupTestCoroutines() =
throw UnsupportedOperationException(
"Calling `cleanupTestCoroutines` inside `runTest` is prohibited: " +
"it will be called at the end of the test in any case."
)
fun cleanup() = testScope.cleanupTestCoroutines()
/** Throws an exception if the coroutine is not completing. */
fun tryGetCompletionCause(): Throwable? = completionCause
}