chore: optimize pre-commit, remove tracked binaries, fix lint issues

- Move slow hooks (mypy, pylint, bandit, pytest, prettier) to pre-push stage
- Remove redundant autoflake (ruff covers F401/F841)
- Fix shellcheck OOM by batching files with xargs -n 40
- Remove tracked .o, .wav, .pyc binaries from git
- Move pomodoro wav files to ../testsAndMisc_binaries/ with symlinks
- Add *.o, *.so, *.a to .gitignore
- Refactor hltb._pick_best_hltb_entry to fix C901/PLR0911/SIM102
- Fix SC2034 warnings in gif_to_square.sh and upgrade.sh
- Add disk_cleanup_check.sh script
- Various test and code improvements across screen_locker,
  steam_backlog_enforcer, word_frequency, moviepy_showcase
This commit is contained in:
Krzysztof kuhy Rudnicki 2026-04-10 18:44:51 +02:00
parent 6a9f137845
commit c1cdd4535f
7 changed files with 344 additions and 112 deletions

View File

@ -15,6 +15,29 @@ import time
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# Real Steam directory — used as a safety check to block destructive
# operations that leak through during testing.
_REAL_STEAMAPPS = Path("~/.local/share/Steam/steamapps").expanduser()
def _assert_not_real_steam(path: Path) -> None:
"""Raise if *path* is inside the real Steam directory.
Defence-in-depth guard: even if test fixtures fail to
redirect ``STEAMAPPS_PATH``, destructive operations
(uninstall, rmtree, unlink) will refuse to touch real files.
"""
try:
path.resolve().relative_to(_REAL_STEAMAPPS.resolve())
except ValueError:
return # path is NOT under real Steam — safe to proceed
if STEAMAPPS_PATH.resolve() == _REAL_STEAMAPPS.resolve():
msg = (
f"SAFETY: refusing destructive operation on real Steam path "
f"{path!s} — STEAMAPPS_PATH was not redirected by test fixtures"
)
raise RuntimeError(msg)
def _echo(msg: str = "", *, end: str = "\n", flush: bool = False) -> None: def _echo(msg: str = "", *, end: str = "\n", flush: bool = False) -> None:
"""Write user-facing CLI output to stdout. """Write user-facing CLI output to stdout.
@ -56,6 +79,7 @@ PROTECTED_APP_IDS = {
1007020, # Proton EasyAntiCheat Runtime 1007020, # Proton EasyAntiCheat Runtime
# Games allowed to be installed anytime # Games allowed to be installed anytime
3949040, # RV There Yet? 3949040, # RV There Yet?
2252570,
} }
STEAMAPPS_PATH = Path("~/.local/share/Steam/steamapps").expanduser() STEAMAPPS_PATH = Path("~/.local/share/Steam/steamapps").expanduser()
@ -312,6 +336,7 @@ def _remove_manifest(manifest: Path, game_name: str, app_id: int) -> bool:
game_name: Human-readable game name for logging. game_name: Human-readable game name for logging.
app_id: Steam application ID. app_id: Steam application ID.
""" """
_assert_not_real_steam(manifest)
try: try:
if manifest.exists(): if manifest.exists():
manifest.unlink() manifest.unlink()
@ -333,6 +358,7 @@ def _remove_game_dirs(install_dir: Path | None, app_id: int) -> bool:
""" """
success = True success = True
if install_dir and install_dir.is_dir(): if install_dir and install_dir.is_dir():
_assert_not_real_steam(install_dir)
try: try:
shutil.rmtree(install_dir) shutil.rmtree(install_dir)
logger.info("Removed game files: %s", install_dir) logger.info("Removed game files: %s", install_dir)
@ -343,6 +369,7 @@ def _remove_game_dirs(install_dir: Path | None, app_id: int) -> bool:
for subdir in ("shadercache", "compatdata"): for subdir in ("shadercache", "compatdata"):
cache_path = STEAMAPPS_PATH / subdir / str(app_id) cache_path = STEAMAPPS_PATH / subdir / str(app_id)
if cache_path.is_dir(): if cache_path.is_dir():
_assert_not_real_steam(cache_path)
with contextlib.suppress(OSError): with contextlib.suppress(OSError):
shutil.rmtree(cache_path) shutil.rmtree(cache_path)
logger.debug("Removed %s/%d", subdir, app_id) logger.debug("Removed %s/%d", subdir, app_id)

View File

