mirror of
https://github.com/kuhyx/steam-backlog-enforcer.git
synced 2026-11-11 12:47:32 +01:00
`requires-python` and the CI pin already said 3.14, but ruff, mypy and pylint were still grading against 3.10 -- three tools judging an interpreter nothing runs any more, and ruff withholding the pyupgrade fixes that only become safe at the newer target. `pin_ci_python.py` rewrites exactly these three knobs. What the stale target was hiding: `datetime.timezone.utc` where `datetime.UTC` is now correct (UP017), `socket.timeout` where `TimeoutError` is (UP041), deprecated `typing` imports (UP035), PEP 695 `type` aliases (UP040), and ruff's stable formatter applying PEP 758 -- `except (A, B):` becomes `except A, B:`. That last one makes the source 3.14-only at the syntax level, which is what `requires-python = ">=3.14"` already promised; kuhy chose it deliberately over holding ruff a release back (2026-08-29). ruff, ruff-format, mypy --strict and pylint all clean at the new target. The two modified web/ TypeScript files are another session's in-flight work and are deliberately left unstaged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SfnJmxxsZ534AKk16rC2Gr
226 lines
8.0 KiB
Python
226 lines
8.0 KiB
Python
"""Tests for HLTB internal helpers, detail fetching, and leisure times — part 3."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
from typing import Any, Self
|
|
from unittest.mock import AsyncMock, patch
|
|
|
|
import aiohttp
|
|
|
|
from steam_backlog_enforcer._hltb_comp_extract import _extract_dlc_relationships
|
|
from steam_backlog_enforcer._hltb_detail import (
|
|
_collect_dlc_relationships,
|
|
_extract_base_leisure_hours,
|
|
_fetch_dlc_leisure_hours,
|
|
)
|
|
from steam_backlog_enforcer._hltb_page_parse import (
|
|
_apply_dlc_leisure_overrides,
|
|
_as_positive_int,
|
|
)
|
|
from steam_backlog_enforcer._hltb_types import (
|
|
HLTBResult,
|
|
)
|
|
|
|
|
|
class _FakeTextResponse:
|
|
"""Async context manager mimicking aiohttp response for text."""
|
|
|
|
def __init__(self, status: int, text: str = "") -> None:
|
|
"""Test init."""
|
|
self.status = status
|
|
self._text = text
|
|
|
|
async def __aenter__(self) -> Self:
|
|
"""Test aenter."""
|
|
return self
|
|
|
|
async def __aexit__(self, *args: object) -> None:
|
|
"""Test aexit."""
|
|
|
|
async def text(self) -> str:
|
|
"""Test text."""
|
|
return self._text
|
|
|
|
|
|
class TestInternalHelpers:
|
|
"""Tests for internal helper coverage."""
|
|
|
|
def test_as_positive_int_float(self) -> None:
|
|
"""Test as positive int float."""
|
|
assert _as_positive_int(1.9) == 1
|
|
|
|
def test_as_positive_int_invalid_type(self) -> None:
|
|
"""Test as positive int invalid type."""
|
|
assert not _as_positive_int(object())
|
|
|
|
def test_extract_base_leisure_non_dict_game(self) -> None:
|
|
"""Test extract base leisure non dict game."""
|
|
data: dict[str, Any] = {"game": [123]}
|
|
assert _extract_base_leisure_hours(data) == -1
|
|
|
|
def test_extract_base_leisure_platform_data_comp_high_is_max(self) -> None:
|
|
"""Test extract base leisure platform data comp high is max."""
|
|
data: dict[str, Any] = {
|
|
"game": [{"comp_100_h": 16063}],
|
|
"platformData": [{"platform": "PC", "comp_high": 23760}],
|
|
}
|
|
assert _extract_base_leisure_hours(data) == round(23760 / 3600, 2)
|
|
|
|
def test_extract_base_leisure_h_field_exceeds_platform_comp_high(self) -> None:
|
|
"""Test extract base leisure h field exceeds platform comp high."""
|
|
data: dict[str, Any] = {
|
|
"game": [{"comp_100_h": 25000}],
|
|
"platformData": [{"platform": "PC", "comp_high": 23760}],
|
|
}
|
|
assert _extract_base_leisure_hours(data) == round(25000 / 3600, 2)
|
|
|
|
def test_extract_base_leisure_max_of_multiple_platforms(self) -> None:
|
|
"""Test extract base leisure max of multiple platforms."""
|
|
data: dict[str, Any] = {
|
|
"game": [{}],
|
|
"platformData": [
|
|
{"platform": "PC", "comp_high": 23760},
|
|
{"platform": "Switch", "comp_high": 18000},
|
|
],
|
|
}
|
|
assert _extract_base_leisure_hours(data) == round(23760 / 3600, 2)
|
|
|
|
def test_extract_base_leisure_platform_data_not_list(self) -> None:
|
|
"""Test extract base leisure platform data not list."""
|
|
data: dict[str, Any] = {
|
|
"game": [{"comp_100_h": 16063}],
|
|
"platformData": "not_a_list",
|
|
}
|
|
assert _extract_base_leisure_hours(data) == round(16063 / 3600, 2)
|
|
|
|
def test_extract_base_leisure_platform_non_dict_entry_skipped(self) -> None:
|
|
"""Test extract base leisure platform non dict entry skipped."""
|
|
data: dict[str, Any] = {
|
|
"game": [{"comp_100_h": 16063}],
|
|
"platformData": ["bad", {"platform": "PC", "comp_high": 23760}],
|
|
}
|
|
assert _extract_base_leisure_hours(data) == round(23760 / 3600, 2)
|
|
|
|
def test_extract_base_leisure_platform_comp_high_zero_skipped(self) -> None:
|
|
"""Test extract base leisure platform comp high zero skipped."""
|
|
data: dict[str, Any] = {
|
|
"game": [{"comp_100_h": 16063}],
|
|
"platformData": [{"platform": "PC", "comp_high": 0}],
|
|
}
|
|
assert _extract_base_leisure_hours(data) == round(16063 / 3600, 2)
|
|
|
|
def test_extract_base_leisure_max_of_h_fields(self) -> None:
|
|
"""Test extract base leisure max of h fields."""
|
|
data: dict[str, Any] = {
|
|
"game": [
|
|
{
|
|
"comp_main_h": 14951,
|
|
"comp_plus_h": 17957,
|
|
"comp_100_h": 16063,
|
|
"comp_all_h": 17959,
|
|
}
|
|
],
|
|
}
|
|
assert _extract_base_leisure_hours(data) == round(17959 / 3600, 2)
|
|
|
|
def test_extract_base_leisure_fallback_to_avg_comp_main(self) -> None:
|
|
"""Test extract base leisure fallback to avg comp main."""
|
|
data: dict[str, Any] = {
|
|
"game": [{"comp_main": 10800, "comp_plus": 0, "comp_100": 0}],
|
|
}
|
|
assert _extract_base_leisure_hours(data) == round(10800 / 3600, 2)
|
|
|
|
def test_extract_dlc_relationships_skips_non_dict(self) -> None:
|
|
"""Test extract dlc relationships skips non dict."""
|
|
data: dict[str, Any] = {
|
|
"relationships": [
|
|
"bad",
|
|
{"game_type": "dlc", "game_id": 7, "comp_100": 3600},
|
|
],
|
|
}
|
|
assert _extract_dlc_relationships(data) == [(7, 1.0)]
|
|
|
|
def test_collect_dlc_relationships_ignores_non_positive_id(self) -> None:
|
|
"""Test collect dlc relationships ignores non positive id."""
|
|
valid = [
|
|
HLTBResult(
|
|
app_id=1,
|
|
game_name="Game",
|
|
completionist_hours=1.0,
|
|
similarity=1.0,
|
|
hltb_game_id=123,
|
|
)
|
|
]
|
|
details: list[dict[str, Any] | None] = [
|
|
{
|
|
"relationships": [
|
|
{"game_type": "dlc", "game_id": 0, "comp_100": 3600},
|
|
]
|
|
}
|
|
]
|
|
by_app, ids = _collect_dlc_relationships(valid, details)
|
|
assert by_app[1] == [(0, 1.0)]
|
|
assert ids == []
|
|
|
|
def test_apply_dlc_leisure_overrides(self) -> None:
|
|
"""Test apply dlc leisure overrides."""
|
|
adjusted = _apply_dlc_leisure_overrides(
|
|
base_hours=6.0,
|
|
dlc_rels=[(10, 1.0), (11, 2.0)],
|
|
dlc_hours_by_id={10: 3.0},
|
|
)
|
|
assert adjusted == 8.0
|
|
|
|
def test_fetch_dlc_leisure_hours_empty(self) -> None:
|
|
"""Test fetch dlc leisure hours empty."""
|
|
|
|
async def _run() -> dict[int, float]:
|
|
"""Test run."""
|
|
async with aiohttp.ClientSession() as session:
|
|
return await _fetch_dlc_leisure_hours(asyncio.Semaphore(1), session, [])
|
|
|
|
assert asyncio.run(_run()) == {}
|
|
|
|
def test_fetch_dlc_leisure_hours_skips_none_data(self) -> None:
|
|
"""Test fetch dlc leisure hours skips none data."""
|
|
|
|
async def _run() -> dict[int, float]:
|
|
"""Test run."""
|
|
async with aiohttp.ClientSession() as session:
|
|
with patch(
|
|
"steam_backlog_enforcer._hltb_detail._fetch_detail_one",
|
|
new_callable=AsyncMock,
|
|
return_value=None,
|
|
):
|
|
return await _fetch_dlc_leisure_hours(
|
|
asyncio.Semaphore(1),
|
|
session,
|
|
[1],
|
|
)
|
|
|
|
assert asyncio.run(_run()) == {}
|
|
|
|
def test_fetch_dlc_leisure_hours_skips_non_positive_leisure(self) -> None:
|
|
"""Test fetch dlc leisure hours skips non positive leisure."""
|
|
bad_dlc_data: dict[str, Any] = {
|
|
"game": [{"comp_100_h": 0, "comp_100": 0}],
|
|
"relationships": [],
|
|
}
|
|
|
|
async def _run() -> dict[int, float]:
|
|
"""Test run."""
|
|
async with aiohttp.ClientSession() as session:
|
|
with patch(
|
|
"steam_backlog_enforcer._hltb_detail._fetch_detail_one",
|
|
new_callable=AsyncMock,
|
|
return_value=bad_dlc_data,
|
|
):
|
|
return await _fetch_dlc_leisure_hours(
|
|
asyncio.Semaphore(1),
|
|
session,
|
|
[1],
|
|
)
|
|
|
|
assert asyncio.run(_run()) == {}
|