Massive refactoring #1
34
tests/playback/test_playback_chapter_selection.py
Normal file
34
tests/playback/test_playback_chapter_selection.py
Normal file
@@ -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
|
||||||
21
tests/playback/test_playback_elapsed_math.py
Normal file
21
tests/playback/test_playback_elapsed_math.py
Normal file
@@ -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
|
||||||
67
tests/playback/test_playback_process_helpers.py
Normal file
67
tests/playback/test_playback_process_helpers.py
Normal file
@@ -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
|
||||||
20
tests/playback/test_playback_seek_targets.py
Normal file
20
tests/playback/test_playback_seek_targets.py
Normal file
@@ -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")
|
||||||
Reference in New Issue
Block a user