proto-socket/kotlin/crosstest/kotlin_dart_web.kt

193 lines
7.1 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.runBlocking
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeoutOrNull
import java.io.File
import java.security.KeyFactory
import java.security.KeyStore
import java.security.cert.CertificateFactory
import java.security.cert.X509Certificate
import java.security.spec.PKCS8EncodedKeySpec
import java.util.Base64
import java.util.concurrent.TimeUnit
import javax.net.ssl.KeyManagerFactory
import javax.net.ssl.SSLContext
import kotlin.system.exitProcess
private const val HOST = "127.0.0.1"
private const val WS_PORT = 29498
private const val WSS_PORT = 29499
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 {
runWebSuites()
println("PASS scenario=ws-send-push")
println("PASS scenario=ws-request-response")
println("PASS scenario=wss-send-push")
println("PASS scenario=wss-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 runWebSuites() = coroutineScope {
val wsReceived = CompletableDeferred<Boolean>()
val wssReceived = CompletableDeferred<Boolean>()
val certFile = kotlinResourceFile("server.crt")
val keyFile = kotlinResourceFile("server.key")
val serverCtx = buildServerSslContext(certFile.path, keyFile.path)
val wsServer = webServer(WS_PORT, null, wsReceived)
val wssServer = webServer(WSS_PORT, serverCtx, wssReceived)
wsServer.start()
wssServer.start()
delay(100)
try {
runDartBrowserTest("test/browser_ws_kotlin_test.dart")
expectReceived("WS", wsReceived)
expectReceived("WSS", wssReceived)
} finally {
wssServer.stop()
wsServer.stop()
}
}
private fun webServer(
port: Int,
sslContext: SSLContext?,
received: CompletableDeferred<Boolean>,
): WsServer {
val server = WsServer(HOST, port, WS_PATH, sslContext = sslContext) { 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") {
if (!received.isCompleted) received.complete(true)
runBlocking {
client.send(
TestData.newBuilder()
.setIndex(200)
.setMessage("push from kotlin server")
.build(),
)
}
}
TestData.newBuilder()
.setIndex(req.index * 2)
.setMessage("echo: ${req.message}")
.build()
}
}
return server
}
private suspend fun expectReceived(
transport: String,
received: CompletableDeferred<Boolean>,
) {
val ok = withTimeoutOrNull(SERVER_OBSERVATION_MS) { received.await() } ?: false
check(ok) { "$transport web send-push server did not receive expected data" }
}
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 buildServerSslContext(certPath: String, keyPath: String): SSLContext {
val certFactory = CertificateFactory.getInstance("X.509")
val cert = File(certPath).inputStream().use {
certFactory.generateCertificate(it) as X509Certificate
}
val keyPem = File(keyPath).readText()
.replace("-----BEGIN PRIVATE KEY-----", "")
.replace("-----END PRIVATE KEY-----", "")
.replace("\\s+".toRegex(), "")
val privateKey = KeyFactory.getInstance("RSA")
.generatePrivate(PKCS8EncodedKeySpec(Base64.getDecoder().decode(keyPem)))
val keyStore = KeyStore.getInstance("PKCS12")
keyStore.load(null, null)
keyStore.setKeyEntry("toki", privateKey, CharArray(0), arrayOf(cert))
val keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm())
keyManagerFactory.init(keyStore, CharArray(0))
val ctx = SSLContext.getInstance("TLS")
ctx.init(keyManagerFactory.keyManagers, null, null)
return ctx
}
private fun kotlinResourceFile(name: String): File {
var dir = File(System.getProperty("user.dir")).absoluteFile
while (dir.parentFile != null) {
val direct = File(dir, "src/test/resources/$name").canonicalFile
if (direct.isFile) return direct
val nested = File(dir, "kotlin/src/test/resources/$name").canonicalFile
if (nested.isFile) return nested
dir = dir.parentFile
}
error("cannot resolve kotlin test resource $name")
}
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")
}