Coverage for tests/io/test_real_files.py: 100%
49 statements
« prev ^ index » next coverage.py v7.15.1, created at 2026-07-15 09:33 +0000
« prev ^ index » next coverage.py v7.15.1, created at 2026-07-15 09:33 +0000
1"""Format-agnostic checks against the real downloaded recordings.
3Every file in the reference tarball ships a JSON sidecar (produced by
4``scripts/generate_metadata.py``) carrying the ground-truth ``format``,
5``resolution``, event/polarity/trigger counts, etc. This module discovers
6*all* of them -- whatever the format -- and verifies the decoder output
7against that ground truth.
9Adding a new format is zero-code: drop the recording plus its JSON sidecar
10into the tarball and it is exercised here automatically. The per-file
11parametrization (fixture ``real_event_file``) is generated in
12``tests/conftest.py::pytest_generate_tests``.
13"""
14import numpy as np
15import pytest
17from typing import Any
19# Recordings whose Metavision reference counts differ slightly from a
20# byte-exact decode: Metavision filters a handful of events, while our decoder
21# matches expelliarmus exactly. Allow a tiny relative tolerance on *event*
22# counts for these (triggers are always exact). See tests/io/test_evt.py for
23# the investigation behind each number.
24_COUNT_TOLERANT_FILES = {
25 'evt2_195_falling_particles.raw', # +14
26 'evt3_flash.raw', # +3345
27 'evt3_laser.raw', # +132
28}
31def test_real_file_matches_metadata(real_event_file: Any) -> None:
32 """Decode a real recording end-to-end and check it against its JSON sidecar.
34 Verifies (only what the metadata provides, so it stays format-agnostic):
35 sensor resolution, coordinate bounds, total/positive/negative event counts,
36 external-trigger counts, and an overall upward timestamp trend.
37 """
38 from evutils.io import EventReader
39 from evutils.io.decoders import get_reader_from_filename
41 ef = real_event_file
42 meta = ef.metadata
44 # Only ask for triggers from formats whose decoder can actually read them;
45 # for the rest we instead assert the metadata isn't hiding any (below).
46 supports_triggers = getattr(
47 get_reader_from_filename(ef.path), "SUPPORTS_EXT_TRIGGERS", False
48 )
50 n = n_pos = n_neg = 0
51 n_tr_pos = n_tr_neg = 0
52 x_max = y_max = -1
53 first_t = last_t = None
55 with EventReader(ef.path, ext_trigger=supports_triggers, chunk_size=10_000_000) as reader:
56 shape = list(reader.shape())
57 for chunk in reader:
58 ev, tr = chunk if isinstance(chunk, tuple) else (chunk, None)
59 if len(ev):
60 n += len(ev)
61 n_pos += int(np.count_nonzero(ev["p"] == 1))
62 n_neg += int(np.count_nonzero(ev["p"] == 0))
63 x_max = max(x_max, int(ev["x"].max()))
64 y_max = max(y_max, int(ev["y"].max()))
65 if first_t is None:
66 first_t = int(ev["t"][0])
67 last_t = int(ev["t"][-1])
68 if tr is not None and len(tr):
69 n_tr_pos += int(np.count_nonzero(tr["p"] == 1))
70 n_tr_neg += int(np.count_nonzero(tr["p"] == 0))
72 assert n > 0, f"{ef.path.name}: decoded no events"
74 def check(name: str, actual: int, expected: int, tol: int = 0) -> None:
75 assert abs(actual - expected) <= tol, (
76 f"{ef.path.name}: {name} actual {actual} != expected {expected}"
77 + (f" (tolerance {tol})" if tol else "")
78 )
80 # --- resolution ---
81 width, height = meta["resolution"]
82 if shape != [None, None]:
83 assert shape == meta["resolution"], (
84 f"{ef.path.name}: shape {shape} != metadata {meta['resolution']}"
85 )
86 assert x_max < width, f"{ef.path.name}: x max {x_max} >= width {width}"
87 assert y_max < height, f"{ef.path.name}: y max {y_max} >= height {height}"
89 # --- event counts (exact, save for the known Metavision-filtered files) ---
90 rtol = 1e-3 if ef.path.name in _COUNT_TOLERANT_FILES else 0.0
91 check("total events", n, meta["count"], int(meta["count"] * rtol))
92 check("positive events", n_pos, meta["pos_count"], int(meta["pos_count"] * rtol))
93 check("negative events", n_neg, meta["neg_count"], int(meta["neg_count"] * rtol))
95 # --- external triggers ---
96 tr_meta = meta.get("external_triggers", {"total": 0, "positive": 0, "negative": 0})
97 if supports_triggers:
98 check("total triggers", n_tr_pos + n_tr_neg, tr_meta["total"])
99 check("positive triggers", n_tr_pos, tr_meta["positive"])
100 check("negative triggers", n_tr_neg, tr_meta["negative"])
101 else:
102 # A format we cannot read triggers from must not be hiding any.
103 assert tr_meta["total"] == 0, (
104 f"{ef.path.name}: metadata declares {tr_meta['total']} triggers but "
105 f"'{ef.path.suffix}' decoding cannot read them"
106 )
108 # --- timestamps trend upward overall (streams are not strictly monotonic) ---
109 assert first_t is not None and first_t <= last_t, (
110 f"{ef.path.name}: timestamps not increasing overall ({first_t} -> {last_t})"
111 )