File size: 7,492 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
"""ๆธ…ๆด—ๆŠฅๅ‘Š็ป„่ฃ…ๅ•ๆต‹(app/processing/cleaning/report.py)ใ€‚

่ฆ†็›–:ๅ››ๅฑ‚็ป“ๆžœ็š„ๆฏไธ€ๅฑ‚ใ€ๆ—ถ้—ด่ฝดๆ‹ๅนณไธŽๅŽป้‡ใ€่ฎญ็ปƒ็”จ้€”ๅˆคๅฎšใ€fail-closed ่ฏญไน‰ใ€‚

ๅ…จๆ˜ฏ็บฏๅ‡ฝๆ•ฐ โ€”โ€” ไธ้œ€่ฆ็œŸ่ง†้ข‘ใ€ไธ้œ€่ฆ cv2ใ€ไธ้œ€่ฆไปปไฝ• mockใ€‚

    pytest tests/test_cleaning_report.py
    python3 tests/test_cleaning_report.py
"""
import sys
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parents[1]))

from app.processing.cleaning import contract as C  # noqa: E402
from app.processing.cleaning import report as R  # noqa: E402


def _range(start, end, code="video.freeze", severity=C.WARN, stream="s1"):
    return C.Range(start_sec=start, end_sec=end, code=code,
                   severity=severity, stream=stream)


def _stream(name="s1", status=C.PASS, ranges=(), channels=("rgb", "time")):
    return C.StreamResult(stream=name, status=status, ranges=tuple(ranges),
                          channels=tuple(channels))


# โ”€โ”€ ็ฌฌไบŒๅฑ‚:ๆ—ถ้—ด่ฝด โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
def test_timeline_sorted_by_time():
    streams = [
        _stream("s1", ranges=[_range(5, 6), _range(1, 2)]),
        _stream("s2", ranges=[_range(3, 4)]),
    ]
    axis = R.build_timeline(streams)
    assert [row["start_sec"] for row in axis] == [1, 3, 5]


def test_timeline_dedupes_identical_rows():
    dup = _range(1, 2)
    axis = R.build_timeline([_stream("s1", ranges=[dup, dup])])
    assert len(axis) == 1


def test_timeline_fills_stream_from_parent():
    """range ่‡ชๅทฑๆฒกๅ†™ stream ๆ—ถ,ไปŽๆ‰€ๅฑž็š„ stream ่กฅไธŠใ€‚"""
    bare = C.Range(start_sec=1, end_sec=2, code="x", severity=C.WARN)
    axis = R.build_timeline([_stream("cam_a", ranges=[bare])])
    assert axis[0]["stream"] == "cam_a"


def test_timeline_accepts_dict_streams():
    """ๆŠฅๅ‘ŠไปŽ็ฃ็›˜่ฏปๅ›žๆฅๆ—ถ streams ๆ˜ฏ dict,ไธๆ˜ฏ StreamResultใ€‚"""
    axis = R.build_timeline([{
        "stream": "s1",
        "ranges": [{"start_sec": 1, "end_sec": 2, "code": "x", "severity": C.WARN}],
    }])
    assert axis[0]["code"] == "x"


def test_timeline_empty():
    assert R.build_timeline([]) == []


# โ”€โ”€ ็ฌฌไธ‰ๅฑ‚:ๆ•ด้›†็ป“่ฎบ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
def test_episode_takes_worst_stream():
    episode = R.rollup_episode([_stream("a", C.PASS), _stream("b", C.FAIL)])
    assert episode["status"] == C.FAIL
    assert episode["failed_streams"] == ["b"]


def test_episode_includes_findings_in_rollup():
    """่ทจๆจกๆ€ๆฃ€ๆŸฅๆฒกๆœ‰ๅฏนๅบ”็š„ stream,ๅฎƒ็š„็ป“่ฎบไนŸ่ฆๅ‚ไธŽๆ•ด้›†ๅˆคๅฎšใ€‚"""
    finding = C.Finding(rule="cross.sync", version="1.0", status=C.FAIL,
                        scope="cross_modal")
    episode = R.rollup_episode([_stream("a", C.PASS)], [finding])
    assert episode["status"] == C.FAIL


def test_episode_all_pass():
    episode = R.rollup_episode([_stream("a", C.PASS), _stream("b", C.PASS)])
    assert episode["status"] == C.PASS
    assert episode["summary"] == "All checks passed"


def test_episode_summary_counts_each_bucket():
    episode = R.rollup_episode([
        _stream("a", C.FAIL), _stream("b", C.WARN), _stream("c", C.PENDING),
    ])
    assert "1 stream(s) failed" in episode["summary"]
    assert "1 stream(s) with warnings" in episode["summary"]
    assert "1 stream(s) pending review" in episode["summary"]


