Skip to content

Commit

Permalink
fix EventuallyPredicate, add a producerless version, and inform user …
Browse files Browse the repository at this point in the history
…about predicate failures for #2044
  • Loading branch information
jschneidereit committed Feb 5, 2021
1 parent 742d8f2 commit 8d40345
Show file tree
Hide file tree
Showing 2 changed files with 44 additions and 7 deletions.
Expand Up @@ -7,6 +7,7 @@ import io.kotest.assertions.fail
import io.kotest.assertions.throwables.shouldThrow
import io.kotest.assertions.timing.EventuallyConfig
import io.kotest.assertions.timing.eventually
import io.kotest.assertions.timing.eventuallyPredicate
import io.kotest.assertions.until.fibonacci
import io.kotest.assertions.until.fixed
import io.kotest.core.spec.style.WordSpec
Expand All @@ -20,7 +21,10 @@ import java.io.FileNotFoundException
import java.io.IOException
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import kotlin.time.*
import kotlin.time.TimeSource
import kotlin.time.days
import kotlin.time.milliseconds
import kotlin.time.seconds

class EventuallyTest : WordSpec() {

Expand Down Expand Up @@ -113,6 +117,15 @@ class EventuallyTest : WordSpec() {
message.shouldContain("The first error was caused by: first")
message.shouldContain("The last error was caused by: last")
}
"display the number of times the predicate failed" {
var count = 0
val maximumRetries = 3
val message = shouldThrow<AssertionError> {
eventuallyPredicate(500.milliseconds, retries = maximumRetries) { count++ >= maximumRetries + 1 }
}.message
message.shouldContain("Eventually block failed after 500ms; attempted \\d+ time\\(s\\); FixedInterval\\(duration=25.0ms\\) delay between attempts".toRegex())
message.shouldContain("The provided predicate failed $maximumRetries times")
}
"allow suspendable functions" {
eventually(100.milliseconds) {
delay(25)
Expand Down Expand Up @@ -181,6 +194,13 @@ class EventuallyTest : WordSpec() {
result shouldBe "xxxxxxxxxxx"
}

"eventually with a more succinct predicate" {
var i = 0
eventuallyPredicate(2.seconds) {
i++ == 2
}
}

"fail tests that fail a predicate" {
shouldThrow<AssertionError> {
eventually(1.seconds, predicate = { it == 2 }) {
Expand Down
Expand Up @@ -57,6 +57,18 @@ suspend fun <T> eventually(duration: Duration, poll: Duration, f: SuspendingProd
suspend fun <T> eventually(duration: Duration, exceptionClass: KClass<out Throwable>, f: SuspendingProducer<T>): T =
eventually(EventuallyConfig(duration = duration, exceptionClass = exceptionClass), f = f)

/**
* Runs a predicate until the predicate returns true as long as the specified duration hasn't passed
*/
suspend fun eventuallyPredicate(
duration: Duration,
interval: Interval = 25.milliseconds.fixed(),
retries: Int = Int.MAX_VALUE,
predicate: EventuallyPredicate<Unit>
) {
eventually(EventuallyConfig(duration, interval, retries), predicate, f = { })
}

/**
* Runs a function until the following constraints are eventually met:
* the optional [predicate] must be satisfied, defaults to true
Expand All @@ -70,7 +82,7 @@ suspend fun <T> eventually(duration: Duration, exceptionClass: KClass<out Throwa
suspend fun <T> eventually(
duration: Duration = Duration.INFINITE,
interval: Interval = 25.milliseconds.fixed(),
predicate: EventuallyPredicate<T> = EventuallyPredicate { true },
predicate: EventuallyPredicate<T> = { true },
listener: EventuallyListener<T> = EventuallyListener { },
retries: Int = Int.MAX_VALUE,
exceptionClass: KClass<out Throwable>? = null,
Expand All @@ -83,7 +95,7 @@ suspend fun <T> eventually(
*/
suspend fun <T> eventually(
config: EventuallyConfig,
predicate: EventuallyPredicate<T> = EventuallyPredicate { true },
predicate: EventuallyPredicate<T> = { true },
listener: EventuallyListener<T> = EventuallyListener { },
f: SuspendingProducer<T>,
): T {
Expand All @@ -92,13 +104,16 @@ suspend fun <T> eventually(
var times = 0
var firstError: Throwable? = null
var lastError: Throwable? = null
var predicateFailedTimes = 0

while (end.hasNotPassedNow() && times < config.retries) {
try {
val result = f()
listener.onEval(EventuallyState(result, start, end, times, firstError, lastError))
if (predicate.test(result)) {
if (predicate(result)) {
return result
} else {
predicateFailedTimes++
}
} catch (e: Throwable) {
if (AssertionError::class.isInstance(e) || config.exceptionClass?.isInstance(e) == true) {
Expand All @@ -118,6 +133,10 @@ suspend fun <T> eventually(
val message = StringBuilder().apply {
appendLine("Eventually block failed after ${config.duration}; attempted $times time(s); ${config.interval} delay between attempts")

if (predicateFailedTimes > 0) {
appendLine("The provided predicate failed $predicateFailedTimes times")
}

if (firstError != null) {
appendLine("The first error was caused by: ${firstError.message}")
appendLine(firstError.stackTraceToString())
Expand Down Expand Up @@ -153,9 +172,7 @@ data class EventuallyState<T>(
val thisError: Throwable?,
)

fun interface EventuallyPredicate<T> {
fun test(result: T): Boolean
}
typealias EventuallyPredicate<T> = (T) -> Boolean

fun interface EventuallyListener<T> {
fun onEval(state: EventuallyState<T>)
Expand Down

0 comments on commit 8d40345

Please sign in to comment.