mirror of
https://github.com/kuhyx/screen-locker.git
synced 2026-11-11 18:53:55 +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
189 lines
7.6 KiB
Python
189 lines
7.6 KiB
Python
"""A declared test hold keeps every workout-locker path from showing a window.
|
|
|
|
``holds`` (kuhyx/utils, ``holds-v0.1.0``) answers "is a proctored-test hold
|
|
active?". These tests fake it with a stand-in module so they pin only
|
|
screen-locker's side: the rung, its placement below the bookkeeping, and the
|
|
fail-closed behaviour when the package cannot be read.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from contextlib import ExitStack
|
|
from datetime import datetime
|
|
import logging
|
|
import sys
|
|
from typing import TYPE_CHECKING
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
from screen_locker import _hold
|
|
from screen_locker._hold import (
|
|
HOLD_REASON,
|
|
skip_during_hold,
|
|
workout_hold_active,
|
|
)
|
|
from screen_locker.tests._hold_fakes import FakeHolds, fake_holds_module
|
|
from screen_locker.tests.conftest import create_locker
|
|
|
|
if TYPE_CHECKING:
|
|
from pathlib import Path
|
|
|
|
NOW = datetime(2026, 11, 5, 11, 5).astimezone()
|
|
|
|
|
|
@pytest.fixture(name="fake_holds")
|
|
def _fake_holds_fixture(monkeypatch: pytest.MonkeyPatch) -> FakeHolds:
|
|
"""Install a covering ``holds`` for the duration of one test."""
|
|
mod = fake_holds_module(covers=True)
|
|
monkeypatch.setitem(sys.modules, "holds", mod)
|
|
return mod
|
|
|
|
|
|
class TestWorkoutHoldActive:
|
|
"""The wrapper around ``holds.covers("workout", now)``."""
|
|
|
|
def test_covered_is_true_and_asks_for_the_workout_gate(
|
|
self, fake_holds: FakeHolds
|
|
) -> None:
|
|
"""An active hold reads True, asked about the 'workout' gate and now."""
|
|
assert workout_hold_active(NOW) is True
|
|
assert fake_holds.calls == [("workout", NOW)]
|
|
|
|
def test_not_covered_is_false(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
"""No active hold reads 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(
|
|
self, monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture
|
|
) -> None:
|
|
"""A missing package means the gate behaves normally, logged at error."""
|
|
monkeypatch.setitem(sys.modules, "holds", None)
|
|
with caplog.at_level(logging.ERROR, logger="screen_locker._hold"):
|
|
assert workout_hold_active(NOW) is False
|
|
assert "holds" in caplog.text
|
|
assert "normal" in caplog.text
|
|
|
|
def test_runtime_error_fails_closed_and_says_so(
|
|
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_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:
|
|
"""Without ``now`` the clock is holds' own (None)."""
|
|
assert workout_hold_active() is True
|
|
assert fake_holds.calls == [("workout", None)]
|
|
|
|
|
|
class TestSkipDuringHold:
|
|
"""The rung on its own."""
|
|
|
|
def test_no_hold_lets_the_ladder_proceed(
|
|
self, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
"""Without a hold the rung records nothing."""
|
|
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:
|
|
"""An active hold records a test_hold_active skip."""
|
|
record_skip = MagicMock()
|
|
assert skip_during_hold(record_skip) is True
|
|
reason, detail = record_skip.call_args.args
|
|
assert reason == HOLD_REASON == "test_hold_active"
|
|
assert "hold" in detail.lower()
|
|
|
|
def test_the_seam_is_the_module_function(self) -> None:
|
|
"""Tests patch ``_hold.workout_hold_active``, never a from-import."""
|
|
record_skip = MagicMock()
|
|
with patch.object(_hold, "workout_hold_active", return_value=True):
|
|
assert skip_during_hold(record_skip) is True
|
|
|
|
|
|
def _fall_through_the_ladder(locker: object) -> ExitStack:
|
|
"""Every rung above the hold rung falls through (see test_night_lockdown_rung)."""
|
|
stack = ExitStack()
|
|
for patcher in (
|
|
patch.object(locker, "_is_scheduled_skip_today", return_value=False),
|
|
patch.object(locker, "_ingest_synced_manual_workouts"),
|
|
patch.object(locker, "_auto_fill_week_runnerup_bonus"),
|
|
patch.object(locker, "_check_today_state_exits", return_value=False),
|
|
patch.object(locker, "has_logged_today", return_value=False),
|
|
patch("screen_locker._startup_checks.has_weekly_minimum", return_value=False),
|
|
patch("screen_locker._startup_checks.process_week_transition", return_value=[]),
|
|
patch(
|
|
"screen_locker._startup_checks.reset_to_base_if_new_day",
|
|
return_value=False,
|
|
),
|
|
):
|
|
stack.enter_context(patcher)
|
|
return stack
|
|
|
|
|
|
@pytest.mark.parametrize("relaxed", [True, False], ids=["relaxed_day", "strict_day"])
|
|
class TestLadder:
|
|
"""``_check_non_verify_exits`` during and outside a hold."""
|
|
|
|
def test_hold_means_no_prompt_and_no_lock(
|
|
self,
|
|
mock_tk: MagicMock,
|
|
mock_sys_exit: MagicMock,
|
|
tmp_path: Path,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
*,
|
|
relaxed: bool,
|
|
) -> None:
|
|
"""The hold skips before the optional prompt, the heat dialog or the lock."""
|
|
# 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_module(covers=True))
|
|
with (
|
|
patch("screen_locker._startup_checks.record_decision") as record,
|
|
patch("screen_locker._startup_checks.is_relaxed_day", return_value=relaxed),
|
|
patch.object(locker, "_check_heat_skip_exit") as heat,
|
|
patch("screen_locker._startup_checks.apply_flat_bonuses_if_new") as bonus,
|
|
_fall_through_the_ladder(locker),
|
|
):
|
|
locker._check_non_verify_exits()
|
|
assert [c.args[0].reason for c in record.call_args_list] == ["test_hold_active"]
|
|
assert record.call_args.args[0].locked is False
|
|
heat.assert_not_called()
|
|
assert getattr(locker, "_relaxed_day_mode", False) is False
|
|
# Credit bookkeeping above the rung is untouched.
|
|
bonus.assert_called_once()
|
|
mock_sys_exit.assert_called_once_with(0)
|
|
|
|
def test_without_a_hold_the_ladder_is_unchanged(
|
|
self,
|
|
mock_tk: MagicMock,
|
|
mock_sys_exit: MagicMock,
|
|
tmp_path: Path,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
*,
|
|
relaxed: bool,
|
|
) -> None:
|
|
"""No hold: the relaxed prompt or the enforced lock still happen."""
|
|
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,
|
|
patch("screen_locker._startup_checks.is_relaxed_day", return_value=relaxed),
|
|
patch.object(locker, "_check_heat_skip_exit", return_value="x"),
|
|
patch("screen_locker._startup_checks.apply_flat_bonuses_if_new"),
|
|
_fall_through_the_ladder(locker),
|
|
):
|
|
locker._check_non_verify_exits()
|
|
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()
|