6
This commit is contained in:
@@ -0,0 +1,153 @@
|
||||
"""WebUI API 测试:状态快照、启动/中止/配置、会话列表与回放数据源。"""
|
||||
|
||||
import http.client
|
||||
import json
|
||||
import time
|
||||
|
||||
import pytest
|
||||
|
||||
from referee import Referee
|
||||
from referee.server import ActionInterfaceServer
|
||||
from referee.webui import WebUIServer
|
||||
|
||||
|
||||
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 ui(tmp_path):
|
||||
referee = Referee(action_port=0, data_dir=tmp_path / "data")
|
||||
ActionInterfaceServer(referee)
|
||||
server = WebUIServer(referee, 0)
|
||||
import threading
|
||||
threading.Thread(target=server.serve_forever, daemon=True).start()
|
||||
port = server.httpd.server_address[1]
|
||||
yield referee, port
|
||||
server.shutdown()
|
||||
|
||||
|
||||
def call(port, method, path, obj=None):
|
||||
conn = http.client.HTTPConnection("127.0.0.1", port, timeout=5)
|
||||
body = json.dumps(obj).encode("utf-8") if obj is not None else None
|
||||
conn.request(method, path, body=body,
|
||||
headers={"Content-Type": "application/json"})
|
||||
response = conn.getresponse()
|
||||
data = response.read()
|
||||
conn.close()
|
||||
return response.status, json.loads(data) if data else None
|
||||
|
||||
|
||||
def test_state_idle_snapshot(ui):
|
||||
referee, port = ui
|
||||
status, state = call(port, "GET", "/ui/api/state")
|
||||
assert status == 200
|
||||
assert state["phase"] == "idle"
|
||||
assert set(state["modules"]) == {"q3_practice", "q4_practice", "q3_formal", "q4_formal"}
|
||||
assert state["modules"]["q3_formal"]["attempts_left"] == 3
|
||||
assert state["spec"]["omega_radius_m"] == 1800
|
||||
|
||||
|
||||
def test_start_countdown_open_abort_cycle(ui):
|
||||
referee, port = ui
|
||||
status, body = call(port, "POST", "/ui/api/start",
|
||||
{"module": "q3_practice", "robot_id": "2026001",
|
||||
"countdown_s": 0.05, "window_s": 5})
|
||||
assert status == 200 and body["started"] is True
|
||||
assert wait_until(lambda: referee.session.phase == "open")
|
||||
status, state = call(port, "GET", "/ui/api/state")
|
||||
assert state["phase"] == "open" and state["robot_id"] == "2026001"
|
||||
assert "virtual_time_s" in state and "window_remaining_s" in state
|
||||
status, body = call(port, "POST", "/ui/api/abort", {})
|
||||
assert body["aborted"] is True
|
||||
status, state = call(port, "GET", "/ui/api/state")
|
||||
assert state["phase"] == "finished" and state["end_reason"] == "manual_abort"
|
||||
assert state["truth"]["revealed"] is True # 演练结束显示真值统计
|
||||
|
||||
|
||||
def test_start_rejects_running_session_and_bad_module(ui):
|
||||
referee, port = ui
|
||||
call(port, "POST", "/ui/api/start",
|
||||
{"module": "q3_practice", "robot_id": "2026001", "countdown_s": 5})
|
||||
status, body = call(port, "POST", "/ui/api/start",
|
||||
{"module": "q3_practice", "robot_id": "2026001"})
|
||||
assert status == 409
|
||||
status, body = call(port, "POST", "/ui/api/start",
|
||||
{"module": "nope", "robot_id": "2026001"})
|
||||
assert status == 409
|
||||
call(port, "POST", "/ui/api/abort", {})
|
||||
|
||||
|
||||
def test_config_endpoints(ui):
|
||||
referee, port = ui
|
||||
status, body = call(port, "POST", "/ui/api/config", {"display_limit": 100})
|
||||
assert body["configured"]["display_limit"] == 100
|
||||
status, body = call(port, "POST", "/ui/api/config", {"display_limit": 1})
|
||||
assert status == 409
|
||||
status, body = call(port, "POST", "/ui/api/config", {"action_port": 8080})
|
||||
assert body["configured"]["action_port"] == 8080
|
||||
|
||||
|
||||
def test_sessions_list_and_replay_source(ui):
|
||||
referee, port = ui
|
||||
call(port, "POST", "/ui/api/start",
|
||||
{"module": "q3_practice", "robot_id": "2026001",
|
||||
"countdown_s": 0.05, "window_s": 5, "seed": 11, "source_count": 10})
|
||||
assert wait_until(lambda: referee.session.phase == "open")
|
||||
session = referee.session
|
||||
from referee.interface import InterfaceResult
|
||||
from referee.jsonio import dumps
|
||||
referee.record_action(session, "POST", "/measure",
|
||||
{"request_id": "m1", "position": {"x": 5000, "y": 0},
|
||||
"channel": 1},
|
||||
InterfaceResult(200, dumps({"accepted": True,
|
||||
"virtual_time_s": 1005.0, "measure_result": "no_signal"}
|
||||
).encode(), {"accepted": True, "virtual_time_s": 1005.0,
|
||||
"measure_result": "no_signal"}))
|
||||
call(port, "POST", "/ui/api/abort", {})
|
||||
status, data = call(port, "GET", "/ui/api/sessions")
|
||||
assert status == 200 and len(data["sessions"]) == 1
|
||||
case = data["sessions"][0]["case_code"]
|
||||
assert data["sessions"][0]["robot_id"] == "2026001"
|
||||
status, replay = call(port, "GET", f"/ui/api/replay?case={case}")
|
||||
assert status == 200
|
||||
kinds = [event["event"] for event in replay["events"]]
|
||||
assert kinds[0] == "session_start" and kinds[-1] == "session_end"
|
||||
assert any(event["event"] == "action" for event in replay["events"])
|
||||
assert replay["meta"]["module"] == "q3_practice"
|
||||
assert len(replay["meta"]["truth"]) == 10 # 回放含真值(评测机侧)
|
||||
assert call(port, "GET", "/ui/api/replay?case=NOPE-CODE-0000")[0] == 404
|
||||
assert call(port, "GET", "/ui/api/replay?case=../etc")[0] == 400
|
||||
|
||||
|
||||
def test_static_page_served(ui):
|
||||
_, port = ui
|
||||
conn = http.client.HTTPConnection("127.0.0.1", port, timeout=5)
|
||||
conn.request("GET", "/")
|
||||
response = conn.getresponse()
|
||||
page = response.read().decode("utf-8")
|
||||
conn.close()
|
||||
assert response.status == 200
|
||||
assert "text/html" in response.getheader("Content-Type")
|
||||
assert "评测机" in page and "replay.js" in page
|
||||
# 回放器:手动选定 JSONL 的入口(文件选择按钮 + 拖拽容器)必须存在
|
||||
assert 'id="rp-file"' in page and 'id="rp-open-file"' in page
|
||||
assert 'id="rp-stage"' in page
|
||||
|
||||
|
||||
def test_replay_script_served_with_file_import(ui):
|
||||
_, port = ui
|
||||
conn = http.client.HTTPConnection("127.0.0.1", port, timeout=5)
|
||||
conn.request("GET", "/replay.js")
|
||||
response = conn.getresponse()
|
||||
script = response.read().decode("utf-8")
|
||||
conn.close()
|
||||
assert response.status == 200
|
||||
# 双格式归一化:评测机 action 事件 + 客户端 accepted 事件
|
||||
assert "normalizeEvents" in script and "loadFromText" in script
|
||||
assert 'ev.event === "accepted"' in script
|
||||
Reference in New Issue
Block a user