Fix mechanical pylint findings in tests

Booleaness asserts, hoisted imports, fixture(name=...) instead of
shadowing arguments, explicit encodings, snake_case names, comparison
of bound methods, dead protected-access disables.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WzxdWYdwRAtMg7ki6TKxuU
This commit is contained in:
Krzysztof Rudnicki 2026-11-01 21:02:40 +01:00
parent f3875302d7
commit 4a36b6b264
111 changed files with 313 additions and 359 deletions

View File

@ -45,8 +45,8 @@ def mock_tk() -> Generator[MagicMock]:
yield mock
@pytest.fixture
def mock_sys_exit() -> Generator[MagicMock]:
@pytest.fixture(name="mock_sys_exit")
def mock_sys_exit_fixture() -> Generator[MagicMock]:
"""Mock sys.exit to prevent test termination."""
with patch("screen_locker.screen_lock.sys.exit") as mock:
yield mock

View File

@ -24,7 +24,12 @@ import sys
from typing import TYPE_CHECKING
from unittest.mock import patch
import freedays._api
import pytest
import sickdays
import sickdays.gui
import screen_locker._night_lockdown
if TYPE_CHECKING:
from collections.abc import Iterator
@ -44,7 +49,6 @@ def _no_free_days_by_default(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) ->
chain in every test here -- it is the very first check in the chain --
and the failures would name early-bird and sick-day logic instead.
"""
import freedays._api
monkeypatch.setattr(
freedays._api,
@ -64,8 +68,6 @@ def _no_sick_days_by_default(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) ->
developer's real today sick. ``sync_quietly`` is stubbed so the button's
background push never reaches Firebase.
"""
import sickdays
import sickdays.gui # so its modules are in sys.modules below
pool = sickdays.Paths.under(tmp_path / "sickdays")
# Every sickdays module that bound ``resolve_paths`` by name: the readers
@ -193,7 +195,6 @@ def _no_real_night_lockdown(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) ->
this every enforce-path test would skip the lock on any evening after the
real lockdown fired -- and pass in the morning.
"""
import screen_locker._night_lockdown
monkeypatch.setattr(
screen_locker._night_lockdown,

View File

@ -19,6 +19,7 @@ from unittest.mock import MagicMock, patch
import pytest
from screen_locker import _sync_client
from screen_locker._degraded_sources import clear_degraded_sources
from screen_locker._earned import flat_earners
from screen_locker.tests._gatelock_fixtures import (
@ -37,7 +38,7 @@ from screen_locker.tests._locker_factories import (
)
from screen_locker.tests._opt_in_fixtures import (
_mock_sys_exit,
mock_sys_exit,
mock_sys_exit_fixture,
mock_tk,
temp_log_file,
)
@ -61,7 +62,7 @@ __all__ = [
"create_locker_early_bird",
"create_locker_relaxed_day",
"dual_output",
"mock_sys_exit",
"mock_sys_exit_fixture",
"mock_tk",
"temp_log_file",
]
@ -153,7 +154,7 @@ def _block_real_network() -> Iterator[None]:
@pytest.fixture(autouse=True)
def _clear_degraded_sources() -> Iterator[None]:
def _clear_degraded_sources() -> None:
"""Reset the process-scoped degraded-backend registry around every test.
``_degraded_sources._degraded`` is a module-level list, so a test that
@ -162,7 +163,6 @@ def _clear_degraded_sources() -> Iterator[None]:
the local venvs do not) exposes it -- it failed on push and nowhere else.
"""
clear_degraded_sources()
return
@pytest.fixture(autouse=True)
@ -245,6 +245,5 @@ def _no_real_firebase_config(monkeypatch: pytest.MonkeyPatch) -> None:
"""
# Patch _sync_client's binding: crdt_sync's own CONFIG_FILE is bound to
# the real home at import, which _isolate_home cannot redirect.
from screen_locker import _sync_client
monkeypatch.setattr(_sync_client, "CONFIG_FILE", Path("/nonexistent/firebase.json"))

View File

@ -1,5 +1,4 @@
"""Tests for ADB commands, phone connection, and database operations."""
# pylint: disable=protected-access,unused-argument
from __future__ import annotations

View File

@ -1,5 +1,4 @@
"""Tests for ADB commands, phone connection, and database operations."""
# pylint: disable=protected-access,unused-argument
from __future__ import annotations

View File

@ -4,7 +4,6 @@ Before this, both fell through to building the demo lock screen. Split from
``test_cli.py`` for the 250-line cap. ``sys.exit`` is a no-op suite-wide (see
conftest), so each test installs one that really raises ``SystemExit``.
"""
# pylint: disable=protected-access
from __future__ import annotations
@ -47,7 +46,7 @@ def test_help_prints_usage_and_exits_zero(
"""Help goes to stdout and the process exits 0 before any UI."""
with pytest.raises(SystemExit) as exited:
_main(["--production", flag])
assert exited.value.code == 0
assert not exited.value.code
out = capsys.readouterr().out
assert out.startswith("usage: python3 -m screen_locker.screen_lock")

View File

@ -11,6 +11,7 @@ import freedays
import sickdays
from screen_locker._compliance_state import explain_lock_decision
from screen_locker._log_mixin import write_signed_entry
from screen_locker.tests._sick_helpers import mark_sick
if TYPE_CHECKING:
@ -176,7 +177,6 @@ class TestExplainLockDecision:
even after the user already dismissed today's prompt, which is
misleading — the popup is not about to be shown again.
"""
from screen_locker._log_mixin import write_signed_entry
files = self._files(tmp_path)
write_signed_entry(files["log_file"], _today(), {"type": "relaxed_day_skip"})

View File

@ -50,7 +50,7 @@ class TestFindSiblingRefreshToken:
expires_at="2026-08-24T12:00:00+00:00",
)
assert find_sibling_refresh_tokens(tmp_path, skip="screen_locker") == []
assert not find_sibling_refresh_tokens(tmp_path, skip="screen_locker")
def test_skips_unreadable_and_malformed_caches(self, tmp_path: Path) -> None:
"""A corrupt sibling must not abort the search over the healthy ones."""
@ -69,7 +69,7 @@ class TestFindSiblingRefreshToken:
def test_reports_nothing_when_no_sibling_has_one(self, tmp_path: Path) -> None:
"""No source is a real outcome; the caller must hear it, not guess."""
assert find_sibling_refresh_tokens(tmp_path, skip="screen_locker") == []
assert not find_sibling_refresh_tokens(tmp_path, skip="screen_locker")
def test_missing_config_root_is_not_an_error(self, tmp_path: Path) -> None:
"""A machine with no ~/.config yet has no donor -- and must not crash.
@ -78,9 +78,7 @@ class TestFindSiblingRefreshToken:
would take the screen for a reason that has nothing to do with
working out.
"""
assert (
find_sibling_refresh_tokens(tmp_path / "nope", skip="screen_locker") == []
)
assert not find_sibling_refresh_tokens(tmp_path / "nope", skip="screen_locker")
def test_cache_without_a_usable_token_is_skipped(self, tmp_path: Path) -> None:
"""Readable JSON is not the same as a usable credential.

View File

@ -11,6 +11,7 @@ from __future__ import annotations
from types import SimpleNamespace
from typing import TYPE_CHECKING
from screen_locker import _sync_client
from screen_locker._credential_recovery import recover_session
from screen_locker.tests._credential_fixtures import write_cache as _write_cache
@ -94,7 +95,6 @@ class TestTryRecoverFirebaseSession:
instead of clobbering the machine's live credential -- the exact
accident this suite caused on 2026-08-27.
"""
from screen_locker import _sync_client
_write_cache(
tmp_path / ".config" / "diet_guard" / "firebase_auth.json",

View File

@ -4,7 +4,6 @@ The autouse ``_no_desktop_notifications`` fixture stubs
``notify_workout_credit`` for the whole suite; the real one is captured here
at import time, before any fixture runs.
"""
# pylint: disable=protected-access
from __future__ import annotations
@ -23,7 +22,7 @@ if TYPE_CHECKING:
from collections.abc import Iterator
from pathlib import Path
_REAL_NOTIFY = _credit_notify.notify_workout_credit
_real_notify = _credit_notify.notify_workout_credit
_FRIDAY = "2026-10-09" # Thu-Sun: tonight is the second config field
_MOD = "screen_locker._credit_notify"
@ -150,7 +149,7 @@ def test_notify_workout_credit_sends_the_composed_body() -> None:
patch(f"{_MOD}.gaming_minutes_today", return_value=60),
patch(f"{_MOD}.send_notification", return_value=True) as send,
):
assert _REAL_NOTIFY((0, 1200, 0), (0, 1260, 0), None)
assert _real_notify((0, 1200, 0), (0, 1260, 0), None)
send.assert_called_once_with(
_credit_notify.TITLE, "Shutdown 21:00 (+1h) \N{MIDDLE DOT} gaming 1.0h"
)

View File

@ -4,7 +4,6 @@ The two StrongLifts ingestion paths of ONE session used to earn +2h and then
+1h (2026-10-09). These tests drive the real shutdown arithmetic over a fake
config so the minute figures, not just the branch taken, are pinned.
"""
# pylint: disable=protected-access
from __future__ import annotations
@ -154,7 +153,7 @@ class TestSlotCreditMinutes:
with caplog.at_level(logging.WARNING, logger="screen_locker._workout_credit"):
result, notify = _credit(locker, [_entry("pc_workout_verified")])
assert result.already_counted_today is True
assert result.extra_bonus_delta == 0
assert not result.extra_bonus_delta
assert locker._read_shutdown_config() == _START # untouched
notify.assert_not_called()
assert any(

View File

@ -163,7 +163,7 @@ class TestReadDecisions:
) -> None:
"""No history yet is normal, but is still stated out loud."""
with caplog.at_level("WARNING"):
assert read_decisions(log_file=tmp_path / "nope.jsonl") == []
assert not read_decisions(log_file=tmp_path / "nope.jsonl")
assert any("No decision log" in r.message for r in caplog.records)
def test_unreadable_file_is_reported(
@ -176,7 +176,7 @@ class TestReadDecisions:
patch.object(Path, "read_text", side_effect=OSError("locked")),
caplog.at_level("WARNING"),
):
assert read_decisions(log_file=target) == []
assert not read_decisions(log_file=target)
assert any("Could not read" in r.message for r in caplog.records)
def test_corrupt_lines_are_skipped_not_fatal(

View File

@ -75,4 +75,4 @@ class TestReasonsExtra:
"workout_logged_today": lambda: False,
},
)
assert extra == {}
assert not extra

View File

@ -45,8 +45,8 @@ _LEDGERED = pytest.mark.parametrize(
)
@pytest.fixture
def key(tmp_path: Path) -> Iterator[Path]:
@pytest.fixture(name="key")
def key_fixture(tmp_path: Path) -> Iterator[Path]:
with signing_key(tmp_path) as path:
yield path

View File

@ -77,7 +77,7 @@ class TestProcessWeekTransition:
year, week, _ = now.isocalendar()
f = tmp_path / "state.json"
f.write_text(json.dumps({"last_processed_iso_week": f"{year}-W{week:02d}"}))
assert process_week_transition(tmp_path / "log.json", f) == []
assert not process_week_transition(tmp_path / "log.json", f)
@staticmethod
def _current_week_str() -> str:
@ -175,7 +175,7 @@ class TestProcessWeekTransition:
rewards = process_week_transition(tmp_path / "log.json", f)
assert any("Streak reset" in r for r in rewards)
assert json.loads(f.read_text())["consecutive_5plus_weeks"] == 0
assert not json.loads(f.read_text())["consecutive_5plus_weeks"]
def test_no_reset_message_when_streak_was_zero(self, tmp_path: Path) -> None:
"""Zero streak + < 5 workouts: no reset message (line 101 branch False)."""
@ -233,7 +233,7 @@ class TestCurrentStreak:
def test_returns_zero_when_file_missing(self, tmp_path: Path) -> None:
"""Missing file falls through _load_state → default 0."""
assert current_streak(tmp_path / "nonexistent.json") == 0
assert not current_streak(tmp_path / "nonexistent.json")
def test_returns_stored_streak(self, tmp_path: Path) -> None:
"""Stored streak value is returned correctly."""

