blob: 183eb8cb3ab72e146d278f0f012cebe396a18c1a [file] [log] [blame]
/*
* 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.internal.*
import kotlinx.coroutines.flow.*
import kotlin.coroutines.*
import kotlin.test.*
import kotlin.time.*
import kotlin.time.Duration.Companion.milliseconds
class RunTestTest {
/** Tests that [withContext] that sends work to other threads works in [runTest]. */
@Test
fun testWithContextDispatching() = runTest {
var counter = 0
withContext(Dispatchers.Default) {
counter += 1
}
assertEquals(counter, 1)
}
/** Tests that joining [GlobalScope.launch] works in [runTest]. */
@Test
fun testJoiningForkedJob() = runTest {
var counter = 0
val job = GlobalScope.launch {
counter += 1
}
job.join()
assertEquals(counter, 1)
}
/** Tests [suspendCoroutine] not failing [runTest]. */
@Test
fun testSuspendCoroutine() = runTest {
val answer = suspendCoroutine<Int> {
it.resume(42)
}
assertEquals(42, answer)
}
/** Tests that [runTest] attempts to detect it being run inside another [runTest] and failing in such scenarios. */
@Test
fun testNestedRunTestForbidden() = runTest {
assertFailsWith<IllegalStateException> {
runTest { }
}
}
/** Tests that even the dispatch timeout of `0` is fine if all the dispatches go through the same scheduler. */
@Test
fun testRunTestWithZeroDispatchTimeoutWithControlledDispatches() = runTest(dispatchTimeoutMs = 0) {
// below is some arbitrary concurrent code where all dispatches go through the same scheduler.
launch {
delay(2000)
}
val deferred = async {
val job = launch(StandardTestDispatcher(testScheduler)) {
launch {
delay(500)
}
delay(1000)
}
job.join()
}
deferred.await()
}
/** Tests that too low of a dispatch timeout causes crashes. */
@Test
fun testRunTestWithSmallDispatchTimeout() = testResultMap({ fn ->
try {
fn()
fail("shouldn't be reached")
} catch (e: Throwable) {
assertIs<UncompletedCoroutinesError>(e)
}
}) {
runTest(dispatchTimeoutMs = 100) {
withContext(Dispatchers.Default) {
delay(10000)
3
}
fail("shouldn't be reached")
}
}
/**
* Tests that [runTest] times out after the specified time.
*/
@Test
fun testRunTestWithSmallTimeout() = testResultMap({ fn ->
try {
fn()
fail("shouldn't be reached")
} catch (e: Throwable) {
assertIs<UncompletedCoroutinesError>(e)
}
}) {
runTest(timeout = 100.milliseconds) {
withContext(Dispatchers.Default) {
delay(10000)
3
}
fail("shouldn't be reached")
}
}
/** Tests that [runTest] times out after the specified time, even if the test framework always knows the test is
* still doing something. */
@Test
fun testRunTestWithSmallTimeoutAndManyDispatches() = testResultMap({ fn ->
try {
fn()
fail("shouldn't be reached")
} catch (e: Throwable) {
assertIs<UncompletedCoroutinesError>(e)
}
}) {
runTest(timeout = 100.milliseconds) {
while (true) {
withContext(Dispatchers.Default) {
delay(10)
3
}
}
}
}
/** Tests that, on timeout, the names of the active coroutines are listed,
* whereas the names of the completed ones are not. */
@Test
@NoJs
@NoNative
fun testListingActiveCoroutinesOnTimeout(): TestResult {
val name1 = "GoodUniqueName"
val name2 = "BadUniqueName"
return testResultMap({
try {
it()
fail("unreached")
} catch (e: UncompletedCoroutinesError) {
assertContains(e.message ?: "", name1)
assertFalse((e.message ?: "").contains(name2))
}
}) {
runTest(dispatchTimeoutMs = 10) {
launch(CoroutineName(name1)) {
CompletableDeferred<Unit>().await()
}
launch(CoroutineName(name2)) {
}
}
}
}
/** Tests that the [UncompletedCoroutinesError] suppresses an exception with which the coroutine is completing. */
@Test
fun testFailureWithPendingCoroutine() = testResultMap({
try {
it()
fail("unreached")
} catch (e: UncompletedCoroutinesError) {
@Suppress("INVISIBLE_MEMBER")
val suppressed = unwrap(e).suppressedExceptions
assertEquals(1, suppressed.size, "$suppressed")
assertIs<TestException>(suppressed[0]).also {
assertEquals("A", it.message)
}
}
}) {
runTest(timeout = 10.milliseconds) {
launch(start = CoroutineStart.UNDISPATCHED) {
withContext(NonCancellable + Dispatchers.Default) {
delay(100.milliseconds)
}
}
throw TestException("A")
}
}
/** Tests that real delays can be accounted for with a large enough dispatch timeout. */
@Test
fun testRunTestWithLargeDispatchTimeout() = runTest(dispatchTimeoutMs = 5000) {
withContext(Dispatchers.Default) {
delay(50)
}
}
/** Tests that delays can be accounted for with a large enough timeout. */
@Test
fun testRunTestWithLargeTimeout() = runTest(timeout = 5000.milliseconds) {
withContext(Dispatchers.Default) {
delay(50)
}
}
/** Tests uncaught exceptions being suppressed by the dispatch timeout error. */
@Test
fun testRunTestTimingOutAndThrowing() = testResultMap({ fn ->
try {
fn()
fail("unreached")
} catch (e: UncompletedCoroutinesError) {
@Suppress("INVISIBLE_MEMBER")
val suppressed = unwrap(e).suppressedExceptions
assertEquals(1, suppressed.size, "$suppressed")
assertIs<TestException>(suppressed[0]).also {
assertEquals("A", it.message)
}
}
}) {
runTest(timeout = 1.milliseconds) {
coroutineContext[CoroutineExceptionHandler]!!.handleException(coroutineContext, TestException("A"))
withContext(Dispatchers.Default) {
delay(10000)
3
}
fail("shouldn't be reached")
}
}
/** Tests that passing invalid contexts to [runTest] causes it to fail (on JS, without forking). */
@Test
fun testRunTestWithIllegalContext() {
for (ctx in TestScopeTest.invalidContexts) {
assertFailsWith<IllegalArgumentException> {
runTest(ctx) { }
}
}
}
/** Tests that throwing exceptions in [runTest] fails the test with them. */
@Test
fun testThrowingInRunTestBody() = testResultMap({
assertFailsWith<RuntimeException> { it() }
}) {
runTest {
throw RuntimeException()
}
}
/** Tests that throwing exceptions in pending tasks [runTest] fails the test with them. */
@Test
fun testThrowingInRunTestPendingTask() = testResultMap({
assertFailsWith<RuntimeException> { it() }
}) {
runTest {
launch {
delay(SLOW)
throw RuntimeException()
}
}
}
@Test
fun reproducer2405() = runTest {
val dispatcher = StandardTestDispatcher(testScheduler)
var collectedError = false
withContext(dispatcher) {
flow { emit(1) }
.combine(
flow<String> { throw IllegalArgumentException() }
) { int, string -> int.toString() + string }
.catch { emit("error") }
.collect {
assertEquals("error", it)
collectedError = true
}
}
assertTrue(collectedError)
}
/** Tests that, once the test body has thrown, the child coroutines are cancelled. */
@Test
fun testChildrenCancellationOnTestBodyFailure(): TestResult {
var job: Job? = null
return testResultMap({
assertFailsWith<AssertionError> { it() }
assertTrue(job!!.isCancelled)
}) {
runTest {
job = launch {
while (true) {
delay(1000)
}
}
throw AssertionError()
}
}
}
/** Tests that [runTest] reports [TimeoutCancellationException]. */
@Test
fun testTimeout() = testResultMap({
assertFailsWith<TimeoutCancellationException> { it() }
}) {
runTest {
withTimeout(50) {
launch {
delay(1000)
}
}
}
}
/** Checks that [runTest] throws the root cause and not [JobCancellationException] when a child coroutine throws. */
@Test
fun testRunTestThrowsRootCause() = testResultMap({
assertFailsWith<TestException> { it() }
}) {
runTest {
launch {
throw TestException()
}
}
}
/** Tests that [runTest] completes its job. */
@Test
fun testCompletesOwnJob(): TestResult {
var handlerCalled = false
return testResultMap({
it()
assertTrue(handlerCalled)
}) {
runTest {
coroutineContext.job.invokeOnCompletion {
handlerCalled = true
}
}
}
}
/** Tests that [runTest] doesn't complete the job that was passed to it as an argument. */
@Test
fun testDoesNotCompleteGivenJob(): TestResult {
var handlerCalled = false
val job = Job()
job.invokeOnCompletion {
handlerCalled = true
}
return testResultMap({
it()
assertFalse(handlerCalled)
assertEquals(0, job.children.filter { it.isActive }.count())
}) {
runTest(job) {
assertTrue(coroutineContext.job in job.children)
}
}
}
/** Tests that, when the test body fails, the reported exceptions are suppressed. */
@Test
fun testSuppressedExceptions() = testResultMap({
try {
it()
fail("should not be reached")
} catch (e: TestException) {
assertEquals("w", e.message)
val suppressed = e.suppressedExceptions +
(e.suppressedExceptions.firstOrNull()?.suppressedExceptions ?: emptyList())
assertEquals(3, suppressed.size)
assertEquals("x", suppressed[0].message)
assertEquals("y", suppressed[1].message)
assertEquals("z", suppressed[2].message)
}
}) {
runTest {
launch(SupervisorJob()) { throw TestException("x") }
launch(SupervisorJob()) { throw TestException("y") }
launch(SupervisorJob()) { throw TestException("z") }
throw TestException("w")
}
}
/** Tests that [TestScope.runTest] does not inherit the exception handler and works. */
@Test
fun testScopeRunTestExceptionHandler(): TestResult {
val scope = TestScope()
return testResultMap({
try {
it()
fail("should not be reached")
} catch (e: TestException) {
// expected
}
}) {
scope.runTest {
launch(SupervisorJob()) { throw TestException("x") }
}
}
}
/**
* Tests that if the main coroutine is completed without a dispatch, [runTest] will not consider this to be
* inactivity.
*
* The test will hang if this is not the case.
*/
@Test
fun testCoroutineCompletingWithoutDispatch() = runTest(timeout = Duration.INFINITE) {
launch(Dispatchers.Default) { delay(100) }
}
}