Skip to content

Commit 65b244f

Browse files
committed
fixup! Add MCP conformance test coverage
1 parent 59b6400 commit 65b244f

File tree

1 file changed

+63
-67
lines changed
  • kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance

1 file changed

+63
-67
lines changed

kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformanceTest.kt

Lines changed: 63 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,16 @@ import java.net.HttpURLConnection
1313
import java.net.ServerSocket
1414
import java.net.URI
1515
import java.util.concurrent.TimeUnit
16+
import kotlin.properties.Delegates
1617

1718
private val logger = KotlinLogging.logger {}
1819

1920
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
2021
class ConformanceTest {
2122

2223
private var serverProcess: Process? = null
23-
private var serverPort: Int = 0
24-
private val serverErrorOutput = StringBuilder()
24+
private var serverPort: Int by Delegates.notNull()
25+
private val serverErrorOutput = StringBuffer()
2526

2627
companion object {
2728
private val SERVER_SCENARIOS = listOf(
@@ -43,6 +44,12 @@ class ConformanceTest {
4344

4445
private const val DEFAULT_TEST_TIMEOUT_SECONDS = 30L
4546
private const val DEFAULT_SERVER_STARTUP_TIMEOUT_SECONDS = 10
47+
private const val INITIAL_BACKOFF_MS = 50L
48+
private const val MAX_BACKOFF_MS = 500L
49+
private const val BACKOFF_MULTIPLIER = 1.5
50+
private const val CONNECTION_TIMEOUT_MS = 500
51+
private const val GRACEFUL_SHUTDOWN_SECONDS = 5L
52+
private const val FORCE_SHUTDOWN_SECONDS = 2L
4653

4754
private fun findFreePort(): Int {
4855
return ServerSocket(0).use { it.localPort }
@@ -62,14 +69,14 @@ class ConformanceTest {
6269
): Boolean {
6370
val deadline = System.currentTimeMillis() + (timeoutSeconds * 1000)
6471
var lastError: Exception? = null
65-
var backoffMs = 50L
72+
var backoffMs = INITIAL_BACKOFF_MS
6673

6774
while (System.currentTimeMillis() < deadline) {
6875
try {
6976
val connection = URI(url).toURL().openConnection() as HttpURLConnection
7077
connection.requestMethod = "GET"
71-
connection.connectTimeout = 500
72-
connection.readTimeout = 500
78+
connection.connectTimeout = CONNECTION_TIMEOUT_MS
79+
connection.readTimeout = CONNECTION_TIMEOUT_MS
7380
connection.connect()
7481

7582
val responseCode = connection.responseCode
@@ -79,7 +86,7 @@ class ConformanceTest {
7986
} catch (e: Exception) {
8087
lastError = e
8188
Thread.sleep(backoffMs)
82-
backoffMs = (backoffMs * 1.5).toLong().coerceAtMost(500)
89+
backoffMs = (backoffMs * BACKOFF_MULTIPLIER).toLong().coerceAtMost(MAX_BACKOFF_MS)
8390
}
8491
}
8592

@@ -102,12 +109,13 @@ class ConformanceTest {
102109
serverPort.toString()
103110
)
104111

105-
serverProcess = processBuilder.start()
112+
val process = processBuilder.start()
113+
serverProcess = process
106114

107115
// capture stderr in the background
108116
Thread {
109117
try {
110-
BufferedReader(InputStreamReader(serverProcess!!.errorStream)).use { reader ->
118+
BufferedReader(InputStreamReader(process.errorStream)).use { reader ->
111119
reader.lineSequence().forEach { line ->
112120
serverErrorOutput.appendLine(line)
113121
logger.debug { "Server stderr: $line" }
@@ -116,6 +124,9 @@ class ConformanceTest {
116124
} catch (e: Exception) {
117125
logger.trace(e) { "Error reading server stderr" }
118126
}
127+
}.apply {
128+
name = "server-stderr-reader"
129+
isDaemon = true
119130
}.start()
120131

121132
logger.info { "Waiting for server to start..." }
@@ -139,29 +150,26 @@ class ConformanceTest {
139150

140151
@AfterAll
141152
fun stopServer() {
142-
if (serverProcess == null) {
143-
logger.debug { "No server process to stop" }
144-
return
145-
}
146-
147-
logger.info { "Stopping conformance test server (PID: ${serverProcess?.pid()})" }
148-
149-
try {
150-
serverProcess?.destroy()
151-
val terminated = serverProcess?.waitFor(5, TimeUnit.SECONDS) ?: false
153+
serverProcess?.also { process ->
154+
logger.info { "Stopping conformance test server (PID: ${process.pid()})" }
152155

153-
if (!terminated) {
154-
logger.warn { "Server did not terminate gracefully, forcing shutdown..." }
155-
serverProcess?.destroyForcibly()
156-
serverProcess?.waitFor(2, TimeUnit.SECONDS) ?: false
157-
} else {
158-
logger.info { "Server stopped gracefully" }
156+
try {
157+
process.destroy()
158+
val terminated = process.waitFor(GRACEFUL_SHUTDOWN_SECONDS, TimeUnit.SECONDS)
159+
160+
if (!terminated) {
161+
logger.warn { "Server did not terminate gracefully, forcing shutdown..." }
162+
process.destroyForcibly()
163+
process.waitFor(FORCE_SHUTDOWN_SECONDS, TimeUnit.SECONDS)
164+
} else {
165+
logger.info { "Server stopped gracefully" }
166+
}
167+
} catch (e: Exception) {
168+
logger.error(e) { "Error stopping server process" }
169+
} finally {
170+
serverProcess = null
159171
}
160-
} catch (e: Exception) {
161-
logger.error(e) { "Error stopping server process" }
162-
} finally {
163-
serverProcess = null
164-
}
172+
} ?: logger.debug { "No server process to stop" }
165173
}
166174

167175
@TestFactory
@@ -185,45 +193,20 @@ class ConformanceTest {
185193
}
186194

187195
private fun runServerConformanceTest(scenario: String, serverUrl: String) {
188-
logger.info { "Running server conformance test: $scenario" }
189-
190-
val timeoutSeconds = System.getenv("CONFORMANCE_TEST_TIMEOUT_SECONDS")?.toLongOrNull()
191-
?: DEFAULT_TEST_TIMEOUT_SECONDS
192-
193-
val process = ProcessBuilder(
196+
val processBuilder = ProcessBuilder(
194197
"npx",
195198
"@modelcontextprotocol/conformance",
196199
"server",
197200
"--url", serverUrl,
198201
"--scenario", scenario
199202
).apply {
200203
inheritIO()
201-
}.start()
202-
203-
val completed = process.waitFor(timeoutSeconds, TimeUnit.SECONDS)
204-
205-
if (!completed) {
206-
logger.error { "Server conformance test '$scenario' timed out after $timeoutSeconds seconds" }
207-
process.destroyForcibly()
208-
throw AssertionError("❌ Server conformance test '$scenario' timed out after $timeoutSeconds seconds")
209204
}
210205

211-
val exitCode = process.exitValue()
212-
213-
if (exitCode != 0) {
214-
logger.error { "Server conformance test '$scenario' failed with exit code: $exitCode" }
215-
throw AssertionError("❌ Server conformance test '$scenario' failed (exit code: $exitCode). Check test output above for details.")
216-
}
217-
218-
logger.info { "✅ Server conformance test '$scenario' passed!" }
206+
runConformanceTest("server", scenario, processBuilder)
219207
}
220208

221209
private fun runClientConformanceTest(scenario: String) {
222-
logger.info { "Running client conformance test: $scenario" }
223-
224-
val timeoutSeconds = System.getenv("CONFORMANCE_TEST_TIMEOUT_SECONDS")?.toLongOrNull()
225-
?: DEFAULT_TEST_TIMEOUT_SECONDS
226-
227210
val testClasspath = getTestClasspath()
228211

229212
val clientCommand = listOf(
@@ -232,31 +215,44 @@ class ConformanceTest {
232215
"io.modelcontextprotocol.kotlin.sdk.conformance.ConformanceClientKt"
233216
)
234217

235-
val process = ProcessBuilder(
218+
val processBuilder = ProcessBuilder(
236219
"npx",
237220
"@modelcontextprotocol/conformance",
238221
"client",
239222
"--command", clientCommand.joinToString(" "),
240223
"--scenario", scenario
241224
).apply {
242225
inheritIO()
243-
}.start()
226+
}
244227

228+
runConformanceTest("client", scenario, processBuilder)
229+
}
230+
231+
private fun runConformanceTest(
232+
type: String,
233+
scenario: String,
234+
processBuilder: ProcessBuilder
235+
) {
236+
logger.info { "Running $type conformance test: $scenario" }
237+
238+
val timeoutSeconds = System.getenv("CONFORMANCE_TEST_TIMEOUT_SECONDS")?.toLongOrNull()
239+
?: DEFAULT_TEST_TIMEOUT_SECONDS
240+
241+
val process = processBuilder.start()
245242
val completed = process.waitFor(timeoutSeconds, TimeUnit.SECONDS)
246243

247244
if (!completed) {
248-
logger.error { "Client conformance test '$scenario' timed out after $timeoutSeconds seconds" }
245+
logger.error { "${type.replaceFirstChar { it.uppercase() }} conformance test '$scenario' timed out after $timeoutSeconds seconds" }
249246
process.destroyForcibly()
250-
throw AssertionError("Client conformance test '$scenario' timed out after $timeoutSeconds seconds")
247+
throw AssertionError("${type.replaceFirstChar { it.uppercase() }} conformance test '$scenario' timed out after $timeoutSeconds seconds")
251248
}
252249

253-
val exitCode = process.exitValue()
254-
255-
if (exitCode != 0) {
256-
logger.error { "Client conformance test '$scenario' failed with exit code: $exitCode" }
257-
throw AssertionError("❌ Client conformance test '$scenario' failed (exit code: $exitCode). Check test output above for details.")
250+
when (val exitCode = process.exitValue()) {
251+
0 -> logger.info { "${type.replaceFirstChar { it.uppercase() }} conformance test '$scenario' passed!" }
252+
else -> {
253+
logger.error { "${type.replaceFirstChar { it.uppercase() }} conformance test '$scenario' failed with exit code: $exitCode" }
254+
throw AssertionError("${type.replaceFirstChar { it.uppercase() }} conformance test '$scenario' failed (exit code: $exitCode). Check test output above for details.")
255+
}
258256
}
259-
260-
logger.info { "✅ Client conformance test '$scenario' passed!" }
261257
}
262258
}

0 commit comments

Comments
 (0)