@@ -13,15 +13,16 @@ import java.net.HttpURLConnection
1313import java.net.ServerSocket
1414import java.net.URI
1515import java.util.concurrent.TimeUnit
16+ import kotlin.properties.Delegates
1617
1718private val logger = KotlinLogging .logger {}
1819
1920@TestInstance(TestInstance .Lifecycle .PER_CLASS )
2021class 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