import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'package:proto_socket/proto_socket.dart'; const _host = '127.0.0.1'; const _tcpPort = 29090; const _wsPort = 29092; const _tlsTcpPort = 29094; const _wssPort = 29096; 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 _goDir = Directory('$_repoRoot/go').path; final _certFile = '$_repoRoot/dart/test/certs/server.crt'; final _keyFile = '$_repoRoot/dart/test/certs/server.key'; class _DartTcpClient extends ProtobufClient { _DartTcpClient(Socket socket) : super(socket, _heartbeatIntervalSeconds, _heartbeatWaitSeconds, { TestData.getDefault().info_.qualifiedMessageName: TestData.fromBuffer, }); } class _DartWsClient extends WsProtobufClient { _DartWsClient(WebSocket ws) : super(ws, _heartbeatIntervalSeconds, _heartbeatWaitSeconds, { TestData.getDefault().info_.qualifiedMessageName: TestData.fromBuffer, }); } class _DartTcpServer extends ProtobufServer { final void Function(ProtobufClient) onConnected; _DartTcpServer(int port, this.onConnected) : super(_host, port, (socket) => _DartTcpClient(socket)); @override void onClientConnected(ProtobufClient client) => onConnected(client); } class _DartTlsTcpServer extends ProtobufServer { final void Function(ProtobufClient) onConnected; _DartTlsTcpServer(int port, SecurityContext ctx, this.onConnected) : super.secure(_host, port, ctx, (socket) => _DartTcpClient(socket)); @override void onClientConnected(ProtobufClient client) => onConnected(client); } class _DartWsServer extends WsProtobufServer { final void Function(WsProtobufClient) onConnected; _DartWsServer(int port, this.onConnected) : super(_host, port, (ws) => _DartWsClient(ws)); @override void onClientConnected(WsProtobufClient client) => onConnected(client); } class _DartWssServer extends WsProtobufServer { final void Function(WsProtobufClient) onConnected; _DartWssServer(int port, SecurityContext ctx, this.onConnected) : super.secure(_host, port, ctx, (ws) => _DartWsClient(ws)); @override void onClientConnected(WsProtobufClient client) => onConnected(client); } Future main() async { print( 'INFO typeName dart=${TestData.getDefault().info_.qualifiedMessageName}'); final selectedTransport = Platform.environment['PROTO_SOCKET_CROSSTEST_TRANSPORT']; try { if (selectedTransport == null || selectedTransport == 'tcp') { await _runTcp(); } if (selectedTransport == null || selectedTransport == 'ws') { await _runWs(); } if (selectedTransport == null || selectedTransport == 'tls' || selectedTransport == 'wss') { await _runTls(selectedTransport); } print('PASS all dart-server/go-client crosstests passed'); } catch (error) { stderr.writeln('FAIL crosstest error=$error'); exitCode = 1; } } Future _runTcp() async { await _runTcpSendPush(); await Future.delayed(const Duration(milliseconds: 150)); await _runTcpRequests(); await Future.delayed(const Duration(milliseconds: 150)); await _runTcpLegacySimpleReceive(); } Future _runTcpLegacySimpleReceive() async { final received = Completer(); final server = _DartTcpServer(_tcpPort, (client) { client.addListener((data) { if (!received.isCompleted) { received.complete(data.index == 101); } }); }); await _withServer(server.start, server.stop, () async { await _runGoClient('tcp', _tcpPort, 'legacy-receive', {}); bool ok; try { ok = await received.future.timeout(_serverObservationWindow); } on TimeoutException { throw StateError('TCP legacy simple-name server did not receive packet'); } if (!ok) { throw StateError('TCP legacy simple-name server received unexpected data'); } print('PASS scenario=tcp-legacy-simple-receive detail=server-received-legacy-alias'); }); } Future _runWs() async { await _runWsSendPush(); await Future.delayed(const Duration(milliseconds: 150)); await _runWsRequests(); } Future _runTcpSendPush() async { final received = Completer(); final server = _DartTcpServer(_tcpPort, (client) { client.addListener((data) { print('SERVER_RECEIVED index=${data.index} message=${data.message}'); final valid = data.index == 101 && data.message == 'fire from go client'; if (!received.isCompleted) { received.complete(valid); } if (valid) { unawaited(client.send(TestData() ..index = 200 ..message = 'push from dart server')); } }); }); await _withServer(server.start, server.stop, () async { await _runGoClient('tcp', _tcpPort, 'send-push', {'1', '2'}); // The subprocess only proves the client observed PASS lines. This short // wait also confirms the server-side listener recorded the expected data. final ok = await received.future .timeout(_serverObservationWindow, onTimeout: () => false); if (!ok) { throw StateError('TCP send-push server did not receive expected data'); } }); } Future _runTcpRequests() async { final server = _DartTcpServer(_tcpPort, (client) { client.addRequestListener((req) async { return TestData() ..index = req.index * 2 ..message = 'echo: ${req.message}'; }); }); await _withServer( server.start, server.stop, () => _runGoClient('tcp', _tcpPort, 'requests', {'3', '4'}), ); } Future _runWsSendPush() async { final received = Completer(); final server = _DartWsServer(_wsPort, (client) { client.addListener((data) { print('SERVER_RECEIVED index=${data.index} message=${data.message}'); final valid = data.index == 101 && data.message == 'fire from go client'; if (!received.isCompleted) { received.complete(valid); } if (valid) { unawaited(client.send(TestData() ..index = 200 ..message = 'push from dart server')); } }); }); await _withServer(server.start, server.stop, () async { await _runGoClient('ws', _wsPort, 'send-push', {'1', '2'}); // The subprocess only proves the client observed PASS lines. This short // wait also confirms the server-side listener recorded the expected data. final ok = await received.future .timeout(_serverObservationWindow, onTimeout: () => false); if (!ok) { throw StateError('WS send-push server did not receive expected data'); } }); } Future _runWsRequests() async { final server = _DartWsServer(_wsPort, (client) { client.addRequestListener((req) async { return TestData() ..index = req.index * 2 ..message = 'echo: ${req.message}'; }); }); await _withServer( server.start, server.stop, () => _runGoClient('ws', _wsPort, 'requests', {'3', '4'}), ); } Future _withServer(Future Function() start, Future Function() stop, Future Function() body) async { await start(); try { await body(); } finally { await stop(); } } Future _runTls(String? selectedTransport) async { final ctx = SecurityContext() ..useCertificateChain(_certFile) ..usePrivateKey(_keyFile); if (selectedTransport == null || selectedTransport == 'tls') { await _runTlsTcpSendPush(ctx); await Future.delayed(const Duration(milliseconds: 150)); await _runTlsTcpRequests(ctx); await Future.delayed(const Duration(milliseconds: 150)); } if (selectedTransport == null || selectedTransport == 'wss') { await _runWssSendPush(ctx); await Future.delayed(const Duration(milliseconds: 150)); await _runWssRequests(ctx); } } Future _runTlsTcpSendPush(SecurityContext ctx) async { final received = Completer(); final server = _DartTlsTcpServer(_tlsTcpPort, ctx, (client) { client.addListener((data) { final valid = data.index == 101 && data.message == 'fire from go client'; if (!received.isCompleted) received.complete(valid); if (valid) { unawaited(client.send(TestData() ..index = 200 ..message = 'push from dart server')); } }); }); await _withServer(server.start, server.stop, () async { await _runGoClient('tls', _tlsTcpPort, 'send-push', {'1', '2'}, certFile: _certFile); final ok = await received.future .timeout(_serverObservationWindow, onTimeout: () => false); if (!ok) { throw StateError('TLS TCP send-push server did not receive expected data'); } }); } Future _runTlsTcpRequests(SecurityContext ctx) async { final server = _DartTlsTcpServer(_tlsTcpPort, ctx, (client) { client.addRequestListener((req) async { return TestData() ..index = req.index * 2 ..message = 'echo: ${req.message}'; }); }); await _withServer( server.start, server.stop, () => _runGoClient('tls', _tlsTcpPort, 'requests', {'3', '4'}, certFile: _certFile), ); } Future _runWssSendPush(SecurityContext ctx) async { final received = Completer(); final server = _DartWssServer(_wssPort, ctx, (client) { client.addListener((data) { final valid = data.index == 101 && data.message == 'fire from go client'; if (!received.isCompleted) received.complete(valid); if (valid) { unawaited(client.send(TestData() ..index = 200 ..message = 'push from dart server')); } }); }); await _withServer(server.start, server.stop, () async { await _runGoClient('wss', _wssPort, 'send-push', {'1', '2'}, certFile: _certFile); final ok = await received.future .timeout(_serverObservationWindow, onTimeout: () => false); if (!ok) { throw StateError('WSS send-push server did not receive expected data'); } }); } Future _runWssRequests(SecurityContext ctx) async { final server = _DartWssServer(_wssPort, ctx, (client) { client.addRequestListener((req) async { return TestData() ..index = req.index * 2 ..message = 'echo: ${req.message}'; }); }); await _withServer( server.start, server.stop, () => _runGoClient('wss', _wssPort, 'requests', {'3', '4'}, certFile: _certFile), ); } Future _runGoClient( String mode, int port, String phase, Set expectedScenarios, {String? certFile}) async { final args = [ 'run', './crosstest/dart_go_client', '--mode=$mode', '--port=$port', '--phase=$phase', if (certFile != null) '--cert=$certFile', ]; final process = await Process.start( 'go', args, workingDirectory: _goDir, ); final resultLines = []; final stdoutDone = process.stdout .transform(utf8.decoder) .transform(const LineSplitter()) .listen((line) { print(line); if (line.startsWith('PASS ') || line.startsWith('FAIL ')) { resultLines.add(line); } }).asFuture(); final stderrDone = process.stderr .transform(utf8.decoder) .transform(const LineSplitter()) .listen(stderr.writeln) .asFuture(); final code = await process.exitCode.timeout(_processTimeout, onTimeout: () { process.kill(ProcessSignal.sigkill); return -1; }); await Future.wait([stdoutDone, stderrDone]) .timeout(_streamTimeout, onTimeout: () { throw StateError('go-client $mode/$phase streams did not finish'); }); _validateResultLines( 'go-client $mode/$phase', code, resultLines, expectedScenarios); } void _validateResultLines( String label, int exitCode, List lines, Set expectedScenarios, ) { final failed = lines.where((line) => line.startsWith('FAIL ')).toList(); final passed = {}; for (final line in lines.where((line) => line.startsWith('PASS '))) { final match = RegExp(r'scenario=([^ ]+)').firstMatch(line); if (match != null) { passed.add(match.group(1)!); } } final missing = expectedScenarios.difference(passed); if (exitCode != 0 || failed.isNotEmpty || missing.isNotEmpty) { throw StateError( '$label failed exitCode=$exitCode failed=$failed missing=$missing'); } }