Skip to content

Commit ae82a7a

Browse files
committed
fixup! Introduce Kotlin integration tests
Signed-off-by: Sergey Karpov <[email protected]>
1 parent 8802871 commit ae82a7a

File tree

4 files changed

+102
-4
lines changed

4 files changed

+102
-4
lines changed

kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/KotlinTestBase.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import io.modelcontextprotocol.kotlin.sdk.Implementation
1010
import io.modelcontextprotocol.kotlin.sdk.ServerCapabilities
1111
import io.modelcontextprotocol.kotlin.sdk.client.Client
1212
import io.modelcontextprotocol.kotlin.sdk.client.SseClientTransport
13+
import io.modelcontextprotocol.kotlin.sdk.integration.utils.Retry
1314
import io.modelcontextprotocol.kotlin.sdk.server.Server
1415
import io.modelcontextprotocol.kotlin.sdk.server.ServerOptions
1516
import io.modelcontextprotocol.kotlin.sdk.server.mcp
@@ -21,6 +22,7 @@ import kotlin.time.Duration.Companion.seconds
2122
import io.ktor.server.cio.CIO as ServerCIO
2223
import io.ktor.server.sse.SSE as ServerSSE
2324

25+
@Retry(times = 3)
2426
abstract class KotlinTestBase {
2527

2628
protected val host = "localhost"

kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TypeScriptClientKotlinServerTest.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,6 @@ class TypeScriptClientKotlinServerTest : TypeScriptTestBase() {
159159

160160
threads.forEach { it.join() }
161161

162-
println("EXCEPTIONS: ${exceptions.joinToString { it.message ?: "" }}")
163162
assertTrue(exceptions.isEmpty(), "No exceptions should occur: ${exceptions.joinToString { it.message ?: "" }}")
164163

165164
val sortedOutputs = outputs.sortedBy { it.first }.map { it.second }

kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/TypeScriptTestBase.kt

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,28 @@
11
package io.modelcontextprotocol.kotlin.sdk.integration.typescript
22

3+
import io.modelcontextprotocol.kotlin.sdk.integration.utils.Retry
34
import org.junit.jupiter.api.BeforeAll
45
import java.io.BufferedReader
56
import java.io.File
67
import java.io.InputStreamReader
78
import java.net.ServerSocket
89
import java.net.Socket
10+
import java.nio.file.Files
911
import java.util.concurrent.TimeUnit
1012

13+
@Retry(times = 3)
1114
abstract class TypeScriptTestBase {
1215

1316
protected val projectRoot: File get() = File(System.getProperty("user.dir"))
14-
protected val tsClientDir: File get() = File(projectRoot, "src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/utils")
17+
protected val tsClientDir: File
18+
get() = File(
19+
projectRoot,
20+
"src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/utils"
21+
)
1522

1623
companion object {
1724
@JvmStatic
18-
private val tempRootDir: File =
19-
java.nio.file.Files.createTempDirectory("typescript-sdk-").toFile().apply { deleteOnExit() }
25+
private val tempRootDir: File = Files.createTempDirectory("typescript-sdk-").toFile().apply { deleteOnExit() }
2026

2127
@JvmStatic
2228
protected val sdkDir: File = File(tempRootDir, "typescript-sdk")
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
package io.modelcontextprotocol.kotlin.sdk.integration.utils
2+
3+
import org.junit.jupiter.api.extension.ExtendWith
4+
import org.junit.jupiter.api.extension.ExtensionContext
5+
import org.junit.jupiter.api.extension.InvocationInterceptor
6+
import org.junit.jupiter.api.extension.InvocationInterceptor.Invocation
7+
import org.junit.jupiter.api.extension.ReflectiveInvocationContext
8+
import java.lang.annotation.Inherited
9+
import java.lang.reflect.AnnotatedElement
10+
import java.lang.reflect.Method
11+
import java.util.*
12+
13+
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION)
14+
@Retention(AnnotationRetention.RUNTIME)
15+
@Inherited
16+
@ExtendWith(RetryExtension::class)
17+
annotation class Retry(
18+
val times: Int = 3,
19+
val delayMs: Long = 0,
20+
)
21+
22+
class RetryExtension : InvocationInterceptor {
23+
override fun interceptTestMethod(
24+
invocation: Invocation<Void>,
25+
invocationContext: ReflectiveInvocationContext<Method>,
26+
extensionContext: ExtensionContext
27+
) {
28+
executeWithRetry(invocation, extensionContext)
29+
}
30+
31+
private fun resolveRetryAnnotation(extensionContext: ExtensionContext): Retry? {
32+
val methodAnn = extensionContext.testMethod.flatMap { findRetry(it) }
33+
if (methodAnn.isPresent) return methodAnn.get()
34+
val classAnn = extensionContext.testClass.flatMap { findRetry(it) }
35+
return classAnn.orElse(null)
36+
}
37+
38+
private fun findRetry(element: AnnotatedElement): Optional<Retry> {
39+
return Optional.ofNullable(element.getAnnotation(Retry::class.java))
40+
}
41+
42+
private fun executeWithRetry(invocation: Invocation<Void>, extensionContext: ExtensionContext) {
43+
val retry = resolveRetryAnnotation(extensionContext)
44+
if (retry == null || retry.times <= 1) {
45+
// No retry requested or only one attempt
46+
invocation.proceed()
47+
return
48+
}
49+
50+
var attempt = 1
51+
var lastError: Throwable? = null
52+
val maxAttempts = retry.times
53+
val delay = retry.delayMs
54+
55+
while (attempt <= maxAttempts) {
56+
try {
57+
if (attempt > 1) {
58+
println("[RetryExtension] Attempt $attempt/$maxAttempts for ${describeTest(extensionContext)}")
59+
}
60+
invocation.proceed()
61+
if (attempt > 1) {
62+
println("[RetryExtension] Succeeded on attempt $attempt for ${describeTest(extensionContext)}")
63+
}
64+
return
65+
} catch (t: Throwable) {
66+
lastError = t
67+
if (attempt >= maxAttempts) {
68+
println("[RetryExtension] Giving up after $attempt attempts for ${describeTest(extensionContext)}: ${t.message}")
69+
throw t
70+
} else {
71+
println("[RetryExtension] Failure on attempt $attempt for ${describeTest(extensionContext)}: ${t.message}")
72+
if (delay > 0) {
73+
try {
74+
Thread.sleep(delay)
75+
} catch (_: InterruptedException) {
76+
}
77+
}
78+
}
79+
}
80+
attempt++
81+
}
82+
// Should not reach here; rethrow last error defensively if it happens
83+
lastError?.let { throw it }
84+
}
85+
86+
private fun describeTest(ctx: ExtensionContext): String {
87+
val methodName = ctx.testMethod.map(Method::getName).orElse("<unknown>")
88+
val className = ctx.testClass.map { it.name }.orElse("<unknown>")
89+
return "$className#$methodName"
90+
}
91+
}

0 commit comments

Comments
 (0)