feat: dart web crosstest and related test infrastructure updates

This commit is contained in:
toki 2026-05-30 22:59:23 +09:00
parent c94f2e82b5
commit 617ec02090
19 changed files with 440 additions and 129 deletions

View file

@ -210,7 +210,7 @@ func main() {
Go 구현체는 다음 표면도 제공한다. Go 구현체는 다음 표면도 제공한다.
- TCP: `NewTcpServer`, `DialTcp`, `NewTcpServerTLS`, `DialTcpTLS` - TCP: `NewTcpServer`, `DialTcp`, `NewTcpServerTLS`, `DialTcpTLS`
- WebSocket: `NewWsServer`, `DialWs`, `NewWsServerTLS`, `DialWss` - WebSocket: `NewWsServer`, `NewWsServerWithOptions`, `DialWs`, `NewWsServerTLS`, `NewWsServerTLSWithOptions`, `DialWss`
- Shared helpers: `Send`, `SendRequest`, `AddListenerTyped`, `AddRequestListenerTyped`, `Broadcast` - Shared helpers: `Send`, `SendRequest`, `AddListenerTyped`, `AddRequestListenerTyped`, `Broadcast`
다른 저장소에서 이 모듈을 사용하는 복사 가능한 scaffold가 필요하면 [examples/go-module-consumer/](examples/go-module-consumer/)를 본다. 다른 저장소에서 이 모듈을 사용하는 복사 가능한 scaffold가 필요하면 [examples/go-module-consumer/](examples/go-module-consumer/)를 본다.
@ -309,17 +309,17 @@ cd typescript
### Dart.io와 Dart.web E2E 매트릭스 ### Dart.io와 Dart.web E2E 매트릭스
위 표의 `Dart` 열은 Dart VM/IO client를 뜻한다. `Dart.web`은 별도 축이다. 브라우저 WS/WSS client만 가능하며, 브라우저에서는 TCP와 server 역할이 불가능하므로 해당 cell은 `N/A`다. Dart.web WSS는 self-signed test certificate로 자동화할 수 없는 항목이 아니다. 현재 남은 coverage gap은 브라우저가 테스트용 self-signed CA/certificate를 신뢰하도록 만드는 자동화와 그에 따른 Dart.web WSS 매트릭스 편입이다. 위 표의 `Dart` 열은 Dart VM/IO client를 뜻한다. `Dart.web`은 별도 축이다. 브라우저 WS/WSS client만 가능하며, 브라우저에서는 TCP와 server 역할이 불가능하므로 해당 cell은 `N/A`다. Dart.web WSS는 테스트용 self-signed certificate의 SPKI를 허용하는 Chrome 실행 wrapper로 local/remote 브라우저 검증에 포함한다.
| Server | Dart.io TCP | Dart.io WS | Dart.io TLS TCP | Dart.io WSS | Dart.web WS | Dart.web WSS | Dart.web TCP | | Server | Dart.io TCP | Dart.io WS | Dart.io TLS TCP | Dart.io WSS | Dart.web WS | Dart.web WSS | Dart.web TCP |
|---|---|---|---|---|---|---|---| |---|---|---|---|---|---|---|---|
| Dart.io | Covered | Covered | Covered | Covered | Covered | Required gap | N/A | | Dart.io | Covered | Covered | Covered | Covered | Covered | Covered | N/A |
| Go | Covered | Covered | Covered | Covered | Covered | Required gap | N/A | | Go | Covered | Covered | Covered | Covered | Covered | Covered | N/A |
| Kotlin | Covered | Covered | Covered | Covered | Covered | Required gap | N/A | | Kotlin | Covered | Covered | Covered | Covered | Covered | Covered | N/A |
| Python | Covered | Covered | Covered | Covered | Covered | Required gap | N/A | | Python | Covered | Covered | Covered | Covered | Covered | Covered | N/A |
| TypeScript | Covered | Covered | Covered | Covered | Covered | Required gap | N/A | | TypeScript | Covered | Covered | Covered | Covered | Covered | Covered | N/A |
각 Dart.web WS cell은 server-language runner가 해당 WS server를 띄우고, matching browser test file에 대해 `dart test -p chrome`을 실행하며, send-push와 request-response 시나리오를 검증한다. Dart.web WSS cell도 같은 시나리오를 검증해야 하며, 테스트용 self-signed CA/certificate trust 자동화가 추가되기 전까지는 완전 coverage의 남은 항목으로 본다. 각 Dart.web cell은 server-language runner가 WS와 WSS server를 함께 띄우고, matching browser test file에 대해 `dart test -p chrome`을 실행하며, WS send-push/request-response와 WSS send-push/request-response 4개 시나리오를 검증한다.
```bash ```bash
cd dart && dart run crosstest/dart_web.dart cd dart && dart run crosstest/dart_web.dart

View file

@ -55,7 +55,7 @@ VERSIONING.md — 프로토콜/패키지 버전 정책
- 기본 검증 명령은 `bash agent-ops/skills/project/run-proto-socket-test-matrix/scripts/run_matrix.sh --all`이다. - 기본 검증 명령은 `bash agent-ops/skills/project/run-proto-socket-test-matrix/scripts/run_matrix.sh --all`이다.
- 테스트 coverage 평가는 라인/브랜치 수치가 아니라 프로토콜 계약 시나리오가 매트릭스에서 빠짐없이 검증되는지로 판단한다. - 테스트 coverage 평가는 라인/브랜치 수치가 아니라 프로토콜 계약 시나리오가 매트릭스에서 빠짐없이 검증되는지로 판단한다.
- Dart.web remote browser host 사용은 의도된 local 테스트 환경이다. 환경을 이전할 때는 그대로 유지할 규칙이 아니라 새 local 환경 규칙으로 재정의해야 한다. - Dart.web remote browser host 사용은 의도된 local 테스트 환경이다. 환경을 이전할 때는 그대로 유지할 규칙이 아니라 새 local 환경 규칙으로 재정의해야 한다.
- Dart.web WSS는 self-signed certificate로 자동화할 수 없는 항목이 아니다. 현재 남은 coverage gap은 브라우저가 테스트용 self-signed CA/certificate를 신뢰하도록 만드는 자동화와 그에 따른 Dart.web WSS 매트릭스 편입이다. - Dart.web WSS는 테스트용 self-signed certificate의 SPKI를 허용하는 Chrome 실행 wrapper로 local/remote 브라우저 매트릭스에 포함한다.
- CI/CD runner 연결은 현재 설계상 과하며, 사용자가 명시적으로 요청하지 않으면 제안하거나 요구하지 않는다. - CI/CD runner 연결은 현재 설계상 과하며, 사용자가 명시적으로 요청하지 않으면 제안하거나 요구하지 않는다.
## 마일스톤 컨텍스트 로딩 ## 마일스톤 컨텍스트 로딩

View file

@ -132,19 +132,23 @@ tools/check_proto_sync.sh
|---|---|---| |---|---|---|
| Dart | `dart pub get && dart test && dart compile js test/browser_ws_import_compile.dart -o /tmp/proto_socket_browser_ws_import_compile.js` | PASS | | Dart | `dart pub get && dart test && dart compile js test/browser_ws_import_compile.dart -o /tmp/proto_socket_browser_ws_import_compile.js` | PASS |
**언어간 통신** **언어 PASS 매트릭스**
| 서버 \ 클라이언트 | Dart | Go | Kotlin | Python | TypeScript | | 서버 \ 클라이언트 | Dart.io | Dart.web | Go | Kotlin | Python | TypeScript |
|---|---|---|---|---|---| |---|---|---|---|---|---|---|
| Dart | PASS | PASS | PASS | PASS | PASS | | Dart.io | PASS | PASS | PASS | PASS | PASS | PASS |
| Go | PASS | PASS | PASS | PASS | PASS | PASS |
| Kotlin | PASS | PASS | PASS | PASS | PASS | PASS |
| Python | PASS | PASS | PASS | PASS | PASS | PASS |
| TypeScript | PASS | PASS | PASS | PASS | PASS | PASS |
**Dart.web 클라이언트 통신** **크로스테스트 상세**
| 서버 | Dart.web (WS) | PASS scenarios | Expected | FAIL lines | | 방향 | 결과 | PASS scenarios | Expected | FAIL lines |
|---|---:|---:|---:|---:| |---|---:|---:|---:|---:|
| Dart.io | PASS | 2 | 2 | 0 | | Dart.io -> Dart.web | PASS | 4 | 4 | 0 |
| Go | PASS | 2 | 2 | 0 | | Go -> Dart.web | PASS | 4 | 4 | 0 |
| Kotlin | PASS | 2 | 2 | 0 | | Kotlin -> Dart.web | PASS | 4 | 4 | 0 |
| Python | PASS | 2 | 2 | 0 | | Python -> Dart.web | PASS | 4 | 4 | 0 |
| TypeScript | PASS | 2 | 2 | 0 | | TypeScript -> Dart.web | PASS | 4 | 4 | 0 |
``` ```
## 금지 사항 ## 금지 사항

View file

@ -112,7 +112,7 @@ declare -A web_fail_lines
declare -A web_cmds declare -A web_cmds
declare -A web_block_reason declare -A web_block_reason
expected_web_pass_lines=2 expected_web_pass_lines=4
count_lines() { count_lines() {
local pattern="$1" local pattern="$1"
@ -157,7 +157,11 @@ chrome_available() {
remote_chrome_export_command() { remote_chrome_export_command() {
cat <<'REMOTE_CHROME' cat <<'REMOTE_CHROME'
if [ -x "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" ]; then if [ -n "${CHROME_EXECUTABLE:-}" ] && [ -x "$CHROME_EXECUTABLE" ]; then
:
elif [ -n "${repo_dir:-}" ] && [ -x "$repo_dir/.agent-test/chrome-proto-socket-wss" ]; then
export CHROME_EXECUTABLE="$repo_dir/.agent-test/chrome-proto-socket-wss"
elif [ -x "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" ]; then
export CHROME_EXECUTABLE="/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" export CHROME_EXECUTABLE="/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
elif command -v google-chrome >/dev/null 2>&1; then elif command -v google-chrome >/dev/null 2>&1; then
export CHROME_EXECUTABLE="$(command -v google-chrome)" export CHROME_EXECUTABLE="$(command -v google-chrome)"
@ -172,8 +176,28 @@ fi
REMOTE_CHROME REMOTE_CHROME
} }
remote_chrome_wrapper_setup_command() {
cat <<'REMOTE_CHROME_WRAPPER'
if [ -x "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" ] &&
command -v openssl >/dev/null 2>&1 &&
[ -f dart/test/certs/server.crt ]; then
mkdir -p "$repo_dir/.agent-test"
spki="$(openssl x509 -in dart/test/certs/server.crt -pubkey -noout |
openssl pkey -pubin -outform DER |
openssl dgst -sha256 -binary |
openssl base64)"
cat > "$repo_dir/.agent-test/chrome-proto-socket-wss" <<WRAPPER
#!/usr/bin/env bash
set -e
exec "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" --ignore-certificate-errors-spki-list="$spki" --allow-insecure-localhost "\$@"
WRAPPER
chmod +x "$repo_dir/.agent-test/chrome-proto-socket-wss"
fi
REMOTE_CHROME_WRAPPER
}
prepare_remote_web_repo() { prepare_remote_web_repo() {
local repo_name_q repo_url_q repo_ref_q chrome_cmd local repo_name_q repo_url_q repo_ref_q chrome_cmd chrome_wrapper_cmd
if [ -z "$remote_web_repo_url" ]; then if [ -z "$remote_web_repo_url" ]; then
echo "remote repository URL is not configured" >&2 echo "remote repository URL is not configured" >&2
@ -184,6 +208,7 @@ prepare_remote_web_repo() {
repo_url_q="$(single_quote "$remote_web_repo_url")" repo_url_q="$(single_quote "$remote_web_repo_url")"
repo_ref_q="$(single_quote "$remote_web_ref")" repo_ref_q="$(single_quote "$remote_web_ref")"
chrome_cmd="$(remote_chrome_export_command)" chrome_cmd="$(remote_chrome_export_command)"
chrome_wrapper_cmd="$(remote_chrome_wrapper_setup_command)"
remote_exec " remote_exec "
set -e set -e
@ -207,6 +232,7 @@ fi
if [ -f python/pyproject.toml ]; then if [ -f python/pyproject.toml ]; then
(cd python && python3 -m pip install -e '.[dev]') (cd python && python3 -m pip install -e '.[dev]')
fi fi
$chrome_wrapper_cmd
$chrome_cmd $chrome_cmd
printf 'remote Dart.web repo: %s\n' \"\$repo_dir\" printf 'remote Dart.web repo: %s\n' \"\$repo_dir\"
printf 'remote Dart.web git ref: %s\n' \"\$(git rev-parse --short HEAD)\" printf 'remote Dart.web git ref: %s\n' \"\$(git rev-parse --short HEAD)\"

View file

@ -53,5 +53,5 @@
- 관련 경로: `agent-ops/skills/project/run-proto-socket-test-matrix/SKILL.md`, `agent-ops/skills/project/run-proto-socket-test-matrix/scripts/run_matrix.sh`, `README.md` - 관련 경로: `agent-ops/skills/project/run-proto-socket-test-matrix/SKILL.md`, `agent-ops/skills/project/run-proto-socket-test-matrix/scripts/run_matrix.sh`, `README.md`
- 선행 작업: 사용 가능 언어 parity - 선행 작업: 사용 가능 언어 parity
- 후속 작업: 안정화 기준선 - 후속 작업: 안정화 기준선
- 검증 기록: 2026-05-30에 `bash agent-ops/skills/project/run-proto-socket-test-matrix/scripts/run_matrix.sh --all` 통과. Proto 동기화, Dart/Go/Kotlin/Python/TypeScript 동일 언어 테스트, 20개 일반 크로스 언어 방향, Dart.web WS 5개 방향이 모두 `PASS`. - 검증 기록: 2026-05-30에 `bash agent-ops/skills/project/run-proto-socket-test-matrix/scripts/run_matrix.sh --all` 통과. Proto 동기화, Dart/Go/Kotlin/Python/TypeScript 동일 언어 테스트, 20개 일반 크로스 언어 방향, Dart.web WS/WSS 5개 방향이 모두 `PASS`.
- 확인 필요: CI/CD 연결은 현재 완전성 기준이 아니다. 상위 `../oto` 프로젝트의 표준 실행 방식이 확정되고 사용자가 명시적으로 요청하면 이 저장소의 로컬 검증 진입점을 외부 실행기로 연결한다. - 확인 필요: CI/CD 연결은 현재 완전성 기준이 아니다. 상위 `../oto` 프로젝트의 표준 실행 방식이 확정되고 사용자가 명시적으로 요청하면 이 저장소의 로컬 검증 진입점을 외부 실행기로 연결한다.

View file

@ -46,6 +46,6 @@
- 관련 경로: `README.md`, `PROTOCOL.md`, `VERSIONING.md`, `PORTING_GUIDE.md`, `agent-roadmap/milestones/continuous-verification.md`, `agent-ops/skills/project/run-proto-socket-test-matrix/scripts/run_matrix.sh` - 관련 경로: `README.md`, `PROTOCOL.md`, `VERSIONING.md`, `PORTING_GUIDE.md`, `agent-roadmap/milestones/continuous-verification.md`, `agent-ops/skills/project/run-proto-socket-test-matrix/scripts/run_matrix.sh`
- 선행 작업: 지속 검증 - 선행 작업: 지속 검증
- 후속 작업: C# Unity/.NET 포트, Swift Apple 플랫폼 포트, 릴리즈 준비 - 후속 작업: C# Unity/.NET 포트, Swift Apple 플랫폼 포트, 릴리즈 준비
- 검증 근거: 2026-05-30에 `bash agent-ops/skills/project/run-proto-socket-test-matrix/scripts/run_matrix.sh --all` 통과. Proto 동기화, Dart/Go/Kotlin/Python/TypeScript 동일 언어 테스트, 20개 일반 크로스 언어 방향, Dart.web WS 5개 방향이 모두 `PASS`. - 검증 근거: 2026-05-30에 `bash agent-ops/skills/project/run-proto-socket-test-matrix/scripts/run_matrix.sh --all` 통과. Proto 동기화, Dart/Go/Kotlin/Python/TypeScript 동일 언어 테스트, 20개 일반 크로스 언어 방향, Dart.web WS/WSS 5개 방향이 모두 `PASS`.
- 문서 정합성: 2026-05-22 기준 README, PROTOCOL, VERSIONING, PORTING_GUIDE의 protocol `0.1`, 지원 언어, 새 언어 보류, package registry 보류, validation gate 설명이 충돌하지 않는다. - 문서 정합성: 2026-05-22 기준 README, PROTOCOL, VERSIONING, PORTING_GUIDE의 protocol `0.1`, 지원 언어, 새 언어 보류, package registry 보류, validation gate 설명이 충돌하지 않는다.
- 안정화 판단: 최신 전체 매트릭스 PASS와 문서 정합성 점검 기준으로 protocol `0.1` 및 현재 5개 언어 구현을 동결/완성형 후보로 둔다. 다만 coverage 완전성 평가는 라인 수치가 아니라 프로토콜 시나리오 coverage로 판단하며, Dart.web WSS self-signed certificate trust 자동화와 매트릭스 편입은 남은 coverage gap으로 기록한다. 새 변경 후보가 발견되기 전까지 protocol/API 변경은 compatibility work로만 다룬다. - 안정화 판단: 최신 전체 매트릭스 PASS와 문서 정합성 점검 기준으로 protocol `0.1` 및 현재 5개 언어 구현을 동결/완성형 후보로 둔다. Coverage 완전성 평가는 라인 수치가 아니라 프로토콜 시나리오 coverage로 판단하며, Dart.web WSS는 테스트용 self-signed certificate SPKI Chrome wrapper를 통해 매트릭스에 포함한다. 새 변경 후보가 발견되기 전까지 protocol/API 변경은 compatibility work로만 다룬다.

View file

@ -6,6 +6,7 @@ import 'package:proto_socket/proto_socket.dart';
const _host = '127.0.0.1'; const _host = '127.0.0.1';
const _wsPort = 29098; const _wsPort = 29098;
const _wssPort = 29099;
const _heartbeatIntervalSeconds = 30; const _heartbeatIntervalSeconds = 30;
const _heartbeatWaitSeconds = 10; const _heartbeatWaitSeconds = 10;
const _serverObservationWindow = Duration(milliseconds: 500); const _serverObservationWindow = Duration(milliseconds: 500);
@ -14,6 +15,8 @@ const _streamTimeout = Duration(seconds: 5);
final _repoRoot = File.fromUri(Platform.script).parent.parent.parent.path; final _repoRoot = File.fromUri(Platform.script).parent.parent.parent.path;
final _dartDir = Directory('$_repoRoot/dart').path; final _dartDir = Directory('$_repoRoot/dart').path;
final _certFile = '$_repoRoot/dart/test/certs/server.crt';
final _keyFile = '$_repoRoot/dart/test/certs/server.key';
class _ServerWsClient extends WsProtobufClient { class _ServerWsClient extends WsProtobufClient {
_ServerWsClient(WebSocket ws) _ServerWsClient(WebSocket ws)
@ -28,6 +31,10 @@ class _DartWebWsServer extends WsProtobufServer {
_DartWebWsServer(int port, this.onConnected) _DartWebWsServer(int port, this.onConnected)
: super(_host, port, (ws) => _ServerWsClient(ws)); : super(_host, port, (ws) => _ServerWsClient(ws));
_DartWebWsServer.secure(
int port, SecurityContext securityContext, this.onConnected)
: super.secure(_host, port, securityContext, (ws) => _ServerWsClient(ws));
@override @override
void onClientConnected(WsProtobufClient client) => onConnected(client); void onClientConnected(WsProtobufClient client) => onConnected(client);
} }
@ -36,9 +43,11 @@ Future<void> main() async {
print( print(
'INFO typeName dart=${TestData.getDefault().info_.qualifiedMessageName}'); 'INFO typeName dart=${TestData.getDefault().info_.qualifiedMessageName}');
try { try {
await _runWebWsSuite(); await _runWebSuites();
print('PASS scenario=send-push'); print('PASS scenario=ws-send-push');
print('PASS scenario=request-response'); print('PASS scenario=ws-request-response');
print('PASS scenario=wss-send-push');
print('PASS scenario=wss-request-response');
print('PASS all dart-server/dart-web-client crosstests passed'); print('PASS all dart-server/dart-web-client crosstests passed');
} catch (error) { } catch (error) {
stderr.writeln('FAIL crosstest error=$error'); stderr.writeln('FAIL crosstest error=$error');
@ -46,9 +55,35 @@ Future<void> main() async {
} }
} }
Future<void> _runWebWsSuite() async { Future<void> _runWebSuites() async {
final received = Completer<bool>(); final wsReceived = Completer<bool>();
final server = _DartWebWsServer(_wsPort, (client) { final wssReceived = Completer<bool>();
final securityContext = SecurityContext()
..useCertificateChain(_certFile)
..usePrivateKey(_keyFile);
final wsServer = _DartWebWsServer(_wsPort, (client) {
_configureWebClient(client, wsReceived);
});
final wssServer =
_DartWebWsServer.secure(_wssPort, securityContext, (client) {
_configureWebClient(client, wssReceived);
});
await wsServer.start();
await wssServer.start();
try {
await _runDartBrowserTest('test/browser_ws_dart_io_test.dart');
await _expectReceived('WS', wsReceived);
await _expectReceived('WSS', wssReceived);
} finally {
await Future.wait([
wssServer.stop(),
wsServer.stop(),
]);
}
}
void _configureWebClient(WsProtobufClient client, Completer<bool> received) {
client.addRequestListener<TestData, TestData>((req) async { client.addRequestListener<TestData, TestData>((req) async {
print('SERVER_RECEIVED index=${req.index} message=${req.message}'); print('SERVER_RECEIVED index=${req.index} message=${req.message}');
if (req.index == 101 && req.message == 'fire from dart web client') { if (req.index == 101 && req.message == 'fire from dart web client') {
@ -63,18 +98,14 @@ Future<void> _runWebWsSuite() async {
..index = req.index * 2 ..index = req.index * 2
..message = 'echo: ${req.message}'; ..message = 'echo: ${req.message}';
}); });
}); }
await server.start(); Future<void> _expectReceived(String transport, Completer<bool> received) async {
try {
await _runDartBrowserTest('test/browser_ws_dart_io_test.dart');
final ok = await received.future final ok = await received.future
.timeout(_serverObservationWindow, onTimeout: () => false); .timeout(_serverObservationWindow, onTimeout: () => false);
if (!ok) { if (!ok) {
throw StateError('WS web send-push server did not receive expected data'); throw StateError(
} '$transport web send-push server did not receive expected data');
} finally {
await server.stop();
} }
} }
@ -101,8 +132,8 @@ Future<void> _runDartBrowserTest(String testPath) async {
process.kill(ProcessSignal.sigkill); process.kill(ProcessSignal.sigkill);
return -1; return -1;
}); });
await Future.wait([stdoutDone, stderrDone]) await Future.wait([stdoutDone, stderrDone]).timeout(_streamTimeout,
.timeout(_streamTimeout, onTimeout: () { onTimeout: () {
throw StateError('dart browser test streams did not finish'); throw StateError('dart browser test streams did not finish');
}); });
if (code != 0) { if (code != 0) {

View file

@ -9,4 +9,10 @@ void main() {
port: 29098, port: 29098,
serverPushMessage: 'push from dart server', serverPushMessage: 'push from dart server',
); );
registerBrowserWsTests(
serverLabel: 'dart.io',
port: 29099,
serverPushMessage: 'push from dart server',
secure: true,
);
} }

View file

@ -9,4 +9,10 @@ void main() {
port: 29498, port: 29498,
serverPushMessage: 'push from kotlin server', serverPushMessage: 'push from kotlin server',
); );
registerBrowserWsTests(
serverLabel: 'kotlin',
port: 29499,
serverPushMessage: 'push from kotlin server',
secure: true,
);
} }

View file

@ -9,4 +9,10 @@ void main() {
port: 29698, port: 29698,
serverPushMessage: 'push from python server', serverPushMessage: 'push from python server',
); );
registerBrowserWsTests(
serverLabel: 'python',
port: 29699,
serverPushMessage: 'push from python server',
secure: true,
);
} }