View File

@ -65,14 +65,14 @@ class TestBonusDroppedAtW42:
# The message the transition produced is kept for the status window,
# for the week it was earned in only.
assert week_rewards(f, today=w42) == tuple(rewards)
assert week_rewards(f, today=datetime(2026, 10, 19, 12, 0, tzinfo=UTC)) == ()
assert not week_rewards(f, today=datetime(2026, 10, 19, 12, 0, tzinfo=UTC))
def test_no_stored_rewards_reads_as_empty(self, tmp_path: Path) -> None:
"""Missing file, or a state without the key, has nothing to show."""
assert week_rewards(tmp_path / "absent.json") == ()
assert not week_rewards(tmp_path / "absent.json")
f = tmp_path / "state.json"
f.write_text(json.dumps({"consecutive_5plus_weeks": 2}))
assert week_rewards(f) == ()
assert not week_rewards(f)
class TestHasExtendedEarlyBird:

View File

@ -149,7 +149,7 @@ class TestApplyCountedBonus:
[{"date": "2000-01-01", "units": 9}, {"date": None}, "junk", {"units": "x"}],
)
def test_a_stale_or_broken_stamp_is_zero(self, stamp: object) -> None:
assert applied_units({"blocks_bonus_units": stamp}, BLOCKS, today_str()) == 0
assert not applied_units({"blocks_bonus_units": stamp}, BLOCKS, today_str())
def test_failed_write_keeps_the_stamp(
self, caplog: pytest.LogCaptureFixture
@ -158,7 +158,7 @@ class TestApplyCountedBonus:
write_ledger(BLOCKS, [_block(1)])
adjust = MagicMock(return_value=False)
assert not apply_counted_bonus(state, adjust, BLOCKS, today_str())
assert state == {}
assert not state
assert "Blocks bonus: failed to write" in caplog.text
def test_unknown_earns_nothing_and_says_so(

View File

@ -10,6 +10,7 @@ from unittest.mock import MagicMock, patch
import pytest
from screen_locker import _constants, _instance
from screen_locker.screen_lock import _assert_not_under_pytest
from screen_locker.tests.conftest import create_locker
@ -75,11 +76,8 @@ class TestScreenLockerInit:
This is the actual fix for "unlocked, then locked again seconds
later" -- see the 2026-08-21 incident notes in screen_lock.py.
"""
from screen_locker import _instance
from screen_locker._constants import INSTANCE_LOCK_FILE
mock_sys_exit.side_effect = SystemExit(0)
held = _instance.acquire(INSTANCE_LOCK_FILE)
held = _instance.acquire(_constants.INSTANCE_LOCK_FILE)
assert held is not None
try:
with pytest.raises(SystemExit):

View File

@ -26,7 +26,7 @@ if TYPE_CHECKING:
from collections.abc import Iterator
# Captured before conftest pins it to noon for every test.
_REAL_NOW = _late_credit._now
_real_now = _late_credit._now
_DAY = date(2026, 10, 17) # a Saturday: the Thu-Sun band is tonight's
_ISO = _DAY.isoformat()
@ -133,7 +133,7 @@ class TestLateCredit:
def test_now_is_the_wall_clock(self) -> None:
with patch.object(_late_credit.time, "time", return_value=123.0):
assert _REAL_NOW() == 123.0
assert _real_now() == 123.0
def test_a_late_credit_clears_the_lift(self) -> None:
"""Past the shutdown the floor is spent: nothing is left to absorb."""

View File

@ -22,8 +22,8 @@ from screen_locker import _earned, _gate_off
SAT = date(2026, 10, 24)
@pytest.fixture
def pools(monkeypatch: pytest.MonkeyPatch) -> dict[str, bool]:
@pytest.fixture(name="pools")
def pools_fixture(monkeypatch: pytest.MonkeyPatch) -> dict[str, bool]:
"""Fake the free-day and sick-day pools; flip the returned flags."""
flags = {"free": False, "sick_off": False}
monkeypatch.setattr(freedays, "is_free_day", lambda _day=None: flags["free"])

View File

@ -38,8 +38,8 @@ LOCAL_DAY = "2026-09-13"
UTC_DAY = "2026-09-12"
@pytest.fixture
def warsaw_after_midnight(monkeypatch: pytest.MonkeyPatch) -> Iterator[None]:
@pytest.fixture(name="warsaw_after_midnight")
def warsaw_after_midnight_fixture(monkeypatch: pytest.MonkeyPatch) -> Iterator[None]:
"""Pin TZ to Europe/Warsaw and the clock to 00:02:35 local on the 13th."""
monkeypatch.setenv("TZ", "Europe/Warsaw")
time.tzset()

View File

@ -50,7 +50,7 @@ class TestLoadWorkoutLog:
def test_missing_file_returns_empty(self, tmp_path: Path) -> None:
"""Non-existent file → {}."""
assert load_workout_log(tmp_path / "nope.json") == {}
assert not load_workout_log(tmp_path / "nope.json")
def test_legacy_dict_value_is_wrapped_in_a_list(self, tmp_path: Path) -> None:
"""The old one-entry-per-day shape reads as a one-element list."""

View File

@ -95,8 +95,8 @@ def test_an_evidence_less_stub_never_consumes_budget(tmp_path: Path) -> None:
)
log_file = _log_from([stub], tmp_path / "log.json")
assert count_in_window(log_file, 7, today=today) == 0
assert count_in_window(log_file, 30, today=today) == 0
assert not count_in_window(log_file, 7, today=today)
assert not count_in_window(log_file, 30, today=today)
def _planned_cases() -> list[tuple[str, dict[str, Any]]]:

View File

@ -85,7 +85,7 @@ def test_logs_a_valid_workout(tmp_path: Path) -> None:
with patch(
"screen_locker._log_mixin.compute_entry_hmac", return_value="test-signature"
):
assert run_manual_log(log_file, _args()) == 0
assert not run_manual_log(log_file, _args())
(entry,) = _entries(log_file)
data = entry["workout_data"]
@ -142,7 +142,7 @@ def test_rerunning_the_same_workout_logs_it_once(tmp_path: Path) -> None:
"""The record id is derived from (date, start), so a repeat is a no-op."""
log_file = tmp_path / "log.json"
assert run_manual_log(log_file, _args()) == 0
assert not run_manual_log(log_file, _args())
assert run_manual_log(log_file, _args()) == 1
assert len(_entries(log_file)) == 1
@ -151,8 +151,8 @@ def test_the_rolling_budget_still_caps_headless_entries(tmp_path: Path) -> None:
"""The third workout in a week is refused, exactly as on the phone."""
log_file = tmp_path / "log.json"
assert run_manual_log(log_file, _args(start="07:00", end="08:30")) == 0
assert run_manual_log(log_file, _args(start="10:00", end="11:30")) == 0
assert not run_manual_log(log_file, _args(start="07:00", end="08:30"))
assert not run_manual_log(log_file, _args(start="10:00", end="11:30"))
assert run_manual_log(log_file, _args(start="17:30", end="19:00")) == 1
assert len(_entries(log_file)) == 2
@ -162,7 +162,7 @@ def test_date_defaults_to_today(tmp_path: Path) -> None:
log_file = tmp_path / "log.json"
args = [a for a in _args() if a not in {"--date", _TODAY}]
assert run_manual_log(log_file, args) == 0
assert not run_manual_log(log_file, args)
assert _entries(log_file)
@ -190,5 +190,5 @@ def test_table_tennis_needs_no_activity_name(tmp_path: Path) -> None:
"Nittaku",
]
assert run_manual_log(log_file, args) == 0
assert not run_manual_log(log_file, args)
assert _entries(log_file)[0]["workout_data"]["activity_type"] == "table tennis"

View File

@ -33,8 +33,8 @@ class Held:
push: MagicMock
@pytest.fixture
def held(tmp_path: Path) -> Iterator[Held]:
@pytest.fixture(name="held")
def held_fixture(tmp_path: Path) -> Iterator[Held]:
"""Redirect the pending ledger and stub the publisher: nothing leaves the box."""
ledger = tmp_path / "manual_pending.json"
with (
@ -60,7 +60,7 @@ def test_a_future_date_is_held_not_logged(
log_file = tmp_path / "log.json"
day = _days_from_today(ahead)
assert run_manual_log(log_file, _args(date=day)) == 0
assert not run_manual_log(log_file, _args(date=day))
assert _entries(log_file) == []
assert list(json.loads(held.ledger.read_text())) == [f"manual:{day}T17:30"]
@ -93,8 +93,8 @@ def test_a_future_entry_that_breaks_the_budget_is_refused(
"""Scheduling counts the logged credits: a full week cannot take a 3rd."""
log_file = tmp_path / "log.json"
day = _days_from_today(1)
assert run_manual_log(log_file, _args(start="07:00", end="08:30")) == 0
assert run_manual_log(log_file, _args(start="10:00", end="11:30")) == 0
assert not run_manual_log(log_file, _args(start="07:00", end="08:30"))
assert not run_manual_log(log_file, _args(start="10:00", end="11:30"))
# Today holds 2; tomorrow's 7-day window contains today, so it is full too.
assert run_manual_log(log_file, _args(date=day)) == 1
@ -106,7 +106,7 @@ def test_scheduling_the_same_entry_twice_is_refused(tmp_path: Path, held: Held)
day = _days_from_today(2)
log_file = tmp_path / "log.json"
assert run_manual_log(log_file, _args(date=day)) == 0
assert not run_manual_log(log_file, _args(date=day))
assert run_manual_log(log_file, _args(date=day)) == 1
assert len(json.loads(held.ledger.read_text())) == 1
@ -116,8 +116,8 @@ def test_a_pending_future_entry_counts_against_today(
) -> None:
"""Today's entry sees pending future ones: they share a 7-day window."""
log_file = tmp_path / "log.json"
assert run_manual_log(log_file, _args(date=_days_from_today(1))) == 0
assert run_manual_log(log_file, _args(date=_days_from_today(2), start="08:00")) == 0
assert not run_manual_log(log_file, _args(date=_days_from_today(1)))
assert not run_manual_log(log_file, _args(date=_days_from_today(2), start="08:00"))
assert run_manual_log(log_file, _args()) == 1
assert _entries(log_file) == []

View File

@ -1,5 +1,4 @@
"""Wiring of future-dated manual workouts: push, sync pass, poke reset, budget helper."""
# pylint: disable=protected-access
from __future__ import annotations
@ -120,7 +119,9 @@ class TestSyncPassReadsTheLedger:
) as ingest:
locker._ingest_due_local_manual_workouts()
assert ingest.call_args.args[1] == [(_RID, _PAYLOAD)]
assert ingest.call_args.kwargs["on_ingested"] == locker._credit_ingested_workout
assert [ingest.call_args.kwargs["on_ingested"]] == [
locker._credit_ingested_workout
]
assert locker.workout_data == {}

View File

@ -1,5 +1,4 @@
"""Tests for the pending ledger of future-dated manual workouts held on this PC."""
# pylint: disable=protected-access
from __future__ import annotations
@ -132,7 +131,7 @@ class TestPendingLedgerRecords:
path = _ledger(tmp_path)
path.write_text(json.dumps({_RID: {"payload": _PAYLOAD, "hlc": "garbage"}}))
with caplog.at_level(logging.WARNING):
assert pending_ledger_records(path) == {}
assert not pending_ledger_records(path)
assert "NOT published" in caplog.text
def test_non_dict_payload_is_skipped(self, tmp_path: Path) -> None:
@ -141,7 +140,7 @@ class TestPendingLedgerRecords:
raw = json.loads(path.read_text())
raw[_RID]["payload"] = "nope"
path.write_text(json.dumps(raw))
assert pending_ledger_records(path) == {}
assert not pending_ledger_records(path)
class TestSettlePending:
@ -203,7 +202,7 @@ class TestPendingFutureDates:
def test_an_already_logged_id_is_not_pending(self, tmp_path: Path) -> None:
log_file = _log_with(tmp_path, _RID, "2026-10-17")
found = pending_future_dates(log_file, [(_RID, _PAYLOAD)], today="2026-10-16")
assert found == {}
assert not found
def test_defaults_to_the_real_local_day(self, tmp_path: Path) -> None:
payload = {**_PAYLOAD, "date": "2999-01-01"}

View File

@ -1,5 +1,4 @@
"""Tests for pushing the PC's workouts (from log.json) to the sync repo."""
# pylint: disable=protected-access
from __future__ import annotations
@ -132,7 +131,7 @@ class TestRecordsFromWorkoutLog:
log_file,
{"2026-07-13": [_entry({"type": "sick_day"}, "2026-07-13T07:00:00+00:00")]},
)
assert records_from_workout_log(log_file) == {}
assert not records_from_workout_log(log_file)
def test_non_dict_workout_data_is_skipped(self, tmp_path: Path) -> None:
"""Non dict workout data is skipped."""
@ -145,7 +144,7 @@ class TestRecordsFromWorkoutLog:
]
},
)
assert records_from_workout_log(log_file) == {}
assert not records_from_workout_log(log_file)
def test_entry_without_derivable_id_is_skipped(self, tmp_path: Path) -> None:
"""A counted type with no id can't be deduped, so it isn't synced."""
@ -154,7 +153,7 @@ class TestRecordsFromWorkoutLog:
log_file, {"2026-07-13": [_entry(_MANUAL, "2026-07-13T07:00:00+00:00")]}
)
with patch("screen_locker._manual_push._derive_workout_id", return_value=None):
assert records_from_workout_log(log_file) == {}
assert not records_from_workout_log(log_file)
def test_is_deterministic_across_calls(self, tmp_path: Path) -> None:
"""Same log → identical records+HLCs, so a re-push is a no-op."""

