111 lines
5.0 KiB
Python
111 lines
5.0 KiB
Python
"""解耦守卫:评测机(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, check=False)
|
||
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
|