mirror of
https://github.com/kuhyx/screen-locker.git
synced 2026-07-04 12:03:09 +02:00
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:
parent
1172aff8fb
commit
4cdfce5fe3
19
.github/workflows/pre-commit.yml
vendored
Normal file
19
.github/workflows/pre-commit.yml
vendored
Normal 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
28
.github/workflows/python-tests.yml
vendored
Normal 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
16
.gitignore
vendored
Normal 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
131
.pre-commit-config.yaml
Normal 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
149
pyproject.toml
Normal 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
14
requirements.txt
Normal 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
|
||||
@ -1,11 +1,10 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||
VENV="$REPO_ROOT/.venv"
|
||||
VENV="$SCRIPT_DIR/.venv"
|
||||
[[ ! -d "$VENV" ]] && python3 -m venv "$VENV"
|
||||
# tkinter is from Python stdlib; install python-tk system package if missing:
|
||||
# Arch: sudo pacman -S python-tk
|
||||
# Debian: sudo apt-get install python3-tk
|
||||
cd "$REPO_ROOT"
|
||||
"$VENV/bin/python" -m python_pkg.screen_locker.screen_lock "$@"
|
||||
cd "$SCRIPT_DIR"
|
||||
"$VENV/bin/python" -m screen_locker.screen_lock "$@"
|
||||
@ -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"
|
||||
# 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"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 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"
|
||||
|
||||
@ -6,7 +6,7 @@ from datetime import datetime, timezone
|
||||
import json
|
||||
import logging
|
||||
|
||||
from python_pkg.screen_locker._constants import (
|
||||
from screen_locker._constants import (
|
||||
EARLY_BIRD_END_HOUR,
|
||||
EARLY_BIRD_END_MINUTE,
|
||||
EARLY_BIRD_START_HOUR,
|
||||
|
||||
@ -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 python_pkg.shared.log_integrity import (
|
||||
HMAC_KEY_FILE,
|
||||
_generate_hmac_key,
|
||||
_load_hmac_key,
|
||||
compute_entry_hmac,
|
||||
verify_entry_hmac,
|
||||
)
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
import secrets
|
||||
|
||||
__all__ = [
|
||||
"HMAC_KEY_FILE",
|
||||
"_generate_hmac_key",
|
||||
"_load_hmac_key",
|
||||
"compute_entry_hmac",
|
||||
"verify_entry_hmac",
|
||||
]
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
# HMAC key for signing state entries (root-owned, 0600)
|
||||
HMAC_KEY_FILE = Path("/etc/workout-locker/hmac.key")
|
||||
|
||||
|
||||
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)
|
||||
|
||||
@ -17,12 +17,12 @@ import subprocess
|
||||
import tempfile
|
||||
import time
|
||||
|
||||
from python_pkg.screen_locker._constants import (
|
||||
from screen_locker._constants import (
|
||||
ADB_TIMEOUT,
|
||||
MIN_WORKOUT_DURATION_MINUTES,
|
||||
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__)
|
||||
|
||||
|
||||
@ -8,12 +8,12 @@ import json
|
||||
import logging
|
||||
import subprocess
|
||||
|
||||
from python_pkg.screen_locker._constants import (
|
||||
from screen_locker._constants import (
|
||||
ADJUST_SHUTDOWN_SCRIPT,
|
||||
SHUTDOWN_CONFIG_FILE,
|
||||
SICK_DAY_STATE_FILE,
|
||||
)
|
||||
from python_pkg.wake_alarm._constants import (
|
||||
from screen_locker._constants import (
|
||||
ALARM_DAYS,
|
||||
RTCWAKE_BIN,
|
||||
WAKE_AFTER_HOURS,
|
||||
|
||||
@ -7,8 +7,8 @@ import logging
|
||||
import tkinter as tk
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from python_pkg.screen_locker import _sick_tracker
|
||||
from python_pkg.screen_locker._constants import (
|
||||
from screen_locker import _sick_tracker
|
||||
from screen_locker._constants import (
|
||||
COMMITMENT_PROMPT_TIMEOUT_SECONDS,
|
||||
SICK_COMMITMENT_FORCED_READ_SECONDS,
|
||||
SICK_JUSTIFICATION_MIN_CHARS,
|
||||
@ -17,7 +17,7 @@ from python_pkg.screen_locker._constants import (
|
||||
if TYPE_CHECKING:
|
||||
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__)
|
||||
|
||||
|
||||
@ -12,7 +12,7 @@ import json
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from python_pkg.screen_locker._constants import (
|
||||
from screen_locker._constants import (
|
||||
SICK_BUDGET_PER_7_DAYS,
|
||||
SICK_BUDGET_PER_30_DAYS,
|
||||
SICK_BUDGET_PER_90_DAYS,
|
||||
@ -23,7 +23,7 @@ from python_pkg.screen_locker._constants import (
|
||||
SICK_LOCKOUT_MULTIPLIER_PER_RECENT,
|
||||
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__)
|
||||
|
||||
|
||||
@ -7,7 +7,7 @@ import socket
|
||||
import struct
|
||||
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__)
|
||||
|
||||
|
||||
@ -5,13 +5,13 @@ from __future__ import annotations
|
||||
from concurrent.futures import ThreadPoolExecutor # pylint: disable=no-name-in-module
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from python_pkg.screen_locker import _sick_tracker
|
||||
from python_pkg.screen_locker._constants import (
|
||||
from screen_locker import _sick_tracker
|
||||
from screen_locker._constants import (
|
||||
NO_PHONE_EXTRA_LOCKOUT_SECONDS,
|
||||
PHONE_PENALTY_DELAY_DEMO,
|
||||
PHONE_PENALTY_DELAY_PRODUCTION,
|
||||
)
|
||||
from python_pkg.screen_locker._weekly_check import (
|
||||
from screen_locker._weekly_check import (
|
||||
WEEKLY_WORKOUT_MINIMUM,
|
||||
count_weekly_workouts,
|
||||
)
|
||||
|
||||
63
screen_locker/_wake_state.py
Normal file
63
screen_locker/_wake_state.py
Normal 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"))
|
||||
@ -14,8 +14,8 @@ import sys
|
||||
import tkinter as tk
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from python_pkg.screen_locker import _sick_tracker
|
||||
from python_pkg.screen_locker._constants import (
|
||||
from screen_locker import _sick_tracker
|
||||
from screen_locker._constants import (
|
||||
EARLY_BIRD_END_HOUR,
|
||||
EARLY_BIRD_END_MINUTE,
|
||||
EARLY_BIRD_START_HOUR,
|
||||
@ -28,23 +28,23 @@ from python_pkg.screen_locker._constants import (
|
||||
SICK_LOCKOUT_SECONDS,
|
||||
STRONGLIFTS_DB_REMOTE,
|
||||
)
|
||||
from python_pkg.screen_locker._early_bird import EarlyBirdMixin
|
||||
from python_pkg.screen_locker._log_integrity import (
|
||||
from screen_locker._early_bird import EarlyBirdMixin
|
||||
from screen_locker._log_integrity import (
|
||||
_load_hmac_key,
|
||||
compute_entry_hmac,
|
||||
verify_entry_hmac,
|
||||
)
|
||||
from python_pkg.screen_locker._phone_verification import PhoneVerificationMixin
|
||||
from python_pkg.screen_locker._shutdown import ShutdownMixin
|
||||
from python_pkg.screen_locker._sick_dialog import SickDialogMixin
|
||||
from python_pkg.screen_locker._ui_flows import UIFlowsMixin
|
||||
from python_pkg.screen_locker._weekly_check import (
|
||||
from screen_locker._phone_verification import PhoneVerificationMixin
|
||||
from screen_locker._shutdown import ShutdownMixin
|
||||
from screen_locker._sick_dialog import SickDialogMixin
|
||||
from screen_locker._ui_flows import UIFlowsMixin
|
||||
from screen_locker._weekly_check import (
|
||||
WEEKLY_WORKOUT_MINIMUM,
|
||||
has_weekly_minimum,
|
||||
is_relaxed_day,
|
||||
)
|
||||
from python_pkg.screen_locker._window_setup import WindowSetupMixin
|
||||
from python_pkg.wake_alarm._state import has_workout_skip_today
|
||||
from screen_locker._window_setup import WindowSetupMixin
|
||||
from screen_locker._wake_state import has_workout_skip_today
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Callable
|
||||
|
||||
@ -17,7 +17,7 @@ from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from python_pkg.screen_locker.screen_lock import ScreenLocker
|
||||
from screen_locker.screen_lock import ScreenLocker
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Generator, Iterator
|
||||
@ -51,9 +51,9 @@ def _block_real_tk_and_exit() -> Iterator[None]:
|
||||
mock = _make_mock_tk()
|
||||
|
||||
with (
|
||||
patch("python_pkg.screen_locker.screen_lock.tk", mock),
|
||||
patch("python_pkg.screen_locker._sick_dialog.tk", mock),
|
||||
patch("python_pkg.screen_locker.screen_lock.sys.exit"),
|
||||
patch("screen_locker.screen_lock.tk", mock),
|
||||
patch("screen_locker._sick_dialog.tk", mock),
|
||||
patch("screen_locker.screen_lock.sys.exit"),
|
||||
):
|
||||
yield
|
||||
|
||||
@ -70,10 +70,10 @@ def mock_subprocess_run() -> Generator[MagicMock]:
|
||||
"""
|
||||
with (
|
||||
patch(
|
||||
"python_pkg.screen_locker._window_setup.shutil.which",
|
||||
"screen_locker._window_setup.shutil.which",
|
||||
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
|
||||
|
||||
@ -84,11 +84,11 @@ def _isolate_sick_history(tmp_path: Path) -> Iterator[None]:
|
||||
target = tmp_path / "sick_history.json"
|
||||
with (
|
||||
patch(
|
||||
"python_pkg.screen_locker._sick_tracker.SICK_HISTORY_FILE",
|
||||
"screen_locker._sick_tracker.SICK_HISTORY_FILE",
|
||||
target,
|
||||
),
|
||||
patch(
|
||||
"python_pkg.screen_locker._constants.SICK_HISTORY_FILE",
|
||||
"screen_locker._constants.SICK_HISTORY_FILE",
|
||||
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."""
|
||||
target = tmp_path / "scheduled_skips.json"
|
||||
with patch(
|
||||
"python_pkg.screen_locker.screen_lock.SCHEDULED_SKIPS_FILE",
|
||||
"screen_locker.screen_lock.SCHEDULED_SKIPS_FILE",
|
||||
target,
|
||||
):
|
||||
yield
|
||||
@ -117,11 +117,11 @@ def _mock_weekly_logic() -> Iterator[None]:
|
||||
"""
|
||||
with (
|
||||
patch(
|
||||
"python_pkg.screen_locker.screen_lock.is_relaxed_day",
|
||||
"screen_locker.screen_lock.is_relaxed_day",
|
||||
return_value=False,
|
||||
),
|
||||
patch(
|
||||
"python_pkg.screen_locker.screen_lock.has_weekly_minimum",
|
||||
"screen_locker.screen_lock.has_weekly_minimum",
|
||||
return_value=False,
|
||||
),
|
||||
):
|
||||
@ -131,7 +131,7 @@ def _mock_weekly_logic() -> Iterator[None]:
|
||||
@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:
|
||||
with patch("screen_locker.screen_lock.tk") as mock:
|
||||
# Set up Tk root mock
|
||||
mock_root = MagicMock()
|
||||
mock_root.winfo_screenwidth.return_value = 1920
|
||||
@ -152,7 +152,7 @@ def mock_tk() -> Generator[MagicMock]:
|
||||
@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:
|
||||
with patch("screen_locker.screen_lock.sys.exit") as 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_time", 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(
|
||||
"python_pkg.screen_locker.screen_lock.has_weekly_minimum",
|
||||
"screen_locker.screen_lock.has_weekly_minimum",
|
||||
return_value=False,
|
||||
),
|
||||
patch.object(ScreenLocker, "_start_phone_check"),
|
||||
|
||||
@ -9,8 +9,8 @@ import time
|
||||
from typing import TYPE_CHECKING
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from python_pkg.screen_locker.screen_lock import STRONGLIFTS_DB_REMOTE
|
||||
from python_pkg.screen_locker.tests.conftest import create_locker
|
||||
from screen_locker.screen_lock import STRONGLIFTS_DB_REMOTE
|
||||
from screen_locker.tests.conftest import create_locker
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pathlib import Path
|
||||
@ -29,7 +29,7 @@ class TestRunAdb:
|
||||
locker = create_locker(mock_tk, tmp_path)
|
||||
mock_result = MagicMock(returncode=0, stdout="ok\n")
|
||||
with patch(
|
||||
"python_pkg.screen_locker._phone_verification.subprocess.run",
|
||||
"screen_locker._phone_verification.subprocess.run",
|
||||
return_value=mock_result,
|
||||
) as mock_run:
|
||||
success, output = locker._run_adb(["devices"])
|
||||
@ -48,7 +48,7 @@ class TestRunAdb:
|
||||
locker = create_locker(mock_tk, tmp_path)
|
||||
mock_result = MagicMock(returncode=1, stdout="")
|
||||
with patch(
|
||||
"python_pkg.screen_locker._phone_verification.subprocess.run",
|
||||
"screen_locker._phone_verification.subprocess.run",
|
||||
return_value=mock_result,
|
||||
):
|
||||
success, _output = locker._run_adb(["devices"])
|
||||
@ -64,7 +64,7 @@ class TestRunAdb:
|
||||
"""Test ADB binary not found."""
|
||||
locker = create_locker(mock_tk, tmp_path)
|
||||
with patch(
|
||||
"python_pkg.screen_locker._phone_verification.subprocess.run",
|
||||
"screen_locker._phone_verification.subprocess.run",
|
||||
side_effect=FileNotFoundError("adb not found"),
|
||||
):
|
||||
success, output = locker._run_adb(["devices"])
|
||||
@ -81,7 +81,7 @@ class TestRunAdb:
|
||||
"""Test ADB OSError."""
|
||||
locker = create_locker(mock_tk, tmp_path)
|
||||
with patch(
|
||||
"python_pkg.screen_locker._phone_verification.subprocess.run",
|
||||
"screen_locker._phone_verification.subprocess.run",
|
||||
side_effect=OSError("permission denied"),
|
||||
):
|
||||
success, output = locker._run_adb(["devices"])
|
||||
@ -98,7 +98,7 @@ class TestRunAdb:
|
||||
"""Test ADB command timeout."""
|
||||
locker = create_locker(mock_tk, tmp_path)
|
||||
with patch(
|
||||
"python_pkg.screen_locker._phone_verification.subprocess.run",
|
||||
"screen_locker._phone_verification.subprocess.run",
|
||||
side_effect=subprocess.TimeoutExpired("adb", 15),
|
||||
):
|
||||
success, output = locker._run_adb(["devices"])
|
||||
|
||||
@ -11,7 +11,7 @@ from typing import TYPE_CHECKING
|
||||
|
||||
import pytest
|
||||
|
||||
from python_pkg.screen_locker.tests.conftest import create_locker
|
||||
from screen_locker.tests.conftest import create_locker
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pathlib import Path
|
||||
|
||||
@ -10,8 +10,8 @@ from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from python_pkg.screen_locker.screen_lock import ScreenLocker
|
||||
from python_pkg.screen_locker.tests.conftest import (
|
||||
from screen_locker.screen_lock import ScreenLocker
|
||||
from screen_locker.tests.conftest import (
|
||||
create_locker,
|
||||
create_locker_early_bird,
|
||||
)
|
||||
@ -222,7 +222,7 @@ class TestSaveEarlyBirdLog:
|
||||
locker = create_locker(mock_tk, tmp_path)
|
||||
locker.log_file = log_file
|
||||
with patch(
|
||||
"python_pkg.screen_locker.screen_lock.compute_entry_hmac",
|
||||
"screen_locker.screen_lock.compute_entry_hmac",
|
||||
return_value=None,
|
||||
):
|
||||
locker._save_early_bird_log()
|
||||
@ -258,7 +258,7 @@ class TestTryAutoUpgradeEarlyBird:
|
||||
MagicMock(return_value=True),
|
||||
)
|
||||
with patch(
|
||||
"python_pkg.screen_locker.screen_lock.compute_entry_hmac",
|
||||
"screen_locker.screen_lock.compute_entry_hmac",
|
||||
return_value=None,
|
||||
):
|
||||
result = locker._try_auto_upgrade_early_bird()
|
||||
@ -334,7 +334,7 @@ class TestHasLoggedTodayEarlyBird:
|
||||
locker = create_locker(mock_tk, tmp_path)
|
||||
locker.log_file = log_file
|
||||
with patch(
|
||||
"python_pkg.screen_locker.screen_lock.verify_entry_hmac",
|
||||
"screen_locker.screen_lock.verify_entry_hmac",
|
||||
return_value=True,
|
||||
):
|
||||
assert locker.has_logged_today() is False
|
||||
@ -366,7 +366,7 @@ class TestInitEarlyBirdFlow:
|
||||
patch.object(ScreenLocker, "_start_phone_check"),
|
||||
patch.object(ScreenLocker, "_start_verify_workout_check"),
|
||||
patch(
|
||||
"python_pkg.screen_locker.screen_lock.has_workout_skip_today",
|
||||
"screen_locker.screen_lock.has_workout_skip_today",
|
||||
return_value=False,
|
||||
),
|
||||
pytest.raises(SystemExit),
|
||||
|
||||
@ -10,8 +10,8 @@ from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from python_pkg.screen_locker.screen_lock import _assert_not_under_pytest
|
||||
from python_pkg.screen_locker.tests.conftest import create_locker
|
||||
from screen_locker.screen_lock import _assert_not_under_pytest
|
||||
from screen_locker.tests.conftest import create_locker
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pathlib import Path
|
||||
@ -23,7 +23,7 @@ class TestAssertNotUnderPytest:
|
||||
def test_raises_when_tk_is_real(self) -> None:
|
||||
"""Guard fires if tk.Tk is the real tkinter class under pytest."""
|
||||
with (
|
||||
patch("python_pkg.screen_locker.screen_lock.tk", tk),
|
||||
patch("screen_locker.screen_lock.tk", tk),
|
||||
pytest.raises(RuntimeError, match="SAFETY"),
|
||||
):
|
||||
_assert_not_under_pytest()
|
||||
@ -128,7 +128,7 @@ class TestHasLoggedToday:
|
||||
locker = create_locker(mock_tk, tmp_path)
|
||||
locker.log_file = log_file
|
||||
with patch(
|
||||
"python_pkg.screen_locker.screen_lock.verify_entry_hmac",
|
||||
"screen_locker.screen_lock.verify_entry_hmac",
|
||||
return_value=True,
|
||||
):
|
||||
assert locker.has_logged_today() is True
|
||||
@ -149,7 +149,7 @@ class TestHasLoggedToday:
|
||||
locker = create_locker(mock_tk, tmp_path)
|
||||
locker.log_file = log_file
|
||||
with patch(
|
||||
"python_pkg.screen_locker.screen_lock.verify_entry_hmac",
|
||||
"screen_locker.screen_lock.verify_entry_hmac",
|
||||
return_value=False,
|
||||
):
|
||||
assert locker.has_logged_today() is False
|
||||
@ -171,11 +171,11 @@ class TestHasLoggedToday:
|
||||
locker.log_file = log_file
|
||||
with (
|
||||
patch(
|
||||
"python_pkg.screen_locker.screen_lock.verify_entry_hmac",
|
||||
"screen_locker.screen_lock.verify_entry_hmac",
|
||||
return_value=False,
|
||||
),
|
||||
patch(
|
||||
"python_pkg.screen_locker.screen_lock._load_hmac_key",
|
||||
"screen_locker.screen_lock._load_hmac_key",
|
||||
return_value=None,
|
||||
),
|
||||
):
|
||||
@ -198,11 +198,11 @@ class TestHasLoggedToday:
|
||||
locker.log_file = log_file
|
||||
with (
|
||||
patch(
|
||||
"python_pkg.screen_locker.screen_lock.verify_entry_hmac",
|
||||
"screen_locker.screen_lock.verify_entry_hmac",
|
||||
return_value=False,
|
||||
),
|
||||
patch(
|
||||
"python_pkg.screen_locker.screen_lock._load_hmac_key",
|
||||
"screen_locker.screen_lock._load_hmac_key",
|
||||
return_value=b"secret-key",
|
||||
),
|
||||
):
|
||||
@ -238,7 +238,7 @@ class TestSaveWorkoutLog:
|
||||
locker.log_file = log_file
|
||||
locker.workout_data = {"type": "running"}
|
||||
with patch(
|
||||
"python_pkg.screen_locker.screen_lock.compute_entry_hmac",
|
||||
"screen_locker.screen_lock.compute_entry_hmac",
|
||||
return_value="abc123",
|
||||
):
|
||||
locker.save_workout_log()
|
||||
@ -263,7 +263,7 @@ class TestSaveWorkoutLog:
|
||||
locker.log_file = log_file
|
||||
locker.workout_data = {"type": "running"}
|
||||
with patch(
|
||||
"python_pkg.screen_locker.screen_lock.compute_entry_hmac",
|
||||
"screen_locker.screen_lock.compute_entry_hmac",
|
||||
return_value=None,
|
||||
):
|
||||
locker.save_workout_log()
|
||||
@ -287,7 +287,7 @@ class TestSaveWorkoutLog:
|
||||
locker.log_file = log_file
|
||||
locker.workout_data = {"type": "strength"}
|
||||
with patch(
|
||||
"python_pkg.screen_locker.screen_lock.compute_entry_hmac",
|
||||
"screen_locker.screen_lock.compute_entry_hmac",
|
||||
return_value="sig",
|
||||
):
|
||||
locker.save_workout_log()
|
||||
@ -312,7 +312,7 @@ class TestSaveWorkoutLog:
|
||||
locker.log_file = log_file
|
||||
locker.workout_data = {"type": "running"}
|
||||
with patch(
|
||||
"python_pkg.screen_locker.screen_lock.compute_entry_hmac",
|
||||
"screen_locker.screen_lock.compute_entry_hmac",
|
||||
return_value="sig",
|
||||
):
|
||||
locker.save_workout_log()
|
||||
@ -335,7 +335,7 @@ class TestSaveWorkoutLog:
|
||||
locker.log_file = log_file
|
||||
locker.workout_data = {"type": "running"}
|
||||
with patch(
|
||||
"python_pkg.screen_locker.screen_lock.compute_entry_hmac",
|
||||
"screen_locker.screen_lock.compute_entry_hmac",
|
||||
return_value="sig",
|
||||
):
|
||||
# Should not raise, just log warning
|
||||
|
||||
@ -10,8 +10,8 @@ from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from python_pkg.screen_locker.screen_lock import ScreenLocker
|
||||
from python_pkg.screen_locker.tests.conftest import create_locker
|
||||
from screen_locker.screen_lock import ScreenLocker
|
||||
from screen_locker.tests.conftest import create_locker
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pathlib import Path
|
||||
@ -59,7 +59,7 @@ class TestAutoUpgradeSickDay:
|
||||
return_value=True,
|
||||
) as mock_adjust,
|
||||
patch(
|
||||
"python_pkg.screen_locker.screen_lock.compute_entry_hmac",
|
||||
"screen_locker.screen_lock.compute_entry_hmac",
|
||||
return_value="sig",
|
||||
),
|
||||
):
|
||||
|
||||
@ -8,14 +8,14 @@ import json
|
||||
from typing import TYPE_CHECKING
|
||||
from unittest.mock import patch
|
||||
|
||||
from python_pkg.screen_locker._log_integrity import (
|
||||
from screen_locker._log_integrity import (
|
||||
_generate_hmac_key,
|
||||
_load_hmac_key,
|
||||
compute_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:
|
||||
from pathlib import Path
|
||||
|
||||
@ -6,7 +6,7 @@ from __future__ import annotations
|
||||
from typing import TYPE_CHECKING
|
||||
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:
|
||||
from pathlib import Path
|
||||
@ -55,7 +55,7 @@ class TestVerifyPhoneWorkout:
|
||||
)
|
||||
|
||||
with patch(
|
||||
"python_pkg.screen_locker._phone_verification.check_clock_skew",
|
||||
"screen_locker._phone_verification.check_clock_skew",
|
||||
return_value=(True, "Clock OK"),
|
||||
):
|
||||
status, message = locker._verify_phone_workout()
|
||||
@ -90,7 +90,7 @@ class TestVerifyPhoneWorkout:
|
||||
)
|
||||
|
||||
with patch(
|
||||
"python_pkg.screen_locker._phone_verification.check_clock_skew",
|
||||
"screen_locker._phone_verification.check_clock_skew",
|
||||
return_value=(True, "Clock OK"),
|
||||
):
|
||||
status, message = locker._verify_phone_workout()
|
||||
@ -138,7 +138,7 @@ class TestVerifyPhoneWorkout:
|
||||
)
|
||||
|
||||
with patch(
|
||||
"python_pkg.screen_locker._phone_verification.check_clock_skew",
|
||||
"screen_locker._phone_verification.check_clock_skew",
|
||||
return_value=(True, "Clock OK"),
|
||||
):
|
||||
status, message = locker._verify_phone_workout()
|
||||
@ -162,7 +162,7 @@ class TestVerifyPhoneWorkout:
|
||||
)
|
||||
|
||||
with patch(
|
||||
"python_pkg.screen_locker._phone_verification.check_clock_skew",
|
||||
"screen_locker._phone_verification.check_clock_skew",
|
||||
return_value=(True, "Clock OK"),
|
||||
):
|
||||
status, _ = locker._verify_phone_workout()
|
||||
@ -189,7 +189,7 @@ class TestVerifyPhoneWorkout:
|
||||
)
|
||||
|
||||
with patch(
|
||||
"python_pkg.screen_locker._phone_verification.check_clock_skew",
|
||||
"screen_locker._phone_verification.check_clock_skew",
|
||||
return_value=(True, "Clock OK"),
|
||||
):
|
||||
status, message = locker._verify_phone_workout()
|
||||
@ -207,7 +207,7 @@ class TestVerifyPhoneWorkout:
|
||||
locker = create_locker(mock_tk, tmp_path)
|
||||
|
||||
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"),
|
||||
):
|
||||
status, message = locker._verify_phone_workout()
|
||||
@ -245,7 +245,7 @@ class TestVerifyPhoneWorkout:
|
||||
)
|
||||
|
||||
with patch(
|
||||
"python_pkg.screen_locker._phone_verification.check_clock_skew",
|
||||
"screen_locker._phone_verification.check_clock_skew",
|
||||
return_value=(True, "Clock OK"),
|
||||
):
|
||||
status, message = locker._verify_phone_workout()
|
||||
@ -288,7 +288,7 @@ class TestVerifyPhoneWorkout:
|
||||
)
|
||||
|
||||
with patch(
|
||||
"python_pkg.screen_locker._phone_verification.check_clock_skew",
|
||||
"screen_locker._phone_verification.check_clock_skew",
|
||||
return_value=(True, "Clock OK"),
|
||||
):
|
||||
status, message = locker._verify_phone_workout()
|
||||
|
||||
@ -6,12 +6,12 @@ from __future__ import annotations
|
||||
from typing import TYPE_CHECKING
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from python_pkg.screen_locker._constants import NO_PHONE_EXTRA_LOCKOUT_SECONDS
|
||||
from python_pkg.screen_locker.screen_lock import (
|
||||
from screen_locker._constants import NO_PHONE_EXTRA_LOCKOUT_SECONDS
|
||||
from screen_locker.screen_lock import (
|
||||
PHONE_PENALTY_DELAY_DEMO,
|
||||
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:
|
||||
from pathlib import Path
|
||||
|
||||
@ -5,7 +5,7 @@ from __future__ import annotations
|
||||
from typing import TYPE_CHECKING
|
||||
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:
|
||||
from pathlib import Path
|
||||
@ -144,7 +144,7 @@ class TestGetLocalSubnetPrefix:
|
||||
mock_sock.__enter__ = MagicMock(return_value=mock_sock)
|
||||
mock_sock.__exit__ = MagicMock(return_value=False)
|
||||
with patch(
|
||||
"python_pkg.screen_locker._phone_verification.socket.socket",
|
||||
"screen_locker._phone_verification.socket.socket",
|
||||
return_value=mock_sock,
|
||||
):
|
||||
result = locker._get_local_subnet_prefix()
|
||||
@ -159,7 +159,7 @@ class TestGetLocalSubnetPrefix:
|
||||
"""Test returns None when socket raises OSError."""
|
||||
locker = create_locker(mock_tk, tmp_path)
|
||||
with patch(
|
||||
"python_pkg.screen_locker._phone_verification.socket.socket",
|
||||
"screen_locker._phone_verification.socket.socket",
|
||||
side_effect=OSError("no network"),
|
||||
):
|
||||
result = locker._get_local_subnet_prefix()
|
||||
@ -194,7 +194,7 @@ class TestTryWirelessReconnect:
|
||||
patch.object(locker, "_try_adb_connect", return_value=True),
|
||||
patch.object(locker, "_has_adb_device", return_value=True),
|
||||
patch(
|
||||
"python_pkg.screen_locker._phone_verification.socket.create_connection",
|
||||
"screen_locker._phone_verification.socket.create_connection",
|
||||
) as mock_conn,
|
||||
):
|
||||
mock_sock = MagicMock()
|
||||
@ -215,7 +215,7 @@ class TestTryWirelessReconnect:
|
||||
with (
|
||||
patch.object(locker, "_get_local_subnet_prefix", return_value="192.168.1"),
|
||||
patch(
|
||||
"python_pkg.screen_locker._phone_verification.socket.create_connection",
|
||||
"screen_locker._phone_verification.socket.create_connection",
|
||||
side_effect=OSError("refused"),
|
||||
),
|
||||
):
|
||||
@ -235,7 +235,7 @@ class TestTryWirelessReconnect:
|
||||
patch.object(locker, "_try_adb_connect", return_value=True),
|
||||
patch.object(locker, "_has_adb_device", return_value=False),
|
||||
patch(
|
||||
"python_pkg.screen_locker._phone_verification.socket.create_connection",
|
||||
"screen_locker._phone_verification.socket.create_connection",
|
||||
) as mock_conn,
|
||||
):
|
||||
mock_sock = MagicMock()
|
||||
@ -257,7 +257,7 @@ class TestTryWirelessReconnect:
|
||||
patch.object(locker, "_get_local_subnet_prefix", return_value="192.168.1"),
|
||||
patch.object(locker, "_try_adb_connect", return_value=False),
|
||||
patch(
|
||||
"python_pkg.screen_locker._phone_verification.socket.create_connection",
|
||||
"screen_locker._phone_verification.socket.create_connection",
|
||||
) as mock_conn,
|
||||
):
|
||||
mock_sock = MagicMock()
|
||||
|
||||
@ -9,12 +9,12 @@ from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from python_pkg.screen_locker.tests.conftest import create_locker
|
||||
from screen_locker.tests.conftest import create_locker
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pathlib import Path
|
||||
|
||||
from python_pkg.screen_locker.screen_lock import ScreenLocker
|
||||
from screen_locker.screen_lock import ScreenLocker
|
||||
|
||||
|
||||
class TestIsScheduledSkipToday:
|
||||
@ -33,7 +33,7 @@ class TestIsScheduledSkipToday:
|
||||
locker = self._make_locker(mock_tk, tmp_path)
|
||||
skip_file = tmp_path / "scheduled_skips.json"
|
||||
with patch(
|
||||
"python_pkg.screen_locker.screen_lock.SCHEDULED_SKIPS_FILE",
|
||||
"screen_locker.screen_lock.SCHEDULED_SKIPS_FILE",
|
||||
skip_file,
|
||||
):
|
||||
assert locker._is_scheduled_skip_today() is False
|
||||
@ -50,7 +50,7 @@ class TestIsScheduledSkipToday:
|
||||
skip_file = tmp_path / "scheduled_skips.json"
|
||||
skip_file.write_text(json.dumps([today]))
|
||||
with patch(
|
||||
"python_pkg.screen_locker.screen_lock.SCHEDULED_SKIPS_FILE",
|
||||
"screen_locker.screen_lock.SCHEDULED_SKIPS_FILE",
|
||||
skip_file,
|
||||
):
|
||||
assert locker._is_scheduled_skip_today() is True
|
||||
@ -66,7 +66,7 @@ class TestIsScheduledSkipToday:
|
||||
skip_file = tmp_path / "scheduled_skips.json"
|
||||
skip_file.write_text(json.dumps(["1999-01-01", "2000-06-15"]))
|
||||
with patch(
|
||||
"python_pkg.screen_locker.screen_lock.SCHEDULED_SKIPS_FILE",
|
||||
"screen_locker.screen_lock.SCHEDULED_SKIPS_FILE",
|
||||
skip_file,
|
||||
):
|
||||
assert locker._is_scheduled_skip_today() is False
|
||||
@ -82,7 +82,7 @@ class TestIsScheduledSkipToday:
|
||||
skip_file = tmp_path / "scheduled_skips.json"
|
||||
skip_file.write_text("{not valid json}")
|
||||
with patch(
|
||||
"python_pkg.screen_locker.screen_lock.SCHEDULED_SKIPS_FILE",
|
||||
"screen_locker.screen_lock.SCHEDULED_SKIPS_FILE",
|
||||
skip_file,
|
||||
):
|
||||
assert locker._is_scheduled_skip_today() is False
|
||||
@ -99,7 +99,7 @@ class TestIsScheduledSkipToday:
|
||||
skip_file.write_text("[]")
|
||||
with (
|
||||
patch(
|
||||
"python_pkg.screen_locker.screen_lock.SCHEDULED_SKIPS_FILE",
|
||||
"screen_locker.screen_lock.SCHEDULED_SKIPS_FILE",
|
||||
skip_file,
|
||||
),
|
||||
patch("builtins.open", side_effect=OSError("permission denied")),
|
||||
@ -117,7 +117,7 @@ class TestIsScheduledSkipToday:
|
||||
skip_file = tmp_path / "scheduled_skips.json"
|
||||
skip_file.write_text("[]")
|
||||
with patch(
|
||||
"python_pkg.screen_locker.screen_lock.SCHEDULED_SKIPS_FILE",
|
||||
"screen_locker.screen_lock.SCHEDULED_SKIPS_FILE",
|
||||
skip_file,
|
||||
):
|
||||
assert locker._is_scheduled_skip_today() is False
|
||||
|
||||
@ -6,7 +6,7 @@ import json
|
||||
from typing import TYPE_CHECKING
|
||||
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:
|
||||
from pathlib import Path
|
||||
@ -228,7 +228,7 @@ class TestSickModeUsedToday:
|
||||
mock_file = MagicMock()
|
||||
mock_file.exists.return_value = False
|
||||
with patch(
|
||||
"python_pkg.screen_locker._shutdown.SICK_DAY_STATE_FILE",
|
||||
"screen_locker._shutdown.SICK_DAY_STATE_FILE",
|
||||
mock_file,
|
||||
):
|
||||
assert locker._sick_mode_used_today() is False
|
||||
@ -243,7 +243,7 @@ class TestSickModeUsedToday:
|
||||
locker = create_locker(mock_tk, tmp_path)
|
||||
state_file = tmp_path / "state.json"
|
||||
with patch(
|
||||
"python_pkg.screen_locker._shutdown.SICK_DAY_STATE_FILE",
|
||||
"screen_locker._shutdown.SICK_DAY_STATE_FILE",
|
||||
state_file,
|
||||
):
|
||||
from datetime import datetime, timezone
|
||||
@ -262,7 +262,7 @@ class TestSickModeUsedToday:
|
||||
locker = create_locker(mock_tk, tmp_path)
|
||||
state_file = tmp_path / "state.json"
|
||||
with patch(
|
||||
"python_pkg.screen_locker._shutdown.SICK_DAY_STATE_FILE",
|
||||
"screen_locker._shutdown.SICK_DAY_STATE_FILE",
|
||||
state_file,
|
||||
):
|
||||
state_file.write_text(json.dumps({"date": "2020-01-01"}))
|
||||
@ -278,7 +278,7 @@ class TestSickModeUsedToday:
|
||||
locker = create_locker(mock_tk, tmp_path)
|
||||
state_file = tmp_path / "state.json"
|
||||
with patch(
|
||||
"python_pkg.screen_locker._shutdown.SICK_DAY_STATE_FILE",
|
||||
"screen_locker._shutdown.SICK_DAY_STATE_FILE",
|
||||
state_file,
|
||||
):
|
||||
state_file.write_text("not json{{{")
|
||||
@ -298,7 +298,7 @@ class TestSaveSickDayState:
|
||||
locker = create_locker(mock_tk, tmp_path)
|
||||
state_file = tmp_path / "state.json"
|
||||
with patch(
|
||||
"python_pkg.screen_locker._shutdown.SICK_DAY_STATE_FILE",
|
||||
"screen_locker._shutdown.SICK_DAY_STATE_FILE",
|
||||
state_file,
|
||||
):
|
||||
result = locker._save_sick_day_state("2026-03-21", 21, 20)
|
||||
@ -319,7 +319,7 @@ class TestSaveSickDayState:
|
||||
mock_path = MagicMock()
|
||||
mock_path.open.side_effect = OSError("permission denied")
|
||||
with patch(
|
||||
"python_pkg.screen_locker._shutdown.SICK_DAY_STATE_FILE",
|
||||
"screen_locker._shutdown.SICK_DAY_STATE_FILE",
|
||||
mock_path,
|
||||
):
|
||||
result = locker._save_sick_day_state("2026-03-21", 21, 20)
|
||||
@ -348,7 +348,7 @@ class TestLoadSickDayState:
|
||||
)
|
||||
)
|
||||
with patch(
|
||||
"python_pkg.screen_locker._shutdown.SICK_DAY_STATE_FILE",
|
||||
"screen_locker._shutdown.SICK_DAY_STATE_FILE",
|
||||
state_file,
|
||||
):
|
||||
result = locker._load_sick_day_state()
|
||||
@ -365,7 +365,7 @@ class TestLoadSickDayState:
|
||||
state_file = tmp_path / "state.json"
|
||||
state_file.write_text(json.dumps({"date": "2026-03-20"}))
|
||||
with patch(
|
||||
"python_pkg.screen_locker._shutdown.SICK_DAY_STATE_FILE",
|
||||
"screen_locker._shutdown.SICK_DAY_STATE_FILE",
|
||||
state_file,
|
||||
):
|
||||
result = locker._load_sick_day_state()
|
||||
@ -391,7 +391,7 @@ class TestWriteRestoredConfig:
|
||||
locker, "_write_shutdown_config", return_value=True
|
||||
) as mock_write,
|
||||
patch(
|
||||
"python_pkg.screen_locker._shutdown.SICK_DAY_STATE_FILE",
|
||||
"screen_locker._shutdown.SICK_DAY_STATE_FILE",
|
||||
state_file,
|
||||
),
|
||||
):
|
||||
@ -412,7 +412,7 @@ class TestWriteRestoredConfig:
|
||||
with (
|
||||
patch.object(locker, "_read_shutdown_config", return_value=None),
|
||||
patch(
|
||||
"python_pkg.screen_locker._shutdown.SICK_DAY_STATE_FILE",
|
||||
"screen_locker._shutdown.SICK_DAY_STATE_FILE",
|
||||
state_file,
|
||||
),
|
||||
):
|
||||
|
||||
@ -7,8 +7,8 @@ import subprocess
|
||||
from typing import TYPE_CHECKING
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from python_pkg.screen_locker._constants import ADJUST_SHUTDOWN_SCRIPT
|
||||
from python_pkg.screen_locker.tests.conftest import create_locker
|
||||
from screen_locker._constants import ADJUST_SHUTDOWN_SCRIPT
|
||||
from screen_locker.tests.conftest import create_locker
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pathlib import Path
|
||||
@ -28,7 +28,7 @@ class TestRestoreOriginalConfigIfNeeded:
|
||||
mock_file = MagicMock()
|
||||
mock_file.exists.return_value = False
|
||||
with patch(
|
||||
"python_pkg.screen_locker._shutdown.SICK_DAY_STATE_FILE",
|
||||
"screen_locker._shutdown.SICK_DAY_STATE_FILE",
|
||||
mock_file,
|
||||
):
|
||||
locker._restore_original_config_if_needed()
|
||||
@ -53,7 +53,7 @@ class TestRestoreOriginalConfigIfNeeded:
|
||||
)
|
||||
with (
|
||||
patch(
|
||||
"python_pkg.screen_locker._shutdown.SICK_DAY_STATE_FILE",
|
||||
"screen_locker._shutdown.SICK_DAY_STATE_FILE",
|
||||
state_file,
|
||||
),
|
||||
patch.object(locker, "_write_restored_config") as mock_restore,
|
||||
@ -84,7 +84,7 @@ class TestRestoreOriginalConfigIfNeeded:
|
||||
)
|
||||
with (
|
||||
patch(
|
||||
"python_pkg.screen_locker._shutdown.SICK_DAY_STATE_FILE",
|
||||
"screen_locker._shutdown.SICK_DAY_STATE_FILE",
|
||||
state_file,
|
||||
),
|
||||
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"}))
|
||||
with (
|
||||
patch(
|
||||
"python_pkg.screen_locker._shutdown.SICK_DAY_STATE_FILE",
|
||||
"screen_locker._shutdown.SICK_DAY_STATE_FILE",
|
||||
state_file,
|
||||
),
|
||||
patch.object(locker, "_write_restored_config") as mock_restore,
|
||||
@ -124,7 +124,7 @@ class TestRestoreOriginalConfigIfNeeded:
|
||||
mock_file.exists.return_value = True
|
||||
mock_file.open.side_effect = OSError("fail")
|
||||
with patch(
|
||||
"python_pkg.screen_locker._shutdown.SICK_DAY_STATE_FILE",
|
||||
"screen_locker._shutdown.SICK_DAY_STATE_FILE",
|
||||
mock_file,
|
||||
):
|
||||
locker._restore_original_config_if_needed()
|
||||
@ -140,7 +140,7 @@ class TestRestoreOriginalConfigIfNeeded:
|
||||
state_file = tmp_path / "state.json"
|
||||
state_file.write_text("not valid json{{{")
|
||||
with patch(
|
||||
"python_pkg.screen_locker._shutdown.SICK_DAY_STATE_FILE",
|
||||
"screen_locker._shutdown.SICK_DAY_STATE_FILE",
|
||||
state_file,
|
||||
):
|
||||
locker._restore_original_config_if_needed()
|
||||
@ -160,7 +160,7 @@ class TestReadShutdownConfig:
|
||||
mock_file = MagicMock()
|
||||
mock_file.exists.return_value = False
|
||||
with patch(
|
||||
"python_pkg.screen_locker._shutdown.SHUTDOWN_CONFIG_FILE",
|
||||
"screen_locker._shutdown.SHUTDOWN_CONFIG_FILE",
|
||||
mock_file,
|
||||
):
|
||||
assert locker._read_shutdown_config() is None
|
||||
@ -176,7 +176,7 @@ class TestReadShutdownConfig:
|
||||
config_file = tmp_path / "shutdown.conf"
|
||||
config_file.write_text("MON_WED_HOUR=21\nTHU_SUN_HOUR=20\nMORNING_END_HOUR=8\n")
|
||||
with patch(
|
||||
"python_pkg.screen_locker._shutdown.SHUTDOWN_CONFIG_FILE",
|
||||
"screen_locker._shutdown.SHUTDOWN_CONFIG_FILE",
|
||||
config_file,
|
||||
):
|
||||
result = locker._read_shutdown_config()
|
||||
@ -193,7 +193,7 @@ class TestReadShutdownConfig:
|
||||
config_file = tmp_path / "shutdown.conf"
|
||||
config_file.write_text("MON_WED_HOUR=21\n")
|
||||
with patch(
|
||||
"python_pkg.screen_locker._shutdown.SHUTDOWN_CONFIG_FILE",
|
||||
"screen_locker._shutdown.SHUTDOWN_CONFIG_FILE",
|
||||
config_file,
|
||||
):
|
||||
result = locker._read_shutdown_config()
|
||||
@ -253,7 +253,7 @@ class TestWriteShutdownConfig:
|
||||
mock_script = MagicMock()
|
||||
mock_script.exists.return_value = False
|
||||
with patch(
|
||||
"python_pkg.screen_locker._shutdown.ADJUST_SHUTDOWN_SCRIPT",
|
||||
"screen_locker._shutdown.ADJUST_SHUTDOWN_SCRIPT",
|
||||
mock_script,
|
||||
):
|
||||
result = locker._write_shutdown_config(21, 20, 8)
|
||||
@ -271,7 +271,7 @@ class TestWriteShutdownConfig:
|
||||
mock_script.exists.return_value = True
|
||||
with (
|
||||
patch(
|
||||
"python_pkg.screen_locker._shutdown.ADJUST_SHUTDOWN_SCRIPT",
|
||||
"screen_locker._shutdown.ADJUST_SHUTDOWN_SCRIPT",
|
||||
mock_script,
|
||||
),
|
||||
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)
|
||||
mock_result = MagicMock(stdout="OK\n")
|
||||
with patch(
|
||||
"python_pkg.screen_locker._shutdown.subprocess.run",
|
||||
"screen_locker._shutdown.subprocess.run",
|
||||
return_value=mock_result,
|
||||
):
|
||||
result = locker._run_shutdown_cmd(["cmd"], 21, 20)
|
||||
@ -309,7 +309,7 @@ class TestRunShutdownCmd:
|
||||
"""Test returns False on SubprocessError."""
|
||||
locker = create_locker(mock_tk, tmp_path)
|
||||
with patch(
|
||||
"python_pkg.screen_locker._shutdown.subprocess.run",
|
||||
"screen_locker._shutdown.subprocess.run",
|
||||
side_effect=subprocess.CalledProcessError(1, "cmd"),
|
||||
):
|
||||
result = locker._run_shutdown_cmd(["cmd"], 21, 20)
|
||||
|
||||
@ -6,9 +6,9 @@ from __future__ import annotations
|
||||
from typing import TYPE_CHECKING
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from python_pkg.screen_locker import _sick_tracker
|
||||
from python_pkg.screen_locker._sick_tracker import SickHistory
|
||||
from python_pkg.screen_locker.tests.conftest import create_locker
|
||||
from screen_locker import _sick_tracker
|
||||
from screen_locker._sick_tracker import SickHistory
|
||||
from screen_locker.tests.conftest import create_locker
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pathlib import Path
|
||||
@ -439,7 +439,7 @@ class TestDisablePaste:
|
||||
"""Tests for the _disable_paste helper."""
|
||||
|
||||
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()
|
||||
import tkinter as tk
|
||||
|
||||
@ -8,8 +8,8 @@ from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from python_pkg.screen_locker import _sick_tracker
|
||||
from python_pkg.screen_locker._constants import (
|
||||
from screen_locker import _sick_tracker
|
||||
from screen_locker._constants import (
|
||||
SICK_BUDGET_PER_7_DAYS,
|
||||
SICK_BUDGET_PER_30_DAYS,
|
||||
SICK_BUDGET_PER_90_DAYS,
|
||||
@ -19,7 +19,7 @@ from python_pkg.screen_locker._constants import (
|
||||
SICK_LOCKOUT_MULTIPLIER_PER_RECENT,
|
||||
SICK_LOCKOUT_SECONDS,
|
||||
)
|
||||
from python_pkg.screen_locker._sick_tracker import (
|
||||
from screen_locker._sick_tracker import (
|
||||
JustificationDraft,
|
||||
SickHistory,
|
||||
add_justification,
|
||||
|
||||
@ -6,7 +6,7 @@ import struct
|
||||
import time
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from python_pkg.screen_locker._time_check import (
|
||||
from screen_locker._time_check import (
|
||||
_NTP_EPOCH_OFFSET,
|
||||
_query_ntp_offset,
|
||||
check_clock_skew,
|
||||
@ -66,7 +66,7 @@ class TestCheckClockSkew:
|
||||
def test_ok_within_threshold(self) -> None:
|
||||
"""Test returns ok when clock offset is small."""
|
||||
with patch(
|
||||
"python_pkg.screen_locker._time_check._query_ntp_offset",
|
||||
"screen_locker._time_check._query_ntp_offset",
|
||||
return_value=2.5,
|
||||
):
|
||||
ok, message = check_clock_skew()
|
||||
@ -77,7 +77,7 @@ class TestCheckClockSkew:
|
||||
def test_fails_when_skew_exceeds_threshold(self) -> None:
|
||||
"""Test returns failure when clock offset exceeds max."""
|
||||
with patch(
|
||||
"python_pkg.screen_locker._time_check._query_ntp_offset",
|
||||
"screen_locker._time_check._query_ntp_offset",
|
||||
return_value=600.0,
|
||||
):
|
||||
ok, message = check_clock_skew()
|
||||
@ -88,7 +88,7 @@ class TestCheckClockSkew:
|
||||
def test_ntp_unreachable_passes(self) -> None:
|
||||
"""Test returns ok when NTP server is unreachable (fail-open)."""
|
||||
with patch(
|
||||
"python_pkg.screen_locker._time_check._query_ntp_offset",
|
||||
"screen_locker._time_check._query_ntp_offset",
|
||||
return_value=None,
|
||||
):
|
||||
ok, message = check_clock_skew()
|
||||
@ -99,7 +99,7 @@ class TestCheckClockSkew:
|
||||
def test_negative_offset_detected(self) -> None:
|
||||
"""Test detects clock ahead with negative offset."""
|
||||
with patch(
|
||||
"python_pkg.screen_locker._time_check._query_ntp_offset",
|
||||
"screen_locker._time_check._query_ntp_offset",
|
||||
return_value=-400.0,
|
||||
):
|
||||
ok, message = check_clock_skew()
|
||||
|
||||
@ -5,7 +5,7 @@ from __future__ import annotations
|
||||
from typing import TYPE_CHECKING
|
||||
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:
|
||||
from pathlib import Path
|
||||
|
||||
@ -5,11 +5,11 @@ from __future__ import annotations
|
||||
from typing import TYPE_CHECKING
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from python_pkg.screen_locker._sick_tracker import SickHistory
|
||||
from python_pkg.screen_locker.screen_lock import (
|
||||
from screen_locker._sick_tracker import SickHistory
|
||||
from screen_locker.screen_lock import (
|
||||
SICK_LOCKOUT_SECONDS,
|
||||
)
|
||||
from python_pkg.screen_locker.tests.conftest import create_locker
|
||||
from screen_locker.tests.conftest import create_locker
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pathlib import Path
|
||||
|
||||
@ -5,7 +5,7 @@ from __future__ import annotations
|
||||
from typing import TYPE_CHECKING
|
||||
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:
|
||||
from pathlib import Path
|
||||
|
||||
@ -9,7 +9,7 @@ from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from python_pkg.screen_locker.tests.conftest import create_locker
|
||||
from screen_locker.tests.conftest import create_locker
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pathlib import Path
|
||||
|
||||
@ -5,7 +5,7 @@ from __future__ import annotations
|
||||
from typing import TYPE_CHECKING
|
||||
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:
|
||||
from pathlib import Path
|
||||
@ -109,7 +109,7 @@ class TestVTSwitching:
|
||||
) -> None:
|
||||
"""No crash and no subprocess call when setxkbmap is not installed."""
|
||||
with patch(
|
||||
"python_pkg.screen_locker._window_setup.shutil.which",
|
||||
"screen_locker._window_setup.shutil.which",
|
||||
return_value=None,
|
||||
):
|
||||
create_locker(mock_tk, tmp_path, demo_mode=False)
|
||||
@ -128,7 +128,7 @@ class TestVTSwitching:
|
||||
mock_subprocess_run.reset_mock()
|
||||
|
||||
with patch(
|
||||
"python_pkg.screen_locker._window_setup.shutil.which",
|
||||
"screen_locker._window_setup.shutil.which",
|
||||
return_value=None,
|
||||
):
|
||||
locker.close()
|
||||
|
||||
@ -5,7 +5,7 @@ from __future__ import annotations
|
||||
from typing import TYPE_CHECKING
|
||||
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:
|
||||
from pathlib import Path
|
||||
@ -26,7 +26,7 @@ class TestIsTomorrowAlarmDay:
|
||||
|
||||
# Sunday 2026-04-12 → tomorrow Monday
|
||||
with patch(
|
||||
"python_pkg.screen_locker._shutdown.datetime",
|
||||
"screen_locker._shutdown.datetime",
|
||||
) as mock_dt:
|
||||
mock_dt.now.return_value = datetime(2026, 4, 12, 23, 0, tzinfo=timezone.utc)
|
||||
mock_dt.side_effect = datetime
|
||||
@ -34,7 +34,7 @@ class TestIsTomorrowAlarmDay:
|
||||
|
||||
# Ensure timedelta works
|
||||
with patch(
|
||||
"python_pkg.screen_locker._shutdown.timedelta",
|
||||
"screen_locker._shutdown.timedelta",
|
||||
timedelta,
|
||||
):
|
||||
assert locker._is_tomorrow_alarm_day() is True
|
||||
@ -52,10 +52,10 @@ class TestIsTomorrowAlarmDay:
|
||||
# Monday 2026-04-13 → tomorrow Tuesday (weekday=1)
|
||||
with (
|
||||
patch(
|
||||
"python_pkg.screen_locker._shutdown.datetime",
|
||||
"screen_locker._shutdown.datetime",
|
||||
) as mock_dt,
|
||||
patch(
|
||||
"python_pkg.screen_locker._shutdown.timedelta",
|
||||
"screen_locker._shutdown.timedelta",
|
||||
timedelta,
|
||||
),
|
||||
):
|
||||
@ -76,10 +76,10 @@ class TestIsTomorrowAlarmDay:
|
||||
# Thursday 2026-04-16 → tomorrow Friday (weekday=4)
|
||||
with (
|
||||
patch(
|
||||
"python_pkg.screen_locker._shutdown.datetime",
|
||||
"screen_locker._shutdown.datetime",
|
||||
) as mock_dt,
|
||||
patch(
|
||||
"python_pkg.screen_locker._shutdown.timedelta",
|
||||
"screen_locker._shutdown.timedelta",
|
||||
timedelta,
|
||||
),
|
||||
):
|
||||
@ -100,7 +100,7 @@ class TestScheduleRtcwake:
|
||||
"""Successful rtcwake call returns True."""
|
||||
locker = create_locker(mock_tk, tmp_path)
|
||||
with patch(
|
||||
"python_pkg.screen_locker._shutdown.subprocess.run",
|
||||
"screen_locker._shutdown.subprocess.run",
|
||||
) as mock_run:
|
||||
mock_run.return_value = MagicMock(returncode=0)
|
||||
assert locker._schedule_rtcwake() is True
|
||||
@ -119,7 +119,7 @@ class TestScheduleRtcwake:
|
||||
import subprocess
|
||||
|
||||
with patch(
|
||||
"python_pkg.screen_locker._shutdown.subprocess.run",
|
||||
"screen_locker._shutdown.subprocess.run",
|
||||
side_effect=subprocess.SubprocessError("rtcwake failed"),
|
||||
):
|
||||
assert locker._schedule_rtcwake() is False
|
||||
|
||||
@ -5,7 +5,7 @@ from __future__ import annotations
|
||||
from typing import TYPE_CHECKING
|
||||
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:
|
||||
from pathlib import Path
|
||||
@ -22,7 +22,7 @@ class TestWakeSkipIntegration:
|
||||
) -> None:
|
||||
"""Screen locker exits if wake alarm granted workout skip today."""
|
||||
with patch(
|
||||
"python_pkg.screen_locker.screen_lock.has_workout_skip_today",
|
||||
"screen_locker.screen_lock.has_workout_skip_today",
|
||||
return_value=True,
|
||||
):
|
||||
create_locker(mock_tk, tmp_path, has_logged=False)
|
||||
@ -37,7 +37,7 @@ class TestWakeSkipIntegration:
|
||||
) -> None:
|
||||
"""Screen locker proceeds normally if no wake skip active."""
|
||||
with patch(
|
||||
"python_pkg.screen_locker.screen_lock.has_workout_skip_today",
|
||||
"screen_locker.screen_lock.has_workout_skip_today",
|
||||
return_value=False,
|
||||
):
|
||||
locker = create_locker(mock_tk, tmp_path, has_logged=False)
|
||||
@ -53,7 +53,7 @@ class TestWakeSkipIntegration:
|
||||
) -> None:
|
||||
"""has_logged_today exits before wake skip is even checked."""
|
||||
with patch(
|
||||
"python_pkg.screen_locker.screen_lock.has_workout_skip_today",
|
||||
"screen_locker.screen_lock.has_workout_skip_today",
|
||||
return_value=True,
|
||||
):
|
||||
create_locker(mock_tk, tmp_path, has_logged=True)
|
||||
@ -69,7 +69,7 @@ class TestWakeSkipIntegration:
|
||||
) -> None:
|
||||
"""verify_only mode checks sick day log, not wake skip."""
|
||||
with patch(
|
||||
"python_pkg.screen_locker.screen_lock.has_workout_skip_today",
|
||||
"screen_locker.screen_lock.has_workout_skip_today",
|
||||
return_value=True,
|
||||
):
|
||||
create_locker(
|
||||
|
||||
203
screen_locker/tests/test_wake_state.py
Normal file
203
screen_locker/tests/test_wake_state.py
Normal 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
|
||||
@ -8,7 +8,7 @@ import json
|
||||
from typing import TYPE_CHECKING, Any
|
||||
from unittest.mock import patch
|
||||
|
||||
from python_pkg.screen_locker._weekly_check import (
|
||||
from screen_locker._weekly_check import (
|
||||
_RELAXED_WEEKDAYS,
|
||||
WEEKLY_WORKOUT_MINIMUM,
|
||||
count_weekly_workouts,
|
||||
|
||||
@ -5,8 +5,8 @@ from __future__ import annotations
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from python_pkg.screen_locker.screen_lock import ScreenLocker
|
||||
from python_pkg.screen_locker.tests.conftest import (
|
||||
from screen_locker.screen_lock import ScreenLocker
|
||||
from screen_locker.tests.conftest import (
|
||||
create_locker,
|
||||
create_locker_relaxed_day,
|
||||
)
|
||||
@ -43,11 +43,11 @@ class TestRelaxedDayBranch:
|
||||
ScreenLocker, "_try_auto_upgrade_early_bird", return_value=False
|
||||
),
|
||||
patch(
|
||||
"python_pkg.screen_locker.screen_lock.is_relaxed_day",
|
||||
"screen_locker.screen_lock.is_relaxed_day",
|
||||
return_value=True,
|
||||
),
|
||||
patch(
|
||||
"python_pkg.screen_locker.screen_lock.has_weekly_minimum",
|
||||
"screen_locker.screen_lock.has_weekly_minimum",
|
||||
return_value=False,
|
||||
),
|
||||
patch.object(ScreenLocker, "_start_phone_check") as mock_phone,
|
||||
@ -75,11 +75,11 @@ class TestRelaxedDayBranch:
|
||||
ScreenLocker, "_try_auto_upgrade_early_bird", return_value=False
|
||||
),
|
||||
patch(
|
||||
"python_pkg.screen_locker.screen_lock.is_relaxed_day",
|
||||
"screen_locker.screen_lock.is_relaxed_day",
|
||||
return_value=True,
|
||||
),
|
||||
patch(
|
||||
"python_pkg.screen_locker.screen_lock.has_weekly_minimum",
|
||||
"screen_locker.screen_lock.has_weekly_minimum",
|
||||
return_value=False,
|
||||
),
|
||||
patch.object(ScreenLocker, "_setup_window") as mock_full,
|
||||
@ -109,11 +109,11 @@ class TestRelaxedDayBranch:
|
||||
ScreenLocker, "_try_auto_upgrade_early_bird", return_value=False
|
||||
),
|
||||
patch(
|
||||
"python_pkg.screen_locker.screen_lock.is_relaxed_day",
|
||||
"screen_locker.screen_lock.is_relaxed_day",
|
||||
return_value=True,
|
||||
),
|
||||
patch(
|
||||
"python_pkg.screen_locker.screen_lock.has_weekly_minimum",
|
||||
"screen_locker.screen_lock.has_weekly_minimum",
|
||||
return_value=False,
|
||||
),
|
||||
patch.object(ScreenLocker, "_grab_input") as mock_grab,
|
||||
@ -148,7 +148,7 @@ class TestWeeklyMinimumBranch:
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
with patch(
|
||||
"python_pkg.screen_locker.screen_lock.has_weekly_minimum",
|
||||
"screen_locker.screen_lock.has_weekly_minimum",
|
||||
return_value=True,
|
||||
):
|
||||
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
|
||||
# and _relaxed_day_mode stays False (full lock path taken).
|
||||
with patch(
|
||||
"python_pkg.screen_locker.screen_lock.has_weekly_minimum",
|
||||
"screen_locker.screen_lock.has_weekly_minimum",
|
||||
return_value=False,
|
||||
):
|
||||
locker = create_locker(mock_tk, tmp_path, has_logged=False)
|
||||
@ -179,7 +179,7 @@ class TestWeeklyMinimumBranch:
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
with patch(
|
||||
"python_pkg.screen_locker.screen_lock.has_weekly_minimum",
|
||||
"screen_locker.screen_lock.has_weekly_minimum",
|
||||
) as mock_weekly:
|
||||
create_locker_relaxed_day(mock_tk, tmp_path)
|
||||
|
||||
@ -192,7 +192,7 @@ class TestWeeklyMinimumBranch:
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
with patch(
|
||||
"python_pkg.screen_locker.screen_lock.has_weekly_minimum",
|
||||
"screen_locker.screen_lock.has_weekly_minimum",
|
||||
) as mock_weekly:
|
||||
create_locker(mock_tk, tmp_path, has_logged=True)
|
||||
|
||||
@ -217,7 +217,7 @@ class TestStartRelaxedDayFlow:
|
||||
locker = self._make_locker(mock_tk, tmp_path)
|
||||
with (
|
||||
patch(
|
||||
"python_pkg.screen_locker._ui_flows.count_weekly_workouts",
|
||||
"screen_locker._ui_flows.count_weekly_workouts",
|
||||
return_value=2,
|
||||
),
|
||||
patch.object(locker, "_text") as mock_text,
|
||||
@ -241,7 +241,7 @@ class TestStartRelaxedDayFlow:
|
||||
locker = self._make_locker(mock_tk, tmp_path)
|
||||
with (
|
||||
patch(
|
||||
"python_pkg.screen_locker._ui_flows.count_weekly_workouts",
|
||||
"screen_locker._ui_flows.count_weekly_workouts",
|
||||
return_value=0,
|
||||
),
|
||||
patch.object(locker, "_button") as mock_button,
|
||||
@ -268,7 +268,7 @@ class TestStartRelaxedDayFlow:
|
||||
locker = self._make_locker(mock_tk, tmp_path)
|
||||
with (
|
||||
patch(
|
||||
"python_pkg.screen_locker._ui_flows.count_weekly_workouts",
|
||||
"screen_locker._ui_flows.count_weekly_workouts",
|
||||
return_value=1,
|
||||
),
|
||||
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, "has_logged_today", return_value=False),
|
||||
patch(
|
||||
"python_pkg.screen_locker.screen_lock.has_workout_skip_today",
|
||||
"screen_locker.screen_lock.has_workout_skip_today",
|
||||
return_value=True,
|
||||
),
|
||||
):
|
||||
@ -553,7 +553,7 @@ class TestCheckTodayStateExits:
|
||||
patch.object(locker, "_is_sick_day_log", return_value=False),
|
||||
patch.object(locker, "has_logged_today", return_value=False),
|
||||
patch(
|
||||
"python_pkg.screen_locker.screen_lock.has_workout_skip_today",
|
||||
"screen_locker.screen_lock.has_workout_skip_today",
|
||||
return_value=False,
|
||||
),
|
||||
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, "has_logged_today", return_value=False),
|
||||
patch(
|
||||
"python_pkg.screen_locker.screen_lock.has_workout_skip_today",
|
||||
"screen_locker.screen_lock.has_workout_skip_today",
|
||||
return_value=False,
|
||||
),
|
||||
patch.object(locker, "_is_early_bird_time", return_value=False),
|
||||
|
||||
@ -4,11 +4,11 @@ After=graphical-session.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
WorkingDirectory=/home/kuhy/testsAndMisc
|
||||
WorkingDirectory=/opt/screen-locker
|
||||
Environment=DISPLAY=:0
|
||||
Environment=PYTHONPATH=/home/kuhy/testsAndMisc
|
||||
Environment=PYTHONPATH=/opt/screen-locker
|
||||
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
|
||||
RestartSec=2s
|
||||
RestartPreventExitStatus=0
|
||||
Loading…
Reference in New Issue
Block a user