Repository: Kotlin/kotlinx.coroutines
File: kotlinx-coroutines-test/common/src/TestCoroutineScope.kt (Kotlin)

Code before:
    @ExperimentalCoroutinesApi
    public val testScheduler: TestCoroutineScheduler
        get() = coroutineContext[TestCoroutineScheduler]
            ?: throw UnsupportedOperationException("This scope does not have a TestCoroutineScheduler linked to it")
}

private class TestCoroutineScopeImpl(
    override val coroutineContext: CoroutineContext,
    private val ownJob: CompletableJob?
):
    TestCoroutineScope,
    UncaughtExceptionCaptor by coroutineContext.uncaughtExceptionCaptor
{
    override val testScheduler: TestCoroutineScheduler
        get() = coroutineContext[TestCoroutineScheduler]!!

    /** These jobs existed before the coroutine scope was used, so it's alright if they don't get cancelled. */
    private val initialJobs = coroutineContext.activeJobs()

    override fun cleanupTestCoroutines() {
        coroutineContext.uncaughtExceptionCaptor.cleanupTestCoroutinesCaptor()
        coroutineContext.delayController?.cleanupTestCoroutines()
        val jobs = coroutineContext.activeJobs()
        if ((jobs - initialJobs).isNotEmpty()) {
            val exception = UncompletedCoroutinesError("Test finished with active jobs: $jobs")
            ownJob?.completeExceptionally(exception)
            throw exception
        }
        ownJob?.complete()
    }
}

private fun CoroutineContext.activeJobs(): Set<Job> {
    return checkNotNull(this[Job]).children.filter { it.isActive }.toSet()
}

/**
 * A coroutine scope for launching test coroutines.
 *
 * It ensures that all the test module machinery is properly initialized.
 * * If [context] doesn't define a [TestCoroutineScheduler] for orchestrating the virtual time used for delay-skipping,
 *   a new one is created, unless a [TestDispatcher] is provided, in which case [TestDispatcher.scheduler] is used.
 * * If [context] doesn't have a [ContinuationInterceptor], a [TestCoroutineDispatcher] is created.
 * * If [context] does not provide a [CoroutineExceptionHandler], [TestCoroutineExceptionHandler] is created
 *   automatically.
 * * If [context] provides a [Job], that job is used for the new scope, but is not completed once the scope completes.
 *   On the other hand, if there is no [Job] in the context, a [CompletableJob] is created and completed on
 *   [TestCoroutineScope.cleanupTestCoroutines].
 *
 * @throws IllegalArgumentException if [context] has both [TestCoroutineScheduler] and a [TestDispatcher] linked to a
 * different scheduler.

Review comment (suggestion):
If `uncaughtExceptionCaptor.cleanupTestCoroutinesCaptor` throws an exception, job won't be completed

Code after:
    public val testScheduler: TestCoroutineScheduler
}

private class TestCoroutineScopeImpl(
    override val coroutineContext: CoroutineContext
):
    TestCoroutineScope,
    UncaughtExceptionCaptor by coroutineContext.uncaughtExceptionCaptor
{
    override val testScheduler: TestCoroutineScheduler
        get() = coroutineContext[TestCoroutineScheduler]!!

    /** These jobs existed before the coroutine scope was used, so it's alright if they don't get cancelled. */
    private val initialJobs = coroutineContext.activeJobs()

    override fun cleanupTestCoroutines() {
        coroutineContext.uncaughtExceptionCaptor.cleanupTestCoroutinesCaptor()
        coroutineContext.delayController?.cleanupTestCoroutines()
        val jobs = coroutineContext.activeJobs()
        if ((jobs - initialJobs).isNotEmpty())
            throw UncompletedCoroutinesError("Test finished with active jobs: $jobs")
    }
}

private fun CoroutineContext.activeJobs(): Set<Job> {
    return checkNotNull(this[Job]).children.filter { it.isActive }.toSet()
}

/**
 * A coroutine scope for launching test coroutines.
 *
 * It ensures that all the test module machinery is properly initialized.
 * * If [context] doesn't define a [TestCoroutineScheduler] for orchestrating the virtual time used for delay-skipping,
 *   a new one is created, unless a [TestDispatcher] is provided, in which case [TestDispatcher.scheduler] is used.
 * * If [context] doesn't have a [ContinuationInterceptor], a [TestCoroutineDispatcher] is created.
 * * If [context] does not provide a [CoroutineExceptionHandler], [TestCoroutineExceptionHandler] is created
 *   automatically.
 * * If [context] provides a [Job], that job is used for the new scope; otherwise, a [CompletableJob] is created.
 *
 * @throws IllegalArgumentException if [context] has both [TestCoroutineScheduler] and a [TestDispatcher] linked to a
 * different scheduler.
 * @throws IllegalArgumentException if [context] has a [ContinuationInterceptor] that is not a [TestDispatcher].
 * @throws IllegalArgumentException if [context] has an [CoroutineExceptionHandler] that is not an
 * [UncaughtExceptionCaptor].
 */
@Suppress("FunctionName")
@ExperimentalCoroutinesApi
public fun TestCoroutineScope(context: CoroutineContext = EmptyCoroutineContext): TestCoroutineScope {
    val scheduler: TestCoroutineScheduler
    val dispatcher = when (val dispatcher = context[ContinuationInterceptor]) {
        is TestDispatcher -> {