From 4bc9b3fd3f92e885028b712f674d7941fc6da783 Mon Sep 17 00:00:00 2001 From: Kharec Date: Wed, 18 Feb 2026 03:17:42 +0100 Subject: [PATCH] test: add focused playback helper unit coverage --- .../test_playback_chapter_selection.py | 34 ++++++++++ tests/playback/test_playback_elapsed_math.py | 21 ++++++ .../playback/test_playback_process_helpers.py | 67 +++++++++++++++++++ tests/playback/test_playback_seek_targets.py | 20 ++++++ 4 files changed, 142 insertions(+) create mode 100644 tests/playback/test_playback_chapter_selection.py create mode 100644 tests/playback/test_playback_elapsed_math.py create mode 100644 tests/playback/test_playback_process_helpers.py create mode 100644 tests/playback/test_playback_seek_targets.py diff --git a/tests/playback/test_playback_chapter_selection.py b/tests/playback/test_playback_chapter_selection.py new file mode 100644 index 0000000..0581aa7 --- /dev/null +++ b/tests/playback/test_playback_chapter_selection.py @@ -0,0 +1,34 @@ +from __future__ import annotations + +from auditui.playback.chapters import get_current_chapter, get_current_chapter_index + + +CHAPTERS = [ + {"title": "One", "start_time": 0.0, "end_time": 60.0}, + {"title": "Two", "start_time": 60.0, "end_time": 120.0}, +] + + +def test_get_current_chapter_handles_empty_chapter_list() -> None: + """Ensure empty chapter metadata still returns a sensible fallback row.""" + assert get_current_chapter(12.0, [], 300.0) == ("Unknown Chapter", 12.0, 300.0) + + +def test_get_current_chapter_returns_matching_chapter_window() -> None: + """Ensure chapter selection returns title and chapter-relative timing.""" + assert get_current_chapter(75.0, CHAPTERS, 120.0) == ("Two", 15.0, 60.0) + + +def test_get_current_chapter_falls_back_to_last_chapter() -> None: + """Ensure elapsed values past known ranges map to last chapter.""" + assert get_current_chapter(150.0, CHAPTERS, 200.0) == ("Two", 90.0, 60.0) + + +def test_get_current_chapter_index_returns_none_without_chapters() -> None: + """Ensure chapter index lookup returns None when no chapters exist.""" + assert get_current_chapter_index(10.0, []) is None + + +def test_get_current_chapter_index_returns_last_when_past_end() -> None: + """Ensure chapter index lookup falls back to the final chapter index.""" + assert get_current_chapter_index(200.0, CHAPTERS) == 1 diff --git a/tests/playback/test_playback_elapsed_math.py b/tests/playback/test_playback_elapsed_math.py new file mode 100644 index 0000000..2891a2e --- /dev/null +++ b/tests/playback/test_playback_elapsed_math.py @@ -0,0 +1,21 @@ +from __future__ import annotations + +from auditui.playback.elapsed import get_elapsed +from auditui.playback import elapsed as elapsed_mod + + +def test_get_elapsed_returns_zero_without_start_time() -> None: + """Ensure elapsed computation returns zero when playback has not started.""" + assert get_elapsed(None, None, 0.0, False) == 0.0 + + +def test_get_elapsed_while_paused_uses_pause_start(monkeypatch) -> None: + """Ensure paused elapsed is fixed at pause_start minus previous pauses.""" + monkeypatch.setattr(elapsed_mod.time, "time", lambda: 500.0) + assert get_elapsed(100.0, 250.0, 20.0, True) == 130.0 + + +def test_get_elapsed_subtracts_pause_duration_when_resumed(monkeypatch) -> None: + """Ensure resumed elapsed removes newly accumulated paused duration.""" + monkeypatch.setattr(elapsed_mod.time, "time", lambda: 400.0) + assert get_elapsed(100.0, 300.0, 10.0, False) == 190.0 diff --git a/tests/playback/test_playback_process_helpers.py b/tests/playback/test_playback_process_helpers.py new file mode 100644 index 0000000..ea7a58b --- /dev/null +++ b/tests/playback/test_playback_process_helpers.py @@ -0,0 +1,67 @@ +from __future__ import annotations + +import subprocess +from pathlib import Path + +from auditui.playback import process as process_mod + + +class DummyProc: + """Minimal subprocess-like object for terminate_process tests.""" + + def __init__(self, alive: bool = True) -> None: + """Initialize process state and bookkeeping flags.""" + self._alive = alive + self.terminated = False + self.killed = False + self.pid = 123 + + def poll(self) -> int | None: + """Return None while process is alive and 0 when stopped.""" + return None if self._alive else 0 + + def terminate(self) -> None: + """Mark process as terminated and no longer alive.""" + self.terminated = True + self._alive = False + + def wait(self, timeout: float | None = None) -> int: + """Return immediately to emulate a cooperative shutdown.""" + del timeout + return 0 + + def kill(self) -> None: + """Mark process as killed and no longer alive.""" + self.killed = True + self._alive = False + + +def test_build_ffplay_cmd_includes_activation_seek_and_speed() -> None: + """Ensure ffplay command includes optional playback arguments when set.""" + cmd = process_mod.build_ffplay_cmd(Path("book.aax"), "abcd", 12.5, 1.2) + assert "-activation_bytes" in cmd + assert "-ss" in cmd + assert "atempo=1.20" in " ".join(cmd) + + +def test_terminate_process_handles_alive_process() -> None: + """Ensure terminate_process gracefully shuts down a running process.""" + proc = DummyProc(alive=True) + process_mod.terminate_process(proc) # type: ignore[arg-type] + assert proc.terminated is True + + +def test_run_ffplay_returns_none_when_unavailable(monkeypatch) -> None: + """Ensure ffplay launch exits early when binary is not on PATH.""" + monkeypatch.setattr(process_mod, "is_ffplay_available", lambda: False) + assert process_mod.run_ffplay(["ffplay", "book.aax"]) == (None, None) + + +def test_send_signal_delegates_to_os_kill(monkeypatch) -> None: + """Ensure send_signal forwards process PID and signal to os.kill.""" + seen: list[tuple[int, object]] = [] + monkeypatch.setattr( + process_mod.os, "kill", lambda pid, sig: seen.append((pid, sig)) + ) + process_mod.send_signal(DummyProc(), process_mod.signal.SIGSTOP) # type: ignore[arg-type] + assert seen and seen[0][0] == 123 diff --git a/tests/playback/test_playback_seek_targets.py b/tests/playback/test_playback_seek_targets.py new file mode 100644 index 0000000..07fb9d4 --- /dev/null +++ b/tests/playback/test_playback_seek_targets.py @@ -0,0 +1,20 @@ +from __future__ import annotations + +from auditui.playback.seek import compute_seek_target + + +def test_forward_seek_returns_new_position_and_message() -> None: + """Ensure forward seek computes expected position and status message.""" + target = compute_seek_target(10.0, 100.0, 30.0, "forward") + assert target == (40.0, "Skipped forward 30s") + + +def test_forward_seek_returns_none_near_end() -> None: + """Ensure seeking too close to end returns an invalid seek result.""" + assert compute_seek_target(95.0, 100.0, 10.0, "forward") is None + + +def test_backward_seek_clamps_to_zero() -> None: + """Ensure backward seek cannot go below zero.""" + target = compute_seek_target(5.0, None, 30.0, "backward") + assert target == (0.0, "Skipped backward 30s")