6
This commit is contained in:
@@ -19,9 +19,10 @@ from mcm26b.config import ProblemConfig
|
||||
from mcm26b.evaluation import recompute_virtual_time
|
||||
from mcm26b.planner.bayes_loop import BayesLoop
|
||||
from mcm26b.protocol.client import SimulatorClient
|
||||
from mcm26b.sim import (LocalSimTransport, LocalSimulator,
|
||||
ScriptedFaultTransport, Source)
|
||||
from mcm26b.sim.scenes import random_omni_scene
|
||||
from mcm26b.harness import (LocalSimTransport, LocalSimulator,
|
||||
ScriptedFaultTransport)
|
||||
from referee import Source
|
||||
from mcm26b.harness import random_omni_scene
|
||||
|
||||
CONFIG = replace(ProblemConfig(),
|
||||
search=replace(ProblemConfig().search,
|
||||
@@ -147,8 +148,8 @@ class BayesLoopAcceptanceTests(unittest.TestCase):
|
||||
client = SimulatorClient("T001", config=CONFIG,
|
||||
transport=LocalSimTransport(simulator))
|
||||
loop = loop_cls(client, CONFIG)
|
||||
expected = seven_points() if name in ("bayes", "seven") \
|
||||
else coverage_points(CONFIG)
|
||||
expected = seven_points(CONFIG.bayes.station_ring_radius_m) \
|
||||
if name in ("bayes", "seven") else coverage_points(CONFIG)
|
||||
self.assertEqual(loop.ledger.points, expected)
|
||||
|
||||
|
||||
|
||||
@@ -22,9 +22,10 @@ from mcm26b.coverage import coverage_points
|
||||
from mcm26b.evaluation import recompute_virtual_time
|
||||
from mcm26b.planner import OmnidirectionalLoop
|
||||
from mcm26b.protocol.client import SimulatorClient
|
||||
from mcm26b.sim import (LocalSimTransport, LocalSimulator,
|
||||
ScriptedFaultTransport, Source)
|
||||
from mcm26b.sim.scenes import random_omni_scene
|
||||
from mcm26b.harness import (LocalSimTransport, LocalSimulator,
|
||||
ScriptedFaultTransport)
|
||||
from referee import Source
|
||||
from mcm26b.harness import random_omni_scene
|
||||
|
||||
CONFIG = replace(ProblemConfig(),
|
||||
search=replace(ProblemConfig().search,
|
||||
@@ -216,7 +217,7 @@ class SimulatorVirtualCapTests(unittest.TestCase):
|
||||
simulator = LocalSimulator([], robot_id="T001", config=CONFIG)
|
||||
simulator.entered = True
|
||||
simulator.virtual_time_s = CONFIG.field.max_virtual_duration_s
|
||||
from mcm26b.sim import SimulatorClosed
|
||||
from referee import SimulatorClosed
|
||||
with self.assertRaises(SimulatorClosed):
|
||||
simulator.handle(
|
||||
"/measure", {"arena_id": "default", "robot_id": "T001",
|
||||
|
||||
@@ -20,8 +20,9 @@ from mcm26b.planner import (DirectionalExtension, fallback_cell_bound,
|
||||
fallback_clear_cells, safe_disk_radius)
|
||||
from mcm26b.protocol.client import SimulatorClient
|
||||
from mcm26b.protocol.rules import DIRECTION, NO_SIGNAL
|
||||
from mcm26b.sim import LocalSimTransport, LocalSimulator, Source
|
||||
from mcm26b.sim.scenes import mixed_scene
|
||||
from mcm26b.harness import LocalSimTransport, LocalSimulator
|
||||
from referee import Source
|
||||
from mcm26b.harness import mixed_scene
|
||||
|
||||
CONFIG = replace(ProblemConfig(),
|
||||
search=replace(ProblemConfig().search,
|
||||
|
||||
@@ -13,7 +13,7 @@ from mcm26b.planner import (_worst_posterior_radius, bearing_deg,
|
||||
clear_certificate, select_measurement_point,
|
||||
select_second_point)
|
||||
from mcm26b.protocol.client import SimulatorClient
|
||||
from mcm26b.sim import LocalSimTransport, LocalSimulator
|
||||
from mcm26b.harness import LocalSimTransport, LocalSimulator
|
||||
|
||||
CONFIG = replace(ProblemConfig(),
|
||||
search=replace(ProblemConfig().search,
|
||||
|
||||
@@ -20,8 +20,9 @@ from mcm26b.protocol.client import (DeadlineExceeded, ProtocolError,
|
||||
SimulatorClient)
|
||||
from mcm26b.protocol.log import SessionLog
|
||||
from mcm26b.protocol.transport import TransportError
|
||||
from mcm26b.sim import (LocalSimTransport, LocalSimulator,
|
||||
PlaybackTransport, ScriptedFaultTransport, Source)
|
||||
from mcm26b.harness import (LocalSimTransport, LocalSimulator,
|
||||
PlaybackTransport, ScriptedFaultTransport)
|
||||
from referee import Source
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ from mcm26b.config import CostConfig
|
||||
from mcm26b.protocol.rules import CLEAR_NO_TARGET
|
||||
from mcm26b.protocol.state import RobotState, StateError
|
||||
from mcm26b.protocol.timing import clear_cost, measure_cost
|
||||
from mcm26b.sim import LocalSimTransport, LocalSimulator
|
||||
from mcm26b.harness import LocalSimTransport, LocalSimulator
|
||||
from mcm26b.protocol.client import ActionRejected, SimulatorClient
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
"""解耦守卫:评测机(referee)与参赛队实现(mcm26b)互不依赖。
|
||||
|
||||
双方只通过附件2 定义的 HTTP 协议耦合;本测试固化三条边界:
|
||||
1. 运行时零依赖——导入参赛队全部运行时模块不会拉起 referee;
|
||||
2. 源码零引用——referee 包内不出现任何 mcm26b 字样;
|
||||
3. 规则不漂移——两侧独立持有的题面常量、字段表、计时模型必须一致
|
||||
(105/111/194/199 锚点)。
|
||||
"""
|
||||
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from mcm26b.config import ProblemConfig
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
REFEREE_SRC = ROOT / "referee" / "src" / "referee"
|
||||
IMPORT_LINE = re.compile(r"^\s*(from\s+|import\s+)mcm26b\b", re.MULTILINE)
|
||||
|
||||
|
||||
def test_runtime_code_never_imports_referee():
|
||||
code = (
|
||||
"import sys\n"
|
||||
"import mcm26b, mcm26b.cli, mcm26b.config\n"
|
||||
"import mcm26b.protocol, mcm26b.geometry, mcm26b.belief\n"
|
||||
"import mcm26b.coverage, mcm26b.planner, mcm26b.evaluation\n"
|
||||
"assert 'referee' not in sys.modules, '运行时代码不得导入 referee'\n"
|
||||
"print('clean')\n"
|
||||
)
|
||||
result = subprocess.run([sys.executable, "-c", code], capture_output=True,
|
||||
text=True, cwd=ROOT)
|
||||
assert result.returncode == 0, result.stderr
|
||||
assert "clean" in result.stdout
|
||||
|
||||
|
||||
def test_referee_source_has_no_mcm26b_import():
|
||||
offenders = [str(path) for path in REFEREE_SRC.rglob("*.py")
|
||||
if IMPORT_LINE.search(path.read_text(encoding="utf-8"))]
|
||||
assert offenders == [], f"referee 包不得依赖参赛队代码: {offenders}"
|
||||
|
||||
|
||||
def test_spec_constants_do_not_drift():
|
||||
from referee import CostSpec as RCost, FieldSpec as RField
|
||||
field, cost = ProblemConfig().field, ProblemConfig().cost
|
||||
rf, rc = RField(), RCost()
|
||||
pairs = [
|
||||
(field.omega_radius_m, rf.omega_radius_m),
|
||||
(field.channel_min, rf.channel_min), (field.channel_max, rf.channel_max),
|
||||
(field.source_count_min, rf.source_count_min),
|
||||
(field.source_count_max, rf.source_count_max),
|
||||
(field.r_eff_min_m, rf.r_eff_min_m), (field.r_eff_max_m, rf.r_eff_max_m),
|
||||
(field.near_radius_m, rf.near_radius_m),
|
||||
(field.clear_radius_m, rf.clear_radius_m),
|
||||
(field.bearing_error_half_deg, rf.bearing_error_half_deg),
|
||||
(field.bearing_decimals, rf.bearing_decimals),
|
||||
(field.coord_abs_limit_m, rf.coord_abs_limit_m),
|
||||
(field.directional_half_angle_deg, rf.directional_half_angle_deg),
|
||||
(field.max_virtual_duration_s, rf.max_virtual_duration_s),
|
||||
(field.max_real_duration_s, rf.max_real_duration_s),
|
||||
(field.test_window_s, rf.test_window_s),
|
||||
(cost.speed_mps, rc.speed_mps), (cost.measure_s, rc.measure_s),
|
||||
(cost.switch_s, rc.switch_s),
|
||||
(cost.clear_success_s, rc.clear_success_s),
|
||||
(cost.clear_fail_s, rc.clear_fail_s),
|
||||
]
|
||||
drifted = [(a, b) for a, b in pairs if a != b]
|
||||
assert drifted == [], f"题面常量漂移: {drifted}"
|
||||
|
||||
|
||||
def test_rules_tables_do_not_drift():
|
||||
from mcm26b.protocol import rules as client_rules
|
||||
from referee import rules as server_rules
|
||||
assert set(client_rules.ACTION_FIELDS) == set(server_rules.ACTION_FIELDS)
|
||||
assert set(client_rules.COMMON_FIELDS) == set(server_rules.COMMON_FIELDS)
|
||||
assert set(client_rules.POSITION_FIELDS) == set(server_rules.POSITION_FIELDS)
|
||||
assert client_rules.MEASURE_RESULTS == server_rules.MEASURE_RESULTS
|
||||
assert client_rules.CLEAR_RESULTS == server_rules.CLEAR_RESULTS
|
||||
assert client_rules.EXIT_USER == server_rules.EXIT_USER
|
||||
assert client_rules.BASE_RESPONSE_FIELDS == server_rules.BASE_RESPONSE_FIELDS
|
||||
assert client_rules.STATUS_OK == server_rules.STATUS_OK
|
||||
assert client_rules.STATUS_BAD_REQUEST == server_rules.STATUS_BAD_REQUEST
|
||||
assert client_rules.STATUS_CONFLICT == server_rules.STATUS_CONFLICT
|
||||
|
||||
|
||||
def test_timing_models_agree_on_attachment_example():
|
||||
"""同一指令序列在两侧计时模型下总耗时一致(附件示例 105/111/194/199)。"""
|
||||
from mcm26b.protocol.timing import clear_cost as client_clear
|
||||
from mcm26b.protocol.timing import measure_cost as client_measure
|
||||
from referee.timing import clear_cost as server_clear
|
||||
from referee.timing import measure_cost as server_measure
|
||||
|
||||
steps = [
|
||||
("m", (0.0, 0.0), (300.0, 400.0), 1, 1),
|
||||
("m", (300.0, 400.0), (300.0, 400.0), 1, 2),
|
||||
("c", (300.0, 400.0), (300.0, 0.0), None, None),
|
||||
("m", (300.0, 0.0), (300.0, 0.0), 2, 2),
|
||||
]
|
||||
expected = [105, 6, 83, 5] # 单步耗时(累计 105/111/194/199)
|
||||
for step, total in zip(steps, expected):
|
||||
if step[0] == "m":
|
||||
_, prev, pos, prev_ch, ch = step
|
||||
client = client_measure(prev, pos, prev_ch, ch)
|
||||
server = server_measure(prev, pos, prev_ch, ch)
|
||||
else:
|
||||
_, prev, pos, _, _ = step
|
||||
client = client_clear(prev, pos, success=False)
|
||||
server = server_clear(prev, pos, success=False)
|
||||
assert client.total_us == server.total_us
|
||||
assert client.total_s == total
|
||||
@@ -1,200 +0,0 @@
|
||||
"""审核报告的独立反例:时限、原始协议、并发和量化。"""
|
||||
import json
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from dataclasses import replace
|
||||
from threading import Event
|
||||
|
||||
import pytest
|
||||
|
||||
from mcm26b.config import ProblemConfig
|
||||
from mcm26b.sim import LocalSimulator, LocalSimTransport, SimulatorClosed, Source
|
||||
|
||||
|
||||
def payload(id="e", **extra):
|
||||
return dict(arena_id="default", robot_id="2026001", request_id=id, **extra)
|
||||
|
||||
|
||||
def measure(id="m", x=0, y=0):
|
||||
return payload(id, position=dict(x=x, y=y), channel=1)
|
||||
|
||||
|
||||
def test_late_enter_and_deadline():
|
||||
now = [1400.25]
|
||||
sim = LocalSimulator([], clock=lambda: now[0], wall_clock=lambda: 1234.5,
|
||||
window_started_at=0)
|
||||
response = sim.handle("/enter", payload())[1]
|
||||
assert response["remaining_real_duration_s"] == 99
|
||||
assert response["real_timestamp_ms"] == 1234500
|
||||
now[0] = 1500
|
||||
for path, body in [("/measure", measure()), ("/exit", payload("x"))]:
|
||||
with pytest.raises(SimulatorClosed):
|
||||
sim.handle(path, body)
|
||||
assert sim.virtual_time_s == 0
|
||||
|
||||
|
||||
def test_program_deadline_and_subsecond_enter():
|
||||
now = [100]
|
||||
sim = LocalSimulator([], clock=lambda: now[0], window_started_at=0)
|
||||
assert sim.handle("/enter", payload())[1]["remaining_real_duration_s"] == 1200
|
||||
now[0] = 1300
|
||||
with pytest.raises(SimulatorClosed):
|
||||
sim.handle("/measure", measure())
|
||||
late = LocalSimulator([], clock=lambda: 1499.5, window_started_at=0)
|
||||
assert late.handle("/enter", payload())[1]["remaining_real_duration_s"] == 0
|
||||
expired = LocalSimulator([], clock=lambda: 1500, window_started_at=0)
|
||||
with pytest.raises(SimulatorClosed):
|
||||
expired.handle("/enter", payload())
|
||||
|
||||
|
||||
def test_stress_mode_is_explicit():
|
||||
config = ProblemConfig()
|
||||
config = replace(config, field=replace(config.field, max_real_duration_s=1800))
|
||||
official = LocalSimulator([], config=config, clock=lambda: 0)
|
||||
stress = LocalSimulator([], config=config, clock=lambda: 0, stress_mode=True)
|
||||
assert official.handle("/enter", payload())[1]["remaining_real_duration_s"] == 1200
|
||||
assert stress.handle("/enter", payload())[1]["remaining_real_duration_s"] == 1800
|
||||
|
||||
|
||||
def test_virtual_crossing_action_finishes_then_connection_closes():
|
||||
sim = LocalSimulator([])
|
||||
sim.handle("/enter", payload())
|
||||
assert sim.handle("/measure", measure(x=1800000))[1]["virtual_time_s"] == 360005
|
||||
for path, body in [("/measure", measure("m2")), ("/exit", payload("x"))]:
|
||||
with pytest.raises(SimulatorClosed):
|
||||
sim.handle(path, body)
|
||||
assert not sim.exited
|
||||
|
||||
|
||||
def test_microseconds_accumulate_and_signed_zero_is_same_location():
|
||||
sim = LocalSimulator([Source(1, 1000, 0, 1500)])
|
||||
sim.handle("/enter", payload())
|
||||
a = sim.handle("/measure", measure("a", 0.0))[1]
|
||||
b = sim.handle("/measure", measure("b", -0.0))[1]
|
||||
assert a["svd_deg"] == b["svd_deg"]
|
||||
for i in range(100):
|
||||
point = 1 if i % 2 == 0 else 0
|
||||
sim.handle("/measure", measure(str(i), point, point))
|
||||
assert sim.virtual_time_s == 10 + 100 * 5282843 / 1000000
|
||||
|
||||
|
||||
@pytest.mark.parametrize("bad", ["e\n", "e\u200b", "e\u0085", "e\ud800"])
|
||||
def test_identifier_rejection_does_not_consume_id(bad):
|
||||
sim = LocalSimulator([])
|
||||
assert sim.handle("/enter", payload(bad))[0] == 400
|
||||
assert sim._idempotency == {}
|
||||
assert sim.handle("/enter", payload())[1]["accepted"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("body,status", [
|
||||
(b'{"request_id":"a","request_id":"b"}', 400),
|
||||
(b'{"position":{"x":1,"x":2}}', 400),
|
||||
(b'{}' + b' ' * 65535, 413),
|
||||
(b'\xef\xbb\xbf{}', 400), (b'\xff', 400), (b'{', 400),
|
||||
(b'[]', 400), (b'{"x":NaN}', 400), (b'{"x":Infinity}', 400),
|
||||
(b'{"a":' + b'[' * 16 + b'0' + b']' * 16 + b'}', 400),
|
||||
], ids=["duplicate-root", "duplicate-nested", "oversize", "bom", "utf8",
|
||||
"syntax", "non-object", "nan", "infinity", "depth"])
|
||||
def test_raw_json(body, status):
|
||||
sim = LocalSimulator([])
|
||||
response = LocalSimTransport(sim).post("http://local/enter", body,
|
||||
{"Content-Type": "application/json"}, None)
|
||||
assert response.status == status
|
||||
assert json.loads(response.body)["accepted"] is False
|
||||
assert not sim.entered
|
||||
|
||||
|
||||
@pytest.mark.parametrize("method,path,headers,status", [
|
||||
("POST", "/enter?x=1", {"Content-Type": "application/json"}, 404),
|
||||
("POST", "/enter?", {"Content-Type": "application/json"}, 404),
|
||||
("POST", "/enter/", {"Content-Type": "application/json"}, 404),
|
||||
("GET", "/enter", {"Content-Type": "application/json"}, 405),
|
||||
("POST", "/enter", {"Content-Type": "text/plain"}, 415),
|
||||
("POST", "/enter", {"Content-Type": "application/json;foo=bar"}, 415),
|
||||
("POST", "/enter", {"Content-Type": "application/json", "Content-Encoding": "gzip"}, 415),
|
||||
("POST", "/enter", {"content-type": "Application/JSON; charset=UTF-8"}, 200),
|
||||
])
|
||||
def test_http_boundary(method, path, headers, status):
|
||||
transport = LocalSimTransport(LocalSimulator([]))
|
||||
response = transport.request(method, "http://local" + path,
|
||||
json.dumps(payload()).encode(), headers)
|
||||
assert response.status == status
|
||||
|
||||
|
||||
def test_concurrent_registration_and_action_finishing_after_deadline():
|
||||
now = [0]
|
||||
sim = LocalSimulator([], clock=lambda: now[0])
|
||||
sim.handle("/enter", payload())
|
||||
started, release = Event(), Event()
|
||||
observe = sim._observe
|
||||
calls = []
|
||||
|
||||
def blocking_observe(pos, channel):
|
||||
calls.append(1)
|
||||
started.set()
|
||||
assert release.wait(5)
|
||||
return observe(pos, channel)
|
||||
|
||||
sim._observe = blocking_observe
|
||||
with ThreadPoolExecutor(max_workers=2) as pool:
|
||||
first = pool.submit(sim.handle, "/measure", measure())
|
||||
try:
|
||||
assert started.wait(5)
|
||||
assert sim.handle("/measure", measure("other"))[0] == 409
|
||||
assert sim.handle("/measure", measure(x=1))[0] == 409
|
||||
waiting = Event()
|
||||
completed = sim._pending["done"]
|
||||
|
||||
class ObservedEvent:
|
||||
def wait(self):
|
||||
waiting.set()
|
||||
return completed.wait(5)
|
||||
|
||||
def set(self):
|
||||
completed.set()
|
||||
|
||||
sim._pending["done"] = ObservedEvent()
|
||||
duplicate = pool.submit(sim.handle, "/measure", measure())
|
||||
assert waiting.wait(5)
|
||||
finally:
|
||||
release.set()
|
||||
assert first.result(timeout=5) == duplicate.result(timeout=5)
|
||||
assert calls == [1]
|
||||
assert sim.virtual_time_s == 5
|
||||
# 执行中跨过现实截止仍返回结果,后续新请求断连。
|
||||
sim._observe = lambda pos, channel: (now.__setitem__(0, 1200) or observe(pos, channel))
|
||||
assert sim.handle("/measure", measure("last"))[1]["accepted"]
|
||||
with pytest.raises(SimulatorClosed):
|
||||
sim.handle("/exit", payload("exit"))
|
||||
|
||||
|
||||
def test_raw_size_and_depth_boundaries_are_inclusive():
|
||||
sim = LocalSimulator([])
|
||||
transport = LocalSimTransport(sim)
|
||||
headers = {"Content-Type": "application/json"}
|
||||
deep = b'{"extra":' + b'[' * 15 + b'0' + b']' * 15 + b'}'
|
||||
# 深度16合法,因缺少公共字段得到400,补公共字段后应为业务拒绝200。
|
||||
body = json.dumps({**payload(), **json.loads(deep)}).encode()
|
||||
assert transport.post("http://local/enter", body, headers, None).status == 200
|
||||
body = json.dumps(payload()).encode()
|
||||
response = transport.post("http://local/enter", body.ljust(65536), headers, None)
|
||||
assert json.loads(response.body)["accepted"]
|
||||
|
||||
|
||||
def test_huge_numeric_fields_return_bad_request():
|
||||
sim = LocalSimulator([])
|
||||
sim.handle("/enter", payload())
|
||||
huge = 10 ** 400
|
||||
assert sim.handle("/measure", measure(x=huge))[0] == 400
|
||||
body = measure()
|
||||
body["channel"] = huge
|
||||
assert sim.handle("/measure", body)[0] == 400
|
||||
assert sim.handle("/measure", measure())[1]["accepted"]
|
||||
|
||||
|
||||
def test_explicit_exit_closes_new_requests():
|
||||
sim = LocalSimulator([])
|
||||
sim.handle("/enter", payload())
|
||||
assert sim.handle("/exit", payload("exit"))[1]["exit_reason"] == "user_exit"
|
||||
assert sim.closed_reason == "user_exit"
|
||||
with pytest.raises(SimulatorClosed):
|
||||
sim.handle("/measure", measure())
|
||||
Reference in New Issue
Block a user