"""真实 HTTP 承载测试:接口仅回环、仅窗口期开放、协议边界与官方一致。""" import http.client import json import time import pytest from referee import Referee from referee.server import ActionInterfaceServer def wait_until(condition, timeout=5.0, interval=0.02): deadline = time.monotonic() + timeout while time.monotonic() < deadline: if condition(): return True time.sleep(interval) return False @pytest.fixture() def referee(tmp_path): ref = Referee(action_port=0, data_dir=tmp_path / "data") ActionInterfaceServer(ref) return ref def open_port(server): assert server._httpd is not None return server._httpd.server_address[1] def post(port, path, obj=None, *, headers=None, method="POST", raw=None): conn = http.client.HTTPConnection("127.0.0.1", port, timeout=5) body = raw if raw is not None else json.dumps(obj or {}).encode("utf-8") conn.request(method, path, body=body, headers=headers or {"Content-Type": "application/json"}) response = conn.getresponse() data = response.read() conn.close() parsed = json.loads(data) if data else None return response.status, parsed def payload(id="e", **extra): return dict(arena_id="default", robot_id="2026001", request_id=id, **extra) def start_open_session(referee, **opts): opts.setdefault("countdown_s", 0.05) session = referee.start("q3_practice", "2026001", **opts) assert wait_until(lambda: session.phase == "open") return session def test_binding_is_loopback_only(referee): session = start_open_session(referee) server = referee._open_interface.__self__ # ActionInterfaceServer port = open_port(server) assert server._httpd.server_address[0] == "127.0.0.1" status, body = post(port, "/enter", payload("enter")) assert status == 200 and body["accepted"] is True assert body["remaining_real_duration_s"] == 1200 def test_interface_refuses_connections_outside_window(referee): session = referee.start("q3_practice", "2026001", countdown_s=5) server = referee._open_interface.__self__ assert server.is_open is False # 倒计时期间未监听 assert wait_until(lambda: session.phase == "open", timeout=6) assert server.is_open is True # 窗口开启后才绑定端口 def test_full_protocol_round_trip_over_http(referee): session = start_open_session(referee) server = referee._open_interface.__self__ port = open_port(server) status, body = post(port, "/enter", payload("enter-1")) assert status == 200 and body["accepted"] is True assert body["virtual_time_s"] == 0 # 远点检测:目标域内源 r_eff<=1500,必然 no_signal status, body = post(port, "/measure", payload( "m-1", position=dict(x=5000, y=0), channel=1)) assert status == 200 and body["measure_result"] == "no_signal" assert body["virtual_time_s"] == 1005 # 移动 1000s + 检测 5s status, body = post(port, "/clear", payload( "c-1", position=dict(x=5000, y=0), channel=2)) assert body["clear_result"] == "no_target_in_range" assert body["virtual_time_s"] == 1008 # 未发现 3s status, body = post(port, "/exit", payload("exit-1")) assert body["exit_reason"] == "user_exit" assert wait_until(lambda: session.phase == "finished") assert session.end_reason == "user_exit" def test_protocol_error_statuses_over_http(referee): start_open_session(referee) server = referee._open_interface.__self__ port = open_port(server) assert post(port, "/unknown", payload())[0] == 404 assert post(port, "/enter/", payload())[0] == 404 assert post(port, "/enter", payload(), method="GET")[0] == 405 assert post(port, "/enter", payload(), headers={"Content-Type": "text/plain"})[0] == 415 assert post(port, "/enter", raw=b'{"request_id":"a","request_id":"b"}')[0] == 400 assert post(port, "/enter", raw=b"{}" + b" " * 65535)[0] == 413 status, body = post(port, "/enter", payload("real")) assert status == 200 and body["accepted"] is True def test_interface_closes_after_window_timeout(referee): session = start_open_session(referee, window_s=0.4) server = referee._open_interface.__self__ port = open_port(server) assert wait_until(lambda: session.phase == "finished", timeout=5) assert session.end_reason == "window_timeout" assert wait_until(lambda: server._httpd is None) with pytest.raises(OSError): post(port, "/enter", payload("late")) def test_recorded_feed_contains_every_request(referee): session = start_open_session(referee) server = referee._open_interface.__self__ port = open_port(server) post(port, "/enter", payload("enter-1")) post(port, "/unknown", payload("bad")) assert wait_until(lambda: session.request_count >= 2) paths = [(entry["path"], entry["status"]) for entry in session.feed] assert ("/enter", 200) in paths and ("/unknown", 404) in paths