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>
This commit is contained in:
Krzysztof kuhy Rudnicki 2026-05-28 07:43:06 +02:00
parent 1172aff8fb
commit 4cdfce5fe3
50 changed files with 898 additions and 202 deletions

19
.github/workflows/pre-commit.yml vendored Normal file
View File

@ -0,0 +1,19 @@
name: pre-commit
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
pre-commit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install dependencies
run: pip install -r requirements.txt
- uses: pre-commit/action@v3.0.1

28
.github/workflows/python-tests.yml vendored Normal file
View File

@ -0,0 +1,28 @@
name: Tests
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.10", "3.11", "3.12"]
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: pip install -r requirements.txt
- name: Run tests with coverage
run: python -m pytest -v

16
.gitignore vendored Normal file
View File

@ -0,0 +1,16 @@
__pycache__/
*.py[cod]
.Python
build/
dist/
*.egg-info/
.env
.venv/
.pytest_cache/
.mypy_cache/
.ruff_cache/
.coverage
coverage.lcov
htmlcov/
*.log
.DS_Store

131
.pre-commit-config.yaml Normal file
View File

@ -0,0 +1,131 @@
# Pre-commit Configuration for screen-locker
# Install: pre-commit install && pre-commit install --hook-type pre-push
# Run: pre-commit run --all-files
# Update: pre-commit autoupdate
default_language_version:
python: python3
default_stages: [pre-commit]
fail_fast: false
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0
hooks:
- id: trailing-whitespace
args: [--markdown-linebreak-ext=md]
- id: end-of-file-fixer
- id: check-yaml
- id: check-json
- id: check-toml
- id: check-added-large-files
args: [--maxkb=2000]
- id: check-merge-conflict
- id: detect-private-key
- id: debug-statements
- id: name-tests-test
args: [--pytest-test-first]
- id: check-ast
- id: mixed-line-ending
args: [--fix=lf]
- id: requirements-txt-fixer
- repo: local
hooks:
- id: no-noqa
name: Block noqa comments
entry: '(?i)#\s*(noqa|type:\s*ignore)'
language: pygrep
types: [python]
- id: no-ruff-noqa
name: Block ruff noqa file-level comments
entry: '(?i)#\s*ruff:\s*noqa'
language: pygrep
types: [python]
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.15.2
hooks:
- id: ruff
args: [--fix, --unsafe-fixes, --exit-non-zero-on-fix, --show-fixes]
types_or: [python, pyi]
- id: ruff-format
types_or: [python, pyi]
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.13.0
hooks:
- id: mypy
args:
- --ignore-missing-imports
- --no-error-summary
- --disable-error-code=no-untyped-def
- --disable-error-code=no-untyped-call
- --disable-error-code=var-annotated
- --disable-error-code=no-any-unimported
- --disable-error-code=type-arg
- --disable-error-code=no-any-return
- --disable-error-code=misc
- --disable-error-code=unused-ignore
- --disable-error-code=unreachable
- --disable-error-code=assignment
- --disable-error-code=no-redef
- --disable-error-code=attr-defined
- --disable-error-code=arg-type
- --disable-error-code=union-attr
- --disable-error-code=call-overload
- --disable-error-code=return-value
- --disable-error-code=redundant-cast
- --disable-error-code=empty-body
- --disable-error-code=list-item
- repo: https://github.com/pylint-dev/pylint
rev: v3.3.2
hooks:
- id: pylint
args:
- --rcfile=pyproject.toml
- --fail-under=8.0
- --jobs=4
additional_dependencies:
- pytest
- repo: https://github.com/PyCQA/bandit
rev: 1.7.10
hooks:
- id: bandit
args:
- -c
- pyproject.toml
- --severity-level=high
- --confidence-level=medium
- --skip=B113
additional_dependencies: ["bandit[toml]"]
exclude: ^(tests/|.*test.*\.py$)
- repo: local
hooks:
- id: pytest-coverage
name: pytest with coverage enforcement
entry: python -m pytest
language: system
types: [python]
pass_filenames: false
require_serial: true
stages: [pre-push]
- repo: https://github.com/codespell-project/codespell
rev: v2.3.0
hooks:
- id: codespell
args:
- --skip=*.json,*.lock,.git,__pycache__,.venv
- --ignore-words-list=als,ans,ect,nd,som,sur,te,nam,numer,lew,sie,wil,postion,clen,ther,folow,derrive,ony,tje,noe,theses,crate,doubleclick,wile,tabel,pary,blok,bloc,proces,serwer,parametr,adres,hart,dout,metod,tekst,synonim,grup,mosty,lokal,skalar,milion,nowe,tre,hel,alph
- repo: local
hooks:
- id: shellcheck
name: shellcheck
entry: bash -c 'printf "%s\0" "$@" | xargs -0 -n 40 shellcheck --severity=warning' --
language: system
types: [shell]

149
pyproject.toml Normal file
View File

@ -0,0 +1,149 @@
[project]
name = "screen-locker"
version = "1.0.0"
description = "Tkinter/systemd screen locker with workout tracking, sick-day management, and wake-alarm integration"
requires-python = ">=3.10"
dependencies = [] # pure stdlib — tkinter is bundled with Python
[tool.ruff]
target-version = "py310"
include = ["*.py", "**/*.py"]
exclude = [".git", ".venv", "__pycache__", "build", "dist", ".eggs"]
[tool.ruff.lint]
select = ["ALL"]
ignore = [
"D203",
"D213",
"COM812",
"ISC001",
"S603",
]
fixable = ["ALL"]
unfixable = []
[tool.ruff.lint.per-file-ignores]
"**/tests/**/*.py" = ["ARG", "D", "PLC0415", "PLR2004", "S101", "SLF001"]
"**/test_*.py" = ["ARG", "D", "PLC0415", "PLR2004", "S101", "SLF001"]
[tool.ruff.lint.pydocstyle]
convention = "google"
[tool.ruff.lint.isort]
force-single-line = false
force-sort-within-sections = true
known-first-party = ["screen_locker"]
[tool.ruff.lint.flake8-quotes]
docstring-quotes = "double"
inline-quotes = "double"
[tool.ruff.lint.flake8-tidy-imports]
ban-relative-imports = "all"
[tool.ruff.format]
quote-style = "double"
indent-style = "space"
skip-magic-trailing-comma = false
line-ending = "auto"
docstring-code-format = true
[tool.mypy]
python_version = "3.10"
strict = true
warn_return_any = true
warn_unused_configs = true
disallow_untyped_defs = true
disallow_incomplete_defs = true
check_untyped_defs = true
disallow_untyped_decorators = true
no_implicit_optional = true
warn_redundant_casts = true
warn_unused_ignores = true
warn_no_return = true
warn_unreachable = true
disallow_any_unimported = true
disallow_any_explicit = false
disallow_any_generics = true
disallow_subclassing_any = true
strict_equality = true
extra_checks = true
ignore_missing_imports = true
show_error_codes = true
color_output = true
exclude = [".venv/"]
[tool.pylint.main]
analyse-fallback-blocks = true
persistent = true
jobs = 0
py-version = "3.10"
ignore = [".venv", "__pycache__"]
ignore-patterns = [".*\\.pyi$"]
[tool.pylint.messages_control]
enable = "all"
disable = []
[tool.pylint.design]
min-public-methods = 0
max-module-lines = 1000
max-attributes = 10
[tool.pylint.typecheck]
generated-members = [
".*\\.assert_called_once_with",
".*\\.assert_called_once",
".*\\.assert_called",
".*\\.assert_not_called",
".*\\.assert_any_call",
".*\\.call_args",
".*\\.call_args_list",
".*\\.call_count",
]
[tool.bandit]
exclude_dirs = ["tests", ".venv"]
[tool.pytest.ini_options]
testpaths = ["screen_locker/tests"]
python_files = ["test_*.py", "*_test.py"]
python_classes = ["Test*"]
python_functions = ["test_*"]
addopts = [
"-v",
"--strict-markers",
"--strict-config",
"-ra",
"--cov=screen_locker",
"--cov-branch",
"--cov-report=term-missing",
"--cov-report=lcov",
]
filterwarnings = [
"error",
"ignore::DeprecationWarning",
"default::pytest.PytestUnraisableExceptionWarning",
]
[tool.coverage.run]
source = ["screen_locker"]
branch = true
omit = [
"*/__pycache__/*",
"*/tests/*",
"*/.venv/*",
]
[tool.coverage.report]
fail_under = 100
show_missing = true
skip_covered = false
exclude_lines = [
"pragma: no cover",
"raise NotImplementedError",
"raise AssertionError",
"if TYPE_CHECKING:",
'if __name__ == "__main__":',
]
partial_branches = ["pragma: no branch"]

14
requirements.txt Normal file
View File

@ -0,0 +1,14 @@
# Screen Locker — development dependencies
# Runtime: pure Python stdlib (tkinter, subprocess, socket, sqlite3, etc.)
bandit>=1.7.0
codespell>=2.2.0
coverage>=7.4.0
mypy>=1.8.0
pre-commit>=3.6.0
pylint>=3.0.0
pytest>=8.0.0
pytest-cov>=4.1.0
pytest-randomly>=3.15.0
pytest-sugar>=1.0.0
pytest-xdist>=3.5.0
ruff>=0.8.0

View File

@ -1,11 +1,10 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -e set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" VENV="$SCRIPT_DIR/.venv"
VENV="$REPO_ROOT/.venv"
[[ ! -d "$VENV" ]] && python3 -m venv "$VENV" [[ ! -d "$VENV" ]] && python3 -m venv "$VENV"
# tkinter is from Python stdlib; install python-tk system package if missing: # tkinter is from Python stdlib; install python-tk system package if missing:
# Arch: sudo pacman -S python-tk # Arch: sudo pacman -S python-tk
# Debian: sudo apt-get install python3-tk # Debian: sudo apt-get install python3-tk
cd "$REPO_ROOT" cd "$SCRIPT_DIR"
"$VENV/bin/python" -m python_pkg.screen_locker.screen_lock "$@" "$VENV/bin/python" -m screen_locker.screen_lock "$@"

View File

@ -48,3 +48,16 @@ SICK_DAY_STATE_FILE = Path(__file__).resolve().parent / "sick_day_state.json"
SICK_HISTORY_FILE = Path(__file__).resolve().parent / "sick_history.json" SICK_HISTORY_FILE = Path(__file__).resolve().parent / "sick_history.json"
# JSON list of ISO date strings ("YYYY-MM-DD") for which the screen lock is skipped. # JSON list of ISO date strings ("YYYY-MM-DD") for which the screen lock is skipped.
SCHEDULED_SKIPS_FILE = Path(__file__).resolve().parent / "scheduled_skips.json" SCHEDULED_SKIPS_FILE = Path(__file__).resolve().parent / "scheduled_skips.json"
# ---------------------------------------------------------------------------
# Wake-alarm integration (originally from wake_alarm._constants / _state).
# These must match the values used by the companion wake_alarm service.
# ---------------------------------------------------------------------------
# Days on which the wake alarm fires (0=Mon … 6=Sun).
ALARM_DAYS: frozenset[int] = frozenset({0, 4, 5, 6})
# How many hours after midnight the alarm triggers (configurable in wake_alarm).
WAKE_AFTER_HOURS: int = 8
# Path to the rtcwake binary.
RTCWAKE_BIN: str = "/usr/sbin/rtcwake"
# State file written by wake_alarm; read here to check for workout skip.
WAKE_STATE_FILE = Path(__file__).resolve().parent.parent / "wake_alarm" / "wake_state.json"

View File

