Hour classification (PR-4): - Powered-off: POH delta < 90% of wall-clock span - Active: DUW delta >= 256 MiB - Idle: powered on + sampled + below active threshold - Unknown: unsampled without POH evidence - Four splits sum to exactly wall_clock_seconds - Disabled time is never an hour state Monitoring periods (FL-8): - ensure_period_open: opens period at run moment if none exists - close_period: closes with end cause - is_inside_period: checks timestamp against period bounds - Never backdated; wall-clock outside periods excluded from denominator Day aggregates (ST-4, PR-5): - Derived monotonically from hour rows - UTC-bounded; no 23/25-hour days - Coverage: known seconds / period wall-clock - Gap hours inside periods contribute unknown seconds - Hours outside periods excluded entirely - No absent hour interpolated/estimated/fabricated (FL-3) Raw sample pruning (ST-5): - Prunes samples older than 14 days - Hour observations and day aggregates retained indefinitely Closes #22
105 lines
3.7 KiB
Python
105 lines
3.7 KiB
Python
"""Raw sample pruning tests.
|
|
|
|
Spec §3.4, ST-5: Raw samples pruned to 14 days; hour observations and
|
|
day aggregates retained indefinitely.
|
|
"""
|
|
import sqlite3
|
|
import sys
|
|
from datetime import datetime, timezone, timedelta
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
|
|
|
|
from fenris.store import init_store
|
|
from fenris.pruning import prune_old_samples
|
|
|
|
|
|
@pytest.fixture
|
|
def store_conn(tmp_path: Path):
|
|
db_path = tmp_path / "test.db"
|
|
conn = init_store(db_path)
|
|
yield conn
|
|
conn.close()
|
|
|
|
|
|
def _insert_sample(conn, ts_iso, device="/dev/nvme0"):
|
|
conn.execute(
|
|
"INSERT INTO samples (ts, device, data_units_written, data_units_read, "
|
|
" bytes_written, bytes_read, percentage_used) VALUES (?, ?, 0, 0, 0, 0, 0)",
|
|
(ts_iso, device),
|
|
)
|
|
conn.commit()
|
|
|
|
|
|
class TestPruneOldSamples:
|
|
"""Spec §3.4: Raw samples pruned to 14 days."""
|
|
|
|
def test_keeps_recent_samples(self, store_conn):
|
|
now = datetime(2026, 9, 15, 12, 0, 0, tzinfo=timezone.utc)
|
|
# Insert a sample 1 day ago
|
|
ts = (now - timedelta(days=1)).isoformat()
|
|
_insert_sample(store_conn, ts)
|
|
|
|
pruned = prune_old_samples(store_conn, now, retention_days=14)
|
|
assert pruned == 0
|
|
|
|
cursor = store_conn.execute("SELECT COUNT(*) FROM samples")
|
|
assert cursor.fetchone()[0] == 1
|
|
|
|
def test_removes_old_samples(self, store_conn):
|
|
now = datetime(2026, 9, 15, 12, 0, 0, tzinfo=timezone.utc)
|
|
# Insert samples at 10, 14, and 15 days ago
|
|
for days_ago in [10, 14, 15]:
|
|
ts = (now - timedelta(days=days_ago)).isoformat()
|
|
_insert_sample(store_conn, ts)
|
|
|
|
pruned = prune_old_samples(store_conn, now, retention_days=14)
|
|
assert pruned == 1 # Only the 15-day-old sample removed
|
|
|
|
cursor = store_conn.execute("SELECT COUNT(*) FROM samples")
|
|
assert cursor.fetchone()[0] == 2
|
|
|
|
def test_removes_many_old_samples(self, store_conn):
|
|
now = datetime(2026, 9, 15, 12, 0, 0, tzinfo=timezone.utc)
|
|
for days_ago in range(1, 30):
|
|
ts = (now - timedelta(days=days_ago)).isoformat()
|
|
_insert_sample(store_conn, ts)
|
|
|
|
pruned = prune_old_samples(store_conn, now, retention_days=14)
|
|
assert pruned == 15 # Days 15-29 removed
|
|
|
|
cursor = store_conn.execute("SELECT COUNT(*) FROM samples")
|
|
assert cursor.fetchone()[0] == 14 # Days 1-14 kept
|
|
|
|
def test_empty_store_no_error(self, store_conn):
|
|
now = datetime(2026, 9, 15, 12, 0, 0, tzinfo=timezone.utc)
|
|
pruned = prune_old_samples(store_conn, now, retention_days=14)
|
|
assert pruned == 0
|
|
|
|
def test_hour_observations_not_pruned(self, store_conn):
|
|
"""Hour observations are retained indefinitely."""
|
|
now = datetime(2026, 9, 15, 12, 0, 0, tzinfo=timezone.utc)
|
|
# Insert an old sample and a recent sample
|
|
_insert_sample(store_conn, (now - timedelta(days=20)).isoformat())
|
|
_insert_sample(store_conn, (now - timedelta(days=1)).isoformat())
|
|
|
|
# Insert an old hour observation
|
|
store_conn.execute(
|
|
"INSERT INTO hour_observations (hour, active_seconds, sample_count) "
|
|
"VALUES (?, 3600, 1)",
|
|
((now - timedelta(days=20)).replace(hour=0, minute=0, second=0).isoformat(),),
|
|
)
|
|
store_conn.commit()
|
|
|
|
prune_old_samples(store_conn, now, retention_days=14)
|
|
|
|
# Sample removed
|
|
cursor = store_conn.execute("SELECT COUNT(*) FROM samples")
|
|
assert cursor.fetchone()[0] == 1
|
|
|
|
# Hour observation retained
|
|
cursor = store_conn.execute("SELECT COUNT(*) FROM hour_observations")
|
|
assert cursor.fetchone()[0] == 1
|