fix(test): improve timing stability in tests (#506)

This commit is contained in:
Konstantin Pavlov 2026-02-06 12:25:18 +02:00 • committed by GitHub
commit 7eb2a93c46
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 66 additions and 35 deletions

View file

@ -1,5 +1,6 @@
package io.modelcontextprotocol.kotlin.sdk.client
import io.kotest.assertions.nondeterministic.eventually
import io.kotest.assertions.throwables.shouldThrow
import io.kotest.matchers.shouldBe
import io.kotest.matchers.string.shouldContain
@ -13,6 +14,7 @@ import kotlinx.coroutines.test.runTest
import kotlin.test.BeforeTest
import kotlin.test.Test
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds
abstract class AbstractClientTransportLifecycleTest<T : AbstractTransport> {
@ -60,11 +62,15 @@ abstract class AbstractClientTransportLifecycleTest<T : AbstractTransport> {
val transport = createTransport()
transport.start()
// Give transport time to initialize
delay(50.milliseconds)
transport.close()
shouldThrow<Exception> {
transport.send(PingRequest().toJSON())
// Verify that send throws after close using eventually for reliability
eventually(2.seconds) {
shouldThrow<Exception> {
transport.send(PingRequest().toJSON())
}
}
}
@ -76,13 +82,17 @@ abstract class AbstractClientTransportLifecycleTest<T : AbstractTransport> {
transport.onClose { closeCallCount++ }
transport.start()
// Give transport time to initialize
delay(50.milliseconds)
// Multiple close attempts
transport.close()
transport.close()
closeCallCount shouldBe 1
// Verify onClose was called exactly once using eventually for cross-platform reliability
eventually(2.seconds) {
closeCallCount shouldBe 1
}
}
protected abstract fun createTransport(): T

View file

@ -1,5 +1,6 @@
package io.modelcontextprotocol.kotlin.sdk.client.stdio
import io.kotest.assertions.nondeterministic.eventually
import io.kotest.assertions.throwables.shouldThrow
import io.kotest.matchers.booleans.shouldBeFalse
import io.kotest.matchers.shouldBe
@ -27,7 +28,7 @@ import java.util.stream.Stream
import kotlin.concurrent.atomics.AtomicBoolean
import kotlin.concurrent.atomics.ExperimentalAtomicApi
import kotlin.test.Test
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds
/**
* Tests for StdioClientTransport error handling: EOF, IO errors, and edge cases.
@ -57,10 +58,13 @@ class StdioClientTransportErrorHandlingTest {
transport.onClose { closeCalled.store(true) }
transport.start()
delay(200.milliseconds)
// Stderr EOF should not close transport
closeCalled.load() shouldBe false
// Use eventually to handle timing differences across platforms (especially Windows)
eventually(2.seconds) {
// Wait for stderr to be processed, then verify transport is still open
closeCalled.load() shouldBe false
}
transport.close()
closeCalled.load() shouldBe true
@ -87,9 +91,13 @@ class StdioClientTransportErrorHandlingTest {
transport.onClose { closeCallCount++ }
transport.start()
delay(100.milliseconds)
// Explicit close after error already closed it
// FATAL stderr should trigger close, wait for it to complete
eventually(2.seconds) {
closeCallCount shouldBe 1
}
// Explicit close after error already closed it (should be no-op)
transport.close()
closeCallCount shouldBe 1
@ -109,10 +117,12 @@ class StdioClientTransportErrorHandlingTest {
transport.onError { errorCalled = true }
transport.start()
delay(100.milliseconds)
// Empty input should close cleanly without error
errorCalled.shouldBeFalse()
// Wait for EOF processing to complete, verify no error was called
eventually(2.seconds) {
errorCalled.shouldBeFalse()
}
}
companion object {