View File

@ -3,7 +3,6 @@
Split out of test_manual_push.py for the 250-line cap; the shared _entry /
_write_log helpers stay in that module and are imported here.
"""
# pylint: disable=protected-access
from __future__ import annotations
@ -67,7 +66,7 @@ class TestPushPcWorkouts:
result = push_pc_workouts(log_file)
build.assert_not_called()
assert result.pushed is False
assert result.record_count == 0
assert not result.record_count
assert "no counted workouts" in result.reason
def test_sync_error_reports_why(self, tmp_path: Path) -> None:

View File

@ -86,7 +86,7 @@ class TestSyncedTwinIsNotReIngested:
ingested = ingest_manual_records(log_file, [_published_twin()], today=LOCAL_DAY)
assert ingested == []
assert not ingested
assert len(json.loads(log_file.read_text())[LOCAL_DAY]) == 1
def test_ingested_entry_keeps_the_wire_id(self, tmp_path: Path) -> None:

View File

@ -48,8 +48,8 @@ def _draft(start: str = "18:00") -> ManualWorkoutDraft:
)
@pytest.fixture
def stubs(tmp_path: Path) -> Iterator[SimpleNamespace]:
@pytest.fixture(name="stubs")
def stubs_fixture(tmp_path: Path) -> Iterator[SimpleNamespace]:
"""Redirect the ledger; stub Firebase pull, the publisher and the identity."""
ledger = tmp_path / "pending.json"
pulled: list[tuple[str, dict]] = []

View File

@ -1,9 +1,8 @@
"""Tests for ingesting synced manual workouts into log.json."""
# pylint: disable=protected-access
from __future__ import annotations
from datetime import UTC
from datetime import UTC, datetime
import json
from typing import TYPE_CHECKING
from unittest.mock import patch
@ -121,14 +120,13 @@ class TestIngestManualRecords:
record = _manual_record(_TT_DRAFT)
ingest_manual_records(log_file, [record], today=_DATE)
again = ingest_manual_records(log_file, [record], today=_DATE)
assert again == []
assert not again
def test_skips_a_non_manual_payload(self, tmp_path: Path) -> None:
"""Skips a non manual payload."""
log_file = tmp_path / "log.json"
assert (
assert not (
ingest_manual_records(log_file, [("s", {"succeeded": True})], today=_DATE)
== []
)
def test_skips_a_record_without_a_date(self, tmp_path: Path) -> None:
@ -136,7 +134,7 @@ class TestIngestManualRecords:
log_file = tmp_path / "log.json"
_, payload = _manual_record(_TT_DRAFT)
del payload["date"]
assert ingest_manual_records(log_file, [("m", payload)], today=_DATE) == []
assert not ingest_manual_records(log_file, [("m", payload)], today=_DATE)
def test_appends_alongside_an_existing_counted_day(self, tmp_path: Path) -> None:
"""A day may hold several workouts: a synced manual is appended next to
@ -161,21 +159,21 @@ class TestIngestManualRecords:
log_file = tmp_path / "log.json"
_, payload = _manual_record(_TT_DRAFT)
payload["rpe"] = None # reconstruct_draft -> None
assert ingest_manual_records(log_file, [("m", payload)], today=_DATE) == []
assert not ingest_manual_records(log_file, [("m", payload)], today=_DATE)
def test_skips_an_invalid_draft(self, tmp_path: Path) -> None:
"""Skips an invalid draft."""
log_file = tmp_path / "log.json"
_, payload = _manual_record(_TT_DRAFT)
payload["location_name"] = " " # fails validate_manual_workout
assert ingest_manual_records(log_file, [("m", payload)], today=_DATE) == []
assert not ingest_manual_records(log_file, [("m", payload)], today=_DATE)
def test_skips_when_budget_exhausted(self, tmp_path: Path) -> None:
"""Skips when budget exhausted."""
log_file = tmp_path / "log.json"
record = _manual_record(_TT_DRAFT)
with patch.object(_manual_sync, "budget_error_for_date", return_value="full"):
assert ingest_manual_records(log_file, [record], today=_DATE) == []
assert not ingest_manual_records(log_file, [record], today=_DATE)
def test_writes_unsigned_entry_when_hmac_key_unavailable(
self, tmp_path: Path
@ -190,6 +188,5 @@ class TestIngestManualRecords:
def _dt(date: str) -> object:
from datetime import datetime
return datetime.strptime(date, "%Y-%m-%d").replace(tzinfo=UTC)

View File

@ -44,8 +44,8 @@ class TestDeferral:
today=_TODAY,
on_ingested=lambda entry, _prior: calls.append(entry),
)
assert ingested == []
assert calls == []
assert not ingested
assert not calls
assert _days(log_file) == {}
assert "deferred" in caplog.text
@ -56,7 +56,7 @@ class TestDeferral:
record = _manual_record(_TT_DRAFT, _TOMORROW)
calls: list[dict] = []
assert ingest_manual_records(log_file, [record], today=_TODAY) == []
assert not ingest_manual_records(log_file, [record], today=_TODAY)
ingested = ingest_manual_records(
log_file,
[record],
@ -74,29 +74,27 @@ class TestDeferral:
record_id, payload = _manual_record(_TT_DRAFT, _TODAY)
payload["date"] = "2026-13-99"
with caplog.at_level(logging.WARNING):
assert (
assert not (
ingest_manual_records(
tmp_path / "log.json", [(record_id, payload)], today=_TODAY
)
== []
)
assert "unparsable date" in caplog.text
def test_an_empty_batch_touches_nothing(self, tmp_path: Path) -> None:
assert ingest_manual_records(tmp_path / "log.json", [], today=_TODAY) == []
assert not ingest_manual_records(tmp_path / "log.json", [], today=_TODAY)
assert not (tmp_path / "log.json").exists()
def test_a_non_manual_record_is_ignored(self, tmp_path: Path) -> None:
assert (
assert not (
ingest_manual_records(
tmp_path / "log.json", [("s", {"exercises": []})], today=_TODAY
)
== []
)
def test_defaults_to_the_real_local_day(self, tmp_path: Path) -> None:
record = _manual_record(_TT_DRAFT, "2999-01-01")
assert ingest_manual_records(tmp_path / "log.json", [record]) == []
assert not ingest_manual_records(tmp_path / "log.json", [record])
class TestBudgetAgainstTheWorkoutDate:
@ -136,7 +134,7 @@ class TestBudgetAgainstTheWorkoutDate:
]
with caplog.at_level(logging.INFO):
ingested = ingest_manual_records(log_file, [due, *pending], today=_TOMORROW)
assert ingested == []
assert not ingested
assert "budget exhausted" in caplog.text
def test_a_logged_week_blocks_a_due_record_inside_it(self, tmp_path: Path) -> None:
@ -146,5 +144,5 @@ class TestBudgetAgainstTheWorkoutDate:
log_file, [_manual_record(draft, day)], today=day
)
blocked = _manual_record(_TT_DRAFT, _TODAY)
assert ingest_manual_records(log_file, [blocked], today=_TODAY) == []
assert not ingest_manual_records(log_file, [blocked], today=_TODAY)
assert _days(log_file) == {"2026-10-12": 1, "2026-10-13": 1}

View File

@ -1,5 +1,4 @@
"""Tests for ingesting synced manual workouts into log.json."""
# pylint: disable=protected-access
from __future__ import annotations

View File

@ -1,5 +1,4 @@
"""Tests for ingesting synced manual workouts into log.json."""
# pylint: disable=protected-access
from __future__ import annotations
@ -98,7 +97,7 @@ class TestIngestManualRecordsCallback:
today=_DATE,
on_ingested=lambda entry, prior: calls.append((entry, prior)),
)
assert calls == []
assert not calls
def test_write_chokepoint_collision_is_not_credited_or_returned(
self, tmp_path: Path
@ -120,8 +119,8 @@ class TestIngestManualRecordsCallback:
today=_DATE,
on_ingested=lambda entry, prior: calls.append((entry, prior)),
)
assert ingested == []
assert calls == []
assert not ingested
assert not calls
class TestEmptyStubRecords:
@ -165,7 +164,7 @@ class TestEmptyStubRecords:
)
],
)
assert ingested == []
assert not ingested
stub_records = [r for r in caplog.records if "empty stub" in r.message]
assert stub_records, "the stub must still be reported"
assert all(r.levelname == "INFO" for r in stub_records)

View File

