DataFlowing / processor /tests /test_check_video.py
stouchinglhh
Claude
sync processor from GitLab c697b6e: ็‰ˆๆœฌ 1.19.4 / ๅ‰็ซฏ 1.12.4 โ€”โ€” ๆœๅŠก็ซฏ i18n(้ฆ–ๅฑไธๅ†้—ช่‹ฑๆ–‡)ใ€ๆŽจ็†ๆจกๅ—ๅนถๅ…ฅๆจกๅž‹้กตใ€ๅ‰็ซฏๆŠฝๅ‡บ shared/ใ€็ผ“ๅญ˜้”ฎๆ”น็”จๅ‰็ซฏๅท
bb59211
Raw History Blame Contribute Delete
8.15 kB
"""่ง†้ข‘็ฑปๆฃ€ๆŸฅ้กนๅ•ๆต‹ใ€‚
่ฟ™ไบ›ๆฃ€ๆŸฅ้กน**ไธ่ฏป็ฃ็›˜** โ€”โ€” ่ฏๆฎ็”ฑ evidence.py ้‡‡้›†ๅฅฝๆ”พ่ฟ› StreamEvidence๏ผŒ
ๆต‹่ฏ•็›ดๆŽฅๆž„้€ ๅ‡่ฏๆฎๅณๅฏ๏ผŒไธ้œ€่ฆ็œŸ mp4 ๆˆ– cv2ใ€‚่ฟ™ๆญฃๆ˜ฏๅฝ“ๅˆๆŠŠ IO ๅšๆˆๆณจๅ…ฅ็š„็›ฎ็š„ใ€‚
pytest tests/test_check_video.py
python3 tests/test_check_video.py
"""
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from app.processing.cleaning.checks import ( # noqa: E402
CheckContext, StreamEvidence, get_check, PASS, WARN, FAIL, ERROR,
)
def _ctx(streams, **kwargs) -> CheckContext:
"""่ฃ…ไธ€ไธชๆœ€ๅฐ็š„ๆ‰ง่กŒไธŠไธ‹ๆ–‡ใ€‚"""
return CheckContext(batch_dir=Path("/tmp/nonexistent"),
streams=tuple(streams), **kwargs)
def _rgb(key="umi_rgb", **video) -> StreamEvidence:
return StreamEvidence(key=key, channel="rgb", fps=30.0, video=video)
# โ”€โ”€ video.frame_drop โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
def test_frame_drop_passes_within_tolerance():
check = get_check("video.frame_drop")
ctx = _ctx([_rgb(frame_count=1188, reason="passed")], data_rows=1190)
findings = check.run(ctx)
assert len(findings) == 1
assert findings[0].status == PASS
def test_frame_drop_warns_on_small_gap():
check = get_check("video.frame_drop")
# ๅทฎ 20 ๅธง / ๅฎนๅทฎ max(2, 1200*0.005=6) = 6 โ†’ 20 > 6*5=30? ๅฆ โ†’ WARN
ctx = _ctx([_rgb(frame_count=1180, reason="passed")], data_rows=1200)
findings = check.run(ctx)
assert findings[0].status == WARN
assert findings[0].metrics["diff"] == 20
assert findings[0].metrics["tolerance"] == 6
def test_frame_drop_fails_on_large_gap():
check = get_check("video.frame_drop")
# ๅทฎ 500 ๅธง >> ๅฎนๅทฎ*5 โ†’ FAIL
ctx = _ctx([_rgb(frame_count=700, reason="passed")], data_rows=1200)
assert check.run(ctx)[0].status == FAIL
def test_frame_drop_tolerance_floor_for_small_batches():
"""ๅฐๆ‰นๆฌกๆŒ‰็ปๅฏนๅธงๆ•ฐๅ…œๅบ•๏ผš100 ่กŒ ร— 0.5% = 0 โ†’ ็”จไธ‹้™ 2ใ€‚"""
check = get_check("video.frame_drop")
ctx = _ctx([_rgb(frame_count=98, reason="passed")], data_rows=100)
findings = check.run(ctx)
assert findings[0].metrics["tolerance"] == 2
assert findings[0].status == PASS
def test_frame_drop_ignores_unreadable_video():
"""ๆ‰“ไธๅผ€็š„ๆตไธ่ฏฅ่ขซ็ฎ—ๆˆ"ไธขๅธง" โ€”โ€” ้‚ฃๆ˜ฏ decode_error ็š„่Œ่ดฃใ€‚"""
check = get_check("video.frame_drop")
ctx = _ctx([_rgb(frame_count=0, reason="video_open_failed")], data_rows=1200)
assert check.run(ctx) == []
def test_frame_drop_uses_node_params_over_defaults():
"""่Š‚็‚น้…็ฝฎ็š„้˜ˆๅ€ผ่ฆ็›–่ฟ‡ default_paramsใ€‚"""
check = get_check("video.frame_drop")
ctx = _ctx([_rgb(frame_count=900, reason="passed")], data_rows=1000,
params={"tolerance_ratio": 0.5, "tolerance_min": 500})
assert check.run(ctx)[0].status == PASS
# โ”€โ”€ video.black_screen โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
def test_black_screen_passes_when_no_ranges():
check = get_check("video.black_screen")
assert check.run(_ctx([_rgb(frame_count=300, reason="passed")])) == []
def test_black_screen_reports_ranges_and_frames():
check = get_check("video.black_screen")
# 30 ็ง’้‡Œ้ป‘ 1 ็ง’ = 3.3%๏ผŒไฝŽไบŽ fail_ratio(5%) โ†’ WARN
ctx = _ctx([_rgb(frame_count=900, fps=30.0, reason="black_screen",
black_ranges=[[12.0, 13.0]])],
duration_sec=30.0)
finding = check.run(ctx)[0]
assert finding.status == WARN
assert finding.metrics["segments"] == 1
assert len(finding.ranges) == 1
item = finding.ranges[0]
assert item.start_sec == 12.0 and item.end_sec == 13.0
assert item.start_frame == 360 and item.end_frame == 390
def test_black_screen_fails_when_ratio_exceeds_threshold():
"""ๅ ๆฏ”่ถ…่ฟ‡ 5% โ†’ FAILใ€‚30 ็ง’้‡Œ้ป‘ 3 ็ง’ = 10%ใ€‚"""
check = get_check("video.black_screen")
ctx = _ctx([_rgb(frame_count=900, reason="black_screen",
black_ranges=[[0.0, 3.0]])],
duration_sec=30.0)
assert check.run(ctx)[0].status == FAIL
def test_black_screen_drops_ranges_below_min_sec():
"""็ŸญไบŽ min_sec(0.5s) ็š„้ป‘ๅฑๆฎตๅฟฝ็•ฅ โ€”โ€” ้ฟๅ…้€ๅธง้—ช็ƒๅˆทๅฑใ€‚"""
check = get_check("video.black_screen")
ctx = _ctx([_rgb(frame_count=900, reason="black_screen",
black_ranges=[[1.0, 1.2]])],
duration_sec=30.0)
assert check.run(ctx) == []
# โ”€โ”€ video.freeze โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
def test_freeze_warns_and_escalates_by_duration():
check = get_check("video.freeze")
short = _ctx([_rgb(frame_count=900, reason="freeze_suspected",
freeze_ranges=[[5.0, 8.0]])])
assert check.run(short)[0].status == WARN # 3 ็ง’ < fail_sec(10)
long = _ctx([_rgb(frame_count=900, reason="freeze_suspected",
freeze_ranges=[[5.0, 20.0]])])
assert check.run(long)[0].status == FAIL # 15 ็ง’ >= 10
def test_freeze_metrics_report_longest():
"""longest_sec ๅ–ๆœ€้•ฟ็š„้‚ฃไธ€ๆฎต๏ผš4.0s ๅ’Œ 5.0s โ†’ 5.0ใ€‚"""
check = get_check("video.freeze")
ctx = _ctx([_rgb(frame_count=900, reason="freeze_suspected",
freeze_ranges=[[5.0, 9.0], [20.0, 25.0]])])
finding = check.run(ctx)[0]
assert finding.metrics["segments"] == 2
assert finding.metrics["longest_sec"] == 5.0
# โ”€โ”€ video.decode_error โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
def test_decode_error_passes_without_errors():
check = get_check("video.decode_error")
ctx = _ctx([_rgb(frame_count=300, reason="passed", decode_error_count=0)])
assert check.run(ctx)[0].status == PASS
def test_decode_error_fails_and_marks_frames():
check = get_check("video.decode_error")
ctx = _ctx([_rgb(frame_count=300, fps=30.0, reason="decode_error",
decode_error_count=3, decode_errors=[30, 60, 90])])
finding = check.run(ctx)[0]
assert finding.status == FAIL
assert len(finding.ranges) == 3
assert finding.ranges[0].start_frame == 30
assert finding.ranges[0].start_sec == 1.0
def test_decode_error_marks_opencv_missing_as_error_not_fail():
"""็ผบ OpenCV ๆ˜ฏ็Žฏๅขƒ้—ฎ้ข˜๏ผŒไธ่ฏฅ่ฎฐๆˆๆ•ฐๆฎ่ดจ้‡ไบ‹ๆ•…ใ€‚"""
check = get_check("video.decode_error")
ctx = _ctx([_rgb(reason="opencv_unavailable")])
assert check.run(ctx)[0].status == ERROR
def test_decode_error_marks_unopenable_video_as_fail():
check = get_check("video.decode_error")
ctx = _ctx([_rgb(reason="video_open_failed", error="moov atom not found")])
finding = check.run(ctx)[0]
assert finding.status == FAIL
assert "moov atom" in finding.metrics["error"]
# โ”€โ”€ ่ทจๆต โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
def test_checks_iterate_all_rgb_streams():
"""ๅŒ็›ฎ้กน็›ฎๆœ‰ไธค่ทฏ RGB๏ผŒ้ƒฝ่ฆๆฃ€ๆŸฅใ€‚"""
check = get_check("video.frame_drop")
ctx = _ctx([_rgb("cam_left", frame_count=700, reason="passed"),
_rgb("cam_right", frame_count=1200, reason="passed")],
data_rows=1200)
findings = check.run(ctx)
assert {item.stream for item in findings} == {"cam_left", "cam_right"}
assert [item.status for item in findings].count(FAIL) == 1
if __name__ == "__main__":
import traceback
failures = 0
for name, fn in sorted(globals().items()):
if not name.startswith("test_") or not callable(fn):
continue
try:
fn()
print(f" โœ“ {name}")
except Exception:
failures += 1
print(f" โœ— {name}")
traceback.print_exc()
print(f"\n{'FAILED' if failures else 'OK'} ({failures} ไธชๅคฑ่ดฅ)")
sys.exit(1 if failures else 0)