forked from ChelseaKR/olive-bark-logger
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtest_store_durability.py
More file actions
332 lines (285 loc) · 12.4 KB
/
Copy pathtest_store_durability.py
File metadata and controls
332 lines (285 loc) · 12.4 KB
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
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
"""SQLite durability, migrations, sessions (lineage), and retention pruning."""
from __future__ import annotations
import sqlite3
from monitor.detector import Event
from store import EventStore
from store.db import _MIGRATIONS, SCHEMA_VERSION
def _ev(start: float) -> Event:
return Event(start=start, end=start + 1, duration=1.0, peak_level=-10.0, avg_level=-14.0)
def test_wal_mode_enabled(tmp_path):
with EventStore(tmp_path / "olive.db") as store:
mode = store._conn.execute("PRAGMA journal_mode").fetchone()[0]
assert mode.lower() == "wal"
def test_fresh_db_is_at_latest_schema_version(tmp_path):
with EventStore(tmp_path / "olive.db") as store:
v = store._conn.execute("PRAGMA user_version").fetchone()[0]
assert v == SCHEMA_VERSION
assert store.integrity_ok()
def test_old_v1_database_upgrades_in_place(tmp_path):
db = tmp_path / "olive.db"
# Hand-build a v1 database (events + calibration only, user_version=1).
conn = sqlite3.connect(db)
conn.executescript(_MIGRATIONS[0])
conn.execute("PRAGMA user_version = 1")
conn.execute(
"INSERT INTO events (start, end, duration, peak_level, avg_level) VALUES (1,2,1,-9,-12)"
)
conn.commit()
conn.close()
# Opening it should migrate to the latest schema and keep the old row.
with EventStore(db) as store:
assert store._conn.execute("PRAGMA user_version").fetchone()[0] == SCHEMA_VERSION
assert len(store.events()) == 1
assert store.latest_session() is None # no sessions yet, but the table exists
store._conn.execute("SELECT session_id FROM events") # column now present
def test_old_v2_database_upgrades_and_gains_clock_anomalies(tmp_path):
db = tmp_path / "olive.db"
# Hand-build a v2 database (events + calibration + sessions, user_version=2).
conn = sqlite3.connect(db)
conn.executescript(_MIGRATIONS[0])
conn.executescript(_MIGRATIONS[1])
conn.execute("PRAGMA user_version = 2")
conn.commit()
conn.close()
with EventStore(db) as store:
assert store._conn.execute("PRAGMA user_version").fetchone()[0] == SCHEMA_VERSION
# The new table exists and is queryable, empty to start.
assert store.clock_anomalies() == []
store.add_clock_anomaly(
session_id=None,
kind="forward-jump",
wall_before=1000.0,
wall_after=8200.0,
delta=7200.0,
detected_at=8200.0,
)
assert len(store.clock_anomalies()) == 1
assert store.integrity_ok()
def test_migrations_record_applied_timestamps(tmp_path):
"""`schema_migrations` records when each migration ran. The v3 timestamp is the
forensic era boundary between rows that may carry a baked-in calibration offset
(pre-v3 binaries) and raw-level rows (ADR-0003)."""
import time
# Fresh DB: every migration ran now, so every version has a timestamp.
before = time.time()
with EventStore(tmp_path / "fresh.db") as store:
for v in range(1, SCHEMA_VERSION + 1):
at = store.migration_applied_at(v)
assert at is not None
assert before <= at <= time.time()
assert store.migration_applied_at(99) is None # never applied
# A v2-era DB upgraded in place: v1/v2 predate the bookkeeping (honestly unknown),
# v3 records the upgrade moment — the baked-era/raw-era boundary for this database.
legacy = tmp_path / "legacy.db"
conn = sqlite3.connect(legacy)
conn.executescript(_MIGRATIONS[0])
conn.executescript(_MIGRATIONS[1])
conn.execute("PRAGMA user_version = 2")
conn.commit()
conn.close()
upgrade_start = time.time()
with EventStore(legacy) as store:
assert store.migration_applied_at(1) is None
assert store.migration_applied_at(2) is None
at3 = store.migration_applied_at(3)
assert at3 is not None
assert upgrade_start <= at3 <= time.time()
def test_old_v2_database_upgrades_to_v3_preserving_offset_as_epoch_zero(tmp_path):
db = tmp_path / "olive.db"
# Hand-build a v2 database (events + calibration + sessions) with a single legacy
# calibration row, user_version=2.
conn = sqlite3.connect(db)
conn.executescript(_MIGRATIONS[0])
conn.executescript(_MIGRATIONS[1])
conn.execute("PRAGMA user_version = 2")
conn.execute("INSERT INTO calibration (id, offset, note) VALUES (1, 7.5, 'bench cal')")
conn.commit()
conn.close()
# Opening it migrates to v3 and preserves the old offset as the first epoch (from 0).
with EventStore(db) as store:
assert store._conn.execute("PRAGMA user_version").fetchone()[0] == SCHEMA_VERSION
history = store.calibration_history()
assert len(history) == 1
epoch = history[0]
assert epoch.effective_from == 0.0
assert epoch.offset == 7.5
assert epoch.note == "bench cal"
assert epoch.reference_instrument is None
# Backward-compat accessor still returns the latest (offset, note).
assert store.get_calibration() == (7.5, "bench cal")
# The offset is in force for any timestamp at/after the record start.
assert store.calibration_at(0.0) == 7.5
assert store.calibration_at(10_000.0) == 7.5
def test_add_calibration_is_append_only_and_time_addressable(tmp_path):
with EventStore(tmp_path / "olive.db") as store:
assert store.calibration_history() == []
assert store.calibration_at(100.0) is None # empty -> caller falls back to config
store.add_calibration(3.0, "first", effective_from=100.0)
store.add_calibration(9.0, "second", reference_instrument="B&K 2250", effective_from=500.0)
history = store.calibration_history()
assert [e.offset for e in history] == [3.0, 9.0] # append-only, oldest first
assert history[1].reference_instrument == "B&K 2250"
# get_calibration returns the latest epoch for backward compat.
assert store.get_calibration() == (9.0, "second")
# calibration_at resolves the epoch in force at each instant.
assert store.calibration_at(50.0) == 3.0 # before first epoch -> earliest offset
assert store.calibration_at(100.0) == 3.0
assert store.calibration_at(300.0) == 3.0
assert store.calibration_at(500.0) == 9.0
assert store.calibration_at(9_999.0) == 9.0
def test_old_v6_database_gains_anatomy_columns_and_reads_none(tmp_path):
db = tmp_path / "olive.db"
conn = sqlite3.connect(db)
for migration in _MIGRATIONS[:6]:
conn.executescript(migration)
conn.execute("PRAGMA user_version = 6")
conn.execute(
"INSERT INTO events (start, end, duration, peak_level, avg_level) VALUES (5,7,2,-8,-11)"
)
conn.commit()
conn.close()
with EventStore(db) as store:
assert store._conn.execute("PRAGMA user_version").fetchone()[0] == SCHEMA_VERSION
cols = {row[1] for row in store._conn.execute("PRAGMA table_info(events)")}
assert {"rise_time_s", "loud6_s", "longest_run_s"} <= cols
(event,) = store.events()
assert event.rise_time_s is None
assert event.loud6_s is None
assert event.longest_run_s is None
def test_anatomy_round_trips_through_store(tmp_path):
with EventStore(tmp_path / "olive.db") as store:
store.add_event(
Event(
start=1.0,
end=3.0,
duration=2.0,
peak_level=-8.0,
avg_level=-12.0,
rise_time_s=0.4,
loud6_s=1.6,
longest_run_s=1.9,
)
)
(event,) = store.events()
assert event.rise_time_s == 0.4
assert event.loud6_s == 1.6
assert event.longest_run_s == 1.9
def test_crash_recovery_second_connection_reads_committed_events(tmp_path):
db = tmp_path / "olive.db"
store1 = EventStore(db)
for t in (10.0, 20.0, 30.0):
store1.add_event(_ev(t))
# Simulate an ungraceful exit: open a *second* connection without closing the first.
store2 = EventStore(db)
assert len(store2.events()) == 3
assert store2.integrity_ok()
store1.close()
store2.close()
def test_prune_removes_old_events(tmp_path):
with EventStore(tmp_path / "olive.db") as store:
for t in (100.0, 200.0, 300.0, 400.0):
store.add_event(_ev(t))
removed = store.prune(before=250.0)
assert removed.events == 2
assert removed.total == 2 # nothing else in the store to reach
remaining = [e.start for e in store.events()]
assert remaining == [300.0, 400.0]
def test_session_lifecycle(tmp_path):
with EventStore(tmp_path / "olive.db") as store:
sid = store.start_session(
started_at=1000.0,
device_label="pi-1",
mic_model="USB mic",
placement_note="by the wall",
tz="America/Los_Angeles",
calibration_offset=3.0,
calibration_note="bench",
app_version="0.1.0",
)
store.add_event(_ev(1001.0), session_id=sid)
store.update_session(sid, frames_seen=950, frames_dropped=50, ended_at=2000.0)
s = store.latest_session()
assert s is not None
assert s.id == sid
assert s.device_label == "pi-1"
assert s.ended_at == 2000.0
assert s.frames_seen == 950
assert s.frame_coverage == 0.95
def test_latest_session_none_when_empty(tmp_path):
with EventStore(tmp_path / "olive.db") as store:
assert store.latest_session() is None
def test_v2_database_upgrades_and_legacy_session_params_are_none(tmp_path):
db = tmp_path / "olive.db"
# Hand-build a v2 database (events + calibration + sessions, no detection-param
# columns) with one legacy session row, then open it through EventStore.
conn = sqlite3.connect(db)
conn.executescript(_MIGRATIONS[0])
conn.executescript(_MIGRATIONS[1])
conn.execute("PRAGMA user_version = 2")
conn.execute(
"INSERT INTO sessions (started_at, device_label, mic_model, placement_note, tz, "
"calibration_offset, calibration_note, app_version) "
"VALUES (500, 'pi-legacy', 'mic', 'wall', 'UTC', 0.0, 'note', '0.0.1')"
)
conn.commit()
conn.close()
with EventStore(db) as store:
assert store._conn.execute("PRAGMA user_version").fetchone()[0] == SCHEMA_VERSION
# The five new columns exist and the legacy row reads them back as None.
cols = {r[1] for r in store._conn.execute("PRAGMA table_info(sessions)")}
assert {
"threshold_dbfs",
"min_duration_s",
"debounce_s",
"sample_rate",
"frame_size",
} <= cols
s = store.latest_session()
assert s is not None
assert s.device_label == "pi-legacy"
assert s.threshold_dbfs is None
assert s.min_duration_s is None
assert s.debounce_s is None
assert s.sample_rate is None
assert s.frame_size is None
def test_start_session_round_trips_detection_params(tmp_path):
with EventStore(tmp_path / "olive.db") as store:
sid = store.start_session(
started_at=1000.0,
device_label="pi-1",
mic_model="USB mic",
placement_note="by the wall",
tz="UTC",
calibration_offset=0.0,
calibration_note="bench",
app_version="0.1.0",
threshold_dbfs=-42.0,
min_duration_s=0.5,
debounce_s=1.5,
sample_rate=22050,
frame_size=2205,
)
s = store.latest_session()
assert s is not None
assert s.id == sid
assert s.threshold_dbfs == -42.0
assert s.min_duration_s == 0.5
assert s.debounce_s == 1.5
assert s.sample_rate == 22050
assert s.frame_size == 2205
def test_sessions_returns_all_ordered_oldest_first(tmp_path):
with EventStore(tmp_path / "olive.db") as store:
common = dict(
device_label="pi-1",
mic_model="mic",
placement_note="wall",
tz="UTC",
calibration_offset=0.0,
calibration_note="note",
app_version="0.1.0",
)
store.start_session(started_at=3000.0, threshold_dbfs=-30.0, **common)
store.start_session(started_at=1000.0, threshold_dbfs=-40.0, **common)
store.start_session(started_at=2000.0, threshold_dbfs=-35.0, **common)
starts = [s.started_at for s in store.sessions()]
assert starts == [1000.0, 2000.0, 3000.0]