Skip to content

Commit

Permalink
test: add withTimeoutSafe and delaySafe as a safe replacements fo…
Browse files Browse the repository at this point in the history
  • Loading branch information
amal committed Feb 27, 2023
1 parent 5f389a1 commit 8367b2f
Show file tree
Hide file tree
Showing 10 changed files with 269 additions and 33 deletions.
26 changes: 26 additions & 0 deletions fluxo-core/src/commonTest/kotlin/kt/fluxo/test/DelaySafe.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package kt.fluxo.test

import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.withContext

/**
* Use as a safe replacement for [delay] when used inside a [runTest]/[TestScope].
*
* Delays coroutine for a given time without blocking a thread and resumes it after a specified time.
*
* [Documentation on delay-skipping in tests](https://github.com/Kotlin/kotlinx.coroutines/blob/81e17dd/kotlinx-coroutines-test/README.md#delay-skipping)
*
* @param timeMillis timeout time in milliseconds.
*
* @see delay
* @see withTimeoutSafe
*/
@Suppress("MaxLineLength")
suspend fun delaySafe(timeMillis: Long) {
withContext(Dispatchers.Default) {
delay(timeMillis)
}
}
20 changes: 7 additions & 13 deletions fluxo-core/src/commonTest/kotlin/kt/fluxo/test/TestFlowObserver.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,8 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeout


