mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-10 20:47:03 +01:00
The last six over-cap files split into eleven new modules, in two passes since the first cut left five still over. python_pkg has no violations left at all, down from 50 when this effort started. Each split carries the whole preamble including the module's pytest fixtures - started, toolkit and session are taken as parameters and resolve lexically, so a hand-written import block would have missed them. ruff then pruned 82 unused imports. Pass count is 277 before and after, checked after both passes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
230 lines
7.4 KiB
Python
230 lines
7.4 KiB
Python
"""Tests for the mpv subprocess driver.
|
|
|
|
No mpv binary is ever started: ``subprocess.Popen`` and ``socket.socket`` are
|
|
patched in the module's own namespace.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from pathlib import Path
|
|
import subprocess
|
|
from typing import TYPE_CHECKING
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
from python_pkg.wsg_grabber import player
|
|
|
|
if TYPE_CHECKING:
|
|
from collections.abc import Iterator
|
|
|
|
|
|
def _socket_mock(**kwargs: object) -> MagicMock:
|
|
"""Build a socket stand-in whose reader thread terminates immediately.
|
|
|
|
An unconfigured MagicMock returns a MagicMock from recv(), which is truthy
|
|
and instant, so the drain loop spins as fast as the CPU allows -- and
|
|
MagicMock records every call, so memory grows without bound. That once
|
|
reached 21 GB and took the machine down, hence this helper: every socket
|
|
mock yields EOF unless a test deliberately says otherwise.
|
|
|
|
Args:
|
|
**kwargs: Attributes to set on the mock.
|
|
|
|
Returns:
|
|
MagicMock: Socket double.
|
|
"""
|
|
sock = MagicMock()
|
|
sock.recv.return_value = b""
|
|
for name, value in kwargs.items():
|
|
setattr(sock, name, value)
|
|
return sock
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _safe_sockets() -> Iterator[None]:
|
|
"""Stop a stray socket mock from turning the drain loop into a busy loop.
|
|
|
|
``MpvPlayer`` runs a reader thread that calls ``recv`` until EOF. An
|
|
unconfigured MagicMock returns a MagicMock -- truthy, instant, and it
|
|
records every call -- so the loop spins and memory grows without bound.
|
|
That once reached 21 GB and OOM-killed the machine. Defaulting the module's
|
|
socket factory to something that reports EOF means a test has to opt into
|
|
the hazard deliberately.
|
|
|
|
Yields:
|
|
None: Control returns to the test.
|
|
"""
|
|
with patch(
|
|
"python_pkg.wsg_grabber.player.socket.socket",
|
|
side_effect=lambda *_a, **_k: _socket_mock(),
|
|
):
|
|
yield
|
|
|
|
|
|
@pytest.fixture
|
|
def started(tmp_path: Path) -> Iterator[tuple[player.MpvPlayer, MagicMock, MagicMock]]:
|
|
"""Construct an MpvPlayer with its process and socket mocked out.
|
|
|
|
Args:
|
|
tmp_path: Per-test temporary directory.
|
|
|
|
Yields:
|
|
tuple: The player, its process mock and its socket mock.
|
|
"""
|
|
ipc = tmp_path / "mpv.sock"
|
|
process = MagicMock()
|
|
process.poll.return_value = None
|
|
sock = _socket_mock()
|
|
|
|
def create_socket(_seconds: float) -> None:
|
|
"""Stand in for mpv finally creating its control socket."""
|
|
ipc.write_bytes(b"")
|
|
|
|
# __init__ unlinks any stale socket first, so it has to appear *during*
|
|
# the wait rather than being pre-created.
|
|
with (
|
|
patch("python_pkg.wsg_grabber.player.subprocess.Popen", return_value=process),
|
|
patch("python_pkg.wsg_grabber.player.socket.socket", return_value=sock),
|
|
patch("python_pkg.wsg_grabber.player.time.sleep", side_effect=create_socket),
|
|
):
|
|
yield player.MpvPlayer(1234, ipc), process, sock
|
|
|
|
|
|
def test_argv_embeds_into_the_given_window(tmp_path: Path) -> None:
|
|
argv = player.build_argv(999, tmp_path / "s.sock")
|
|
assert argv[0] == "mpv"
|
|
assert "--wid=999" in argv
|
|
assert f"--input-ipc-server={tmp_path / 's.sock'}" in argv
|
|
|
|
|
|
def test_argv_declines_the_keyboard_so_tk_can_have_it(tmp_path: Path) -> None:
|
|
"""With --wid mpv's child window would otherwise swallow every keypress."""
|
|
argv = player.build_argv(1, tmp_path / "s.sock")
|
|
assert "--input-vo-keyboard=no" in argv
|
|
assert "--input-default-bindings=no" in argv
|
|
|
|
|
|
def test_argv_loops_and_stays_open(tmp_path: Path) -> None:
|
|
argv = player.build_argv(1, tmp_path / "s.sock")
|
|
assert "--loop-file=inf" in argv
|
|
assert "--idle=yes" in argv
|
|
assert "--keep-open=yes" in argv
|
|
|
|
|
|
def test_encode_produces_one_json_line() -> None:
|
|
payload = player.encode(["loadfile", "a.webm", "replace"])
|
|
assert payload.endswith(b"\n")
|
|
assert json.loads(payload) == {
|
|
"command": ["loadfile", "a.webm", "replace"],
|
|
}
|
|
|
|
|
|
def test_play_sends_loadfile(
|
|
started: tuple[player.MpvPlayer, MagicMock, MagicMock],
|
|
tmp_path: Path,
|
|
) -> None:
|
|
mpv, _process, sock = started
|
|
mpv.play(tmp_path / "clip.webm")
|
|
sock.sendall.assert_called_once_with(
|
|
player.encode(["loadfile", str(tmp_path / "clip.webm"), "replace"]),
|
|
)
|
|
|
|
|
|
def test_stop_blanks_the_video(
|
|
started: tuple[player.MpvPlayer, MagicMock, MagicMock],
|
|
) -> None:
|
|
mpv, _process, sock = started
|
|
mpv.stop()
|
|
assert sock.sendall.call_args.args[0] == player.encode(["stop"])
|
|
|
|
|
|
def test_a_dead_socket_does_not_raise(
|
|
started: tuple[player.MpvPlayer, MagicMock, MagicMock],
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""mpv can exit underneath us; is_alive reports that, sending must not blow up."""
|
|
mpv, _process, sock = started
|
|
sock.sendall.side_effect = BrokenPipeError("gone")
|
|
mpv.play(tmp_path / "clip.webm")
|
|
|
|
|
|
def test_is_alive_follows_the_process(
|
|
started: tuple[player.MpvPlayer, MagicMock, MagicMock],
|
|
) -> None:
|
|
mpv, process, _sock = started
|
|
assert mpv.is_alive()
|
|
process.poll.return_value = 0
|
|
assert not mpv.is_alive()
|
|
|
|
|
|
def test_close_quits_cleanly_and_removes_the_socket(
|
|
started: tuple[player.MpvPlayer, MagicMock, MagicMock],
|
|
tmp_path: Path,
|
|
) -> None:
|
|
mpv, process, sock = started
|
|
mpv.close()
|
|
assert sock.sendall.call_args.args[0] == player.encode(["quit"])
|
|
process.wait.assert_called_once()
|
|
process.kill.assert_not_called()
|
|
sock.close.assert_called_once()
|
|
assert not (tmp_path / "mpv.sock").exists()
|
|
|
|
|
|
def test_close_kills_an_mpv_that_ignores_quit(
|
|
started: tuple[player.MpvPlayer, MagicMock, MagicMock],
|
|
) -> None:
|
|
"""An orphaned mpv would leave a window on screen after the reviewer exits."""
|
|
mpv, process, _sock = started
|
|
process.wait.side_effect = [subprocess.TimeoutExpired("mpv", 1), None]
|
|
mpv.close()
|
|
process.kill.assert_called_once()
|
|
|
|
|
|
def test_a_socket_that_never_appears_is_a_timeout(tmp_path: Path) -> None:
|
|
ipc = tmp_path / "never.sock"
|
|
process = MagicMock()
|
|
with (
|
|
patch("python_pkg.wsg_grabber.player.subprocess.Popen", return_value=process),
|
|
patch("python_pkg.wsg_grabber.player.time.sleep"),
|
|
patch(
|
|
"python_pkg.wsg_grabber.player.time.monotonic",
|
|
side_effect=[0.0, 0.0, 9999.0],
|
|
),
|
|
pytest.raises(TimeoutError, match="did not create"),
|
|
):
|
|
player.MpvPlayer(1, ipc)
|
|
process.kill.assert_called_once()
|
|
|
|
|
|
def test_the_socket_is_awaited_rather_than_assumed(tmp_path: Path) -> None:
|
|
"""mpv takes a moment to create the socket; connecting too early fails."""
|
|
ipc = tmp_path / "late.sock"
|
|
process = MagicMock()
|
|
sock = _socket_mock()
|
|
waits = {"n": 0}
|
|
|
|
def create_on_third_try(_seconds: float) -> None:
|
|
waits["n"] += 1
|
|
if waits["n"] >= 3:
|
|
ipc.write_bytes(b"")
|
|
|
|
with (
|
|
patch("python_pkg.wsg_grabber.player.subprocess.Popen", return_value=process),
|
|
patch("python_pkg.wsg_grabber.player.socket.socket", return_value=sock),
|
|
patch(
|
|
"python_pkg.wsg_grabber.player.time.sleep",
|
|
side_effect=create_on_third_try,
|
|
),
|
|
):
|
|
player.MpvPlayer(1, ipc)
|
|
assert waits["n"] == 3
|
|
|
|
|
|
def test_the_player_never_sets_geometry() -> None:
|
|
"""--wid makes mpv fill its parent; setting geometry shrinks the video."""
|
|
argv = player.build_argv(1, Path("sock"))
|
|
assert not any(arg.startswith("--geometry") for arg in argv)
|
|
assert not hasattr(player.MpvPlayer, "resize")
|