@ -6,7 +6,7 @@ from datetime import datetime, timezone
import json import json
import logging import logging
from python_pkg.screen_locker._constants import ( from screen_locker._constants import (
EARLY_BIRD_END_HOUR, EARLY_BIRD_END_HOUR,
EARLY_BIRD_END_MINUTE, EARLY_BIRD_END_MINUTE,
EARLY_BIRD_START_HOUR, EARLY_BIRD_START_HOUR,

View File

@ -1,19 +1,80 @@
"""HMAC-based integrity checking — re-exports from shared package.""" """HMAC-based integrity checking for signed state entries."""
from __future__ import annotations from __future__ import annotations
from python_pkg.shared.log_integrity import ( import hashlib
HMAC_KEY_FILE, import hmac
_generate_hmac_key, import json
_load_hmac_key, import logging
compute_entry_hmac, from pathlib import Path
verify_entry_hmac, import secrets
)
__all__ = [ _logger = logging.getLogger(__name__)
"HMAC_KEY_FILE",
"_generate_hmac_key", # HMAC key for signing state entries (root-owned, 0600)
"_load_hmac_key", HMAC_KEY_FILE = Path("/etc/workout-locker/hmac.key")
"compute_entry_hmac",
"verify_entry_hmac",
] def _load_hmac_key() -> bytes | None:
"""Load HMAC key from the root-owned key file.
Returns the key bytes, or None if the file cannot be read.
"""
try:
return HMAC_KEY_FILE.read_bytes().strip()
except OSError:
_logger.warning("Cannot read HMAC key from %s", HMAC_KEY_FILE)
return None
def _generate_hmac_key() -> bytes | None:
"""Generate a new HMAC key and write it to the key file.
The key file must be writable (requires root or setup script).
Returns the new key bytes, or None on failure.
"""
key = secrets.token_bytes(32)
try:
HMAC_KEY_FILE.parent.mkdir(parents=True, exist_ok=True)
HMAC_KEY_FILE.write_bytes(key)
except OSError:
_logger.warning("Cannot write HMAC key to %s", HMAC_KEY_FILE)
return None
return key
def compute_entry_hmac(entry_data: dict[str, object]) -> str | None:
"""Compute HMAC-SHA256 for a state entry.
Args:
entry_data: The entry dict (without the 'hmac' field).
Returns:
Hex-encoded HMAC string, or None if the key is unavailable.
"""
key = _load_hmac_key()
if key is None:
return None
payload = json.dumps(entry_data, sort_keys=True, separators=(",", ":"))
return hmac.new(key, payload.encode(), hashlib.sha256).hexdigest()
def verify_entry_hmac(entry: dict[str, object]) -> bool:
"""Verify HMAC signature of a state entry.
Args:
entry: The full entry dict including the 'hmac' field.
Returns:
True if the HMAC is valid, False if invalid or key unavailable.
"""
stored_hmac = entry.get("hmac")
if not isinstance(stored_hmac, str):
return False
key = _load_hmac_key()
if key is None:
return False
entry_without_hmac = {k: v for k, v in entry.items() if k != "hmac"}
payload = json.dumps(entry_without_hmac, sort_keys=True, separators=(",", ":"))
expected = hmac.new(key, payload.encode(), hashlib.sha256).hexdigest()
return hmac.compare_digest(stored_hmac, expected)

View File

@ -17,12 +17,12 @@ import subprocess
import tempfile import tempfile
import time import time
from python_pkg.screen_locker._constants import ( from screen_locker._constants import (
ADB_TIMEOUT, ADB_TIMEOUT,
MIN_WORKOUT_DURATION_MINUTES, MIN_WORKOUT_DURATION_MINUTES,
STRONGLIFTS_DB_REMOTE, STRONGLIFTS_DB_REMOTE,
) )
from python_pkg.screen_locker._time_check import check_clock_skew from screen_locker._time_check import check_clock_skew
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)

View File

@ -8,12 +8,12 @@ import json
import logging import logging
import subprocess import subprocess
from python_pkg.screen_locker._constants import ( from screen_locker._constants import (
ADJUST_SHUTDOWN_SCRIPT, ADJUST_SHUTDOWN_SCRIPT,
SHUTDOWN_CONFIG_FILE, SHUTDOWN_CONFIG_FILE,
SICK_DAY_STATE_FILE, SICK_DAY_STATE_FILE,
) )
from python_pkg.wake_alarm._constants import ( from screen_locker._constants import (
ALARM_DAYS, ALARM_DAYS,
RTCWAKE_BIN, RTCWAKE_BIN,
WAKE_AFTER_HOURS, WAKE_AFTER_HOURS,

View File

@ -7,8 +7,8 @@ import logging
import tkinter as tk import tkinter as tk
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from python_pkg.screen_locker import _sick_tracker from screen_locker import _sick_tracker
from python_pkg.screen_locker._constants import ( from screen_locker._constants import (
COMMITMENT_PROMPT_TIMEOUT_SECONDS, COMMITMENT_PROMPT_TIMEOUT_SECONDS,
SICK_COMMITMENT_FORCED_READ_SECONDS, SICK_COMMITMENT_FORCED_READ_SECONDS,
SICK_JUSTIFICATION_MIN_CHARS, SICK_JUSTIFICATION_MIN_CHARS,
@ -17,7 +17,7 @@ from python_pkg.screen_locker._constants import (
if TYPE_CHECKING: if TYPE_CHECKING:
from collections.abc import Callable from collections.abc import Callable
from python_pkg.screen_locker._sick_tracker import SickHistory from screen_locker._sick_tracker import SickHistory
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)

View File

@ -12,7 +12,7 @@ import json
import logging import logging
from typing import Any from typing import Any
from python_pkg.screen_locker._constants import ( from screen_locker._constants import (
SICK_BUDGET_PER_7_DAYS, SICK_BUDGET_PER_7_DAYS,
SICK_BUDGET_PER_30_DAYS, SICK_BUDGET_PER_30_DAYS,
SICK_BUDGET_PER_90_DAYS, SICK_BUDGET_PER_90_DAYS,
@ -23,7 +23,7 @@ from python_pkg.screen_locker._constants import (
SICK_LOCKOUT_MULTIPLIER_PER_RECENT, SICK_LOCKOUT_MULTIPLIER_PER_RECENT,
SICK_LOCKOUT_SECONDS, SICK_LOCKOUT_SECONDS,
) )
from python_pkg.shared.log_integrity import compute_entry_hmac from screen_locker._log_integrity import compute_entry_hmac
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)

View File

@ -7,7 +7,7 @@ import socket
import struct import struct
import time import time
from python_pkg.screen_locker._constants import MAX_CLOCK_SKEW_SECONDS from screen_locker._constants import MAX_CLOCK_SKEW_SECONDS
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)

View File

@ -5,13 +5,13 @@ from __future__ import annotations
from concurrent.futures import ThreadPoolExecutor # pylint: disable=no-name-in-module from concurrent.futures import ThreadPoolExecutor # pylint: disable=no-name-in-module
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from python_pkg.screen_locker import _sick_tracker from screen_locker import _sick_tracker
from python_pkg.screen_locker._constants import ( from screen_locker._constants import (
NO_PHONE_EXTRA_LOCKOUT_SECONDS, NO_PHONE_EXTRA_LOCKOUT_SECONDS,
PHONE_PENALTY_DELAY_DEMO, PHONE_PENALTY_DELAY_DEMO,
PHONE_PENALTY_DELAY_PRODUCTION, PHONE_PENALTY_DELAY_PRODUCTION,
) )
from python_pkg.screen_locker._weekly_check import ( from screen_locker._weekly_check import (
WEEKLY_WORKOUT_MINIMUM, WEEKLY_WORKOUT_MINIMUM,
count_weekly_workouts, count_weekly_workouts,
) )

View File

@ -0,0 +1,63 @@
"""Read wake-alarm state to check if a workout skip was earned today.
This module reads the JSON state file written by the companion wake_alarm
service. It does not import wake_alarm directly the two packages
communicate only through the shared state file on disk.
The state file path defaults to WAKE_STATE_FILE in _constants.py, which
points to the sibling wake_alarm package directory. Override it in tests
by patching ``screen_locker._wake_state.WAKE_STATE_FILE``.
"""
from __future__ import annotations
from datetime import datetime, timezone
import json
import logging
from screen_locker._constants import WAKE_STATE_FILE
from screen_locker._log_integrity import verify_entry_hmac
_logger = logging.getLogger(__name__)
def _today_str() -> str:
"""Return today's date as YYYY-MM-DD in UTC."""
return datetime.now(tz=timezone.utc).strftime("%Y-%m-%d")
def load_wake_state() -> dict[str, object] | None:
"""Load and verify today's wake state.
Returns the state dict if it exists, is valid (HMAC OK), and is
for today. Returns None otherwise.
"""
if not WAKE_STATE_FILE.exists():
return None
try:
with WAKE_STATE_FILE.open() as f:
state = json.load(f)
except (OSError, json.JSONDecodeError):
_logger.warning("Cannot read wake state file")
return None
if not isinstance(state, dict):
return None
if state.get("date") != _today_str():
return None
if not verify_entry_hmac(state):
_logger.warning("Wake state HMAC verification failed")
return None
return state
def has_workout_skip_today() -> bool:
"""Check if the user earned a workout skip for today."""
state = load_wake_state()
if state is None:
return False
return bool(state.get("skip_workout"))

View File

@ -14,8 +14,8 @@ import sys
import tkinter as tk import tkinter as tk
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from python_pkg.screen_locker import _sick_tracker from screen_locker import _sick_tracker
from python_pkg.screen_locker._constants import ( from screen_locker._constants import (
EARLY_BIRD_END_HOUR, EARLY_BIRD_END_HOUR,
EARLY_BIRD_END_MINUTE, EARLY_BIRD_END_MINUTE,
EARLY_BIRD_START_HOUR, EARLY_BIRD_START_HOUR,
@ -28,23 +28,23 @@ from python_pkg.screen_locker._constants import (
SICK_LOCKOUT_SECONDS, SICK_LOCKOUT_SECONDS,
STRONGLIFTS_DB_REMOTE, STRONGLIFTS_DB_REMOTE,
) )
from python_pkg.screen_locker._early_bird import EarlyBirdMixin from screen_locker._early_bird import EarlyBirdMixin
from python_pkg.screen_locker._log_integrity import ( from screen_locker._log_integrity import (
_load_hmac_key, _load_hmac_key,
compute_entry_hmac, compute_entry_hmac,
verify_entry_hmac, verify_entry_hmac,
) )
from python_pkg.screen_locker._phone_verification import PhoneVerificationMixin from screen_locker._phone_verification import PhoneVerificationMixin
from python_pkg.screen_locker._shutdown import ShutdownMixin from screen_locker._shutdown import ShutdownMixin
from python_pkg.screen_locker._sick_dialog import SickDialogMixin from screen_locker._sick_dialog import SickDialogMixin
from python_pkg.screen_locker._ui_flows import UIFlowsMixin from screen_locker._ui_flows import UIFlowsMixin
from python_pkg.screen_locker._weekly_check import ( from screen_locker._weekly_check import (
WEEKLY_WORKOUT_MINIMUM, WEEKLY_WORKOUT_MINIMUM,
has_weekly_minimum, has_weekly_minimum,
is_relaxed_day, is_relaxed_day,
) )
from python_pkg.screen_locker._window_setup import WindowSetupMixin from screen_locker._window_setup import WindowSetupMixin
from python_pkg.wake_alarm._state import has_workout_skip_today from screen_locker._wake_state import has_workout_skip_today
if TYPE_CHECKING: if TYPE_CHECKING:
from collections.abc import Callable from collections.abc import Callable

View File

@ -17,7 +17,7 @@ from unittest.mock import MagicMock, patch
import pytest import pytest
from python_pkg.screen_locker.screen_lock import ScreenLocker from screen_locker.screen_lock import ScreenLocker
if TYPE_CHECKING: if TYPE_CHECKING:
from collections.abc import Generator, Iterator from collections.abc import Generator, Iterator
@ -51,9 +51,9 @@ def _block_real_tk_and_exit() -> Iterator[None]:
mock = _make_mock_tk() mock = _make_mock_tk()
with ( with (
patch("python_pkg.screen_locker.screen_lock.tk", mock), patch("screen_locker.screen_lock.tk", mock),
patch("python_pkg.screen_locker._sick_dialog.tk", mock), patch("screen_locker._sick_dialog.tk", mock),
patch("python_pkg.screen_locker.screen_lock.sys.exit"), patch("screen_locker.screen_lock.sys.exit"),
): ):
yield yield
@ -70,10 +70,10 @@ def mock_subprocess_run() -> Generator[MagicMock]:
""" """
with ( with (
patch( patch(
"python_pkg.screen_locker._window_setup.shutil.which", "screen_locker._window_setup.shutil.which",
return_value="/usr/bin/setxkbmap", return_value="/usr/bin/setxkbmap",
), ),
patch("python_pkg.screen_locker._window_setup.subprocess.run") as mock, patch("screen_locker._window_setup.subprocess.run") as mock,
): ):
yield mock yield mock
@ -84,11 +84,11 @@ def _isolate_sick_history(tmp_path: Path) -> Iterator[None]:
target = tmp_path / "sick_history.json" target = tmp_path / "sick_history.json"
with ( with (
patch( patch(
"python_pkg.screen_locker._sick_tracker.SICK_HISTORY_FILE", "screen_locker._sick_tracker.SICK_HISTORY_FILE",
target, target,
), ),
patch( patch(
"python_pkg.screen_locker._constants.SICK_HISTORY_FILE", "screen_locker._constants.SICK_HISTORY_FILE",
target, target,
), ),
): ):
@ -100,7 +100,7 @@ def _isolate_scheduled_skips(tmp_path: Path) -> Iterator[None]:
"""Redirect SCHEDULED_SKIPS_FILE to tmp_path so tests use a clean file.""" """Redirect SCHEDULED_SKIPS_FILE to tmp_path so tests use a clean file."""
target = tmp_path / "scheduled_skips.json" target = tmp_path / "scheduled_skips.json"
with patch( with patch(
"python_pkg.screen_locker.screen_lock.SCHEDULED_SKIPS_FILE", "screen_locker.screen_lock.SCHEDULED_SKIPS_FILE",
target, target,
): ):
yield yield
@ -117,11 +117,11 @@ def _mock_weekly_logic() -> Iterator[None]:
""" """
with ( with (
patch( patch(
"python_pkg.screen_locker.screen_lock.is_relaxed_day", "screen_locker.screen_lock.is_relaxed_day",
return_value=False, return_value=False,
), ),
patch( patch(
"python_pkg.screen_locker.screen_lock.has_weekly_minimum", "screen_locker.screen_lock.has_weekly_minimum",
return_value=False, return_value=False,
), ),
): ):
@ -131,7 +131,7 @@ def _mock_weekly_logic() -> Iterator[None]:
@pytest.fixture @pytest.fixture
def mock_tk() -> Generator[MagicMock]: def mock_tk() -> Generator[MagicMock]:
"""Mock tkinter module for testing without display.""" """Mock tkinter module for testing without display."""
with patch("python_pkg.screen_locker.screen_lock.tk") as mock: with patch("screen_locker.screen_lock.tk") as mock:
# Set up Tk root mock # Set up Tk root mock
mock_root = MagicMock() mock_root = MagicMock()
mock_root.winfo_screenwidth.return_value = 1920 mock_root.winfo_screenwidth.return_value = 1920
@ -152,7 +152,7 @@ def mock_tk() -> Generator[MagicMock]:
@pytest.fixture @pytest.fixture
def mock_sys_exit() -> Generator[MagicMock]: def mock_sys_exit() -> Generator[MagicMock]:
"""Mock sys.exit to prevent test termination.""" """Mock sys.exit to prevent test termination."""
with patch("python_pkg.screen_locker.screen_lock.sys.exit") as mock: with patch("screen_locker.screen_lock.sys.exit") as mock:
yield mock yield mock
@ -223,9 +223,9 @@ def create_locker_relaxed_day(
patch.object(ScreenLocker, "_is_early_bird_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, "_is_early_bird_time", return_value=False),
patch.object(ScreenLocker, "_try_auto_upgrade_early_bird", return_value=False), patch.object(ScreenLocker, "_try_auto_upgrade_early_bird", return_value=False),
patch("python_pkg.screen_locker.screen_lock.is_relaxed_day", return_value=True), patch("screen_locker.screen_lock.is_relaxed_day", return_value=True),
patch( patch(
"python_pkg.screen_locker.screen_lock.has_weekly_minimum", "screen_locker.screen_lock.has_weekly_minimum",
return_value=False, return_value=False,
), ),
patch.object(ScreenLocker, "_start_phone_check"), patch.object(ScreenLocker, "_start_phone_check"),

View File

@ -9,8 +9,8 @@ import time
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
from python_pkg.screen_locker.screen_lock import STRONGLIFTS_DB_REMOTE from screen_locker.screen_lock import STRONGLIFTS_DB_REMOTE
from python_pkg.screen_locker.tests.conftest import create_locker from screen_locker.tests.conftest import create_locker
if TYPE_CHECKING: if TYPE_CHECKING:
from pathlib import Path from pathlib import Path
@ -29,7 +29,7 @@ class TestRunAdb:
locker = create_locker(mock_tk, tmp_path) locker = create_locker(mock_tk, tmp_path)
mock_result = MagicMock(returncode=0, stdout="ok\n") mock_result = MagicMock(returncode=0, stdout="ok\n")
with patch( with patch(
"python_pkg.screen_locker._phone_verification.subprocess.run", "screen_locker._phone_verification.subprocess.run",
return_value=mock_result, return_value=mock_result,
) as mock_run: ) as mock_run:
success, output = locker._run_adb(["devices"]) success, output = locker._run_adb(["devices"])
@ -48,7 +48,7 @@ class TestRunAdb:
locker = create_locker(mock_tk, tmp_path) locker = create_locker(mock_tk, tmp_path)
mock_result = MagicMock(returncode=1, stdout="") mock_result = MagicMock(returncode=1, stdout="")
with patch( with patch(
"python_pkg.screen_locker._phone_verification.subprocess.run", "screen_locker._phone_verification.subprocess.run",
return_value=mock_result, return_value=mock_result,
): ):
success, _output = locker._run_adb(["devices"]) success, _output = locker._run_adb(["devices"])
@ -64,7 +64,7 @@ class TestRunAdb:
"""Test ADB binary not found.""" """Test ADB binary not found."""
locker = create_locker(mock_tk, tmp_path) locker = create_locker(mock_tk, tmp_path)
with patch( with patch(
"python_pkg.screen_locker._phone_verification.subprocess.run", "screen_locker._phone_verification.subprocess.run",
side_effect=FileNotFoundError("adb not found"), side_effect=FileNotFoundError("adb not found"),
): ):
success, output = locker._run_adb(["devices"]) success, output = locker._run_adb(["devices"])
@ -81,7 +81,7 @@ class TestRunAdb:
"""Test ADB OSError.""" """Test ADB OSError."""
locker = create_locker(mock_tk, tmp_path) locker = create_locker(mock_tk, tmp_path)
with patch( with patch(
"python_pkg.screen_locker._phone_verification.subprocess.run", "screen_locker._phone_verification.subprocess.run",
side_effect=OSError("permission denied"), side_effect=OSError("permission denied"),
): ):
success, output = locker._run_adb(["devices"]) success, output = locker._run_adb(["devices"])
@ -98,7 +98,7 @@ class TestRunAdb:
"""Test ADB command timeout.""" """Test ADB command timeout."""
locker = create_locker(mock_tk, tmp_path) locker = create_locker(mock_tk, tmp_path)
with patch( with patch(
"python_pkg.screen_locker._phone_verification.subprocess.run", "screen_locker._phone_verification.subprocess.run",
side_effect=subprocess.TimeoutExpired("adb", 15), side_effect=subprocess.TimeoutExpired("adb", 15),
): ):
success, output = locker._run_adb(["devices"]) success, output = locker._run_adb(["devices"])

View File

@ -11,7 +11,7 @@ from typing import TYPE_CHECKING
import pytest import pytest
from python_pkg.screen_locker.tests.conftest import create_locker from screen_locker.tests.conftest import create_locker
if TYPE_CHECKING: if TYPE_CHECKING:
from pathlib import Path from pathlib import Path

View File

@ -10,8 +10,8 @@ from unittest.mock import MagicMock, patch
import pytest import pytest
from python_pkg.screen_locker.screen_lock import ScreenLocker from screen_locker.screen_lock import ScreenLocker
from python_pkg.screen_locker.tests.conftest import ( from screen_locker.tests.conftest import (
create_locker, create_locker,
create_locker_early_bird, create_locker_early_bird,
) )
@ -222,7 +222,7 @@ class TestSaveEarlyBirdLog:
locker = create_locker(mock_tk, tmp_path) locker = create_locker(mock_tk, tmp_path)
locker.log_file = log_file locker.log_file = log_file
with patch( with patch(
"python_pkg.screen_locker.screen_lock.compute_entry_hmac", "screen_locker.screen_lock.compute_entry_hmac",
return_value=None, return_value=None,
): ):
locker._save_early_bird_log() locker._save_early_bird_log()
@ -258,7 +258,7 @@ class TestTryAutoUpgradeEarlyBird:
MagicMock(return_value=True), MagicMock(return_value=True),
) )
with patch( with patch(
"python_pkg.screen_locker.screen_lock.compute_entry_hmac", "screen_locker.screen_lock.compute_entry_hmac",
return_value=None, return_value=None,
): ):
result = locker._try_auto_upgrade_early_bird() result = locker._try_auto_upgrade_early_bird()
@ -334,7 +334,7 @@ class TestHasLoggedTodayEarlyBird:
locker = create_locker(mock_tk, tmp_path) locker = create_locker(mock_tk, tmp_path)
locker.log_file = log_file locker.log_file = log_file
with patch( with patch(
"python_pkg.screen_locker.screen_lock.verify_entry_hmac", "screen_locker.screen_lock.verify_entry_hmac",
return_value=True, return_value=True,
): ):
assert locker.has_logged_today() is False assert locker.has_logged_today() is False
@ -366,7 +366,7 @@ class TestInitEarlyBirdFlow:
patch.object(ScreenLocker, "_start_phone_check"), patch.object(ScreenLocker, "_start_phone_check"),
patch.object(ScreenLocker, "_start_verify_workout_check"), patch.object(ScreenLocker, "_start_verify_workout_check"),
patch( patch(
"python_pkg.screen_locker.screen_lock.has_workout_skip_today", "screen_locker.screen_lock.has_workout_skip_today",
return_value=False, return_value=False,
), ),
pytest.raises(SystemExit), pytest.raises(SystemExit),

View File

@ -10,8 +10,8 @@ from unittest.mock import MagicMock, patch
import pytest import pytest
from python_pkg.screen_locker.screen_lock import _assert_not_under_pytest from screen_locker.screen_lock import _assert_not_under_pytest
from python_pkg.screen_locker.tests.conftest import create_locker from screen_locker.tests.conftest import create_locker
if TYPE_CHECKING: if TYPE_CHECKING:
from pathlib import Path from pathlib import Path
@ -23,7 +23,7 @@ class TestAssertNotUnderPytest:
def test_raises_when_tk_is_real(self) -> None: def test_raises_when_tk_is_real(self) -> None:
"""Guard fires if tk.Tk is the real tkinter class under pytest.""" """Guard fires if tk.Tk is the real tkinter class under pytest."""
with ( with (
patch("python_pkg.screen_locker.screen_lock.tk", tk), patch("screen_locker.screen_lock.tk", tk),
pytest.raises(RuntimeError, match="SAFETY"), pytest.raises(RuntimeError, match="SAFETY"),
): ):
_assert_not_under_pytest() _assert_not_under_pytest()
@ -128,7 +128,7 @@ class TestHasLoggedToday:
locker = create_locker(mock_tk, tmp_path) locker = create_locker(mock_tk, tmp_path)
locker.log_file = log_file locker.log_file = log_file
with patch( with patch(
"python_pkg.screen_locker.screen_lock.verify_entry_hmac", "screen_locker.screen_lock.verify_entry_hmac",
return_value=True, return_value=True,
): ):
assert locker.has_logged_today() is True assert locker.has_logged_today() is True
@ -149,7 +149,7 @@ class TestHasLoggedToday:
locker = create_locker(mock_tk, tmp_path) locker = create_locker(mock_tk, tmp_path)
locker.log_file = log_file locker.log_file = log_file
with patch( with patch(
"python_pkg.screen_locker.screen_lock.verify_entry_hmac", "screen_locker.screen_lock.verify_entry_hmac",
return_value=False, return_value=False,
): ):
assert locker.has_logged_today() is False assert locker.has_logged_today() is False
@ -171,11 +171,11 @@ class TestHasLoggedToday:
locker.log_file = log_file locker.log_file = log_file
with ( with (
patch( patch(
"python_pkg.screen_locker.screen_lock.verify_entry_hmac", "screen_locker.screen_lock.verify_entry_hmac",
return_value=False, return_value=False,
), ),
patch( patch(
"python_pkg.screen_locker.screen_lock._load_hmac_key", "screen_locker.screen_lock._load_hmac_key",
return_value=None, return_value=None,
), ),
): ):
@ -198,11 +198,11 @@ class TestHasLoggedToday:
locker.log_file = log_file locker.log_file = log_file
with ( with (
patch( patch(
"python_pkg.screen_locker.screen_lock.verify_entry_hmac", "screen_locker.screen_lock.verify_entry_hmac",
return_value=False, return_value=False,
), ),
patch( patch(
"python_pkg.screen_locker.screen_lock._load_hmac_key", "screen_locker.screen_lock._load_hmac_key",
return_value=b"secret-key", return_value=b"secret-key",
), ),
): ):
@ -238,7 +238,7 @@ class TestSaveWorkoutLog:
locker.log_file = log_file locker.log_file = log_file
locker.workout_data = {"type": "running"} locker.workout_data = {"type": "running"}
with patch( with patch(
"python_pkg.screen_locker.screen_lock.compute_entry_hmac", "screen_locker.screen_lock.compute_entry_hmac",
return_value="abc123", return_value="abc123",
): ):
locker.save_workout_log() locker.save_workout_log()
@ -263,7 +263,7 @@ class TestSaveWorkoutLog:
locker.log_file = log_file locker.log_file = log_file
locker.workout_data = {"type": "running"} locker.workout_data = {"type": "running"}
with patch( with patch(
"python_pkg.screen_locker.screen_lock.compute_entry_hmac", "screen_locker.screen_lock.compute_entry_hmac",
return_value=None, return_value=None,
): ):
locker.save_workout_log() locker.save_workout_log()
@ -287,7 +287,7 @@ class TestSaveWorkoutLog:
locker.log_file = log_file locker.log_file = log_file
locker.workout_data = {"type": "strength"} locker.workout_data = {"type": "strength"}
with patch( with patch(
"python_pkg.screen_locker.screen_lock.compute_entry_hmac", "screen_locker.screen_lock.compute_entry_hmac",
return_value="sig", return_value="sig",
): ):
locker.save_workout_log() locker.save_workout_log()
@ -312,7 +312,7 @@ class TestSaveWorkoutLog:
locker.log_file = log_file locker.log_file = log_file
locker.workout_data = {"type": "running"} locker.workout_data = {"type": "running"}
with patch( with patch(
"python_pkg.screen_locker.screen_lock.compute_entry_hmac", "screen_locker.screen_lock.compute_entry_hmac",
return_value="sig", return_value="sig",
): ):
locker.save_workout_log() locker.save_workout_log()
@ -335,7 +335,7 @@ class TestSaveWorkoutLog:
locker.log_file = log_file locker.log_file = log_file
locker.workout_data = {"type": "running"} locker.workout_data = {"type": "running"}
with patch( with patch(
"python_pkg.screen_locker.screen_lock.compute_entry_hmac", "screen_locker.screen_lock.compute_entry_hmac",
return_value="sig", return_value="sig",
): ):
# Should not raise, just log warning # Should not raise, just log warning

View File

@ -10,8 +10,8 @@ from unittest.mock import MagicMock, patch
import pytest import pytest
from python_pkg.screen_locker.screen_lock import ScreenLocker from screen_locker.screen_lock import ScreenLocker
from python_pkg.screen_locker.tests.conftest import create_locker from screen_locker.tests.conftest import create_locker
if TYPE_CHECKING: if TYPE_CHECKING:
from pathlib import Path from pathlib import Path
@ -59,7 +59,7 @@ class TestAutoUpgradeSickDay:
return_value=True, return_value=True,
) as mock_adjust, ) as mock_adjust,
patch( patch(
"python_pkg.screen_locker.screen_lock.compute_entry_hmac", "screen_locker.screen_lock.compute_entry_hmac",
return_value="sig", return_value="sig",
), ),
): ):

View File

@ -8,14 +8,14 @@ import json
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from unittest.mock import patch from unittest.mock import patch
from python_pkg.screen_locker._log_integrity import ( from screen_locker._log_integrity import (
_generate_hmac_key, _generate_hmac_key,
_load_hmac_key, _load_hmac_key,
compute_entry_hmac, compute_entry_hmac,
verify_entry_hmac, verify_entry_hmac,
) )
_HMAC_KEY_FILE_PATH = "python_pkg.shared.log_integrity.HMAC_KEY_FILE" _HMAC_KEY_FILE_PATH = "screen_locker._log_integrity.HMAC_KEY_FILE"
if TYPE_CHECKING: if TYPE_CHECKING:
from pathlib import Path from pathlib import Path

View File

@ -6,7 +6,7 @@ from __future__ import annotations
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
from python_pkg.screen_locker.tests.conftest import create_locker from screen_locker.tests.conftest import create_locker
if TYPE_CHECKING: if TYPE_CHECKING:
from pathlib import Path from pathlib import Path
@ -55,7 +55,7 @@ class TestVerifyPhoneWorkout:
) )
with patch( with patch(
"python_pkg.screen_locker._phone_verification.check_clock_skew", "screen_locker._phone_verification.check_clock_skew",
return_value=(True, "Clock OK"), return_value=(True, "Clock OK"),
): ):
status, message = locker._verify_phone_workout() status, message = locker._verify_phone_workout()
@ -90,7 +90,7 @@ class TestVerifyPhoneWorkout:
) )
with patch( with patch(
"python_pkg.screen_locker._phone_verification.check_clock_skew", "screen_locker._phone_verification.check_clock_skew",
return_value=(True, "Clock OK"), return_value=(True, "Clock OK"),
): ):
status, message = locker._verify_phone_workout() status, message = locker._verify_phone_workout()
@ -138,7 +138,7 @@ class TestVerifyPhoneWorkout:
) )
with patch( with patch(
"python_pkg.screen_locker._phone_verification.check_clock_skew", "screen_locker._phone_verification.check_clock_skew",
return_value=(True, "Clock OK"), return_value=(True, "Clock OK"),
): ):
status, message = locker._verify_phone_workout() status, message = locker._verify_phone_workout()
@ -162,7 +162,7 @@ class TestVerifyPhoneWorkout:
) )
with patch( with patch(
"python_pkg.screen_locker._phone_verification.check_clock_skew", "screen_locker._phone_verification.check_clock_skew",
return_value=(True, "Clock OK"), return_value=(True, "Clock OK"),
): ):
status, _ = locker._verify_phone_workout() status, _ = locker._verify_phone_workout()
@ -189,7 +189,7 @@ class TestVerifyPhoneWorkout:
) )
with patch( with patch(
"python_pkg.screen_locker._phone_verification.check_clock_skew", "screen_locker._phone_verification.check_clock_skew",
return_value=(True, "Clock OK"), return_value=(True, "Clock OK"),
): ):
status, message = locker._verify_phone_workout() status, message = locker._verify_phone_workout()
@ -207,7 +207,7 @@ class TestVerifyPhoneWorkout:
locker = create_locker(mock_tk, tmp_path) locker = create_locker(mock_tk, tmp_path)
with patch( with patch(
"python_pkg.screen_locker._phone_verification.check_clock_skew", "screen_locker._phone_verification.check_clock_skew",
return_value=(False, "System clock is 600s ahead"), return_value=(False, "System clock is 600s ahead"),
): ):
status, message = locker._verify_phone_workout() status, message = locker._verify_phone_workout()
@ -245,7 +245,7 @@ class TestVerifyPhoneWorkout:
) )
with patch( with patch(
"python_pkg.screen_locker._phone_verification.check_clock_skew", "screen_locker._phone_verification.check_clock_skew",
return_value=(True, "Clock OK"), return_value=(True, "Clock OK"),
): ):
status, message = locker._verify_phone_workout() status, message = locker._verify_phone_workout()
@ -288,7 +288,7 @@ class TestVerifyPhoneWorkout:
) )
with patch( with patch(
"python_pkg.screen_locker._phone_verification.check_clock_skew", "screen_locker._phone_verification.check_clock_skew",
return_value=(True, "Clock OK"), return_value=(True, "Clock OK"),
): ):
status, message = locker._verify_phone_workout() status, message = locker._verify_phone_workout()

