Skip to content

Commit

Permalink
Improve tests
Browse files Browse the repository at this point in the history
  • Loading branch information
dkhalanskyjb committed Oct 15, 2021
1 parent e8c09bc commit 83d0175
Show file tree
Hide file tree
Showing 13 changed files with 149 additions and 131 deletions.
5 changes: 3 additions & 2 deletions kotlinx-coroutines-test/common/src/DelayController.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/*
* 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

Expand Down Expand Up @@ -143,7 +144,7 @@ public interface DelayController {
public class UncompletedCoroutinesError(message: String): AssertionError(message)

internal interface SchedulerAsDelayController: DelayController {
public val scheduler: TestCoroutineScheduler
val scheduler: TestCoroutineScheduler

/** @suppress */
@Deprecated("This property delegates to the test scheduler, which may cause confusing behavior unless made explicit.",
Expand Down Expand Up @@ -186,7 +187,7 @@ internal interface SchedulerAsDelayController: DelayController {
scheduler.runCurrent()
if (!scheduler.isIdle()) {
throw UncompletedCoroutinesError(
"Unfinished coroutines during teardown. Ensure all coroutines are" +
"Unfinished coroutines during tear-down. Ensure all coroutines are" +
" completed or cancelled by your test."
)
}
Expand Down
30 changes: 29 additions & 1 deletion kotlinx-coroutines-test/common/test/Helpers.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

package kotlinx.coroutines.test

import kotlinx.atomicfu.*
import kotlin.test.*
import kotlin.time.*

Expand Down Expand Up @@ -32,4 +33,31 @@ inline fun <T> assertRunsFast(block: () -> T): T = assertRunsFast(Duration.secon
/**
* Passes [test] as an argument to [block], but as a function returning not a [TestResult] but [Unit].
*/
expect fun testResultMap(block: (() -> Unit) -> Unit, test: () -> TestResult): TestResult
expect fun testResultMap(block: (() -> Unit) -> Unit, test: () -> TestResult): TestResult

/**
* A class inheriting from which allows to check the execution order inside tests.
*
* @see TestBase
*/
open class OrderedExecutionTestBase {
private val actionIndex = atomic(0)
private val finished = atomic(false)

/** Expect the next action to be [index] in order. */
protected fun expect(index: Int) {
val wasIndex = actionIndex.incrementAndGet()
check(index == wasIndex) { "Expecting action index $index but it is actually $wasIndex" }
}

/** Expect this action to be final, with the given [index]. */
protected fun finish(index: Int) {
expect(index)
check(!finished.getAndSet(true)) { "Should call 'finish(...)' at most once" }
}

@AfterTest
fun ensureFinishCalls() {
assertTrue(finished.value || actionIndex.value == 0, "Expected `finish` to be called")
}
}
23 changes: 0 additions & 23 deletions kotlinx-coroutines-test/common/test/RunTestOrderTest.kt

This file was deleted.

2 changes: 1 addition & 1 deletion kotlinx-coroutines-test/common/test/RunTestTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ class RunTestTest {
delay(2000)
}
val deferred = async {
val job = launch(TestCoroutineDispatcher(testScheduler)) {
val job = launch(StandardTestDispatcher(testScheduler)) {
launch {
delay(500)
}
Expand Down
27 changes: 27 additions & 0 deletions kotlinx-coroutines-test/common/test/StandardTestDispatcherTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* 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.*

class StandardTestDispatcherTest: OrderedExecutionTestBase() {

/** Tests that the [StandardTestDispatcher] follows an execution order similar to `runBlocking`. */
@Test
fun testFlowsNotSkippingValues() {
val scope = createTestCoroutineScope(StandardTestDispatcher())
scope.launch {
// https://github.com/Kotlin/kotlinx.coroutines/issues/1626#issuecomment-554632852
val list = flowOf(1).onStart { emit(0) }
.combine(flowOf("A")) { int, str -> "$str$int" }
.toList()
assertEquals(list, listOf("A0", "A1"))
}
scope.cleanupTestCoroutines()
}

}
134 changes: 77 additions & 57 deletions kotlinx-coroutines-test/common/test/TestCoroutineSchedulerTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ class TestCoroutineSchedulerTest {
@Test
fun testContextElement() = runTest {
assertFailsWith<IllegalStateException> {
withContext(TestCoroutineDispatcher()) {
withContext(StandardTestDispatcher()) {
}
}
}
Expand Down Expand Up @@ -44,28 +44,35 @@ class TestCoroutineSchedulerTest {
/** Tests that if [TestCoroutineScheduler.advanceTimeBy] encounters an arithmetic overflow, all the tasks scheduled
* until the moment [Long.MAX_VALUE] get run. */
@Test
fun testAdvanceTimeByEnormousDelays() = runTest {
val initialDelay = 10L
delay(initialDelay)
assertEquals(initialDelay, currentTime)
var enteredInfinity = false
launch {
delay(Long.MAX_VALUE - 1) // delay(Long.MAX_VALUE) does nothing
assertEquals(Long.MAX_VALUE, currentTime)
enteredInfinity = true
}
var enteredNearInfinity = false
launch {
delay(Long.MAX_VALUE - initialDelay - 1)
assertEquals(Long.MAX_VALUE - 1, currentTime)
enteredNearInfinity = true
fun testAdvanceTimeByEnormousDelays() = forTestDispatchers {
assertRunsFast {
with (createTestCoroutineScope(it)) {
launch {
val initialDelay = 10L
delay(initialDelay)
assertEquals(initialDelay, currentTime)
var enteredInfinity = false
launch {
delay(Long.MAX_VALUE - 1) // delay(Long.MAX_VALUE) does nothing
assertEquals(Long.MAX_VALUE, currentTime)
enteredInfinity = true
}
var enteredNearInfinity = false
launch {
delay(Long.MAX_VALUE - initialDelay - 1)
assertEquals(Long.MAX_VALUE - 1, currentTime)
enteredNearInfinity = true
}
testScheduler.advanceTimeBy(Long.MAX_VALUE)
assertFalse(enteredInfinity)
assertTrue(enteredNearInfinity)
assertEquals(Long.MAX_VALUE, currentTime)
testScheduler.runCurrent()
assertTrue(enteredInfinity)
}
testScheduler.advanceUntilIdle()
}
}
testScheduler.advanceTimeBy(Long.MAX_VALUE)
assertFalse(enteredInfinity)
assertTrue(enteredNearInfinity)
assertEquals(Long.MAX_VALUE, currentTime)
testScheduler.runCurrent()
assertTrue(enteredInfinity)
}

/** Tests the basic functionality of [TestCoroutineScheduler.advanceTimeBy]. */
Expand Down Expand Up @@ -124,48 +131,52 @@ class TestCoroutineSchedulerTest {

/** Tests that [TestCoroutineScheduler.runCurrent] will not run new tasks after the current time has advanced. */
@Test
fun testRunCurrentNotDrainingQueue() = assertRunsFast {
val scheduler = TestCoroutineScheduler()
val scope = createTestCoroutineScope(scheduler)
var stage = 1
scope.launch {
delay(SLOW)
launch {
fun testRunCurrentNotDrainingQueue() = forTestDispatchers {
assertRunsFast {
val scheduler = it.scheduler
val scope = createTestCoroutineScope(it)
var stage = 1
scope.launch {
delay(SLOW)
stage = 3
launch {
delay(SLOW)
stage = 3
}
scheduler.advanceTimeBy(SLOW)
stage = 2
}
scheduler.advanceTimeBy(SLOW)
stage = 2
assertEquals(1, stage)
scheduler.runCurrent()
assertEquals(2, stage)
scheduler.runCurrent()
assertEquals(3, stage)
}
scheduler.advanceTimeBy(SLOW)
assertEquals(1, stage)
scheduler.runCurrent()
assertEquals(2, stage)
scheduler.runCurrent()
assertEquals(3, stage)
}

/** Tests that [TestCoroutineScheduler.advanceUntilIdle] doesn't hang when itself running in a scheduler task. */
@Test
fun testNestedAdvanceUntilIdle() = assertRunsFast {
val scheduler = TestCoroutineScheduler()
val scope = createTestCoroutineScope(scheduler)
var executed = false
scope.launch {
launch {
delay(SLOW)
executed = true
fun testNestedAdvanceUntilIdle() = forTestDispatchers {
assertRunsFast {
val scheduler = it.scheduler
val scope = createTestCoroutineScope(it)
var executed = false
scope.launch {
launch {
delay(SLOW)
executed = true
}
scheduler.advanceUntilIdle()
}
scheduler.advanceUntilIdle()
assertTrue(executed)
}
scheduler.advanceUntilIdle()
assertTrue(executed)
}

/** Tests [yield] scheduling tasks for future execution and not executing immediately. */
@Test
fun testYield() {
val scope = createTestCoroutineScope()
fun testYield() = forTestDispatchers {
val scope = createTestCoroutineScope(it)
var stage = 0
scope.launch {
yield()
Expand Down Expand Up @@ -205,8 +216,8 @@ class TestCoroutineSchedulerTest {

/** Tests that timeouts get triggered. */
@Test
fun testSmallTimeouts() {
val scope = createTestCoroutineScope(TestCoroutineDispatcher())
fun testSmallTimeouts() = forTestDispatchers {
val scope = createTestCoroutineScope(it)
scope.checkTimeout(true) {
val half = SLOW / 2
delay(half)
Expand All @@ -216,8 +227,8 @@ class TestCoroutineSchedulerTest {

/** Tests that timeouts don't get triggered if the code finishes in time. */
@Test
fun testLargeTimeouts() {
val scope = createTestCoroutineScope()
fun testLargeTimeouts() = forTestDispatchers {
val scope = createTestCoroutineScope(it)
scope.checkTimeout(false) {
val half = SLOW / 2
delay(half)
Expand All @@ -227,8 +238,8 @@ class TestCoroutineSchedulerTest {

/** Tests that timeouts get triggered if the code fails to finish in time asynchronously. */
@Test
fun testSmallAsynchronousTimeouts() {
val scope = createTestCoroutineScope()
fun testSmallAsynchronousTimeouts() = forTestDispatchers {
val scope = createTestCoroutineScope(it)
val deferred = CompletableDeferred<Unit>()
scope.launch {
val half = SLOW / 2
Expand All @@ -243,8 +254,8 @@ class TestCoroutineSchedulerTest {

/** Tests that timeouts don't get triggered if the code finishes in time, even if it does so asynchronously. */
@Test
fun testLargeAsynchronousTimeouts() {
val scope = createTestCoroutineScope()
fun testLargeAsynchronousTimeouts() = forTestDispatchers {
val scope = createTestCoroutineScope(it)
val deferred = CompletableDeferred<Unit>()
scope.launch {
val half = SLOW / 2
Expand All @@ -256,4 +267,13 @@ class TestCoroutineSchedulerTest {
deferred.await()
}
}

private fun forTestDispatchers(block: (TestDispatcher) -> Unit): Unit =
listOf(TestCoroutineDispatcher(), StandardTestDispatcher(), UnconfinedTestDispatcher()).forEach {
try {
block(it)
} catch (e: Throwable) {
throw RuntimeException("Test failed for dispatcher $it", e)
}
}
}
6 changes: 3 additions & 3 deletions kotlinx-coroutines-test/common/test/TestCoroutineScopeTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ class TestCoroutineScopeTest {
}
// Reuses the scheduler that the dispatcher is linked to.
run {
val dispatcher = TestCoroutineDispatcher()
val dispatcher = StandardTestDispatcher()
val scope = createTestCoroutineScope(dispatcher)
assertSame(dispatcher.scheduler, scope.coroutineContext[TestCoroutineScheduler])
}
Expand All @@ -43,7 +43,7 @@ class TestCoroutineScopeTest {
// Doesn't touch the passed dispatcher and the scheduler if they match.
run {
val scheduler = TestCoroutineScheduler()
val dispatcher = TestCoroutineDispatcher(scheduler)
val dispatcher = StandardTestDispatcher(scheduler)
val scope = createTestCoroutineScope(scheduler + dispatcher)
assertSame(scheduler, scope.coroutineContext[TestCoroutineScheduler])
assertSame(dispatcher, scope.coroutineContext[ContinuationInterceptor])
Expand Down Expand Up @@ -123,7 +123,7 @@ class TestCoroutineScopeTest {
companion object {
internal val invalidContexts = listOf(
Dispatchers.Default, // not a [TestDispatcher]
TestCoroutineDispatcher() + TestCoroutineScheduler(), // the dispatcher is not linked to the scheduler
StandardTestDispatcher() + TestCoroutineScheduler(), // the dispatcher is not linked to the scheduler
CoroutineExceptionHandler { _, _ -> }, // not an `UncaughtExceptionCaptor`
)
}
Expand Down
15 changes: 1 addition & 14 deletions kotlinx-coroutines-test/common/test/TestDispatchersTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,11 @@
*/
package kotlinx.coroutines.test

import kotlinx.atomicfu.*
import kotlinx.coroutines.*
import kotlin.coroutines.*
import kotlin.test.*

class TestDispatchersTest {
private val actionIndex = atomic(0)
private val finished = atomic(false)

private fun expect(index: Int) {
val wasIndex = actionIndex.incrementAndGet()
check(index == wasIndex) { "Expecting action index $index but it is actually $wasIndex" }
}

private fun finish(index: Int) {
expect(index)
check(!finished.getAndSet(true)) { "Should call 'finish(...)' at most once" }
}
class TestDispatchersTest: OrderedExecutionTestBase() {

@BeforeTest
fun setUp() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import kotlinx.coroutines.*
import kotlin.coroutines.*
import kotlin.test.*

@Suppress("DEPRECATION")
class TestBuildersTest {

@Test
Expand Down

0 comments on commit 83d0175

Please sign in to comment.