134 lines
5.2 KiB
Python
134 lines
5.2 KiB
Python
"""真实 HTTP 承载测试:接口仅回环、仅窗口期开放、协议边界与官方一致。"""
|
|
|
|
# pylint: disable=redefined-outer-name # pytest 夹具名必须与测试函数形参同名,非遮蔽缺陷
|
|
|
|
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 local_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(request_id="e", **extra):
|
|
return dict(arena_id="default", robot_id="TEAM-0001", request_id=request_id, **extra)
|
|
|
|
|
|
def start_open_session(local_referee, **opts):
|
|
opts.setdefault("countdown_s", 0.05)
|
|
session = local_referee.start("q3_practice", "TEAM-0001", **opts)
|
|
assert wait_until(lambda: session.phase == "open")
|
|
return session
|
|
|
|
|
|
def test_binding_is_loopback_only(local_referee):
|
|
_session = start_open_session(local_referee)
|
|
server = local_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(local_referee):
|
|
session = local_referee.start("q3_practice", "TEAM-0001", countdown_s=5)
|
|
server = local_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(local_referee):
|
|
session = start_open_session(local_referee)
|
|
server = local_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(local_referee):
|
|
start_open_session(local_referee)
|
|
server = local_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(local_referee):
|
|
session = start_open_session(local_referee, window_s=0.4)
|
|
server = local_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(local_referee):
|
|
session = start_open_session(local_referee)
|
|
server = local_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
|