Coverage for tests/io/test_phase1_regressions.py: 100%

95 statements  

« prev     ^ index     » next       coverage.py v7.15.1, created at 2026-07-18 05:24 +0000

1"""Regression tests for the Phase-1 correctness fixes. 

2 

3Each test pins one previously-broken behaviour: 

4- tiny chunk_size used to underflow the C parsers' capacity headroom 

5 (size_t wrap => heap corruption hazard); 

6- delta_t fast-path overshoot (``_dt_carry``) used to be invisible to 

7 mixed-mode reads and ``read_all()`` (silent event loss); 

8- ``max_events`` was not actually enforced within one C delta_t call; 

9- ``EventWriter.write()`` silently dropped its ``triggers`` argument; 

10- EVT2.1 with a non-legacy endianness header decoded silently into garbage. 

11""" 

12import numpy as np 

13import pytest 

14 

15from typing import Any 

16 

17 

18def _uniform_events(n: int = 5000, dt: int = 13) -> Any: 

19 ev = np.zeros(n, dtype=np.dtype([('t', np.int64), ('x', np.uint16), 

20 ('y', np.uint16), ('p', np.uint8)])) 

21 ev['t'] = np.arange(n, dtype=np.int64) * dt 

22 ev['x'] = np.arange(n) % 1280 

23 ev['y'] = np.arange(n) % 720 

24 ev['p'] = np.arange(n) % 2 

25 return ev 

26 

27 

28def _write(tmp_path: Any, fmt: str, ev: Any) -> Any: 

29 from evutils.io import EventWriter 

30 p = tmp_path / f"reg_{fmt}.raw" 

31 with EventWriter(p, format=fmt) as w: 

32 w.write(ev) 

33 return p 

34 

35 

36#################################### 

37# C1: tiny chunk_size 

38#################################### 

39 

40def test_tiny_chunk_size_rejected(tmp_path: Any) -> None: 

41 from evutils.io import EventReader 

42 p = _write(tmp_path, "evt3", _uniform_events(100)) 

43 with pytest.raises(ValueError, match="chunk_size"): 

44 EventReader(p, n_events=10, chunk_size=16).read() 

45 

46 

47def test_minimal_chunk_size_decodes(tmp_path: Any) -> None: 

48 """chunk_size=128 (the minimum) must decode the whole stream, not hang or 

49 corrupt: the parser's capacity headroom (64) stays below the buffer size.""" 

50 from evutils.io import EventReader 

51 ev = _uniform_events(1000) 

52 p = _write(tmp_path, "evt3", ev) 

53 total = 0 

54 with EventReader(p, n_events=200, chunk_size=128) as r: 

55 for c in r: 

56 total += len(c) 

57 assert total == len(ev) 

58 

59 

60#################################### 

61# C3: carry visibility on mixed paths 

62#################################### 

63 

64@pytest.mark.parametrize("fmt", ["evt2", "evt3"]) 

65def test_mixed_delta_t_then_read_all_loses_nothing(tmp_path: Any, fmt: str) -> None: 

66 from evutils.io import EventReader 

67 ev = _uniform_events(5000) 

68 p = _write(tmp_path, fmt, ev) 

69 got = [] 

70 with EventReader(p, delta_t=10_000) as r: 

71 # a few fast-path windows... 

72 for _ in range(3): 

73 got.append(np.asarray(r.read()).copy()) 

74 # ...then drain the rest at once: the fast-path overshoot carry must be 

75 # folded in, not dropped. 

76 got.append(np.asarray(r.read_all()).copy()) 

77 out = np.concatenate([g for g in got if len(g)]) 

78 assert len(out) == len(ev) 

79 assert np.array_equal(out['t'], ev['t']) 

80 

81 

82@pytest.mark.parametrize("fmt", ["evt2", "evt3"]) 

83def test_mixed_delta_t_then_n_events_override(tmp_path: Any, fmt: str) -> None: 

84 from evutils.io import EventReader 

85 ev = _uniform_events(5000) 

86 p = _write(tmp_path, fmt, ev) 

87 got = [] 

88 with EventReader(p, delta_t=10_000) as r: 

89 got.append(np.asarray(r.read()).copy()) # fast path window 

90 got.append(np.asarray(r.read(n_events=700)).copy()) # override -> acc path 

91 while True: # drain 

92 c = np.asarray(r.read()) 

93 if len(c) == 0: 

94 break 

95 got.append(c.copy()) 

96 out = np.concatenate([g for g in got if len(g)]) 

97 assert len(out) == len(ev) 

98 assert np.array_equal(out['t'], ev['t']) 

99 

100 

101#################################### 

102# C4: max_events honoured in delta_t mode 

103#################################### 

104 

105def test_delta_t_windows_respect_max_events(tmp_path: Any) -> None: 

106 from evutils.io import EventReader 

107 ev = _uniform_events(5000) 

108 p = _write(tmp_path, "evt3", ev) 

109 total = 0 

110 # One delta_t window would span the whole file; the cap must split it. 

111 with EventReader(p, delta_t=10_000_000, max_events=1000) as r: 

112 for c in r: 

113 assert len(c) <= 1000 

114 total += len(c) 

115 assert total == len(ev) 

116 

117 

118#################################### 

119# C5: EventWriter.write forwards triggers 

120#################################### 

121 

122def test_writer_warns_on_unsupported_triggers(tmp_path: Any) -> None: 

123 """No encoder implements trigger *encoding* yet, so the honest contract is: 

124 EventWriter forwards triggers to the encoder and warns LOUDLY (once) when 

125 the encoder cannot store them -- never a silent drop. When an encoder gains 

126 trigger support (SUPPORTS_WRITE_TRIGGERS = True) this becomes a real 

127 round-trip test.""" 

128 from evutils.io import EventReader, EventWriter 

129 from evutils.types import TriggerArray 

130 ev = _uniform_events(2000) 

131 tr = TriggerArray(t=[100, 5000, 20_000], p=[1, 0, 1], id=[0, 1, 0]) 

132 p = tmp_path / "trig.raw" 

133 with EventWriter(p, format="evt3") as w: 

134 with pytest.warns(UserWarning, match="triggers"): 

135 w.write(ev, triggers=tr) 

136 w.write(ev, triggers=tr) # warning fires only once 

137 with EventReader(p) as r: 

138 out = r.read_all() 

139 assert len(out) == 2 * len(ev) # events themselves are unaffected 

140 

141 

142#################################### 

143# C9: EVT2.1 endianness guard 

144#################################### 

145 

146def test_evt21_non_legacy_endianness_rejected() -> None: 

147 from evutils.io import EventReader 

148 hdr = b"% evt 2.1\n% endianness little\n% geometry 1280x720\n% end\n" 

149 with pytest.raises(NotImplementedError, match="endianness"): 

150 EventReader(hdr).read() 

151 

152 

153def test_evt21_legacy_endianness_accepted(tmp_path: Any) -> None: 

154 from evutils.io import EventReader 

155 ev = _uniform_events(500) 

156 p = _write(tmp_path, "evt21", ev) # writer emits "% endianness legacy" 

157 with EventReader(p) as r: 

158 out = r.read_all() 

159 assert len(out) == len(ev)