forked from ChelseaKR/olive-bark-logger
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtest_report_content.py
More file actions
280 lines (224 loc) · 9.55 KB
/
Copy pathtest_report_content.py
File metadata and controls
280 lines (224 loc) · 9.55 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
"""Merge-blocking: the report carries methodology + limitations and honest framing."""
from __future__ import annotations
from datetime import datetime, timezone
from monitor.config import Config
from monitor.detector import Event
from report.aggregate import summarize
from report.render import (
LIMITATIONS_HEADING,
METHODOLOGY_HEADING,
NO_CLOCK_ANOMALY_NOTE,
NO_SOURCE_NOTE,
PARAM_CHANGE_NOTE,
RELATIVE_DBFS_NOTE,
build_report,
generate_report_from_db,
)
def _report(events=None, **cfg):
config = Config(**cfg)
summary = summarize(events or [], quiet_hours=config.quiet_hours, tz=config.tzinfo())
return build_report(summary, config=config, generated_at="2026-01-01 00:00 UTC")
def test_has_methodology_and_limitations_headings():
html = _report()
assert f"<h2>{METHODOLOGY_HEADING}</h2>" in html
assert f"<h2>{LIMITATIONS_HEADING}</h2>" in html
def test_states_relative_dbfs_limitation():
html = _report()
assert RELATIVE_DBFS_NOTE in html
assert "relative" in html.lower() and "dbfs" in html.lower()
def test_states_no_source_attribution():
html = _report()
assert NO_SOURCE_NOTE in html
assert "cannot prove" in html.lower()
def test_states_no_audio_recorded():
html = _report()
assert "No audio was recorded" in html
def test_uncalibrated_is_disclosed():
html = _report(calibration_offset=0.0)
assert "No calibration offset is applied" in html
def test_calibrated_offset_is_disclosed():
html = _report(calibration_offset=12.5)
assert "+12.5 dB" in html
def test_reports_event_numbers():
start = datetime(2026, 1, 1, 23, tzinfo=timezone.utc).timestamp()
ev = Event(start=start, end=start + 5, duration=5.0, peak_level=-8.0, avg_level=-12.0)
html = _report([ev])
assert "Total events" in html
assert ">1<" in html # the count appears
def test_measurement_conditions_without_session():
html = _report()
assert "<h2>Measurement conditions</h2>" in html
assert "were not recorded" in html
def test_measurement_conditions_with_session():
from store import Session
session = Session(
id=1,
started_at=0.0,
ended_at=1.0,
device_label="pi-1",
mic_model="USB mic",
placement_note="by the wall",
tz="UTC",
calibration_offset=0.0,
calibration_note="x",
frames_seen=990,
frames_dropped=10,
app_version="0.1.0",
)
config = Config()
summary = summarize([], quiet_hours=config.quiet_hours, tz=config.tzinfo())
html = build_report(
summary, config=config, generated_at="2026-01-01 00:00 UTC", session=session
)
assert "pi-1" in html and "USB mic" in html and "by the wall" in html
assert "99.0%" in html # frame coverage
def test_event_types_section_appears_with_tags():
base = datetime(2026, 1, 1, 12, tzinfo=timezone.utc).timestamp()
events = [
Event(base, base + 2, 2.0, -8, -12, coarse_tag="bark-like"),
Event(base + 10, base + 11, 1.0, -9, -13, coarse_tag="ambient"),
Event(base + 20, base + 22, 2.0, -7, -11, coarse_tag="bark-like"),
]
html = _report(events)
assert "<h2>Event types (coarse hint)</h2>" in html
assert "bark-like" in html and "ambient" in html
assert "hint, not a fact" in html
def test_event_types_section_absent_without_tags():
base = datetime(2026, 1, 1, 12, tzinfo=timezone.utc).timestamp()
html = _report([Event(base, base + 2, 2.0, -8, -12)])
assert "Event types" not in html
def test_no_clock_anomalies_disclosed_by_default():
html = _report()
assert NO_CLOCK_ANOMALY_NOTE in html
def test_clock_anomaly_disclosure_line_appears_from_db(tmp_path):
from store import EventStore
db = tmp_path / "olive.db"
with EventStore(db) as store:
store.add_clock_anomaly(
session_id=None,
kind="forward-jump",
wall_before=1010.0,
wall_after=8210.0,
delta=7200.0,
detected_at=8210.0,
)
html = generate_report_from_db(str(db), Config(tz="UTC"), generated_at="2026-01-01 00:00 UTC")
assert "Clock jumped forward by 7200.0 s" in html
assert NO_CLOCK_ANOMALY_NOTE not in html
def _session(sid, started_at, **params):
from store import Session
return Session(
id=sid,
started_at=started_at,
ended_at=started_at + 100,
device_label="pi-1",
mic_model="USB mic",
placement_note="by the wall",
tz="UTC",
calibration_offset=0.0,
calibration_note="x",
frames_seen=100,
frames_dropped=0,
app_version="0.1.0",
**params,
)
def test_single_session_shows_no_parameter_epochs_table():
session = _session(1, 0.0, threshold_dbfs=-35.0, min_duration_s=0.4, debounce_s=1.0)
config = Config()
summary = summarize([], quiet_hours=config.quiet_hours, tz=config.tzinfo())
html = build_report(
summary,
config=config,
generated_at="2026-01-01 00:00 UTC",
session=session,
sessions=[session],
)
assert "Detection-parameter epochs" not in html
assert PARAM_CHANGE_NOTE not in html
def test_two_sessions_with_different_thresholds_render_epochs(tmp_path):
from monitor.detector import Event
from store import EventStore
db = tmp_path / "olive.db"
with EventStore(db) as store:
common = dict(
device_label="pi-1",
mic_model="USB mic",
placement_note="by the wall",
tz="UTC",
calibration_offset=0.0,
calibration_note="x",
app_version="0.1.0",
min_duration_s=0.4,
debounce_s=1.0,
sample_rate=16000,
frame_size=1600,
)
base = datetime(2026, 1, 1, 12, tzinfo=timezone.utc).timestamp()
s1 = store.start_session(started_at=base, threshold_dbfs=-35.0, **common)
store.add_event(Event(base + 1, base + 3, 2.0, -8, -12), session_id=s1)
later = datetime(2026, 1, 5, 12, tzinfo=timezone.utc).timestamp()
s2 = store.start_session(started_at=later, threshold_dbfs=-42.0, **common)
store.add_event(Event(later + 1, later + 3, 2.0, -20, -24), session_id=s2)
html = generate_report_from_db(str(db), Config(tz="UTC"), generated_at="2026-01-06 00:00 UTC")
assert "Detection-parameter epochs" in html
assert PARAM_CHANGE_NOTE in html
# Both thresholds in force during the record are named.
assert "-35 dBFS" in html
assert "-42 dBFS" in html
# Each epoch is dated by its first session's start.
assert "2026-01-01" in html and "2026-01-05" in html
def test_fmt_seconds_minutes_and_hours():
# Exercise the minute/hour formatting branches via long durations.
base = datetime(2026, 1, 1, 12, tzinfo=timezone.utc).timestamp()
long_event = Event(start=base, end=base + 4000, duration=4000.0, peak_level=-5, avg_level=-9)
html = _report([long_event])
assert " h" in html # 4000 s renders in hours
# --- R1: "what this can and cannot prove" cover page -------------------------
def test_cover_page_present_and_states_limits():
html = _report()
assert "<h2>What this can and cannot prove</h2>" in html
assert 'class="cover"' in html
assert "What it can show" in html and "What it cannot prove" in html
# The cover restates the headline limitations in lay terms.
assert "no source attribution" in html
assert "not the units an ordinance" in html
assert "is not the same as a violation" in html
assert "not legal advice" in html
# --- R2: calibration-honesty banner + provenance ----------------------------
def test_uncalibrated_banner_is_prominent():
html = _report(calibration_offset=0.0)
assert 'class="banner"' in html
assert "Uncalibrated — these readings are relative, not dB(A)." in html
assert 'role="note"' in html
def test_calibrated_banner_shows_provenance():
html = _report(
calibration_offset=12.5,
calibration_note="Ref: Brand X, IEC 61672 Class 2",
)
assert "banner-ok" in html
assert "+12.5 dB" in html
assert "Brand X, IEC 61672 Class 2" in html
# --- R3: quiet-hours duration rollup (no verdict) ---------------------------
def test_duration_rollup_reports_minutes_without_a_verdict():
base = datetime(2026, 1, 1, 23, tzinfo=timezone.utc).timestamp()
ev = Event(start=base, end=base + 120, duration=120.0, peak_level=-8.0, avg_level=-12.0)
html = _report([ev]) # 23:00 is within the default 22:00-08:00 window
assert "<h2>Quiet-hours duration rollup</h2>" in html
assert "Loud time within quiet hours, per day" in html
# The no-verdict line is mandatory and must be present verbatim in spirit.
assert "This is a measurement, not a determination" in html
assert "is not the same as a violation" in html
# Ordinance reference framing is hedged as jurisdiction-dependent.
assert "vary by jurisdiction" in html
def test_duration_rollup_empty_when_no_quiet_hours_events():
base = datetime(2026, 1, 1, 12, tzinfo=timezone.utc).timestamp()
ev = Event(start=base, end=base + 5, duration=5.0, peak_level=-8.0, avg_level=-12.0)
html = _report([ev]) # noon -> outside the quiet window
assert "<h2>Quiet-hours duration rollup</h2>" in html
assert "nothing to roll up" in html
# --- R5: reader-facing no-audio rationale -----------------------------------
def test_reader_facing_no_audio_rationale_present():
html = _report()
assert "<h2>Why there is deliberately no audio</h2>" in html
assert "deliberate privacy choice, not missing data" in html
assert "leaked, subpoenaed, or misused" in html