2026-03-21 17:51:36 +01:00
|
|
|
"""Tests for hltb module — part 2 (missing coverage)."""
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
2026-05-03 22:30:48 +02:00
|
|
|
import asyncio
|
steam_backlog_enforcer: fix stats command — show real Rush/Leisure/Worst data
Four bugs fixed:
- HLTB search returned 0 results for ~87 games with special chars (™, ®, &,
standalone -, (Legacy), RHCP, etc.) — add _sanitize_search_name() and
extend _build_search_variants() with Steam-suffix and edition stripping
- fetch_hltb_detail_missing returned immediately because `app_id not in rush`
was always False (all keys present with -1) — fix to `rush.get(id,-1) <= 0`
- save_hltb_cache overwrote rush/leisure on confidence-only partial saves —
now reads existing cache and preserves data when extras dicts are empty
- _filter_qualifying_games excluded 57 games with stale snapshot hours (-1)
even though HLTB hours cache had valid data — add cache fallback
Result: stats shows Rush 64,670h / Leisure 136,807h / Worst 228,594h for
all 785 qualifying games with full rush+leisure detail.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 07:02:48 +02:00
|
|
|
from typing import TYPE_CHECKING
|
2026-03-21 17:51:36 +01:00
|
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
|
|
2026-05-03 22:30:48 +02:00
|
|
|
from typing_extensions import Self
|
|
|
|
|
|
2026-05-28 07:21:29 +02:00
|
|
|
from steam_backlog_enforcer._hltb_search import _AuthInfo
|
|
|
|
|
from steam_backlog_enforcer.hltb import (
|
2026-03-21 17:51:36 +01:00
|
|
|
HLTB_BASE_URL,
|
|
|
|
|
HLTBResult,
|
2026-05-03 22:30:48 +02:00
|
|
|
_fetch_batch_confidence_only,
|
|
|
|
|
fetch_hltb_confidence,
|
|
|
|
|
fetch_hltb_confidence_cached,
|
steam_backlog_enforcer: fix stats command — show real Rush/Leisure/Worst data
Four bugs fixed:
- HLTB search returned 0 results for ~87 games with special chars (™, ®, &,
standalone -, (Legacy), RHCP, etc.) — add _sanitize_search_name() and
extend _build_search_variants() with Steam-suffix and edition stripping
- fetch_hltb_detail_missing returned immediately because `app_id not in rush`
was always False (all keys present with -1) — fix to `rush.get(id,-1) <= 0`
- save_hltb_cache overwrote rush/leisure on confidence-only partial saves —
now reads existing cache and preserves data when extras dicts are empty
- _filter_qualifying_games excluded 57 games with stale snapshot hours (-1)
even though HLTB hours cache had valid data — add cache fallback
Result: stats shows Rush 64,670h / Leisure 136,807h / Worst 228,594h for
all 785 qualifying games with full rush+leisure detail.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 07:02:48 +02:00
|
|
|
fetch_hltb_detail_missing,
|
2026-03-21 17:51:36 +01:00
|
|
|
fetch_hltb_times_cached,
|
|
|
|
|
get_hltb_submit_url,
|
|
|
|
|
)
|
|
|
|
|
|
steam_backlog_enforcer: fix stats command — show real Rush/Leisure/Worst data
Four bugs fixed:
- HLTB search returned 0 results for ~87 games with special chars (™, ®, &,
standalone -, (Legacy), RHCP, etc.) — add _sanitize_search_name() and
extend _build_search_variants() with Steam-suffix and edition stripping
- fetch_hltb_detail_missing returned immediately because `app_id not in rush`
was always False (all keys present with -1) — fix to `rush.get(id,-1) <= 0`
- save_hltb_cache overwrote rush/leisure on confidence-only partial saves —
now reads existing cache and preserves data when extras dicts are empty
- _filter_qualifying_games excluded 57 games with stale snapshot hours (-1)
even though HLTB hours cache had valid data — add cache fallback
Result: stats shows Rush 64,670h / Leisure 136,807h / Worst 228,594h for
all 785 qualifying games with full rush+leisure detail.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 07:02:48 +02:00
|
|
|
if TYPE_CHECKING:
|
2026-05-28 07:21:29 +02:00
|
|
|
from steam_backlog_enforcer._hltb_types import _HLTBExtras
|
steam_backlog_enforcer: fix stats command — show real Rush/Leisure/Worst data
Four bugs fixed:
- HLTB search returned 0 results for ~87 games with special chars (™, ®, &,
standalone -, (Legacy), RHCP, etc.) — add _sanitize_search_name() and
extend _build_search_variants() with Steam-suffix and edition stripping
- fetch_hltb_detail_missing returned immediately because `app_id not in rush`
was always False (all keys present with -1) — fix to `rush.get(id,-1) <= 0`
- save_hltb_cache overwrote rush/leisure on confidence-only partial saves —
now reads existing cache and preserves data when extras dicts are empty
- _filter_qualifying_games excluded 57 games with stale snapshot hours (-1)
even though HLTB hours cache had valid data — add cache fallback
Result: stats shows Rush 64,670h / Leisure 136,807h / Worst 228,594h for
all 785 qualifying games with full rush+leisure detail.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 07:02:48 +02:00
|
|
|
|
2026-05-28 07:21:29 +02:00
|
|
|
PKG = "steam_backlog_enforcer.hltb"
|
2026-03-21 17:51:36 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestFetchHltbTimesCached:
|
|
|
|
|
"""Tests for fetch_hltb_times_cached."""
|
|
|
|
|
|
|
|
|
|
def test_all_cached(self) -> None:
|
|
|
|
|
with (
|
|
|
|
|
patch(f"{PKG}.load_hltb_cache", return_value={440: 50.0}),
|
|
|
|
|
):
|
|
|
|
|
result = fetch_hltb_times_cached([(440, "TF2")])
|
|
|
|
|
assert result == {440: 50.0}
|
|
|
|
|
|
|
|
|
|
def test_uncached_games_fetched(self) -> None:
|
|
|
|
|
with (
|
|
|
|
|
patch(f"{PKG}.load_hltb_cache", return_value={440: 50.0}),
|
|
|
|
|
patch(f"{PKG}.fetch_hltb_times") as mock_fetch,
|
|
|
|
|
patch(f"{PKG}.save_hltb_cache") as mock_save,
|
|
|
|
|
patch(f"{PKG}.time.monotonic", side_effect=[0.0, 2.0]),
|
|
|
|
|
):
|
|
|
|
|
# fetch_hltb_times modifies cache in-place
|
|
|
|
|
def add_to_cache(
|
Reduce per-file-ignores by fixing lint violations across codebase
Fix ruff violations in ~15 source files and ~60+ test files to minimize
per-file-ignores in pyproject.toml. Remaining ignores are justified with
comments explaining why each suppression is necessary.
Source fixes: FBT003 (keyword args), S310 (URL validation), SLF001
(private access), T201 (print→logging), C901 (complexity), E501 (line
length), E402 (import order).
Test fixes: SIM117 (combined with), FBT (boolean args), PERF203 (try in
loop), S310/S607 (URLs/executables), E402/E501 (imports/lines), S108
(tmp paths), PLR0913 (too many args), ARG (unused args), ANN (type
annotations), RUF059 (unused unpacked vars), PT019 (fixture naming).
Remaining per-file-ignores (with justifications):
- Tests: ARG, D, PLC0415, PLR2004, S101, SLF001
- music_gen sources: PLC0415 (heavy ML lazy imports)
- moviepy_showcase: PLC0415 (circular dependency)
- generate_images: PLR0913 (matplotlib helpers need many params)
- praca_magisterska_video: E501, E402 (long paths, mpl.use)
2026-03-25 18:58:05 +01:00
|
|
|
_games: object,
|
2026-03-21 17:51:36 +01:00
|
|
|
cache: dict[int, float] | None = None,
|
2026-05-03 22:30:48 +02:00
|
|
|
polls: dict[int, int] | None = None,
|
2026-03-21 17:51:36 +01:00
|
|
|
progress_cb: object = None,
|
steam_backlog_enforcer: fix stats command — show real Rush/Leisure/Worst data
Four bugs fixed:
- HLTB search returned 0 results for ~87 games with special chars (™, ®, &,
standalone -, (Legacy), RHCP, etc.) — add _sanitize_search_name() and
extend _build_search_variants() with Steam-suffix and edition stripping
- fetch_hltb_detail_missing returned immediately because `app_id not in rush`
was always False (all keys present with -1) — fix to `rush.get(id,-1) <= 0`
- save_hltb_cache overwrote rush/leisure on confidence-only partial saves —
now reads existing cache and preserves data when extras dicts are empty
- _filter_qualifying_games excluded 57 games with stale snapshot hours (-1)
even though HLTB hours cache had valid data — add cache fallback
Result: stats shows Rush 64,670h / Leisure 136,807h / Worst 228,594h for
all 785 qualifying games with full rush+leisure detail.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 07:02:48 +02:00
|
|
|
extras: _HLTBExtras | None = None,
|
2026-03-21 17:51:36 +01:00
|
|
|
) -> list[object]:
|
|
|
|
|
if cache is not None:
|
|
|
|
|
cache[730] = 20.0
|
2026-05-03 22:30:48 +02:00
|
|
|
if polls is not None:
|
|
|
|
|
polls[730] = 0
|
steam_backlog_enforcer: fix stats command — show real Rush/Leisure/Worst data
Four bugs fixed:
- HLTB search returned 0 results for ~87 games with special chars (™, ®, &,
standalone -, (Legacy), RHCP, etc.) — add _sanitize_search_name() and
extend _build_search_variants() with Steam-suffix and edition stripping
- fetch_hltb_detail_missing returned immediately because `app_id not in rush`
was always False (all keys present with -1) — fix to `rush.get(id,-1) <= 0`
- save_hltb_cache overwrote rush/leisure on confidence-only partial saves —
now reads existing cache and preserves data when extras dicts are empty
- _filter_qualifying_games excluded 57 games with stale snapshot hours (-1)
even though HLTB hours cache had valid data — add cache fallback
Result: stats shows Rush 64,670h / Leisure 136,807h / Worst 228,594h for
all 785 qualifying games with full rush+leisure detail.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 07:02:48 +02:00
|
|
|
if extras is not None:
|
|
|
|
|
extras.count_comp[730] = 0
|
2026-03-21 17:51:36 +01:00
|
|
|
return []
|
|
|
|
|
|
|
|
|
|
mock_fetch.side_effect = add_to_cache
|
|
|
|
|
result = fetch_hltb_times_cached(
|
|
|
|
|
[(440, "TF2"), (730, "CS")],
|
|
|
|
|
)
|
|
|
|
|
assert result[440] == 50.0
|
|
|
|
|
assert result[730] == 20.0
|
|
|
|
|
mock_save.assert_called_once()
|
|
|
|
|
|
|
|
|
|
def test_uncached_with_progress_cb(self) -> None:
|
|
|
|
|
cb = MagicMock()
|
|
|
|
|
with (
|
|
|
|
|
patch(f"{PKG}.load_hltb_cache", return_value={}),
|
|
|
|
|
patch(f"{PKG}.fetch_hltb_times") as mock_fetch,
|
|
|
|
|
patch(f"{PKG}.save_hltb_cache"),
|
|
|
|
|
patch(f"{PKG}.time.monotonic", side_effect=[0.0, 1.0]),
|
|
|
|
|
):
|
|
|
|
|
mock_fetch.return_value = []
|
|
|
|
|
result = fetch_hltb_times_cached(
|
|
|
|
|
[(440, "TF2")],
|
|
|
|
|
progress_cb=cb,
|
|
|
|
|
)
|
|
|
|
|
assert 440 not in result or result.get(440) == -1
|
|
|
|
|
|
|
|
|
|
def test_uncached_zero_elapsed(self) -> None:
|
|
|
|
|
"""Covers the elapsed == 0 branch for rate calculation."""
|
|
|
|
|
with (
|
|
|
|
|
patch(f"{PKG}.load_hltb_cache", return_value={}),
|
|
|
|
|
patch(f"{PKG}.fetch_hltb_times") as mock_fetch,
|
|
|
|
|
patch(f"{PKG}.save_hltb_cache"),
|
|
|
|
|
patch(f"{PKG}.time.monotonic", side_effect=[5.0, 5.0]),
|
|
|
|
|
):
|
|
|
|
|
mock_fetch.return_value = []
|
|
|
|
|
fetch_hltb_times_cached([(440, "TF2")])
|
|
|
|
|
|
|
|
|
|
def test_found_count(self) -> None:
|
|
|
|
|
"""Covers the found count in logging."""
|
|
|
|
|
with (
|
|
|
|
|
patch(f"{PKG}.load_hltb_cache", return_value={}),
|
|
|
|
|
patch(f"{PKG}.fetch_hltb_times") as mock_fetch,
|
|
|
|
|
patch(f"{PKG}.save_hltb_cache"),
|
|
|
|
|
patch(f"{PKG}.time.monotonic", side_effect=[0.0, 3.0]),
|
|
|
|
|
):
|
|
|
|
|
|
|
|
|
|
def add_found(
|
Reduce per-file-ignores by fixing lint violations across codebase
Fix ruff violations in ~15 source files and ~60+ test files to minimize
per-file-ignores in pyproject.toml. Remaining ignores are justified with
comments explaining why each suppression is necessary.
Source fixes: FBT003 (keyword args), S310 (URL validation), SLF001
(private access), T201 (print→logging), C901 (complexity), E501 (line
length), E402 (import order).
Test fixes: SIM117 (combined with), FBT (boolean args), PERF203 (try in
loop), S310/S607 (URLs/executables), E402/E501 (imports/lines), S108
(tmp paths), PLR0913 (too many args), ARG (unused args), ANN (type
annotations), RUF059 (unused unpacked vars), PT019 (fixture naming).
Remaining per-file-ignores (with justifications):
- Tests: ARG, D, PLC0415, PLR2004, S101, SLF001
- music_gen sources: PLC0415 (heavy ML lazy imports)
- moviepy_showcase: PLC0415 (circular dependency)
- generate_images: PLR0913 (matplotlib helpers need many params)
- praca_magisterska_video: E501, E402 (long paths, mpl.use)
2026-03-25 18:58:05 +01:00
|
|
|
_games: object,
|
2026-03-21 17:51:36 +01:00
|
|
|
cache: dict[int, float] | None = None,
|
2026-05-03 22:30:48 +02:00
|
|
|
polls: dict[int, int] | None = None,
|
2026-03-21 17:51:36 +01:00
|
|
|
progress_cb: object = None,
|
steam_backlog_enforcer: fix stats command — show real Rush/Leisure/Worst data
Four bugs fixed:
- HLTB search returned 0 results for ~87 games with special chars (™, ®, &,
standalone -, (Legacy), RHCP, etc.) — add _sanitize_search_name() and
extend _build_search_variants() with Steam-suffix and edition stripping
- fetch_hltb_detail_missing returned immediately because `app_id not in rush`
was always False (all keys present with -1) — fix to `rush.get(id,-1) <= 0`
- save_hltb_cache overwrote rush/leisure on confidence-only partial saves —
now reads existing cache and preserves data when extras dicts are empty
- _filter_qualifying_games excluded 57 games with stale snapshot hours (-1)
even though HLTB hours cache had valid data — add cache fallback
Result: stats shows Rush 64,670h / Leisure 136,807h / Worst 228,594h for
all 785 qualifying games with full rush+leisure detail.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 07:02:48 +02:00
|
|
|
extras: _HLTBExtras | None = None,
|
2026-03-21 17:51:36 +01:00
|
|
|
) -> list[object]:
|
|
|
|
|
if cache is not None:
|
|
|
|
|
cache[440] = 50.0
|
|
|
|
|
cache[730] = -1
|
2026-05-03 22:30:48 +02:00
|
|
|
if polls is not None:
|
|
|
|
|
polls[440] = 5
|
|
|
|
|
polls[730] = 0
|
steam_backlog_enforcer: fix stats command — show real Rush/Leisure/Worst data
Four bugs fixed:
- HLTB search returned 0 results for ~87 games with special chars (™, ®, &,
standalone -, (Legacy), RHCP, etc.) — add _sanitize_search_name() and
extend _build_search_variants() with Steam-suffix and edition stripping
- fetch_hltb_detail_missing returned immediately because `app_id not in rush`
was always False (all keys present with -1) — fix to `rush.get(id,-1) <= 0`
- save_hltb_cache overwrote rush/leisure on confidence-only partial saves —
now reads existing cache and preserves data when extras dicts are empty
- _filter_qualifying_games excluded 57 games with stale snapshot hours (-1)
even though HLTB hours cache had valid data — add cache fallback
Result: stats shows Rush 64,670h / Leisure 136,807h / Worst 228,594h for
all 785 qualifying games with full rush+leisure detail.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 07:02:48 +02:00
|
|
|
if extras is not None:
|
|
|
|
|
extras.count_comp[440] = 15
|
|
|
|
|
extras.count_comp[730] = 0
|
2026-03-21 17:51:36 +01:00
|
|
|
return []
|
|
|
|
|
|
|
|
|
|
mock_fetch.side_effect = add_found
|
|
|
|
|
result = fetch_hltb_times_cached(
|
|
|
|
|
[(440, "TF2"), (730, "CS")],
|
|
|
|
|
)
|
|
|
|
|
assert result[440] == 50.0
|
|
|
|
|
assert result[730] == -1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestGetHltbSubmitUrl:
|
|
|
|
|
"""Tests for get_hltb_submit_url."""
|
|
|
|
|
|
|
|
|
|
def test_found(self) -> None:
|
|
|
|
|
mock_result = HLTBResult(
|
|
|
|
|
app_id=0,
|
|
|
|
|
game_name="TF2",
|
|
|
|
|
completionist_hours=50.0,
|
|
|
|
|
similarity=1.0,
|
|
|
|
|
hltb_game_id=12345,
|
|
|
|
|
)
|
|
|
|
|
with patch(f"{PKG}.fetch_hltb_times", return_value=[mock_result]):
|
|
|
|
|
url = get_hltb_submit_url("TF2")
|
|
|
|
|
assert url == f"{HLTB_BASE_URL}/submit/game/12345"
|
|
|
|
|
|
|
|
|
|
def test_not_found_empty(self) -> None:
|
|
|
|
|
with patch(f"{PKG}.fetch_hltb_times", return_value=[]):
|
|
|
|
|
url = get_hltb_submit_url("Unknown Game")
|
|
|
|
|
assert url is None
|
|
|
|
|
|
|
|
|
|
def test_not_found_no_id(self) -> None:
|
|
|
|
|
mock_result = HLTBResult(
|
|
|
|
|
app_id=0,
|
|
|
|
|
game_name="TF2",
|
|
|
|
|
completionist_hours=50.0,
|
|
|
|
|
similarity=1.0,
|
|
|
|
|
hltb_game_id=0,
|
|
|
|
|
)
|
|
|
|
|
with patch(f"{PKG}.fetch_hltb_times", return_value=[mock_result]):
|
|
|
|
|
url = get_hltb_submit_url("TF2")
|
|
|
|
|
assert url is None
|
2026-05-03 22:30:48 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class _DummySession:
|
|
|
|
|
"""Minimal async context manager used to mock aiohttp ClientSession."""
|
|
|
|
|
|
|
|
|
|
async def __aenter__(self) -> Self:
|
|
|
|
|
"""Enter async context."""
|
|
|
|
|
return self
|
|
|
|
|
|
|
|
|
|
async def __aexit__(self, *_args: object) -> bool:
|
|
|
|
|
"""Exit async context."""
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestConfidenceHelpers:
|
|
|
|
|
"""Coverage tests for confidence-fetch helpers."""
|
|
|
|
|
|
|
|
|
|
def test_fetch_batch_confidence_only_returns_empty_without_auth(self) -> None:
|
|
|
|
|
with (
|
|
|
|
|
patch(f"{PKG}.aiohttp.ClientSession", return_value=_DummySession()),
|
|
|
|
|
patch(f"{PKG}.aiohttp.TCPConnector"),
|
|
|
|
|
patch(f"{PKG}._get_hltb_search_url", return_value="https://example"),
|
|
|
|
|
patch(f"{PKG}._get_auth_info", return_value=None),
|
|
|
|
|
):
|
|
|
|
|
result = asyncio.run(
|
|
|
|
|
_fetch_batch_confidence_only([(1, "Game")], {}, {}, None),
|
|
|
|
|
)
|
|
|
|
|
assert result == []
|
|
|
|
|
|
|
|
|
|
def test_fetch_batch_confidence_only_handles_empty_hp_and_default_counts(
|
|
|
|
|
self,
|
|
|
|
|
) -> None:
|
|
|
|
|
auth_token = str(1)
|
|
|
|
|
with (
|
|
|
|
|
patch(f"{PKG}.aiohttp.ClientSession", return_value=_DummySession()),
|
|
|
|
|
patch(f"{PKG}.aiohttp.TCPConnector"),
|
|
|
|
|
patch(f"{PKG}._get_hltb_search_url", return_value="https://example"),
|
|
|
|
|
patch(
|
|
|
|
|
f"{PKG}._get_auth_info",
|
|
|
|
|
return_value=_AuthInfo(token=auth_token, hp_key="", hp_val=""),
|
|
|
|
|
),
|
|
|
|
|
patch(f"{PKG}._search_one", side_effect=[None]) as mock_search,
|
|
|
|
|
):
|
|
|
|
|
result = asyncio.run(
|
|
|
|
|
_fetch_batch_confidence_only(
|
|
|
|
|
games=[(1, "Game")],
|
|
|
|
|
cache={},
|
|
|
|
|
polls={},
|
|
|
|
|
progress_cb=None,
|
|
|
|
|
count_comp=None,
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
assert result == []
|
|
|
|
|
mock_search.assert_called_once()
|
|
|
|
|
|
|
|
|
|
def test_fetch_hltb_confidence_initializes_optional_dicts(self) -> None:
|
|
|
|
|
with patch(f"{PKG}.asyncio.run", return_value=[]) as mock_run:
|
|
|
|
|
result = fetch_hltb_confidence([(1, "Game")])
|
|
|
|
|
assert result == []
|
|
|
|
|
mock_run.assert_called_once()
|
|
|
|
|
|
|
|
|
|
def test_fetch_hltb_confidence_empty_games_returns_empty(self) -> None:
|
|
|
|
|
with patch(f"{PKG}.asyncio.run") as mock_run:
|
|
|
|
|
result = fetch_hltb_confidence([])
|
|
|
|
|
assert result == []
|
|
|
|
|
mock_run.assert_not_called()
|
|
|
|
|
|
|
|
|
|
def test_fetch_hltb_confidence_cached_all_cached_skips_fetch(self) -> None:
|
|
|
|
|
with (
|
|
|
|
|
patch(f"{PKG}.load_hltb_cache", return_value={1: 12.0}),
|
|
|
|
|
patch(f"{PKG}.load_hltb_polls_cache", return_value={1: 30}),
|
|
|
|
|
patch(f"{PKG}.load_hltb_count_comp_cache", return_value={1: 200}),
|
|
|
|
|
patch(f"{PKG}.fetch_hltb_confidence") as mock_fetch,
|
|
|
|
|
patch(f"{PKG}.save_hltb_cache") as mock_save,
|
|
|
|
|
):
|
|
|
|
|
result = fetch_hltb_confidence_cached([(1, "Game")])
|
|
|
|
|
assert result == {1: 12.0}
|
|
|
|
|
mock_fetch.assert_not_called()
|
|
|
|
|
mock_save.assert_not_called()
|
steam_backlog_enforcer: fix stats command — show real Rush/Leisure/Worst data
Four bugs fixed:
- HLTB search returned 0 results for ~87 games with special chars (™, ®, &,
standalone -, (Legacy), RHCP, etc.) — add _sanitize_search_name() and
extend _build_search_variants() with Steam-suffix and edition stripping
- fetch_hltb_detail_missing returned immediately because `app_id not in rush`
was always False (all keys present with -1) — fix to `rush.get(id,-1) <= 0`
- save_hltb_cache overwrote rush/leisure on confidence-only partial saves —
now reads existing cache and preserves data when extras dicts are empty
- _filter_qualifying_games excluded 57 games with stale snapshot hours (-1)
even though HLTB hours cache had valid data — add cache fallback
Result: stats shows Rush 64,670h / Leisure 136,807h / Worst 228,594h for
all 785 qualifying games with full rush+leisure detail.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 07:02:48 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestFetchHltbDetailMissing:
|
|
|
|
|
"""Tests for fetch_hltb_detail_missing."""
|
|
|
|
|
|
|
|
|
|
def test_no_missing_returns_zero(self) -> None:
|
|
|
|
|
"""All games in rush cache → early return without fetching."""
|
|
|
|
|
with (
|
|
|
|
|
patch(f"{PKG}.load_hltb_rush_cache", return_value={440: 15.0}),
|
|
|
|
|
patch(f"{PKG}.fetch_hltb_times") as mock_fetch,
|
|
|
|
|
):
|
|
|
|
|
result = fetch_hltb_detail_missing([(440, "TF2")])
|
|
|
|
|
assert result == 0
|
|
|
|
|
mock_fetch.assert_not_called()
|
|
|
|
|
|
|
|
|
|
def test_fetches_missing_and_returns_count(self) -> None:
|
|
|
|
|
"""Games not in rush cache are fetched; returns count with rush data."""
|
|
|
|
|
|
|
|
|
|
def add_rush(
|
|
|
|
|
_games: object,
|
|
|
|
|
cache: dict[int, float] | None = None,
|
|
|
|
|
polls: dict[int, int] | None = None,
|
|
|
|
|
progress_cb: object = None,
|
|
|
|
|
extras: _HLTBExtras | None = None,
|
|
|
|
|
) -> list[object]:
|
|
|
|
|
if extras is not None:
|
|
|
|
|
extras.rush[730] = 10.0
|
|
|
|
|
if cache is not None:
|
|
|
|
|
cache[730] = 25.0
|
|
|
|
|
return []
|
|
|
|
|
|
|
|
|
|
with (
|
|
|
|
|
patch(f"{PKG}.load_hltb_rush_cache", return_value={440: 15.0}),
|
|
|
|
|
patch(f"{PKG}.load_hltb_cache", return_value={730: 20.0}),
|
|
|
|
|
patch(f"{PKG}.load_hltb_polls_cache", return_value={}),
|
|
|
|
|
patch(f"{PKG}.load_hltb_count_comp_cache", return_value={}),
|
|
|
|
|
patch(f"{PKG}.load_hltb_leisure_100h_cache", return_value={}),
|
|
|
|
|
patch(f"{PKG}.load_hltb_game_id_cache", return_value={}),
|
|
|
|
|
patch(f"{PKG}.fetch_hltb_times", side_effect=add_rush),
|
|
|
|
|
patch(f"{PKG}.save_hltb_cache") as mock_save,
|
|
|
|
|
patch(f"{PKG}.time.monotonic", side_effect=[0.0, 2.0]),
|
|
|
|
|
):
|
|
|
|
|
result = fetch_hltb_detail_missing([(440, "TF2"), (730, "CS")])
|
|
|
|
|
assert result == 1
|
|
|
|
|
mock_save.assert_called_once()
|
|
|
|
|
|
|
|
|
|
def test_restores_prior_hours_when_not_refound(self) -> None:
|
|
|
|
|
"""Hours are restored when re-fetch finds nothing for the game."""
|
|
|
|
|
saved: dict[int, float] = {}
|
|
|
|
|
|
|
|
|
|
def capture_save(
|
|
|
|
|
cache: dict[int, float],
|
|
|
|
|
_polls: object,
|
|
|
|
|
_extras: object = None,
|
|
|
|
|
) -> None:
|
|
|
|
|
saved.update(cache)
|
|
|
|
|
|
|
|
|
|
with (
|
|
|
|
|
patch(f"{PKG}.load_hltb_rush_cache", return_value={}),
|
|
|
|
|
patch(f"{PKG}.load_hltb_cache", return_value={730: 20.0}),
|
|
|
|
|
patch(f"{PKG}.load_hltb_polls_cache", return_value={}),
|
|
|
|
|
patch(f"{PKG}.load_hltb_count_comp_cache", return_value={}),
|
|
|
|
|
patch(f"{PKG}.load_hltb_leisure_100h_cache", return_value={}),
|
|
|
|
|
patch(f"{PKG}.load_hltb_game_id_cache", return_value={}),
|
|
|
|
|
patch(f"{PKG}.fetch_hltb_times"), # no-op, cache stays empty
|
|
|
|
|
patch(f"{PKG}.save_hltb_cache", side_effect=capture_save),
|
|
|
|
|
patch(f"{PKG}.time.monotonic", side_effect=[0.0, 1.0]),
|
|
|
|
|
):
|
|
|
|
|
fetch_hltb_detail_missing([(730, "CS")])
|
|
|
|
|
assert saved[730] == 20.0
|
|
|
|
|
|
|
|
|
|
def test_does_not_restore_when_refound(self) -> None:
|
|
|
|
|
"""Prior hours are NOT restored when re-fetch successfully finds game."""
|
|
|
|
|
|
|
|
|
|
def add_hours_and_rush(
|
|
|
|
|
_games: object,
|
|
|
|
|
cache: dict[int, float] | None = None,
|
|
|
|
|
polls: dict[int, int] | None = None,
|
|
|
|
|
progress_cb: object = None,
|
|
|
|
|
extras: _HLTBExtras | None = None,
|
|
|
|
|
) -> list[object]:
|
|
|
|
|
if cache is not None:
|
|
|
|
|
cache[730] = 30.0
|
|
|
|
|
if extras is not None:
|
|
|
|
|
extras.rush[730] = 12.0
|
|
|
|
|
return []
|
|
|
|
|
|
|
|
|
|
saved: dict[int, float] = {}
|
|
|
|
|
|
|
|
|
|
def capture_save(
|
|
|
|
|
cache: dict[int, float],
|
|
|
|
|
_polls: object,
|
|
|
|
|
_extras: object = None,
|
|
|
|
|
) -> None:
|
|
|
|
|
saved.update(cache)
|
|
|
|
|
|
|
|
|
|
with (
|
|
|
|
|
patch(f"{PKG}.load_hltb_rush_cache", return_value={}),
|
|
|
|
|
patch(f"{PKG}.load_hltb_cache", return_value={730: 20.0}),
|
|
|
|
|
patch(f"{PKG}.load_hltb_polls_cache", return_value={}),
|
|
|
|
|
patch(f"{PKG}.load_hltb_count_comp_cache", return_value={}),
|
|
|
|
|
patch(f"{PKG}.load_hltb_leisure_100h_cache", return_value={}),
|
|
|
|
|
patch(f"{PKG}.load_hltb_game_id_cache", return_value={}),
|
|
|
|
|
patch(f"{PKG}.fetch_hltb_times", side_effect=add_hours_and_rush),
|
|
|
|
|
patch(f"{PKG}.save_hltb_cache", side_effect=capture_save),
|
|
|
|
|
patch(f"{PKG}.time.monotonic", side_effect=[0.0, 1.0]),
|
|
|
|
|
):
|
|
|
|
|
result = fetch_hltb_detail_missing([(730, "CS")])
|
|
|
|
|
assert result == 1
|
|
|
|
|
assert saved[730] == 30.0
|
|
|
|
|
|
|
|
|
|
def test_zero_elapsed_rate(self) -> None:
|
|
|
|
|
"""Covers the elapsed == 0 branch in the rate calculation."""
|
|
|
|
|
with (
|
|
|
|
|
patch(f"{PKG}.load_hltb_rush_cache", return_value={}),
|
|
|
|
|
patch(f"{PKG}.load_hltb_cache", return_value={}),
|
|
|
|
|
patch(f"{PKG}.load_hltb_polls_cache", return_value={}),
|
|
|
|
|
patch(f"{PKG}.load_hltb_count_comp_cache", return_value={}),
|
|
|
|
|
patch(f"{PKG}.load_hltb_leisure_100h_cache", return_value={}),
|
|
|
|
|
patch(f"{PKG}.load_hltb_game_id_cache", return_value={}),
|
|
|
|
|
patch(f"{PKG}.fetch_hltb_times"),
|
|
|
|
|
patch(f"{PKG}.save_hltb_cache"),
|
|
|
|
|
patch(f"{PKG}.time.monotonic", side_effect=[5.0, 5.0]),
|
|
|
|
|
):
|
|
|
|
|
result = fetch_hltb_detail_missing([(730, "CS")])
|
|
|
|
|
assert result == 0
|