umbau 1.0
This commit is contained in:
108
tests/test_session_recorder.py
Normal file
108
tests/test_session_recorder.py
Normal file
@ -0,0 +1,108 @@
|
||||
"""Tests fuer app/recordings/recorder.py (Umsetzungsauftrag Teil A D2 /
|
||||
Teil E E2): Puffer/Executor-Auslagerung, Hash-Kette, Rotation und
|
||||
Groessenbegrenzung."""
|
||||
import asyncio
|
||||
import json
|
||||
|
||||
import pytest
|
||||
|
||||
from app.recordings import recorder as rec_mod
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _isolated_recordings_dir(tmp_path, monkeypatch):
|
||||
"""settings ist ein Prozess-Singleton (siehe conftest.py) -- fuer
|
||||
Testisolation wird recordings_dir direkt umgebogen statt ueber die
|
||||
Umgebungsvariable (die nach dem ersten Import wirkungslos waere)."""
|
||||
from app.config import settings
|
||||
|
||||
recordings_dir = tmp_path / "recordings"
|
||||
recordings_dir.mkdir(parents=True, exist_ok=True)
|
||||
monkeypatch.setattr(settings, "recordings_dir", recordings_dir)
|
||||
monkeypatch.setattr(rec_mod.SessionRecorder, "FLUSH_INTERVAL_S", 0.02)
|
||||
yield
|
||||
|
||||
|
||||
async def test_record_is_non_blocking_and_flushes_via_background_task():
|
||||
rec = rec_mod.SessionRecorder(session_id=1)
|
||||
for i in range(10):
|
||||
rec.record("output", f"frame-{i}")
|
||||
with open(rec.path, encoding="utf-8") as fh:
|
||||
assert fh.read() == "" # record() darf nicht sofort schreiben
|
||||
|
||||
await asyncio.sleep(0.2)
|
||||
with open(rec.path, encoding="utf-8") as fh:
|
||||
lines = [l for l in fh if l.strip()]
|
||||
assert len(lines) == 10
|
||||
assert rec_mod.verify_recording(rec.path)
|
||||
await rec.aclose()
|
||||
|
||||
|
||||
async def test_aclose_flushes_pending_buffer_even_with_long_interval():
|
||||
rec_mod.SessionRecorder.FLUSH_INTERVAL_S = 100
|
||||
rec = rec_mod.SessionRecorder(session_id=2)
|
||||
for i in range(5):
|
||||
rec.record("input", f"x{i}")
|
||||
await rec.aclose()
|
||||
with open(rec.path, encoding="utf-8") as fh:
|
||||
lines = [l for l in fh if l.strip()]
|
||||
assert len(lines) == 5
|
||||
assert rec_mod.verify_recording(rec.path)
|
||||
|
||||
|
||||
async def test_concurrent_recorders_do_not_interfere():
|
||||
recs = [rec_mod.SessionRecorder(session_id=100 + i) for i in range(5)]
|
||||
for i, r in enumerate(recs):
|
||||
for j in range(20):
|
||||
r.record("output", f"s{i}-{j}")
|
||||
await asyncio.sleep(0.3)
|
||||
for r in recs:
|
||||
with open(r.path, encoding="utf-8") as fh:
|
||||
lines = [l for l in fh if l.strip()]
|
||||
assert len(lines) == 20
|
||||
assert rec_mod.verify_recording(r.path)
|
||||
await r.aclose()
|
||||
|
||||
|
||||
async def test_rotation_splits_into_multiple_valid_parts():
|
||||
from app.config import settings
|
||||
|
||||
settings.recording_max_part_bytes = 400
|
||||
settings.recording_max_total_bytes = 10_000_000
|
||||
rec = rec_mod.SessionRecorder(session_id=3)
|
||||
rec._max_part_bytes = 400
|
||||
rec._max_total_bytes = 10_000_000
|
||||
for i in range(60):
|
||||
rec.record("output", f"payload-{i:04d}-" + ("x" * 20))
|
||||
await rec.aclose()
|
||||
|
||||
parts = list(rec_mod._iter_part_paths(rec.path))
|
||||
assert len(parts) > 1
|
||||
assert rec_mod.verify_recording_set(rec.path)
|
||||
assert rec_mod.count_recording_entries(rec.path) == 60
|
||||
|
||||
for p in parts:
|
||||
with open(p, encoding="utf-8") as fh:
|
||||
first_line = next((l for l in fh if l.strip()), None)
|
||||
assert first_line is not None
|
||||
assert json.loads(first_line)["prev_hash"] == rec_mod.GENESIS_HASH
|
||||
|
||||
|
||||
async def test_total_size_limit_truncates_and_stops_accepting_entries():
|
||||
rec = rec_mod.SessionRecorder(session_id=4)
|
||||
rec._max_part_bytes = 10_000_000
|
||||
rec._max_total_bytes = 500
|
||||
for i in range(50):
|
||||
rec.record("output", f"payload-{i:04d}-" + ("y" * 20))
|
||||
await rec.aclose()
|
||||
|
||||
total_entries = rec_mod.count_recording_entries(rec.path)
|
||||
assert 0 < total_entries < 50
|
||||
assert rec_mod.verify_recording_set(rec.path)
|
||||
|
||||
entries = list(rec_mod.iter_recording_entries(rec.path))
|
||||
assert "truncated" in entries[-1]["data"]
|
||||
|
||||
rec.record("output", "should-be-dropped")
|
||||
await asyncio.sleep(0.1)
|
||||
assert rec_mod.count_recording_entries(rec.path) == total_entries
|
||||
Reference in New Issue
Block a user