package kotlinx.coroutines.test import kotlinx.coroutines.* import kotlinx.coroutines.internal.* import kotlinx.coroutines.flow.* import kotlinx.coroutines.testing.* import kotlin.coroutines.* import kotlin.test.* import kotlin.test.assertFailsWith 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 { 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 { 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(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(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(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().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", "INVISIBLE_REFERENCE") // do not remove the INVISIBLE_REFERENCE suppression: required in K2 val suppressed = unwrap(e).suppressedExceptions assertEquals(1, suppressed.size, "$suppressed") assertIs(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", "INVISIBLE_REFERENCE") // do not remove the INVISIBLE_REFERENCE suppression: required in K2 val suppressed = unwrap(e).suppressedExceptions assertEquals(1, suppressed.size, "$suppressed") assertIs(suppressed[0]).also { assertEquals("A", it.message) } } }) { runTest(timeout = 100.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 { runTest(ctx) { } } } } /** Tests that throwing exceptions in [runTest] fails the test with them. */ @Test fun testThrowingInRunTestBody() = testResultMap({ assertFailsWith { it() } }) { runTest { throw RuntimeException() } } /** Tests that throwing exceptions in pending tasks [runTest] fails the test with them. */ @Test fun testThrowingInRunTestPendingTask() = testResultMap({ assertFailsWith { 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 { 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 { it() } assertTrue(job!!.isCancelled) }) { runTest { job = launch { while (true) { delay(1000) } } throw AssertionError() } } } /** Tests that [runTest] reports [TimeoutCancellationException]. */ @Test fun testTimeout() = testResultMap({ assertFailsWith { 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 { 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) } } /** * Tests that [runTest] cleans up the exception handler even if it threw on initialization. * * This test must be run manually, because it writes garbage to the log. * * The JVM-only source set contains a test equivalent to this one that isn't ignored. */ @Test @Ignore fun testExceptionCaptorCleanedUpOnPreliminaryExit(): TestResult = testResultChain({ // step 1: installing the exception handler println("step 1") runTest { } }, { it.getOrThrow() // step 2: throwing an uncaught exception to be caught by the exception-handling system println("step 2") createTestResult { launch(NonCancellable) { throw TestException("A") } } }, { it.getOrThrow() // step 3: trying to run a test should immediately fail, even before entering the test body println("step 3") try { runTest { fail("unreached") } fail("unreached") } catch (e: UncaughtExceptionsBeforeTest) { val cause = e.suppressedExceptions.single() assertIs(cause) assertEquals("A", cause.message) } // step 4: trying to run a test again should not fail with an exception println("step 4") runTest { } }, { it.getOrThrow() // step 5: throwing an uncaught exception to be caught by the exception-handling system, again println("step 5") createTestResult { launch(NonCancellable) { throw TestException("B") } } }, { it.getOrThrow() // step 6: trying to run a test should immediately fail, again println("step 6") try { runTest { fail("unreached") } fail("unreached") } catch (e: Exception) { val cause = e.suppressedExceptions.single() assertIs(cause) assertEquals("B", cause.message) } // step 7: trying to run a test again should not fail with an exception, again println("step 7") runTest { } }) }