View File

@ -6,12 +6,12 @@ from __future__ import annotations
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from unittest.mock import MagicMock from unittest.mock import MagicMock
from python_pkg.screen_locker._constants import NO_PHONE_EXTRA_LOCKOUT_SECONDS from screen_locker._constants import NO_PHONE_EXTRA_LOCKOUT_SECONDS
from python_pkg.screen_locker.screen_lock import ( from screen_locker.screen_lock import (
PHONE_PENALTY_DELAY_DEMO, PHONE_PENALTY_DELAY_DEMO,
PHONE_PENALTY_DELAY_PRODUCTION, PHONE_PENALTY_DELAY_PRODUCTION,
) )
from python_pkg.screen_locker.tests.conftest import create_locker from screen_locker.tests.conftest import create_locker
if TYPE_CHECKING: if TYPE_CHECKING:
from pathlib import Path from pathlib import Path

View File

@ -5,7 +5,7 @@ from __future__ import annotations
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
from python_pkg.screen_locker.tests.conftest import create_locker from screen_locker.tests.conftest import create_locker
if TYPE_CHECKING: if TYPE_CHECKING:
from pathlib import Path from pathlib import Path
@ -144,7 +144,7 @@ class TestGetLocalSubnetPrefix:
mock_sock.__enter__ = MagicMock(return_value=mock_sock) mock_sock.__enter__ = MagicMock(return_value=mock_sock)
mock_sock.__exit__ = MagicMock(return_value=False) mock_sock.__exit__ = MagicMock(return_value=False)
with patch( with patch(
"python_pkg.screen_locker._phone_verification.socket.socket", "screen_locker._phone_verification.socket.socket",
return_value=mock_sock, return_value=mock_sock,
): ):
result = locker._get_local_subnet_prefix() result = locker._get_local_subnet_prefix()
@ -159,7 +159,7 @@ class TestGetLocalSubnetPrefix:
"""Test returns None when socket raises OSError.""" """Test returns None when socket raises OSError."""
locker = create_locker(mock_tk, tmp_path) locker = create_locker(mock_tk, tmp_path)
with patch( with patch(
"python_pkg.screen_locker._phone_verification.socket.socket", "screen_locker._phone_verification.socket.socket",
side_effect=OSError("no network"), side_effect=OSError("no network"),
): ):
result = locker._get_local_subnet_prefix() result = locker._get_local_subnet_prefix()
@ -194,7 +194,7 @@ class TestTryWirelessReconnect:
patch.object(locker, "_try_adb_connect", return_value=True), patch.object(locker, "_try_adb_connect", return_value=True),
patch.object(locker, "_has_adb_device", return_value=True), patch.object(locker, "_has_adb_device", return_value=True),
patch( patch(
"python_pkg.screen_locker._phone_verification.socket.create_connection", "screen_locker._phone_verification.socket.create_connection",
) as mock_conn, ) as mock_conn,
): ):
mock_sock = MagicMock() mock_sock = MagicMock()
@ -215,7 +215,7 @@ class TestTryWirelessReconnect:
with ( with (
patch.object(locker, "_get_local_subnet_prefix", return_value="192.168.1"), patch.object(locker, "_get_local_subnet_prefix", return_value="192.168.1"),
patch( patch(
"python_pkg.screen_locker._phone_verification.socket.create_connection", "screen_locker._phone_verification.socket.create_connection",
side_effect=OSError("refused"), side_effect=OSError("refused"),
), ),
): ):
@ -235,7 +235,7 @@ class TestTryWirelessReconnect:
patch.object(locker, "_try_adb_connect", return_value=True), patch.object(locker, "_try_adb_connect", return_value=True),
patch.object(locker, "_has_adb_device", return_value=False), patch.object(locker, "_has_adb_device", return_value=False),
patch( patch(
"python_pkg.screen_locker._phone_verification.socket.create_connection", "screen_locker._phone_verification.socket.create_connection",
) as mock_conn, ) as mock_conn,
): ):
mock_sock = MagicMock() mock_sock = MagicMock()
@ -257,7 +257,7 @@ class TestTryWirelessReconnect:
patch.object(locker, "_get_local_subnet_prefix", return_value="192.168.1"), patch.object(locker, "_get_local_subnet_prefix", return_value="192.168.1"),
patch.object(locker, "_try_adb_connect", return_value=False), patch.object(locker, "_try_adb_connect", return_value=False),
patch( patch(
"python_pkg.screen_locker._phone_verification.socket.create_connection", "screen_locker._phone_verification.socket.create_connection",
) as mock_conn, ) as mock_conn,
): ):
mock_sock = MagicMock() mock_sock = MagicMock()

