mirror of
https://github.com/kuhyx/steam-backlog-enforcer.git
synced 2026-07-04 13:23:18 +02:00
- steam_backlog_enforcer: extract _hltb_search.py and _scanning_confidence.py;
split oversized test files into *_part2/3/4.py
- screen_locker: extract _early_bird.py and _window_setup.py from screen_lock.py;
fix patch targets in tests (screen_lock.* -> _window_setup.*)
- wake_alarm: use shutil.which('xset') to avoid S607; add TestDisplayHelpers tests
- linux_configuration/usage_report: split into _parsing.py and _types.py;
add bin/__init__.py (INP001); fix RUF002 (× -> x)
- pre-commit: add require_serial: true to pytest-coverage hook to prevent
file batching across 24 CPU cores (was causing 12 parallel partial-coverage runs)
448 lines
14 KiB
Python
448 lines
14 KiB
Python
"""Tests for _cmd_done module."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from unittest.mock import patch
|
|
|
|
from python_pkg.steam_backlog_enforcer._cmd_done import (
|
|
_try_reassign_shorter_game,
|
|
)
|
|
from python_pkg.steam_backlog_enforcer.config import Config, State
|
|
from python_pkg.steam_backlog_enforcer.steam_api import GameInfo
|
|
|
|
CMD_DONE_PKG = "python_pkg.steam_backlog_enforcer._cmd_done"
|
|
|
|
|
|
def _snap(**overrides: object) -> dict[str, object]:
|
|
snapshot: dict[str, object] = {
|
|
"app_id": 1,
|
|
"name": "G",
|
|
"total_achievements": 10,
|
|
"unlocked_achievements": 0,
|
|
"playtime_minutes": 60,
|
|
"completionist_hours": -1,
|
|
"comp_100_count": 3,
|
|
"count_comp": 15,
|
|
}
|
|
snapshot["app_id"] = overrides.get("app_id", 1)
|
|
snapshot.update(overrides)
|
|
return snapshot
|
|
|
|
|
|
class TestTryReassignShorterGame:
|
|
"""Tests for _try_reassign_shorter_game."""
|
|
|
|
def test_no_snapshot(self) -> None:
|
|
with patch(f"{CMD_DONE_PKG}.load_snapshot", return_value=None):
|
|
assert not _try_reassign_shorter_game({}, 1, 10.0, State(), Config())
|
|
|
|
def test_no_shorter_candidate(self) -> None:
|
|
snap = [
|
|
_snap(
|
|
app_id=1, name="G", unlocked_achievements=5, completionist_hours=10.0
|
|
),
|
|
_snap(app_id=2, name="H", unlocked_achievements=5),
|
|
]
|
|
with (
|
|
patch(f"{CMD_DONE_PKG}.load_snapshot", return_value=snap),
|
|
patch(f"{CMD_DONE_PKG}._echo"),
|
|
):
|
|
result = _try_reassign_shorter_game(
|
|
{1: 10.0},
|
|
1,
|
|
10.0,
|
|
State(),
|
|
Config(),
|
|
)
|
|
assert not result
|
|
|
|
def test_reassigns(self) -> None:
|
|
snap = [
|
|
_snap(
|
|
app_id=1,
|
|
name="Long",
|
|
unlocked_achievements=5,
|
|
completionist_hours=100.0,
|
|
),
|
|
_snap(
|
|
app_id=2, name="Short", unlocked_achievements=5, completionist_hours=5.0
|
|
),
|
|
]
|
|
state = State(current_app_id=2, current_game_name="Short")
|
|
short_game = GameInfo(
|
|
app_id=2,
|
|
name="Short",
|
|
total_achievements=10,
|
|
unlocked_achievements=5,
|
|
playtime_minutes=60,
|
|
completionist_hours=5.0,
|
|
)
|
|
with (
|
|
patch(f"{CMD_DONE_PKG}.load_snapshot", return_value=snap),
|
|
patch(f"{CMD_DONE_PKG}._echo"),
|
|
patch(
|
|
f"{CMD_DONE_PKG}._pick_next_shortest_candidate",
|
|
return_value=(short_game, 0, 0),
|
|
),
|
|
patch(f"{CMD_DONE_PKG}.pick_next_game"),
|
|
patch(
|
|
f"{CMD_DONE_PKG}.get_all_owned_app_ids",
|
|
return_value=[1, 2, 3],
|
|
),
|
|
patch(f"{CMD_DONE_PKG}.hide_other_games", return_value=5) as mock_hide,
|
|
):
|
|
result = _try_reassign_shorter_game(
|
|
{1: 100.0, 2: 5.0},
|
|
1,
|
|
100.0,
|
|
state,
|
|
Config(),
|
|
)
|
|
assert result
|
|
mock_hide.assert_called_once_with([1, 2, 3], 2)
|
|
|
|
def test_reassigns_no_hide_when_no_owned_ids(self) -> None:
|
|
snap = [
|
|
_snap(
|
|
app_id=1,
|
|
name="Long",
|
|
unlocked_achievements=5,
|
|
completionist_hours=100.0,
|
|
),
|
|
_snap(
|
|
app_id=2, name="Short", unlocked_achievements=5, completionist_hours=5.0
|
|
),
|
|
]
|
|
state = State(current_app_id=2, current_game_name="Short")
|
|
short_game = GameInfo(
|
|
app_id=2,
|
|
name="Short",
|
|
total_achievements=10,
|
|
unlocked_achievements=5,
|
|
playtime_minutes=60,
|
|
completionist_hours=5.0,
|
|
)
|
|
with (
|
|
patch(f"{CMD_DONE_PKG}.load_snapshot", return_value=snap),
|
|
patch(f"{CMD_DONE_PKG}._echo") as mock_echo,
|
|
patch(
|
|
f"{CMD_DONE_PKG}._pick_next_shortest_candidate",
|
|
return_value=(short_game, 0, 0),
|
|
),
|
|
patch(f"{CMD_DONE_PKG}.pick_next_game"),
|
|
patch(f"{CMD_DONE_PKG}.get_all_owned_app_ids", return_value=[1, 2]),
|
|
patch(f"{CMD_DONE_PKG}.hide_other_games", return_value=0),
|
|
):
|
|
result = _try_reassign_shorter_game(
|
|
{1: 100.0, 2: 5.0},
|
|
1,
|
|
100.0,
|
|
state,
|
|
Config(),
|
|
)
|
|
assert result
|
|
# hidden == 0, so "hid N games" should NOT be echoed
|
|
for call in mock_echo.call_args_list:
|
|
assert "hid" not in str(call)
|
|
|
|
def test_reassigns_skip_hide_when_no_app_assigned(self) -> None:
|
|
snap = [
|
|
_snap(
|
|
app_id=1,
|
|
name="Long",
|
|
unlocked_achievements=5,
|
|
completionist_hours=100.0,
|
|
),
|
|
_snap(
|
|
app_id=2, name="Short", unlocked_achievements=5, completionist_hours=5.0
|
|
),
|
|
]
|
|
state = State(current_app_id=None, current_game_name="")
|
|
short_game = GameInfo(
|
|
app_id=2,
|
|
name="Short",
|
|
total_achievements=10,
|
|
unlocked_achievements=5,
|
|
playtime_minutes=60,
|
|
completionist_hours=5.0,
|
|
)
|
|
with (
|
|
patch(f"{CMD_DONE_PKG}.load_snapshot", return_value=snap),
|
|
patch(f"{CMD_DONE_PKG}._echo"),
|
|
patch(
|
|
f"{CMD_DONE_PKG}._pick_next_shortest_candidate",
|
|
return_value=(short_game, 0, 0),
|
|
),
|
|
patch(f"{CMD_DONE_PKG}.pick_next_game"),
|
|
patch(f"{CMD_DONE_PKG}.get_all_owned_app_ids") as mock_owned,
|
|
patch(f"{CMD_DONE_PKG}.hide_other_games") as mock_hide,
|
|
):
|
|
result = _try_reassign_shorter_game(
|
|
{1: 100.0, 2: 5.0},
|
|
1,
|
|
100.0,
|
|
state,
|
|
Config(),
|
|
)
|
|
assert result
|
|
mock_owned.assert_not_called()
|
|
mock_hide.assert_not_called()
|
|
|
|
def test_playable_none(self) -> None:
|
|
snap = [
|
|
_snap(
|
|
app_id=1,
|
|
name="Long",
|
|
unlocked_achievements=5,
|
|
completionist_hours=100.0,
|
|
),
|
|
_snap(
|
|
app_id=2, name="Short", unlocked_achievements=5, completionist_hours=5.0
|
|
),
|
|
]
|
|
with (
|
|
patch(f"{CMD_DONE_PKG}.load_snapshot", return_value=snap),
|
|
patch(
|
|
f"{CMD_DONE_PKG}._pick_next_shortest_candidate",
|
|
return_value=(None, 0, 0),
|
|
),
|
|
patch(f"{CMD_DONE_PKG}._echo"),
|
|
):
|
|
result = _try_reassign_shorter_game(
|
|
{1: 100.0, 2: 5.0},
|
|
1,
|
|
100.0,
|
|
State(),
|
|
Config(),
|
|
)
|
|
assert not result
|
|
|
|
def test_playable_longer(self) -> None:
|
|
"""Playable candidate is longer than current — no reassign."""
|
|
snap = [
|
|
_snap(
|
|
app_id=1,
|
|
name="Short",
|
|
unlocked_achievements=5,
|
|
completionist_hours=10.0,
|
|
),
|
|
_snap(
|
|
app_id=2,
|
|
name="Long",
|
|
unlocked_achievements=5,
|
|
completionist_hours=200.0,
|
|
),
|
|
]
|
|
long_game = GameInfo(
|
|
app_id=2,
|
|
name="Long",
|
|
total_achievements=10,
|
|
unlocked_achievements=5,
|
|
playtime_minutes=60,
|
|
completionist_hours=200.0,
|
|
)
|
|
with (
|
|
patch(f"{CMD_DONE_PKG}.load_snapshot", return_value=snap),
|
|
patch(
|
|
f"{CMD_DONE_PKG}._pick_next_shortest_candidate",
|
|
return_value=(long_game, 0, 0),
|
|
),
|
|
patch(f"{CMD_DONE_PKG}._echo"),
|
|
):
|
|
result = _try_reassign_shorter_game(
|
|
{1: 10.0, 2: 200.0},
|
|
1,
|
|
10.0,
|
|
State(),
|
|
Config(),
|
|
)
|
|
assert not result
|
|
|
|
def test_refreshes_stale_shorter_snapshot_entry(self) -> None:
|
|
"""Uncached shorter snapshot candidates are refreshed before reassigning."""
|
|
snap = [
|
|
_snap(
|
|
app_id=1,
|
|
name="Current",
|
|
unlocked_achievements=5,
|
|
completionist_hours=20.1,
|
|
),
|
|
_snap(app_id=2, name="Lacuna", completionist_hours=0.9),
|
|
]
|
|
state = State(current_app_id=1, current_game_name="Current")
|
|
refreshed_short = GameInfo(
|
|
app_id=2,
|
|
name="Lacuna",
|
|
total_achievements=10,
|
|
unlocked_achievements=0,
|
|
playtime_minutes=60,
|
|
completionist_hours=18.8,
|
|
)
|
|
with (
|
|
patch(f"{CMD_DONE_PKG}.load_snapshot", return_value=snap),
|
|
patch(
|
|
f"{CMD_DONE_PKG}.fetch_hltb_times_cached",
|
|
return_value={2: 18.8},
|
|
) as mock_fetch_hltb,
|
|
patch(
|
|
f"{CMD_DONE_PKG}._pick_next_shortest_candidate",
|
|
return_value=(refreshed_short, 0, 0),
|
|
) as mock_pick_candidate,
|
|
patch(f"{CMD_DONE_PKG}.pick_next_game"),
|
|
patch(f"{CMD_DONE_PKG}._echo"),
|
|
patch(f"{CMD_DONE_PKG}.get_all_owned_app_ids", return_value=[]),
|
|
patch(f"{CMD_DONE_PKG}.hide_other_games"),
|
|
):
|
|
result = _try_reassign_shorter_game(
|
|
{1: 20.1},
|
|
1,
|
|
20.1,
|
|
state,
|
|
Config(),
|
|
)
|
|
|
|
assert result
|
|
mock_fetch_hltb.assert_called_once_with([(2, "Lacuna")])
|
|
mock_pick_candidate.assert_called_once()
|
|
|
|
def test_reassigns_when_current_confidence_too_low(self) -> None:
|
|
"""If current game fails confidence thresholds, reassign anyway."""
|
|
snap = [
|
|
_snap(
|
|
app_id=1,
|
|
name="Current",
|
|
unlocked_achievements=5,
|
|
completionist_hours=20.0,
|
|
comp_100_count=0,
|
|
count_comp=0,
|
|
),
|
|
_snap(
|
|
app_id=2,
|
|
name="Confident",
|
|
unlocked_achievements=5,
|
|
completionist_hours=25.0,
|
|
),
|
|
]
|
|
state = State(current_app_id=2, current_game_name="Confident")
|
|
confident_game = GameInfo(
|
|
app_id=2,
|
|
name="Confident",
|
|
total_achievements=10,
|
|
unlocked_achievements=5,
|
|
playtime_minutes=60,
|
|
completionist_hours=25.0,
|
|
comp_100_count=3,
|
|
count_comp=15,
|
|
)
|
|
with (
|
|
patch(f"{CMD_DONE_PKG}.load_snapshot", return_value=snap),
|
|
patch(
|
|
f"{CMD_DONE_PKG}._pick_next_shortest_candidate",
|
|
return_value=(confident_game, 0, 0),
|
|
),
|
|
patch(f"{CMD_DONE_PKG}.pick_next_game"),
|
|
patch(f"{CMD_DONE_PKG}.get_all_owned_app_ids", return_value=[]),
|
|
patch(f"{CMD_DONE_PKG}.hide_other_games"),
|
|
patch(f"{CMD_DONE_PKG}._echo") as mock_echo,
|
|
):
|
|
result = _try_reassign_shorter_game(
|
|
{1: 20.0, 2: 25.0},
|
|
1,
|
|
20.0,
|
|
state,
|
|
Config(),
|
|
)
|
|
|
|
assert result
|
|
assert any(
|
|
"confidence too low" in str(call).lower()
|
|
for call in mock_echo.call_args_list
|
|
)
|
|
|
|
def test_does_not_force_refresh_current_when_cached_confidence_is_good(
|
|
self,
|
|
) -> None:
|
|
"""Current-game confidence check should use cache-backed values first."""
|
|
snap = [
|
|
_snap(
|
|
app_id=1,
|
|
name="Current",
|
|
unlocked_achievements=5,
|
|
completionist_hours=20.0,
|
|
comp_100_count=0,
|
|
count_comp=0,
|
|
),
|
|
_snap(
|
|
app_id=2,
|
|
name="Shorter",
|
|
unlocked_achievements=5,
|
|
completionist_hours=5.0,
|
|
comp_100_count=3,
|
|
count_comp=15,
|
|
),
|
|
]
|
|
with (
|
|
patch(f"{CMD_DONE_PKG}.load_snapshot", return_value=snap),
|
|
patch(f"{CMD_DONE_PKG}.load_hltb_polls_cache", return_value={1: 36, 2: 20}),
|
|
patch(
|
|
f"{CMD_DONE_PKG}.load_hltb_count_comp_cache",
|
|
return_value={1: 200, 2: 50},
|
|
),
|
|
patch(f"{CMD_DONE_PKG}._refresh_candidate_confidence") as mock_refresh,
|
|
patch(
|
|
f"{CMD_DONE_PKG}._pick_next_shortest_candidate",
|
|
return_value=(None, 0, 0),
|
|
),
|
|
patch(f"{CMD_DONE_PKG}._echo"),
|
|
):
|
|
result = _try_reassign_shorter_game(
|
|
{1: 20.0, 2: 5.0},
|
|
1,
|
|
20.0,
|
|
State(),
|
|
Config(),
|
|
)
|
|
|
|
assert not result
|
|
mock_refresh.assert_not_called()
|
|
|
|
def test_only_checks_strictly_shorter_candidates_when_not_forced(self) -> None:
|
|
"""No confidence checks should run for non-shorter games."""
|
|
snap = [
|
|
_snap(
|
|
app_id=1,
|
|
name="Current",
|
|
unlocked_achievements=5,
|
|
completionist_hours=4.0,
|
|
comp_100_count=10,
|
|
count_comp=40,
|
|
),
|
|
_snap(
|
|
app_id=2,
|
|
name="TooLong",
|
|
unlocked_achievements=5,
|
|
completionist_hours=8.0,
|
|
comp_100_count=1,
|
|
count_comp=8,
|
|
),
|
|
]
|
|
with (
|
|
patch(f"{CMD_DONE_PKG}.load_snapshot", return_value=snap),
|
|
patch(f"{CMD_DONE_PKG}.load_hltb_polls_cache", return_value={1: 10, 2: 1}),
|
|
patch(
|
|
f"{CMD_DONE_PKG}.load_hltb_count_comp_cache", return_value={1: 40, 2: 8}
|
|
),
|
|
patch(f"{CMD_DONE_PKG}._pick_next_shortest_candidate") as mock_pick,
|
|
patch(f"{CMD_DONE_PKG}._echo"),
|
|
):
|
|
result = _try_reassign_shorter_game(
|
|
{1: 4.0, 2: 8.0},
|
|
1,
|
|
4.0,
|
|
State(),
|
|
Config(),
|
|
)
|
|
|
|
assert not result
|
|
mock_pick.assert_not_called()
|