Vendor pinned T5 and T6 plugin sources under the new plugins layout. Add native HTTP deadlines, status and timing diagnostics, telemetry integration, and build/test tooling.
106 lines
5.0 KiB
Python
106 lines
5.0 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"):
|
|
url = path if "://" in path else base + path
|
|
completed = subprocess.run([str(executable), url, connect, total], 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.
|
|
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()
|