View File

@ -9,12 +9,12 @@ from unittest.mock import MagicMock, patch
import pytest import pytest
from python_pkg.screen_locker.tests.conftest import create_locker from screen_locker.tests.conftest import create_locker
if TYPE_CHECKING: if TYPE_CHECKING:
from pathlib import Path from pathlib import Path
from python_pkg.screen_locker.screen_lock import ScreenLocker from screen_locker.screen_lock import ScreenLocker
class TestIsScheduledSkipToday: class TestIsScheduledSkipToday:
@ -33,7 +33,7 @@ class TestIsScheduledSkipToday:
locker = self._make_locker(mock_tk, tmp_path) locker = self._make_locker(mock_tk, tmp_path)
skip_file = tmp_path / "scheduled_skips.json" skip_file = tmp_path / "scheduled_skips.json"
with patch( with patch(
"python_pkg.screen_locker.screen_lock.SCHEDULED_SKIPS_FILE", "screen_locker.screen_lock.SCHEDULED_SKIPS_FILE",
skip_file, skip_file,
): ):
assert locker._is_scheduled_skip_today() is False assert locker._is_scheduled_skip_today() is False
@ -50,7 +50,7 @@ class TestIsScheduledSkipToday:
skip_file = tmp_path / "scheduled_skips.json" skip_file = tmp_path / "scheduled_skips.json"
skip_file.write_text(json.dumps([today])) skip_file.write_text(json.dumps([today]))
with patch( with patch(
"python_pkg.screen_locker.screen_lock.SCHEDULED_SKIPS_FILE", "screen_locker.screen_lock.SCHEDULED_SKIPS_FILE",
skip_file, skip_file,
): ):
assert locker._is_scheduled_skip_today() is True assert locker._is_scheduled_skip_today() is True
@ -66,7 +66,7 @@ class TestIsScheduledSkipToday:
skip_file = tmp_path / "scheduled_skips.json" skip_file = tmp_path / "scheduled_skips.json"
skip_file.write_text(json.dumps(["1999-01-01", "2000-06-15"])) skip_file.write_text(json.dumps(["1999-01-01", "2000-06-15"]))
with patch( with patch(
"python_pkg.screen_locker.screen_lock.SCHEDULED_SKIPS_FILE", "screen_locker.screen_lock.SCHEDULED_SKIPS_FILE",
skip_file, skip_file,
): ):
assert locker._is_scheduled_skip_today() is False assert locker._is_scheduled_skip_today() is False
@ -82,7 +82,7 @@ class TestIsScheduledSkipToday:
skip_file = tmp_path / "scheduled_skips.json" skip_file = tmp_path / "scheduled_skips.json"
skip_file.write_text("{not valid json}") skip_file.write_text("{not valid json}")
with patch( with patch(
"python_pkg.screen_locker.screen_lock.SCHEDULED_SKIPS_FILE", "screen_locker.screen_lock.SCHEDULED_SKIPS_FILE",
skip_file, skip_file,
): ):
assert locker._is_scheduled_skip_today() is False assert locker._is_scheduled_skip_today() is False
@ -99,7 +99,7 @@ class TestIsScheduledSkipToday:
skip_file.write_text("[]") skip_file.write_text("[]")
with ( with (
patch( patch(
"python_pkg.screen_locker.screen_lock.SCHEDULED_SKIPS_FILE", "screen_locker.screen_lock.SCHEDULED_SKIPS_FILE",
skip_file, skip_file,
), ),
patch("builtins.open", side_effect=OSError("permission denied")), patch("builtins.open", side_effect=OSError("permission denied")),
@ -117,7 +117,7 @@ class TestIsScheduledSkipToday:
skip_file = tmp_path / "scheduled_skips.json" skip_file = tmp_path / "scheduled_skips.json"
skip_file.write_text("[]") skip_file.write_text("[]")
with patch( with patch(
"python_pkg.screen_locker.screen_lock.SCHEDULED_SKIPS_FILE", "screen_locker.screen_lock.SCHEDULED_SKIPS_FILE",
skip_file, skip_file,
): ):
assert locker._is_scheduled_skip_today() is False assert locker._is_scheduled_skip_today() is False

View File

@ -6,7 +6,7 @@ import json
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
from python_pkg.screen_locker.tests.conftest import create_locker from screen_locker.tests.conftest import create_locker
if TYPE_CHECKING: if TYPE_CHECKING:
from pathlib import Path from pathlib import Path
@ -228,7 +228,7 @@ class TestSickModeUsedToday:
mock_file = MagicMock() mock_file = MagicMock()
mock_file.exists.return_value = False mock_file.exists.return_value = False
with patch( with patch(
"python_pkg.screen_locker._shutdown.SICK_DAY_STATE_FILE", "screen_locker._shutdown.SICK_DAY_STATE_FILE",
mock_file, mock_file,
): ):
assert locker._sick_mode_used_today() is False assert locker._sick_mode_used_today() is False
@ -243,7 +243,7 @@ class TestSickModeUsedToday:
locker = create_locker(mock_tk, tmp_path) locker = create_locker(mock_tk, tmp_path)
state_file = tmp_path / "state.json" state_file = tmp_path / "state.json"
with patch( with patch(
"python_pkg.screen_locker._shutdown.SICK_DAY_STATE_FILE", "screen_locker._shutdown.SICK_DAY_STATE_FILE",
state_file, state_file,
): ):
from datetime import datetime, timezone from datetime import datetime, timezone
@ -262,7 +262,7 @@ class TestSickModeUsedToday:
locker = create_locker(mock_tk, tmp_path) locker = create_locker(mock_tk, tmp_path)
state_file = tmp_path / "state.json" state_file = tmp_path / "state.json"
with patch( with patch(
"python_pkg.screen_locker._shutdown.SICK_DAY_STATE_FILE", "screen_locker._shutdown.SICK_DAY_STATE_FILE",
state_file, state_file,
): ):
state_file.write_text(json.dumps({"date": "2020-01-01"})) state_file.write_text(json.dumps({"date": "2020-01-01"}))
@ -278,7 +278,7 @@ class TestSickModeUsedToday:
locker = create_locker(mock_tk, tmp_path) locker = create_locker(mock_tk, tmp_path)
state_file = tmp_path / "state.json" state_file = tmp_path / "state.json"
with patch( with patch(
"python_pkg.screen_locker._shutdown.SICK_DAY_STATE_FILE", "screen_locker._shutdown.SICK_DAY_STATE_FILE",
state_file, state_file,
): ):
state_file.write_text("not json{{{") state_file.write_text("not json{{{")
@ -298,7 +298,7 @@ class TestSaveSickDayState:
locker = create_locker(mock_tk, tmp_path) locker = create_locker(mock_tk, tmp_path)
state_file = tmp_path / "state.json" state_file = tmp_path / "state.json"
with patch( with patch(
"python_pkg.screen_locker._shutdown.SICK_DAY_STATE_FILE", "screen_locker._shutdown.SICK_DAY_STATE_FILE",
state_file, state_file,
): ):
result = locker._save_sick_day_state("2026-03-21", 21, 20) result = locker._save_sick_day_state("2026-03-21", 21, 20)
@ -319,7 +319,7 @@ class TestSaveSickDayState:
mock_path = MagicMock() mock_path = MagicMock()
mock_path.open.side_effect = OSError("permission denied") mock_path.open.side_effect = OSError("permission denied")
with patch( with patch(
"python_pkg.screen_locker._shutdown.SICK_DAY_STATE_FILE", "screen_locker._shutdown.SICK_DAY_STATE_FILE",
mock_path, mock_path,
): ):
result = locker._save_sick_day_state("2026-03-21", 21, 20) result = locker._save_sick_day_state("2026-03-21", 21, 20)
@ -348,7 +348,7 @@ class TestLoadSickDayState:
) )
) )
with patch( with patch(
"python_pkg.screen_locker._shutdown.SICK_DAY_STATE_FILE", "screen_locker._shutdown.SICK_DAY_STATE_FILE",
state_file, state_file,
): ):
result = locker._load_sick_day_state() result = locker._load_sick_day_state()
@ -365,7 +365,7 @@ class TestLoadSickDayState:
state_file = tmp_path / "state.json" state_file = tmp_path / "state.json"
state_file.write_text(json.dumps({"date": "2026-03-20"})) state_file.write_text(json.dumps({"date": "2026-03-20"}))
with patch( with patch(
"python_pkg.screen_locker._shutdown.SICK_DAY_STATE_FILE", "screen_locker._shutdown.SICK_DAY_STATE_FILE",
state_file, state_file,
): ):
result = locker._load_sick_day_state() result = locker._load_sick_day_state()
@ -391,7 +391,7 @@ class TestWriteRestoredConfig:
locker, "_write_shutdown_config", return_value=True locker, "_write_shutdown_config", return_value=True
) as mock_write, ) as mock_write,
patch( patch(
"python_pkg.screen_locker._shutdown.SICK_DAY_STATE_FILE", "screen_locker._shutdown.SICK_DAY_STATE_FILE",
state_file, state_file,
), ),
): ):
@ -412,7 +412,7 @@ class TestWriteRestoredConfig:
with ( with (
patch.object(locker, "_read_shutdown_config", return_value=None), patch.object(locker, "_read_shutdown_config", return_value=None),
patch( patch(
"python_pkg.screen_locker._shutdown.SICK_DAY_STATE_FILE", "screen_locker._shutdown.SICK_DAY_STATE_FILE",
state_file, state_file,
), ),
): ):

