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
Replace until with eventually and make eventually more configurable #2022
Changes from 1 commit
7598f0f
3b2b274
415f7da
7b51ab7
4ffc0af
01ae80c
b43bc2d
8f9307b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
* text=auto eol=lf | ||
*.bat text=auto eol=crlf |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
package io.kotest.assertions | ||
|
||
typealias SuspendingPredicate<T> = suspend (T) -> Boolean | ||
|
||
typealias SuspendingProducer<T> = suspend () -> T | ||
|
||
interface NondeterministicListener<in T> { | ||
suspend fun onEval(t: T) | ||
|
||
companion object { | ||
val noop = object : NondeterministicListener<Any?> { | ||
override suspend fun onEval(t: Any?) { } | ||
jschneidereit marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
} | ||
} | ||
|
||
fun <T> nondeterministicListener(f: suspend (T) -> Unit) = object : NondeterministicListener<T> { | ||
override suspend fun onEval(t: T) = f(t) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,11 @@ | ||
package io.kotest.assertions.timing | ||
|
||
import io.kotest.assertions.NondeterministicListener | ||
import io.kotest.assertions.SuspendingPredicate | ||
import io.kotest.assertions.SuspendingProducer | ||
import io.kotest.assertions.failure | ||
import io.kotest.assertions.until.Interval | ||
import io.kotest.assertions.until.fixed | ||
import kotlinx.coroutines.delay | ||
import kotlin.reflect.KClass | ||
import kotlin.time.Duration | ||
|
@@ -9,63 +14,116 @@ import kotlin.time.TimeSource | |
import kotlin.time.milliseconds | ||
|
||
@OptIn(ExperimentalTime::class) | ||
suspend fun <T> eventually(duration: Duration, f: suspend () -> T): T = | ||
eventually(duration, 25.milliseconds, Exception::class, f) | ||
data class Eventually<T, E : Throwable> ( | ||
val duration: Duration = Duration.INFINITE, | ||
val interval: Interval = 25.milliseconds.fixed(), | ||
val listener: NondeterministicListener<T> = NondeterministicListener.noop, | ||
val retries: Int = Int.MAX_VALUE, | ||
val exceptionClass: KClass<E>? = null, | ||
) { | ||
init { | ||
require(retries > 0) { "Retries should not be less than one" } | ||
} | ||
|
||
fun withInterval(interval: Interval) = this.copy(interval = interval) | ||
fun withListener(listener: NondeterministicListener<T>) = this.copy(listener = listener) | ||
fun withRetries(retries: Int) = this.copy(retries = retries) | ||
|
||
suspend operator fun invoke( | ||
predicate: SuspendingPredicate<T> = { true }, | ||
f: SuspendingProducer<T>, | ||
): T { | ||
val end = TimeSource.Monotonic.markNow().plus(duration) | ||
var times = 0 | ||
var firstError: Throwable? = null | ||
var lastError: Throwable? = null | ||
while (end.hasNotPassedNow() && times < retries) { | ||
try { | ||
val result = f() | ||
listener.onEval(result) | ||
if (predicate(result)) { | ||
return result | ||
} | ||
} catch (e: Throwable) { | ||
if (AssertionError::class.isInstance(e) || exceptionClass?.isInstance(e) == true) { | ||
if (firstError == null) { | ||
firstError = e | ||
} else { | ||
lastError = e | ||
} | ||
} else { | ||
throw e | ||
} | ||
} | ||
times++ | ||
delay(interval.next(times)) | ||
} | ||
|
||
val message = StringBuilder().apply { | ||
append("Eventually block failed after ${duration}; attempted $times time(s); $interval delay between attempts") | ||
|
||
if (firstError != null) { | ||
appendLine("The first error was caused by: ${firstError.message}") | ||
appendLine(firstError.stackTraceToString()) | ||
} | ||
|
||
if (lastError != null) { | ||
appendLine("The last error was caused by: ${lastError.message}") | ||
appendLine(lastError.stackTraceToString()) | ||
} | ||
} | ||
|
||
throw failure(message.toString()) | ||
} | ||
} | ||
|
||
@OptIn(ExperimentalTime::class) | ||
suspend fun <T> eventually(duration: Duration, poll: Duration, f: suspend () -> T): T = | ||
eventually(duration, poll, Exception::class, f) | ||
suspend fun <T> eventually( | ||
duration: Duration = Duration.INFINITE, | ||
interval: Interval = 25.milliseconds.fixed(), | ||
listener: NondeterministicListener<T> = NondeterministicListener.noop, | ||
retries: Int = Int.MAX_VALUE, | ||
exceptionClass: KClass<Exception> = Exception::class, | ||
predicate: SuspendingPredicate<T> = { true }, | ||
f: SuspendingProducer<T> | ||
): T = Eventually(duration, interval, listener, retries, exceptionClass).invoke(predicate, f) | ||
|
||
@OptIn(ExperimentalTime::class) | ||
suspend fun <T, E : Throwable> eventually( | ||
duration: Duration, | ||
exceptionClass: KClass<E>, | ||
f: suspend () -> T | ||
): T = eventually(duration, 25.milliseconds, exceptionClass, f) | ||
suspend fun <T> eventually(duration: Duration, interval: Interval, f: SuspendingProducer<T>): T = | ||
eventually(duration, interval, f = f) | ||
|
||
@OptIn(ExperimentalTime::class) | ||
suspend fun <T, E : Throwable> eventually( | ||
duration: Duration, | ||
poll: Duration, | ||
exceptionClass: KClass<E>, | ||
f: suspend () -> T | ||
): T { | ||
val end = TimeSource.Monotonic.markNow().plus(duration) | ||
var times = 0 | ||
var firstError: Throwable? = null | ||
var lastError: Throwable? = null | ||
while (end.hasNotPassedNow()) { | ||
try { | ||
return f() | ||
} catch (e: Throwable) { | ||
// we only accept exceptions of type exceptionClass and AssertionError | ||
// if we didn't accept AssertionError then a matcher failure would immediately fail this function | ||
if (!exceptionClass.isInstance(e) && !AssertionError::class.isInstance(e)) | ||
throw e | ||
if (firstError == null) | ||
firstError = e | ||
else | ||
lastError = e | ||
} | ||
times++ | ||
delay(poll.toLongMilliseconds()) | ||
} | ||
suspend fun <T> eventually(duration: Duration, predicate: SuspendingPredicate<T>, f: SuspendingProducer<T>): T = | ||
eventually(duration, predicate = predicate, f = f) | ||
|
||
val sb = StringBuilder() | ||
sb.appendLine("Eventually block failed after ${duration}; attempted $times time(s); $poll delay between attempts") | ||
sb.appendLine() | ||
@OptIn(ExperimentalTime::class) | ||
suspend fun <T, E : Throwable> eventually(duration: Duration, exceptionClass: KClass<E>, f: SuspendingProducer<T>): T = | ||
eventually(duration, exceptionClass = exceptionClass, f = f) | ||
|
||
if (firstError != null) { | ||
sb.appendLine("The first error was caused by: ${firstError.message}") | ||
sb.appendLine(firstError.stackTraceToString()) | ||
} | ||
@OptIn(ExperimentalTime::class) | ||
suspend fun <T> eventually(duration: Duration, interval: Interval, predicate: SuspendingPredicate<T>, f: SuspendingProducer<T>): T = | ||
eventually(duration, interval, predicate = predicate, f = f) | ||
|
||
if (lastError != null) { | ||
sb.appendLine("The last error was caused by: ${lastError.message}") | ||
sb.appendLine(lastError.stackTraceToString()) | ||
} | ||
@OptIn(ExperimentalTime::class) | ||
suspend fun <T, E : Throwable> eventually(duration: Duration, interval: Interval, exceptionClass: KClass<E>, f: SuspendingProducer<T>): T = | ||
eventually(duration, interval, exceptionClass = exceptionClass, f = f) | ||
|
||
@OptIn(ExperimentalTime::class) | ||
@Deprecated("Use eventually with an interval, using Duration based poll is deprecated", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we put an example in the deprecatioin message, and add an extension method to turn a duration into an interval if we don't have one already There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah, the first argument to ReplaceWith is an expression that describes how to replace the call site of the deprecated functions. But I also updated the message to include the fixed, fib, and exp calls to convert a Duration to an Interval 👍 |
||
ReplaceWith("eventually(duration, poll.fixed(), f = f)", "io.kotest.assertions.until.fixed") | ||
) | ||
suspend fun <T> eventually(duration: Duration, poll: Duration, f: SuspendingProducer<T>): T = | ||
eventually(duration, poll.fixed(), f = f) | ||
|
||
@OptIn(ExperimentalTime::class) | ||
@Deprecated("Use eventually with an interval, using Duration based poll is deprecated", | ||
ReplaceWith("eventually(duration, poll.fixed(), predicate = predicate, f = f)", "io.kotest.assertions.until.fixed")) | ||
suspend fun <T> eventually(duration: Duration, poll: Duration, predicate: SuspendingPredicate<T>, f: SuspendingProducer<T>): T = | ||
eventually(duration, poll.fixed(), predicate = predicate, f = f) | ||
|
||
@OptIn(ExperimentalTime::class) | ||
@Deprecated("Use eventually with an interval, using Duration based poll is deprecated", | ||
ReplaceWith("eventually(duration, poll.fixed(), exceptionClass = exceptionClass, f = f)", "io.kotest.assertions.until.fixed")) | ||
suspend fun <T, E : Throwable> eventually(duration: Duration, poll: Duration, exceptionClass: KClass<E>, f: suspend () -> T): T = | ||
eventually(duration, poll.fixed(), exceptionClass = exceptionClass, f = f) | ||
|
||
throw failure( | ||
sb.toString() | ||
) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if this doesn't work because of something related to
buildSrc
🤔There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does it not compile or ... ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can just list on multiple lines too
freeCompilerArgs = freeCompilerArgs + "-Xopt-in=kotlin.RequiresOptIn"
freeCompilerArgs = freeCompilerArgs + "-Xopt-in=kotlin.time.ExperimentalTime"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It doesn't get rid of the warning about
RequiresOptin
and doesn't auto optin toExperimentalTime
unfortunatelyThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can't get it to work on MPP either