mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-10 22:07: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
220 lines
7.0 KiB
Python
220 lines
7.0 KiB
Python
"""Tests for how the player reports IPC failures."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from typing import TYPE_CHECKING
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
from python_pkg.wsg_grabber import logs, player
|
|
|
|
if TYPE_CHECKING:
|
|
from collections.abc import Iterator
|
|
from pathlib import Path
|
|
|
|
|
|
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_a_failed_send_is_logged_not_swallowed(
|
|
started: tuple[player.MpvPlayer, MagicMock, MagicMock],
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""A dead socket must not make a frozen reviewer look like a working one."""
|
|
mpv, _process, sock = started
|
|
sock.sendall.side_effect = BrokenPipeError("gone")
|
|
log = logs.start("debug")
|
|
try:
|
|
mpv.play(tmp_path / "clip.webm")
|
|
finally:
|
|
logs.stop()
|
|
recorded = [
|
|
json.loads(line)
|
|
for line in log.read_text(encoding="utf-8").splitlines()
|
|
if line
|
|
]
|
|
assert any(r["event"] == "player.send_failed" for r in recorded)
|
|
assert any(r["level"] == "error" for r in recorded)
|
|
|
|
|
|
def test_the_socket_is_drained_so_mpv_keeps_accepting_commands(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""The regression that made the reviewer freeze on one video.
|
|
|
|
mpv pushes events down the same socket; a client that never reads lets the
|
|
buffer fill, at which point mpv stops reading commands while still looking
|
|
perfectly alive.
|
|
"""
|
|
ipc = tmp_path / "mpv.sock"
|
|
process = MagicMock()
|
|
process.poll.return_value = None
|
|
sock = _socket_mock()
|
|
seen: list[int] = []
|
|
|
|
def recv(size: int) -> bytes:
|
|
seen.append(size)
|
|
return b'{"event":"property-change"}\n' if len(seen) < 3 else b""
|
|
|
|
sock.recv.side_effect = recv
|
|
|
|
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=lambda _s: ipc.write_bytes(b""),
|
|
),
|
|
):
|
|
mpv = player.MpvPlayer(1, ipc)
|
|
mpv._reader.join(timeout=5)
|
|
mpv.close()
|
|
|
|
assert seen, "the reader never read from the socket"
|
|
assert not mpv._reader.is_alive()
|
|
|
|
|
|
def test_the_reader_stops_on_a_socket_error(tmp_path: Path) -> None:
|
|
ipc = tmp_path / "mpv.sock"
|
|
process = MagicMock()
|
|
process.poll.return_value = None
|
|
sock = _socket_mock()
|
|
sock.recv.side_effect = ConnectionResetError("gone")
|
|
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=lambda _s: ipc.write_bytes(b""),
|
|
),
|
|
):
|
|
mpv = player.MpvPlayer(1, ipc)
|
|
mpv._reader.join(timeout=5)
|
|
assert not mpv._reader.is_alive()
|
|
mpv.close()
|
|
|
|
|
|
def test_the_reader_keeps_going_through_read_timeouts(tmp_path: Path) -> None:
|
|
"""A quiet socket must not be mistaken for a closed one."""
|
|
ipc = tmp_path / "mpv.sock"
|
|
process = MagicMock()
|
|
process.poll.return_value = None
|
|
sock = _socket_mock()
|
|
calls = {"n": 0}
|
|
|
|
def recv(_size: int) -> bytes:
|
|
calls["n"] += 1
|
|
if calls["n"] < 3:
|
|
raise TimeoutError
|
|
return b""
|
|
|
|
sock.recv.side_effect = recv
|
|
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=lambda _s: ipc.write_bytes(b""),
|
|
),
|
|
):
|
|
mpv = player.MpvPlayer(1, ipc)
|
|
mpv._reader.join(timeout=5)
|
|
mpv.close()
|
|
assert calls["n"] >= 3
|
|
|
|
|
|
def test_mpv_errors_are_logged_rather_than_discarded(
|
|
started: tuple[player.MpvPlayer, MagicMock, MagicMock],
|
|
) -> None:
|
|
"""A file mpv cannot decode should be visible, not just look stuck."""
|
|
mpv, _process, _sock = started
|
|
log = logs.start("debug")
|
|
try:
|
|
mpv._note_errors(
|
|
b'{"error":"success"}\n'
|
|
b"\n"
|
|
b"garbage\n"
|
|
b'{"request_id":1,"error":"loadfile failed"}\n',
|
|
)
|
|
finally:
|
|
logs.stop()
|
|
recorded = [
|
|
json.loads(line)
|
|
for line in log.read_text(encoding="utf-8").splitlines()
|
|
if line
|
|
]
|
|
failures = [r for r in recorded if r["event"] == "player.reply_error"]
|
|
assert len(failures) == 1
|
|
assert failures[0]["detail"] == "loadfile failed"
|