Files

111 lines
5.4 KiB
Python

#!/usr/bin/env python3
"""Compile both real HTTP utilities and exercise native deadlines on loopback."""
import http.server
import json
import os
from pathlib import Path
import socketserver
import subprocess
import tempfile
import threading
import time
ROOT = Path(__file__).resolve().parents[1]
class Handler(http.server.BaseHTTPRequestHandler):
def log_message(self, *_):
pass
def do_POST(self):
body = self.rfile.read(int(self.headers.get("Content-Length", 0)))
assert body == b'{"event":"heartbeat"}'
assert self.headers.get("Content-Type") == "application/json"
assert self.headers.get("Authorization") == "Bearer test-only"
if self.path == "/stall":
time.sleep(2)
return
if self.path == "/partial":
self.send_response(200)
self.send_header("Content-Length", "100")
self.end_headers()
self.wfile.write(b"{")
self.wfile.flush()
time.sleep(2)
return
if self.path == "/slow":
time.sleep(0.1)
status = int(self.path[1:]) if self.path[1:].isdigit() else 200
payload = b'{"ok":true}' if status == 200 else b'{"ok":false}'
self.send_response(status)
self.send_header("Content-Length", str(len(payload)))
self.end_headers()
self.wfile.write(payload)
class StallTLS(socketserver.BaseRequestHandler):
def handle(self):
# Accept TCP but never finish TLS: the connection deadline must fire.
time.sleep(6)
class Server(socketserver.ThreadingTCPServer):
daemon_threads = True
def main():
with Server(("127.0.0.1", 0), Handler) as http, Server(("127.0.0.1", 0), StallTLS) as tls, tempfile.TemporaryDirectory(prefix="tws-http-tests-") as temp:
for server in (http, tls):
threading.Thread(target=server.serve_forever, daemon=True).start()
try:
for game in ("t5", "t6"):
source = ROOT / game / "plugins" / f"tws-{game}-gsc-utils" / "src" / "utils"
executable = Path(temp) / game
subprocess.run([os.environ.get("CXX", "c++"), "-std=c++20", "-Wall", "-Wextra", "-Werror", "-I", str(source),
str(ROOT / "tools/tests/http_probe.cpp"), str(source / "http.cpp"), "-lcurl", "-o", str(executable)], check=True)
base = f"http://127.0.0.1:{http.server_address[1]}"
env = {**os.environ, "NO_PROXY": "*", "no_proxy": "*"}
def probe(path, connect="1000", total="1000", cap=None):
url = path if "://" in path else base + path
completed = subprocess.run([str(executable), url, connect, total] + ([] if cap is None else [str(cap)]), env=env, capture_output=True, text=True, check=True, timeout=8)
return json.loads(completed.stdout)
normal = probe("/200", "-", "-")
assert normal["curl_code"] == 0 and normal["http_status"] == 200 and normal["body_size"] == 11, normal
assert 0 <= normal["dns_ms"] <= normal["connect_ms"] <= normal["first_byte_ms"] <= normal["total_ms"], normal
for status in (401, 429, 500):
rejected = probe(f"/{status}")
assert rejected["curl_code"] == 0 and rejected["http_status"] == status and rejected["body_size"] == 12, rejected
for path, status in (("/stall", 0), ("/partial", 200)):
timed_out = probe(path, total="200")
assert timed_out["curl_code"] == 28 and timed_out["http_status"] == status, timed_out
assert 150 <= timed_out["total_ms"] < 1500 and timed_out["wall_ms"] < 1500, timed_out
assert timed_out["body_size"] == 0, timed_out
if status: assert timed_out["first_byte_ms"] > 0, timed_out
assert probe("/200")["curl_code"] == 0 # The next request completes after the stall.
exact = probe("/200", cap=11)
assert exact["curl_code"] == 0 and exact["body_size"] == 11, exact
bounded = probe("/200", cap=10)
assert bounded["curl_code"] == 23 and bounded["body_size"] == 0, bounded
assert probe("/200")["body_size"] == 11
slow = probe("/slow")
assert slow["curl_code"] == 0 and slow["first_byte_ms"] >= 90, slow
tls_url = f"https://127.0.0.1:{tls.server_address[1]}/"
stalled_connect = probe(tls_url, connect="150", total="1000")
assert stalled_connect["curl_code"] == 28 and 100 <= stalled_connect["total_ms"] < 800, stalled_connect
# Total timeout must also bound connection setup when it is shorter.
total_connect = probe(tls_url, connect="1000", total="150")
assert total_connect["curl_code"] == 28 and 100 <= total_connect["total_ms"] < 800, total_connect
for connect, total in (("0", "100"), ("100", "0"), ("-1", "100")):
invalid = probe("/200", connect, total)
assert invalid["curl_code"] == 43 and invalid["http_status"] == 0, invalid
print(f"PASS {game}: POST body/headers, HTTP errors, stalled/partial responses, recovery, TLS/connect and total deadlines, invalid limits", flush=True)
finally:
http.shutdown()
tls.shutdown()
if __name__ == "__main__":
main()