File size: 8,150 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
"""่ง†้ข‘็ฑปๆฃ€ๆŸฅ้กนๅ•ๆต‹ใ€‚

่ฟ™ไบ›ๆฃ€ๆŸฅ้กน**ไธ่ฏป็ฃ็›˜** โ€”โ€” ่ฏๆฎ็”ฑ 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)