mirror of
https://github.com/kuhyx/screen-locker.git
synced 2026-11-11 17:53:56 +01:00
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
99 lines
4.0 KiB
Python
99 lines
4.0 KiB
Python
"""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"))
|