testsAndMisc/python_pkg/wsg_grabber/tests/test_player_socket.py
Krzysztof kuhy Rudnicki 4b2cd0c398 Clear the wsg_grabber test modules: python_pkg is now under the cap
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
2026-08-16 18:19:26 +02:00

131 lines
4.4 KiB
Python

"""Tests for the socket stand-ins and connection teardown."""
from __future__ import annotations
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
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_socket_stand_ins_always_report_eof() -> None:
"""The contract that keeps the reader thread from becoming a busy loop.
A mock whose recv() returns a MagicMock is truthy and instant, so the drain
loop spins and the mock's recorded-call list grows without bound -- 21 GB
and an OOM kill, in the incident this guards against.
"""
assert _socket_mock().recv(65536) == b""
def test_the_reader_returns_promptly_once_asked_to_stop(
started: tuple[player.MpvPlayer, MagicMock, MagicMock],
) -> None:
"""Shutdown must not wait on another read."""
mpv, _process, sock = started
sock.recv.reset_mock()
mpv._stopping.set()
mpv._drain()
sock.recv.assert_not_called()
def test_a_refused_connection_does_not_leak_the_socket(tmp_path: Path) -> None:
"""A stale socket file makes connect() fail; nothing may be left open."""
ipc = tmp_path / "stale.sock"
process = MagicMock()
sock = _socket_mock()
sock.connect.side_effect = ConnectionRefusedError("stale")
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""),
),
pytest.raises(ConnectionRefusedError),
):
player.MpvPlayer(1, ipc)
sock.close.assert_called_once()
process.kill.assert_called_once()
process.wait.assert_called_once() # reaped, not left a zombie