Skip to content

Commit 09934a9

Browse files
committed
ae.net.http.websocket: Add integration tests with Python websockets peer
Add two-phase integration test using Python websockets as an independent peer implementation: Phase 1: D client connects to Python echo server, verifying permessage-deflate is negotiated (ws.deflateEnabled assertion) and data round-trips correctly. Phase 2: D server accepts Python client, which verifies permessage-deflate appears in the negotiated extensions list and that echo works through the compressed channel. Both phases confirm compression is actually in use, not just that uncompressed echo works. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 7fb4f01 commit 09934a9

2 files changed

Lines changed: 190 additions & 1 deletion

File tree

flake.nix

Lines changed: 119 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,9 +116,44 @@
116116
'';
117117
};
118118

119+
# WebSocket test binary - needs HAVE_WS_PEER version flag and zlib
120+
websocket-test-bin = pkgs.stdenv.mkDerivation {
121+
name = "ae-websocket-test-bin";
122+
123+
nativeBuildInputs = [ pkgs.ldc pkgs.zlib ];
124+
dontStrip = true;
125+
126+
unpackPhase = ''
127+
cp -a ${self} ae
128+
'';
129+
130+
buildPhase = ''
131+
echo "Compiling WebSocket tests..."
132+
133+
# ASOCKETS_DEBUG_IDLE: DO NOT REMOVE - essential for detecting stuck event loops
134+
ldc2 \
135+
-i \
136+
-I. \
137+
-g \
138+
-d-debug=ae_unittest \
139+
-d-debug=ASOCKETS_DEBUG_IDLE \
140+
-d-version=HAVE_WS_PEER \
141+
-unittest \
142+
--main \
143+
-of=ws_test \
144+
-L=-lz \
145+
ae/net/http/websocket.d
146+
'';
147+
148+
installPhase = ''
149+
mkdir -p $out/bin
150+
cp ws_test $out/bin/
151+
'';
152+
};
153+
119154
in {
120155
packages = {
121-
inherit mysql-test-bin psql-test-bin;
156+
inherit mysql-test-bin psql-test-bin websocket-test-bin;
122157
};
123158

124159
checks = {
@@ -265,6 +300,89 @@
265300
touch $out
266301
'';
267302
};
303+
304+
# WebSocket integration tests (with Python websockets peer)
305+
websocket = pkgs.stdenv.mkDerivation {
306+
name = "ae-websocket-test";
307+
src = self;
308+
309+
nativeBuildInputs = [
310+
websocket-test-bin
311+
(pkgs.python3.withPackages (ps: [ ps.websockets ]))
312+
];
313+
314+
buildPhase = ''
315+
export HOME="$TMPDIR"
316+
317+
echo "=== Phase 1: D client with Python server ==="
318+
319+
# Start Python WebSocket echo server (permessage-deflate enabled by default)
320+
python3 << 'PYEOF' &
321+
import asyncio, websockets
322+
async def echo(ws):
323+
async for msg in ws:
324+
await ws.send(msg)
325+
async def main():
326+
async with websockets.serve(echo, "127.0.0.1", 18765):
327+
await asyncio.Future()
328+
asyncio.run(main())
329+
PYEOF
330+
PY_PID=$!
331+
332+
# Wait for Python server to be ready
333+
for i in $(seq 1 30); do
334+
if python3 -c "import socket; s = socket.create_connection(('127.0.0.1', 18765), timeout=1); s.close()" 2>/dev/null; then
335+
break
336+
fi
337+
sleep 0.5
338+
done
339+
340+
echo "Python server ready, running D client test..."
341+
WS_TEST_MODE=client WS_SERVER_PORT=18765 ws_test
342+
echo "D client test passed!"
343+
344+
kill $PY_PID || true
345+
wait $PY_PID 2>/dev/null || true
346+
347+
echo "=== Phase 2: D server with Python client ==="
348+
349+
# Start D WebSocket echo server
350+
WS_TEST_MODE=server WS_PORT=18766 WS_READY_FILE="$TMPDIR/ws_ready" ws_test &
351+
D_PID=$!
352+
353+
# Wait for D server to be ready
354+
for i in $(seq 1 30); do
355+
if [ -f "$TMPDIR/ws_ready" ]; then
356+
break
357+
fi
358+
sleep 0.5
359+
done
360+
361+
echo "D server ready, running Python client test..."
362+
python3 << 'PYEOF'
363+
import asyncio, websockets
364+
async def main():
365+
async with websockets.connect("ws://127.0.0.1:18766") as ws:
366+
# Verify permessage-deflate was negotiated
367+
ext_names = [e.name for e in ws.extensions]
368+
assert "permessage-deflate" in ext_names, f"Expected permessage-deflate, got: {ext_names}"
369+
await ws.send("Hello from Python client")
370+
response = await ws.recv()
371+
assert response == b"Hello from Python client", f"Expected echo, got: {response!r}"
372+
print("Python client: echo verified with compression!")
373+
asyncio.run(main())
374+
PYEOF
375+
echo "Python client test passed!"
376+
377+
# Wait for D server to exit (closes after client disconnects)
378+
wait $D_PID
379+
echo "All WebSocket integration tests passed!"
380+
'';
381+
382+
installPhase = ''
383+
touch $out
384+
'';
385+
};
268386
};
269387