@ -1,5 +1,4 @@
"""Tests for the manual-workout pure-logic module."""
# pylint: disable=protected-access
from __future__ import annotations
@ -71,13 +70,13 @@ class TestCountInWindow:
def test_missing_file_returns_zero(self, tmp_path: Path) -> None:
"""Missing file returns zero."""
log_file = tmp_path / "does_not_exist.json"
assert count_in_window(log_file, 7, today=_TODAY) == 0
assert not count_in_window(log_file, 7, today=_TODAY)
def test_corrupt_json_returns_zero(self, tmp_path: Path) -> None:
"""Corrupt JSON returns zero."""
log_file = tmp_path / "log.json"
log_file.write_text("not json")
assert count_in_window(log_file, 7, today=_TODAY) == 0
assert not count_in_window(log_file, 7, today=_TODAY)
def test_skips_invalid_date_keys(self, tmp_path: Path) -> None:
"""Skips invalid date keys."""
@ -96,12 +95,12 @@ class TestCountInWindow:
"""Returns zero when today invalid."""
log_file = tmp_path / "log.json"
_write_logs(log_file, {"2026-07-04": "manual_workout"})
assert count_in_window(log_file, 7, today="bogus") == 0
assert not count_in_window(log_file, 7, today="bogus")
def test_uses_today_default_when_none(self, tmp_path: Path) -> None:
"""Uses today default when none."""
log_file = tmp_path / "log.json"
assert count_in_window(log_file, 7) == 0
assert not count_in_window(log_file, 7)
def test_multiple_same_day_entries_each_count(self, tmp_path: Path) -> None:
"""Per-entry counting: same-day manual workouts no longer collapse to
@ -152,7 +151,7 @@ class TestIsBudgetExhausted:
]
entries = dict.fromkeys(dates, "manual_workout")
_write_logs(log_file, entries)
assert count_in_window(log_file, 7, today=_TODAY) == 0
assert not count_in_window(log_file, 7, today=_TODAY)
assert is_budget_exhausted(log_file, today=_TODAY) is True

View File

@ -1,5 +1,4 @@
"""Tests for the workout-date field and the future-dated submit path."""
# pylint: disable=protected-access
from __future__ import annotations

View File

@ -1,5 +1,4 @@
"""Tests for the manual-workout evidence-form dialog mixin."""
# pylint: disable=protected-access
from __future__ import annotations
@ -88,7 +87,7 @@ class TestGridCursor:
locker._mw_grid_counters = {}
parent = object()
# Fresh (even) cursor: reserves row 0, then row 1 on the next call.
assert locker._mw_next_full_row(parent) == 0
assert not locker._mw_next_full_row(parent)
assert locker._mw_next_full_row(parent) == 1
def test_full_row_from_odd_cursor_bumps_to_next_row(

View File

@ -1,5 +1,4 @@
"""Tests for manual-workout integration in _ui_flows.py and screen_lock.py."""
# pylint: disable=protected-access
from __future__ import annotations

View File

@ -1,5 +1,4 @@
"""Tests for manual-workout integration in _ui_flows.py and screen_lock.py."""
# pylint: disable=protected-access
from __future__ import annotations
@ -64,7 +63,7 @@ class TestApplyWorkoutCredit:
assert result.already_counted_today is True
assert result.shutdown_adjusted is False
assert result.extra_bonus_delta == 0
assert not result.extra_bonus_delta
locker._try_adjust_shutdown_for_workout.assert_not_called()
locker.save_workout_log.assert_called_once() # log entry write attempted
@ -87,7 +86,7 @@ class TestApplyWorkoutCredit:
assert result.already_counted_today is False
assert result.shutdown_adjusted is True
assert result.extra_bonus_delta == 0
assert not result.extra_bonus_delta
locker._try_adjust_shutdown_for_workout.assert_called_once()
def test_additional_manual_workout_earns_plus_one_hour(

View File

@ -2,7 +2,6 @@
Split out of test_manual_workout_features_part2.py for the 250-line cap.
"""
# pylint: disable=protected-access
from __future__ import annotations
@ -58,7 +57,7 @@ class TestApplyWorkoutCredit:
result = locker._apply_workout_credit()
assert result.shutdown_adjusted is False
assert result.extra_bonus_delta == 0
assert not result.extra_bonus_delta
locker._try_adjust_shutdown_for_workout.assert_not_called()
locker._adjust_shutdown_time_by.assert_not_called()
@ -88,7 +87,7 @@ class TestApplyWorkoutCredit:
result = locker._apply_credit_for_written_entry(prior)
assert result.shutdown_adjusted is False
assert result.extra_bonus_delta == 0
assert not result.extra_bonus_delta
locker._try_adjust_shutdown_for_workout.assert_not_called()
locker._adjust_shutdown_time_by.assert_not_called()
notify.assert_not_called()

View File

@ -1,5 +1,4 @@
"""Tests for the manual-workout pure-logic module."""
# pylint: disable=protected-access
from __future__ import annotations
@ -140,7 +139,7 @@ class TestBuildEntry:
def test_duration_minutes_empty_when_unparsable(self) -> None:
"""Duration minutes empty when unparsable."""
entry = build_entry(_tt_draft(start_time="bogus"))
assert entry["duration_minutes"] == ""
assert not entry["duration_minutes"]
def test_strips_whitespace_fields(self) -> None:
"""Strips whitespace fields."""

View File

@ -1,5 +1,4 @@
"""Tests for manual-workout integration in status_view.py."""
# pylint: disable=protected-access
from __future__ import annotations

View File

@ -69,7 +69,7 @@ def test_the_retry_budget_is_screen_lockers_own() -> None:
"""The conftest zeroes it; raising it here makes the shared reader wait."""
naps: list[float] = []
assert morning_skip_today(NOW, wait=True, sleep=naps.append) is None
assert naps == []
assert not naps
with patch.object(_morning_session, "MORNING_RETRY_SECONDS", 10.0):
assert morning_skip_today(NOW, wait=True, sleep=naps.append) is None
assert naps

View File

@ -31,7 +31,7 @@ EVENING = datetime(2026, 10, 17, 21, 5).astimezone()
def _stamp(when: str) -> None:
"""Write the redirected stamp for a LOCAL wall-clock time."""
epoch = int(datetime.fromisoformat(when).astimezone().timestamp())
_night_lockdown.NIGHT_LOCKDOWN_STAMP.write_text(f"{epoch}\n")
_night_lockdown.NIGHT_LOCKDOWN_STAMP.write_text(f"{epoch}\n", encoding="utf-8")
class TestTonightsLockdown:
@ -53,7 +53,9 @@ class TestTonightsLockdown:
assert tonights_lockdown(NOW) is None
def test_garbage_is_not(self) -> None:
_night_lockdown.NIGHT_LOCKDOWN_STAMP.write_text("not an epoch")
_night_lockdown.NIGHT_LOCKDOWN_STAMP.write_text(
"not an epoch", encoding="utf-8"
)
assert tonights_lockdown(NOW) is None

View File

@ -1,5 +1,4 @@
"""Tests for phone workout verification, phone check, and unlock operations."""
# pylint: disable=protected-access,unused-argument
from __future__ import annotations

View File

@ -1,5 +1,4 @@
"""Tests for _start_phone_check, polling, and startup result routing."""
# pylint: disable=protected-access,unused-argument
from __future__ import annotations

View File

@ -40,7 +40,7 @@ def _adb_pull_from(path_contents: dict[str, str]) -> MagicMock:
content = path_contents.get(remote)
if content is None:
return False, ""
Path(dest).write_text(content)
Path(dest).write_text(content, encoding="utf-8")
return True, ""
return MagicMock(side_effect=fake_run_adb)

View File

@ -39,7 +39,7 @@ class TestTryFillStronglifitsForWeek:
log_file = tmp_path / "log.json"
log_file.write_text("{}")
object.__setattr__(locker, "_has_adb_device", MagicMock(return_value=False))
assert locker._try_fill_stronglifts_for_week(log_file) == 0
assert not locker._try_fill_stronglifts_for_week(log_file)
def test_returns_zero_when_no_json_pulled(
self, mock_tk: MagicMock, mock_sys_exit: MagicMock, tmp_path: Path
@ -51,7 +51,7 @@ class TestTryFillStronglifitsForWeek:
object.__setattr__(
locker, "_pull_workout_app_json", MagicMock(return_value=None)
)
assert locker._try_fill_stronglifts_for_week(log_file) == 0
assert not locker._try_fill_stronglifts_for_week(log_file)
def test_returns_zero_when_date_unparsable(
self, mock_tk: MagicMock, mock_sys_exit: MagicMock, tmp_path: Path
@ -65,7 +65,7 @@ class TestTryFillStronglifitsForWeek:
"_pull_workout_app_json",
MagicMock(return_value={"date": "not-a-date", "exercises": ["x"]}),
)
assert locker._try_fill_stronglifts_for_week(log_file) == 0
assert not locker._try_fill_stronglifts_for_week(log_file)
def test_returns_zero_when_date_outside_current_week(
self, mock_tk: MagicMock, mock_sys_exit: MagicMock, tmp_path: Path
@ -88,7 +88,7 @@ class TestTryFillStronglifitsForWeek:
}
),
)
assert locker._try_fill_stronglifts_for_week(log_file) == 0
assert not locker._try_fill_stronglifts_for_week(log_file)
def test_returns_zero_when_content_invalid(
self, mock_tk: MagicMock, mock_sys_exit: MagicMock, tmp_path: Path
@ -104,7 +104,7 @@ class TestTryFillStronglifitsForWeek:
"_pull_workout_app_json",
MagicMock(return_value={"date": yesterday, "exercises": []}),
)
assert locker._try_fill_stronglifts_for_week(log_file) == 0
assert not locker._try_fill_stronglifts_for_week(log_file)
def test_fills_gap_for_past_week_day(
self, mock_tk: MagicMock, mock_sys_exit: MagicMock, tmp_path: Path
@ -191,5 +191,5 @@ class TestTryFillStronglifitsForWeek:
)
with patch("screen_locker._phone_verification.datetime", _FakeDatetime):
assert locker._try_fill_stronglifts_for_week(log_file) == 0
assert not locker._try_fill_stronglifts_for_week(log_file)
assert json.loads(log_file.read_text()) == {}

View File