@ -154,6 +154,10 @@ def _pick_best_hltb_entry(
When a short name like "FAITH" matches both "FAITH" (demo) and When a short name like "FAITH" matches both "FAITH" (demo) and
"FAITH: The Unholy Trinity" (full game), prefer the full game "FAITH: The Unholy Trinity" (full game), prefer the full game
since Steam often lists the full game under the shorter name. since Steam often lists the full game under the shorter name.
When an exact match like "Timberman" (26 h) competes against an
unrelated subtitle entry like "Timberman: The Big Adventure" (2 h),
the exact match wins because it has more hours.
""" """
if not candidates: if not candidates:
return None return None
@ -165,36 +169,72 @@ def _pick_best_hltb_entry(
return usable[0] return usable[0]
lower = search_name.lower() lower = search_name.lower()
best_exact = _find_exact_match(usable, lower)
best_extended = _find_best_extended(usable, lower)
return _resolve_exact_vs_extended(best_exact, best_extended, usable)
def _find_exact_match(
usable: list[tuple[dict[str, Any], float]],
lower: str,
) -> tuple[dict[str, Any], float] | None:
"""Find best exact name/alias match (highest comp_100)."""
return next(
(
(e, s)
for e, s in sorted(
usable,
key=lambda x: x[0].get("comp_100", 0),
reverse=True,
)
if (e.get("game_name") or "").lower() == lower
or (e.get("game_alias") or "").lower() == lower
),
None,
)
def _find_best_extended(
usable: list[tuple[dict[str, Any], float]],
lower: str,
) -> tuple[dict[str, Any], float] | None:
"""Find best extended entry ("Name: Subtitle" / "Name - Subtitle").
Skips subset entries (prologue, demo, etc.).
"""
best: tuple[dict[str, Any], float] | None = None
for entry, sim in usable: for entry, sim in usable:
entry_name = (entry.get("game_name") or "").lower() entry_name = (entry.get("game_name") or "").lower()
if entry_name.startswith((lower + ":", lower + " -")): if entry_name.startswith((lower + ":", lower + " -")):
suffix = entry_name[len(lower) :].lstrip(" :-") suffix = entry_name[len(lower) :].lstrip(" :-")
if not any(suffix.startswith(kw) for kw in _SUBSET_SUFFIXES): if not any(suffix.startswith(kw) for kw in _SUBSET_SUFFIXES) and (
# Only prefer this extended entry when it has strictly more best is None or entry.get("comp_100", 0) > best[0].get("comp_100", 0)
# comp_100 than any exact-name match. This prevents ):
# "Killing Floor: Toy Master" (1.2 h) from beating best = (entry, sim)
# "Killing Floor" (296 h) while still letting return best
# "FAITH: The Unholy Trinity" (7 h) beat "FAITH" (0.5 h demo).
extended_hours = entry.get("comp_100", 0)
best_exact = next( def _resolve_exact_vs_extended(
( best_exact: tuple[dict[str, Any], float] | None,
(e, s) best_extended: tuple[dict[str, Any], float] | None,
for e, s in sorted( usable: list[tuple[dict[str, Any], float]],
usable, ) -> tuple[dict[str, Any], float]:
key=lambda x: x[0].get("comp_100", 0), """Decide between exact match, extended entry, or highest similarity."""
reverse=True, if best_exact is not None and best_extended is not None:
) exact_hours = best_exact[0].get("comp_100", 0)
if (e.get("game_name") or "").lower() == lower extended_hours = best_extended[0].get("comp_100", 0)
or (e.get("game_alias") or "").lower() == lower # Prefer the extended entry only when it has strictly more hours
), # than the exact match. This lets "FAITH: The Unholy Trinity"
None, # (7 h) beat "FAITH" (0.5 h demo) while preventing
) # "Timberman: The Big Adventure" (2 h) from beating
if ( # "Timberman" (26 h).
best_exact is not None if extended_hours > exact_hours:
and best_exact[0].get("comp_100", 0) >= extended_hours return best_extended
): return best_exact
return best_exact if best_exact is not None:
return entry, sim return best_exact
if best_extended is not None:
return best_extended
# Fall back to highest similarity. # Fall back to highest similarity.
return max(usable, key=lambda x: x[1]) return max(usable, key=lambda x: x[1])

View File

@ -7,12 +7,14 @@ to temporary directories. This stops tests from accidentally:
user's current assignment) user's current assignment)
- Reading real appmanifest files from ~/.local/share/Steam/steamapps - Reading real appmanifest files from ~/.local/share/Steam/steamapps
- Modifying /etc/hosts via the store blocker - Modifying /etc/hosts via the store blocker
- Corrupting the HLTB cache on disk
- Launching real Steam or calling real subprocess commands
""" """
from __future__ import annotations from __future__ import annotations
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from unittest.mock import patch from unittest.mock import MagicMock, patch
import pytest import pytest
@ -57,6 +59,12 @@ def _isolate_filesystem(tmp_path: Path) -> Iterator[None]:
"python_pkg.steam_backlog_enforcer.game_install.STEAMAPPS_PATH", "python_pkg.steam_backlog_enforcer.game_install.STEAMAPPS_PATH",
fake_steamapps, fake_steamapps,
), ),
# HLTB cache file (computed at import time from CONFIG_DIR, so
# patching CONFIG_DIR alone does not redirect it)
patch(
"python_pkg.steam_backlog_enforcer._hltb_types.HLTB_CACHE_FILE",
fake_config / "hltb_cache.json",
),
# /etc/hosts (store blocker) # /etc/hosts (store blocker)
patch( patch(
"python_pkg.steam_backlog_enforcer.store_blocker.HOSTS_FILE", "python_pkg.steam_backlog_enforcer.store_blocker.HOSTS_FILE",
@ -68,3 +76,43 @@ def _isolate_filesystem(tmp_path: Path) -> Iterator[None]:
), ),
): ):
yield yield
@pytest.fixture(autouse=True)
def _block_real_subprocesses() -> Iterator[None]:
"""Block subprocess calls that could launch real Steam or modify system.
Individual tests that need to test subprocess behaviour should
patch the specific module's ``subprocess.run`` / ``subprocess.Popen``
themselves their local patch will override this one.
"""
noop_run = MagicMock(return_value=MagicMock(returncode=1))
noop_popen = MagicMock()
with (
patch(
"python_pkg.steam_backlog_enforcer.game_install.subprocess.run",
noop_run,
),
patch(
"python_pkg.steam_backlog_enforcer.game_install.subprocess.Popen",
noop_popen,
),
patch(
"python_pkg.steam_backlog_enforcer.enforcer.subprocess.run",
noop_run,
),
patch(
"python_pkg.steam_backlog_enforcer.store_blocker.subprocess.run",
noop_run,
),
patch(
"python_pkg.steam_backlog_enforcer.library_hider.subprocess.run",
noop_run,
),
patch(
"python_pkg.steam_backlog_enforcer.library_hider.subprocess.Popen",
noop_popen,
),
):
yield

