- Rename package/module from toki_socket to proto_socket in Dart, Kotlin, Python - Update crosstest implementations to use renamed packages - Add new proto_socket skill, deprecate add-toki-socket-crosstest-language skill - Update domain rules for all languages - Update documentation (README, PORTING_GUIDE, PROTOCOL, VERSIONING)
216 lines
6.3 KiB
Dart
216 lines
6.3 KiB
Dart
import 'dart:async';
|
|
import 'dart:io';
|
|
|
|
import 'package:proto_socket/proto_socket.dart';
|
|
|
|
const _host = '127.0.0.1';
|
|
const _wsPath = '/';
|
|
const _heartbeatIntervalSeconds = 30;
|
|
const _heartbeatWaitSeconds = 10;
|
|
const _connectWindow = Duration(seconds: 3);
|
|
const _requestWindow = Duration(seconds: 2);
|
|
|
|
class _TcpClient extends ProtobufClient {
|
|
_TcpClient(Socket socket)
|
|
: super(socket, _heartbeatIntervalSeconds, _heartbeatWaitSeconds, {
|
|
TestData.getDefault().info_.qualifiedMessageName: TestData.fromBuffer,
|
|
});
|
|
}
|
|
|
|
class _WsClient extends WsProtobufClient {
|
|
_WsClient(WebSocket ws)
|
|
: super(ws, _heartbeatIntervalSeconds, _heartbeatWaitSeconds, {
|
|
TestData.getDefault().info_.qualifiedMessageName: TestData.fromBuffer,
|
|
});
|
|
}
|
|
|
|
class _ClientHandle {
|
|
final Communicator client;
|
|
final Future<void> Function() close;
|
|
|
|
_ClientHandle(this.client, this.close);
|
|
}
|
|
|
|
Future<void> main(List<String> args) async {
|
|
final mode = _argValue(args, 'mode') ?? 'tcp';
|
|
final phase = _argValue(args, 'phase') ?? 'send-push';
|
|
final port = int.tryParse(_argValue(args, 'port') ?? '');
|
|
final certPath = _argValue(args, 'cert');
|
|
|
|
print(
|
|
'INFO typeName dart=${TestData.getDefault().info_.qualifiedMessageName}');
|
|
|
|
if (port == null) {
|
|
_fail('setup', 'port is required');
|
|
exitCode = 1;
|
|
return;
|
|
}
|
|
|
|
late _ClientHandle handle;
|
|
try {
|
|
handle = await _connectWithRetry(mode, port, certPath: certPath);
|
|
} catch (error) {
|
|
_fail('setup', error.toString());
|
|
exitCode = 1;
|
|
return;
|
|
}
|
|
|
|
var ok = false;
|
|
try {
|
|
switch (phase) {
|
|
case 'send-push':
|
|
ok = await _runSendPush(handle.client);
|
|
break;
|
|
case 'requests':
|
|
ok = await _runRequests(handle.client);
|
|
break;
|
|
default:
|
|
_fail('setup', 'unknown phase "$phase"');
|
|
}
|
|
} finally {
|
|
await handle.close();
|
|
}
|
|
|
|
if (!ok) {
|
|
exitCode = 1;
|
|
}
|
|
exit(exitCode);
|
|
}
|
|
|
|
Future<_ClientHandle> _connectWithRetry(String mode, int port,
|
|
{String? certPath}) async {
|
|
final deadline = DateTime.now().add(_connectWindow);
|
|
Object? lastError;
|
|
|
|
while (DateTime.now().isBefore(deadline)) {
|
|
try {
|
|
switch (mode) {
|
|
case 'tcp':
|
|
final socket = await ProtobufClient.connect(_host, port)
|
|
.timeout(const Duration(milliseconds: 300));
|
|
final client = _TcpClient(socket);
|
|
return _ClientHandle(client, client.close);
|
|
case 'ws':
|
|
final ws = await WsProtobufClient.connect(_host, port, path: _wsPath)
|
|
.timeout(const Duration(milliseconds: 300));
|
|
final client = _WsClient(ws);
|
|
return _ClientHandle(client, client.close);
|
|
case 'tls':
|
|
final ctx = _buildClientSecurityContext(certPath, mode);
|
|
final socket =
|
|
await ProtobufClient.connectSecure(_host, port, context: ctx)
|
|
.timeout(const Duration(milliseconds: 300));
|
|
final client = _TcpClient(socket);
|
|
return _ClientHandle(client, client.close);
|
|
case 'wss':
|
|
final ctx = _buildClientSecurityContext(certPath, mode);
|
|
final ws = await WsProtobufClient.connectSecure(_host, port,
|
|
path: _wsPath, context: ctx)
|
|
.timeout(const Duration(milliseconds: 300));
|
|
final client = _WsClient(ws);
|
|
return _ClientHandle(client, client.close);
|
|
default:
|
|
throw ArgumentError('unknown mode "$mode"');
|
|
}
|
|
} catch (error) {
|
|
lastError = error;
|
|
await Future<void>.delayed(const Duration(milliseconds: 100));
|
|
}
|
|
}
|
|
|
|
throw TimeoutException(
|
|
'connect $mode:$port timed out: $lastError', _connectWindow);
|
|
}
|
|
|
|
SecurityContext _buildClientSecurityContext(String? certPath, String mode) {
|
|
if (certPath == null || certPath.isEmpty) {
|
|
throw ArgumentError('--cert is required for $mode mode');
|
|
}
|
|
return SecurityContext()..setTrustedCertificates(certPath);
|
|
}
|
|
|
|
Future<bool> _runSendPush(Communicator client) async {
|
|
final pushCompleter = Completer<TestData>();
|
|
client.addListener<TestData>((data) {
|
|
if (!pushCompleter.isCompleted) {
|
|
pushCompleter.complete(data);
|
|
}
|
|
});
|
|
|
|
await client.send(TestData()
|
|
..index = 101
|
|
..message = 'fire from dart client');
|
|
_pass('1', 'fire-and-forget sent');
|
|
|
|
try {
|
|
final push = await pushCompleter.future.timeout(_requestWindow);
|
|
if (push.index != 200 || push.message != 'push from kotlin server') {
|
|
_fail(
|
|
'2', 'unexpected push index=${push.index} message="${push.message}"');
|
|
return false;
|
|
}
|
|
_pass('2', 'received push from kotlin server');
|
|
return true;
|
|
} catch (error) {
|
|
_fail('2', error.toString());
|
|
return false;
|
|
}
|
|
}
|
|
|
|
Future<bool> _runRequests(Communicator client) async {
|
|
try {
|
|
final single = await client
|
|
.sendRequest<TestData, TestData>(TestData()
|
|
..index = 21
|
|
..message = 'single request from dart')
|
|
.timeout(_requestWindow);
|
|
if (single.index != 42 ||
|
|
single.message != 'echo: single request from dart') {
|
|
_fail('3',
|
|
'unexpected response index=${single.index} message="${single.message}"');
|
|
return false;
|
|
}
|
|
_pass('3', 'single request response matched');
|
|
|
|
final futures = <Future<void>>[];
|
|
for (var i = 0; i < 5; i++) {
|
|
futures.add(() async {
|
|
final index = 30 + i;
|
|
final message = 'multi request $i from dart';
|
|
final res = await client
|
|
.sendRequest<TestData, TestData>(TestData()
|
|
..index = index
|
|
..message = message)
|
|
.timeout(_requestWindow);
|
|
if (res.index != index * 2 || res.message != 'echo: $message') {
|
|
throw StateError(
|
|
'request $i got index=${res.index} message="${res.message}"');
|
|
}
|
|
}());
|
|
}
|
|
await Future.wait(futures);
|
|
_pass('4', 'concurrent request responses matched');
|
|
return true;
|
|
} catch (error) {
|
|
_fail('4', error.toString());
|
|
return false;
|
|
}
|
|
}
|
|
|
|
String? _argValue(List<String> args, String name) {
|
|
final prefix = '--$name=';
|
|
for (final arg in args) {
|
|
if (arg.startsWith(prefix)) {
|
|
return arg.substring(prefix.length);
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
void _pass(String scenario, String detail) {
|
|
print('PASS scenario=$scenario detail=$detail');
|
|
}
|
|
|
|
void _fail(String scenario, String error) {
|
|
print('FAIL scenario=$scenario error=$error');
|
|
}
|