Skip to content

Commit

Permalink
Remove TimeMarkCompat / MonotonicTimeSourceCompat / timeInMillis (#4105)
Browse files Browse the repository at this point in the history
TimeMarkCompat was only necessary because of a breaking change in Kotlin
1.7 - but that was 2 years ago and Kotest now requires more recent
versions, so TimeMarkCompat can now be removed. [Slack
discussion](https://slack-chats.kotlinlang.org/t/18926932/what-s-kotest-s-minimum-supported-kotlin-version-i-m-trying-#e406321c-881f-465f-9716-35925a980daf).

`timeInMillis()` is only used for measuring times, so it can be replaced
with stdlib `measureTime {}`/`measureTimedValue {}`
  • Loading branch information
aSemy committed Jun 17, 2024
1 parent 3370cd9 commit 773f188
Show file tree
Hide file tree
Showing 43 changed files with 291 additions and 360 deletions.
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
package io.kotest.assertions.nondeterministic

import io.kotest.assertions.failure
import io.kotest.mpp.timeInMillis
import kotlinx.coroutines.delay
import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds
import kotlin.time.TimeSource

/**
* Runs the [test] function continually for the given [duration], failing if an exception is
Expand All @@ -32,12 +32,12 @@ suspend fun <T> continually(

delay(config.initialDelay)

val start = timeInMillis()
val end = start + config.duration.inWholeMilliseconds
val start = TimeSource.Monotonic.markNow()
val end = start.plus(config.duration)
var iterations = 0
var result: Result<T> = Result.failure(IllegalStateException("No successful result"))

while (timeInMillis() < end) {
while (end.hasNotPassedNow()) {
runCatching {
test()
}.onSuccess {
Expand All @@ -48,10 +48,11 @@ suspend fun <T> continually(
is AssertionError -> {
if (iterations == 0) throw it
throw failure(
"Test failed after ${start}ms; expected to pass for ${config.duration}; attempted $iterations times\nUnderlying failure was: ${it.message}",
"Test failed after ${start}; expected to pass for ${config.duration}; attempted $iterations times\nUnderlying failure was: ${it.message}",
it
)
}

else -> throw it
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ package io.kotest.assertions.nondeterministic
import io.kotest.assertions.ErrorCollectionMode
import io.kotest.assertions.errorCollector
import io.kotest.assertions.failure
import io.kotest.mpp.timeInMillis
import kotlinx.coroutines.delay
import kotlin.math.min
import kotlin.reflect.KClass
import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.TimeMark
import kotlin.time.TimeSource

/**
* Runs a function [test] until it doesn't throw as long as the specified duration hasn't passed.
Expand Down Expand Up @@ -67,7 +67,7 @@ suspend fun <T> eventually(

control.step()
}
} catch (e : ShortCircuitControlException) {
} catch (e: ShortCircuitControlException) {
// Short-circuited out from retries, will throw below

// If we terminated due to an exception, we are missing an iteration in the counter
Expand Down Expand Up @@ -204,8 +204,8 @@ object NoopEventuallyListener : EventuallyListener {

private class EventuallyControl(val config: EventuallyConfiguration) {

val start = timeInMillis()
val end = start + config.duration.inWholeMilliseconds
val start: TimeMark = TimeSource.Monotonic.markNow()
val end: TimeMark = start.plus(config.duration)

var iterations = 0

Expand Down Expand Up @@ -236,19 +236,16 @@ private class EventuallyControl(val config: EventuallyConfiguration) {

suspend fun step() {
lastInterval = config.intervalFn.next(++iterations)
val delayMark = timeInMillis()
val delayMark = start.elapsedNow()
// cap the interval at remaining time
delay(min(lastInterval.inWholeMilliseconds, end - delayMark))
lastDelayPeriod = (timeInMillis() - delayMark).milliseconds
delay(minOf(lastInterval, config.duration - delayMark))
lastDelayPeriod = (start.elapsedNow() - delayMark)
}

fun hasAttemptsRemaining() = timeInMillis() < end && iterations < config.retries
fun hasAttemptsRemaining(): Boolean = end.hasNotPassedNow() && iterations < config.retries

fun buildFailureMessage() = StringBuilder().apply {
val totalDuration = timeInMillis() - start
val printedDuration = if (totalDuration >= 1000) "${totalDuration / 1000}s" else "${totalDuration}ms"

appendLine("Block failed after $printedDuration; attempted $iterations time(s)")
fun buildFailureMessage(): String = buildString {
appendLine("Block failed after ${start.elapsedNow()}; attempted $iterations time(s)")

firstError?.takeIf { config.includeFirst }?.run {
appendLine("The first error was caused by: ${this.message}")
Expand All @@ -259,7 +256,7 @@ private class EventuallyControl(val config: EventuallyConfiguration) {
appendLine("The last error was caused by: ${this.message}")
appendLine(this.stackTraceToString())
}
}.toString()
}
}

internal object ShortCircuitControlException : Throwable()
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package io.kotest.matchers.concurrent.suspension

import io.kotest.assertions.failure
import io.kotest.common.measureTimeMillisCompat
import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.withTimeout
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicReference
import kotlin.time.measureTime
import kotlin.time.toDuration
import kotlin.time.toDurationUnit

/**
* Asserts that the given suspendable lambda completes within the given time.
Expand Down Expand Up @@ -39,14 +41,14 @@ suspend fun <A> shouldCompleteBetween(from: Long, to: Long, unit: TimeUnit, thun
val ref = AtomicReference<A>(null)

try {
val timeElapsed = measureTimeMillisCompat {
val timeElapsed = measureTime {
withTimeout(unit.toMillis(to)) {
val a = thunk()
ref.set(a)
}
}

if (unit.toMillis(from) > timeElapsed) {
if (from.toDuration(unit.toDurationUnit()) > timeElapsed) {
throw failure("Test should not have completed before $from/$unit")
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,10 @@ import io.kotest.assertions.timing.eventually
import io.kotest.assertions.until.fibonacci
import io.kotest.assertions.until.fixed
import io.kotest.assertions.withClue
import io.kotest.common.measureTimeMillisCompat
import io.kotest.core.spec.style.WordSpec
import io.kotest.matchers.comparables.shouldBeGreaterThanOrEqualTo
import io.kotest.matchers.ints.shouldBeLessThan
import io.kotest.matchers.longs.shouldBeGreaterThan
import io.kotest.matchers.longs.shouldBeGreaterThanOrEqual
import io.kotest.matchers.nulls.shouldBeNull
import io.kotest.matchers.nulls.shouldNotBeNull
import io.kotest.matchers.shouldBe
Expand All @@ -33,6 +32,7 @@ import java.util.concurrent.atomic.AtomicInteger
import kotlin.time.Duration.Companion.days
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds
import kotlin.time.measureTime

@OptIn(DelicateCoroutinesApi::class)
class EventuallyTest : WordSpec() {
Expand Down Expand Up @@ -173,15 +173,15 @@ class EventuallyTest : WordSpec() {
}
}
"handle shouldNotBeNull" {
val duration = measureTimeMillisCompat {
val duration = measureTime {
shouldThrow<java.lang.AssertionError> {
eventually(50.milliseconds) {
val str: String? = null
str.shouldNotBeNull()
}
}
}
duration.shouldBeGreaterThanOrEqual(50)
duration shouldBeGreaterThanOrEqualTo 50.milliseconds
}

"eventually with boolean predicate" {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ class ContinuallyTest : FunSpec() {
}

test("pass tests with null values") {
val test = continually(500.milliseconds) {
continually(500.milliseconds) {
null shouldBe null
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,10 @@ import io.kotest.assertions.shouldFail
import io.kotest.assertions.throwables.shouldThrow
import io.kotest.assertions.throwables.shouldThrowAny
import io.kotest.assertions.withClue
import io.kotest.common.measureTimeMillisCompat
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.comparables.shouldBeGreaterThanOrEqualTo
import io.kotest.matchers.ints.shouldBeLessThan
import io.kotest.matchers.longs.shouldBeGreaterThan
import io.kotest.matchers.longs.shouldBeGreaterThanOrEqual
import io.kotest.matchers.nulls.shouldNotBeNull
import io.kotest.matchers.shouldBe
import io.kotest.matchers.string.shouldContain
Expand All @@ -25,6 +24,7 @@ import java.util.concurrent.TimeUnit
import kotlin.time.Duration.Companion.days
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds
import kotlin.time.measureTime

class EventuallyTest : FunSpec() {

Expand Down Expand Up @@ -155,7 +155,7 @@ class EventuallyTest : FunSpec() {
}
}
}.message
message shouldContain """Block failed after \d+ms; attempted $count time\(s\)""".toRegex()
message shouldContain """Block failed after [^;]+; attempted $count time\(s\)""".toRegex()
message shouldContain "The first error was caused by: first"
message shouldContain "The last error was caused by: last"
}
Expand All @@ -177,19 +177,11 @@ class EventuallyTest : FunSpec() {
}
}.message
count shouldBe 2
message shouldContain """Block failed after \d+ms; attempted $count time\(s\)""".toRegex()
message shouldContain """Block failed after [^;]+; attempted $count time\(s\)""".toRegex()
message shouldContain "The first error was caused by: first"
message shouldContain "The last error was caused by: last"
}

test("duration is displayed in whole seconds once past 1000ms") {
shouldFail {
eventually(1100.milliseconds) {
fail("")
}
}.message shouldContain Regex("Block failed after [1-2]s")
}

test("allow suspendable functions") {
eventually(100.milliseconds) {
delay(1)
Expand All @@ -210,15 +202,15 @@ class EventuallyTest : FunSpec() {
}

test("handle shouldNotBeNull") {
val duration = measureTimeMillisCompat {
val duration = measureTime {
shouldThrow<java.lang.AssertionError> {
eventually(50.milliseconds) {
val str: String? = null
str.shouldNotBeNull()
}
}
}
duration.shouldBeGreaterThanOrEqual(50)
duration shouldBeGreaterThanOrEqualTo 50.milliseconds
}

test("support fibonacci interval functions") {
Expand Down Expand Up @@ -271,13 +263,13 @@ class EventuallyTest : FunSpec() {
duration = 5.seconds
retries = 2
}
val message = shouldThrow<AssertionError> {
val message = shouldFail {
eventually(config) {
1 shouldBe 2
}
}.message

message shouldContain """Block failed after \d{1,3}ms; attempted 2 time\(s\)""".toRegex()
message shouldContain """Block failed after [^;]+; attempted 2 time\(s\)""".toRegex()
}

test("override assertion to hard assertion before executing assertion and reset it after executing") {
Expand Down Expand Up @@ -343,15 +335,16 @@ class EventuallyTest : FunSpec() {
}

test("short-circuited exceptions are not retried") {
shouldFail {
val failure = shouldFail {
val config = eventuallyConfig {
duration = 5.seconds
shortCircuit = { true }
}
eventually(config) {
1 shouldBe 2
}
}.message shouldContain """Block failed after \d{1,3}ms; attempted 1 time""".toRegex()
}
failure.message shouldContain """Block failed after [^;]+; attempted 1 time""".toRegex()
}

test("suppress first error") {
Expand All @@ -362,10 +355,9 @@ class EventuallyTest : FunSpec() {
includeFirst = false
}
eventually(config) {
if(count++ == 0)
{
if (count++ == 0) {
fail("first")
}else{
} else {
fail("last")
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -712,15 +712,15 @@ public final class io/kotest/assertions/timing/ContinuallyListener$Companion {
}

public final class io/kotest/assertions/timing/ContinuallyState {
public fun <init> (Lio/kotest/common/TimeMarkCompat;Lio/kotest/common/TimeMarkCompat;I)V
public final fun component1 ()Lio/kotest/common/TimeMarkCompat;
public final fun component2 ()Lio/kotest/common/TimeMarkCompat;
public fun <init> (Lkotlin/time/TimeMark;Lkotlin/time/TimeMark;I)V
public final fun component1 ()Lkotlin/time/TimeMark;
public final fun component2 ()Lkotlin/time/TimeMark;
public final fun component3 ()I
public final fun copy (Lio/kotest/common/TimeMarkCompat;Lio/kotest/common/TimeMarkCompat;I)Lio/kotest/assertions/timing/ContinuallyState;
public static synthetic fun copy$default (Lio/kotest/assertions/timing/ContinuallyState;Lio/kotest/common/TimeMarkCompat;Lio/kotest/common/TimeMarkCompat;IILjava/lang/Object;)Lio/kotest/assertions/timing/ContinuallyState;
public final fun copy (Lkotlin/time/TimeMark;Lkotlin/time/TimeMark;I)Lio/kotest/assertions/timing/ContinuallyState;
public static synthetic fun copy$default (Lio/kotest/assertions/timing/ContinuallyState;Lkotlin/time/TimeMark;Lkotlin/time/TimeMark;IILjava/lang/Object;)Lio/kotest/assertions/timing/ContinuallyState;
public fun equals (Ljava/lang/Object;)Z
public final fun getEnd ()Lio/kotest/common/TimeMarkCompat;
public final fun getStart ()Lio/kotest/common/TimeMarkCompat;
public final fun getEnd ()Lkotlin/time/TimeMark;
public final fun getStart ()Lkotlin/time/TimeMark;
public final fun getTimes ()I
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
Expand Down Expand Up @@ -762,21 +762,21 @@ public abstract interface class io/kotest/assertions/timing/EventuallyListener {
}

public final class io/kotest/assertions/timing/EventuallyState {
public fun <init> (Ljava/lang/Object;Lio/kotest/common/TimeMarkCompat;Lio/kotest/common/TimeMarkCompat;ILjava/lang/Throwable;Ljava/lang/Throwable;)V
public fun <init> (Ljava/lang/Object;Lkotlin/time/TimeMark;Lkotlin/time/TimeMark;ILjava/lang/Throwable;Ljava/lang/Throwable;)V
public final fun component1 ()Ljava/lang/Object;
public final fun component2 ()Lio/kotest/common/TimeMarkCompat;
public final fun component3 ()Lio/kotest/common/TimeMarkCompat;
public final fun component2 ()Lkotlin/time/TimeMark;
public final fun component3 ()Lkotlin/time/TimeMark;
public final fun component4 ()I
public final fun component5 ()Ljava/lang/Throwable;
public final fun component6 ()Ljava/lang/Throwable;
public final fun copy (Ljava/lang/Object;Lio/kotest/common/TimeMarkCompat;Lio/kotest/common/TimeMarkCompat;ILjava/lang/Throwable;Ljava/lang/Throwable;)Lio/kotest/assertions/timing/EventuallyState;
public static synthetic fun copy$default (Lio/kotest/assertions/timing/EventuallyState;Ljava/lang/Object;Lio/kotest/common/TimeMarkCompat;Lio/kotest/common/TimeMarkCompat;ILjava/lang/Throwable;Ljava/lang/Throwable;ILjava/lang/Object;)Lio/kotest/assertions/timing/EventuallyState;
public final fun copy (Ljava/lang/Object;Lkotlin/time/TimeMark;Lkotlin/time/TimeMark;ILjava/lang/Throwable;Ljava/lang/Throwable;)Lio/kotest/assertions/timing/EventuallyState;
public static synthetic fun copy$default (Lio/kotest/assertions/timing/EventuallyState;Ljava/lang/Object;Lkotlin/time/TimeMark;Lkotlin/time/TimeMark;ILjava/lang/Throwable;Ljava/lang/Throwable;ILjava/lang/Object;)Lio/kotest/assertions/timing/EventuallyState;
public fun equals (Ljava/lang/Object;)Z
public final fun getEnd ()Lio/kotest/common/TimeMarkCompat;
public final fun getEnd ()Lkotlin/time/TimeMark;
public final fun getFirstError ()Ljava/lang/Throwable;
public final fun getIteration ()I
public final fun getResult ()Ljava/lang/Object;
public final fun getStart ()Lio/kotest/common/TimeMarkCompat;
public final fun getStart ()Lkotlin/time/TimeMark;
public final fun getThisError ()Ljava/lang/Throwable;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,3 @@ package io.kotest.assertions

@Deprecated("Replaced with the io.kotest.assertions.nondeterministic utils. Deprecated in 5.7")
typealias SuspendingProducer<T> = suspend () -> T



Loading

0 comments on commit 773f188

Please sign in to comment.