Spaces:
Running
Running
File size: 12,561 Bytes
bb59211 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 | """清洗契约与检查项注册表单测。
覆盖:设备模态与 device_naming 的口径一致性(漂移守卫)、别名映射、
信道推导、严重度合成、检查项筛选。
pytest tests/test_cleaning_contract.py
python3 tests/test_cleaning_contract.py # 无 pytest 也能跑
"""
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from app.device_naming import DEVICE_LABELS # noqa: E402
from app.processing.cleaning import contract as C # noqa: E402
from app.processing.cleaning import checks as K # noqa: E402
# ── 漂移守卫 ────────────────────────────────────────────────
def test_device_modalities_match_device_naming():
"""两个文件声明的设备模态必须逐字一致。
这是本套设计里唯一靠"人工保持同步"的地方 —— 所以用测试锁死。
任何人在 device_naming 加了新设备类型而忘了同步这里,这个测试立刻红。
"""
assert set(C.DEVICE_MODALITIES) == set(DEVICE_LABELS)
def test_every_channel_of_every_modality_is_declared():
"""MODALITY_CHANNELS 用到的信道都必须在 CHANNELS 里声明。"""
declared = set(C.CHANNELS)
for modality in C.DEVICE_MODALITIES:
assert modality in C.MODALITY_CHANNELS, f"{modality} 没声明信道"
unknown = set(C.MODALITY_CHANNELS[modality]) - declared
assert not unknown, f"{modality} 用了未声明的信道: {unknown}"
def test_aliases_map_into_canonical_set():
"""别名表的值必须是合法的设备模态。"""
for alias, target in C._ALIASES.items():
assert target in C.DEVICE_MODALITIES, f"{alias} → {target} 不是合法模态"
# ── 别名映射 ────────────────────────────────────────────────
def test_normalize_accepts_canonical_and_alias():
# canonical 值原样返回
assert C.normalize_device_modality("mono_rgb") == "mono_rgb"
assert C.normalize_device_modality("gripper_device") == "gripper_device"
# api/projects 与处理模块 slug 用的 camera 后缀写法
assert C.normalize_device_modality("mono_camera") == "mono_rgb"
assert C.normalize_device_modality("stereo_camera") == "stereo_rgb"
# 历史节点类型
assert C.normalize_device_modality("fisheye_camera") == "mono_rgb"
def test_normalize_rejects_unknown_without_raising():
"""认不出来要返回 None 而不是抛异常 —— 调用方据此跳过。"""
assert C.normalize_device_modality("") is None
assert C.normalize_device_modality(None) is None
assert C.normalize_device_modality("some_future_sensor") is None
def test_normalize_accepts_whitespace():
assert C.normalize_device_modality(" mono_rgb ") == "mono_rgb"
# ── 信道推导 ────────────────────────────────────────────────
def test_channels_for_unions_and_dedupes():
channels = C.channels_for(["gripper_device", "mono_rgb"])
assert channels[0] == "rgb" # 保序:gripper_device 先出现
assert "time" in channels
assert len(channels) == len(set(channels)), "有重复信道"
def test_channels_for_unknown_modality_is_empty():
assert C.channels_for(["not_a_modality"]) == ()
assert C.channels_for([]) == ()
# ── 严重度 ──────────────────────────────────────────────────
def test_worst_picks_most_severe():
assert C.worst([C.PASS, C.WARN]) == C.WARN
assert C.worst([C.WARN, C.FAIL]) == C.FAIL
assert C.worst([C.FAIL, C.ERROR]) == C.ERROR
assert C.worst([C.ERROR, C.PENDING, C.WARN]) == C.ERROR
def test_worst_of_empty_is_pass():
assert C.worst([]) == C.PASS
def test_worst_ignores_unknown_status_names():
"""未知状态名按"最轻"处理,不崩溃 —— 新旧报告混读时不炸。"""
assert C.worst([C.PASS, "some_new_status"]) == C.PASS
def test_is_blocking_only_for_fail_and_error():
assert C.is_blocking(C.FAIL) is True
assert C.is_blocking(C.ERROR) is True
assert C.is_blocking(C.WARN) is False
assert C.is_blocking(C.PENDING) is False
assert C.is_blocking(C.PASS) is False
# ── 检查项筛选 ──────────────────────────────────────────────
class _FakeVideoCheck(K.CleaningCheck):
slug = "_test.video"
label = "fake video"
requires_channels = ("rgb",)
# 多张卡片共享 —— video.* 就是这样,只要命中其一就跑
device_cards = ("mono_rgb", "stereo_rgb", "rgbd_camera", "gripper_device")
class _FakeUmiCheck(K.CleaningCheck):
slug = "_test.umi"
label = "fake umi"
requires_channels = ("slam",)
requires_modality = ("gripper_device",)
device_cards = ("gripper_device",)
class _FakeCrossCheck(K.CleaningCheck):
slug = "_test.cross"
label = "fake cross"
cross_modal = True
def _with_checks(*classes, fn):
"""临时注册一批检查项,跑完清理 —— 不污染全局注册表。"""
registered = []
try:
for cls in classes:
K._registry[cls.slug] = cls()
registered.append(cls.slug)
return fn()
finally:
for slug in registered:
K._registry.pop(slug, None)
def test_applicable_filters_by_channel():
def run():
got = {c.slug for c in K.applicable_checks({"rgb"}, {"mono_rgb"})}
assert "_test.video" in got
assert "_test.umi" not in got # 缺 slam 信道
_with_checks(_FakeVideoCheck, _FakeUmiCheck, fn=run)
def test_applicable_filters_by_modality():
def run():
# 有 slam 信道但设备不是 gripper_device → umi 检查不跑
got = {c.slug for c in K.applicable_checks({"slam"}, {"mono_rgb"})}
assert "_test.umi" not in got
got = {c.slug for c in K.applicable_checks({"slam"}, {"gripper_device"})}
assert "_test.umi" in got
_with_checks(_FakeUmiCheck, fn=run)
def test_applicable_filters_by_device_cards():
"""设备卡片闸门 —— 光按信道筛不够。
真实踩过的坑:手套项目(rgbd_camera)的批次里同样有一列 ``action``
(采集端写的全零占位),于是 ``umi.action_present`` 满足
``requires_channels=("action",)`` 就跑了起来,给手套项目报了个
"action 全零"的失败 —— 而对方根本不训练动作。
声明了 device_cards 的检查项,必须只在该设备上跑。
"""
def run():
channels = {"action", "slam"} # 信道全够,只剩设备这一关
# 设备是手套相机 → UMI 检查不跑,尽管信道满足了
got = {c.slug for c in K.applicable_checks(channels, {"rgbd_camera"})}
assert "_test.umi" not in got
# 设备是 UMI 夹爪 → 跑
got = {c.slug for c in K.applicable_checks(channels, {"gripper_device"})}
assert "_test.umi" in got
_with_checks(_FakeUmiCheck, fn=run)
def test_device_cards_gate_is_skipped_for_shared_checks():
"""声明了多张卡片的检查项,只要命中其一就跑(video.* 就是这种)。"""
def run():
got = {c.slug for c in K.applicable_checks({"rgb"}, {"stereo_rgb"})}
assert "_test.video" in got
_with_checks(_FakeVideoCheck, fn=run)
def test_cross_modal_requires_two_modalities():
def run():
# 单模态 → 跨模态检查不跑
got = {c.slug for c in K.applicable_checks(set(), {"mono_rgb"})}
assert "_test.cross" not in got
# 显式声明跨模态成立 → 跑
got = {c.slug for c in K.applicable_checks(
set(), {"mono_rgb", "glove_sensor"}, cross_modal_active=True)}
assert "_test.cross" in got
_with_checks(_FakeCrossCheck, fn=run)
def test_applicable_respects_enabled_and_disabled():
def run():
got = {c.slug for c in K.applicable_checks(
{"rgb"}, {"mono_rgb"}, disabled={"_test.video"})}
assert "_test.video" not in got
got = {c.slug for c in K.applicable_checks(
{"rgb"}, {"mono_rgb"}, enabled={"_test.video"})}
assert got == {"_test.video"}
_with_checks(_FakeVideoCheck, fn=run)
def test_all_checks_is_sorted_by_slug():
"""排序稳定 —— ruleset_revision 是对它取哈希,顺序变了指纹就变。"""
slugs = [c.slug for c in K.all_checks()]
assert slugs == sorted(slugs)
def test_register_rejects_duplicate_slug():
class First(K.CleaningCheck):
slug = "_test.dupe"
class Second(K.CleaningCheck):
slug = "_test.dupe"
try:
K.register_check(First)
try:
K.register_check(Second)
except ValueError as exc:
assert "_test.dupe" in str(exc)
else:
raise AssertionError("重复 slug 应当抛 ValueError")
finally:
K._registry.pop("_test.dupe", None)
def test_register_rejects_empty_slug():
class NoSlug(K.CleaningCheck):
slug = ""
try:
K.register_check(NoSlug)
except ValueError:
pass
else:
raise AssertionError("空 slug 应当抛 ValueError")
# ── 设备卡片分组(设置面板的 tab)────────────────────────────
def test_every_check_declares_device_cards():
"""除跨设备检查外,每项都必须声明属于哪些设备卡片。
否则它在设置面板里【无处可去】—— 用户看不到、也改不了它的阈值。
跨设备检查(cross_modal=True)约定不绑单卡片,只在"跨设备" tab 出现。
"""
for check in K.all_checks():
if check.cross_modal:
continue
assert check.device_cards, f"{check.slug} 没有声明 device_cards"
def test_device_cards_are_known_modalities():
"""device_cards 的取值必须来自 contract 的设备模态表 —— 防止拼错字符串。
拼错不会报错,只会让检查项在面板上凭空消失,很难发现。
"""
known = set(C.DEVICE_MODALITIES)
for check in K.all_checks():
for card in check.device_cards:
assert card in known, f"{check.slug} 声明了未知设备卡片 {card!r}"
def test_checks_for_device_cards_only_returns_requested():
"""未连接的设备不该出现在分组结果里。"""
grouped = K.checks_for_device_cards(["gripper_device"])
assert set(grouped) == {"gripper_device"}
assert all(c.device_cards.count("gripper_device") for c in grouped["gripper_device"])
def test_checks_for_device_cards_places_multi_card_checks():
"""一个检查项可以同时属于多张卡片(video.* 对每种带摄像头的设备都成立)。"""
grouped = K.checks_for_device_cards(["rgbd_camera", "gripper_device"])
in_a = {c.slug for c in grouped["rgbd_camera"]}
in_b = {c.slug for c in grouped["gripper_device"]}
assert "video.frame_drop" in in_a and "video.frame_drop" in in_b
# UMI 专有的只出现在 gripper_device 下
assert "umi.slam_continuity" in in_b and "umi.slam_continuity" not in in_a
def test_video_checks_cover_gripper_device():
"""UMI 夹爪自带鱼眼相机,视频检查对它同样成立。
历史坑:video.* 早期只声明了四种摄像机模态,漏了 gripper_device ——
结果 UMI 项目的设置面板里视频检查整组消失。
"""
grouped = K.checks_for_device_cards(["gripper_device"])
slugs = {c.slug for c in grouped["gripper_device"]}
assert {"video.frame_drop", "video.black_screen",
"video.freeze", "video.decode_error"} <= slugs
if __name__ == "__main__":
_failures = 0
for _name, _fn in sorted(globals().items()):
if not _name.startswith("test_") or not callable(_fn):
continue
import inspect
try:
if "tmp_path" in inspect.signature(_fn).parameters:
import tempfile
with tempfile.TemporaryDirectory() as _tmp:
_fn(Path(_tmp))
else:
_fn()
print(f"PASS {_name}")
except Exception as _exc: # noqa: BLE001
_failures += 1
print(f"FAIL {_name}: {_exc}")
raise SystemExit(1 if _failures else 0)
|