@ -47,8 +47,8 @@ def _result(*, counted: bool = False) -> WorkoutCreditResult:
)
@pytest.fixture
def log_file(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
@pytest.fixture(name="log_file")
def log_file_fixture(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
"""A fresh PokeLocker on a tmp log; the day reset and the credit stubbed."""
log = tmp_path / "poke_log.json"
@ -64,8 +64,8 @@ def log_file(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
return log
@pytest.fixture
def credit(monkeypatch: pytest.MonkeyPatch) -> MagicMock:
@pytest.fixture(name="credit")
def credit_fixture(monkeypatch: pytest.MonkeyPatch) -> MagicMock:
"""Replace the shared credit step; the ingest itself stays real."""
stub = MagicMock(return_value=_result())
monkeypatch.setattr(PokeLocker, "_apply_credit_for_written_entry", stub)

View File

@ -16,8 +16,8 @@ if TYPE_CHECKING:
from pathlib import Path
@pytest.fixture
def log_file(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
@pytest.fixture(name="log_file")
def log_file_fixture(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
"""A fresh PokeLocker on a tmp log; the day reset stubbed."""
log = tmp_path / "poke_log.json"
@ -33,8 +33,8 @@ def log_file(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
return log
@pytest.fixture
def credit(monkeypatch: pytest.MonkeyPatch) -> MagicMock:
@pytest.fixture(name="credit")
def credit_fixture(monkeypatch: pytest.MonkeyPatch) -> MagicMock:
"""Replace the shared credit step."""
stub = MagicMock(
return_value=WorkoutCreditResult(

View File

@ -56,8 +56,8 @@ def _daemon_stubs(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
monkeypatch.setattr(poke_server, "PokeServer", FakeServer)
@pytest.fixture(autouse=True)
def stream(monkeypatch: pytest.MonkeyPatch) -> MagicMock:
@pytest.fixture(name="stream", autouse=True)
def stream_fixture(monkeypatch: pytest.MonkeyPatch) -> MagicMock:
"""The Firebase stream class; never a real connection."""
stub = MagicMock()
monkeypatch.setattr(poke_server, "SessionStream", stub)
@ -127,8 +127,9 @@ class TestMain:
argv = ["--key-file", str(_key_file(tmp_path)), "--port", "1234"]
with caplog.at_level(logging.INFO):
code = poke_server.main([*argv, "--bind", "127.0.0.1"])
(server,) = FakeServer.instances
assert code == 0
assert len(FakeServer.instances) == 1
server = FakeServer.instances[0]
assert not code
assert server.address == ("127.0.0.1", 1234)
assert server.key == bytes.fromhex(_KEY_HEX)
assert server.closed
@ -156,7 +157,7 @@ class TestMain:
code = poke_server.main(
["--key-file", str(_key_file(tmp_path)), "--no-stream"]
)
assert code == 0
assert not code
stream.assert_not_called()
assert "disabled by --no-stream" in caplog.text
assert FakeServer.instances[0].closed
@ -170,7 +171,7 @@ class TestMain:
stream.return_value.start.side_effect = RuntimeError("no network")
with caplog.at_level(logging.ERROR):
code = poke_server.main(["--key-file", str(_key_file(tmp_path))])
assert code == 0
assert not code
assert "Could not start the Firebase session stream" in caplog.text
assert FakeServer.instances[0].closed

View File

@ -33,8 +33,8 @@ def _request(*, sandbox: bool) -> PokeRequest:
)
@pytest.fixture
def pc(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> dict[str, MagicMock]:
@pytest.fixture(name="pc")
def pc_fixture(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> dict[str, MagicMock]:
"""A real PokeLocker on a tmp log with every state-changing step spied."""
def headless(locker_cls: type[PokeLocker]) -> PokeLocker:

View File

@ -22,7 +22,7 @@ if TYPE_CHECKING:
from pathlib import Path
# conftest stubs socket.create_connection process-wide (see test_web_server).
_REAL_CREATE_CONNECTION = socket.create_connection
_real_create_connection = socket.create_connection
_KEY = bytes(range(32))
@ -47,10 +47,10 @@ def _sign(body: bytes) -> str:
return hmac.new(_KEY, body, hashlib.sha256).hexdigest()
@pytest.fixture
def port(monkeypatch: pytest.MonkeyPatch) -> Iterator[int]:
@pytest.fixture(name="port")
def port_fixture(monkeypatch: pytest.MonkeyPatch) -> Iterator[int]:
"""A running PokeServer on 127.0.0.1:0; shut down afterwards."""
monkeypatch.setattr(socket, "create_connection", _REAL_CREATE_CONNECTION)
monkeypatch.setattr(socket, "create_connection", _real_create_connection)
server = PokeServer(("127.0.0.1", 0), _KEY)
# poll_interval: shutdown() waits out one poll, 0.5 s per test by default.
thread = threading.Thread(target=server.serve_forever, args=(0.01,), daemon=True)
@ -63,8 +63,8 @@ def port(monkeypatch: pytest.MonkeyPatch) -> Iterator[int]:
thread.join(timeout=5)
@pytest.fixture
def credit(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> MagicMock:
@pytest.fixture(name="credit")
def credit_fixture(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> MagicMock:
"""Stub the PC side: tmp locker, fixed snapshot, a spy for the credit."""
locker = MagicMock()
locker.log_file = tmp_path / "log.json"

View File

@ -97,10 +97,10 @@ class TestReading:
self, tmp_path: Path, caplog: pytest.LogCaptureFixture
) -> None:
with caplog.at_level(logging.WARNING):
assert rest_day_declarations() == {}
assert not rest_day_declarations()
assert "signing key" in caplog.text
with signing_key(tmp_path, b" "):
assert rest_day_declarations() == {}
assert not rest_day_declarations()
@pytest.mark.usefixtures("key")
def test_a_hand_edited_row_is_ignored(
@ -111,7 +111,7 @@ class TestReading:
edited[0]["day"] = "2026-10-14"
write(edited)
with caplog.at_level(logging.WARNING):
assert rest_day_declarations() == {}
assert not rest_day_declarations()
assert "fails its signature" in caplog.text
@pytest.mark.usefixtures("key")
@ -124,7 +124,7 @@ class TestReading:
"declared_at": 1.0,
}
write([{**body, "hmac": earned_time.entry_signature(body, KEY)}])
assert rest_day_declarations() == {}
assert not rest_day_declarations()
@pytest.mark.usefixtures("key")
def test_malformed_and_foreign_rows_are_ignored(
@ -144,7 +144,7 @@ class TestReading:
]
)
with caplog.at_level(logging.WARNING):
assert rest_day_declarations() == {}
assert not rest_day_declarations()
assert "malformed" in caplog.text
@pytest.mark.usefixtures("key")
@ -153,7 +153,7 @@ class TestReading:
) -> None:
_rest_day.REST_DAY_FILE.write_text("[]")
with caplog.at_level(logging.WARNING):
assert rest_day_declarations() == {}
assert not rest_day_declarations()
assert "no entries array" in caplog.text
@pytest.mark.usefixtures("key")
@ -185,16 +185,16 @@ class TestCli:
return run_declare_rest_day(list(argv))
def test_declare_tomorrow(self, capsys: pytest.CaptureFixture[str]) -> None:
assert self._run("tomorrow") == 0
assert not self._run("tomorrow")
assert "2026-10-10 is a rest day" in capsys.readouterr().out
def test_a_date_and_then_the_list(self, capsys: pytest.CaptureFixture[str]) -> None:
assert self._run("2026-10-11") == 0
assert self._run("--list") == 0
assert not self._run("2026-10-11")
assert not self._run("--list")
assert capsys.readouterr().out.splitlines()[-1] == "2026-10-11"
def test_an_empty_list(self, capsys: pytest.CaptureFixture[str]) -> None:
assert self._run() == 0
assert not self._run()
assert capsys.readouterr().out.strip() == "no rest days declared"
def test_a_bad_date(self, capsys: pytest.CaptureFixture[str]) -> None:

View File

@ -45,7 +45,7 @@ class TestParseSse:
def test_stray_blank_lines_and_comments_yield_nothing(self) -> None:
"""Blank separators and ``:`` comments / unknown fields are ignored."""
assert list(parse_sse(["", "", ": comment", "id: 7", "retry: 5", ""])) == []
assert not list(parse_sse(["", "", ": comment", "id: 7", "retry: 5", ""]))
def test_back_to_back_events_do_not_leak_state(self) -> None:
"""Each event starts with a clean name and data."""
@ -57,7 +57,7 @@ class TestParseSse:
def test_unterminated_event_is_not_emitted(self) -> None:
"""Without the closing blank line the event is incomplete: dropped."""
assert list(parse_sse(["event: put", "data: 1"])) == []
assert not list(parse_sse(["event: put", "data: 1"]))
class TestDeviceUpdatesPut:

View File

@ -86,7 +86,7 @@ class TestScanAndFillWeekRunnerupSkips:
"_find_runnerup_exports_for_date",
MagicMock(return_value=[]),
)
assert locker._scan_and_fill_week_runnerup(log_file) == 0
assert not locker._scan_and_fill_week_runnerup(log_file)
def test_skips_unreadable_tcx(
self,
@ -105,7 +105,7 @@ class TestScanAndFillWeekRunnerupSkips:
MagicMock(return_value=["/sdcard/run.tcx"]),
)
object.__setattr__(locker, "_pull_and_parse_tcx", MagicMock(return_value=None))
assert locker._scan_and_fill_week_runnerup(log_file) == 0
assert not locker._scan_and_fill_week_runnerup(log_file)
def test_skips_not_verified_export(
self,
@ -136,7 +136,7 @@ class TestScanAndFillWeekRunnerupSkips:
return_value={"sport": 0, "duration_seconds": 60, "distance_m": 1000}
),
)
assert locker._scan_and_fill_week_runnerup(log_file) == 0
assert not locker._scan_and_fill_week_runnerup(log_file)
def test_rescanning_an_already_filled_day_is_a_no_op(
self,

View File

@ -122,8 +122,8 @@ class TestBranchCoverageGaps:
result = locker._parse_tcx(path)
# Should still return a dict (0 seconds, 0 m) not None
assert result is not None
assert result["duration_seconds"] == 0
assert result["distance_m"] == 0.0
assert not result["duration_seconds"]
assert not result["distance_m"]
# ---- 161->154: _verify_runnerup_via_files iterates over multiple exports
@ -184,4 +184,4 @@ class TestBranchCoverageGaps:
MagicMock(return_value=[]),
)
# Should not raise; no exports → 0 filled
assert locker._scan_and_fill_week_runnerup(log_file) == 0
assert not locker._scan_and_fill_week_runnerup(log_file)

View File

@ -87,7 +87,7 @@ class TestScanAndFillWeekRunnerupSkips:
MagicMock(return_value=[]),
)
# Should not raise; returns 0 (no exports found).
assert locker._scan_and_fill_week_runnerup(log_file) == 0
assert not locker._scan_and_fill_week_runnerup(log_file)
def test_hmac_none_still_fills_entry(
self,

View File

@ -3,15 +3,12 @@
from __future__ import annotations
import os
from pathlib import Path
import shutil
from typing import TYPE_CHECKING
from unittest.mock import MagicMock
from screen_locker.tests.conftest import create_locker
if TYPE_CHECKING:
from pathlib import Path
# Minimal valid TCX XML for a 40-minute, 6-km run.
_TCX_RUNNING = """\
<?xml version="1.0" encoding="UTF-8"?>
@ -207,7 +204,7 @@ class TestPullRunnerupDb:
def _fake_run_adb(args: list[str]) -> tuple[bool, str]:
if args[0] == "pull" and args[2].endswith(".db"):
# Create the local file so the caller finds it.
open(args[2], "w").close()
Path(args[2]).touch()
return True, ""
object.__setattr__(

View File

@ -2,15 +2,14 @@
from __future__ import annotations
from pathlib import Path
import sqlite3
from typing import TYPE_CHECKING
import time
import tkinter as tk
from unittest.mock import MagicMock, patch
from screen_locker.tests.conftest import create_locker
if TYPE_CHECKING:
from pathlib import Path
# Minimal valid TCX XML for a 40-minute, 6-km run.
_TCX_RUNNING = """\
<?xml version="1.0" encoding="UTF-8"?>
@ -69,7 +68,6 @@ class TestQueryTodaysRun:
def _make_db(self, tmp_path: Path) -> str:
"""Create a minimal RunnerUp DB with one activity for today."""
import time
db_path = str(tmp_path / "runnerup.db")
with sqlite3.connect(db_path) as conn:
@ -94,8 +92,6 @@ class TestQueryTodaysRun:
"(start_time REAL, distance REAL, time REAL, type INTEGER, deleted INTEGER)"
)
# Need a locker instance to call the method.
import tkinter as tk
from unittest.mock import MagicMock
mock_tk = MagicMock()
mock_tk.Tk.return_value = MagicMock()
@ -128,7 +124,7 @@ class TestQueryTodaysRun:
result = locker._query_todays_run(db_path)
assert result is not None
assert "sport" in result
assert result["sport"] == 0
assert not result["sport"]
assert result["distance_m"] == 6000.0
def test_returns_none_on_sqlite_error(
@ -140,8 +136,7 @@ class TestQueryTodaysRun:
"""Corrupt DB → sqlite3.Error caught; returns None."""
locker = create_locker(mock_tk, tmp_path)
corrupt_db = str(tmp_path / "corrupt.db")
with open(corrupt_db, "w") as f:
f.write("this is not a sqlite database")
Path(corrupt_db).write_text("this is not a sqlite database", encoding="utf-8")
result = locker._query_todays_run(corrupt_db)
assert result is None

View File

@ -2,6 +2,7 @@
from __future__ import annotations
from datetime import date, timedelta
import json
from typing import TYPE_CHECKING, Any
from unittest.mock import MagicMock, patch
@ -78,7 +79,7 @@ class TestScanAndFillWeekRunnerup:
log_file = tmp_path / "log.json"
log_file.write_text("{}")
object.__setattr__(locker, "_has_adb_device", MagicMock(return_value=False))
assert locker._scan_and_fill_week_runnerup(log_file) == 0
assert not locker._scan_and_fill_week_runnerup(log_file)
def test_returns_zero_when_all_days_already_logged(
self,
@ -87,7 +88,6 @@ class TestScanAndFillWeekRunnerup:
tmp_path: Path,
) -> None:
"""All days this week already have counted workouts → 0 new fills."""
from datetime import date, timedelta
locker = create_locker(mock_tk, tmp_path)
log_file = tmp_path / "log.json"
@ -108,7 +108,7 @@ class TestScanAndFillWeekRunnerup:
object.__setattr__(
locker, "_find_runnerup_exports_for_date", MagicMock(return_value=[])
)
assert locker._scan_and_fill_week_runnerup(log_file) == 0
assert not locker._scan_and_fill_week_runnerup(log_file)
def test_fills_gap_for_unlogged_day(
self,

View File

@ -78,7 +78,7 @@ class TestParseTcx:
path = _write_tcx(tmp_path, _TCX_RUNNING)
result = locker._parse_tcx(path)
assert result is not None
assert result["sport"] == 0 # Running
assert not result["sport"] # Running
assert result["duration_seconds"] == 2400
assert result["distance_m"] == 6000.0

View File

@ -110,7 +110,7 @@ class TestPullAndParseTcx:
object.__setattr__(locker, "_run_adb", MagicMock(side_effect=_fake_pull))
result = locker._pull_and_parse_tcx("/sdcard/activity.tcx")
assert result is not None
assert result["sport"] == 0
assert not result["sport"]
assert result["duration_seconds"] == 2400

View File

@ -5,16 +5,13 @@ from __future__ import annotations
from datetime import UTC, datetime, timedelta
import json
import logging
from typing import TYPE_CHECKING
from pathlib import Path
from unittest.mock import MagicMock, patch
import pytest
from screen_locker._runnerup_verification import RunnerUpVerificationMixin
if TYPE_CHECKING:
from pathlib import Path
_NS = "http://www.garmin.com/xmlschemas/TrainingCenterDatabase/v2"
_DAY = "2026-10-10"
@ -58,8 +55,8 @@ def drop(folder: Path, name: str, body: str) -> None:
(folder / f"RunnerUp_Pixel_6a_{_DAY}-{name}.tcx").write_text(body)
@pytest.fixture
def verifier(tmp_path: Path) -> Verifier:
@pytest.fixture(name="verifier")
def verifier_fixture(tmp_path: Path) -> Verifier:
return Verifier(tmp_path)
@ -77,7 +74,6 @@ class TestParse:
body = tcx("Other", 10, "08:00")
def pull(args: list[str]) -> tuple[bool, str]:
from pathlib import Path
Path(args[2]).write_text(body)
return True, ""

View File

@ -19,7 +19,7 @@ _SCRIPT = Path(__file__).resolve().parents[2] / "scripts" / "runnerup_watch.sh"
_UNIT = "workout-sync.service"
# conftest replaces subprocess.run process-wide for every test; this module
# needs the real one, captured at import time before any fixture runs.
_REAL_RUN = subprocess.run
_real_run = subprocess.run
def _shim(path: Path, body: str) -> None:
@ -51,7 +51,7 @@ def _run(
"RUNNERUP_WATCH_DIR": str(watch),
"RUNNERUP_SYNC_UNIT": _UNIT,
}
done = _REAL_RUN(
done = _real_run(
["bash", str(_SCRIPT)],
env=env,
capture_output=True,

View File

@ -121,7 +121,7 @@ class TestLocalExportDiscovery:
):
assert locker._has_runnerup_source() is False
status, _ = locker._verify_runnerup_workout()
assert locker._scan_and_fill_week_runnerup(tmp_path / "log.json") == 0
assert not locker._scan_and_fill_week_runnerup(tmp_path / "log.json")
assert status == "no_phone"

View File

@ -131,7 +131,7 @@ class TestNoDisplayStandsDown:
pytest.raises(SystemExit) as raised,
):
create_locker(MagicMock(), tmp_path, demo_mode=False)
assert raised.value.code == 0
assert not raised.value.code
return aborted, exit_mock
def test_it_exits_zero_instead_of_failing(self, tmp_path: Path) -> None:

View File

@ -72,9 +72,9 @@ class TestIngestSyncedManualWorkouts:
) as ingest,
):
locker._ingest_synced_manual_workouts()
assert ingest.call_args.kwargs["on_ingested"] == (
assert [ingest.call_args.kwargs["on_ingested"]] == [
locker._credit_ingested_workout
)
]
assert locker.workout_data == {}
def test_credit_ingested_workout_applies_reward(
@ -191,5 +191,7 @@ class TestIngestSyncedSessions:
) as ingest,
):
locker._ingest_synced_sessions()
assert ingest.call_args.kwargs["on_ingested"] == locker._credit_ingested_workout
assert [ingest.call_args.kwargs["on_ingested"]] == [
locker._credit_ingested_workout
]
assert locker.workout_data == {}

View File

@ -75,8 +75,8 @@ def make_stream(monkeypatch: pytest.MonkeyPatch) -> SessionStream:
return SessionStream(threading.Lock())
@pytest.fixture
def stream(monkeypatch: pytest.MonkeyPatch) -> SessionStream:
@pytest.fixture(name="stream")
def stream_fixture(monkeypatch: pytest.MonkeyPatch) -> SessionStream:
"""A ``SessionStream`` that never reads or writes the real device id."""
return make_stream(monkeypatch)
@ -165,7 +165,7 @@ class TestConsume:
with caplog.at_level(logging.INFO):
stream._consume(response, _PAST)
assert any("renewing its ID token" in r.getMessage() for r in caplog.records)
assert response.raw.chunks == []
assert not response.raw.chunks
keepalives = [r for r in caplog.records if "keep-alive #" in r.getMessage()]
assert len(keepalives) == 1 # stopped after the first event

View File

@ -19,8 +19,8 @@ _RENEW = datetime(2999, 1, 1, tzinfo=UTC)
_LONG = 400.0 # longer than the 300 s minimum lifetime
@pytest.fixture
def stream(monkeypatch: pytest.MonkeyPatch) -> SessionStream:
@pytest.fixture(name="stream")
def stream_fixture(monkeypatch: pytest.MonkeyPatch) -> SessionStream:
"""A ``SessionStream`` that never reads or writes the real device id."""
return make_stream(monkeypatch)

View File

@ -17,8 +17,8 @@ if TYPE_CHECKING:
_LONG = 400.0 # longer than the 300 s minimum lifetime
@pytest.fixture
def stream(monkeypatch: pytest.MonkeyPatch) -> SessionStream:
@pytest.fixture(name="stream")
def stream_fixture(monkeypatch: pytest.MonkeyPatch) -> SessionStream:
"""A ``SessionStream`` that never reads or writes the real device id."""
return make_stream(monkeypatch)
@ -117,7 +117,7 @@ class TestRunForever:
) -> None:
"""Token renewal after a healthy hour is not a failure: no pause."""
script: list[tuple[bool, float, bool]] = [(True, 3600.0, True)] * 2
assert self._run(_Loop(stream, monkeypatch, script)) == []
assert not self._run(_Loop(stream, monkeypatch, script))
def test_planned_end_after_a_short_life_still_backs_off(
self, stream: SessionStream, monkeypatch: pytest.MonkeyPatch
@ -146,7 +146,7 @@ class TestRunForever:
"""A set stop flag exits without connecting."""
loop = _Loop(stream, monkeypatch, [])
stream.run_forever()
assert loop.index == 0
assert not loop.index
class TestThread:

View File

@ -116,25 +116,24 @@ class TestIngestSessionRecords:
log = tmp_path / "log.json"
records = [("r1", _session())]
assert ingest_session_records(log, records) == ["r1"]
assert ingest_session_records(log, records) == []
assert not ingest_session_records(log, records)
assert count_weekly_workouts(log, today=_TODAY) == 1
def test_skips_a_session_with_no_date(self, tmp_path: Path) -> None:
payload = _session()
del payload["date"]
assert ingest_session_records(tmp_path / "log.json", [("r1", payload)]) == []
assert not ingest_session_records(tmp_path / "log.json", [("r1", payload)])
def test_skips_a_session_whose_date_is_not_a_string(self, tmp_path: Path) -> None:
assert (
assert not (
ingest_session_records(tmp_path / "log.json", [("r1", _session(date=7))])
== []
)
def test_skips_a_session_that_fails_validation(self, tmp_path: Path) -> None:
log = tmp_path / "log.json"
short = _session(duration_seconds=60)
assert ingest_session_records(log, [("r1", short)]) == []
assert count_weekly_workouts(log, today=_TODAY) == 0
assert not ingest_session_records(log, [("r1", short)])
assert not count_weekly_workouts(log, today=_TODAY)
def test_calls_back_for_each_newly_ingested_session(self, tmp_path: Path) -> None:
seen: list[tuple[dict, list]] = []
@ -155,7 +154,7 @@ class TestIngestSessionRecords:
ingest_session_records(
log, records, on_ingested=lambda entry, _prior: seen.append(entry)
)
assert seen == []
assert not seen
class TestPullAllSessionRecords:

View File

@ -129,7 +129,7 @@ class TestResetToBaseIfNewDay:
assert reset_to_base_if_new_day(f, mixin) is True
def test_handles_oserror_on_state_write(self, tmp_path: Path) -> None:
"""OSError writing the new state file is caught; function still returns True (lines 96-97)."""
"""An OSError writing the new state file is caught; the reset still returns True."""
# Use a mock path that fails only on "w" opens.
state_mock = MagicMock()
state_mock.exists.return_value = False # triggers fresh-reset path

View File

@ -64,10 +64,10 @@ class TestEarnedShutdownBonusMinutes:
"""First credit is worth 2h, every further one 1h."""
def test_no_credits_earn_nothing(self) -> None:
assert earned_shutdown_bonus_minutes(0) == 0
assert not earned_shutdown_bonus_minutes(0)
def test_negative_is_treated_as_nothing(self) -> None:
assert earned_shutdown_bonus_minutes(-1) == 0
assert not earned_shutdown_bonus_minutes(-1)
def test_first_credit_is_two_hours(self) -> None:
assert earned_shutdown_bonus_minutes(1) == 120
@ -138,7 +138,7 @@ class TestTodayCreditCount:
def test_day_absent_from_log_counts_nothing(self, tmp_path: Path) -> None:
log = _write_log(tmp_path / "log.json", "2026-09-12", _real_2026_09_13())
assert today_credit_count(log, _DAY) == 0
assert not today_credit_count(log, _DAY)
def test_non_dict_entries_are_ignored(self, tmp_path: Path) -> None:
log = tmp_path / "log.json"

View File

@ -7,7 +7,8 @@ four blocks included) must shut down exactly at ``shutdown_ceiling_for(day)``
-- never short of it, and never past it. Checked through screen-locker's own
paths, not a re-implementation: the daily reset's :func:`derive`, and the live
pass (first workout, further workouts, flat gates, counted gate blocks, each
timed at ``first`` so a 22:30 one is a late credit) over :class:`ShutdownMixin` with an in-memory config.
timed at ``first`` so a 22:30 one is a late credit) over :class:`ShutdownMixin`
with an in-memory config.
Every gate maturity state is covered: ``first_credits`` omitted (0.5.0
``penalty_from``), empty (every earner on its ``confirmed_on``), never paid
@ -182,8 +183,8 @@ def _live_pass(day: date, case: tuple[str, bool, int, str]) -> int:
return host.config[1]
@pytest.fixture
def frozen_day() -> Iterator[Callable[[date], Any]]:
@pytest.fixture(name="frozen_day")
def frozen_day_fixture() -> Iterator[Callable[[date], Any]]:
"""Point every live path's ``today`` at the simulated day."""
def at_day(day: date) -> Any:
@ -215,4 +216,4 @@ def test_the_live_pass_lands_every_all_done_day_on_the_ceiling(
_grace_floor.GRACE_STATE_FILE.unlink(missing_ok=True)
if got != ceiling(day):
misses.append((day.isoformat(), got, ceiling(day)))
assert misses == []
assert not misses

View File

@ -49,8 +49,8 @@ def _mixin() -> MagicMock:
return mixin
@pytest.fixture
def answers() -> Iterator[dict[str, bool | None]]:
@pytest.fixture(name="answers")
def answers_fixture() -> Iterator[dict[str, bool | None]]:
"""Every flat earner says no until a test sets it."""
given: dict[str, bool | None] = {"leetcode": False, "reading": False}
with answering(given):

View File

@ -40,15 +40,15 @@ def apply_leetcode_bonus_if_new(state: Path, mixin: MagicMock) -> bool:
return _apply_flat_bonus(state, mixin, earned_time.LEETCODE)
@pytest.fixture
def solved() -> Iterator[MagicMock]:
@pytest.fixture(name="solved")
def solved_fixture() -> Iterator[MagicMock]:
"""LeetCode solved today; reading not, so the stamps here are LeetCode's."""
with answering({"leetcode": True, "reading": False}) as m:
yield m
@pytest.fixture
def unsolved() -> Iterator[MagicMock]:
@pytest.fixture(name="unsolved")
def unsolved_fixture() -> Iterator[MagicMock]:
with answering({"leetcode": False, "reading": False}) as m:
yield m

View File

@ -47,8 +47,8 @@ def apply_reading_bonus_if_new(state: Path, mixin: MagicMock) -> bool:
return _apply_flat_bonus(state, mixin, earned_time.READING)
@pytest.fixture
def answers() -> Iterator[tuple[dict[str, bool | None], MagicMock]]:
@pytest.fixture(name="answers")
def answers_fixture() -> Iterator[tuple[dict[str, bool | None], MagicMock]]:
"""(answers, earned_today mock): every flat earner says no until set."""
given: dict[str, bool | None] = {"leetcode": False, "reading": False}
with answering(given) as mock:

View File

@ -46,7 +46,7 @@ class TestTarget:
def test_a_rest_day_stands_in_for_the_first_workout_only(self) -> None:
assert workout_units(0, rest_day=True) == 1
assert workout_units(2, rest_day=True) == 2
assert workout_units(0, rest_day=False) == 0
assert not workout_units(0, rest_day=False)
def test_an_early_first_task_no_longer_lifts_the_sum(self) -> None:
"""The grace floor is gone (2026-10-17): late credits replace it."""
@ -54,7 +54,7 @@ class TestTarget:
DayInputs(_DAY, {**_FLAT, "anki": True}, first_done=at("18:55"))
)
assert target.minutes == target.earned
assert target.lift == 0
assert not target.lift
def test_a_sick_day_has_the_20_00_floor(self) -> None:
target = derive(DayInputs(_DAY, _FLAT, first_done=at("18:55"), sick_day=True))
@ -63,7 +63,7 @@ class TestTarget:
t.gaming_minutes for t in target.resolution.terms
)
# earned_time 0.13.0: credits stack on 20:00, so there is no lift.
assert target.lift == 0
assert not target.lift
def test_a_sick_day_stacks_gaming_pay_on_20_00(self) -> None:
busy = dict.fromkeys(_FLAT, True)
@ -72,7 +72,7 @@ class TestTarget:
assert target.minutes == min(
23 * 60, SICK_FLOOR + sum(t.gaming_minutes for t in target.resolution.terms)
)
assert target.lift == 0
assert not target.lift
def test_gather_keeps_the_sick_floor_when_the_workout_lock_is_kept(self) -> None:
"""Rule 2: the 20:00 floor is the day's, whatever the keep set says."""
@ -93,7 +93,7 @@ class TestTarget:
assert target.minutes == target.earned == target.resolution.shutdown_minutes
def test_no_log_counts_no_workouts(self) -> None:
assert day_credit_count(None, _DAY) == 0
assert not day_credit_count(None, _DAY)
def test_gather_reads_every_real_source(self, tmp_path: Path) -> None:
log = log_with(

View File

@ -195,7 +195,7 @@ class TestTap:
back = next(
c for c in mock_tk.Button.call_args_list if c.kwargs.get("text") == "Back"
)
assert back.kwargs["command"] == locker._start_phone_check
assert [back.kwargs["command"]] == [locker._start_phone_check]
def test_refusal_on_the_relaxed_prompt_goes_back_to_it(
self, mock_tk: MagicMock, mock_sys_exit: MagicMock, tmp_path: Path
@ -205,4 +205,4 @@ class TestTap:
back = next(
c for c in mock_tk.Button.call_args_list if c.kwargs.get("text") == "Back"
)
assert back.kwargs["command"] == locker._start_relaxed_day_flow
assert [back.kwargs["command"]] == [locker._start_relaxed_day_flow]

View File

@ -63,16 +63,16 @@ def _frame(*args: object, **_kwargs: object) -> MagicMock:
return frame
@pytest.fixture
def panels() -> Iterator[list[FakePanel]]:
@pytest.fixture(name="panels")
def panels_fixture() -> Iterator[list[FakePanel]]:
"""Every KeepPanel the lock builds, in order."""
_MADE.clear()
with patch.object(_ui_flows_sick_panel, "KeepPanel", FakePanel):
yield _MADE
@pytest.fixture
def locker(
@pytest.fixture(name="locker")
def locker_fixture(
mock_tk: MagicMock, mock_sys_exit: MagicMock, tmp_path: Path
) -> ScreenLocker:
"""A retry-screen lock whose floor and close are observable."""

View File

@ -77,7 +77,7 @@ class TestExplainFindings:
def test_no_findings_render_nothing(self) -> None:
"""A healthy machine must not show a diagnostics block."""
assert explain_findings([]) == ""
assert not explain_findings([])
def test_findings_name_the_backend_and_the_consequence(self) -> None:
"""The panel must say it is a sync fault, not a missed workout."""

View File

@ -2,13 +2,14 @@
from __future__ import annotations
from datetime import UTC
from datetime import UTC, datetime
import json
from pathlib import Path
from typing import TYPE_CHECKING
from unittest.mock import MagicMock, patch
from screen_locker._status import _load_extra_benefits, run_status
from screen_locker.tests._sick_helpers import mark_sick
from screen_locker.tests.conftest import _make_locker
if TYPE_CHECKING:
@ -83,7 +84,6 @@ class TestRunStatusNormal:
self, tmp_path: Path, capsys: pytest.CaptureFixture
) -> None:
"""Today == the week's Sunday: the per-day loop runs all 7 days, never breaking early."""
from datetime import datetime
fixed_now = datetime(2026, 7, 5, 12, 0, tzinfo=UTC) # a real Sunday
eb_file = tmp_path / "eb.json"
@ -107,7 +107,6 @@ class TestRunStatusNormal:
self, tmp_path: Path, capsys: pytest.CaptureFixture
) -> None:
"""A date with no workout_log entry but in the sickdays pool → sick_day."""
from screen_locker.tests._sick_helpers import mark_sick
mark_sick()
eb_file = tmp_path / "eb.json"
@ -186,7 +185,6 @@ class TestRunStatusWorkoutLog:
self, tmp_path: Path, capsys: pytest.CaptureFixture
) -> None:
"""Log entry with counted type → ✓ mark printed."""
from datetime import datetime
today = datetime.now(tz=UTC).astimezone().date().isoformat()
log_file = tmp_path / "log.json"
@ -221,7 +219,6 @@ class TestRunStatusWorkoutLog:
self, tmp_path: Path, capsys: pytest.CaptureFixture
) -> None:
"""Log entry with uncounted type → ✗ mark printed."""
from datetime import datetime
today = datetime.now(tz=UTC).astimezone().date().isoformat()
log_file = tmp_path / "log.json"

View File

@ -32,17 +32,17 @@ class TestGatherStatus:
snap = gather_status(**_files(tmp_path), now=_FRIDAY_NOON_UTC)
assert snap.today.date == "2024-01-05"
assert snap.today.entry_types == ()
assert snap.today.day_count == 0
assert not snap.today.entry_types
assert not snap.today.day_count
assert snap.today.counted is False
assert snap.today.is_sick_day is False
assert snap.week.counted_count == 0
assert not snap.week.counted_count
assert snap.week.remaining == 5
assert snap.week.extra == 0
assert not snap.week.extra
assert snap.shutdown.tonight is None
assert snap.shutdown.rest_of_week[0].minutes == 20 * 60
assert snap.shutdown.rest_of_week[0].speculative is False
assert snap.streak == 0
assert not snap.streak
assert snap.early_bird_extended is False
assert snap.lock_explanation.fired is True
assert snap.lock_explanation.stage == "would_lock"
@ -121,7 +121,7 @@ class TestGatherStatus:
assert snap.streak == 2
assert snap.early_bird_extended is True
assert snap.week_rewards == ()
assert not snap.week_rewards
def test_this_weeks_transition_message_is_surfaced(self, tmp_path: Path) -> None:
"""The reward text a week transition produced reaches the snapshot."""
@ -166,8 +166,8 @@ class TestGatherStatus:
):
snap = gather_status(**files, now=_FRIDAY_NOON_UTC)
assert snap.week.counted_count == 0
assert snap.today.entry_types == ()
assert not snap.week.counted_count
assert not snap.today.entry_types
def test_now_defaults_to_current_time_when_omitted(self, tmp_path: Path) -> None:
"""Covers the ``now is None`` branch — just needs to not raise."""
@ -187,9 +187,9 @@ class TestDayStatus:
def test_no_entries_not_sick(self) -> None:
"""No entries not sick."""
day = _day_status(date(2024, 1, 5), [], frozenset())
assert day.entry_types == ()
assert not day.entry_types
assert day.counted is False
assert day.day_count == 0
assert not day.day_count
assert day.is_sick_day is False
def test_no_entries_but_sick(self) -> None:
@ -200,10 +200,10 @@ class TestDayStatus:
def test_non_dict_entries_dropped(self) -> None:
"""Corrupt log data (non-dict entries in the list) are skipped."""
day = _day_status(date(2024, 1, 5), ["corrupt-string-entry"], frozenset())
assert day.entry_types == ()
assert not day.entry_types
assert day.counted is False
assert day.day_count == 0
assert day.source == ""
assert not day.day_count
assert not day.source
def test_multiple_entries_each_count_individually(self) -> None:
"""Two verified + two manual on one day → day_count 4, all types listed."""

View File

@ -129,8 +129,8 @@ class TestGatherStatusLockSuppression:
snap = gather_status(**files, now=_FRIDAY_NOON_UTC)
assert snap.week.counted_count == 5
assert snap.week.remaining == 0
assert snap.week.extra == 0
assert not snap.week.remaining
assert not snap.week.extra
assert snap.lock_explanation.stage == "weekly_minimum_met"
def test_extra_workouts_beyond_minimum(self, tmp_path: Path) -> None:
@ -162,4 +162,4 @@ class TestGatherStatusLockSuppression:
snap = gather_status(**files, now=_FRIDAY_NOON_UTC)
assert snap.week.extra == 1
assert snap.week.remaining == 0
assert not snap.week.remaining

View File

@ -5,7 +5,7 @@ Split from test_status.py to stay under the repo's 400-line file limit.
from __future__ import annotations
from datetime import UTC
from datetime import UTC, datetime
from typing import TYPE_CHECKING
from unittest.mock import MagicMock, patch
@ -133,7 +133,6 @@ class TestRunStatusMinimumStatus:
self, tmp_path: Path, capsys: pytest.CaptureFixture
) -> None:
"""Pin today to Monday so the loop hits d > today on day 2, covering line 64."""
from datetime import datetime
fake_now = datetime(2026, 6, 22, 12, 0, tzinfo=UTC)

