commonMain.arrow.fx.coroutines.predef-test.kt Maven / Gradle / Ivy
package arrow.fx.coroutines
import arrow.core.Either
import arrow.core.Validated
import arrow.core.ValidatedNel
import arrow.core.identity
import arrow.core.invalid
import arrow.core.invalidNel
import arrow.core.left
import arrow.core.right
import arrow.core.valid
import arrow.core.validNel
import io.kotest.assertions.fail
import io.kotest.matchers.Matcher
import io.kotest.matchers.MatcherResult
import io.kotest.matchers.equalityMatcher
import io.kotest.property.Arb
import io.kotest.property.arbitrary.bind
import io.kotest.property.arbitrary.char
import io.kotest.property.arbitrary.choice
import io.kotest.property.arbitrary.choose
import io.kotest.property.arbitrary.constant
import io.kotest.property.arbitrary.int
import io.kotest.property.arbitrary.list
import io.kotest.property.arbitrary.long
import io.kotest.property.arbitrary.map
import io.kotest.property.arbitrary.string
import kotlin.coroutines.Continuation
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.intrinsics.COROUTINE_SUSPENDED
import kotlin.coroutines.intrinsics.intercepted
import kotlin.coroutines.intrinsics.suspendCoroutineUninterceptedOrReturn
import kotlin.coroutines.resume
import kotlin.coroutines.startCoroutine
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.buffer
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.emptyFlow
public data class SideEffect(var counter: Int = 0) {
public fun increment() {
counter++
}
}
public fun Arb.Companion.flow(arbA: Arb): Arb> =
Arb.choose(
10 to Arb.list(arbA).map { it.asFlow() },
10 to Arb.list(arbA).map { channelFlow { it.forEach { send(it) } }.buffer(Channel.RENDEZVOUS) },
1 to Arb.constant(emptyFlow()),
)
public fun Arb.Companion.throwable(): Arb =
Arb.string().map(::RuntimeException)
public fun Arb.Companion.either(left: Arb, right: Arb): Arb> {
val failure: Arb> = left.map { l -> l.left() }
val success: Arb> = right.map { r -> r.right() }
return Arb.choice(failure, success)
}
public fun Arb.Companion.validated(left: Arb, right: Arb): Arb> {
val failure: Arb> = left.map { l -> l.invalid() }
val success: Arb> = right.map { r -> r.valid() }
return Arb.choice(failure, success)
}
public fun Arb.Companion.validatedNel(left: Arb, right: Arb): Arb> {
val failure: Arb> = left.map { l -> l.invalidNel() }
val success: Arb> = right.map { r -> r.validNel() }
return Arb.choice(failure, success)
}
public fun Arb.Companion.intRange(min: Int = Int.MIN_VALUE, max: Int = Int.MAX_VALUE): Arb =
Arb.bind(Arb.int(min, max), Arb.int(min, max)) { a, b ->
if (a < b) a..b else b..a
}
public fun Arb.Companion.longRange(min: Long = Long.MIN_VALUE, max: Long = Long.MAX_VALUE): Arb =
Arb.bind(Arb.long(min, max), Arb.long(min, max)) { a, b ->
if (a < b) a..b else b..a
}
public fun Arb.Companion.charRange(): Arb =
Arb.bind(Arb.char(), Arb.char()) { a, b ->
if (a < b) a..b else b..a
}
public fun Arb.Companion.function(arb: Arb): Arb<() -> O> =
arb.map { { it } }
public fun Arb.Companion.unit(): Arb =
Arb.constant(Unit)
public fun Arb.Companion.functionAToB(arb: Arb): Arb<(A) -> B> =
arb.map { b: B -> { _: A -> b } }
public fun Arb.Companion.nullable(arb: Arb): Arb =
Arb.Companion.choice(arb, arb.map { null })
/** Useful for testing success & error scenarios with an `Either` generator **/
public fun Either.rethrow(): A =
fold({ throw it }, ::identity)
public fun Result.toEither(): Either =
fold({ a -> Either.Right(a) }, { e -> Either.Left(e) })
public suspend fun Throwable.suspend(): Nothing =
suspendCoroutineUninterceptedOrReturn { cont ->
suspend { throw this }.startCoroutine(
Continuation(Dispatchers.Default) {
cont.intercepted().resumeWith(it)
}
)
COROUTINE_SUSPENDED
}
public suspend fun A.suspend(): A =
suspendCoroutineUninterceptedOrReturn { cont ->
suspend { this }.startCoroutine(
Continuation(Dispatchers.Default) {
cont.intercepted().resumeWith(it)
}
)
COROUTINE_SUSPENDED
}
public fun A.suspended(): suspend () -> A =
suspend { suspend() }
/**
* Example usage:
* ```kotlin
* import arrow.fx.coroutines.assertThrowable
*
* fun main() {
* val exception = assertThrowable {
* throw IllegalArgumentException("Talk to a duck")
* }
* require("Talk to a duck" == exception.message)
* }
* ```
*
* @see Assertions.assertThrows
*/
public inline fun assertThrowable(executable: () -> A): Throwable {
val a = try {
executable.invoke()
} catch (e: Throwable) {
e
}
return if (a is Throwable) a else fail("Expected an exception but found: $a")
}
public suspend fun CoroutineContext.shift(): Unit =
suspendCoroutineUninterceptedOrReturn { cont ->
suspend { this }.startCoroutine(
Continuation(this) {
cont.resume(Unit)
}
)
COROUTINE_SUSPENDED
}
public fun leftException(e: Throwable): Matcher> =
object : Matcher> {
override fun test(value: Either): MatcherResult =
when (value) {
is Either.Left -> when {
value.value::class != e::class -> MatcherResult(
false,
"Expected exception of type ${e::class} but found ${value.value::class}",
"Should not be exception of type ${e::class}"
)
value.value.message != e.message -> MatcherResult(
false,
"Expected exception with message ${e.message} but found ${value.value.message}",
"Should not be exception with message ${e.message}"
)
else -> MatcherResult(
true,
"Expected exception of type ${e::class} and found ${value.value::class}",
"Expected exception of type ${e::class} and found ${value.value::class}"
)
}
is Either.Right -> MatcherResult(
false,
"Expected Either.Left with exception of type ${e::class} and found Right with ${value.value}",
"Should not be Either.Left with exception"
)
}
}
public fun either(e: Either): Matcher> =
object : Matcher> {
override fun test(value: Either): MatcherResult =
when (value) {
is Either.Left -> when {
value.value::class != (e.swap().orNull() ?: Int)::class -> MatcherResult(
false,
"Expected $e but found $value",
"Should not be $e"
)
value.value.message != (e.swap().orNull()?.message ?: -1) -> MatcherResult(
false,
"Expected $e but found $value",
"Should not be $e"
)
else -> MatcherResult(
true,
"Expected exception of type ${e::class} and found ${value.value::class}",
"Expected exception of type ${e::class} and found ${value.value::class}"
)
}
is Either.Right -> equalityMatcher(e).test(value)
}
}
public suspend fun awaitExitCase(send: Channel, exit: CompletableDeferred): A =
guaranteeCase({
send.receive()
awaitCancellation()
}) { ex -> exit.complete(ex) }
public suspend fun awaitExitCase(start: CompletableDeferred, exit: CompletableDeferred): A =
guaranteeCase({
start.complete(Unit)
awaitCancellation()
}) { ex -> exit.complete(ex) }