chore: optimize pre-commit, remove tracked binaries, fix lint issues
- Move slow hooks (mypy, pylint, bandit, pytest, prettier) to pre-push stage
- Remove redundant autoflake (ruff covers F401/F841)
- Fix shellcheck OOM by batching files with xargs -n 40
- Remove tracked .o, .wav, .pyc binaries from git
- Move pomodoro wav files to ../testsAndMisc_binaries/ with symlinks
- Add *.o, *.so, *.a to .gitignore
- Refactor hltb._pick_best_hltb_entry to fix C901/PLR0911/SIM102
- Fix SC2034 warnings in gif_to_square.sh and upgrade.sh
- Add disk_cleanup_check.sh script
- Various test and code improvements across screen_locker,
steam_backlog_enforcer, word_frequency, moviepy_showcase
2026-04-10 18:44:51 +02:00
|
|
|
"""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.
|
|
|
|
|
"""
|
2026-03-16 22:46:48 +01:00
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
import tkinter as tk
|
2026-03-27 15:54:01 +01:00
|
|
|
from typing import TYPE_CHECKING
|
2026-03-16 22:46:48 +01:00
|
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
|
|
|
|
|
|
import pytest
|
|
|
|
|
|
|
|
|
|
from python_pkg.screen_locker.screen_lock import ScreenLocker
|
|
|
|
|
|
|
|
|
|
if TYPE_CHECKING:
|
chore: optimize pre-commit, remove tracked binaries, fix lint issues
- Move slow hooks (mypy, pylint, bandit, pytest, prettier) to pre-push stage
- Remove redundant autoflake (ruff covers F401/F841)
- Fix shellcheck OOM by batching files with xargs -n 40
- Remove tracked .o, .wav, .pyc binaries from git
- Move pomodoro wav files to ../testsAndMisc_binaries/ with symlinks
- Add *.o, *.so, *.a to .gitignore
- Refactor hltb._pick_best_hltb_entry to fix C901/PLR0911/SIM102
- Fix SC2034 warnings in gif_to_square.sh and upgrade.sh
- Add disk_cleanup_check.sh script
- Various test and code improvements across screen_locker,
steam_backlog_enforcer, word_frequency, moviepy_showcase
2026-04-10 18:44:51 +02:00
|
|
|
from collections.abc import Generator, Iterator
|
2026-05-01 19:07:34 +02:00
|
|
|
from typing import Literal
|
chore: optimize pre-commit, remove tracked binaries, fix lint issues
- Move slow hooks (mypy, pylint, bandit, pytest, prettier) to pre-push stage
- Remove redundant autoflake (ruff covers F401/F841)
- Fix shellcheck OOM by batching files with xargs -n 40
- Remove tracked .o, .wav, .pyc binaries from git
- Move pomodoro wav files to ../testsAndMisc_binaries/ with symlinks
- Add *.o, *.so, *.a to .gitignore
- Refactor hltb._pick_best_hltb_entry to fix C901/PLR0911/SIM102
- Fix SC2034 warnings in gif_to_square.sh and upgrade.sh
- Add disk_cleanup_check.sh script
- Various test and code improvements across screen_locker,
steam_backlog_enforcer, word_frequency, moviepy_showcase
2026-04-10 18:44:51 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
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("python_pkg.screen_locker.screen_lock.tk", mock),
|
2026-05-14 19:52:15 +02:00
|
|
|
patch("python_pkg.screen_locker._sick_dialog.tk", mock),
|
chore: optimize pre-commit, remove tracked binaries, fix lint issues
- Move slow hooks (mypy, pylint, bandit, pytest, prettier) to pre-push stage
- Remove redundant autoflake (ruff covers F401/F841)
- Fix shellcheck OOM by batching files with xargs -n 40
- Remove tracked .o, .wav, .pyc binaries from git
- Move pomodoro wav files to ../testsAndMisc_binaries/ with symlinks
- Add *.o, *.so, *.a to .gitignore
- Refactor hltb._pick_best_hltb_entry to fix C901/PLR0911/SIM102
- Fix SC2034 warnings in gif_to_square.sh and upgrade.sh
- Add disk_cleanup_check.sh script
- Various test and code improvements across screen_locker,
steam_backlog_enforcer, word_frequency, moviepy_showcase
2026-04-10 18:44:51 +02:00
|
|
|
patch("python_pkg.screen_locker.screen_lock.sys.exit"),
|
|
|
|
|
):
|
|
|
|
|
yield
|
2026-03-16 22:46:48 +01:00
|
|
|
|
|
|
|
|
|
2026-05-16 15:41:40 +02:00
|
|
|
@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(
|
|
|
|
|
"python_pkg.screen_locker.screen_lock.shutil.which",
|
|
|
|
|
return_value="/usr/bin/setxkbmap",
|
|
|
|
|
),
|
|
|
|
|
patch("python_pkg.screen_locker.screen_lock.subprocess.run") as mock,
|
|
|
|
|
):
|
|
|
|
|
yield mock
|
|
|
|
|
|
|
|
|
|
|
2026-05-14 19:52:15 +02:00
|
|
|
@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(
|
|
|
|
|
"python_pkg.screen_locker._sick_tracker.SICK_HISTORY_FILE",
|
|
|
|
|
target,
|
|
|
|
|
),
|
|
|
|
|
patch(
|
|
|
|
|
"python_pkg.screen_locker._constants.SICK_HISTORY_FILE",
|
|
|
|
|
target,
|
|
|
|
|
),
|
|
|
|
|
):
|
|
|
|
|
yield
|
|
|
|
|
|
|
|
|
|
|
2026-05-22 16:00:15 +02:00
|
|
|
@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(
|
|
|
|
|
"python_pkg.screen_locker.screen_lock.SCHEDULED_SKIPS_FILE",
|
|
|
|
|
target,
|
|
|
|
|
):
|
|
|
|
|
yield
|
|
|
|
|
|
|
|
|
|
|
2026-03-16 22:46:48 +01:00
|
|
|
@pytest.fixture
|
|
|
|
|
def mock_tk() -> Generator[MagicMock]:
|
|
|
|
|
"""Mock tkinter module for testing without display."""
|
|
|
|
|
with patch("python_pkg.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("python_pkg.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,
|
2026-03-29 22:50:24 +02:00
|
|
|
verify_only: bool = False,
|
|
|
|
|
is_sick_day_log: bool = False,
|
2026-03-16 22:46:48 +01:00
|
|
|
) -> ScreenLocker:
|
2026-05-01 19:07:34 +02:00
|
|
|
"""Create a ScreenLocker instance with early bird paths disabled."""
|
2026-03-16 22:46:48 +01:00
|
|
|
with (
|
|
|
|
|
patch.object(Path, "resolve", return_value=tmp_path),
|
|
|
|
|
patch.object(ScreenLocker, "has_logged_today", return_value=has_logged),
|
2026-03-29 22:50:24 +02:00
|
|
|
patch.object(
|
|
|
|
|
ScreenLocker,
|
|
|
|
|
"_is_sick_day_log",
|
|
|
|
|
return_value=is_sick_day_log,
|
|
|
|
|
),
|
2026-05-01 19:07:34 +02:00
|
|
|
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,
|
|
|
|
|
),
|
2026-03-16 22:46:48 +01:00
|
|
|
patch.object(ScreenLocker, "_start_phone_check"),
|
2026-03-29 22:50:24 +02:00
|
|
|
patch.object(ScreenLocker, "_start_verify_workout_check"),
|
2026-03-16 22:46:48 +01:00
|
|
|
):
|
2026-03-29 22:50:24 +02:00
|
|
|
return ScreenLocker(
|
|
|
|
|
demo_mode=demo_mode,
|
|
|
|
|
verify_only=verify_only,
|
|
|
|
|
)
|
2026-05-01 19:07:34 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
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_verify_workout_check"),
|
|
|
|
|
):
|
|
|
|
|
return ScreenLocker(demo_mode=demo_mode)
|