View File

@ -5,7 +5,7 @@ Split out of test_status.py for the 250-line cap.
from __future__ import annotations
from datetime import UTC
from datetime import UTC, datetime
import json
from typing import TYPE_CHECKING
from unittest.mock import patch
@ -33,7 +33,6 @@ class TestWeeklyTotalUsesPerWorkoutRule:
days are supposed to support. Also covers manual workouts counting
individually (no once-per-day collapse), same as verified ones.
"""
from datetime import datetime
today = datetime.now(tz=UTC).astimezone().date().isoformat()
log_file = tmp_path / "log.json"

View File

@ -1,5 +1,4 @@
"""The status window shows the workout streak and early-bird state it tracks."""
# pylint: disable=protected-access
from __future__ import annotations

View File

@ -46,8 +46,8 @@ class _Fakes:
self.recover = MagicMock(side_effect=lambda: self.recovery)
@pytest.fixture
def fakes(monkeypatch: pytest.MonkeyPatch) -> _Fakes:
@pytest.fixture(name="fakes")
def fakes_fixture(monkeypatch: pytest.MonkeyPatch) -> _Fakes:
"""Patch config loading, the credential store, providers and recovery."""
fake = _Fakes()
monkeypatch.setattr(

View File

@ -70,16 +70,16 @@ class Credit:
return BatchOutcome(ingested=tuple(ids), credited=len(ids))
@pytest.fixture
def credit(monkeypatch: pytest.MonkeyPatch) -> Credit:
@pytest.fixture(name="credit")
def credit_fixture(monkeypatch: pytest.MonkeyPatch) -> Credit:
"""Replace the real (locker-building) credit step with a recorder."""
fake = Credit()
monkeypatch.setattr(_stream_cache._poke_credit, "credit_sessions", fake)
return fake
@pytest.fixture
def cache(credit: Credit) -> DeviceLogCache:
@pytest.fixture(name="cache")
def cache_fixture(credit: Credit) -> DeviceLogCache:
"""A cache whose own device dir is ``pc-own``."""
gate = threading.Lock()
credit.gate = gate

View File

@ -20,16 +20,16 @@ from screen_locker.tests.test_stream_cache import (
)
@pytest.fixture
def credit(monkeypatch: pytest.MonkeyPatch) -> Credit:
@pytest.fixture(name="credit")
def credit_fixture(monkeypatch: pytest.MonkeyPatch) -> Credit:
"""Replace the real (locker-building) credit step with a recorder."""
fake = Credit()
monkeypatch.setattr(_stream_cache._poke_credit, "credit_sessions", fake)
return fake
@pytest.fixture
def cache(credit: Credit) -> DeviceLogCache:
@pytest.fixture(name="cache")
def cache_fixture(credit: Credit) -> DeviceLogCache:
"""A cache whose own device dir is ``pc-own``."""
gate = threading.Lock()
credit.gate = gate

View File

@ -120,7 +120,7 @@ class TestFirebaseDegradationIsVisible:
_workout_sync.sync_client()
assert _sync_client.degraded_sources() == []
assert not _sync_client.degraded_sources()
def test_successful_recovery_returns_a_working_client(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
@ -152,7 +152,7 @@ class TestFirebaseDegradationIsVisible:
recovered = _workout_sync.sync_client()
assert isinstance(recovered, ReachableClient)
assert _sync_client.degraded_sources() == [], (
assert not _sync_client.degraded_sources(), (
"a source that recovered is not degraded"
)
@ -226,4 +226,4 @@ class TestFirebaseDegradationIsVisible:
_workout_sync.sync_client()
assert _sync_client.degraded_sources() == []
assert not _sync_client.degraded_sources()

View File

@ -64,7 +64,7 @@ class TestRejectedCredentialTriggersRecovery:
"a rejected credential must trigger the same self-heal as a "
"failed construction"
)
assert _sync_client.degraded_sources() == []
assert not _sync_client.degraded_sources()
def test_a_still_rejected_credential_is_degraded_loudly(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch

View File

@ -64,7 +64,7 @@ class TestPushState:
status = _gather(tmp_path)
assert status.pushed is False
assert status.peer_count == 0
assert not status.peer_count
assert status.last_push is None
def test_a_pushed_rev_counts_as_pushed(self, tmp_path: Path) -> None:
@ -86,7 +86,7 @@ class TestPushState:
status = _gather(tmp_path)
assert status.pushed is False
assert status.peer_count == 0
assert not status.peer_count
def test_a_non_object_state_file_reads_as_nothing_known(
self, tmp_path: Path
@ -101,7 +101,7 @@ class TestPushState:
json.dumps({"pushed_rev": "abc", "peer_revs": "not-a-map"})
)
assert _gather(tmp_path).peer_count == 0
assert not _gather(tmp_path).peer_count
def test_an_unstattable_state_file_has_no_timestamp(self, tmp_path: Path) -> None:
"""A path whose parent is a file cannot be stat'd; that is not fatal."""

