proto-socket/dart/crosstest/dart_go.dart
toki 21eed0e2ab fullname 테스트 지원: 모든 언어 간 crosstest에 FullName 필드 추가
- Dart, Go, Kotlin, Python, TypeScript 전 언어 간 crosstest에 FullName 테스트 추가
- crosstest 파일 40개 이상에 FullName 필드 지원 코드 추가
- 테스트 매트릭스 스크립트 및 마일스톤 문서 업데이트
2026-06-16 21:56:48 +09:00

397 lines
12 KiB
Dart

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<void> 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<void> _runTcp() async {
await _runTcpSendPush();
await Future<void>.delayed(const Duration(milliseconds: 150));
await _runTcpRequests();
await Future<void>.delayed(const Duration(milliseconds: 150));
await _runTcpLegacySimpleReceive();
}
Future<void> _runTcpLegacySimpleReceive() async {
final received = Completer<bool>();
final server = _DartTcpServer(_tcpPort, (client) {
client.addListener<TestData>((data) {
if (!received.isCompleted) {
received.complete(data.index == 101);
}
});
});
await _withServer(server.start, server.stop, () async {
await _runGoClient('tcp', _tcpPort, 'legacy-receive', <String>{});
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<void> _runWs() async {
await _runWsSendPush();
await Future<void>.delayed(const Duration(milliseconds: 150));
await _runWsRequests();
}
Future<void> _runTcpSendPush() async {
final received = Completer<bool>();
final server = _DartTcpServer(_tcpPort, (client) {
client.addListener<TestData>((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<void> _runTcpRequests() async {
final server = _DartTcpServer(_tcpPort, (client) {
client.addRequestListener<TestData, TestData>((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<void> _runWsSendPush() async {
final received = Completer<bool>();
final server = _DartWsServer(_wsPort, (client) {
client.addListener<TestData>((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<void> _runWsRequests() async {
final server = _DartWsServer(_wsPort, (client) {
client.addRequestListener<TestData, TestData>((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<void> _withServer(Future<void> Function() start,
Future<void> Function() stop, Future<void> Function() body) async {
await start();
try {
await body();
} finally {
await stop();
}
}
Future<void> _runTls(String? selectedTransport) async {
final ctx = SecurityContext()
..useCertificateChain(_certFile)
..usePrivateKey(_keyFile);
if (selectedTransport == null || selectedTransport == 'tls') {
await _runTlsTcpSendPush(ctx);
await Future<void>.delayed(const Duration(milliseconds: 150));
await _runTlsTcpRequests(ctx);
await Future<void>.delayed(const Duration(milliseconds: 150));
}
if (selectedTransport == null || selectedTransport == 'wss') {
await _runWssSendPush(ctx);
await Future<void>.delayed(const Duration(milliseconds: 150));
await _runWssRequests(ctx);
}
}
Future<void> _runTlsTcpSendPush(SecurityContext ctx) async {
final received = Completer<bool>();
final server = _DartTlsTcpServer(_tlsTcpPort, ctx, (client) {
client.addListener<TestData>((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<void> _runTlsTcpRequests(SecurityContext ctx) async {
final server = _DartTlsTcpServer(_tlsTcpPort, ctx, (client) {
client.addRequestListener<TestData, TestData>((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<void> _runWssSendPush(SecurityContext ctx) async {
final received = Completer<bool>();
final server = _DartWssServer(_wssPort, ctx, (client) {
client.addListener<TestData>((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<void> _runWssRequests(SecurityContext ctx) async {
final server = _DartWssServer(_wssPort, ctx, (client) {
client.addRequestListener<TestData, TestData>((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<void> _runGoClient(
String mode, int port, String phase, Set<String> 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 = <String>[];
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<void>();
final stderrDone = process.stderr
.transform(utf8.decoder)
.transform(const LineSplitter())
.listen(stderr.writeln)
.asFuture<void>();
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<String> lines,
Set<String> expectedScenarios,
) {
final failed = lines.where((line) => line.startsWith('FAIL ')).toList();
final passed = <String>{};
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');
}
}