/**
Expand Down Expand Up @@ -45,18 +42,15 @@ class TestFlowObserver<T>(flow: Flow<T>) {
* @param throwTimeout Throw if timed out (by default)
*/
suspend fun awaitFor(timeout: Long = 5000L, throwTimeout: Boolean = true, condition: TestFlowObserver<T>.() -> Boolean) {
// https://github.com/Kotlin/kotlinx.coroutines/blob/master/kotlinx-coroutines-test/README.md#using-withtimeout-inside-runtest
withContext(Dispatchers.Default) {
try {
withTimeout(timeout) {
@Suppress("UNUSED_EXPRESSION")
while (!condition()) {
delay(AWAIT_TIMEOUT_MS)
}
try {
withTimeoutSafe(timeout) {
@Suppress("UNUSED_EXPRESSION")
while (!condition()) {
delaySafe(AWAIT_TIMEOUT_MS)
}
} catch (e: TimeoutCancellationException) {
if (throwTimeout) throw e
}
} catch (e: TimeoutCancellationException) {
if (throwTimeout) throw e
}
}

Expand Down
42 changes: 42 additions & 0 deletions fluxo-core/src/commonTest/kotlin/kt/fluxo/test/WithTimeoutSafe.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package kt.fluxo.test

import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeout
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract

/**
* Use as a safe replacement for [withTimeout] when used inside a [runTest]/[TestScope].
*
* Runs a given suspending [block] of code inside a coroutine with a specified [timeout][timeMillis] and throws
* a [TimeoutCancellationException] if the timeout was exceeded.
*
* [Issue with the problem](https://github.com/Kotlin/kotlinx.coroutines/issues/3588)
*
* [Documented as intended behavior](https://github.com/Kotlin/kotlinx.coroutines/blob/master/kotlinx-coroutines-test/README.md#using-withtimeout-inside-runtest)
*
* @param timeMillis timeout time in milliseconds.
*
* @see withTimeout
* @see delaySafe
*/
@Suppress("MaxLineLength")
suspend inline fun <T> withTimeoutSafe(timeMillis: Long, crossinline block: suspend CoroutineScope.() -> T): T {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
val context = currentCoroutineContext()
return withContext(Dispatchers.Default) {
withTimeout(timeMillis) {
withContext(context) {
block()
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,20 @@ package kt.fluxo.tests

import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.isActive
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.withContext
import kotlinx.coroutines.yield
import kt.fluxo.core.FluxoClosedException
import kt.fluxo.core.closeAndWait
import kt.fluxo.core.container
import kt.fluxo.core.updateState
import kt.fluxo.test.delaySafe
import kt.fluxo.test.getValue
import kt.fluxo.test.runUnitTest
import kt.fluxo.test.setValue
Expand Down Expand Up @@ -116,9 +114,7 @@ class ContainerExceptionHandlerTest {
flow {
while (true) {
emit(Unit)
withContext(Dispatchers.Default) {
delay(1000)
}
delaySafe(1000)
}
}.collect()
} catch (ce: CancellationException) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
package kt.fluxo.tests

import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.runTest
import kt.fluxo.core.Container
import kt.fluxo.core.container
import kt.fluxo.core.updateState
import kt.fluxo.test.CoroutineScopeAwareTest
import kt.fluxo.test.delaySafe
import kt.fluxo.test.test
import kotlin.random.Random
import kotlin.test.Test
Expand All @@ -24,7 +24,7 @@ internal class ContainerThreadingTest : CoroutineScopeAwareTest() {
val newState = Random.nextInt()

container.send {
delay(Long.MAX_VALUE)
delaySafe(Long.MAX_VALUE)
}
container.send {
updateState { newState }
Expand Down Expand Up @@ -104,7 +104,7 @@ internal class ContainerThreadingTest : CoroutineScopeAwareTest() {

private fun Container<TestState, Nothing>.one(delay: Boolean = false) = send {
if (delay) {
delay(Random.nextLong(20))
delaySafe(Random.nextLong(20))
}
updateState {
it.copy(ids = value.ids + 1)
Expand All @@ -113,7 +113,7 @@ internal class ContainerThreadingTest : CoroutineScopeAwareTest() {

private fun Container<TestState, Nothing>.two(delay: Boolean = false) = send {
if (delay) {
delay(Random.nextLong(20))
delaySafe(Random.nextLong(20))
}
updateState {
it.copy(ids = value.ids + 2)
Expand All @@ -122,7 +122,7 @@ internal class ContainerThreadingTest : CoroutineScopeAwareTest() {

private fun Container<TestState, Nothing>.three(delay: Boolean = false) = send {
if (delay) {
delay(Random.nextLong(20))
delaySafe(Random.nextLong(20))
}
updateState {
it.copy(ids = value.ids + 3)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers.Unconfined
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.first
Expand All @@ -30,6 +29,7 @@ import kt.fluxo.test.IgnoreNativeAndJs
import kt.fluxo.test.KMM_PLATFORM
import kt.fluxo.test.Platform
import kt.fluxo.test.TestLoggingStoreFactory
import kt.fluxo.test.delaySafe
import kt.fluxo.test.runUnitTest
import kotlin.random.Random
import kotlin.test.Ignore
Expand Down Expand Up @@ -80,7 +80,7 @@ internal class InputStrategyTest : CoroutineScopeAwareTest() {
finishLock.unlock()
}
} else if (intent == lastValue) {
if (generic) delay(timeMillis = 25)
if (generic) delaySafe(timeMillis = 25)
finishLock.unlock()
}
},
Expand All @@ -95,7 +95,7 @@ internal class InputStrategyTest : CoroutineScopeAwareTest() {
if ((generic || parallel) && (nonOrderedEqual || equal)) {
// TODO: Store.awaitIdle()
while (results.size < NUMBER_OF_ITEMS) {
delay(timeMillis = 10)
delaySafe(timeMillis = 10)
}
}
store.closeAndWait()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import kt.fluxo.core.dsl.accept
import kt.fluxo.core.intent
import kt.fluxo.core.store
import kt.fluxo.core.updateState
import kt.fluxo.test.delaySafe
import kt.fluxo.test.runUnitTest
import kotlin.test.Ignore
import kotlin.test.Test
Expand Down Expand Up @@ -135,9 +136,7 @@ internal class SideJobTest {
store.intent {
sideJob { wasRestarted ->
assertFalse(wasRestarted)
withContext(Dispatchers.Default) {
delay(timeMillis = 1_000)
}
delaySafe(timeMillis = 1_000)
updateState { "a" }
}
sideJob { wasRestarted ->
Expand Down
Loading

0 comments on commit 8367b2f

Please sign in to comment.