View File

@ -55,7 +55,7 @@ class TestTombstoneRecords:
"""Ledger → crdt_sync Records, loud about anything it cannot push."""
def test_missing_ledger_is_empty(self, tmp_path: Path) -> None:
assert tombstone_records(tmp_path / "absent.json") == {}
assert not tombstone_records(tmp_path / "absent.json")
def test_ledgered_id_becomes_a_deleted_record(self, tmp_path: Path) -> None:
path = tmp_path / "sync_tombstones.json"
@ -79,7 +79,7 @@ class TestTombstoneRecords:
) -> None:
path = tmp_path / "sync_tombstones.json"
path.write_text("{not json")
assert tombstone_records(path) == {}
assert not tombstone_records(path)
assert "unreadable" in caplog.text
def test_non_object_ledger_is_ignored_and_warns(
@ -87,20 +87,20 @@ class TestTombstoneRecords:
) -> None:
path = tmp_path / "sync_tombstones.json"
path.write_text("[]")
assert tombstone_records(path) == {}
assert not tombstone_records(path)
assert "not a JSON object" in caplog.text
def test_non_dict_entries_are_dropped(self, tmp_path: Path) -> None:
path = tmp_path / "sync_tombstones.json"
path.write_text(json.dumps({_RID: "garbage"}))
assert tombstone_records(path) == {}
assert not tombstone_records(path)
def test_entry_without_usable_hlc_is_skipped_and_warns(
self, tmp_path: Path, caplog
) -> None:
path = tmp_path / "sync_tombstones.json"
path.write_text(json.dumps({_RID: {"reason": "x"}}))
assert tombstone_records(path) == {}
assert not tombstone_records(path)
assert "no usable deleted_hlc" in caplog.text

