Skip to content

Commit

Permalink
Updated eventually config to have one type parameter; removed result … (
Browse files Browse the repository at this point in the history
#2024)

* Updated eventually config to have one type parameter; removed result into state for cleaner fun interface; setup opt in at the gradle level

* Updated eventually config to have one type parameter; removed result into state for cleaner fun interface; setup opt in at the gradle level

* Updated eventually config to have one type parameter; removed result into state for cleaner fun interface; setup opt in at the gradle level
  • Loading branch information
sksamuel committed Jan 31, 2021
1 parent a1c230f commit beb6105
Show file tree
Hide file tree
Showing 13 changed files with 72 additions and 86 deletions.
13 changes: 5 additions & 8 deletions kotest-assertions/kotest-assertions-core/build.gradle.kts
Expand Up @@ -38,14 +38,6 @@ kotlin {
iosArm32()
}

targets.all {
compilations.all {
kotlinOptions {
freeCompilerArgs = freeCompilerArgs + listOf("-Xopt-in=kotlin.RequiresOptIn", "-Xopt-in=kotlin.time.ExperimentalTime")
}
}
}

sourceSets {

val commonMain by getting {
Expand Down Expand Up @@ -75,6 +67,11 @@ kotlin {
implementation(Libs.Apache.commonslang)
}
}

all {
languageSettings.useExperimentalAnnotation("kotlin.time.ExperimentalTime")
languageSettings.useExperimentalAnnotation("kotlin.experimental.ExperimentalTypeInference")
}
}
}

Expand Down
Expand Up @@ -11,7 +11,6 @@ import kotlin.time.hours
import kotlin.time.milliseconds
import kotlin.time.seconds

