Skip to content

Implement TestScope #3015

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Nov 16, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 15 additions & 6 deletions kotlinx-coroutines-test/common/src/TestScope.kt
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import kotlin.coroutines.*
* The usual way to access a [TestScope] is to call [runTest], but it can also be constructed manually, in order to
* use it to initialize the components that participate in the test.
*
* #### Differences from [TestCoroutineScope]
* #### Differences from the deprecated [TestCoroutineScope]
*
* * This doesn't provide an equivalent of [TestCoroutineScope.cleanupTestCoroutines], and so can't be used as a
* standalone mechanism for writing tests: it does require that [runTest] is eventually called.
Expand Down Expand Up @@ -186,11 +186,20 @@ internal class TestScopeImpl(context: CoroutineContext) :
finished = true
uncaughtExceptions
}
if (exceptions.isEmpty() && (children.any { it.isActive } || !testScheduler.isIdle()))
throw UncompletedCoroutinesError(
"Unfinished coroutines during teardown. Ensure all coroutines are" +
" completed or cancelled by your test."
)
val activeJobs = children.filter { it.isActive }.toList()
if (exceptions.isEmpty()) {
if (activeJobs.isNotEmpty())
throw UncompletedCoroutinesError(
"Active jobs found during the tear-down. " +
"Ensure that all coroutines are completed or cancelled by your test. " +
"The active jobs: $activeJobs"
)
if (!testScheduler.isIdle())
throw UncompletedCoroutinesError(
"Unfinished coroutines found during the tear-down. " +
"Ensure that all coroutines are completed or cancelled by your test."
)
}
return exceptions
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,19 +71,25 @@ public fun runBlockingTestOnTestScope(
context: CoroutineContext = EmptyCoroutineContext,
testBody: suspend TestScope.() -> Unit
) {
val startJobs = context.activeJobs()
val scope = TestScope(TestCoroutineDispatcher() + SupervisorJob() + context).asSpecificImplementation()
val completeContext = TestCoroutineDispatcher() + SupervisorJob() + context
val startJobs = completeContext.activeJobs()
val scope = TestScope(completeContext).asSpecificImplementation()
scope.enter()
val scheduler = scope.testScheduler
val deferred = scope.async {
scope.start(CoroutineStart.UNDISPATCHED, scope) {
scope.testBody()
}
scheduler.advanceUntilIdle()
deferred.getCompletionExceptionOrNull()?.let {
throw it
scope.testScheduler.advanceUntilIdle()
scope.getCompletionExceptionOrNull()?.let {
val exceptions = try {
scope.leave()
} catch (e: UncompletedCoroutinesError) {
listOf()
}
(listOf(it) + exceptions).throwAll()
return
}
scope.leave().throwAll()
val jobs = context.activeJobs() - startJobs
val jobs = completeContext.activeJobs() - startJobs
if (jobs.isNotEmpty())
throw UncompletedCoroutinesError("Some jobs were not completed at the end of the test: $jobs")
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
/*
* 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.flow.*
import kotlin.test.*

/** Copy of [RunTestTest], but for [runBlockingTestOnTestScope], where applicable. */
@Suppress("DEPRECATION")
class RunBlockingTestOnTestScopeTest {

@Test
fun testRunTestWithIllegalContext() {
for (ctx in TestScopeTest.invalidContexts) {
assertFailsWith<IllegalArgumentException> {
runBlockingTestOnTestScope(ctx) { }
}
}
}

@Test
fun testThrowingInRunTestBody() {
assertFailsWith<RuntimeException> {
runBlockingTestOnTestScope {
throw RuntimeException()
}
}
}

@Test
fun testThrowingInRunTestPendingTask() {
assertFailsWith<RuntimeException> {
runBlockingTestOnTestScope {
launch {
delay(SLOW)
throw RuntimeException()
}
}
}
}

@Test
fun reproducer2405() = runBlockingTestOnTestScope {
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)
}

@Test
fun testChildrenCancellationOnTestBodyFailure() {
var job: Job? = null
assertFailsWith<AssertionError> {
runBlockingTestOnTestScope {
job = launch {
while (true) {
delay(1000)
}
}
throw AssertionError()
}
}
assertTrue(job!!.isCancelled)
}

@Test
fun testTimeout() {
assertFailsWith<TimeoutCancellationException> {
runBlockingTestOnTestScope {
withTimeout(50) {
launch {
delay(1000)
}
}
}
}
}

@Test
fun testRunTestThrowsRootCause() {
assertFailsWith<TestException> {
runBlockingTestOnTestScope {
launch {
throw TestException()
}
}
}
}

@Test
fun testCompletesOwnJob() {
var handlerCalled = false
runBlockingTestOnTestScope {
coroutineContext.job.invokeOnCompletion {
handlerCalled = true
}
}
assertTrue(handlerCalled)
}

@Test
fun testDoesNotCompleteGivenJob() {
var handlerCalled = false
val job = Job()
job.invokeOnCompletion {
handlerCalled = true
}
runBlockingTestOnTestScope(job) {
assertTrue(coroutineContext.job in job.children)
}
assertFalse(handlerCalled)
assertEquals(0, job.children.filter { it.isActive }.count())
}

@Test
fun testSuppressedExceptions() {
try {
runBlockingTestOnTestScope {
launch(SupervisorJob()) { throw TestException("x") }
launch(SupervisorJob()) { throw TestException("y") }
launch(SupervisorJob()) { throw TestException("z") }
throw TestException("w")
}
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)
}
}

@Test
fun testScopeRunTestExceptionHandler(): TestResult {
val scope = TestCoroutineScope()
return testResultMap({
try {
it()
fail("should not be reached")
} catch (e: TestException) {
// expected
}
}) {
scope.runTest {
launch(SupervisorJob()) { throw TestException("x") }
}
}
}
}
Loading