608 lines
25 KiB
Python
Executable File
608 lines
25 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Dependency-free maze32 WebSocket protocol and six-client verifier."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import base64
|
|
import hashlib
|
|
import http.client
|
|
import json
|
|
import os
|
|
import socket
|
|
import struct
|
|
import threading
|
|
import time
|
|
from typing import Any
|
|
|
|
MALFORMED_STRESS_MESSAGES = 80
|
|
|
|
|
|
class WebSocket:
|
|
def __init__(self, host: str, port: int = 80, timeout: float = 8.0):
|
|
self.host = host
|
|
self.sequence = 0
|
|
self.socket = socket.create_connection((host, port), timeout=timeout)
|
|
self.socket.settimeout(timeout)
|
|
key = base64.b64encode(os.urandom(16)).decode("ascii")
|
|
request = (
|
|
f"GET /ws HTTP/1.1\r\nHost: {host}:{port}\r\nUpgrade: websocket\r\n"
|
|
f"Connection: Upgrade\r\nSec-WebSocket-Key: {key}\r\n"
|
|
"Sec-WebSocket-Version: 13\r\n\r\n"
|
|
)
|
|
self.socket.sendall(request.encode("ascii"))
|
|
response = self._receive_until(b"\r\n\r\n")
|
|
if not response.startswith(b"HTTP/1.1 101"):
|
|
self.socket.close()
|
|
raise RuntimeError(response.decode("utf-8", "replace").strip())
|
|
expected = base64.b64encode(
|
|
hashlib.sha1((key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11").encode()).digest()
|
|
)
|
|
if expected not in response:
|
|
self.socket.close()
|
|
raise RuntimeError("WebSocket accept key did not match")
|
|
|
|
def _receive_until(self, marker: bytes) -> bytes:
|
|
data = bytearray()
|
|
while marker not in data:
|
|
chunk = self.socket.recv(1024)
|
|
if not chunk:
|
|
raise ConnectionError("connection closed")
|
|
data.extend(chunk)
|
|
return bytes(data)
|
|
|
|
def send(self, message_type: str, payload: dict[str, Any], request_id: str) -> None:
|
|
self.sequence += 1
|
|
message = json.dumps(
|
|
{"v": 1, "type": message_type, "seq": self.sequence,
|
|
"requestId": request_id, "payload": payload},
|
|
ensure_ascii=False,
|
|
separators=(",", ":"),
|
|
).encode("utf-8")
|
|
self.send_raw(message)
|
|
|
|
def send_raw(self, message: bytes) -> None:
|
|
mask = os.urandom(4)
|
|
length = len(message)
|
|
if length < 126:
|
|
header = bytes((0x81, 0x80 | length))
|
|
else:
|
|
header = bytes((0x81, 0x80 | 126)) + struct.pack("!H", length)
|
|
masked = bytes(value ^ mask[index % 4] for index, value in enumerate(message))
|
|
self.socket.sendall(header + mask + masked)
|
|
|
|
def receive(self) -> dict[str, Any]:
|
|
first, second = self._read_exact(2)
|
|
opcode = first & 0x0F
|
|
length = second & 0x7F
|
|
if length == 126:
|
|
length = struct.unpack("!H", self._read_exact(2))[0]
|
|
elif length == 127:
|
|
length = struct.unpack("!Q", self._read_exact(8))[0]
|
|
if second & 0x80:
|
|
mask = self._read_exact(4)
|
|
else:
|
|
mask = None
|
|
payload = self._read_exact(length)
|
|
if mask:
|
|
payload = bytes(value ^ mask[index % 4] for index, value in enumerate(payload))
|
|
if opcode == 0x8:
|
|
raise ConnectionError("WebSocket closed")
|
|
if opcode != 0x1:
|
|
return self.receive()
|
|
return json.loads(payload.decode("utf-8"))
|
|
|
|
def receive_type(self, message_type: str, attempts: int = 20) -> dict[str, Any]:
|
|
for _ in range(attempts):
|
|
message = self.receive()
|
|
if message.get("type") == message_type:
|
|
return message
|
|
raise RuntimeError(f"did not receive {message_type}")
|
|
|
|
def _read_exact(self, length: int) -> bytes:
|
|
data = bytearray()
|
|
while len(data) < length:
|
|
chunk = self.socket.recv(length - len(data))
|
|
if not chunk:
|
|
raise ConnectionError("connection closed")
|
|
data.extend(chunk)
|
|
return bytes(data)
|
|
|
|
def close(self) -> None:
|
|
try:
|
|
self.socket.sendall(b"\x88\x80" + b"\0\0\0\0")
|
|
except OSError:
|
|
pass
|
|
self.socket.close()
|
|
|
|
|
|
def join(client: WebSocket, role: str, name: str | None = None) -> dict[str, Any]:
|
|
payload = {"role": role}
|
|
if name is not None:
|
|
payload["name"] = name
|
|
client.send("join", payload, f"join-{role}")
|
|
return client.receive_type("session")["payload"]
|
|
|
|
|
|
def receive_view(client: WebSocket, maximum_messages: int = 800) -> dict[str, Any]:
|
|
parts: dict[int, dict[str, Any]] = {}
|
|
sequence: int | None = None
|
|
expected = 0
|
|
for _ in range(maximum_messages):
|
|
message = client.receive()
|
|
if message.get("type") != "view":
|
|
continue
|
|
payload = message["payload"]
|
|
if sequence != message["seq"]:
|
|
sequence = message["seq"]
|
|
expected = payload["chunks"]
|
|
parts = {}
|
|
parts[payload["chunk"]] = payload
|
|
if len(parts) == expected:
|
|
first = dict(parts[0])
|
|
first["cells"] = [cell for index in range(expected) for cell in parts[index]["cells"]]
|
|
first["objects"] = [
|
|
item for index in range(expected) for item in parts[index]["objects"]
|
|
]
|
|
return first
|
|
raise RuntimeError("did not receive a complete view")
|
|
|
|
|
|
def open_direction(view: dict[str, Any]) -> str:
|
|
passages = next(cell[1] for cell in view["cells"] if cell[0] == view["self"])
|
|
for bit, direction in ((1, "north"), (2, "east"), (4, "south"), (8, "west")):
|
|
if passages & bit:
|
|
return direction
|
|
raise AssertionError("player cell has no open passage")
|
|
|
|
|
|
def diagnostics(host: str) -> dict[str, Any]:
|
|
connection = http.client.HTTPConnection(host, 80, timeout=8.0)
|
|
try:
|
|
connection.request("GET", "/diagnostics")
|
|
response = connection.getresponse()
|
|
payload = response.read()
|
|
if response.status != 200:
|
|
raise RuntimeError(f"diagnostics returned HTTP {response.status}")
|
|
return json.loads(payload.decode("utf-8"))
|
|
finally:
|
|
connection.close()
|
|
|
|
|
|
def verify_m010(host: str, stress_moves: int, repeat_matches: int) -> None:
|
|
clients: list[WebSocket] = []
|
|
try:
|
|
baseline = diagnostics(host)
|
|
player_1 = WebSocket(host, timeout=30.0)
|
|
player_2 = WebSocket(host, timeout=30.0)
|
|
clients.extend((player_1, player_2))
|
|
join(player_1, "player", "Нагрузочный Один")
|
|
session_2 = join(player_2, "player", "Нагрузочный Два")
|
|
observers = [WebSocket(host, timeout=120.0) for _ in range(4)]
|
|
clients.extend(observers)
|
|
for observer in observers:
|
|
join(observer, "observer")
|
|
|
|
observer_views: list[dict[str, Any] | None] = [None] * len(observers)
|
|
failures: list[BaseException] = []
|
|
|
|
def read_observer(index: int) -> None:
|
|
try:
|
|
observers[index].receive_type("active", attempts=50)
|
|
observer_views[index] = receive_view(observers[index])
|
|
except BaseException as error: # Preserve worker failure for the main thread.
|
|
failures.append(error)
|
|
|
|
readers = [threading.Thread(target=read_observer, args=(index,))
|
|
for index in range(len(observers))]
|
|
for reader in readers:
|
|
reader.start()
|
|
player_views: list[dict[str, Any] | None] = [None, None]
|
|
player_failures: list[BaseException] = []
|
|
|
|
def read_player(index: int, client: WebSocket) -> None:
|
|
try:
|
|
client.receive_type("active", attempts=50)
|
|
player_views[index] = receive_view(client)
|
|
except BaseException as error: # Preserve worker failure for the main thread.
|
|
player_failures.append(error)
|
|
|
|
player_readers = [
|
|
threading.Thread(target=read_player, args=(0, player_1)),
|
|
threading.Thread(target=read_player, args=(1, player_2)),
|
|
]
|
|
for reader in player_readers:
|
|
reader.start()
|
|
player_1.send("start", {}, "m010-start")
|
|
for reader in player_readers:
|
|
reader.join(timeout=180.0)
|
|
for reader in readers:
|
|
reader.join(timeout=180.0)
|
|
assert not player_failures and all(not reader.is_alive() for reader in player_readers), (
|
|
f"player read failures: {player_failures!r}"
|
|
)
|
|
assert not failures and all(not reader.is_alive() for reader in readers), (
|
|
f"observer read failures: {failures!r}"
|
|
)
|
|
player_1_view = player_views[0]
|
|
assert player_1_view is not None and player_views[1] is not None
|
|
assert all(view is not None and len(view["cells"]) == 8192
|
|
for view in observer_views)
|
|
|
|
six_client_sample = diagnostics(host)
|
|
limits = six_client_sample["limits"]
|
|
assert six_client_sample["activeConnections"] == 6
|
|
assert six_client_sample["connectionHighWater"] == 6
|
|
assert six_client_sample["queueHighWater"] <= limits["queueEntries"]
|
|
print("M010 six-client load: PASS (2 players + 4 complete observers)")
|
|
|
|
observers[0].send("restart", {}, "m010-observer-restart")
|
|
observer_error = observers[0].receive_type("error", attempts=30)
|
|
assert observer_error["payload"]["code"] == "read_only"
|
|
|
|
heartbeat_latencies_ms: list[float] = []
|
|
for index, client in enumerate(clients):
|
|
started = time.monotonic()
|
|
client.send("heartbeat", {}, f"m010-heartbeat-{index}")
|
|
client.receive_type("ack", attempts=30)
|
|
heartbeat_latencies_ms.append((time.monotonic() - started) * 1000.0)
|
|
assert max(heartbeat_latencies_ms) <= limits["playerLatencyBudgetMs"]
|
|
print(f"M010 six-client responsiveness: PASS (max {max(heartbeat_latencies_ms):.1f} ms)")
|
|
|
|
slow_observer = observers[0]
|
|
slow_observer.socket.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 1024)
|
|
drain_stop = threading.Event()
|
|
drain_failures: list[BaseException] = []
|
|
|
|
def drain_fast_observer(observer: WebSocket) -> None:
|
|
observer.socket.settimeout(1.0)
|
|
while not drain_stop.is_set():
|
|
try:
|
|
observer.receive()
|
|
except socket.timeout:
|
|
continue
|
|
except BaseException as error: # Preserve an unexpected disconnect.
|
|
drain_failures.append(error)
|
|
return
|
|
|
|
drainers = [threading.Thread(target=drain_fast_observer, args=(observer,))
|
|
for observer in observers[1:]]
|
|
for drainer in drainers:
|
|
drainer.start()
|
|
|
|
opposite = {"north": "south", "east": "west", "south": "north", "west": "east"}
|
|
direction = open_direction(player_1_view)
|
|
command_latencies_ms: list[float] = []
|
|
for move_index in range(stress_moves):
|
|
time.sleep(0.16)
|
|
started = time.monotonic()
|
|
player_1.send("move", {"direction": direction}, f"m010-move-{move_index}")
|
|
player_1.receive_type("ack", attempts=50)
|
|
command_latencies_ms.append((time.monotonic() - started) * 1000.0)
|
|
player_1_view = receive_view(player_1)
|
|
receive_view(player_2)
|
|
direction = opposite[direction]
|
|
|
|
drain_stop.set()
|
|
for drainer in drainers:
|
|
drainer.join(timeout=3.0)
|
|
assert not drain_failures and all(not drainer.is_alive() for drainer in drainers), (
|
|
f"observer drain failures: {drain_failures!r}"
|
|
)
|
|
for observer in observers[1:]:
|
|
observer.socket.settimeout(30.0)
|
|
|
|
abusive_observer = observers[1]
|
|
|
|
def malformed_flood() -> None:
|
|
try:
|
|
for _ in range(MALFORMED_STRESS_MESSAGES):
|
|
abusive_observer.send_raw(b"{")
|
|
except OSError:
|
|
pass
|
|
|
|
flood = threading.Thread(target=malformed_flood)
|
|
flood.start()
|
|
stress_heartbeat_latencies_ms: list[float] = []
|
|
for index in range(10):
|
|
started = time.monotonic()
|
|
player_1.send("heartbeat", {}, f"m010-stress-heartbeat-{index}")
|
|
player_1.receive_type("ack", attempts=50)
|
|
stress_heartbeat_latencies_ms.append((time.monotonic() - started) * 1000.0)
|
|
time.sleep(0.02)
|
|
flood.join(timeout=10.0)
|
|
assert not flood.is_alive()
|
|
stressed = diagnostics(host)
|
|
maximum_latency_ms = max(command_latencies_ms + stress_heartbeat_latencies_ms)
|
|
assert maximum_latency_ms <= limits["playerLatencyBudgetMs"], (
|
|
f"maximum player latency {maximum_latency_ms:.1f} ms"
|
|
)
|
|
assert stressed["malformedMessages"] > baseline["malformedMessages"]
|
|
assert stressed["rateLimitedMessages"] > baseline["rateLimitedMessages"]
|
|
assert stressed["queueHighWater"] <= limits["queueEntries"]
|
|
assert stressed["minimumInternalFreeBytes"] > 32768
|
|
assert stressed["minimumInternalLargestBlockBytes"] > 16384
|
|
print(
|
|
"M010 slow/malformed stress: PASS "
|
|
f"(max player latency {maximum_latency_ms:.1f} ms, "
|
|
f"dropped {stressed['messagesDropped'] - baseline['messagesDropped']})"
|
|
)
|
|
|
|
for observer in observers:
|
|
observer.close()
|
|
if observer in clients:
|
|
clients.remove(observer)
|
|
observers.clear()
|
|
time.sleep(1.0)
|
|
|
|
reset_heap_samples: list[int] = []
|
|
for match_index in range(repeat_matches):
|
|
token_2 = session_2["token"]
|
|
player_2.close()
|
|
clients.remove(player_2)
|
|
player_1.receive_type("pause", attempts=100)
|
|
player_1.socket.settimeout(70.0)
|
|
result = player_1.receive_type("match_result", attempts=200)["payload"]
|
|
assert result["result"] == "draw"
|
|
|
|
player_2 = WebSocket(host, timeout=30.0)
|
|
clients.append(player_2)
|
|
player_2.send("restore", {"token": token_2}, f"m010-restore-{match_index}")
|
|
session_2 = player_2.receive_type("session", attempts=50)["payload"]
|
|
player_1.send("restart", {}, f"m010-restart-{match_index}")
|
|
player_1.receive_type("ack", attempts=100)
|
|
time.sleep(0.2)
|
|
sample = diagnostics(host)
|
|
reset_heap_samples.append(sample["internalFreeBytes"])
|
|
if match_index + 1 < repeat_matches:
|
|
player_views = [None, None]
|
|
player_failures = []
|
|
player_readers = [
|
|
threading.Thread(target=read_player, args=(0, player_1)),
|
|
threading.Thread(target=read_player, args=(1, player_2)),
|
|
]
|
|
for reader in player_readers:
|
|
reader.start()
|
|
player_1.send("start", {}, f"m010-start-{match_index + 1}")
|
|
for reader in player_readers:
|
|
reader.join(timeout=40.0)
|
|
assert not player_failures and all(
|
|
not reader.is_alive() for reader in player_readers
|
|
)
|
|
|
|
final_sample = diagnostics(host)
|
|
assert final_sample["matchesCompleted"] - baseline["matchesCompleted"] >= repeat_matches
|
|
assert final_sample["matchesReset"] - baseline["matchesReset"] >= repeat_matches
|
|
if len(reset_heap_samples) > 1:
|
|
assert max(reset_heap_samples) - min(reset_heap_samples) <= 32768
|
|
print(
|
|
f"M010 repeated-match stability: PASS ({repeat_matches} matches, "
|
|
f"heap range {max(reset_heap_samples) - min(reset_heap_samples)} bytes)"
|
|
)
|
|
print(json.dumps(final_sample, ensure_ascii=False, sort_keys=True))
|
|
finally:
|
|
for client in clients:
|
|
client.close()
|
|
|
|
|
|
def verify_m009(host: str) -> None:
|
|
clients: list[WebSocket] = []
|
|
try:
|
|
player_1 = WebSocket(host)
|
|
player_2 = WebSocket(host)
|
|
clients.extend((player_1, player_2))
|
|
join(player_1, "player", "Наблюдаемый Один")
|
|
session_2 = join(player_2, "player", "Наблюдаемый Два")
|
|
observers = [WebSocket(host) for _ in range(4)]
|
|
clients.extend(observers)
|
|
for observer in observers:
|
|
join(observer, "observer")
|
|
observer.socket.settimeout(30.0)
|
|
|
|
observer_views: list[dict[str, Any] | None] = [None] * 4
|
|
failures: list[BaseException] = []
|
|
|
|
def read_observer(index: int) -> None:
|
|
try:
|
|
observers[index].receive_type("active", attempts=40)
|
|
observer_views[index] = receive_view(observers[index])
|
|
except BaseException as error: # Preserve worker failure for the main thread.
|
|
failures.append(error)
|
|
|
|
readers = [threading.Thread(target=read_observer, args=(index,)) for index in range(4)]
|
|
for reader in readers:
|
|
reader.start()
|
|
player_1.send("start", {}, "m009-start")
|
|
player_1.receive_type("countdown")
|
|
player_2.receive_type("countdown")
|
|
player_1.receive_type("active")
|
|
player_2.receive_type("active")
|
|
player_1_view = receive_view(player_1)
|
|
player_2_view = receive_view(player_2)
|
|
for reader in readers:
|
|
reader.join(timeout=35.0)
|
|
assert not failures and all(not reader.is_alive() for reader in readers)
|
|
assert all(view is not None and len(view["cells"]) == 8192 for view in observer_views)
|
|
print("four-observer full-map delivery: PASS (8192 cells each)")
|
|
|
|
observers[0].send("move", {"direction": "north"}, "observer-move")
|
|
error = observers[0].receive_type("error")
|
|
assert error["payload"]["code"] == "read_only"
|
|
print("observer mutation authorization: PASS (read_only)")
|
|
|
|
direction = open_direction(player_1_view)
|
|
player_1.send("move", {"direction": direction}, "before-observer-close")
|
|
player_1.receive_type("ack")
|
|
player_1_view = receive_view(player_1)
|
|
observers[-1].close()
|
|
clients.remove(observers[-1])
|
|
observers.pop()
|
|
time.sleep(0.2)
|
|
player_1.send("move", {"direction": open_direction(player_1_view)},
|
|
"after-observer-close")
|
|
player_1.receive_type("ack")
|
|
print("observer disconnect continuity: PASS")
|
|
|
|
preserved_cell = player_2_view["self"]
|
|
preserved_inventory = player_2_view.get("inventory")
|
|
token_2 = session_2["token"]
|
|
player_2.close()
|
|
clients.remove(player_2)
|
|
pause = player_1.receive_type("pause", attempts=30)["payload"]
|
|
assert pause["slot"] == 2
|
|
player_1.send("move", {"direction": "north"}, "paused-move")
|
|
error = player_1.receive_type("error", attempts=30)
|
|
assert error["payload"]["code"] == "premature"
|
|
|
|
restored = WebSocket(host)
|
|
clients.append(restored)
|
|
restored.send("restore", {"token": token_2}, "restore-player-2")
|
|
restored_session = restored.receive_type("session")["payload"]
|
|
assert restored_session["slot"] == 2 and restored_session["token"] == token_2
|
|
player_1.receive_type("resume", attempts=30)
|
|
restored_view = receive_view(restored)
|
|
assert restored_view["self"] == preserved_cell
|
|
assert restored_view.get("inventory") == preserved_inventory
|
|
print("player pause and reconnect: PASS (same position and inventory)")
|
|
|
|
restored.close()
|
|
clients.remove(restored)
|
|
player_1.socket.settimeout(70.0)
|
|
started = time.monotonic()
|
|
result = player_1.receive_type("match_result", attempts=60)["payload"]
|
|
elapsed = time.monotonic() - started
|
|
assert result["result"] == "draw" and elapsed >= 59.0
|
|
print(f"reconnect timeout draw: PASS ({elapsed:.1f} seconds)")
|
|
finally:
|
|
for client in clients:
|
|
client.close()
|
|
|
|
|
|
def verify(host: str) -> None:
|
|
clients: list[WebSocket] = []
|
|
try:
|
|
player_1 = WebSocket(host)
|
|
clients.append(player_1)
|
|
player_1.send_raw(b"{")
|
|
error = player_1.receive_type("error")
|
|
assert error["payload"]["code"] == "malformed"
|
|
session_1 = join(player_1, "player", "Тестовый Пилот")
|
|
player_1.send_raw(
|
|
b'{"v":1,"type":"start","seq":1,"requestId":"stale","payload":{}}'
|
|
)
|
|
error = player_1.receive_type("error")
|
|
assert error["payload"]["code"] == "stale_sequence"
|
|
print("malformed and stale message rejection: PASS")
|
|
duplicate = WebSocket(host)
|
|
clients.append(duplicate)
|
|
duplicate.send("join", {"role": "player", "name": "Тестовый Пилот"}, "duplicate")
|
|
error = duplicate.receive_type("error")
|
|
assert error["payload"]["code"] == "duplicate_name"
|
|
duplicate.send("join", {"role": "player", "name": "Запасной Пилот"}, "player-2")
|
|
session_2 = duplicate.receive_type("session")["payload"]
|
|
assert session_1["slot"] == 1 and session_2["slot"] == 2
|
|
|
|
observers = []
|
|
for _ in range(4):
|
|
observer = WebSocket(host)
|
|
clients.append(observer)
|
|
join(observer, "observer")
|
|
observers.append(observer)
|
|
lobby = observers[-1].receive_type("lobby")["payload"]
|
|
assert lobby["ready"] and lobby["observers"] == 4
|
|
print("six-client capacity: PASS (2 players + 4 observers)")
|
|
print("duplicate player name: PASS")
|
|
|
|
try:
|
|
excess = WebSocket(host, timeout=1.0)
|
|
except (OSError, RuntimeError, ConnectionError):
|
|
print("seventh connection rejection: PASS")
|
|
else:
|
|
try:
|
|
excess.send("join", {"role": "observer"}, "excess")
|
|
excess.receive()
|
|
except (OSError, ConnectionError):
|
|
print("seventh connection rejection: PASS")
|
|
else:
|
|
raise AssertionError("seventh WebSocket unexpectedly remained connected")
|
|
finally:
|
|
excess.close()
|
|
time.sleep(0.5)
|
|
|
|
duplicate.send("start", {}, "not-owner")
|
|
error = duplicate.receive_type("error")
|
|
assert error["payload"]["code"] == "not_owner"
|
|
print("Player 2 start authorization: PASS")
|
|
|
|
token_2 = session_2["token"]
|
|
duplicate.close()
|
|
clients.remove(duplicate)
|
|
time.sleep(0.15)
|
|
restored = WebSocket(host)
|
|
clients.append(restored)
|
|
restored.send("restore", {"token": token_2}, "restore")
|
|
restored_session = restored.receive_type("session")["payload"]
|
|
assert restored_session["slot"] == 2 and restored_session["token"] == token_2
|
|
print("session refresh restoration: PASS (Player 2 retained slot 2)")
|
|
|
|
player_1.send("start", {}, "owner-start")
|
|
countdown_1 = player_1.receive_type("countdown")["payload"]
|
|
countdown_2 = restored.receive_type("countdown")["payload"]
|
|
assert countdown_1 == countdown_2
|
|
assert countdown_1["steps"] == [3, 2, 1]
|
|
active_1 = player_1.receive_type("active")["payload"]
|
|
active_2 = restored.receive_type("active")["payload"]
|
|
assert active_1["activatedAtMs"] == countdown_1["activationAtMs"]
|
|
assert active_1 == active_2
|
|
print(
|
|
"authoritative countdown: PASS "
|
|
f"(activationAtMs={countdown_1['activationAtMs']})"
|
|
)
|
|
|
|
observer = observers.pop()
|
|
observer.close()
|
|
clients.remove(observer)
|
|
time.sleep(0.5)
|
|
oversized = WebSocket(host)
|
|
oversized.send_raw(b"x" * 513)
|
|
try:
|
|
error = oversized.receive_type("error")
|
|
assert error["payload"]["code"] == "too_large"
|
|
except (OSError, ConnectionError):
|
|
pass
|
|
print("oversized message rejection: PASS")
|
|
oversized.close()
|
|
finally:
|
|
for client in clients:
|
|
client.close()
|
|
|
|
|
|
def main() -> None:
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("host", help="maze32 IP address or hostname")
|
|
parser.add_argument("--verify", action="store_true", help="run the Milestone 004 scenario")
|
|
parser.add_argument("--verify-m009", action="store_true", help="run the Milestone 009 scenario")
|
|
parser.add_argument("--verify-m010", action="store_true", help="run the Milestone 010 load and stability scenario")
|
|
parser.add_argument("--stress-moves", type=int, default=80,
|
|
help="accepted moves sent while one observer is not reading")
|
|
parser.add_argument("--repeat-matches", type=int, default=2,
|
|
help="real 60-second timeout matches completed and reset")
|
|
arguments = parser.parse_args()
|
|
if arguments.stress_moves < 1 or arguments.repeat_matches < 1:
|
|
parser.error("--stress-moves and --repeat-matches must be positive")
|
|
if arguments.verify_m010:
|
|
verify_m010(arguments.host, arguments.stress_moves, arguments.repeat_matches)
|
|
elif arguments.verify_m009:
|
|
verify_m009(arguments.host)
|
|
elif arguments.verify:
|
|
verify(arguments.host)
|
|
else:
|
|
client = WebSocket(arguments.host)
|
|
client.send("suggest_name", {}, "manual")
|
|
print(json.dumps(client.receive(), ensure_ascii=False, indent=2))
|
|
client.close()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|