1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
|
"""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", "layout.js", "render.js", "legacy.js",
"vendor/elk-api.js", "vendor/elk-worker.min.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")
|