@file:Suppress("DEPRECATION") package kotlinx.coroutines.test import kotlinx.coroutines.* import kotlinx.coroutines.testing.* import kotlin.coroutines.* import kotlin.test.* import kotlin.test.assertFailsWith class TestCoroutineScopeTest { /** Tests failing to create a [TestCoroutineScope] with incorrect contexts. */ @Test fun testCreateThrowsOnInvalidArguments() { for (ctx in invalidContexts) { assertFailsWith { createTestCoroutineScope(ctx) } } } /** Tests that a newly-created [TestCoroutineScope] provides the correct scheduler. */ @Test fun testCreateProvidesScheduler() { // Creates a new scheduler. run { val scope = createTestCoroutineScope() assertNotNull(scope.coroutineContext[TestCoroutineScheduler]) } // Reuses the scheduler that the dispatcher is linked to. run { 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 = 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 = StandardTestDispatcher(scheduler) val scope = createTestCoroutineScope(scheduler + dispatcher) assertSame(scheduler, scope.coroutineContext[TestCoroutineScheduler]) assertSame(dispatcher, scope.coroutineContext[ContinuationInterceptor]) } // Reuses the scheduler of `Dispatchers.Main` run { val scheduler = TestCoroutineScheduler() val mainDispatcher = StandardTestDispatcher(scheduler) Dispatchers.setMain(mainDispatcher) try { val scope = createTestCoroutineScope() assertSame(scheduler, scope.coroutineContext[TestCoroutineScheduler]) assertNotSame(mainDispatcher, scope.coroutineContext[ContinuationInterceptor]) } finally { Dispatchers.resetMain() } } // Does not reuse the scheduler of `Dispatchers.Main` if one is explicitly passed run { val mainDispatcher = StandardTestDispatcher() Dispatchers.setMain(mainDispatcher) try { val scheduler = TestCoroutineScheduler() val scope = createTestCoroutineScope(scheduler) assertSame(scheduler, scope.coroutineContext[TestCoroutineScheduler]) assertNotSame(mainDispatcher.scheduler, scope.coroutineContext[TestCoroutineScheduler]) assertNotSame(mainDispatcher, scope.coroutineContext[ContinuationInterceptor]) } finally { Dispatchers.resetMain() } } } /** Tests that the cleanup procedure throws if there were uncompleted delays by the end. */ @Test fun testPresentDelaysThrowing() { val scope = createTestCoroutineScope() var result = false scope.launch { delay(5) result = true } assertFalse(result) assertFailsWith { scope.cleanupTestCoroutines() } assertFalse(result) } /** Tests that the cleanup procedure throws if there were active jobs by the end. */ @Test fun testActiveJobsThrowing() { val scope = createTestCoroutineScope() var result = false val deferred = CompletableDeferred() scope.launch { deferred.await() result = true } assertFalse(result) assertFailsWith { scope.cleanupTestCoroutines() } assertFalse(result) } /** Tests that the cleanup procedure doesn't throw if it detects that the job is already cancelled. */ @Test fun testCancelledDelaysNotThrowing() { val scope = createTestCoroutineScope() var result = false val deferred = CompletableDeferred() val job = scope.launch { deferred.await() result = true } job.cancel() assertFalse(result) scope.cleanupTestCoroutines() assertFalse(result) } /** Tests that uncaught exceptions are thrown at the cleanup. */ @Test fun testThrowsUncaughtExceptionsOnCleanup() { val scope = createTestCoroutineScope() val exception = TestException("test") scope.launch { throw exception } assertFailsWith { scope.cleanupTestCoroutines() } } /** Tests that uncaught exceptions take priority over uncompleted jobs when throwing on cleanup. */ @Test fun testUncaughtExceptionsPrioritizedOnCleanup() { val scope = createTestCoroutineScope() val exception = TestException("test") scope.launch { throw exception } scope.launch { delay(1000) } assertFailsWith { scope.cleanupTestCoroutines() } } /** Tests that cleaning up twice is forbidden. */ @Test fun testClosingTwice() { val scope = createTestCoroutineScope() scope.cleanupTestCoroutines() assertFailsWith { scope.cleanupTestCoroutines() } } /** Tests that, when reporting several exceptions, the first one is thrown, with the rest suppressed. */ @Test fun testSuppressedExceptions() { createTestCoroutineScope().apply { launch(SupervisorJob()) { throw TestException("x") } launch(SupervisorJob()) { throw TestException("y") } launch(SupervisorJob()) { throw TestException("z") } try { cleanupTestCoroutines() fail("should not be reached") } catch (e: TestException) { assertEquals("x", e.message) assertEquals(2, e.suppressedExceptions.size) assertEquals("y", e.suppressedExceptions[0].message) assertEquals("z", e.suppressedExceptions[1].message) } } } /** Tests that constructing a new [TestCoroutineScope] using another one's scope works and overrides the exception * handler. */ @Test fun testCopyingContexts() { val deferred = CompletableDeferred() val scope1 = createTestCoroutineScope() scope1.launch { deferred.await() } // a pending job in the outer scope val scope2 = createTestCoroutineScope(scope1.coroutineContext) val scope3 = createTestCoroutineScope(scope1.coroutineContext) assertEquals( scope1.coroutineContext.minusKey(CoroutineExceptionHandler), scope2.coroutineContext.minusKey(CoroutineExceptionHandler)) scope2.launch(SupervisorJob()) { throw TestException("x") } // will fail the cleanup of scope2 try { scope2.cleanupTestCoroutines() fail("should not be reached") } catch (e: TestException) { } scope3.cleanupTestCoroutines() // the pending job in the outer scope will not cause this to fail try { scope1.cleanupTestCoroutines() fail("should not be reached") } catch (e: UncompletedCoroutinesError) { // the pending job in the outer scope } } companion object { internal val invalidContexts = listOf( Dispatchers.Default, // not a [TestDispatcher] CoroutineExceptionHandler { _, _ -> }, // not an [UncaughtExceptionCaptor] StandardTestDispatcher() + TestCoroutineScheduler(), // the dispatcher is not linked to the scheduler ) } }