View File

@ -30,8 +30,8 @@ if TYPE_CHECKING:
_CUTOVER = date(2026, 10, 10)
@pytest.fixture
def tutor(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> Iterator[Any]:
@pytest.fixture(name="tutor")
def tutor_fixture(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> Iterator[Any]:
"""The tutor registry from ``_CUTOVER``, its ledger signed, today pinned.
Args:

View File

@ -66,13 +66,13 @@ class TestTrackSummary:
assert summary["ended_at"] == (_START + timedelta(seconds=600)).timestamp()
def test_standing_and_vehicles_are_not_moving(self) -> None:
assert track_summary(walk(600, speed=0.1))["moving_seconds"] == 0
assert track_summary(walk(600, speed=8.0))["moving_seconds"] == 0
assert not track_summary(walk(600, speed=0.1))["moving_seconds"]
assert not track_summary(walk(600, speed=8.0))["moving_seconds"]
def test_a_gps_dropout_earns_nothing(self) -> None:
"""Two points 100 s apart at walking pace: motion unseen, not counted."""
summary = track_summary(activity([(0, "0.1", None), (100, "130", None)]))
assert summary["moving_seconds"] == 0
assert not summary["moving_seconds"]
def test_unusable_points_are_skipped(
self, caplog: pytest.LogCaptureFixture

View File

@ -61,7 +61,7 @@ class TestWorkoutCreditsToday:
def test_no_entries_is_zero(self, tmp_path: Path) -> None:
"""An untouched day earns nothing."""
with _with_log({}, tmp_path):
assert workout_credits_today(now=_SATURDAY) == 0
assert not workout_credits_today(now=_SATURDAY)
def test_a_workout_counts(self, tmp_path: Path) -> None:
"""A verified workout today earns a credit."""
@ -79,19 +79,19 @@ class TestWorkoutCreditsToday:
def test_a_relaxed_day_skip_earns_nothing(self, tmp_path: Path) -> None:
"""Dismissing the popup is not a workout — so it is a 6h gaming day."""
with _with_log({"2026-08-29": [_entry("relaxed_day_skip")]}, tmp_path):
assert workout_credits_today(now=_SATURDAY) == 0
assert not workout_credits_today(now=_SATURDAY)
def test_yesterdays_workout_does_not_count_today(self, tmp_path: Path) -> None:
"""The budget is per gaming day, so only today's entries count."""
with _with_log({"2026-08-28": [_entry("phone_verified")]}, tmp_path):
assert workout_credits_today(now=_SATURDAY) == 0
assert not workout_credits_today(now=_SATURDAY)
def test_non_dict_entries_are_ignored(self, tmp_path: Path) -> None:
"""A malformed entry must not crash the endpoint."""
log = tmp_path / "log.json"
log.write_text(json.dumps({"2026-08-29": ["junk"]}), encoding="utf-8")
with patch(f"{_PKG}.LOG_FILE", log):
assert workout_credits_today(now=_SATURDAY) == 0
assert not workout_credits_today(now=_SATURDAY)
class TestStatusPayload:

View File

@ -32,17 +32,17 @@ _PKG = "screen_locker._web_server"
# a server we started ourselves. Captured at import time, before any fixture
# runs, which is the only moment the real function is still reachable. The
# fixture's own docstring says tests needing a real probe patch it locally.
_REAL_CREATE_CONNECTION = socket.create_connection
_real_create_connection = socket.create_connection
@pytest.fixture
def real_loopback(monkeypatch: pytest.MonkeyPatch) -> None:
@pytest.fixture(name="real_loopback")
def real_loopback_fixture(monkeypatch: pytest.MonkeyPatch) -> None:
"""Restore real socket connections for tests that drive the server.
Args:
monkeypatch: pytest's attribute patcher, which undoes this on teardown.
"""
monkeypatch.setattr(socket, "create_connection", _REAL_CREATE_CONNECTION)
monkeypatch.setattr(socket, "create_connection", _real_create_connection)
@contextmanager
@ -206,5 +206,5 @@ class TestServeEntryPoints:
def test_main_returns_zero(self) -> None:
"""The console-script entry point reports success on a clean exit."""
with patch(f"{_PKG}.serve") as served:
assert main() == 0
assert not main()
served.assert_called_once()

Some files were not shown because too many files have changed in this diff Show More