blob: 1a626137906d45a52216fd7ed373485855c6ee1e [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")
package kotlinx.coroutines.test
import kotlinx.coroutines.*
import kotlin.coroutines.*
import kotlin.test.*
class TestCoroutineScopeTest {
/** Tests failing to create a [TestCoroutineScope] with incorrect contexts. */
@Test
fun testCreateThrowsOnInvalidArguments() {
for (ctx in invalidContexts) {
assertFailsWith<IllegalArgumentException> {
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<AssertionError> { 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<String>()
scope.launch {
deferred.await()
result = true
}
assertFalse(result)
assertFailsWith<AssertionError> { 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<String>()
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<TestException> {
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<TestException> {
scope.cleanupTestCoroutines()
}
}
/** Tests that cleaning up twice is forbidden. */
@Test
fun testClosingTwice() {
val scope = createTestCoroutineScope()
scope.cleanupTestCoroutines()
assertFailsWith<IllegalStateException> {
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<Unit>()
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
)
}
}