proto-socket/kotlin/crosstest/kotlin_dart_web.kt
toki 5df325bf96 feat: Dart Web E2E 테스트 및 크로스 테스트 추가
- Dart Web 브라우저 E2E 테스트 프레임워크 추가
- browser_ws_dart_io_test, browser_ws_kotlin_test 등 브라우저 통합 테스트 추가
- Dart Web 크로스테스트 케이스 추가 (go, kotlin, python, typescript)
- TypeScript 브라우저 웹소켓 클라이언트 개선
- Node.js 웹소켓 서버 클라이언트 업데이트
- 테스트 실행 매트릭스 스킬 및 스크립트 업데이트
- README.md 문서 업데이트
2026-05-25 00:30:37 +09:00

125 lines
4.4 KiB
Kotlin

@file:JvmName("KotlinDartWebKt")
package com.tokilabs.proto_socket.crosstest
import com.tokilabs.proto_socket.ParserMap
import com.tokilabs.proto_socket.WsClient
import com.tokilabs.proto_socket.WsServer
import com.tokilabs.proto_socket.addRequestListenerTyped
import com.tokilabs.proto_socket.typeNameOf
import com.tokilabs.proto_socket.packets.TestData
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeoutOrNull
import java.io.File
import java.util.concurrent.TimeUnit
import kotlin.system.exitProcess
private const val HOST = "127.0.0.1"
private const val WS_PORT = 29498
private const val WS_PATH = "/"
private const val PROCESS_TIMEOUT_MS = 60_000L
private const val SERVER_OBSERVATION_MS = 500L
private const val STREAM_JOIN_TIMEOUT_MS = 5_000L
fun main() = runBlocking {
println("INFO typeName kotlin=${typeNameOf<TestData>()}")
try {
runWebWs()
println("PASS scenario=send-push")
println("PASS scenario=request-response")
println("PASS all kotlin-server/dart-web-client crosstests passed")
} catch (error: Throwable) {
System.err.println("FAIL crosstest error=${error.message ?: error}")
exitProcess(1)
}
}
private suspend fun runWebWs() = coroutineScope {
val received = CompletableDeferred<Boolean>()
val server = WsServer(HOST, WS_PORT, WS_PATH) { conn ->
WsClient.forServer(conn, 0, 0, parserMap())
}
server.onClientConnected = { client ->
addRequestListenerTyped<TestData, TestData>(client.communicator) { req ->
println("SERVER_RECEIVED index=${req.index} message=${req.message}")
if (req.index == 101 && req.message == "fire from dart web client") {
received.complete(true)
launch {
client.send(
TestData.newBuilder()
.setIndex(200)
.setMessage("push from kotlin server")
.build(),
)
}
}
TestData.newBuilder()
.setIndex(req.index * 2)
.setMessage("echo: ${req.message}")
.build()
}
}
server.start()
delay(100)
try {
runDartBrowserTest("test/browser_ws_kotlin_test.dart")
val ok = withTimeoutOrNull(SERVER_OBSERVATION_MS) { received.await() } ?: false
check(ok) { "WS web send-push server did not receive expected data" }
} finally {
server.stop()
}
}
private fun destroyProcessTree(process: Process) {
process.toHandle().descendants().forEach { it.destroyForcibly() }
process.destroyForcibly()
}
private suspend fun runDartBrowserTest(testPath: String) = withContext(Dispatchers.IO) {
val process = ProcessBuilder("dart", "test", "-p", "chrome", testPath)
.directory(dartDir())
.redirectErrorStream(false)
.start()
val stdoutThread = Thread {
process.inputStream.bufferedReader().forEachLine { line -> println(line) }
}
val stderrThread = Thread {
process.errorStream.bufferedReader().forEachLine { line -> System.err.println(line) }
}
stdoutThread.start()
stderrThread.start()
val finished = process.waitFor(PROCESS_TIMEOUT_MS, TimeUnit.MILLISECONDS)
if (!finished) {
destroyProcessTree(process)
}
stdoutThread.join(STREAM_JOIN_TIMEOUT_MS)
stderrThread.join(STREAM_JOIN_TIMEOUT_MS)
check(!stdoutThread.isAlive && !stderrThread.isAlive) {
"dart browser test stream readers did not finish"
}
val exitCode = if (finished) process.exitValue() else -1
check(exitCode == 0) { "dart browser test failed exitCode=$exitCode" }
}
private fun parserMap(): ParserMap =
mapOf(typeNameOf<TestData>() to { TestData.parseFrom(it) })
private fun dartDir(): File {
var dir = File(System.getProperty("user.dir")).absoluteFile
while (dir.parentFile != null) {
val candidate = File(dir, "../dart").canonicalFile
if (File(candidate, "pubspec.yaml").isFile) return candidate
val direct = File(dir, "dart").canonicalFile
if (File(direct, "pubspec.yaml").isFile) return direct
dir = dir.parentFile
}
error("cannot resolve dart directory")
}