Skip to content

Commit a77fa4b

Browse files
kpavlovvova-jb
authored andcommitted
feat: Add Awaitility extensions for coroutine support in test-utils (#1331)
# feat: Add Awaitility extensions for coroutine support in test-utils - Introduced custom Awaitility extensions to support suspending assertions. - Updated `a2a-test` to use new `test-utils` dependencies for Awaitility utilities. - Upgraded `mockk` and `mockito` dependencies to newer versions. ## Motivation and Context [A2AServerJsonRpcIntegrationTest.kt#L74 is flaky](https://github.com/JetBrains/koog/pull/1116/files#annotation_43754744518) It must use awatility to verify eventually consistent conditions ## Breaking Changes No --- #### Type of the changes - [x] New feature (non-breaking change which adds functionality) - [x] Bug fix (non-breaking change which fixes an issue) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update - [x] Tests improvement - [ ] Refactoring #### Checklist - [x] The pull request has a description of the proposed change - [x] I read the [Contributing Guidelines](https://github.com/JetBrains/koog/blob/main/CONTRIBUTING.md) before opening the pull request - [x] The pull request uses **`develop`** as the base branch - [x] Tests for the changes have been added - [x] All new and existing tests passed ##### Additional steps for pull requests adding a new feature - [ ] An issue describing the proposed change exists - [ ] The pull request includes a link to the issue - [ ] The change was discussed and approved in the issue - [ ] Docs have been added / updated
1 parent 3ba118c commit a77fa4b

File tree

6 files changed

+176
-10
lines changed

6 files changed

+176
-10
lines changed

a2a/a2a-test/build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ kotlin {
2121

2222
jvmMain {
2323
dependencies {
24-
api(kotlin("test-junit5"))
24+
api(project(":test-utils"))
2525
}
2626
}
2727

a2a/a2a-test/src/jvmMain/kotlin/ai/koog/a2a/test/BaseA2AProtocolTest.kt

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import ai.koog.a2a.model.TaskStatusUpdateEvent
2121
import ai.koog.a2a.model.TextPart
2222
import ai.koog.a2a.model.TransportProtocol
2323
import ai.koog.a2a.transport.Request
24+
import ai.koog.test.utils.untilAsserted
2425
import io.kotest.assertions.throwables.shouldThrowExactly
2526
import io.kotest.inspectors.shouldForAll
2627
import io.kotest.matchers.collections.shouldHaveSize
@@ -32,6 +33,7 @@ import io.kotest.matchers.string.shouldStartWith
3233
import io.kotest.matchers.types.shouldBeInstanceOf
3334
import kotlinx.coroutines.flow.toList
3435
import kotlinx.coroutines.test.runTest
36+
import org.awaitility.kotlin.await
3537
import kotlin.time.Duration
3638
import kotlin.uuid.ExperimentalUuidApi
3739
import kotlin.uuid.Uuid
@@ -265,15 +267,19 @@ abstract class BaseA2AProtocolTest {
265267
)
266268
)
267269

268-
val response = client.getTask(getTaskRequest)
270+
await
271+
.ignoreExceptions()
272+
.untilAsserted(this) {
273+
val response = client.getTask(getTaskRequest)
269274

270-
response.data should {
271-
it.id shouldBe taskId
272-
it.contextId shouldBe "test-context"
273-
it.status should {
274-
it.state shouldBe TaskState.Completed
275+
response.data should { task ->
276+
task.id shouldBe taskId
277+
task.contextId shouldBe "test-context"
278+
task.status should { status ->
279+
status.state shouldBe TaskState.Completed
280+
}
281+
}
275282
}
276-
}
277283
}
278284

279285
open fun `test cancel task`() = runTest(timeout = testTimeout) {

gradle/libs.versions.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ ktor3 = "3.2.2"
2525
lettuce = "6.5.5.RELEASE"
2626
logback = "1.5.13"
2727
mcp = "0.8.1"
28-
mockito = "5.19.0"
29-
mockk = "1.13.8"
28+
mockito = "5.21.0"
29+
mockk = "1.14.7"
3030
mokksy = "0.5.1"
3131
mysql = "8.0.33"
3232
netty = "4.2.6.Final"

test-utils/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ kotlin {
2323
api(kotlin("test-junit5"))
2424
api(libs.junit.jupiter.params)
2525
api(libs.testcontainers)
26+
api(libs.awaitility)
2627
runtimeOnly(libs.slf4j.simple)
2728
}
2829
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package ai.koog.test.utils
2+
3+
import kotlinx.coroutines.CoroutineScope
4+
import kotlinx.coroutines.runBlocking
5+
import org.awaitility.core.ConditionFactory
6+
import kotlin.coroutines.ContinuationInterceptor
7+
import kotlin.time.Duration
8+
import kotlin.time.toJavaDuration
9+
10+
/**
11+
* Repeatedly evaluates the given block until it does not throw any exceptions,
12+
* providing support for asynchronous or coroutine-based operations. This method
13+
* is particularly helpful in cases where conditions are expected to eventually
14+
* become true due to the nature of concurrent or delayed behavior.
15+
*
16+
* @param scope The [CoroutineScope] used to provide the context for the suspendable block.
17+
* @param block A suspendable lambda function containing the assertions or conditions to be verified.
18+
*/
19+
public fun ConditionFactory.untilAsserted(scope: CoroutineScope, block: suspend () -> Unit) {
20+
val self: ConditionFactory = this
21+
self.untilAsserted {
22+
runBlocking(scope.coroutineContext.minusKey(ContinuationInterceptor)) {
23+
block()
24+
}
25+
}
26+
}
27+
28+
/**
29+
* Repeatedly evaluates the given block until it does not throw any exceptions.
30+
* This method is useful for asserting conditions that may eventually become true,
31+
* particularly in scenarios involving asynchronous or concurrent operations.
32+
*
33+
* @param block A suspendable lambda function containing the assertions or conditions to be verified.
34+
*/
35+
public fun ConditionFactory.untilAsserted(block: suspend () -> Unit) {
36+
val self: ConditionFactory = this
37+
self.untilAsserted {
38+
runBlocking {
39+
block()
40+
}
41+
}
42+
}
43+
44+
/**
45+
* Repeatedly evaluates the given block until it does not throw any exceptions.
46+
* This method is useful for asserting conditions that may eventually become true,
47+
* particularly in scenarios involving asynchronous or concurrent operations.
48+
*
49+
* @param block A suspendable lambda function containing the assertions or conditions to be verified.
50+
*/
51+
public fun ConditionFactory.atMost(duration: Duration): ConditionFactory =
52+
this.atMost(duration.toJavaDuration())
53+
54+
/**
55+
* Specifies the minimum amount of time that the condition should be evaluated for.
56+
*
57+
* @param duration The minimum duration to evaluate the condition, represented as a [Duration].
58+
* @return The updated [ConditionFactory] instance with the specified minimum duration applied.
59+
*/
60+
public fun ConditionFactory.atLeast(duration: Duration): ConditionFactory =
61+
this.atLeast(duration.toJavaDuration())
62+
63+
/**
64+
* Specifies the delay before polling begins when evaluating a condition.
65+
*
66+
* @param duration The duration to wait before the first polling attempt, represented as a [Duration].
67+
* @return The updated [ConditionFactory] instance with the specified polling delay applied.
68+
*/
69+
public fun ConditionFactory.pollDelay(duration: Duration): ConditionFactory =
70+
this.pollDelay(duration.toJavaDuration())
71+
72+
/**
73+
* Specifies the interval between consecutive polling attempts when evaluating a condition.
74+
*
75+
* @param duration The interval duration between polling attempts, represented as a [Duration].
76+
* @return The updated [ConditionFactory] instance with the specified polling interval applied.
77+
*/
78+
public fun ConditionFactory.pollInterval(duration: Duration): ConditionFactory =
79+
this.pollInterval(duration.toJavaDuration())
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package ai.koog.test.utils
2+
3+
import kotlinx.coroutines.delay
4+
import kotlinx.coroutines.test.runTest
5+
import org.awaitility.Awaitility.await
6+
import org.awaitility.core.ConditionTimeoutException
7+
import org.junit.jupiter.api.Assertions.assertEquals
8+
import org.junit.jupiter.api.Test
9+
import org.junit.jupiter.api.assertThrows
10+
import java.time.Duration
11+
import java.util.concurrent.atomic.AtomicInteger
12+
import kotlin.test.fail
13+
import kotlin.time.Duration.Companion.milliseconds
14+
import kotlin.time.Duration.Companion.seconds
15+
16+
class AwaitilityExtensionsTest {
17+
18+
@Test
19+
fun `untilAsserted with suspend block eventually succeeds`() = runTest {
20+
val counter = AtomicInteger(0)
21+
22+
await()
23+
.atMost(2.seconds)
24+
.untilAsserted(block = {
25+
delay(30)
26+
val current = counter.incrementAndGet()
27+
if (current < 5) {
28+
throw AssertionError("Not enough yet: $current")
29+
}
30+
})
31+
32+
assertEquals(5, counter.get())
33+
}
34+
35+
@Test
36+
fun `untilAsserted with suspend block fails on timeout`() = runTest {
37+
assertThrows<ConditionTimeoutException> {
38+
await()
39+
.atMost(200.milliseconds)
40+
.pollInterval(10.milliseconds)
41+
.untilAsserted(block = {
42+
delay(50)
43+
fail("Always failing")
44+
})
45+
}
46+
}
47+
48+
@Test
49+
fun `untilAsserted with scope and suspend block should eventually succeed`() = runTest {
50+
val counter = AtomicInteger(0)
51+
52+
await()
53+
.atMost(2.seconds)
54+
.atLeast(40.milliseconds)
55+
.pollInterval(10.milliseconds)
56+
.untilAsserted(this) {
57+
val current = counter.incrementAndGet()
58+
if (current < 5) {
59+
throw AssertionError("Not enough yet: $current")
60+
}
61+
}
62+
63+
assertEquals(5, counter.get())
64+
}
65+
66+
@Test
67+
fun `untilAsserted with scope and suspend block fails on timeout`() = runTest {
68+
assertThrows<ConditionTimeoutException> {
69+
await()
70+
.pollDelay(0.milliseconds)
71+
.atLeast(10.milliseconds)
72+
.atMost(200.milliseconds)
73+
.pollInterval(Duration.ofMillis(10))
74+
.untilAsserted(this) {
75+
delay(50)
76+
throw AssertionError("Always failing")
77+
}
78+
}
79+
}
80+
}

0 commit comments

Comments
 (0)