summaryrefslogtreecommitdiff
path: root/graph/test_transport.py
diff options
context:
space:
mode:
Diffstat (limited to 'graph/test_transport.py')
-rw-r--r--graph/test_transport.py97
1 files changed, 97 insertions, 0 deletions
diff --git a/graph/test_transport.py b/graph/test_transport.py
new file mode 100644
index 00000000..2d81a274
--- /dev/null
+++ b/graph/test_transport.py
@@ -0,0 +1,97 @@
+"""Check HTTP assets and a delayed, fragmented WebSocket upgrade.
+
+Build the chapter first, then run: python graph/test_transport.py chapter25
+Uses only the Python standard library; does not launch a browser.
+"""
+
+from pathlib import Path
+import socket
+import subprocess
+import sys
+import time
+import urllib.request
+
+
+def check(chapter):
+ root = Path(__file__).resolve().parent.parent
+ number = int(chapter.removeprefix("chapter"))
+ assert 18 <= number <= 25
+ chapter_dir = root / chapter
+ package = "com.seaofnodes.simple." + ("print." if number >= 20 else "")
+ log = root / "build" / "graph-transport" / (chapter + ".log")
+ log.parent.mkdir(parents=True, exist_ok=True)
+ with log.open("w") as output:
+ process = subprocess.Popen(
+ ["java", "-ea", "-Dsimple.graph.open=false", "-cp",
+ "build/classes/main", package + "JSViewer"],
+ cwd=chapter_dir, stdout=output, stderr=subprocess.STDOUT)
+ try:
+ deadline = time.monotonic() + 10
+ url = None
+ while time.monotonic() < deadline and process.poll() is None:
+ lines = log.read_text().splitlines()
+ addresses = [line.removeprefix("Graph viewer: ") for line in lines
+ if line.startswith("Graph viewer: ")]
+ if addresses:
+ url = addresses[0]
+ break
+ time.sleep(.05)
+ assert url and url.startswith("http://127.0.0.1:"), log.read_text()
+ # The browser must be able to load all assets before connecting.
+ for asset in ("index.html", "viewer.js", "vendor/d3.v7.min.js",
+ "vendor/d3-graphviz.js", "vendor/graphviz.umd.js"):
+ with urllib.request.urlopen(url.rsplit("/", 1)[0] + "/" + asset,
+ timeout=3) as response:
+ assert response.status == 200
+ assert response.read() == (root / "graph/web" / asset).read_bytes()
+ with socket.create_connection(("127.0.0.1", 12345), timeout=3) as client:
+ # Reproduces the old ready() race: accept precedes any headers.
+ time.sleep(.3)
+ client.sendall(b"GET / HTTP/1.1\r\nHost: localhost:12345\r\n")
+ time.sleep(.1)
+ client.sendall(b"Upgrade: websocket\r\nConnection: Upgrade\r\n"
+ b"Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n"
+ b"Sec-WebSocket-Version: 13\r\n")
+ # Even with the key present, the header block is not complete.
+ client.settimeout(.2)
+ try:
+ early = client.recv(1)
+ except socket.timeout:
+ pass
+ else:
+ raise AssertionError("Responded before headers ended: " + repr(early))
+ client.settimeout(3)
+ client.sendall(b"\r\n")
+ with client.makefile("rb") as stream:
+ assert stream.readline() == b"HTTP/1.1 101 Switching Protocols\r\n"
+ headers = []
+ while True:
+ line = stream.readline()
+ assert line, "Connection closed during upgrade"
+ if line == b"\r\n":
+ break
+ headers.append(line)
+ assert b"Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n" in headers
+ assert stream.read(3) == b"\x81\x01!", "Missing source request"
+ # Masked 'null' asks the compiler to exit normally.
+ mask = b"abcd"
+ payload = bytes(byte ^ mask[i % 4] for i, byte in enumerate(b"null"))
+ client.sendall(b"\x81\x84" + mask + payload)
+ assert stream.read(2) == b"\x88\x00", "Invalid close frame"
+ assert process.wait(timeout=5) == 0, log.read_text()
+ # The HTTP server must not outlive the compiler session.
+ try:
+ urllib.request.urlopen(url, timeout=.5)
+ except OSError:
+ pass
+ else:
+ raise AssertionError("HTTP server survived viewer shutdown")
+ finally:
+ if process.poll() is None:
+ process.terminate()
+ process.wait(timeout=5)
+ print(chapter + ": HTTP assets, fragmented upgrade, greeting, and shutdown passed")
+
+
+if __name__ == "__main__":
+ check(sys.argv[1] if len(sys.argv) > 1 else "chapter25")