mirror of
https://github.com/kuhyx/screen-locker.git
synced 2026-11-10 17:47:23 +01:00
Fix lint: hold status mirror, split tests
Some checks failed
dependency freshness / dependencies on newest stable (push) Has been cancelled
md-naming / markdown naming convention (push) Has been cancelled
pre-commit / pre-commit (${{ matrix.group }}) (flutter, flutter-analyze, ) (push) Has been cancelled
pre-commit / pre-commit (${{ matrix.group }}) (mypy, mypy, ) (push) Has been cancelled
pre-commit / pre-commit (${{ matrix.group }}) (pylint, pylint, ) (push) Has been cancelled
pre-commit / pre-commit (${{ matrix.group }}) (rest, , mypy,pylint,flutter-analyze) (push) Has been cancelled
Tests / test (push) Has been cancelled
Tests / lock-popup-safety (push) Has been cancelled
Tests / flutter test (shard ${{ matrix.shard }}) (0) (push) Has been cancelled
Tests / flutter test (shard ${{ matrix.shard }}) (1) (push) Has been cancelled
Tests / flutter test (shard ${{ matrix.shard }}) (2) (push) Has been cancelled
Tests / flutter test (shard ${{ matrix.shard }}) (3) (push) Has been cancelled
Tests / flutter test (shard ${{ matrix.shard }}) (4) (push) Has been cancelled
Tests / flutter test (shard ${{ matrix.shard }}) (5) (push) Has been cancelled
Tests / flutter test (shard ${{ matrix.shard }}) (6) (push) Has been cancelled
Tests / flutter test (shard ${{ matrix.shard }}) (7) (push) Has been cancelled
Tests / flutter test (shard ${{ matrix.shard }}) (8) (push) Has been cancelled
Tests / flutter test (shard ${{ matrix.shard }}) (9) (push) Has been cancelled
Tests / web-coverage (push) Has been cancelled
repo-contract / repo contract (push) Has been cancelled
Tests / flutter-coverage (push) Has been cancelled
Some checks failed
dependency freshness / dependencies on newest stable (push) Has been cancelled
md-naming / markdown naming convention (push) Has been cancelled
pre-commit / pre-commit (${{ matrix.group }}) (flutter, flutter-analyze, ) (push) Has been cancelled
pre-commit / pre-commit (${{ matrix.group }}) (mypy, mypy, ) (push) Has been cancelled
pre-commit / pre-commit (${{ matrix.group }}) (pylint, pylint, ) (push) Has been cancelled
pre-commit / pre-commit (${{ matrix.group }}) (rest, , mypy,pylint,flutter-analyze) (push) Has been cancelled
Tests / test (push) Has been cancelled
Tests / lock-popup-safety (push) Has been cancelled
Tests / flutter test (shard ${{ matrix.shard }}) (0) (push) Has been cancelled
Tests / flutter test (shard ${{ matrix.shard }}) (1) (push) Has been cancelled
Tests / flutter test (shard ${{ matrix.shard }}) (2) (push) Has been cancelled
Tests / flutter test (shard ${{ matrix.shard }}) (3) (push) Has been cancelled
Tests / flutter test (shard ${{ matrix.shard }}) (4) (push) Has been cancelled
Tests / flutter test (shard ${{ matrix.shard }}) (5) (push) Has been cancelled
Tests / flutter test (shard ${{ matrix.shard }}) (6) (push) Has been cancelled
Tests / flutter test (shard ${{ matrix.shard }}) (7) (push) Has been cancelled
Tests / flutter test (shard ${{ matrix.shard }}) (8) (push) Has been cancelled
Tests / flutter test (shard ${{ matrix.shard }}) (9) (push) Has been cancelled
Tests / web-coverage (push) Has been cancelled
repo-contract / repo contract (push) Has been cancelled
Tests / flutter-coverage (push) Has been cancelled
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WzxdWVSoczW2NHQd887FUm
This commit is contained in:
parent
6919ddea28
commit
0987975bca
@ -38,6 +38,7 @@ from screen_locker._compliance_trace import (
|
||||
describe_degraded_sources,
|
||||
)
|
||||
from screen_locker._day import today_str as local_day
|
||||
from screen_locker._hold import workout_hold_active
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Sequence
|
||||
@ -153,7 +154,6 @@ def _chain_flags(
|
||||
relaxed_day: bool,
|
||||
wake_skip: bool,
|
||||
evening_lockdown: bool,
|
||||
hold: bool,
|
||||
local_dt: datetime,
|
||||
pending: bool,
|
||||
) -> dict[str, bool]:
|
||||
@ -167,7 +167,7 @@ def _chain_flags(
|
||||
"sick_day": is_sick_day_today(local_dt.date()),
|
||||
"already_logged": has_logged_today(log_file, today=today_str),
|
||||
"wake_alarm_skip": wake_skip,
|
||||
"test_hold_active": hold,
|
||||
"test_hold_active": workout_hold_active(local_dt),
|
||||
"relaxed_day_already_skipped": relaxed_day
|
||||
and is_relaxed_day_skipped_today(log_file, today=today_str),
|
||||
"relaxed_day": relaxed_day,
|
||||
@ -204,7 +204,6 @@ def explain_lock_decision(
|
||||
relaxed_day: bool,
|
||||
wake_skip: bool = False,
|
||||
evening_lockdown: bool = False,
|
||||
hold: bool = False,
|
||||
now: datetime | None = None,
|
||||
degraded_sources: Sequence[DegradedSource] = (),
|
||||
) -> LockExplanation:
|
||||
@ -222,7 +221,6 @@ def explain_lock_decision(
|
||||
relaxed_day=relaxed_day,
|
||||
wake_skip=wake_skip,
|
||||
evening_lockdown=evening_lockdown,
|
||||
hold=hold,
|
||||
local_dt=instant.astimezone(),
|
||||
pending=pending,
|
||||
)
|
||||
|
||||
@ -50,7 +50,6 @@ from screen_locker._extra_benefits import (
|
||||
has_extended_early_bird,
|
||||
week_rewards,
|
||||
)
|
||||
from screen_locker._hold import workout_hold_active
|
||||
from screen_locker._log_io import load_workout_log
|
||||
from screen_locker._morning_session import has_workout_skip_today
|
||||
from screen_locker._night_lockdown import tonights_lockdown
|
||||
@ -171,7 +170,6 @@ def gather_status(
|
||||
relaxed_day=is_relaxed_day(today=today_local),
|
||||
wake_skip=has_workout_skip_today(),
|
||||
evening_lockdown=tonights_lockdown(instant) is not None,
|
||||
hold=workout_hold_active(instant),
|
||||
now=instant,
|
||||
)
|
||||
|
||||
|
||||
28
screen_locker/tests/_hold_fakes.py
Normal file
28
screen_locker/tests/_hold_fakes.py
Normal file
@ -0,0 +1,28 @@
|
||||
"""A ``holds`` stand-in for tests: ``covers`` returns a value or raises it."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class FakeHolds:
|
||||
"""A ``holds`` stand-in: ``covers`` returns the value, or raises it."""
|
||||
|
||||
def __init__(self, covers: object) -> None:
|
||||
self._covers = covers
|
||||
self.calls: list[tuple[str, datetime | None]] = []
|
||||
|
||||
def covers(self, gate: str, now: datetime | None = None) -> bool:
|
||||
"""Record the call, then answer like ``holds.covers``."""
|
||||
self.calls.append((gate, now))
|
||||
if isinstance(self._covers, BaseException):
|
||||
raise self._covers
|
||||
return bool(self._covers)
|
||||
|
||||
|
||||
def fake_holds_module(*, covers: object) -> FakeHolds:
|
||||
"""A ``holds`` stand-in to install with ``monkeypatch.setitem(sys.modules, ...)``."""
|
||||
return FakeHolds(covers)
|
||||
@ -18,14 +18,12 @@ from unittest.mock import MagicMock, patch
|
||||
import pytest
|
||||
|
||||
from screen_locker import _hold
|
||||
from screen_locker._compliance_state import explain_lock_decision
|
||||
from screen_locker._hold import (
|
||||
HOLD_REASON,
|
||||
skip_during_hold,
|
||||
watch_for_hold,
|
||||
workout_hold_active,
|
||||
)
|
||||
from screen_locker._status_data import gather_status
|
||||
from screen_locker.tests._hold_fakes import FakeHolds, fake_holds_module
|
||||
from screen_locker.tests.conftest import create_locker
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@ -34,29 +32,10 @@ if TYPE_CHECKING:
|
||||
NOW = datetime(2026, 11, 5, 11, 5).astimezone()
|
||||
|
||||
|
||||
class _FakeHolds:
|
||||
"""A ``holds`` stand-in: ``covers`` returns the value, or raises it."""
|
||||
|
||||
def __init__(self, covers: object) -> None:
|
||||
self._covers = covers
|
||||
self.calls: list[tuple[str, datetime | None]] = []
|
||||
|
||||
def covers(self, gate: str, now: datetime | None = None) -> bool:
|
||||
"""Record the call, then answer like ``holds.covers``."""
|
||||
self.calls.append((gate, now))
|
||||
if isinstance(self._covers, BaseException):
|
||||
raise self._covers
|
||||
return bool(self._covers)
|
||||
|
||||
|
||||
def _fake_holds(*, covers: object) -> _FakeHolds:
|
||||
return _FakeHolds(covers)
|
||||
|
||||
|
||||
@pytest.fixture(name="fake_holds")
|
||||
def _fake_holds_fixture(monkeypatch: pytest.MonkeyPatch) -> _FakeHolds:
|
||||
def _fake_holds_fixture(monkeypatch: pytest.MonkeyPatch) -> FakeHolds:
|
||||
"""Install a covering ``holds`` for the duration of one test."""
|
||||
mod = _fake_holds(covers=True)
|
||||
mod = fake_holds_module(covers=True)
|
||||
monkeypatch.setitem(sys.modules, "holds", mod)
|
||||
return mod
|
||||
|
||||
@ -65,7 +44,7 @@ class TestWorkoutHoldActive:
|
||||
"""The wrapper around ``holds.covers("workout", now)``."""
|
||||
|
||||
def test_covered_is_true_and_asks_for_the_workout_gate(
|
||||
self, fake_holds: _FakeHolds
|
||||
self, fake_holds: FakeHolds
|
||||
) -> None:
|
||||
"""An active hold reads True, asked about the 'workout' gate and now."""
|
||||
assert workout_hold_active(NOW) is True
|
||||
@ -73,7 +52,7 @@ class TestWorkoutHoldActive:
|
||||
|
||||
def test_not_covered_is_false(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""No active hold reads False."""
|
||||
monkeypatch.setitem(sys.modules, "holds", _fake_holds(covers=False))
|
||||
monkeypatch.setitem(sys.modules, "holds", fake_holds_module(covers=False))
|
||||
assert workout_hold_active(NOW) is False
|
||||
|
||||
def test_import_error_fails_closed_and_says_so(
|
||||
@ -90,14 +69,14 @@ class TestWorkoutHoldActive:
|
||||
self, monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture
|
||||
) -> None:
|
||||
"""A holds call that blows up means no hold, logged at error."""
|
||||
monkeypatch.setitem(sys.modules, "holds", _fake_holds(covers=OSError("boom")))
|
||||
monkeypatch.setitem(
|
||||
sys.modules, "holds", fake_holds_module(covers=OSError("boom"))
|
||||
)
|
||||
with caplog.at_level(logging.ERROR, logger="screen_locker._hold"):
|
||||
assert workout_hold_active(NOW) is False
|
||||
assert "boom" in caplog.text
|
||||
|
||||
def test_default_now_is_passed_through_as_none(
|
||||
self, fake_holds: _FakeHolds
|
||||
) -> None:
|
||||
def test_default_now_is_passed_through_as_none(self, fake_holds: FakeHolds) -> None:
|
||||
"""Without ``now`` the clock is holds' own (None)."""
|
||||
assert workout_hold_active() is True
|
||||
assert fake_holds.calls == [("workout", None)]
|
||||
@ -110,12 +89,12 @@ class TestSkipDuringHold:
|
||||
self, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
"""Without a hold the rung records nothing."""
|
||||
monkeypatch.setitem(sys.modules, "holds", _fake_holds(covers=False))
|
||||
monkeypatch.setitem(sys.modules, "holds", fake_holds_module(covers=False))
|
||||
record_skip = MagicMock()
|
||||
assert skip_during_hold(record_skip) is False
|
||||
record_skip.assert_not_called()
|
||||
|
||||
def test_hold_records_the_skip(self, fake_holds: _FakeHolds) -> None:
|
||||
def test_hold_records_the_skip(self, fake_holds: FakeHolds) -> None:
|
||||
"""An active hold records a test_hold_active skip."""
|
||||
record_skip = MagicMock()
|
||||
assert skip_during_hold(record_skip) is True
|
||||
@ -167,7 +146,7 @@ class TestLadder:
|
||||
# After construction: building the locker runs the ladder itself.
|
||||
locker = create_locker(mock_tk, tmp_path)
|
||||
mock_sys_exit.reset_mock()
|
||||
monkeypatch.setitem(sys.modules, "holds", _fake_holds(covers=True))
|
||||
monkeypatch.setitem(sys.modules, "holds", fake_holds_module(covers=True))
|
||||
with (
|
||||
patch("screen_locker._startup_checks.record_decision") as record,
|
||||
patch("screen_locker._startup_checks.is_relaxed_day", return_value=relaxed),
|
||||
@ -194,7 +173,7 @@ class TestLadder:
|
||||
relaxed: bool,
|
||||
) -> None:
|
||||
"""No hold: the relaxed prompt or the enforced lock still happen."""
|
||||
monkeypatch.setitem(sys.modules, "holds", _fake_holds(covers=False))
|
||||
monkeypatch.setitem(sys.modules, "holds", fake_holds_module(covers=False))
|
||||
locker = create_locker(mock_tk, tmp_path)
|
||||
with (
|
||||
patch("screen_locker._startup_checks.record_decision") as record,
|
||||
@ -207,91 +186,3 @@ class TestLadder:
|
||||
expected = "relaxed_day" if relaxed else "enforced"
|
||||
assert [c.args[0].reason for c in record.call_args_list] == [expected]
|
||||
mock_sys_exit.assert_not_called()
|
||||
|
||||
|
||||
class TestWatchForHold:
|
||||
"""An already-open window closes within one poll of a hold starting."""
|
||||
|
||||
def test_arms_a_poll_no_longer_than_thirty_seconds(self) -> None:
|
||||
"""The first check is scheduled on the Tk loop, <= 30 s out."""
|
||||
root, close = MagicMock(), MagicMock()
|
||||
watch_for_hold(root, close)
|
||||
delay, _tick = root.after.call_args.args
|
||||
assert 0 < delay <= 30_000
|
||||
close.assert_not_called()
|
||||
|
||||
def test_no_hold_rearms_and_does_not_close(
|
||||
self, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
"""Without a hold the tick only schedules the next one."""
|
||||
monkeypatch.setitem(sys.modules, "holds", _fake_holds(covers=False))
|
||||
root, close = MagicMock(), MagicMock()
|
||||
watch_for_hold(root, close)
|
||||
root.after.call_args.args[1]()
|
||||
assert root.after.call_count == 2
|
||||
close.assert_not_called()
|
||||
|
||||
def test_hold_closes_the_window_and_stops_polling(
|
||||
self, monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture
|
||||
) -> None:
|
||||
"""A hold that starts mid-window closes it, once, and says so."""
|
||||
monkeypatch.setitem(sys.modules, "holds", _fake_holds(covers=True))
|
||||
root, close = MagicMock(), MagicMock()
|
||||
watch_for_hold(root, close)
|
||||
with caplog.at_level(logging.WARNING, logger="screen_locker._hold"):
|
||||
root.after.call_args.args[1]()
|
||||
close.assert_called_once_with()
|
||||
assert root.after.call_count == 1
|
||||
assert "hold" in caplog.text
|
||||
|
||||
def test_construction_arms_the_watch(
|
||||
self, mock_tk: MagicMock, tmp_path: Path
|
||||
) -> None:
|
||||
"""A built locker has the watch armed on its own root and ``close``."""
|
||||
with patch("screen_locker.screen_lock.watch_for_hold") as watch:
|
||||
locker = create_locker(mock_tk, tmp_path)
|
||||
watch.assert_called_once_with(locker.root, locker.close)
|
||||
|
||||
|
||||
class TestStatusMirror:
|
||||
"""The status explainer names the hold instead of ``would_lock``."""
|
||||
|
||||
def _explain(self, tmp_path: Path, *, hold: bool) -> object:
|
||||
return explain_lock_decision(
|
||||
log_file=tmp_path / "log.json",
|
||||
early_bird_pending_file=tmp_path / "early_bird_pending.json",
|
||||
weekly_minimum_met=False,
|
||||
relaxed_day=False,
|
||||
hold=hold,
|
||||
)
|
||||
|
||||
def test_an_active_hold_is_the_terminal_stage(self, tmp_path: Path) -> None:
|
||||
"""The explanation stops at test_hold_active without firing."""
|
||||
result = self._explain(tmp_path, hold=True)
|
||||
assert result.stage == "test_hold_active"
|
||||
assert result.fired is False
|
||||
|
||||
def test_without_it_the_chain_still_would_lock(self, tmp_path: Path) -> None:
|
||||
"""No hold: the explanation still reaches would_lock."""
|
||||
assert self._explain(tmp_path, hold=False).stage == "would_lock"
|
||||
|
||||
def test_snapshot_reads_the_hold(
|
||||
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
"""``gather_status`` asks ``holds`` and the explanation follows it."""
|
||||
monkeypatch.setitem(sys.modules, "holds", _fake_holds(covers=True))
|
||||
snap = gather_status(
|
||||
log_file=tmp_path / "log.json",
|
||||
early_bird_pending_file=tmp_path / "early_bird_pending.json",
|
||||
extra_benefits_file=tmp_path / "extra.json",
|
||||
shutdown_config_file=tmp_path / "shutdown.conf",
|
||||
now=NOW,
|
||||
)
|
||||
assert snap.lock_explanation.stage == "test_hold_active"
|
||||
|
||||
def test_other_conditions_name_the_hold(
|
||||
self, mock_tk: MagicMock, tmp_path: Path, fake_holds: _FakeHolds
|
||||
) -> None:
|
||||
"""A decision made for another reason still reports the hold as also=."""
|
||||
locker = create_locker(mock_tk, tmp_path)
|
||||
assert "test_hold_active" in str(locker._other_conditions("enforced"))
|
||||
|
||||
98
screen_locker/tests/test_hold_watch.py
Normal file
98
screen_locker/tests/test_hold_watch.py
Normal file
@ -0,0 +1,98 @@
|
||||
"""An open workout window closes when a hold starts; the status page names it."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import sys
|
||||
from typing import TYPE_CHECKING
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from screen_locker._compliance_state import explain_lock_decision
|
||||
from screen_locker._hold import watch_for_hold
|
||||
from screen_locker.tests._hold_fakes import fake_holds_module
|
||||
from screen_locker.tests.conftest import create_locker
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class TestWatchForHold:
|
||||
"""An already-open window closes within one poll of a hold starting."""
|
||||
|
||||
def test_arms_a_poll_no_longer_than_thirty_seconds(self) -> None:
|
||||
"""The first check is scheduled on the Tk loop, <= 30 s out."""
|
||||
root, close = MagicMock(), MagicMock()
|
||||
watch_for_hold(root, close)
|
||||
delay, _tick = root.after.call_args.args
|
||||
assert 0 < delay <= 30_000
|
||||
close.assert_not_called()
|
||||
|
||||
def test_no_hold_rearms_and_does_not_close(
|
||||
self, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
"""Without a hold the tick only schedules the next one."""
|
||||
monkeypatch.setitem(sys.modules, "holds", fake_holds_module(covers=False))
|
||||
root, close = MagicMock(), MagicMock()
|
||||
watch_for_hold(root, close)
|
||||
root.after.call_args.args[1]()
|
||||
assert root.after.call_count == 2
|
||||
close.assert_not_called()
|
||||
|
||||
def test_hold_closes_the_window_and_stops_polling(
|
||||
self, monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture
|
||||
) -> None:
|
||||
"""A hold that starts mid-window closes it, once, and says so."""
|
||||
monkeypatch.setitem(sys.modules, "holds", fake_holds_module(covers=True))
|
||||
root, close = MagicMock(), MagicMock()
|
||||
watch_for_hold(root, close)
|
||||
with caplog.at_level(logging.WARNING, logger="screen_locker._hold"):
|
||||
root.after.call_args.args[1]()
|
||||
close.assert_called_once_with()
|
||||
assert root.after.call_count == 1
|
||||
assert "hold" in caplog.text
|
||||
|
||||
def test_construction_arms_the_watch(
|
||||
self, mock_tk: MagicMock, tmp_path: Path
|
||||
) -> None:
|
||||
"""A built locker has the watch armed on its own root and ``close``."""
|
||||
with patch("screen_locker.screen_lock.watch_for_hold") as watch:
|
||||
locker = create_locker(mock_tk, tmp_path)
|
||||
watch.assert_called_once_with(locker.root, locker.close)
|
||||
|
||||
|
||||
class TestStatusMirror:
|
||||
"""The status explainer names the hold instead of ``would_lock``."""
|
||||
|
||||
def _explain(self, tmp_path: Path, *, covers: bool) -> object:
|
||||
sys.modules["holds"] = fake_holds_module(covers=covers)
|
||||
return explain_lock_decision(
|
||||
log_file=tmp_path / "log.json",
|
||||
early_bird_pending_file=tmp_path / "early_bird_pending.json",
|
||||
weekly_minimum_met=False,
|
||||
relaxed_day=False,
|
||||
)
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _restore_holds(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""Let the test's own ``sys.modules['holds']`` be undone afterwards."""
|
||||
monkeypatch.setitem(sys.modules, "holds", sys.modules.get("holds"))
|
||||
|
||||
def test_an_active_hold_is_the_terminal_stage(self, tmp_path: Path) -> None:
|
||||
"""The explanation stops at test_hold_active without firing."""
|
||||
result = self._explain(tmp_path, covers=True)
|
||||
assert result.stage == "test_hold_active"
|
||||
assert result.fired is False
|
||||
|
||||
def test_without_it_the_chain_still_would_lock(self, tmp_path: Path) -> None:
|
||||
"""No hold: the explanation still reaches would_lock."""
|
||||
assert self._explain(tmp_path, covers=False).stage == "would_lock"
|
||||
|
||||
def test_other_conditions_name_the_hold(
|
||||
self, mock_tk: MagicMock, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
"""A decision made for another reason still reports the hold as also=."""
|
||||
locker = create_locker(mock_tk, tmp_path)
|
||||
monkeypatch.setitem(sys.modules, "holds", fake_holds_module(covers=True))
|
||||
assert "test_hold_active" in str(locker._other_conditions("enforced"))
|
||||
Loading…
Reference in New Issue
Block a user