@OptIn(ExperimentalTime::class)
class ContinuallyTest : WordSpec() {

init {
Expand Down
Expand Up @@ -20,7 +20,6 @@ import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import kotlin.time.*

@OptIn(ExperimentalTime::class)
class EventuallyTest : WordSpec() {

init {
Expand Down Expand Up @@ -167,8 +166,12 @@ class EventuallyTest : WordSpec() {
"eventually with T predicate, interval, and listener" {
var t = ""
val latch = CountDownLatch(5)
val result = eventually(5.seconds, 250.milliseconds.fixed(),
listener = { _, _ -> latch.countDown() }, predicate = { t == "xxxxxxxxxxx" }) {
val result = eventually(
5.seconds,
250.milliseconds.fixed(),
predicate = { t == "xxxxxxxxxxx" },
listener = { _ -> latch.countDown() },
) {
t += "x"
t
}
Expand All @@ -187,8 +190,12 @@ class EventuallyTest : WordSpec() {
"support fibonacci intervals" {
var t = ""
val latch = CountDownLatch(5)
val result = eventually(10.seconds, 200.milliseconds.fibonacci(),
listener = { _, _ -> latch.countDown() }, predicate = { t == "xxxxxx" }) {
val result = eventually(
duration = 10.seconds,
interval = 200.milliseconds.fibonacci(),
predicate = { t == "xxxxxx" },
listener = { latch.countDown() },
) {
t += "x"
t
}
Expand All @@ -197,8 +204,10 @@ class EventuallyTest : WordSpec() {
}

"eventually has a shareable configuration" {
val slow = EventuallyConfig<Int, Throwable>(duration = 5.seconds)
val fast = slow.copy(retries = 1)
val slow = EventuallyConfig<Int>(duration = 5.seconds)

var i = 0
val fast = slow.copy(retries = 1, predicate = { i == 1 })

assertSoftly {
slow.retries shouldBe Int.MAX_VALUE
Expand All @@ -211,8 +220,7 @@ class EventuallyTest : WordSpec() {
5
}

var i = 0
eventually(fast, predicate = { i == 1 }) {
eventually(fast) {
i++
}

Expand Down
@@ -1,7 +1,6 @@
package com.sksamuel.kotest.assertions.until

import io.kotest.assertions.throwables.shouldThrow
import io.kotest.assertions.timing.eventually
import io.kotest.assertions.until.fibonacci
import io.kotest.assertions.until.fixed
import io.kotest.assertions.until.until
Expand All @@ -10,11 +9,9 @@ import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.shouldBe
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import kotlin.time.ExperimentalTime
import kotlin.time.milliseconds
import kotlin.time.seconds

@OptIn(ExperimentalTime::class)
class UntilTest : FunSpec({

test("until with boolean predicate") {
Expand Down
5 changes: 5 additions & 0 deletions kotest-assertions/kotest-assertions-shared/build.gradle.kts
Expand Up @@ -98,6 +98,11 @@ kotlin {
val tvosMain by getting {
dependsOn(desktopMain)
}

all {
languageSettings.useExperimentalAnnotation("kotlin.time.ExperimentalTime")
languageSettings.useExperimentalAnnotation("kotlin.experimental.ExperimentalTypeInference")
}
}
}

Expand Down
Expand Up @@ -4,28 +4,26 @@ import io.kotest.mpp.bestName
import kotlin.reflect.KClass
import kotlin.time.Duration
import kotlin.time.DurationUnit
import kotlin.time.ExperimentalTime
import kotlin.time.TimeSource
import kotlin.time.seconds
import kotlinx.coroutines.delay

/**
* Retry [f] until it's a success or [maxRetry]/[timeout] is reached
*
*
* This will treat any Exception as a failure, along with [AssertionError].
*
*
* Retry delay might increase exponentially if you choose a [multiplier] value. For example, if you want to configure
* 5 [maxRetry], with an initial [delay] of 1s between requests, the delay between requests will increase when you
* choose 2 as your [multiplier]:
* 1 - Failed (wait 1s before retrying)
* 2 - Failed (wait 2s before retrying)
* 3 - Failed (wait 4s before retrying)
* ..
*
*
* If either timeout or max retries is reached, the execution will be aborted and an exception will be thrown.
*
*
* */
@OptIn(ExperimentalTime::class)
suspend fun <T> retry(
maxRetry: Int,
timeout: Duration,
Expand All @@ -51,7 +49,6 @@ suspend fun <T> retry(
* If either timeout or max retries is reached, the execution will be aborted and an exception will be thrown.
*
* */
@OptIn(ExperimentalTime::class)
suspend fun <T, E : Throwable> retry(
maxRetry: Int,
timeout: Duration,
Expand Down
Expand Up @@ -7,7 +7,6 @@ import io.kotest.assertions.until.fixed
import kotlinx.coroutines.delay
import kotlin.time.*

@OptIn(ExperimentalTime::class)
data class ContinuallyState(val start: TimeMark, val end: TimeMark, val times: Int)

fun interface ContinuallyListener<in T> {
Expand All @@ -18,7 +17,6 @@ fun interface ContinuallyListener<in T> {
}
}

@OptIn(ExperimentalTime::class)
data class Continually<T> (
val duration: Duration = Duration.INFINITE,
val interval: Interval = 25.milliseconds.fixed(),
Expand Down Expand Up @@ -50,13 +48,11 @@ data class Continually<T> (
}
}

@OptIn(ExperimentalTime::class)
@Deprecated("Use continually with an interval, using Duration based poll is deprecated",
ReplaceWith("continually(duration, poll.fixed(), f = f)", "io.kotest.assertions.until.fixed")
)
suspend fun <T> continually(duration: Duration, poll: Duration, f: suspend () -> T) =
continually(duration, poll.fixed(), f = f)

@OptIn(ExperimentalTime::class)
suspend fun <T> continually(duration: Duration, interval: Interval = 10.milliseconds.fixed(), f: suspend () -> T) =
Continually<T>(duration, interval).invoke(f)
Expand Up @@ -12,34 +12,40 @@ import kotlin.time.*
/**
* Runs a function until it doesn't throw as long as the specified duration hasn't passed
*/
@OptIn(ExperimentalTime::class)
suspend fun <T> eventually(duration: Duration, f: SuspendingProducer<T>): T =
eventually(EventuallyConfig(duration = duration, exceptionClass = Throwable::class), f = f)

/**
* Runs a function until it doesn't throw and the result satisfies the predicate as long as the specified duration hasn't passed
*/
@OptIn(ExperimentalTime::class)
suspend fun <T> eventually(duration: Duration, predicate: SuspendingPredicate<T>, f: SuspendingProducer<T>): T =
eventually(EventuallyConfig(duration = duration, exceptionClass = Throwable::class), predicate, f)
suspend fun <T : Any> eventually(
duration: Duration,
interval: Interval,
f: SuspendingProducer<T>
): T = eventually(EventuallyConfig(duration, interval), f)

suspend fun <T> eventually(
duration: Duration,
interval: Interval,
listener: EventuallyListener<T>,
f: SuspendingProducer<T>
): T = eventually(EventuallyConfig(duration = duration, interval, listener = listener), f)


@OptIn(ExperimentalTime::class)
@Deprecated("""
@Deprecated(
"""
Use eventually with an interval, using Duration based poll is deprecated.
To convert an existing duration to an interval you can Duration.fixed(), Duration.exponential(), or Duration.fibonacci().
""",
ReplaceWith(
"eventually(duration, interval = poll.fixed(), f = f)",
"io.kotest.assertions.until.fixed"
))
)
)
suspend fun <T> eventually(duration: Duration, poll: Duration, f: SuspendingProducer<T>): T =
eventually(EventuallyConfig(duration = duration, interval = poll.fixed(), exceptionClass = Throwable::class), f = f)

/**
* Runs a function until it doesn't throw the specified exception as long as the specified duration hasn't passed
*/
@OptIn(ExperimentalTime::class)
suspend fun <T, E : Throwable> eventually(duration: Duration, exceptionClass: KClass<E>, f: SuspendingProducer<T>): T =
suspend fun <T> eventually(duration: Duration, exceptionClass: KClass<out Throwable>, f: SuspendingProducer<T>): T =
eventually(EventuallyConfig(duration = duration, exceptionClass = exceptionClass), f = f)

/**
Expand All @@ -52,25 +58,22 @@ suspend fun <T, E : Throwable> eventually(duration: Duration, exceptionClass: KC
* [eventually] will delay the specified [interval] between iterations, defaults to 25 [milliseconds]
* [eventually] will pass the resulting value and state (see [EventuallyState]) into the optional [listener]
*/
@OptIn(ExperimentalTime::class)
suspend fun <T> eventually(
duration: Duration = Duration.INFINITE,
interval: Interval = 25.milliseconds.fixed(),
listener: EventuallyListener<T> = EventuallyListener.noop,
retries: Int = Int.MAX_VALUE,
exceptionClass: KClass<Throwable>? = Throwable::class,
predicate: SuspendingPredicate<T> = { true },
listener: EventuallyListener<T> = EventuallyListener { },
retries: Int = Int.MAX_VALUE,
exceptionClass: KClass<out Throwable>? = null,
f: SuspendingProducer<T>
): T = eventually(EventuallyConfig(duration, interval, listener, retries, exceptionClass), predicate, f)
): T = eventually(EventuallyConfig(duration, interval, predicate, listener, retries, exceptionClass), f)

/**
* Runs a function until it doesn't throw and the result satisfies the predicate as long as the specified duration hasn't passed
* and uses [EventuallyConfig] to control the duration, interval, listener, retries, and exceptionClass.
*/
@OptIn(ExperimentalTime::class)
suspend fun <T, E : Throwable> eventually(
config: EventuallyConfig<T, E>,
predicate: SuspendingPredicate<T> = { true },
suspend fun <T> eventually(
config: EventuallyConfig<T>,
f: SuspendingProducer<T>,
): T {
val start = TimeSource.Monotonic.markNow()
Expand All @@ -82,8 +85,8 @@ suspend fun <T, E : Throwable> eventually(
while (end.hasNotPassedNow() && times < config.retries) {
try {
val result = f()
config.listener.onEval(result, EventuallyState(start, end, times, firstError, lastError))
if (predicate(result)) {
config.listener.onEval(EventuallyState(result, start, end, times, firstError, lastError))
if (config.predicate(result)) {
return result
}
} catch (e: Throwable) {
Expand Down Expand Up @@ -118,28 +121,28 @@ suspend fun <T, E : Throwable> eventually(
throw failure(message.toString())
}

@OptIn(ExperimentalTime::class)
data class EventuallyConfig<T, E : Throwable> (
data class EventuallyConfig<T>(
val duration: Duration = Duration.INFINITE,
val interval: Interval = 25.milliseconds.fixed(),
val listener: EventuallyListener<T> = EventuallyListener.noop,
val predicate: SuspendingPredicate<T> = { true },
val listener: EventuallyListener<T> = EventuallyListener {},
val retries: Int = Int.MAX_VALUE,
val exceptionClass: KClass<E>? = null,
val exceptionClass: KClass<out Throwable>? = null,
) {
init {
require(retries > 0) { "Retries should not be less than one" }
}
}

@OptIn(ExperimentalTime::class)
data class EventuallyState (
val start: TimeMark, val end: TimeMark, val times: Int, val firstError: Throwable?, val lastError: Throwable?,
data class EventuallyState<T>(
val result: T,
val start: TimeMark,
val end: TimeMark,
val times: Int,
val firstError: Throwable?,
val lastError: Throwable?,
)

fun interface EventuallyListener<in T> {
fun onEval(t: T, state: EventuallyState)

companion object {
val noop = EventuallyListener<Any?> { _, _ -> }
}
fun interface EventuallyListener<T> {
fun onEval(state: EventuallyState<T>)
}
Expand Up @@ -2,10 +2,8 @@ package io.kotest.assertions.until

import kotlin.math.pow
import kotlin.time.Duration
import kotlin.time.ExperimentalTime
import kotlin.time.milliseconds

@OptIn(ExperimentalTime::class)
class ExponentialInterval(private val base: Duration) : Interval {
override fun toString() = "ExponentialInterval(${::base.name}=$base)"

Expand All @@ -15,5 +13,4 @@ class ExponentialInterval(private val base: Duration) : Interval {
}
}

@OptIn(ExperimentalTime::class)
fun Duration.exponential() = ExponentialInterval(this)
@@ -1,7 +1,6 @@
package io.kotest.assertions.until

import kotlin.time.Duration
import kotlin.time.ExperimentalTime
import kotlin.time.milliseconds

/**
Expand All @@ -14,7 +13,6 @@ import kotlin.time.milliseconds
* @param offset Added to the count, so if the offset is 4, then the first value will be the 4th fib number.
* @param base The duration that is multiplied by the fibonacci value
*/
@OptIn(ExperimentalTime::class)
class FibonacciInterval(private val base: Duration, private val offset: Int) : Interval {

init {
Expand All @@ -30,7 +28,6 @@ class FibonacciInterval(private val base: Duration, private val offset: Int) : I
}
}

@OptIn(ExperimentalTime::class)
fun Duration.fibonacci() = FibonacciInterval(this, 0)

fun fibonacci(n: Int): Int {
Expand Down
@@ -1,12 +1,10 @@
package io.kotest.assertions.until

import kotlin.time.Duration
import kotlin.time.ExperimentalTime

/**
* Generates a fixed (linear) poll interval based on the supplied duration
*/
@OptIn(ExperimentalTime::class)
class FixedInterval(private val duration: Duration) : Interval {
override fun toString() = "FixedInterval(${::duration.name}=$duration)"

Expand All @@ -15,5 +13,4 @@ class FixedInterval(private val duration: Duration) : Interval {
}
}

@OptIn(ExperimentalTime::class)
fun Duration.fixed() = FixedInterval(this)
fun Duration.fixed(): FixedInterval = FixedInterval(this)

0 comments on commit beb6105

Please sign in to comment.