Skip to content

Commit 49cac87

Browse files
claudeCyberShadow
authored andcommitted
ae.net.http.websocket: Add no_context_takeover integration tests
Add two integration test phases (3 and 4) that exercise the no_context_takeover code path with Python websockets configured to request server_no_context_takeover and client_no_context_takeover. Each test sends 3 messages through the echo loop. If the zlib stream reset is not happening between messages, the second message fails to decompress because the contexts are out of sync. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent af40364 commit 49cac87

2 files changed

Lines changed: 156 additions & 0 deletions

File tree

flake.nix

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -409,6 +409,80 @@ PYEOF
409409
echo "Python client test passed!"
410410
411411
# Wait for D server to exit (closes after client disconnects)
412+
wait $D_PID
413+
414+
echo "=== Phase 3: D client with Python server (no_context_takeover) ==="
415+
416+
# Start Python echo server with no_context_takeover
417+
python3 << 'PYEOF' &
418+
import asyncio, websockets
419+
from websockets.extensions.permessage_deflate import ServerPerMessageDeflateFactory
420+
async def echo(ws):
421+
async for msg in ws:
422+
await ws.send(msg)
423+
async def main():
424+
async with websockets.serve(
425+
echo, "127.0.0.1", 18768,
426+
extensions=[ServerPerMessageDeflateFactory(
427+
server_no_context_takeover=True,
428+
client_no_context_takeover=True,
429+
)],
430+
):
431+
await asyncio.Future()
432+
asyncio.run(main())
433+
PYEOF
434+
PY_PID=$!
435+
436+
for i in $(seq 1 30); do
437+
if python3 -c "import socket; s = socket.create_connection(('127.0.0.1', 18768), timeout=1); s.close()" 2>/dev/null; then
438+
break
439+
fi
440+
sleep 0.5
441+
done
442+
443+
echo "Python server (no_context_takeover) ready, running D client test..."
444+
WS_TEST_MODE=client_nctx WS_SERVER_PORT=18768 ws_test
445+
echo "D client no_context_takeover test passed!"
446+
447+
kill $PY_PID || true
448+
wait $PY_PID 2>/dev/null || true
449+
450+
echo "=== Phase 4: D server with Python client (no_context_takeover) ==="
451+
452+
WS_TEST_MODE=server_nctx WS_PORT=18767 WS_READY_FILE="$TMPDIR/ws_ready_nctx" ws_test &
453+
D_PID=$!
454+
455+
for i in $(seq 1 30); do
456+
if [ -f "$TMPDIR/ws_ready_nctx" ]; then
457+
break
458+
fi
459+
sleep 0.5
460+
done
461+
462+
echo "D server ready, running Python client (no_context_takeover) test..."
463+
python3 << 'PYEOF'
464+
import asyncio, websockets
465+
from websockets.extensions.permessage_deflate import ClientPerMessageDeflateFactory
466+
async def main():
467+
async with websockets.connect(
468+
"ws://127.0.0.1:18767",
469+
extensions=[ClientPerMessageDeflateFactory(
470+
server_no_context_takeover=True,
471+
client_no_context_takeover=True,
472+
)],
473+
) as ws:
474+
ext_names = [e.name for e in ws.protocol.extensions]
475+
assert "permessage-deflate" in ext_names, f"Expected permessage-deflate, got: {ext_names}"
476+
messages = ["Message one", "Message two", "Message three"]
477+
for msg in messages:
478+
await ws.send(msg)
479+
response = await ws.recv()
480+
assert response == msg.encode(), f"Expected {msg!r}, got: {response!r}"
481+
print("Python client: no_context_takeover echo verified!")
482+
asyncio.run(main())
483+
PYEOF
484+
echo "Python client no_context_takeover test passed!"
485+
412486
wait $D_PID
413487
echo "All WebSocket integration tests passed!"
414488
'';

net/http/websocket.d

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -985,3 +985,85 @@ debug(ae_unittest) unittest
985985
socketManager.loop();
986986
assert(ok);
987987
}
988+
989+
version (HAVE_WS_PEER)
990+
static if (haveZlib)
991+
debug(ae_unittest) unittest
992+
{
993+
// Integration test: D client connects to external echo server configured
994+
// with no_context_takeover. Multiple messages are sent to verify that
995+
// the zlib stream is properly reset between messages.
996+
import std.process : environment;
997+
if (environment.get("WS_TEST_MODE", "") != "client_nctx") return;
998+
999+
import ae.net.asockets : socketManager;
1000+
1001+
auto port = environment.get("WS_SERVER_PORT", "18765");
1002+
enum messages = ["Message one", "Message two", "Message three"];
1003+
int received;
1004+
1005+
bool ok;
1006+
connectWebSocket(
1007+
"ws://127.0.0.1:" ~ port ~ "/",
1008+
(WebSocketAdapter ws) {
1009+
assert(ws.deflateEnabled, "permessage-deflate was not negotiated");
1010+
ws.handleReadData = (Data data) {
1011+
assert(data.toGC() == messages[received], "Echo mismatch on message " ~ received.to!string);
1012+
received++;
1013+
if (received < messages.length)
1014+
ws.send(Data(messages[received].asBytes));
1015+
else
1016+
{
1017+
ok = true;
1018+
ws.disconnect("Test complete");
1019+
}
1020+
};
1021+
ws.send(Data(messages[0].asBytes));
1022+
},
1023+
(string error) {
1024+
assert(false, "WebSocket connection failed: " ~ error);
1025+
},
1026+
);
1027+
1028+
socketManager.loop();
1029+
assert(ok);
1030+
assert(received == messages.length);
1031+
}
1032+
1033+
version (HAVE_WS_PEER)
1034+
static if (haveZlib)
1035+
debug(ae_unittest) unittest
1036+
{
1037+
// Integration test: D server accepts connection from external client
1038+
// configured with no_context_takeover. Multiple messages verify that
1039+
// the zlib stream is properly reset between messages.
1040+
import std.process : environment;
1041+
if (environment.get("WS_TEST_MODE", "") != "server_nctx") return;
1042+
1043+
import ae.net.http.server : HttpServer;
1044+
import ae.net.asockets : socketManager;
1045+
import ae.sys.timing : setTimeout;
1046+
1047+
auto s = new HttpServer;
1048+
int received;
1049+
s.handleRequest = (HttpRequest request, HttpServerConnection serverConn) {
1050+
auto ws = accept(request, serverConn);
1051+
assert(ws.deflateEnabled, "permessage-deflate was not negotiated with client");
1052+
ws.handleReadData = (Data data) {
1053+
received++;
1054+
ws.send(data); // echo
1055+
};
1056+
ws.handleDisconnect = (string reason, DisconnectType type) {
1057+
setTimeout({ s.close(); }, 0.seconds);
1058+
};
1059+
};
1060+
auto port = environment.get("WS_PORT", "18767").to!ushort;
1061+
s.listen(port, "127.0.0.1");
1062+
1063+
// Signal readiness to the test harness
1064+
import std.file : fileWrite = write;
1065+
fileWrite(environment.get("WS_READY_FILE", "/tmp/ws_ready_nctx"), "ready");
1066+
1067+
socketManager.loop();
1068+
assert(received == 3, "Expected 3 messages, got " ~ received.to!string);
1069+
}

0 commit comments

Comments
 (0)