Skip to content
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

fix EventuallyPredicate, add a producerless version, and inform user … #2046

Merged
merged 3 commits into from
Feb 9, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we make this work without requiring the name overload ?

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