270388
devShells.default = pkgs.mkShell {

net/http/websocket.d

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -830,6 +830,77 @@ debug(ae_unittest) unittest
830830
assert(ok);
831831
}
832832

833+
version (HAVE_WS_PEER)
834+
static if (haveZlib)
835+
debug(ae_unittest) unittest
836+
{
837+
// Integration test: D client connects to external WebSocket echo server.
838+
// The peer server must support permessage-deflate (RFC 7692).
839+
import std.process : environment;
840+
if (environment.get("WS_TEST_MODE", "") != "client") return;
841+
842+
import ae.net.asockets : socketManager;
843+
844+
auto port = environment.get("WS_SERVER_PORT", "18765");
845+
846+
bool ok;
847+
connectWebSocket(
848+
"ws://127.0.0.1:" ~ port ~ "/",
849+
(WebSocketAdapter ws) {
850+
assert(ws.deflateEnabled, "permessage-deflate was not negotiated with server");
851+
ws.handleReadData = (Data data) {
852+
assert(data.toGC() == "Hello from D client");
853+
ok = true;
854+
ws.disconnect("Test complete");
855+
};
856+
ws.send(Data("Hello from D client".asBytes));
857+
},
858+
(string error) {
859+
assert(false, "WebSocket connection failed: " ~ error);
860+
},
861+
);
862+
863+
socketManager.loop();
864+
assert(ok);
865+
}
866+
867+
version (HAVE_WS_PEER)
868+
static if (haveZlib)
869+
debug(ae_unittest) unittest
870+
{
871+
// Integration test: D server accepts connection from external WebSocket client.
872+
// The peer client must support permessage-deflate (RFC 7692).
873+
import std.process : environment;
874+
if (environment.get("WS_TEST_MODE", "") != "server") return;
875+
876+
import ae.net.http.server : HttpServer;
877+
import ae.net.asockets : socketManager;
878+
import ae.sys.timing : setTimeout;
879+
880+
auto s = new HttpServer;
881+
bool ok;
882+
s.handleRequest = (HttpRequest request, HttpServerConnection serverConn) {
883+
auto ws = accept(request, serverConn);
884+
assert(ws.deflateEnabled, "permessage-deflate was not negotiated with client");
885+
ws.handleReadData = (Data data) {
886+
ok = true;
887+
ws.send(data); // echo
888+
};
889+
ws.handleDisconnect = (string reason, DisconnectType type) {
890+
setTimeout({ s.close(); }, 0.seconds);
891+
};
892+
};
893+
auto port = environment.get("WS_PORT", "18766").to!ushort;
894+
s.listen(port, "127.0.0.1");
895+
896+
// Signal readiness to the test harness
897+
import std.file : fileWrite = write;
898+
fileWrite(environment.get("WS_READY_FILE", "/tmp/ws_ready"), "ready");
899+
900+
socketManager.loop();
901+
assert(ok);
902+
}
903+
833904
static if (haveZlib)
834905
debug(ae_unittest) unittest
835906
{

0 commit comments

Comments
 (0)