Organize scripts and add TWS plugins with HTTP timeouts
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.
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user