Files
mcm-26-b/tests/test_referee_decoupling.py
T
2026-09-11 11:36:08 +08:00

111 lines
5.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""解耦守卫:评测机(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