#!/usr/bin/env python3 """Validate MP3 Player close-and-resume evidence captured from serial.""" from __future__ import annotations import argparse import re from pathlib import Path def evaluate_resume_trace(trace: str, fixture: str, minimum_position: int = 6) -> dict[str, int]: """Require real playback, persisted position, and same-file relaunch evidence.""" escaped = re.escape(fixture) initial = re.search(rf"Starting MP3 playback: {escaped} .* resume=0\b", trace) stream_opened = re.search(r"Audio stream opened: \d+ Hz, \d+ channels", trace) persisted = re.search(rf"History saved on hide: {escaped} pos (\d+) total \d+", trace) resumed = re.search(rf"Resuming last play {escaped} at (\d+) sec from history", trace) playback_matches = list(re.finditer(rf"Starting MP3 playback: {escaped} .* resume=(\d+)\b", trace)) resumed_playback = playback_matches[-1] if playback_matches else None if "Playback task stuck, force deleting" in trace: raise RuntimeError("forced playback task termination invalidates the device test") if not initial or not stream_opened: raise RuntimeError("missing verified playback evidence for the fixture") if not persisted or not resumed or not resumed_playback: raise RuntimeError("missing persisted or resumed playback evidence") persisted_position = int(persisted.group(1)) resumed_position = int(resumed.group(1)) resumed_playback_position = int(resumed_playback.group(1)) if persisted_position < minimum_position: raise RuntimeError(f"persisted position {persisted_position} is below {minimum_position}") if resumed_position != persisted_position or resumed_playback_position != persisted_position: raise RuntimeError("relaunch did not resume the persisted position on the same file") return {"persisted_position": persisted_position, "resumed_position": resumed_position} def main() -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--trace", type=Path, required=True, help="Captured serial log") parser.add_argument("--fixture", required=True, help="Absolute fixture path logged by MP3 Player") parser.add_argument("--minimum-position", type=int, default=6) args = parser.parse_args() result = evaluate_resume_trace(args.trace.read_text(encoding="utf-8", errors="replace"), args.fixture, args.minimum_position) print(f"PASS persisted_position={result['persisted_position']} resumed_position={result['resumed_position']}") return 0 if __name__ == "__main__": raise SystemExit(main())