screen-locker/screen_locker/tests/conftest.py
Krzysztof kuhy Rudnicki 4cdfce5fe3 chore: set up as standalone repo
Extracted from testsAndMisc monorepo. Changes:
- Rewrote imports from python_pkg.screen_locker.* → screen_locker.*
- Vendored python_pkg.shared.log_integrity → screen_locker._log_integrity
- Vendored wake_alarm constants (ALARM_DAYS, WAKE_AFTER_HOURS, RTCWAKE_BIN) into _constants.py
- Extracted has_workout_skip_today into new screen_locker._wake_state module
- Added tests for _wake_state.py (392 tests, 100% branch coverage)
- Moved scripts/service files to repo root
- Added standalone pyproject.toml, requirements.txt, .pre-commit-config.yaml, .gitignore
- Added GitHub Actions CI workflows

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 07:43:06 +02:00

274 lines
9.0 KiB
Python

"""Shared fixtures and helpers for screen_locker tests.
Safety:
``_block_real_tk_and_exit`` (autouse) replaces the **entire** ``tk``
module reference inside ``screen_lock`` with a MagicMock and stubs
``sys.exit``. This makes it physically impossible for any test to
create a real Tk root window, go fullscreen, or grab input — even if
the test forgets to request the explicit ``mock_tk`` fixture.
"""
from __future__ import annotations
from pathlib import Path
import tkinter as tk
from typing import TYPE_CHECKING
from unittest.mock import MagicMock, patch
import pytest
from screen_locker.screen_lock import ScreenLocker
if TYPE_CHECKING:
from collections.abc import Generator, Iterator
from typing import Literal
def _make_mock_tk() -> MagicMock:
"""Build a MagicMock that stands in for the ``tkinter`` module."""
mock = MagicMock()
mock_root = MagicMock()
mock_root.winfo_screenwidth.return_value = 1920
mock_root.winfo_screenheight.return_value = 1080
mock.Tk.return_value = mock_root
mock_frame = MagicMock()
mock_frame.winfo_children.return_value = []
mock.Frame.return_value = mock_frame
# Keep real TclError so ``except tk.TclError`` still works.
mock.TclError = tk.TclError
return mock
@pytest.fixture(autouse=True)
def _block_real_tk_and_exit() -> Iterator[None]:
"""Replace the whole ``tk`` module and ``sys.exit`` for every test.
Patching the entire module (not just ``tk.Tk``) ensures that
**nothing** in tkinter can touch the real display server.
"""
mock = _make_mock_tk()
with (
patch("screen_locker.screen_lock.tk", mock),
patch("screen_locker._sick_dialog.tk", mock),
patch("screen_locker.screen_lock.sys.exit"),
):
yield
@pytest.fixture(autouse=True)
def mock_subprocess_run() -> Generator[MagicMock]:
"""Block real subprocess calls (e.g. setxkbmap) for every test.
Also exposed as a named fixture so individual tests can assert
on the calls made (e.g. VT switching tests).
``shutil.which`` is mocked to return a stable fake path so tests work
regardless of whether setxkbmap is installed on the host machine.
"""
with (
patch(
"screen_locker._window_setup.shutil.which",
return_value="/usr/bin/setxkbmap",
),
patch("screen_locker._window_setup.subprocess.run") as mock,
):
yield mock
@pytest.fixture(autouse=True)
def _isolate_sick_history(tmp_path: Path) -> Iterator[None]:
"""Redirect SICK_HISTORY_FILE to tmp_path so tests cannot touch real state."""
target = tmp_path / "sick_history.json"
with (
patch(
"screen_locker._sick_tracker.SICK_HISTORY_FILE",
target,
),
patch(
"screen_locker._constants.SICK_HISTORY_FILE",
target,
),
):
yield
@pytest.fixture(autouse=True)
def _isolate_scheduled_skips(tmp_path: Path) -> Iterator[None]:
"""Redirect SCHEDULED_SKIPS_FILE to tmp_path so tests use a clean file."""
target = tmp_path / "scheduled_skips.json"
with patch(
"screen_locker.screen_lock.SCHEDULED_SKIPS_FILE",
target,
):
yield
@pytest.fixture(autouse=True)
def _mock_weekly_logic() -> Iterator[None]:
"""Default to Fri-Mon enforcement with weekly minimum not yet met.
Without this, tests that run on a Tue/Wed/Thu would hit the relaxed-day
branch instead of the full-lock path that existing tests expect.
Setting has_weekly_minimum=False ensures the full lock is shown
(weekly quota not reached → enforce).
"""
with (
patch(
"screen_locker.screen_lock.is_relaxed_day",
return_value=False,
),
patch(
"screen_locker.screen_lock.has_weekly_minimum",
return_value=False,
),
):
yield
@pytest.fixture
def mock_tk() -> Generator[MagicMock]:
"""Mock tkinter module for testing without display."""
with patch("screen_locker.screen_lock.tk") as mock:
# Set up Tk root mock
mock_root = MagicMock()
mock_root.winfo_screenwidth.return_value = 1920
mock_root.winfo_screenheight.return_value = 1080
mock.Tk.return_value = mock_root
# Set up Frame mock
mock_frame = MagicMock()
mock_frame.winfo_children.return_value = []
mock.Frame.return_value = mock_frame
# Set up TclError as actual exception class
mock.TclError = tk.TclError
yield mock
@pytest.fixture
def mock_sys_exit() -> Generator[MagicMock]:
"""Mock sys.exit to prevent test termination."""
with patch("screen_locker.screen_lock.sys.exit") as mock:
yield mock
@pytest.fixture
def _mock_sys_exit(mock_sys_exit: MagicMock) -> MagicMock:
"""Alias for mock_sys_exit when the return value is unused."""
return mock_sys_exit
@pytest.fixture
def temp_log_file(tmp_path: Path) -> Path:
"""Create a temporary log file path."""
return tmp_path / "workout_log.json"
def create_locker(
_mock_tk: MagicMock,
tmp_path: Path,
*,
demo_mode: bool = True,
has_logged: bool = False,
verify_only: bool = False,
is_sick_day_log: bool = False,
) -> ScreenLocker:
"""Create a ScreenLocker instance with early bird paths disabled."""
with (
patch.object(Path, "resolve", return_value=tmp_path),
patch.object(ScreenLocker, "has_logged_today", return_value=has_logged),
patch.object(
ScreenLocker,
"_is_sick_day_log",
return_value=is_sick_day_log,
),
patch.object(ScreenLocker, "_is_early_bird_log", return_value=False),
patch.object(ScreenLocker, "_is_early_bird_time", return_value=False),
patch.object(
ScreenLocker,
"_try_auto_upgrade_early_bird",
return_value=False,
),
patch.object(ScreenLocker, "_start_phone_check"),
patch.object(ScreenLocker, "_start_relaxed_day_flow"),
patch.object(ScreenLocker, "_start_verify_workout_check"),
):
return ScreenLocker(
demo_mode=demo_mode,
verify_only=verify_only,
)
def create_locker_relaxed_day(
_mock_tk: MagicMock,
tmp_path: Path,
*,
demo_mode: bool = True,
has_logged: bool = False,
) -> ScreenLocker:
"""Create a ScreenLocker in relaxed-day mode (Tue/Wed/Thu).
``is_relaxed_day`` returns True so ``_relaxed_day_mode`` is set and
``_start_relaxed_day_flow`` is called instead of ``_start_phone_check``.
The autouse ``_mock_weekly_logic`` fixture is overridden here.
"""
with (
patch.object(Path, "resolve", return_value=tmp_path),
patch.object(ScreenLocker, "has_logged_today", return_value=has_logged),
patch.object(ScreenLocker, "_is_sick_day_log", return_value=False),
patch.object(ScreenLocker, "_is_early_bird_log", return_value=False),
patch.object(ScreenLocker, "_is_early_bird_time", return_value=False),
patch.object(ScreenLocker, "_try_auto_upgrade_early_bird", return_value=False),
patch("screen_locker.screen_lock.is_relaxed_day", return_value=True),
patch(
"screen_locker.screen_lock.has_weekly_minimum",
return_value=False,
),
patch.object(ScreenLocker, "_start_phone_check"),
patch.object(ScreenLocker, "_start_relaxed_day_flow"),
patch.object(ScreenLocker, "_start_verify_workout_check"),
):
return ScreenLocker(demo_mode=demo_mode)
def create_locker_early_bird(
_mock_tk: MagicMock,
tmp_path: Path,
*,
state: Literal["none", "log_active", "log_expired"] = "none",
has_logged: bool = False,
demo_mode: bool = True,
) -> ScreenLocker:
"""Create a ScreenLocker configured for early bird path testing.
Args:
state: One of:
- "none": outside early bird window, no early bird log.
- "log_active": early bird log exists, still in window.
- "log_expired": early bird log exists, past 8:30 AM.
has_logged: Return value for has_logged_today mock.
demo_mode: Passed to ScreenLocker constructor.
"""
is_early_bird_log = state in ("log_active", "log_expired")
is_early_bird_time = state == "log_active"
with (
patch.object(Path, "resolve", return_value=tmp_path),
patch.object(ScreenLocker, "has_logged_today", return_value=has_logged),
patch.object(ScreenLocker, "_is_sick_day_log", return_value=False),
patch.object(
ScreenLocker, "_is_early_bird_log", return_value=is_early_bird_log
),
patch.object(
ScreenLocker, "_is_early_bird_time", return_value=is_early_bird_time
),
patch.object(ScreenLocker, "_try_auto_upgrade_early_bird", return_value=False),
patch.object(ScreenLocker, "_start_phone_check"),
patch.object(ScreenLocker, "_start_relaxed_day_flow"),
patch.object(ScreenLocker, "_start_verify_workout_check"),
):
return ScreenLocker(demo_mode=demo_mode)