View File

@ -6,7 +6,10 @@ import os
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
import pytest
from python_pkg.steam_backlog_enforcer.game_install import ( from python_pkg.steam_backlog_enforcer.game_install import (
_assert_not_real_steam,
_echo, _echo,
_ensure_steam_running, _ensure_steam_running,
_get_real_user, _get_real_user,
@ -22,7 +25,43 @@ from python_pkg.steam_backlog_enforcer.game_install import (
if TYPE_CHECKING: if TYPE_CHECKING:
from pathlib import Path from pathlib import Path
import pytest
PKG = "python_pkg.steam_backlog_enforcer.game_install"
class TestAssertNotRealSteam:
"""Tests for the _assert_not_real_steam safety guard."""
def test_allows_tmp_path(self, tmp_path: Path) -> None:
"""Non-Steam paths pass through without raising."""
_assert_not_real_steam(tmp_path / "appmanifest_440.acf")
def test_raises_when_real_steam_not_redirected(self, tmp_path: Path) -> None:
"""Raises when path is under real Steam and STEAMAPPS_PATH is real."""
real = tmp_path / "real_steam"
real.mkdir()
fake_manifest = real / "appmanifest_440.acf"
fake_manifest.touch()
with (
patch(f"{PKG}._REAL_STEAMAPPS", real),
patch(f"{PKG}.STEAMAPPS_PATH", real),
pytest.raises(RuntimeError, match="SAFETY"),
):
_assert_not_real_steam(fake_manifest)
def test_allows_when_steamapps_redirected(self, tmp_path: Path) -> None:
"""No raise when STEAMAPPS_PATH differs from _REAL_STEAMAPPS."""
real = tmp_path / "real_steam"
real.mkdir()
fake_manifest = real / "appmanifest_440.acf"
fake_manifest.touch()
redirected = tmp_path / "fake_steam"
redirected.mkdir()
with (
patch(f"{PKG}._REAL_STEAMAPPS", real),
patch(f"{PKG}.STEAMAPPS_PATH", redirected),
):
_assert_not_real_steam(fake_manifest)
class TestEcho: class TestEcho:

View File

@ -345,3 +345,89 @@ class TestPickBestHltbEntry:
) )
assert result is not None assert result is not None
assert result[0]["game_name"] == "NEEDY GIRL OVERDOSE" assert result[0]["game_name"] == "NEEDY GIRL OVERDOSE"
def test_exact_match_beats_different_subtitled_game(self) -> None:
"""Exact 'Timberman' (26.5 h) must beat 'Timberman: The Big Adventure' (2 h).
Unlike FAITH where the short name is a demo, here the short name
is the real full game and the subtitled entry is a different, shorter
game. The exact match should win because it has more hours.
"""
base: dict[str, Any] = {
"game_name": "Timberman",
"comp_100": 95400, # 26.5 h
}
other: dict[str, Any] = {
"game_name": "Timberman: The Big Adventure",
"comp_100": 7200, # 2 h
}
timberman_vs: dict[str, Any] = {
"game_name": "Timberman VS",
"comp_100": 23400, # 6.5 h
}
result = _pick_best_hltb_entry(
"Timberman",
[(other, 0.49), (timberman_vs, 0.86), (base, 1.0)],
)
assert result is not None
assert result[0]["game_name"] == "Timberman"
def test_exact_match_wins_even_when_extended_appears_first(self) -> None:
"""Exact match wins regardless of candidate ordering."""
base: dict[str, Any] = {
"game_name": "Timberman",
"comp_100": 95400, # 26.5 h
}
other: dict[str, Any] = {
"game_name": "Timberman: The Big Adventure",
"comp_100": 7200, # 2 h
}
# Extended entry appears first in the list.
result = _pick_best_hltb_entry(
"Timberman",
[(other, 0.49), (base, 1.0)],
)
assert result is not None
assert result[0]["game_name"] == "Timberman"
def test_exact_only_no_extended(self) -> None:
"""Exact match returned when no extended entries exist at all."""
exact: dict[str, Any] = {
"game_name": "Celeste",
"comp_100": 180000, # 50 h
}
unrelated: dict[str, Any] = {
"game_name": "Unrelated Game",
"comp_100": 7200,
}
result = _pick_best_hltb_entry(
"Celeste",
[(exact, 1.0), (unrelated, 0.6)],
)
assert result is not None
assert result[0]["game_name"] == "Celeste"
def test_no_exact_no_extended_falls_back(self) -> None:
"""When no exact or extended match exists, fall to highest similarity."""
a: dict[str, Any] = {"game_name": "FooBar", "comp_100": 3600}
b: dict[str, Any] = {"game_name": "FooBaz", "comp_100": 7200}
result = _pick_best_hltb_entry("Foo", [(a, 0.7), (b, 0.8)])
assert result is not None
assert result[0]["game_name"] == "FooBaz"
def test_extended_only_no_exact(self) -> None:
"""Extended entry returned when no exact name match exists."""
extended: dict[str, Any] = {
"game_name": "Neon: Ultimate Edition",
"comp_100": 36000,
}
unrelated: dict[str, Any] = {
"game_name": "Neon Lights",
"comp_100": 3600,
}
result = _pick_best_hltb_entry(
"Neon",
[(extended, 0.6), (unrelated, 0.7)],
)
assert result is not None
assert result[0]["game_name"] == "Neon: Ultimate Edition"

View File

@ -20,8 +20,6 @@ from python_pkg.steam_backlog_enforcer.library_hider import (
_wait_for_cdp_ready, _wait_for_cdp_ready,
_wait_for_collections_ready, _wait_for_collections_ready,
ensure_steam_debug_port, ensure_steam_debug_port,
hide_other_games,
unhide_all_games,
) )
@ -425,85 +423,3 @@ class TestEnsureSteamDebugPort:
), ),
): ):
ensure_steam_debug_port() ensure_steam_debug_port()
class TestHideOtherGames:
"""Tests for hide_other_games."""
def test_hides(self) -> None:
with (
patch(
"python_pkg.steam_backlog_enforcer.library_hider.ensure_steam_debug_port",
),
patch(
"python_pkg.steam_backlog_enforcer.library_hider._evaluate_js",
return_value={
"result": {"result": {"value": '{"totalHidden": 5}'}},
},
),
patch(
"python_pkg.steam_backlog_enforcer.library_hider._cdp_result_value",
return_value='{"totalHidden": 5}',
),
):
count = hide_other_games([1, 2, 3], 1)
assert count == 5
def test_empty_list(self) -> None:
with (
patch(
"python_pkg.steam_backlog_enforcer.library_hider.ensure_steam_debug_port",
),
patch(
"python_pkg.steam_backlog_enforcer.library_hider._evaluate_js",
return_value={
"result": {"result": {"value": '{"totalHidden": 0}'}},
},
),
patch(
"python_pkg.steam_backlog_enforcer.library_hider._cdp_result_value",
return_value='{"totalHidden": 0}',
),
):
count = hide_other_games([1], 1)
assert count == 0
def test_no_allowed(self) -> None:
with (
patch(
"python_pkg.steam_backlog_enforcer.library_hider.ensure_steam_debug_port",
),
patch(
"python_pkg.steam_backlog_enforcer.library_hider._evaluate_js",
return_value={
"result": {"result": {"value": '{"totalHidden": 2}'}},
},
),
patch(
"python_pkg.steam_backlog_enforcer.library_hider._cdp_result_value",
return_value='{"totalHidden": 2}',
),
):
count = hide_other_games([1, 2], None)
assert count == 2
class TestUnhideAllGames:
"""Tests for unhide_all_games."""
def test_unhides(self) -> None:
with (
patch(
"python_pkg.steam_backlog_enforcer.library_hider.ensure_steam_debug_port",
),
patch(
"python_pkg.steam_backlog_enforcer.library_hider._evaluate_js",
return_value={"result": {"result": {"value": '{"count": 10}'}}},
),
patch(
"python_pkg.steam_backlog_enforcer.library_hider._cdp_result_value",
return_value='{"count": 10}',
),
):
count = unhide_all_games([1, 2, 3])
assert count == 10