View File

@ -7,8 +7,8 @@ import subprocess
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
from python_pkg.screen_locker._constants import ADJUST_SHUTDOWN_SCRIPT from screen_locker._constants import ADJUST_SHUTDOWN_SCRIPT
from python_pkg.screen_locker.tests.conftest import create_locker from screen_locker.tests.conftest import create_locker
if TYPE_CHECKING: if TYPE_CHECKING:
from pathlib import Path from pathlib import Path
@ -28,7 +28,7 @@ class TestRestoreOriginalConfigIfNeeded:
mock_file = MagicMock() mock_file = MagicMock()
mock_file.exists.return_value = False mock_file.exists.return_value = False
with patch( with patch(
"python_pkg.screen_locker._shutdown.SICK_DAY_STATE_FILE", "screen_locker._shutdown.SICK_DAY_STATE_FILE",
mock_file, mock_file,
): ):
locker._restore_original_config_if_needed() locker._restore_original_config_if_needed()
@ -53,7 +53,7 @@ class TestRestoreOriginalConfigIfNeeded:
) )
with ( with (
patch( patch(
"python_pkg.screen_locker._shutdown.SICK_DAY_STATE_FILE", "screen_locker._shutdown.SICK_DAY_STATE_FILE",
state_file, state_file,
), ),
patch.object(locker, "_write_restored_config") as mock_restore, patch.object(locker, "_write_restored_config") as mock_restore,
@ -84,7 +84,7 @@ class TestRestoreOriginalConfigIfNeeded:
) )
with ( with (
patch( patch(
"python_pkg.screen_locker._shutdown.SICK_DAY_STATE_FILE", "screen_locker._shutdown.SICK_DAY_STATE_FILE",
state_file, state_file,
), ),
patch.object(locker, "_write_restored_config") as mock_restore, patch.object(locker, "_write_restored_config") as mock_restore,
@ -104,7 +104,7 @@ class TestRestoreOriginalConfigIfNeeded:
state_file.write_text(json.dumps({"date": "2020-01-01"})) state_file.write_text(json.dumps({"date": "2020-01-01"}))
with ( with (
patch( patch(
"python_pkg.screen_locker._shutdown.SICK_DAY_STATE_FILE", "screen_locker._shutdown.SICK_DAY_STATE_FILE",
state_file, state_file,
), ),
patch.object(locker, "_write_restored_config") as mock_restore, patch.object(locker, "_write_restored_config") as mock_restore,
@ -124,7 +124,7 @@ class TestRestoreOriginalConfigIfNeeded:
mock_file.exists.return_value = True mock_file.exists.return_value = True
mock_file.open.side_effect = OSError("fail") mock_file.open.side_effect = OSError("fail")
with patch( with patch(
"python_pkg.screen_locker._shutdown.SICK_DAY_STATE_FILE", "screen_locker._shutdown.SICK_DAY_STATE_FILE",
mock_file, mock_file,
): ):
locker._restore_original_config_if_needed() locker._restore_original_config_if_needed()
@ -140,7 +140,7 @@ class TestRestoreOriginalConfigIfNeeded:
state_file = tmp_path / "state.json" state_file = tmp_path / "state.json"
state_file.write_text("not valid json{{{") state_file.write_text("not valid json{{{")
with patch( with patch(
"python_pkg.screen_locker._shutdown.SICK_DAY_STATE_FILE", "screen_locker._shutdown.SICK_DAY_STATE_FILE",
state_file, state_file,
): ):
locker._restore_original_config_if_needed() locker._restore_original_config_if_needed()
@ -160,7 +160,7 @@ class TestReadShutdownConfig:
mock_file = MagicMock() mock_file = MagicMock()
mock_file.exists.return_value = False mock_file.exists.return_value = False
with patch( with patch(
"python_pkg.screen_locker._shutdown.SHUTDOWN_CONFIG_FILE", "screen_locker._shutdown.SHUTDOWN_CONFIG_FILE",
mock_file, mock_file,
): ):
assert locker._read_shutdown_config() is None assert locker._read_shutdown_config() is None
@ -176,7 +176,7 @@ class TestReadShutdownConfig:
config_file = tmp_path / "shutdown.conf" config_file = tmp_path / "shutdown.conf"
config_file.write_text("MON_WED_HOUR=21\nTHU_SUN_HOUR=20\nMORNING_END_HOUR=8\n") config_file.write_text("MON_WED_HOUR=21\nTHU_SUN_HOUR=20\nMORNING_END_HOUR=8\n")
with patch( with patch(
"python_pkg.screen_locker._shutdown.SHUTDOWN_CONFIG_FILE", "screen_locker._shutdown.SHUTDOWN_CONFIG_FILE",
config_file, config_file,
): ):
result = locker._read_shutdown_config() result = locker._read_shutdown_config()
@ -193,7 +193,7 @@ class TestReadShutdownConfig:
config_file = tmp_path / "shutdown.conf" config_file = tmp_path / "shutdown.conf"
config_file.write_text("MON_WED_HOUR=21\n") config_file.write_text("MON_WED_HOUR=21\n")
with patch( with patch(
"python_pkg.screen_locker._shutdown.SHUTDOWN_CONFIG_FILE", "screen_locker._shutdown.SHUTDOWN_CONFIG_FILE",
config_file, config_file,
): ):
result = locker._read_shutdown_config() result = locker._read_shutdown_config()
@ -253,7 +253,7 @@ class TestWriteShutdownConfig:
mock_script = MagicMock() mock_script = MagicMock()
mock_script.exists.return_value = False mock_script.exists.return_value = False
with patch( with patch(
"python_pkg.screen_locker._shutdown.ADJUST_SHUTDOWN_SCRIPT", "screen_locker._shutdown.ADJUST_SHUTDOWN_SCRIPT",
mock_script, mock_script,
): ):
result = locker._write_shutdown_config(21, 20, 8) result = locker._write_shutdown_config(21, 20, 8)
@ -271,7 +271,7 @@ class TestWriteShutdownConfig:
mock_script.exists.return_value = True mock_script.exists.return_value = True
with ( with (
patch( patch(
"python_pkg.screen_locker._shutdown.ADJUST_SHUTDOWN_SCRIPT", "screen_locker._shutdown.ADJUST_SHUTDOWN_SCRIPT",
mock_script, mock_script,
), ),
patch.object(locker, "_run_shutdown_cmd", return_value=True) as mock_run, patch.object(locker, "_run_shutdown_cmd", return_value=True) as mock_run,
@ -294,7 +294,7 @@ class TestRunShutdownCmd:
locker = create_locker(mock_tk, tmp_path) locker = create_locker(mock_tk, tmp_path)
mock_result = MagicMock(stdout="OK\n") mock_result = MagicMock(stdout="OK\n")
with patch( with patch(
"python_pkg.screen_locker._shutdown.subprocess.run", "screen_locker._shutdown.subprocess.run",
return_value=mock_result, return_value=mock_result,
): ):
result = locker._run_shutdown_cmd(["cmd"], 21, 20) result = locker._run_shutdown_cmd(["cmd"], 21, 20)
@ -309,7 +309,7 @@ class TestRunShutdownCmd:
"""Test returns False on SubprocessError.""" """Test returns False on SubprocessError."""
locker = create_locker(mock_tk, tmp_path) locker = create_locker(mock_tk, tmp_path)
with patch( with patch(
"python_pkg.screen_locker._shutdown.subprocess.run", "screen_locker._shutdown.subprocess.run",
side_effect=subprocess.CalledProcessError(1, "cmd"), side_effect=subprocess.CalledProcessError(1, "cmd"),
): ):
result = locker._run_shutdown_cmd(["cmd"], 21, 20) result = locker._run_shutdown_cmd(["cmd"], 21, 20)

View File

@ -6,9 +6,9 @@ from __future__ import annotations
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
from python_pkg.screen_locker import _sick_tracker from screen_locker import _sick_tracker
from python_pkg.screen_locker._sick_tracker import SickHistory from screen_locker._sick_tracker import SickHistory
from python_pkg.screen_locker.tests.conftest import create_locker from screen_locker.tests.conftest import create_locker
if TYPE_CHECKING: if TYPE_CHECKING:
from pathlib import Path from pathlib import Path
@ -439,7 +439,7 @@ class TestDisablePaste:
"""Tests for the _disable_paste helper.""" """Tests for the _disable_paste helper."""
def test_swallows_tcl_error(self) -> None: def test_swallows_tcl_error(self) -> None:
from python_pkg.screen_locker._sick_dialog import _disable_paste from screen_locker._sick_dialog import _disable_paste
widget = MagicMock() widget = MagicMock()
import tkinter as tk import tkinter as tk

View File

@ -8,8 +8,8 @@ from unittest.mock import patch
import pytest import pytest
from python_pkg.screen_locker import _sick_tracker from screen_locker import _sick_tracker
from python_pkg.screen_locker._constants import ( from screen_locker._constants import (
SICK_BUDGET_PER_7_DAYS, SICK_BUDGET_PER_7_DAYS,
SICK_BUDGET_PER_30_DAYS, SICK_BUDGET_PER_30_DAYS,
SICK_BUDGET_PER_90_DAYS, SICK_BUDGET_PER_90_DAYS,
@ -19,7 +19,7 @@ from python_pkg.screen_locker._constants import (
SICK_LOCKOUT_MULTIPLIER_PER_RECENT, SICK_LOCKOUT_MULTIPLIER_PER_RECENT,
SICK_LOCKOUT_SECONDS, SICK_LOCKOUT_SECONDS,
) )
from python_pkg.screen_locker._sick_tracker import ( from screen_locker._sick_tracker import (
JustificationDraft, JustificationDraft,
SickHistory, SickHistory,
add_justification, add_justification,

View File

@ -6,7 +6,7 @@ import struct
import time import time
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
from python_pkg.screen_locker._time_check import ( from screen_locker._time_check import (
_NTP_EPOCH_OFFSET, _NTP_EPOCH_OFFSET,
_query_ntp_offset, _query_ntp_offset,
check_clock_skew, check_clock_skew,
@ -66,7 +66,7 @@ class TestCheckClockSkew:
def test_ok_within_threshold(self) -> None: def test_ok_within_threshold(self) -> None:
"""Test returns ok when clock offset is small.""" """Test returns ok when clock offset is small."""
with patch( with patch(
"python_pkg.screen_locker._time_check._query_ntp_offset", "screen_locker._time_check._query_ntp_offset",
return_value=2.5, return_value=2.5,
): ):
ok, message = check_clock_skew() ok, message = check_clock_skew()
@ -77,7 +77,7 @@ class TestCheckClockSkew:
def test_fails_when_skew_exceeds_threshold(self) -> None: def test_fails_when_skew_exceeds_threshold(self) -> None:
"""Test returns failure when clock offset exceeds max.""" """Test returns failure when clock offset exceeds max."""
with patch( with patch(
"python_pkg.screen_locker._time_check._query_ntp_offset", "screen_locker._time_check._query_ntp_offset",
return_value=600.0, return_value=600.0,
): ):
ok, message = check_clock_skew() ok, message = check_clock_skew()
@ -88,7 +88,7 @@ class TestCheckClockSkew:
def test_ntp_unreachable_passes(self) -> None: def test_ntp_unreachable_passes(self) -> None:
"""Test returns ok when NTP server is unreachable (fail-open).""" """Test returns ok when NTP server is unreachable (fail-open)."""
with patch( with patch(
"python_pkg.screen_locker._time_check._query_ntp_offset", "screen_locker._time_check._query_ntp_offset",
return_value=None, return_value=None,
): ):
ok, message = check_clock_skew() ok, message = check_clock_skew()
@ -99,7 +99,7 @@ class TestCheckClockSkew:
def test_negative_offset_detected(self) -> None: def test_negative_offset_detected(self) -> None:
"""Test detects clock ahead with negative offset.""" """Test detects clock ahead with negative offset."""
with patch( with patch(
"python_pkg.screen_locker._time_check._query_ntp_offset", "screen_locker._time_check._query_ntp_offset",
return_value=-400.0, return_value=-400.0,
): ):
ok, message = check_clock_skew() ok, message = check_clock_skew()

View File

@ -5,7 +5,7 @@ from __future__ import annotations
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from unittest.mock import MagicMock from unittest.mock import MagicMock
from python_pkg.screen_locker.tests.conftest import create_locker from screen_locker.tests.conftest import create_locker
if TYPE_CHECKING: if TYPE_CHECKING:
from pathlib import Path from pathlib import Path

View File

@ -5,11 +5,11 @@ from __future__ import annotations
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from unittest.mock import MagicMock from unittest.mock import MagicMock
from python_pkg.screen_locker._sick_tracker import SickHistory from screen_locker._sick_tracker import SickHistory
from python_pkg.screen_locker.screen_lock import ( from screen_locker.screen_lock import (
SICK_LOCKOUT_SECONDS, SICK_LOCKOUT_SECONDS,
) )
from python_pkg.screen_locker.tests.conftest import create_locker from screen_locker.tests.conftest import create_locker
if TYPE_CHECKING: if TYPE_CHECKING:
from pathlib import Path from pathlib import Path

View File

@ -5,7 +5,7 @@ from __future__ import annotations
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from unittest.mock import MagicMock from unittest.mock import MagicMock
from python_pkg.screen_locker.tests.conftest import create_locker from screen_locker.tests.conftest import create_locker
if TYPE_CHECKING: if TYPE_CHECKING:
from pathlib import Path from pathlib import Path

View File

@ -9,7 +9,7 @@ from unittest.mock import MagicMock
import pytest import pytest
from python_pkg.screen_locker.tests.conftest import create_locker from screen_locker.tests.conftest import create_locker
if TYPE_CHECKING: if TYPE_CHECKING:
from pathlib import Path from pathlib import Path

View File

@ -5,7 +5,7 @@ from __future__ import annotations
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from unittest.mock import MagicMock, call, patch from unittest.mock import MagicMock, call, patch
from python_pkg.screen_locker.tests.conftest import create_locker from screen_locker.tests.conftest import create_locker
if TYPE_CHECKING: if TYPE_CHECKING:
from pathlib import Path from pathlib import Path
@ -109,7 +109,7 @@ class TestVTSwitching:
) -> None: ) -> None:
"""No crash and no subprocess call when setxkbmap is not installed.""" """No crash and no subprocess call when setxkbmap is not installed."""
with patch( with patch(
"python_pkg.screen_locker._window_setup.shutil.which", "screen_locker._window_setup.shutil.which",
return_value=None, return_value=None,
): ):
create_locker(mock_tk, tmp_path, demo_mode=False) create_locker(mock_tk, tmp_path, demo_mode=False)
@ -128,7 +128,7 @@ class TestVTSwitching:
mock_subprocess_run.reset_mock() mock_subprocess_run.reset_mock()
with patch( with patch(
"python_pkg.screen_locker._window_setup.shutil.which", "screen_locker._window_setup.shutil.which",
return_value=None, return_value=None,
): ):
locker.close() locker.close()

View File

@ -5,7 +5,7 @@ from __future__ import annotations
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
from python_pkg.screen_locker.tests.conftest import create_locker from screen_locker.tests.conftest import create_locker
if TYPE_CHECKING: if TYPE_CHECKING:
from pathlib import Path from pathlib import Path
@ -26,7 +26,7 @@ class TestIsTomorrowAlarmDay:
# Sunday 2026-04-12 → tomorrow Monday # Sunday 2026-04-12 → tomorrow Monday
with patch( with patch(
"python_pkg.screen_locker._shutdown.datetime", "screen_locker._shutdown.datetime",
) as mock_dt: ) as mock_dt:
mock_dt.now.return_value = datetime(2026, 4, 12, 23, 0, tzinfo=timezone.utc) mock_dt.now.return_value = datetime(2026, 4, 12, 23, 0, tzinfo=timezone.utc)
mock_dt.side_effect = datetime mock_dt.side_effect = datetime
@ -34,7 +34,7 @@ class TestIsTomorrowAlarmDay:
# Ensure timedelta works # Ensure timedelta works
with patch( with patch(
"python_pkg.screen_locker._shutdown.timedelta", "screen_locker._shutdown.timedelta",
timedelta, timedelta,
): ):
assert locker._is_tomorrow_alarm_day() is True assert locker._is_tomorrow_alarm_day() is True
@ -52,10 +52,10 @@ class TestIsTomorrowAlarmDay:
# Monday 2026-04-13 → tomorrow Tuesday (weekday=1) # Monday 2026-04-13 → tomorrow Tuesday (weekday=1)
with ( with (
patch( patch(
"python_pkg.screen_locker._shutdown.datetime", "screen_locker._shutdown.datetime",
) as mock_dt, ) as mock_dt,
patch( patch(
"python_pkg.screen_locker._shutdown.timedelta", "screen_locker._shutdown.timedelta",
timedelta, timedelta,
), ),
): ):
@ -76,10 +76,10 @@ class TestIsTomorrowAlarmDay:
# Thursday 2026-04-16 → tomorrow Friday (weekday=4) # Thursday 2026-04-16 → tomorrow Friday (weekday=4)
with ( with (
patch( patch(
"python_pkg.screen_locker._shutdown.datetime", "screen_locker._shutdown.datetime",
) as mock_dt, ) as mock_dt,
patch( patch(
"python_pkg.screen_locker._shutdown.timedelta", "screen_locker._shutdown.timedelta",
timedelta, timedelta,
), ),
): ):
@ -100,7 +100,7 @@ class TestScheduleRtcwake:
"""Successful rtcwake call returns True.""" """Successful rtcwake call returns True."""
locker = create_locker(mock_tk, tmp_path) locker = create_locker(mock_tk, tmp_path)
with patch( with patch(
"python_pkg.screen_locker._shutdown.subprocess.run", "screen_locker._shutdown.subprocess.run",
) as mock_run: ) as mock_run:
mock_run.return_value = MagicMock(returncode=0) mock_run.return_value = MagicMock(returncode=0)
assert locker._schedule_rtcwake() is True assert locker._schedule_rtcwake() is True
@ -119,7 +119,7 @@ class TestScheduleRtcwake:
import subprocess import subprocess
with patch( with patch(
"python_pkg.screen_locker._shutdown.subprocess.run", "screen_locker._shutdown.subprocess.run",
side_effect=subprocess.SubprocessError("rtcwake failed"), side_effect=subprocess.SubprocessError("rtcwake failed"),
): ):
assert locker._schedule_rtcwake() is False assert locker._schedule_rtcwake() is False

View File

@ -5,7 +5,7 @@ from __future__ import annotations
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
from python_pkg.screen_locker.tests.conftest import create_locker from screen_locker.tests.conftest import create_locker
if TYPE_CHECKING: if TYPE_CHECKING:
from pathlib import Path from pathlib import Path
@ -22,7 +22,7 @@ class TestWakeSkipIntegration:
) -> None: ) -> None:
"""Screen locker exits if wake alarm granted workout skip today.""" """Screen locker exits if wake alarm granted workout skip today."""
with patch( with patch(
"python_pkg.screen_locker.screen_lock.has_workout_skip_today", "screen_locker.screen_lock.has_workout_skip_today",
return_value=True, return_value=True,
): ):
create_locker(mock_tk, tmp_path, has_logged=False) create_locker(mock_tk, tmp_path, has_logged=False)
@ -37,7 +37,7 @@ class TestWakeSkipIntegration:
) -> None: ) -> None:
"""Screen locker proceeds normally if no wake skip active.""" """Screen locker proceeds normally if no wake skip active."""
with patch( with patch(
"python_pkg.screen_locker.screen_lock.has_workout_skip_today", "screen_locker.screen_lock.has_workout_skip_today",
return_value=False, return_value=False,
): ):
locker = create_locker(mock_tk, tmp_path, has_logged=False) locker = create_locker(mock_tk, tmp_path, has_logged=False)
@ -53,7 +53,7 @@ class TestWakeSkipIntegration:
) -> None: ) -> None:
"""has_logged_today exits before wake skip is even checked.""" """has_logged_today exits before wake skip is even checked."""
with patch( with patch(
"python_pkg.screen_locker.screen_lock.has_workout_skip_today", "screen_locker.screen_lock.has_workout_skip_today",
return_value=True, return_value=True,
): ):
create_locker(mock_tk, tmp_path, has_logged=True) create_locker(mock_tk, tmp_path, has_logged=True)
@ -69,7 +69,7 @@ class TestWakeSkipIntegration:
) -> None: ) -> None:
"""verify_only mode checks sick day log, not wake skip.""" """verify_only mode checks sick day log, not wake skip."""
with patch( with patch(
"python_pkg.screen_locker.screen_lock.has_workout_skip_today", "screen_locker.screen_lock.has_workout_skip_today",
return_value=True, return_value=True,
): ):
create_locker( create_locker(

View File

@ -0,0 +1,203 @@
"""Tests for screen_locker._wake_state — 100% branch coverage."""
from __future__ import annotations
import json
from pathlib import Path
from typing import TYPE_CHECKING
from unittest.mock import MagicMock, patch
import pytest
from screen_locker._wake_state import has_workout_skip_today, load_wake_state
if TYPE_CHECKING:
pass
_TODAY = "2026-05-28"
_STATE_PATH = "screen_locker._wake_state.WAKE_STATE_FILE"
# ---------------------------------------------------------------------------
# load_wake_state — file-absent / unreadable paths
# ---------------------------------------------------------------------------
class TestLoadWakeStateFileAbsent:
def test_returns_none_when_file_missing(self, tmp_path: Path) -> None:
"""Returns None when state file does not exist."""
missing = tmp_path / "no_such_file.json"
with patch(_STATE_PATH, missing):
assert load_wake_state() is None
def test_returns_none_on_oserror(self, tmp_path: Path) -> None:
"""Returns None when the state file cannot be opened."""
state_file = tmp_path / "wake_state.json"
state_file.write_text("{}")
with (
patch(_STATE_PATH, state_file),
patch("screen_locker._wake_state.WAKE_STATE_FILE", state_file),
patch("builtins.open", side_effect=OSError("permission denied")),
):
assert load_wake_state() is None
def test_returns_none_on_invalid_json(self, tmp_path: Path) -> None:
"""Returns None when the state file contains invalid JSON."""
state_file = tmp_path / "wake_state.json"
state_file.write_text("not-valid-json")
with patch(_STATE_PATH, state_file):
assert load_wake_state() is None
def test_returns_none_when_state_not_dict(self, tmp_path: Path) -> None:
"""Returns None when the JSON root is not a dict."""
state_file = tmp_path / "wake_state.json"
state_file.write_text("[1, 2, 3]")
with patch(_STATE_PATH, state_file):
assert load_wake_state() is None
# ---------------------------------------------------------------------------
# load_wake_state — date / HMAC validation paths
# ---------------------------------------------------------------------------
class TestLoadWakeStateValidation:
def test_returns_none_when_date_stale(self, tmp_path: Path) -> None:
"""Returns None when the state file is from a previous day."""
state_file = tmp_path / "wake_state.json"
state = {"date": "2000-01-01", "skip_workout": True}
state_file.write_text(json.dumps(state))
with (
patch(_STATE_PATH, state_file),
patch(
"screen_locker._wake_state._today_str",
return_value=_TODAY,
),
):
assert load_wake_state() is None
def test_returns_none_when_hmac_invalid(self, tmp_path: Path) -> None:
"""Returns None when HMAC verification fails."""
state_file = tmp_path / "wake_state.json"
state = {"date": _TODAY, "skip_workout": True, "hmac": "bad"}
state_file.write_text(json.dumps(state))
with (
patch(_STATE_PATH, state_file),
patch(
"screen_locker._wake_state._today_str",
return_value=_TODAY,
),
patch(
"screen_locker._wake_state.verify_entry_hmac",
return_value=False,
),
):
assert load_wake_state() is None
def test_returns_state_when_valid(self, tmp_path: Path) -> None:
"""Returns the state dict when date matches and HMAC is valid."""
state_file = tmp_path / "wake_state.json"
state = {"date": _TODAY, "skip_workout": True, "hmac": "sig"}
state_file.write_text(json.dumps(state))
with (
patch(_STATE_PATH, state_file),
patch(
"screen_locker._wake_state._today_str",
return_value=_TODAY,
),
patch(
"screen_locker._wake_state.verify_entry_hmac",
return_value=True,
),
):
result = load_wake_state()
assert result is not None
assert result["skip_workout"] is True
def test_returns_state_when_skip_false(self, tmp_path: Path) -> None:
"""Returns the state dict even when skip_workout is False."""
state_file = tmp_path / "wake_state.json"
state = {"date": _TODAY, "skip_workout": False, "hmac": "sig"}
state_file.write_text(json.dumps(state))
with (
patch(_STATE_PATH, state_file),
patch(
"screen_locker._wake_state._today_str",
return_value=_TODAY,
),
patch(
"screen_locker._wake_state.verify_entry_hmac",
return_value=True,
),
):
result = load_wake_state()
assert result is not None
assert result["skip_workout"] is False
# ---------------------------------------------------------------------------
# has_workout_skip_today
# ---------------------------------------------------------------------------
class TestHasWorkoutSkipToday:
def test_returns_false_when_no_state(self) -> None:
"""Returns False when load_wake_state returns None."""
with patch(
"screen_locker._wake_state.load_wake_state",
return_value=None,
):
assert has_workout_skip_today() is False
def test_returns_true_when_skip_active(self) -> None:
"""Returns True when state has skip_workout=True."""
with patch(
"screen_locker._wake_state.load_wake_state",
return_value={"date": _TODAY, "skip_workout": True},
):
assert has_workout_skip_today() is True
def test_returns_false_when_skip_inactive(self) -> None:
"""Returns False when state has skip_workout=False."""
with patch(
"screen_locker._wake_state.load_wake_state",
return_value={"date": _TODAY, "skip_workout": False},
):
assert has_workout_skip_today() is False
# ---------------------------------------------------------------------------
# _today_str
# ---------------------------------------------------------------------------
class TestTodayStr:
def test_returns_iso_date_format(self) -> None:
"""_today_str returns a YYYY-MM-DD string."""
from screen_locker._wake_state import _today_str
result = _today_str()
assert len(result) == 10
assert result[4] == "-"
assert result[7] == "-"
# ---------------------------------------------------------------------------
# OSError in load_wake_state body (JSON decode error path)
# ---------------------------------------------------------------------------
class TestLoadWakeStateOsError:
def test_oserror_during_read(self, tmp_path: Path) -> None:
"""Returns None and warns when open() raises OSError."""
state_file = tmp_path / "wake_state.json"
state_file.write_text("{}")
mock_open = MagicMock(side_effect=OSError("disk error"))
with (
patch(_STATE_PATH, state_file),
patch("screen_locker._wake_state.open", mock_open, create=True),
):
# We can't easily patch builtins.open for a specific file,
# so we test by patching the json.load path
with patch("json.load", side_effect=OSError("disk error")):
assert load_wake_state() is None

View File

@ -8,7 +8,7 @@ import json
from typing import TYPE_CHECKING, Any from typing import TYPE_CHECKING, Any
from unittest.mock import patch from unittest.mock import patch
from python_pkg.screen_locker._weekly_check import ( from screen_locker._weekly_check import (
_RELAXED_WEEKDAYS, _RELAXED_WEEKDAYS,
WEEKLY_WORKOUT_MINIMUM, WEEKLY_WORKOUT_MINIMUM,
count_weekly_workouts, count_weekly_workouts,

View File

@ -5,8 +5,8 @@ from __future__ import annotations
from pathlib import Path from pathlib import Path
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
from python_pkg.screen_locker.screen_lock import ScreenLocker from screen_locker.screen_lock import ScreenLocker
from python_pkg.screen_locker.tests.conftest import ( from screen_locker.tests.conftest import (
create_locker, create_locker,
create_locker_relaxed_day, create_locker_relaxed_day,
) )
@ -43,11 +43,11 @@ class TestRelaxedDayBranch:
ScreenLocker, "_try_auto_upgrade_early_bird", return_value=False ScreenLocker, "_try_auto_upgrade_early_bird", return_value=False
), ),
patch( patch(
"python_pkg.screen_locker.screen_lock.is_relaxed_day", "screen_locker.screen_lock.is_relaxed_day",
return_value=True, return_value=True,
), ),
patch( patch(
"python_pkg.screen_locker.screen_lock.has_weekly_minimum", "screen_locker.screen_lock.has_weekly_minimum",
return_value=False, return_value=False,
), ),
patch.object(ScreenLocker, "_start_phone_check") as mock_phone, patch.object(ScreenLocker, "_start_phone_check") as mock_phone,
@ -75,11 +75,11 @@ class TestRelaxedDayBranch:
ScreenLocker, "_try_auto_upgrade_early_bird", return_value=False ScreenLocker, "_try_auto_upgrade_early_bird", return_value=False
), ),
patch( patch(
"python_pkg.screen_locker.screen_lock.is_relaxed_day", "screen_locker.screen_lock.is_relaxed_day",
return_value=True, return_value=True,
), ),
patch( patch(
"python_pkg.screen_locker.screen_lock.has_weekly_minimum", "screen_locker.screen_lock.has_weekly_minimum",
return_value=False, return_value=False,
), ),
patch.object(ScreenLocker, "_setup_window") as mock_full, patch.object(ScreenLocker, "_setup_window") as mock_full,
@ -109,11 +109,11 @@ class TestRelaxedDayBranch:
ScreenLocker, "_try_auto_upgrade_early_bird", return_value=False ScreenLocker, "_try_auto_upgrade_early_bird", return_value=False
), ),
patch( patch(
"python_pkg.screen_locker.screen_lock.is_relaxed_day", "screen_locker.screen_lock.is_relaxed_day",
return_value=True, return_value=True,
), ),
patch( patch(
"python_pkg.screen_locker.screen_lock.has_weekly_minimum", "screen_locker.screen_lock.has_weekly_minimum",
return_value=False, return_value=False,
), ),
patch.object(ScreenLocker, "_grab_input") as mock_grab, patch.object(ScreenLocker, "_grab_input") as mock_grab,
@ -148,7 +148,7 @@ class TestWeeklyMinimumBranch:
tmp_path: Path, tmp_path: Path,
) -> None: ) -> None:
with patch( with patch(
"python_pkg.screen_locker.screen_lock.has_weekly_minimum", "screen_locker.screen_lock.has_weekly_minimum",
return_value=True, return_value=True,
): ):
create_locker(mock_tk, tmp_path, has_logged=False) create_locker(mock_tk, tmp_path, has_logged=False)
@ -164,7 +164,7 @@ class TestWeeklyMinimumBranch:
# create_locker already stubs _start_phone_check; just verify no exit # create_locker already stubs _start_phone_check; just verify no exit
# and _relaxed_day_mode stays False (full lock path taken). # and _relaxed_day_mode stays False (full lock path taken).
with patch( with patch(
"python_pkg.screen_locker.screen_lock.has_weekly_minimum", "screen_locker.screen_lock.has_weekly_minimum",
return_value=False, return_value=False,
): ):
locker = create_locker(mock_tk, tmp_path, has_logged=False) locker = create_locker(mock_tk, tmp_path, has_logged=False)
@ -179,7 +179,7 @@ class TestWeeklyMinimumBranch:
tmp_path: Path, tmp_path: Path,
) -> None: ) -> None:
with patch( with patch(
"python_pkg.screen_locker.screen_lock.has_weekly_minimum", "screen_locker.screen_lock.has_weekly_minimum",
) as mock_weekly: ) as mock_weekly:
create_locker_relaxed_day(mock_tk, tmp_path) create_locker_relaxed_day(mock_tk, tmp_path)
@ -192,7 +192,7 @@ class TestWeeklyMinimumBranch:
tmp_path: Path, tmp_path: Path,
) -> None: ) -> None:
with patch( with patch(
"python_pkg.screen_locker.screen_lock.has_weekly_minimum", "screen_locker.screen_lock.has_weekly_minimum",
) as mock_weekly: ) as mock_weekly:
create_locker(mock_tk, tmp_path, has_logged=True) create_locker(mock_tk, tmp_path, has_logged=True)
@ -217,7 +217,7 @@ class TestStartRelaxedDayFlow:
locker = self._make_locker(mock_tk, tmp_path) locker = self._make_locker(mock_tk, tmp_path)
with ( with (
patch( patch(
"python_pkg.screen_locker._ui_flows.count_weekly_workouts", "screen_locker._ui_flows.count_weekly_workouts",
return_value=2, return_value=2,
), ),
patch.object(locker, "_text") as mock_text, patch.object(locker, "_text") as mock_text,
@ -241,7 +241,7 @@ class TestStartRelaxedDayFlow:
locker = self._make_locker(mock_tk, tmp_path) locker = self._make_locker(mock_tk, tmp_path)
with ( with (
patch( patch(
"python_pkg.screen_locker._ui_flows.count_weekly_workouts", "screen_locker._ui_flows.count_weekly_workouts",
return_value=0, return_value=0,
), ),
patch.object(locker, "_button") as mock_button, patch.object(locker, "_button") as mock_button,
@ -268,7 +268,7 @@ class TestStartRelaxedDayFlow:
locker = self._make_locker(mock_tk, tmp_path) locker = self._make_locker(mock_tk, tmp_path)
with ( with (
patch( patch(
"python_pkg.screen_locker._ui_flows.count_weekly_workouts", "screen_locker._ui_flows.count_weekly_workouts",
return_value=1, return_value=1,
), ),
patch.object(locker, "_button") as mock_button, patch.object(locker, "_button") as mock_button,
@ -534,7 +534,7 @@ class TestCheckTodayStateExits:
patch.object(locker, "_is_sick_day_log", return_value=False), patch.object(locker, "_is_sick_day_log", return_value=False),
patch.object(locker, "has_logged_today", return_value=False), patch.object(locker, "has_logged_today", return_value=False),
patch( patch(
"python_pkg.screen_locker.screen_lock.has_workout_skip_today", "screen_locker.screen_lock.has_workout_skip_today",
return_value=True, return_value=True,
), ),
): ):
@ -553,7 +553,7 @@ class TestCheckTodayStateExits:
patch.object(locker, "_is_sick_day_log", return_value=False), patch.object(locker, "_is_sick_day_log", return_value=False),
patch.object(locker, "has_logged_today", return_value=False), patch.object(locker, "has_logged_today", return_value=False),
patch( patch(
"python_pkg.screen_locker.screen_lock.has_workout_skip_today", "screen_locker.screen_lock.has_workout_skip_today",
return_value=False, return_value=False,
), ),
patch.object(locker, "_is_early_bird_time", return_value=True), patch.object(locker, "_is_early_bird_time", return_value=True),
@ -574,7 +574,7 @@ class TestCheckTodayStateExits:
patch.object(locker, "_is_sick_day_log", return_value=False), patch.object(locker, "_is_sick_day_log", return_value=False),
patch.object(locker, "has_logged_today", return_value=False), patch.object(locker, "has_logged_today", return_value=False),
patch( patch(
"python_pkg.screen_locker.screen_lock.has_workout_skip_today", "screen_locker.screen_lock.has_workout_skip_today",
return_value=False, return_value=False,
), ),
patch.object(locker, "_is_early_bird_time", return_value=False), patch.object(locker, "_is_early_bird_time", return_value=False),

View File

@ -4,11 +4,11 @@ After=graphical-session.target
[Service] [Service]
Type=simple Type=simple
WorkingDirectory=/home/kuhy/testsAndMisc WorkingDirectory=/opt/screen-locker
Environment=DISPLAY=:0 Environment=DISPLAY=:0
Environment=PYTHONPATH=/home/kuhy/testsAndMisc Environment=PYTHONPATH=/opt/screen-locker
ExecStartPre=/bin/sleep 1 ExecStartPre=/bin/sleep 1
ExecStart=/usr/bin/python3 -m python_pkg.screen_locker.screen_lock --production ExecStart=/usr/bin/python3 -m screen_locker.screen_lock --production
Restart=on-failure Restart=on-failure
RestartSec=2s RestartSec=2s
RestartPreventExitStatus=0 RestartPreventExitStatus=0