def test_episode_empty_streams_is_pass():
    """ๆฒกๆœ‰ๆตๅฏๆฃ€ๆŸฅ โ†’ ่ฟ™ไธ€ๅฑ‚ๅˆค PASS(ๆ˜ฏๅฆ fail-closed ็”ฑ build_report/passed ๅ†ณๅฎš)ใ€‚"""
    assert R.rollup_episode([])["status"] == C.PASS


# โ”€โ”€ ็ฌฌๅ››ๅฑ‚:่ฎญ็ปƒ็”จ้€” โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
def test_training_use_fails_on_missing_channel():
    rows = {row["use"]: row for row in R.rollup_training_use({"rgb", "time"})}
    assert rows["pure_vision"]["status"] == C.PASS
    assert rows["vision_tactile_joint"]["status"] == C.FAIL
    assert "not collected" in rows["vision_tactile_joint"]["reason"]
    assert rows["vision_tactile_joint"]["missing"] == ["tactile"]


def test_training_use_reports_all_missing_channels():
    rows = {row["use"]: row for row in R.rollup_training_use(set())}
    assert set(rows["pure_vision"]["missing"]) == {"rgb", "time"}


def test_training_use_inherits_episode_status():
    """ไฟก้“้ฝๅ…จไฝ†ๆ•ด้›†ๆœ‰้—ฎ้ข˜ โ†’ ่ฏฅ็”จ้€”่ทŸ็€ๅˆค warn/fail,่€Œไธๆ˜ฏ passใ€‚"""
    rows = {row["use"]: row for row in R.rollup_training_use({"rgb", "time"}, C.WARN)}
    assert rows["pure_vision"]["status"] == C.WARN
    assert rows["pure_vision"]["reason"] == "data quality problem"


def test_training_use_covers_every_declared_rule():
    rows = R.rollup_training_use({"rgb", "time"})
    assert len(rows) == len(R.TRAINING_USE_RULES)


# โ”€โ”€ ๅฎŒๆ•ดๆŠฅๅ‘Š โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
def test_build_report_shape():
    rep = R.build_report(
        [_stream("cam_a", C.WARN, [_range(1, 2)])],
        [],
        episode_id="ep1", project="proj",
        modalities={"actual": ["mono_rgb"]},
        channels={"rgb", "time"},
        ruleset_revision="abc123",
        template_source="workflow",
    )
    assert rep["schema_version"] == R.SCHEMA_VERSION
    assert rep["episode_id"] == "ep1"
    assert rep["ruleset_revision"] == "abc123"
    assert rep["template_source"] == "workflow"
    assert rep["episode"]["status"] == C.WARN
    assert rep["passed"] is False          # ๅชๆœ‰ PASS ็ฎ—้€š่ฟ‡,WARN ไธ็ฎ—
    assert len(rep["ranges"]) == 1
    assert len(rep["streams"]) == 1
    assert rep["training_use"]


def test_build_report_passed_only_when_all_clean():
    rep = R.build_report([_stream("a", C.PASS)], [], channels={"rgb", "time"})
    assert rep["episode"]["status"] == C.PASS
    assert rep["passed"] is True


def test_warn_is_not_passed():
    """WARN ไธ็ฎ—้€š่ฟ‡ โ€”โ€” ่ฟ™ๆ˜ฏ"ๅกๅœจไบบๅทฅๅฎกๆ ธ"็š„ๅˆคๅฎšไพๆฎ,ๅฟ…้กปๆ˜พๅผ้”ไฝใ€‚"""
    assert R.passed_for(C.WARN) is False
    assert R.passed_for(C.PASS) is True


def test_build_report_pulls_channels_from_streams():
    """ๆฒกๆ˜พๅผไผ  channels ๆ—ถ,ไปŽๅ„ๆกๆต่‡ชๅทฑๅฃฐๆ˜Ž็š„ไฟก้“ๆฑ‡ๆ€ปใ€‚"""
    rep = R.build_report([_stream("a", C.PASS, channels=("rgb", "time", "tactile"))])
    rows = {row["use"]: row for row in rep["training_use"]}
    assert rows["vision_tactile_joint"]["status"] == C.PASS


def test_empty_report_is_fail_closed():
    """ๆฒกๆœ‰ๆ•ฐๆฎ โ‰  ๆ•ฐๆฎๅˆๆ ผใ€‚"""
    rep = R.empty_report(episode_id="ep1", reason="no_data")
    assert rep["passed"] is False
    assert rep["episode"]["status"] == C.ERROR
    assert rep["reason"] == "no_data"
    assert rep["streams"] == []


if __name__ == "__main__":
    import inspect
    import tempfile

    _failures = 0
    for _name, _fn in sorted(globals().items()):
        if not _name.startswith("test_") or not callable(_fn):
            continue
        try:
            if "tmp_path" in inspect.signature(_fn).parameters:
                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)