View file

@ -20,10 +20,17 @@ void registerBrowserWsTests({
required String serverLabel, required String serverLabel,
required int port, required int port,
required String serverPushMessage, required String serverPushMessage,
bool secure = false,
}) { }) {
group('Dart Web WsProtobufClient talks to $serverLabel WS server', () { final transportLabel = secure ? 'WSS' : 'WS';
group(
'Dart Web WsProtobufClient talks to $serverLabel $transportLabel server',
() {
test('send-push: fire-and-forget and receive push', () async { test('send-push: fire-and-forget and receive push', () async {
final ws = await WsProtobufClient.connect(_host, port, path: _wsPath); final ws = secure
? await WsProtobufClient.connectSecure(_host, port, path: _wsPath)
: await WsProtobufClient.connect(_host, port, path: _wsPath);
final client = _BrowserWsClient(ws); final client = _BrowserWsClient(ws);
final pushCompleter = Completer<TestData>(); final pushCompleter = Completer<TestData>();
@ -46,7 +53,9 @@ void registerBrowserWsTests({
}); });
test('request-response: single and concurrent requests', () async { test('request-response: single and concurrent requests', () async {
final ws = await WsProtobufClient.connect(_host, port, path: _wsPath); final ws = secure
? await WsProtobufClient.connectSecure(_host, port, path: _wsPath)
: await WsProtobufClient.connect(_host, port, path: _wsPath);
final client = _BrowserWsClient(ws); final client = _BrowserWsClient(ws);
final single = await client final single = await client

View file

@ -9,4 +9,10 @@ void main() {
port: 29198, port: 29198,
serverPushMessage: 'push from go server', serverPushMessage: 'push from go server',
); );
registerBrowserWsTests(
serverLabel: 'go',
port: 29199,
serverPushMessage: 'push from go server',
secure: true,
);
} }

View file

@ -9,4 +9,10 @@ void main() {
port: 29898, port: 29898,
serverPushMessage: 'push from typescript server', serverPushMessage: 'push from typescript server',
); );
registerBrowserWsTests(
serverLabel: 'typescript',
port: 29899,
serverPushMessage: 'push from typescript server',
secure: true,
);
} }

View file

@ -5,6 +5,7 @@ package main
import ( import (
"bufio" "bufio"
"context" "context"
"crypto/tls"
"fmt" "fmt"
"io" "io"
"os" "os"
@ -24,6 +25,7 @@ import (
const ( const (
host = "127.0.0.1" host = "127.0.0.1"
goDartWebWSPort = 29198 goDartWebWSPort = 29198
goDartWebWSSPort = 29199
wsPath = "/" wsPath = "/"
processTimeout = 60 * time.Second processTimeout = 60 * time.Second
serverObservationWindow = 200 * time.Millisecond serverObservationWindow = 200 * time.Millisecond
@ -50,25 +52,75 @@ func main() {
fmt.Fprintf(os.Stderr, "FAIL crosstest error=%v\n", err) fmt.Fprintf(os.Stderr, "FAIL crosstest error=%v\n", err)
os.Exit(1) os.Exit(1)
} }
fmt.Println("PASS scenario=send-push") fmt.Println("PASS scenario=ws-send-push")
fmt.Println("PASS scenario=request-response") fmt.Println("PASS scenario=ws-request-response")
fmt.Println("PASS scenario=wss-send-push")
fmt.Println("PASS scenario=wss-request-response")
fmt.Println("PASS all go-server/dart-web-client crosstests passed") fmt.Println("PASS all go-server/dart-web-client crosstests passed")
} }
func run() error { func run() error {
return runWebWSSuite() serverTLS, err := loadWebServerTLS()
if err != nil {
return err
}
return runWebSuites(serverTLS)
} }
func runWebWSSuite() error { func loadWebServerTLS() (*tls.Config, error) {
dartDir, err := dartPackageDir()
if err != nil {
return nil, err
}
certFile := filepath.Join(dartDir, "test", "certs", "server.crt")
keyFile := filepath.Join(dartDir, "test", "certs", "server.key")
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
if err != nil {
return nil, err
}
return &tls.Config{Certificates: []tls.Certificate{cert}}, nil
}
func runWebSuites(serverTLS *tls.Config) error {
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
defer cancel() defer cancel()
received := make(chan bool, 1) wsReceived := make(chan bool, 1)
server := toki.NewWsServerWithOptions(host, goDartWebWSPort, wsPath, toki.WsServerOptions{ wssReceived := make(chan bool, 1)
wsServer := newWebServer(goDartWebWSPort, nil, wsReceived)
wssServer := newWebServer(goDartWebWSSPort, serverTLS, wssReceived)
if err := wsServer.Start(ctx); err != nil {
return err
}
defer wsServer.Stop()
if err := wssServer.Start(ctx); err != nil {
return err
}
defer wssServer.Stop()
if err := runDartBrowserTest(); err != nil {
return err
}
if err := expectReceived("WS", wsReceived); err != nil {
return err
}
return expectReceived("WSS", wssReceived)
}
func newWebServer(port int, tlsCfg *tls.Config, received chan<- bool) *toki.WsServer {
options := toki.WsServerOptions{
AcceptOptions: browserAcceptOptions(), AcceptOptions: browserAcceptOptions(),
}, func(conn *websocket.Conn) *toki.WsClient { }
newClient := func(conn *websocket.Conn) *toki.WsClient {
return toki.NewWsClient(conn, 0, 0, parserMap()) return toki.NewWsClient(conn, 0, 0, parserMap())
}) }
var server *toki.WsServer
if tlsCfg != nil {
server = toki.NewWsServerTLSWithOptions(host, port, wsPath, tlsCfg, options, newClient)
} else {
server = toki.NewWsServerWithOptions(host, port, wsPath, options, newClient)
}
server.OnClientConnected = func(client *toki.WsClient) { server.OnClientConnected = func(client *toki.WsClient) {
toki.AddRequestListenerTyped[*packets.TestData, *packets.TestData](&client.Communicator, func(req *packets.TestData) (*packets.TestData, error) { toki.AddRequestListenerTyped[*packets.TestData, *packets.TestData](&client.Communicator, func(req *packets.TestData) (*packets.TestData, error) {
fmt.Printf("SERVER_RECEIVED index=%d message=%s\n", req.GetIndex(), req.GetMessage()) fmt.Printf("SERVER_RECEIVED index=%d message=%s\n", req.GetIndex(), req.GetMessage())
@ -85,21 +137,17 @@ func runWebWSSuite() error {
}, nil }, nil
}) })
} }
if err := server.Start(ctx); err != nil { return server
return err }
}
defer server.Stop()
if err := runDartBrowserTest(); err != nil { func expectReceived(transport string, received <-chan bool) error {
return err
}
select { select {
case ok := <-received: case ok := <-received:
if !ok { if !ok {
return fmt.Errorf("WS web send-push server received unexpected data") return fmt.Errorf("%s web send-push server received unexpected data", transport)
} }
case <-time.After(serverObservationWindow): case <-time.After(serverObservationWindow):
return fmt.Errorf("WS web send-push server did not receive expected data") return fmt.Errorf("%s web send-push server did not receive expected data", transport)
} }
return nil return nil
} }

View file

@ -54,6 +54,12 @@ func NewWsServerWithOptions(host string, port int, path string, options WsServer
return s return s
} }
func NewWsServerTLSWithOptions(host string, port int, path string, tlsCfg *tls.Config, options WsServerOptions, newClient func(*websocket.Conn) *WsClient) *WsServer {
s := NewWsServerTLS(host, port, path, tlsCfg, newClient)
s.acceptOptions = options.AcceptOptions
return s
}
func (s *WsServer) Started() bool { func (s *WsServer) Started() bool {
s.mu.Lock() s.mu.Lock()
defer s.mu.Unlock() defer s.mu.Unlock()

View file

@ -12,16 +12,24 @@ import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeoutOrNull import kotlinx.coroutines.withTimeoutOrNull
import java.io.File 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 java.util.concurrent.TimeUnit
import javax.net.ssl.KeyManagerFactory
import javax.net.ssl.SSLContext
import kotlin.system.exitProcess import kotlin.system.exitProcess
private const val HOST = "127.0.0.1" private const val HOST = "127.0.0.1"
private const val WS_PORT = 29498 private const val WS_PORT = 29498
private const val WSS_PORT = 29499
private const val WS_PATH = "/" private const val WS_PATH = "/"
private const val PROCESS_TIMEOUT_MS = 60_000L private const val PROCESS_TIMEOUT_MS = 60_000L
private const val SERVER_OBSERVATION_MS = 500L private const val SERVER_OBSERVATION_MS = 500L
@ -30,9 +38,11 @@ private const val STREAM_JOIN_TIMEOUT_MS = 5_000L
fun main() = runBlocking { fun main() = runBlocking {
println("INFO typeName kotlin=${typeNameOf<TestData>()}") println("INFO typeName kotlin=${typeNameOf<TestData>()}")
try { try {
runWebWs() runWebSuites()
println("PASS scenario=send-push") println("PASS scenario=ws-send-push")
println("PASS scenario=request-response") 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") println("PASS all kotlin-server/dart-web-client crosstests passed")
} catch (error: Throwable) { } catch (error: Throwable) {
System.err.println("FAIL crosstest error=${error.message ?: error}") System.err.println("FAIL crosstest error=${error.message ?: error}")
@ -40,17 +50,42 @@ fun main() = runBlocking {
} }
} }
private suspend fun runWebWs() = coroutineScope { private suspend fun runWebSuites() = coroutineScope {
val received = CompletableDeferred<Boolean>() val wsReceived = CompletableDeferred<Boolean>()
val server = WsServer(HOST, WS_PORT, WS_PATH) { conn -> 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()) WsClient.forServer(conn, 0, 0, parserMap())
} }
server.onClientConnected = { client -> server.onClientConnected = { client ->
addRequestListenerTyped<TestData, TestData>(client.communicator) { req -> addRequestListenerTyped<TestData, TestData>(client.communicator) { req ->
println("SERVER_RECEIVED index=${req.index} message=${req.message}") println("SERVER_RECEIVED index=${req.index} message=${req.message}")
if (req.index == 101 && req.message == "fire from dart web client") { if (req.index == 101 && req.message == "fire from dart web client") {
received.complete(true) if (!received.isCompleted) received.complete(true)
launch { runBlocking {
client.send( client.send(
TestData.newBuilder() TestData.newBuilder()
.setIndex(200) .setIndex(200)
@ -65,15 +100,15 @@ private suspend fun runWebWs() = coroutineScope {
.build() .build()
} }
} }
server.start() return server
delay(100) }
try {
runDartBrowserTest("test/browser_ws_kotlin_test.dart") private suspend fun expectReceived(
transport: String,
received: CompletableDeferred<Boolean>,
) {
val ok = withTimeoutOrNull(SERVER_OBSERVATION_MS) { received.await() } ?: false val ok = withTimeoutOrNull(SERVER_OBSERVATION_MS) { received.await() } ?: false
check(ok) { "WS web send-push server did not receive expected data" } check(ok) { "$transport web send-push server did not receive expected data" }
} finally {
server.stop()
}
} }
private fun destroyProcessTree(process: Process) { private fun destroyProcessTree(process: Process) {
@ -112,6 +147,39 @@ private suspend fun runDartBrowserTest(testPath: String) = withContext(Dispatche
private fun parserMap(): ParserMap = private fun parserMap(): ParserMap =
mapOf(typeNameOf<TestData>() to { TestData.parseFrom(it) }) 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 { private fun dartDir(): File {
var dir = File(System.getProperty("user.dir")).absoluteFile var dir = File(System.getProperty("user.dir")).absoluteFile
while (dir.parentFile != null) { while (dir.parentFile != null) {

View file

@ -1,6 +1,7 @@
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
import ssl
import sys import sys
from pathlib import Path from pathlib import Path
@ -12,9 +13,12 @@ from proto_socket.ws_server import WsServer
HOST = "127.0.0.1" HOST = "127.0.0.1"
WS_PORT = 29698 WS_PORT = 29698
WSS_PORT = 29699
WS_PATH = "/" WS_PATH = "/"
PROCESS_TIMEOUT = 60.0 PROCESS_TIMEOUT = 60.0
SERVER_OBSERVATION_WINDOW = 0.5 SERVER_OBSERVATION_WINDOW = 0.5
CERT_PATH = Path(__file__).resolve().parents[1] / "test" / "certs" / "server.crt"
KEY_PATH = Path(__file__).resolve().parents[1] / "test" / "certs" / "server.key"
def parser_map(): def parser_map():
@ -24,18 +28,49 @@ def parser_map():
async def main() -> None: async def main() -> None:
print(f"INFO typeName python={type_name_of(TestData)}") print(f"INFO typeName python={type_name_of(TestData)}")
try: try:
await run_web_ws() await run_web_suites()
except Exception as exc: except Exception as exc:
print(f"FAIL crosstest error={exc}", file=sys.stderr) print(f"FAIL crosstest error={exc}", file=sys.stderr)
raise SystemExit(1) from exc raise SystemExit(1) from exc
print("PASS scenario=send-push") print("PASS scenario=ws-send-push")
print("PASS scenario=request-response") print("PASS scenario=ws-request-response")
print("PASS scenario=wss-send-push")
print("PASS scenario=wss-request-response")
print("PASS all python-server/dart-web-client crosstests passed") print("PASS all python-server/dart-web-client crosstests passed")
async def run_web_ws() -> None: async def run_web_suites() -> None:
received = asyncio.get_running_loop().create_future() loop = asyncio.get_running_loop()
server = WsServer(HOST, WS_PORT, WS_PATH, interval_sec=0, wait_sec=0, parser_map=parser_map()) ws_received = loop.create_future()
wss_received = loop.create_future()
ws_server = make_web_server(WS_PORT, None, ws_received)
wss_server = make_web_server(WSS_PORT, make_server_ssl_context(), wss_received)
await ws_server.start()
await wss_server.start()
try:
await run_dart_browser_test("test/browser_ws_python_test.dart")
await expect_received("WS", ws_received)
await expect_received("WSS", wss_received)
finally:
await wss_server.stop()
await ws_server.stop()
def make_web_server(
port: int,
ssl_context: ssl.SSLContext | None,
received: asyncio.Future[bool],
) -> WsServer:
server = WsServer(
HOST,
port,
WS_PATH,
interval_sec=0,
wait_sec=0,
parser_map=parser_map(),
ssl_context=ssl_context,
)
def on_connected(client): def on_connected(client):
def on_request(req): def on_request(req):
@ -53,14 +88,24 @@ async def run_web_ws() -> None:
client.communicator.add_request_listener(type_name_of(TestData), on_request) client.communicator.add_request_listener(type_name_of(TestData), on_request)
server.on_client_connected = on_connected server.on_client_connected = on_connected
await server.start() return server
def make_server_ssl_context() -> ssl.SSLContext:
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
ctx.load_cert_chain(CERT_PATH, KEY_PATH)
return ctx
async def expect_received(transport: str, received: asyncio.Future[bool]) -> None:
try: try:
await run_dart_browser_test("test/browser_ws_python_test.dart")
ok = await asyncio.wait_for(received, SERVER_OBSERVATION_WINDOW) ok = await asyncio.wait_for(received, SERVER_OBSERVATION_WINDOW)
except TimeoutError as exc:
raise RuntimeError(
f"{transport} web send-push server did not receive expected data"
) from exc
if not ok: if not ok:
raise RuntimeError("WS web send-push server received unexpected data") raise RuntimeError(f"{transport} web send-push server received unexpected data")
finally:
await server.stop()
async def run_dart_browser_test(test_path: str) -> None: async def run_dart_browser_test(test_path: str) -> None:

View file

@ -24,4 +24,4 @@
- [ ] Record exact commands used to run TypeScript checks and Go cross tests. - [ ] Record exact commands used to run TypeScript checks and Go cross tests.
- [ ] Record fixed ports `29490` and `29492`. - [ ] Record fixed ports `29490` and `29492`.
- [ ] Record TLS/WSS coverage status. Non-browser TLS/WSS is required; Dart.web WSS remains a required browser trust automation gap until self-signed test CA/certificate handling is added. - [ ] Record TLS/WSS coverage status. Non-browser TLS/WSS is required; Dart.web WSS is covered through the browser matrix with the test certificate SPKI Chrome wrapper.

View file

@ -1,4 +1,5 @@
import * as childProcess from "node:child_process"; import * as childProcess from "node:child_process";
import * as fs from "node:fs";
import * as path from "node:path"; import * as path from "node:path";
import * as readline from "node:readline"; import * as readline from "node:readline";
import { fileURLToPath } from "node:url"; import { fileURLToPath } from "node:url";
@ -14,9 +15,12 @@ import { NodeWsServer } from "../src/node_ws_server.js";
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const repoRoot = path.resolve(path.dirname(__filename), "../.."); const repoRoot = path.resolve(path.dirname(__filename), "../..");
const dartDir = path.join(repoRoot, "dart"); const dartDir = path.join(repoRoot, "dart");
const CERT_PATH = path.join(repoRoot, "typescript/test/certs/server.crt");
const KEY_PATH = path.join(repoRoot, "typescript/test/certs/server.key");
const HOST = "127.0.0.1"; const HOST = "127.0.0.1";
const WS_PORT = 29898; const WS_PORT = 29898;
const WSS_PORT = 29899;
const WS_PATH = "/"; const WS_PATH = "/";
const PROCESS_TIMEOUT_MS = 60_000; const PROCESS_TIMEOUT_MS = 60_000;
const STREAM_JOIN_TIMEOUT_MS = 5_000; const STREAM_JOIN_TIMEOUT_MS = 5_000;
@ -25,9 +29,11 @@ const SERVER_OBSERVATION_MS = 500;
async function main(): Promise<void> { async function main(): Promise<void> {
console.log(`INFO typeName ts=${TestDataSchema.typeName}`); console.log(`INFO typeName ts=${TestDataSchema.typeName}`);
try { try {
await runWebWs(); await runWebSuites();
console.log("PASS scenario=send-push"); console.log("PASS scenario=ws-send-push");
console.log("PASS scenario=request-response"); console.log("PASS scenario=ws-request-response");
console.log("PASS scenario=wss-send-push");
console.log("PASS scenario=wss-request-response");
console.log("PASS all typescript-server/dart-web-client crosstests passed"); console.log("PASS all typescript-server/dart-web-client crosstests passed");
} catch (err) { } catch (err) {
console.error(`FAIL crosstest error=${errorMessage(err)}`); console.error(`FAIL crosstest error=${errorMessage(err)}`);
@ -35,20 +41,40 @@ async function main(): Promise<void> {
} }
} }
async function runWebWs(): Promise<void> { async function runWebSuites(): Promise<void> {
let resolveReceived: ((value: boolean) => void) | null = null; const wsReceived = createReceivedSignal();
const received = new Promise<boolean>((resolve) => { const wssReceived = createReceivedSignal();
resolveReceived = resolve; const wsServer = createWebServer(WS_PORT, wsReceived.resolve);
}); const wssServer = createWebServer(WSS_PORT, wssReceived.resolve, serverTlsOptions());
const server = new NodeWsServer(HOST, WS_PORT, WS_PATH, (ws) => new NodeWsClient(ws, 0, 0, parserMap()));
await wsServer.start();
await wssServer.start();
try {
await runDartBrowserTest("test/browser_ws_typescript_test.dart");
const wsOk = await withTimeout(wsReceived.promise, SERVER_OBSERVATION_MS, "WS web send-push server did not receive expected data");
if (!wsOk) {
throw new Error("WS web send-push server received unexpected data");
}
const wssOk = await withTimeout(wssReceived.promise, SERVER_OBSERVATION_MS, "WSS web send-push server did not receive expected data");
if (!wssOk) {
throw new Error("WSS web send-push server received unexpected data");
}
} finally {
await Promise.allSettled([wssServer.stop(), wsServer.stop()]);
}
}
function createWebServer(
port: number,
resolveReceived: (value: boolean) => void,
tlsOptions?: { cert: Buffer; key: Buffer },
): NodeWsServer {
const server = new NodeWsServer(HOST, port, WS_PATH, (ws) => new NodeWsClient(ws, 0, 0, parserMap()), tlsOptions);
server.onClientConnected = (client) => { server.onClientConnected = (client) => {
addRequestListenerTyped(client.communicator, TestDataSchema, (req) => { addRequestListenerTyped(client.communicator, TestDataSchema, (req) => {
console.log(`SERVER_RECEIVED index=${req.index} message=${req.message}`); console.log(`SERVER_RECEIVED index=${req.index} message=${req.message}`);
if (req.index === 101 && req.message === "fire from dart web client") { if (req.index === 101 && req.message === "fire from dart web client") {
if (resolveReceived !== null) {
resolveReceived(true); resolveReceived(true);
resolveReceived = null;
}
void client.communicator.send( void client.communicator.send(
create(TestDataSchema, { create(TestDataSchema, {
index: 200, index: 200,
@ -62,16 +88,7 @@ async function runWebWs(): Promise<void> {
}); });
}); });
}; };
await server.start(); return server;
try {
await runDartBrowserTest("test/browser_ws_typescript_test.dart");
const ok = await withTimeout(received, SERVER_OBSERVATION_MS, "WS web send-push server did not receive expected data");
if (!ok) {
throw new Error("WS web send-push server received unexpected data");
}
} finally {
await server.stop();
}
} }
async function runDartBrowserTest(testPath: string): Promise<void> { async function runDartBrowserTest(testPath: string): Promise<void> {
@ -130,6 +147,33 @@ function parserMap() {
return new Map([[TestDataSchema.typeName, parserFromSchema(TestDataSchema)]]); return new Map([[TestDataSchema.typeName, parserFromSchema(TestDataSchema)]]);
} }
function createReceivedSignal(): {
promise: Promise<boolean>;
resolve: (value: boolean) => void;
} {
let done = false;
let resolvePromise: (value: boolean) => void;
const promise = new Promise<boolean>((resolve) => {
resolvePromise = resolve;
});
return {
promise,
resolve: (value: boolean) => {
if (!done) {
done = true;
resolvePromise(value);
}
},
};
}
function serverTlsOptions(): { cert: Buffer; key: Buffer } {
return {
cert: fs.readFileSync(CERT_PATH),
key: fs.readFileSync(KEY_PATH),
};
}
function killProcessTree(child: childProcess.ChildProcess): void { function killProcessTree(child: childProcess.ChildProcess): void {
if (process.platform !== "win32" && child.pid !== undefined) { if (process.platform !== "win32" && child.pid !== undefined) {
try { try {