From a3f3ccf4b57e6da160238e7bc7a031f698cbe749 Mon Sep 17 00:00:00 2001 From: toki Date: Thu, 21 May 2026 14:54:01 +0900 Subject: [PATCH] docs: update cross-language crosstest commands in README and add dart_typescript/go_typescript/kotlin_* python_typescript crosstest files --- README.md | 24 ++++++++++++++++++++++++ dart/crosstest/dart_typescript.dart | 10 +++++++--- go/crosstest/go_typescript.go | 13 ++++++++++--- kotlin/crosstest/kotlin_dart.kt | 18 ++++++++++++++---- kotlin/crosstest/kotlin_go.kt | 18 ++++++++++++++---- kotlin/crosstest/kotlin_python.kt | 18 ++++++++++++++---- python/crosstest/python_typescript.py | 11 +++++++++-- 7 files changed, 92 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 397bb44..68f40b1 100644 --- a/README.md +++ b/README.md @@ -196,16 +196,40 @@ Cross-language checks: cd go go run ./crosstest/go_dart.go go run ./crosstest/go_kotlin.go +go run ./crosstest/go_python.go +go run ./crosstest/go_typescript.go ``` ```bash cd dart dart run crosstest/dart_go.dart +dart run crosstest/dart_kotlin.dart +dart run crosstest/dart_python.dart +dart run crosstest/dart_typescript.dart ``` ```bash cd kotlin +./gradlew run -PmainClass=com.tokilabs.proto_socket.crosstest.KotlinDartKt ./gradlew run -PmainClass=com.tokilabs.proto_socket.crosstest.KotlinGoKt +./gradlew run -PmainClass=com.tokilabs.proto_socket.crosstest.KotlinPythonKt +./gradlew run -PmainClass=com.tokilabs.proto_socket.crosstest.KotlinTypescriptKt +``` + +```bash +cd python +python3 crosstest/python_dart.py +python3 crosstest/python_go.py +python3 crosstest/python_kotlin.py +python3 crosstest/python_typescript.py +``` + +```bash +cd typescript +./node_modules/.bin/tsx crosstest/typescript_dart.ts +./node_modules/.bin/tsx crosstest/typescript_go.ts +./node_modules/.bin/tsx crosstest/typescript_kotlin.ts +./node_modules/.bin/tsx crosstest/typescript_python.ts ``` When proto files change, also run: diff --git a/dart/crosstest/dart_typescript.dart b/dart/crosstest/dart_typescript.dart index 2d2fec2..6aa3bd0 100644 --- a/dart/crosstest/dart_typescript.dart +++ b/dart/crosstest/dart_typescript.dart @@ -13,6 +13,7 @@ const _heartbeatIntervalSeconds = 30; const _heartbeatWaitSeconds = 10; const _serverObservationWindow = Duration(milliseconds: 200); const _processTimeout = Duration(seconds: 20); +const _streamTimeout = Duration(seconds: 5); final _repoRoot = File.fromUri(Platform.script).parent.parent.parent.path; final _typescriptDir = Directory('$_repoRoot/typescript').path; final _certFile = '$_repoRoot/dart/test/certs/server.crt'; @@ -295,10 +296,10 @@ Future _runWssRequests(SecurityContext ctx) async { Future _runTypescriptClient( String mode, int port, String phase, Set expectedScenarios, {String? certFile}) async { + final tsxBin = '$_typescriptDir/node_modules/.bin/tsx'; final process = await Process.start( - 'npx', + tsxBin, [ - 'tsx', 'crosstest/dart_typescript_client.ts', '--mode=$mode', '--port=$port', @@ -329,7 +330,10 @@ Future _runTypescriptClient( process.kill(ProcessSignal.sigkill); return -1; }); - await Future.wait([stdoutDone, stderrDone]); + await Future.wait([stdoutDone, stderrDone]) + .timeout(_streamTimeout, onTimeout: () { + throw StateError('typescript-client $mode/$phase streams did not finish'); + }); _validateResultLines( 'typescript-client $mode/$phase', diff --git a/go/crosstest/go_typescript.go b/go/crosstest/go_typescript.go index 9da2c2b..c617d11 100644 --- a/go/crosstest/go_typescript.go +++ b/go/crosstest/go_typescript.go @@ -367,8 +367,8 @@ func runTypescriptClient(mode string, port int, phase string, expected map[strin return err } + tsxBin := filepath.Join(typescriptDir, "node_modules", ".bin", "tsx") args := []string{ - "tsx", "crosstest/go_typescript_client.ts", "--mode=" + mode, fmt.Sprintf("--port=%d", port), @@ -377,8 +377,9 @@ func runTypescriptClient(mode string, port int, phase string, expected map[strin if len(certFile) > 0 && certFile[0] != "" { args = append(args, "--cert="+certFile[0]) } - cmd := exec.CommandContext(ctx, "npx", args...) + cmd := exec.CommandContext(ctx, tsxBin, args...) cmd.Dir = typescriptDir + cmd.WaitDelay = 5 * time.Second stdoutPipe, err := cmd.StdoutPipe() if err != nil { @@ -400,7 +401,13 @@ func runTypescriptClient(mode string, port int, phase string, expected map[strin go scanLines(&wg, stderrPipe, os.Stderr, nil, nil) waitErr := cmd.Wait() - wg.Wait() + done := make(chan struct{}) + go func() { wg.Wait(); close(done) }() + select { + case <-done: + case <-time.After(5 * time.Second): + return fmt.Errorf("typescript client %s/%s pipe scan timed out", mode, phase) + } if ctx.Err() == context.DeadlineExceeded { return fmt.Errorf("typescript client %s/%s timed out", mode, phase) } diff --git a/kotlin/crosstest/kotlin_dart.kt b/kotlin/crosstest/kotlin_dart.kt index a32b66e..9e59a54 100644 --- a/kotlin/crosstest/kotlin_dart.kt +++ b/kotlin/crosstest/kotlin_dart.kt @@ -39,6 +39,7 @@ private const val WSS_PORT = 29496 private const val WS_PATH = "/" private const val PROCESS_TIMEOUT_MS = 20_000L private const val SERVER_OBSERVATION_MS = 200L +private const val STREAM_JOIN_TIMEOUT_MS = 5_000L fun main() = runBlocking { println("INFO typeName kotlin=${typeNameOf()}") @@ -279,6 +280,11 @@ private suspend fun withServer( } } +private fun destroyProcessTree(process: Process) { + process.toHandle().descendants().forEach { it.destroyForcibly() } + process.destroyForcibly() +} + private suspend fun runDartClient( mode: String, port: Int, @@ -321,13 +327,17 @@ private suspend fun runDartClient( val finished = process.waitFor(PROCESS_TIMEOUT_MS, TimeUnit.MILLISECONDS) if (!finished) { - process.destroyForcibly() + destroyProcessTree(process) + } + stdoutThread.join(STREAM_JOIN_TIMEOUT_MS) + stderrThread.join(STREAM_JOIN_TIMEOUT_MS) + val label = "dart-client $mode/$phase" + check(!stdoutThread.isAlive && !stderrThread.isAlive) { + "$label stream readers did not finish" } - stdoutThread.join() - stderrThread.join() validateResultLines( - "dart-client $mode/$phase", + label, if (finished) process.exitValue() else -1, stdoutLines, expectedScenarios, diff --git a/kotlin/crosstest/kotlin_go.kt b/kotlin/crosstest/kotlin_go.kt index 8df1af4..c2854d3 100644 --- a/kotlin/crosstest/kotlin_go.kt +++ b/kotlin/crosstest/kotlin_go.kt @@ -39,6 +39,7 @@ private const val WSS_PORT = 29396 private const val WS_PATH = "/" private const val PROCESS_TIMEOUT_MS = 20_000L private const val SERVER_OBSERVATION_MS = 200L +private const val STREAM_JOIN_TIMEOUT_MS = 5_000L fun main() = runBlocking { println("INFO typeName kotlin=${typeNameOf()}") @@ -279,6 +280,11 @@ private suspend fun withServer( } } +private fun destroyProcessTree(process: Process) { + process.toHandle().descendants().forEach { it.destroyForcibly() } + process.destroyForcibly() +} + private suspend fun runGoClient( mode: String, port: Int, @@ -321,13 +327,17 @@ private suspend fun runGoClient( val finished = process.waitFor(PROCESS_TIMEOUT_MS, TimeUnit.MILLISECONDS) if (!finished) { - process.destroyForcibly() + destroyProcessTree(process) + } + stdoutThread.join(STREAM_JOIN_TIMEOUT_MS) + stderrThread.join(STREAM_JOIN_TIMEOUT_MS) + val label = "go-client $mode/$phase" + check(!stdoutThread.isAlive && !stderrThread.isAlive) { + "$label stream readers did not finish" } - stdoutThread.join() - stderrThread.join() validateResultLines( - "go-client $mode/$phase", + label, if (finished) process.exitValue() else -1, stdoutLines, expectedScenarios, diff --git a/kotlin/crosstest/kotlin_python.kt b/kotlin/crosstest/kotlin_python.kt index 60a005f..3b88b14 100644 --- a/kotlin/crosstest/kotlin_python.kt +++ b/kotlin/crosstest/kotlin_python.kt @@ -39,6 +39,7 @@ private const val WSS_PORT = 29400 private const val WS_PATH = "/" private const val PROCESS_TIMEOUT_MS = 20_000L private const val SERVER_OBSERVATION_MS = 200L +private const val STREAM_JOIN_TIMEOUT_MS = 5_000L fun main() = runBlocking { println("INFO typeName kotlin=${typeNameOf()}") @@ -279,6 +280,11 @@ private suspend fun withServer( } } +private fun destroyProcessTree(process: Process) { + process.toHandle().descendants().forEach { it.destroyForcibly() } + process.destroyForcibly() +} + private suspend fun runPythonClient( mode: String, port: Int, @@ -320,13 +326,17 @@ private suspend fun runPythonClient( val finished = process.waitFor(PROCESS_TIMEOUT_MS, TimeUnit.MILLISECONDS) if (!finished) { - process.destroyForcibly() + destroyProcessTree(process) + } + stdoutThread.join(STREAM_JOIN_TIMEOUT_MS) + stderrThread.join(STREAM_JOIN_TIMEOUT_MS) + val label = "python-client $mode/$phase" + check(!stdoutThread.isAlive && !stderrThread.isAlive) { + "$label stream readers did not finish" } - stdoutThread.join() - stderrThread.join() validateResultLines( - "python-client $mode/$phase", + label, if (finished) process.exitValue() else -1, stdoutLines, expectedScenarios, diff --git a/python/crosstest/python_typescript.py b/python/crosstest/python_typescript.py index 734dc86..ab06a1e 100644 --- a/python/crosstest/python_typescript.py +++ b/python/crosstest/python_typescript.py @@ -1,7 +1,9 @@ from __future__ import annotations import asyncio +import os import re +import signal import ssl import sys from pathlib import Path @@ -266,8 +268,9 @@ async def run_typescript_client( cert: Path | None = None, ) -> None: ts_dir = typescript_package_dir() + tsx_bin = ts_dir / "node_modules" / ".bin" / "tsx" args = [ - "tsx", + str(tsx_bin), "crosstest/python_typescript_client.ts", f"--mode={mode}", f"--port={port}", @@ -276,15 +279,19 @@ async def run_typescript_client( if cert is not None: args.append(f"--cert={cert}") process = await asyncio.create_subprocess_exec( - "npx", *args, cwd=ts_dir, + start_new_session=True, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, ) try: stdout, stderr = await asyncio.wait_for(process.communicate(), PROCESS_TIMEOUT) except TimeoutError as exc: + try: + os.killpg(process.pid, signal.SIGKILL) + except ProcessLookupError: + pass process.kill() await process.wait() raise TimeoutError(f"typescript client {mode}/{phase} timed out") from exc