This commit is contained in:
Horldsence
2026-09-11 11:36:08 +08:00
parent 2d2a9a4e6a
commit a18a139a1d
93 changed files with 9245 additions and 3806 deletions
+6 -5
View File
@@ -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)
+5 -4
View File
@@ -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",
+3 -2
View File
@@ -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,
+1 -1
View File
@@ -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,
+3 -2
View File
@@ -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]
+1 -1
View File
@@ -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
+110
View File
@@ -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
-200
View File
@@ -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())