Spaces:
Running
Running
File size: 8,802 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 | """手套检查项 —— 每一项都必须**被证明能抓到失败**。
真实数据上 5 项全绿(6 集、左右手各 5-6 条流、零误报)。但"全绿"证明不了什么:
**检查项从来没触发过,和检查项根本跑不起来,报告长得一模一样。** 所以这里每条
都构造一份明确的坏数据,断言它被抓到;再造一份好数据,断言不误报。
阈值全部来自实测:
* 阵列 16×16 = 256 维
* 取值门限 500(0 到 500 之间没有值)
* 设备更新 ~7.5Hz → 受压时静止**恰好 4 帧**(正常,不是卡死)
* 整集最低有 11.4% 的帧有压力
pytest tests/test_cleaning_glove.py
python3 tests/test_cleaning_glove.py
"""
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1])) # noqa: E402
from app.processing.cleaning.checks import ( # noqa: E402
CheckContext, StreamEvidence, applicable_checks, get_check,
PASS, WARN, FAIL,
)
SIZE = 256
# ── 构造 ────────────────────────────────────────────────────
def _frame(value: float = 0.0, size: int = SIZE) -> tuple:
return tuple([value] * size)
def _context(series: dict, *, channel="tactile", key="left_glove",
fps=30.0) -> CheckContext:
stream = StreamEvidence(key=key, channel=channel, series=series)
return CheckContext(batch_dir=Path("/nonexistent"), fps=fps, streams=(stream,))
def _run(slug: str, ctx: CheckContext, params: dict | None = None):
ctx.params = params or {}
findings = get_check(slug).run(ctx)
assert findings, f"{slug} 一条结论都没产出 —— 报告会显示为『没问题』"
return findings
def _only(slug, ctx, params=None):
findings = _run(slug, ctx, params)
statuses = {f.status for f in findings}
assert len(statuses) == 1, f"期望单一结论,得到 {statuses}"
return findings[0]
# ── 可达性:检查项必须真的会被选中 ───────────────────────────
def test_glove_checks_are_applicable():
"""★ 防"检查项静默不跑"—— 声明与闸门对不上时它们一条都不会执行。"""
slugs = {c.slug for c in applicable_checks(
channels={"tactile", "device_status", "time"},
modalities={"glove_sensor"})}
for expected in ("glove.array_shape", "glove.device_status",
"glove.frozen_array", "glove.gate_floor",
"glove.no_contact"):
assert expected in slugs, f"{expected} 在手套数据上不可达"
def test_glove_checks_skip_other_devices():
"""UMI 夹爪上不该跑手套检查 —— 设备卡片闸门要生效。"""
slugs = {c.slug for c in applicable_checks(
channels={"tactile", "slam", "time"}, modalities={"gripper_device"})}
assert not any(s.startswith("glove.") for s in slugs)
def test_device_status_check_needs_its_channel():
"""没有状态列(老批次)时不该跑 —— 跑起来只会报"没数据"。"""
slugs = {c.slug for c in applicable_checks(
channels={"tactile", "time"}, modalities={"glove_sensor"})}
assert "glove.device_status" not in slugs
assert "glove.frozen_array" in slugs
# ── 1. 连接状态 ─────────────────────────────────────────────
def test_status_constant_connected_passes():
ctx = _context({"device_status": tuple(["connected"] * 100)}, channel="device_status")
assert _only("glove.device_status", ctx).status is PASS
def test_status_change_mid_recording_fails():
"""★ 从"连着"变成别的 = 掉线,与状态词表无关,见到即拦。"""
values = tuple(["connected"] * 50 + ["disconnected"] * 50)
ctx = _context({"device_status": values}, channel="device_status")
finding = _only("glove.device_status", ctx)
assert finding.status is FAIL
assert finding.metrics["first_change_frame"] == 50
def test_status_unknown_constant_only_warns():
"""词表只见过 connected;别的取值不拦批次(拦错了比不拦更糟)。"""
ctx = _context({"device_status": tuple(["ok"] * 100)}, channel="device_status")
assert _only("glove.device_status", ctx).status is WARN
def test_status_all_blank_warns():
ctx = _context({"device_status": tuple([""] * 100)}, channel="device_status")
assert _only("glove.device_status", ctx).status is WARN
# ── 2. 阵列完整性 ───────────────────────────────────────────
def test_shape_full_array_passes():
ctx = _context({"tactile": tuple([_frame()] * 50)})
assert _only("glove.array_shape", ctx).status is PASS
def test_shape_catches_truncation():
"""★ 正是那个真实踩过的 bug:256 维被静默截成 3 维。"""
ctx = _context({"tactile": tuple([_frame(size=3)] * 500)})
finding = _only("glove.array_shape", ctx)
assert finding.status is FAIL
assert finding.metrics["bad_ratio"] == 1.0
assert finding.metrics["observed_sizes"] == [3]
def test_shape_single_null_frame_only_warns():
"""偶发一帧缺失不该拦住整批。"""
values = [_frame(600.0)] * 999 + [()]
ctx = _context({"tactile": tuple(values)})
assert _only("glove.array_shape", ctx).status is WARN
# ── 3. 取值下限 ─────────────────────────────────────────────
def test_gate_floor_normal_values_pass():
ctx = _context({"tactile": (_frame(0.0), _frame(508.09), _frame(1500.0))})
assert _only("glove.gate_floor", ctx).status is PASS
def test_gate_floor_catches_encoding_change():
"""★ 固件改成直接输出原始 ADC 时,值会掉到门限以下 —— 而别的检查全绿。"""
values = [_frame(v) for v in (5.0, 20.0, 137.0, 400.0)] + [_frame(900.0)]
ctx = _context({"tactile": tuple(values)})
finding = _only("glove.gate_floor", ctx)
assert finding.status is FAIL
assert finding.metrics["below_ratio"] == 0.8
def test_gate_floor_all_zero_is_not_its_business():
"""整集全零是 glove.no_contact 的事,不在这里重复报。"""
findings = get_check("glove.gate_floor").run(_context({"tactile": (_frame(),)}))
assert findings == []
# ── 4. 阵列卡死 ─────────────────────────────────────────────
def test_frozen_normal_device_rate_passes():
"""★ 设备本来就每 4 帧才更新一次 —— 静止 4 帧是**正常**,不是卡死。"""
values = []
for block in range(100):
values.extend([_frame(600.0 + block)] * 4)
ctx = _context({"tactile": tuple(values)})
assert _only("glove.frozen_array", ctx).status is PASS
def test_frozen_detects_stall():
values = [_frame(600.0)] * 500 + [_frame(900.0)] * 10
ctx = _context({"tactile": tuple(values)})
finding = _only("glove.frozen_array", ctx)
assert finding.status is FAIL # 500 帧 ≥ fail_frames(300)
assert finding.metrics["longest_run_frames"] == 500
# 区间要能定位到哪儿卡了
assert finding.ranges and finding.ranges[0].start_frame == 0
def test_frozen_ignores_all_zero_stretches():
"""★ 实测踩过的坑:全零段天然"完全相同",算进去会报出大片假区间。
真实数据里一集有 88% 的帧全零,最长"静止"575 帧 —— 全部来自全零段。
"""
values = [_frame(0.0)] * 800 + [_frame(600.0)] * 10
ctx = _context({"tactile": tuple(values)})
assert _only("glove.frozen_array", ctx).status is PASS
def test_frozen_warn_threshold_between_normal_and_stall():
"""40~300 帧是警告区(正常 4 帧,卡死 300+)。"""
values = [_frame(600.0)] * 100 + [_frame(700.0)] * 10
ctx = _context({"tactile": tuple(values)})
assert _only("glove.frozen_array", ctx).status is WARN
# ── 5. 无接触 ───────────────────────────────────────────────
def test_no_contact_with_pressure_passes():
ctx = _context({"tactile": (_frame(0.0), _frame(620.0))})
assert _only("glove.no_contact", ctx).status is PASS
def test_no_contact_all_zero_warns():
ctx = _context({"tactile": tuple([_frame(0.0)] * 500)})
finding = _only("glove.no_contact", ctx)
assert finding.status is WARN
assert finding.metrics["active_frames"] == 0
if __name__ == "__main__":
import pytest
raise SystemExit(pytest.main([__file__, "-q"]))
|