steam-backlog-enforcer/steam_backlog_enforcer/tests/test_playtime_kill.py
Krzysztof kuhy Rudnicki b5b4b35c07 Split total-block, playtime and MCP by responsibility
Each of these was one file doing several jobs, so they are split along
what they actually do rather than at arbitrary line offsets:

- _total_block: the lock file and the status derived from it stay in
  _total_block; making Steam *not installed* moves to _total_block_purge,
  making it *unreachable* to _total_block_hosts / _total_block_iptables.
  The generic pacman verbs (owner, installed, remove) become _pacman,
  since nothing about them is total-block-specific. Domain and process
  name tables become data modules.
- _playtime: budget state and persistence (_playtime_state), pure
  transitions (_playtime_budget), cmdline process detection
  (_playtime_procs), the warn/cutoff/kill ladder (_playtime_cutoff),
  desktop notifications (_playtime_notify), process-tree walking and
  killing (_playtime_kill), mountinfo parsing (_playtime_mountinfo) and
  the shared privileged-command helper (_playtime_run).
- _mcp: read-only tools (_mcp_query) split from mutating ones
  (_mcp_actions, _mcp_gaming_time), so "what can this server change?" is
  answerable from one file. _mcp keeps the server object and entry point.

No re-export shims: callers import from the module that defines the name.
That keeps a stale patch target loud (AttributeError) instead of letting
it pass while intercepting nothing.

`ruff check --select F821,F401` is run after every extraction -- an earlier
mechanical attempt moved functions without the module-level names they
referenced, which imported fine and only failed when called (349 errors),
so it was reverted and redone by hand.

The autouse fixtures in conftest are now imported by name and held by
__all__; ruff --unsafe-fixes had deleted them as unused, since pytest
resolves fixtures by name.

1324 tests pass at 100.00% coverage in the clean venv; `main status` is
byte-identical to a worktree at HEAD. 49 -> 39 files over the cap.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPpmMpTSkp8UziRc5u8B7P
2026-08-23 20:52:08 +02:00

162 lines
5.4 KiB
Python

"""Tests for stopping the processes a playtime cutoff must end."""
from __future__ import annotations
import signal
from unittest.mock import patch
from steam_backlog_enforcer._playtime_kill import (
kill_gaming_processes,
request_steam_shutdown,
steam_and_launcher_pids,
)
PKG = "steam_backlog_enforcer._playtime_kill"
class TestRequestSteamShutdown:
def test_skips_when_steam_is_absent(self) -> None:
with (
patch(
"steam_backlog_enforcer._playtime_kill.shutil.which", return_value=None
),
patch("steam_backlog_enforcer._playtime_kill._run") as mock_run,
):
request_steam_shutdown()
mock_run.assert_not_called()
def test_sends_shutdown(self) -> None:
with (
patch(
"steam_backlog_enforcer._playtime_kill.shutil.which",
return_value="/usr/bin/steam",
),
patch(
"steam_backlog_enforcer._playtime_kill._run", return_value=True
) as mock_run,
):
request_steam_shutdown()
mock_run.assert_called_once_with(["/usr/bin/steam", "-shutdown"])
class TestSteamAndLauncherPids:
def test_unions_both_name_sets(self) -> None:
with patch(
"steam_backlog_enforcer._playtime_kill.get_pids_by_process_names",
return_value={11: "steam", 22: "lutris"},
) as mock_get:
assert steam_and_launcher_pids() == {11, 22}
names = mock_get.call_args.args[0]
assert "steam" in names
assert "lutris" in names
class TestKillGamingProcesses:
def test_sigterm_by_default(self) -> None:
with (
patch(
"steam_backlog_enforcer._playtime_kill.descendant_pids",
return_value=set(),
),
patch(
"steam_backlog_enforcer._playtime_kill._own_process_chain",
return_value=set(),
),
patch("steam_backlog_enforcer._playtime_kill.os.kill") as mock_kill,
):
assert kill_gaming_processes({7}, force=False) == [7]
mock_kill.assert_called_once_with(7, signal.SIGTERM)
def test_sigkill_when_forced(self) -> None:
with (
patch(
"steam_backlog_enforcer._playtime_kill.descendant_pids",
return_value=set(),
),
patch(
"steam_backlog_enforcer._playtime_kill._own_process_chain",
return_value=set(),
),
patch("steam_backlog_enforcer._playtime_kill.os.kill") as mock_kill,
):
kill_gaming_processes({7}, force=True)
mock_kill.assert_called_once_with(7, signal.SIGKILL)
def test_includes_descendants(self) -> None:
with (
patch(
"steam_backlog_enforcer._playtime_kill.descendant_pids",
return_value={8, 9},
),
patch(
"steam_backlog_enforcer._playtime_kill._own_process_chain",
return_value=set(),
),
patch("steam_backlog_enforcer._playtime_kill.os.kill"),
):
assert kill_gaming_processes({7}, force=False) == [7, 8, 9]
def test_never_signals_our_own_chain(self) -> None:
with (
patch(
"steam_backlog_enforcer._playtime_kill.descendant_pids",
return_value=set(),
),
patch(
"steam_backlog_enforcer._playtime_kill._own_process_chain",
return_value={7},
),
patch("steam_backlog_enforcer._playtime_kill.os.kill") as mock_kill,
):
assert kill_gaming_processes({7, 8}, force=False) == [8]
mock_kill.assert_called_once_with(8, signal.SIGTERM)
def test_process_already_gone(self) -> None:
with (
patch(
"steam_backlog_enforcer._playtime_kill.descendant_pids",
return_value=set(),
),
patch(
"steam_backlog_enforcer._playtime_kill._own_process_chain",
return_value=set(),
),
patch(
"steam_backlog_enforcer._playtime_kill.os.kill",
side_effect=ProcessLookupError,
),
):
assert kill_gaming_processes({7}, force=False) == []
def test_permission_denied(self) -> None:
with (
patch(
"steam_backlog_enforcer._playtime_kill.descendant_pids",
return_value=set(),
),
patch(
"steam_backlog_enforcer._playtime_kill._own_process_chain",
return_value=set(),
),
patch(
"steam_backlog_enforcer._playtime_kill.os.kill",
side_effect=PermissionError,
),
):
assert kill_gaming_processes({7}, force=False) == []
def test_empty_set(self) -> None:
with (
patch(
"steam_backlog_enforcer._playtime_kill.descendant_pids",
return_value=set(),
),
patch(
"steam_backlog_enforcer._playtime_kill._own_process_chain",
return_value=set(),
),
patch("steam_backlog_enforcer._playtime_kill.os.kill") as mock_kill,
):
assert kill_gaming_processes(set(), force=False) == []
mock_kill.assert_not_called()