View File

@ -8,7 +8,9 @@ from unittest.mock import MagicMock, patch
from python_pkg.steam_backlog_enforcer.library_hider import ( from python_pkg.steam_backlog_enforcer.library_hider import (
_run_as_user, _run_as_user,
hide_other_games,
restart_steam, restart_steam,
unhide_all_games,
) )
PKG = "python_pkg.steam_backlog_enforcer.library_hider" PKG = "python_pkg.steam_backlog_enforcer.library_hider"
@ -116,3 +118,77 @@ class TestRestartSteam:
patch(f"{PKG}._wait_for_cdp_ready", return_value=False), patch(f"{PKG}._wait_for_cdp_ready", return_value=False),
): ):
restart_steam() restart_steam()
class TestHideOtherGames:
"""Tests for hide_other_games."""
def test_hides(self) -> None:
with (
patch(f"{PKG}.ensure_steam_debug_port"),
patch(
f"{PKG}._evaluate_js",
return_value={
"result": {"result": {"value": '{"totalHidden": 5}'}},
},
),
patch(
f"{PKG}._cdp_result_value",
return_value='{"totalHidden": 5}',
),
):
count = hide_other_games([1, 2, 3], 1)
assert count == 5
def test_empty_list(self) -> None:
with (
patch(f"{PKG}.ensure_steam_debug_port"),
patch(
f"{PKG}._evaluate_js",
return_value={
"result": {"result": {"value": '{"totalHidden": 0}'}},
},
),
patch(
f"{PKG}._cdp_result_value",
return_value='{"totalHidden": 0}',
),
):
count = hide_other_games([1], 1)
assert count == 0
def test_no_allowed(self) -> None:
with (
patch(f"{PKG}.ensure_steam_debug_port"),
patch(
f"{PKG}._evaluate_js",
return_value={
"result": {"result": {"value": '{"totalHidden": 2}'}},
},
),
patch(
f"{PKG}._cdp_result_value",
return_value='{"totalHidden": 2}',
),
):
count = hide_other_games([1, 2], None)
assert count == 2
class TestUnhideAllGames:
"""Tests for unhide_all_games."""
def test_unhides(self) -> None:
with (
patch(f"{PKG}.ensure_steam_debug_port"),
patch(
f"{PKG}._evaluate_js",
return_value={"result": {"result": {"value": '{"count": 10}'}}},
),
patch(
f"{PKG}._cdp_result_value",
return_value='{"count": 10}',
),
):
count = unhide_all_games([1, 2, 3])
assert count == 10