mirror of
https://github.com/kuhyx/screen-locker.git
synced 2026-11-10 20:47:14 +01:00
Split screen_lock.py under the 250-line cap
Some checks are pending
pre-commit / pre-commit (push) Waiting to run
Tests / test (3.10) (push) Waiting to run
Tests / test (3.11) (push) Waiting to run
Tests / test (3.12) (push) Waiting to run
Tests / lock-popup-safety (push) Waiting to run
release-apk / release (APK) (push) Waiting to run
Some checks are pending
pre-commit / pre-commit (push) Waiting to run
Tests / test (3.10) (push) Waiting to run
Tests / test (3.11) (push) Waiting to run
Tests / test (3.12) (push) Waiting to run
Tests / lock-popup-safety (push) Waiting to run
release-apk / release (APK) (push) Waiting to run
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
c63a02f9eb
commit
4b4695e23a
60
screen_locker/_cli.py
Normal file
60
screen_locker/_cli.py
Normal file
@ -0,0 +1,60 @@
|
||||
"""Command-line entry points for ``screen_lock.py``.
|
||||
|
||||
Split out of ``screen_lock.py`` to keep every file under the 250-line cap.
|
||||
The argument handling lives here; ``screen_lock.py`` keeps only the
|
||||
``ScreenLocker`` class and a one-line ``__main__`` delegation, so the module
|
||||
that every mixin imports stays small.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from pathlib import Path
|
||||
import sys
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from screen_locker._status import run_status
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from screen_locker.screen_lock import ScreenLocker
|
||||
|
||||
__all__ = ["main"]
|
||||
|
||||
_LOG_FILE_NAME = "workout_log.json"
|
||||
|
||||
|
||||
def _headless_locker(locker_cls: type[ScreenLocker]) -> ScreenLocker:
|
||||
"""A ScreenLocker with ``__init__`` bypassed, for the no-UI subcommands.
|
||||
|
||||
``--status`` and ``--sync-only`` touch only ``log_file`` and
|
||||
``workout_data``; constructing the real object would build a Tk UI and,
|
||||
in the ``--production`` case, grab the screen.
|
||||
"""
|
||||
locker = object.__new__(locker_cls)
|
||||
locker.log_file = Path(__file__).resolve().parent / _LOG_FILE_NAME
|
||||
locker.workout_data = {}
|
||||
return locker
|
||||
|
||||
|
||||
def main(locker_cls: type[ScreenLocker], argv: list[str]) -> None:
|
||||
"""Dispatch on ``argv`` and run the requested mode."""
|
||||
if "--status" in argv:
|
||||
run_status(_headless_locker(locker_cls))
|
||||
|
||||
if "--sync-only" in argv:
|
||||
# Headless sync for the timer unit: pull other devices' workouts and
|
||||
# apply their credit, with no Tk and no lock screen. Sync used to run
|
||||
# ONLY at process start, so a workout finished after login was not seen
|
||||
# until the next login — the timer closes that window.
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(levelname)s %(name)s: %(message)s",
|
||||
)
|
||||
_headless_locker(locker_cls).sync_now()
|
||||
sys.exit(0)
|
||||
|
||||
locker = locker_cls(
|
||||
demo_mode="--production" not in argv,
|
||||
verify_only="--verify-workout" in argv,
|
||||
)
|
||||
locker.run()
|
||||
126
screen_locker/_startup_checks.py
Normal file
126
screen_locker/_startup_checks.py
Normal file
@ -0,0 +1,126 @@
|
||||
"""Startup early-exit checks for the screen locker.
|
||||
|
||||
Split out of ``screen_lock.py`` to keep every file under the 250-line cap.
|
||||
These run before any lock window is built and decide whether the lock is
|
||||
needed at all: scheduled skip, weekly minimum already met, relaxed day, or
|
||||
genuine extreme heat.
|
||||
|
||||
The entry point is ``_check_non_verify_exits``, which
|
||||
``_auto_upgrade.AutoUpgradeMixin._check_early_exits`` calls.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import sys
|
||||
|
||||
from screen_locker._constants import (
|
||||
EXTRA_BENEFITS_FILE,
|
||||
HEAT_SKIP_CITY,
|
||||
HEAT_SKIP_TEMP_THRESHOLD,
|
||||
SHUTDOWN_BASE_FILE,
|
||||
SICK_DAY_STATE_FILE,
|
||||
)
|
||||
from screen_locker._extra_benefits import process_week_transition
|
||||
from screen_locker._shutdown_base import reset_to_base_if_new_day
|
||||
from screen_locker._sync_mixin import SyncMixin
|
||||
from screen_locker._temperature import fetch_current_temp_with_status
|
||||
from screen_locker._weekly_check import (
|
||||
WEEKLY_WORKOUT_MINIMUM,
|
||||
has_weekly_minimum,
|
||||
is_relaxed_day,
|
||||
)
|
||||
|
||||
__all__ = ["StartupChecksMixin"]
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class StartupChecksMixin(SyncMixin):
|
||||
"""Decide, before building any UI, whether the lock should run today.
|
||||
|
||||
Inherits ``SyncMixin`` rather than sitting beside it in ``ScreenLocker``'s
|
||||
base list: the early-exit sequence calls ``_ingest_synced_manual_workouts``
|
||||
and ``_auto_fill_week_runnerup_bonus`` directly, so the dependency is real
|
||||
and this keeps the god-class's ancestor count from growing.
|
||||
"""
|
||||
|
||||
def _check_non_verify_exits(self) -> None:
|
||||
"""Check all normal (non-verify) startup early-exit conditions."""
|
||||
if self._is_scheduled_skip_today():
|
||||
_logger.info("Today is a scheduled skip day. Skipping screen lock.")
|
||||
sys.exit(0)
|
||||
return
|
||||
# Award streak / shutdown-bonus / EB-extension rewards from last week
|
||||
# before the daily reset, so a Monday transition's bonus is recorded
|
||||
# in time for _apply_weekly_shutdown_bonus below to see it.
|
||||
for reward_msg in process_week_transition(self.log_file, EXTRA_BENEFITS_FILE):
|
||||
_logger.info("Weekly reward: %s", reward_msg)
|
||||
# Reset shutdown config to base (21:00) at the start of each new day,
|
||||
# then layer this week's earned bonus back on top of the fresh base.
|
||||
if reset_to_base_if_new_day(
|
||||
SHUTDOWN_BASE_FILE, self, sick_day_state_file=SICK_DAY_STATE_FILE
|
||||
):
|
||||
self._apply_weekly_shutdown_bonus()
|
||||
# Ingest any manual workouts synced from the phone (or another device)
|
||||
# before the early-exit checks, so a manual logged off-app still counts
|
||||
# toward today's/this week's minimum.
|
||||
self._ingest_synced_manual_workouts()
|
||||
# Auto-fill any RunnerUp workouts from earlier in the current ISO week
|
||||
# before any early-exit check, so gaps are closed regardless of today's
|
||||
# logged state (early_bird, sick_day, etc.).
|
||||
self._auto_fill_week_runnerup_bonus()
|
||||
if self._check_today_state_exits():
|
||||
return
|
||||
# Day-of-week routing: Tue/Wed/Thu relaxed (optional), Fri-Mon enforced.
|
||||
if is_relaxed_day():
|
||||
_logger.info("Relaxed day (Tue-Thu) - showing optional workout prompt.")
|
||||
self._relaxed_day_mode = True
|
||||
return
|
||||
# Fri-Mon: skip lock when weekly minimum is already met.
|
||||
if has_weekly_minimum(self.log_file):
|
||||
_logger.info(
|
||||
"Weekly minimum of %d workouts met. Skipping screen lock.",
|
||||
WEEKLY_WORKOUT_MINIMUM,
|
||||
)
|
||||
sys.exit(0)
|
||||
return
|
||||
# Only remaining same-day skip: genuine extreme heat. Sick days go
|
||||
# through the justification flow instead; there is no banked
|
||||
# "skip a workout" credit — that mechanic works against the goal of
|
||||
# maximizing weekly workouts, so it was removed in favor of a
|
||||
# shutdown-time-only reward (see _apply_weekly_shutdown_bonus).
|
||||
self._check_heat_skip_exit()
|
||||
|
||||
def _check_heat_skip_exit(self) -> None:
|
||||
"""Exit early if today qualifies for the extreme-heat skip dialog.
|
||||
|
||||
Fail-closed by construction: a failed or timed-out temperature fetch
|
||||
falls straight through to the lock, same as "not hot enough" — the
|
||||
only difference is this logs *why* explicitly, so a fetch failure
|
||||
is never silently indistinguishable from a normal day in the logs.
|
||||
"""
|
||||
check = fetch_current_temp_with_status(HEAT_SKIP_CITY)
|
||||
if check.timed_out:
|
||||
_logger.warning(
|
||||
"Heat-skip temperature check timed out — defaulting to lock."
|
||||
)
|
||||
return
|
||||
if check.temp_celsius is None:
|
||||
_logger.warning(
|
||||
"Heat-skip temperature check failed (network/API error) — "
|
||||
"defaulting to lock."
|
||||
)
|
||||
return
|
||||
if check.temp_celsius < HEAT_SKIP_TEMP_THRESHOLD:
|
||||
return
|
||||
_logger.info(
|
||||
"Temperature %.0f°C exceeds threshold — showing heat-skip dialog.",
|
||||
check.temp_celsius,
|
||||
)
|
||||
if self._show_heat_skip_dialog(check.temp_celsius):
|
||||
self._save_heat_skip_log(check.temp_celsius)
|
||||
_logger.info(
|
||||
"User skipped workout due to heat (%.0f°C).", check.temp_celsius
|
||||
)
|
||||
sys.exit(0)
|
||||
102
screen_locker/_sync_mixin.py
Normal file
102
screen_locker/_sync_mixin.py
Normal file
@ -0,0 +1,102 @@
|
||||
"""Manual-workout sync and RunnerUp auto-fill for the screen locker.
|
||||
|
||||
Split out of ``screen_lock.py`` to keep every file under the 250-line cap.
|
||||
These methods form one cohesive unit: they publish this PC's workouts, ingest
|
||||
records other devices produced, and apply the same shutdown/debt credit a
|
||||
live-logged workout earns.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from screen_locker._constants import EXTRA_BENEFITS_FILE
|
||||
from screen_locker._extra_benefits import weekly_shutdown_bonus_hours
|
||||
from screen_locker._manual_push import push_pc_workouts
|
||||
from screen_locker._manual_sync import ingest_manual_records
|
||||
from screen_locker._weekly_check import (
|
||||
WEEKLY_WORKOUT_MINIMUM,
|
||||
count_weekly_workouts,
|
||||
)
|
||||
from screen_locker._workout_sync import pull_all_manual_records
|
||||
|
||||
__all__ = ["SyncMixin"]
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SyncMixin:
|
||||
"""Publish, ingest and credit manual workouts synced between devices."""
|
||||
|
||||
def sync_now(self) -> None:
|
||||
"""Run one sync pass: publish this PC's workouts, ingest everyone's.
|
||||
|
||||
The public entry point behind ``--sync-only``, which the
|
||||
``workout-sync.timer`` unit calls. Sync used to happen only inside the
|
||||
locker's startup path, so a workout finished after login stayed
|
||||
invisible until the next login.
|
||||
|
||||
Also re-runs the RunnerUp TCX backfill here, not just at login: the
|
||||
login-time scan gets exactly one shot, and if the phone isn't
|
||||
adb-visible at that instant (e.g. USB debugging not yet
|
||||
authorized), a same-day run stays uncredited until the next login.
|
||||
Repeating it every 15 minutes closes that gap.
|
||||
"""
|
||||
self._ingest_synced_manual_workouts()
|
||||
self._auto_fill_week_runnerup_bonus()
|
||||
|
||||
def _ingest_synced_manual_workouts(self) -> None:
|
||||
"""Sync manual workouts: publish this PC's, ingest everyone else's.
|
||||
|
||||
Each newly-ingested record earns the identical shutdown/debt reward a
|
||||
live-logged workout would (see
|
||||
``WorkoutCreditMixin._apply_credit_for_written_entry``), regardless of
|
||||
whether it's dated today or back-dated — there's only one current
|
||||
shutdown config, so a back-dated sync still pushes it.
|
||||
"""
|
||||
push_pc_workouts(self.log_file)
|
||||
ingested = ingest_manual_records(
|
||||
self.log_file,
|
||||
pull_all_manual_records(),
|
||||
on_ingested=self._credit_ingested_manual_workout,
|
||||
)
|
||||
for record_id in ingested:
|
||||
_logger.info("Ingested synced manual workout: %s", record_id)
|
||||
self.workout_data = {}
|
||||
|
||||
def _credit_ingested_manual_workout(
|
||||
self, entry: dict[str, str], prior_entries: list[dict]
|
||||
) -> None:
|
||||
"""Apply the live-workout reward to a manual workout ingested via sync."""
|
||||
self.workout_data = entry
|
||||
credit = self._apply_credit_for_written_entry(prior_entries)
|
||||
if credit.shutdown_adjusted:
|
||||
_logger.info(
|
||||
"Synced manual workout pushed shutdown time +2h: %s",
|
||||
entry.get("source", ""),
|
||||
)
|
||||
elif credit.extra_bonus_delta:
|
||||
_logger.info(
|
||||
"Synced manual workout added +%dh shutdown time: %s",
|
||||
credit.extra_bonus_delta,
|
||||
entry.get("source", ""),
|
||||
)
|
||||
|
||||
def _auto_fill_week_runnerup_bonus(self) -> None:
|
||||
"""Auto-fill missed RunnerUp workouts and award any earned bonus."""
|
||||
prev_count = count_weekly_workouts(self.log_file)
|
||||
n_filled = self._scan_and_fill_week_runnerup(self.log_file)
|
||||
if not n_filled:
|
||||
return
|
||||
new_count = count_weekly_workouts(self.log_file)
|
||||
_logger.info("Auto-filled %d RunnerUp workout(s) from TCX exports.", n_filled)
|
||||
# Award +1h for each newly auto-filled workout above the minimum.
|
||||
bonus = max(0, new_count - max(WEEKLY_WORKOUT_MINIMUM, prev_count))
|
||||
if bonus > 0 and self._adjust_shutdown_time_by(bonus):
|
||||
_logger.info("Auto-fill extra bonus: +%dh shutdown time.", bonus)
|
||||
|
||||
def _apply_weekly_shutdown_bonus(self) -> None:
|
||||
"""Layer this week's earned shutdown bonus back on top of the fresh base."""
|
||||
bonus = weekly_shutdown_bonus_hours(EXTRA_BENEFITS_FILE)
|
||||
if bonus > 0 and self._adjust_shutdown_time_by(bonus):
|
||||
_logger.info("Weekly bonus: +%dh shutdown time this week.", bonus)
|
||||
@ -23,49 +23,31 @@ from screen_locker._constants import (
|
||||
EARLY_BIRD_END_HOUR,
|
||||
EARLY_BIRD_END_MINUTE,
|
||||
EARLY_BIRD_START_HOUR,
|
||||
EXTRA_BENEFITS_FILE,
|
||||
HEAT_SKIP_CITY,
|
||||
HEAT_SKIP_TEMP_THRESHOLD,
|
||||
HMAC_KEY_FILE,
|
||||
MAX_CLOCK_SKEW_SECONDS,
|
||||
MIN_WORKOUT_DURATION_MINUTES,
|
||||
PHONE_PENALTY_DELAY_DEMO,
|
||||
PHONE_PENALTY_DELAY_PRODUCTION,
|
||||
SCHEDULED_SKIPS_FILE,
|
||||
SHUTDOWN_BASE_FILE,
|
||||
SICK_DAY_STATE_FILE,
|
||||
SICK_LOCKOUT_SECONDS,
|
||||
)
|
||||
from screen_locker._early_bird import EarlyBirdMixin
|
||||
from screen_locker._extra_benefits import (
|
||||
process_week_transition,
|
||||
weekly_shutdown_bonus_hours,
|
||||
)
|
||||
from screen_locker._heat_skip import HeatSkipMixin
|
||||
from screen_locker._log_mixin import LogMixin
|
||||
from screen_locker._manual_push import push_pc_workouts
|
||||
from screen_locker._manual_sync import ingest_manual_records
|
||||
from screen_locker._manual_workout_dialog import ManualWorkoutDialogMixin
|
||||
from screen_locker._phone_verification import PhoneVerificationMixin
|
||||
from screen_locker._runnerup_verification import RunnerUpVerificationMixin
|
||||
from screen_locker._shutdown import ShutdownMixin
|
||||
from screen_locker._shutdown_base import reset_to_base_if_new_day
|
||||
from screen_locker._sick_dialog import SickDialogMixin
|
||||
from screen_locker._startup_checks import StartupChecksMixin
|
||||
from screen_locker._surface_group import FrameGroup
|
||||
from screen_locker._temperature import fetch_current_temp_with_status
|
||||
from screen_locker._ui_flows import UIFlowsMixin
|
||||
from screen_locker._ui_flows_relaxed import UIFlowsRelaxedMixin
|
||||
from screen_locker._ui_widgets import UIWidgetsMixin
|
||||
from screen_locker._unlock_view import UnlockViewMixin
|
||||
from screen_locker._weekly_check import (
|
||||
WEEKLY_WORKOUT_MINIMUM,
|
||||
count_weekly_workouts,
|
||||
has_weekly_minimum,
|
||||
is_relaxed_day,
|
||||
)
|
||||
from screen_locker._weekly_check import WEEKLY_WORKOUT_MINIMUM
|
||||
from screen_locker._window_setup import WindowSetupMixin
|
||||
from screen_locker._workout_credit import WorkoutCreditMixin
|
||||
from screen_locker._workout_sync import pull_all_manual_records
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Callable
|
||||
@ -116,6 +98,7 @@ class ScreenLocker(
|
||||
PhoneVerificationMixin,
|
||||
RunnerUpVerificationMixin,
|
||||
SickDialogMixin,
|
||||
StartupChecksMixin,
|
||||
UIFlowsMixin,
|
||||
UIFlowsRelaxedMixin,
|
||||
UIWidgetsMixin,
|
||||
@ -180,159 +163,6 @@ class ScreenLocker(
|
||||
if self._lock is not None: # pragma: no branch
|
||||
self._lock.grab_input()
|
||||
|
||||
def _check_non_verify_exits(self) -> None:
|
||||
"""Check all normal (non-verify) startup early-exit conditions."""
|
||||
if self._is_scheduled_skip_today():
|
||||
_logger.info("Today is a scheduled skip day. Skipping screen lock.")
|
||||
sys.exit(0)
|
||||
return
|
||||
# Award streak / shutdown-bonus / EB-extension rewards from last week
|
||||
# before the daily reset, so a Monday transition's bonus is recorded
|
||||
# in time for _apply_weekly_shutdown_bonus below to see it.
|
||||
for reward_msg in process_week_transition(self.log_file, EXTRA_BENEFITS_FILE):
|
||||
_logger.info("Weekly reward: %s", reward_msg)
|
||||
# Reset shutdown config to base (21:00) at the start of each new day,
|
||||
# then layer this week's earned bonus back on top of the fresh base.
|
||||
if reset_to_base_if_new_day(
|
||||
SHUTDOWN_BASE_FILE, self, sick_day_state_file=SICK_DAY_STATE_FILE
|
||||
):
|
||||
self._apply_weekly_shutdown_bonus()
|
||||
# Ingest any manual workouts synced from the phone (or another device)
|
||||
# before the early-exit checks, so a manual logged off-app still counts
|
||||
# toward today's/this week's minimum.
|
||||
self._ingest_synced_manual_workouts()
|
||||
# Auto-fill any RunnerUp workouts from earlier in the current ISO week
|
||||
# before any early-exit check, so gaps are closed regardless of today's
|
||||
# logged state (early_bird, sick_day, etc.).
|
||||
self._auto_fill_week_runnerup_bonus()
|
||||
if self._check_today_state_exits():
|
||||
return
|
||||
# Day-of-week routing: Tue/Wed/Thu relaxed (optional), Fri-Mon enforced.
|
||||
if is_relaxed_day():
|
||||
_logger.info("Relaxed day (Tue-Thu) - showing optional workout prompt.")
|
||||
self._relaxed_day_mode = True
|
||||
return
|
||||
# Fri-Mon: skip lock when weekly minimum is already met.
|
||||
if has_weekly_minimum(self.log_file):
|
||||
_logger.info(
|
||||
"Weekly minimum of %d workouts met. Skipping screen lock.",
|
||||
WEEKLY_WORKOUT_MINIMUM,
|
||||
)
|
||||
sys.exit(0)
|
||||
return
|
||||
# Only remaining same-day skip: genuine extreme heat. Sick days go
|
||||
# through the justification flow instead; there is no banked
|
||||
# "skip a workout" credit — that mechanic works against the goal of
|
||||
# maximizing weekly workouts, so it was removed in favor of a
|
||||
# shutdown-time-only reward (see _apply_weekly_shutdown_bonus).
|
||||
self._check_heat_skip_exit()
|
||||
|
||||
def sync_now(self) -> None:
|
||||
"""Run one sync pass: publish this PC's workouts, ingest everyone's.
|
||||
|
||||
The public entry point behind ``--sync-only``, which the
|
||||
``workout-sync.timer`` unit calls. Sync used to happen only inside the
|
||||
locker's startup path, so a workout finished after login stayed
|
||||
invisible until the next login.
|
||||
|
||||
Also re-runs the RunnerUp TCX backfill here, not just at login: the
|
||||
login-time scan gets exactly one shot, and if the phone isn't
|
||||
adb-visible at that instant (e.g. USB debugging not yet
|
||||
authorized), a same-day run stays uncredited until the next login.
|
||||
Repeating it every 15 minutes closes that gap.
|
||||
"""
|
||||
self._ingest_synced_manual_workouts()
|
||||
self._auto_fill_week_runnerup_bonus()
|
||||
|
||||
def _ingest_synced_manual_workouts(self) -> None:
|
||||
"""Sync manual workouts: publish this PC's, ingest everyone else's.
|
||||
|
||||
Each newly-ingested record earns the identical shutdown/debt reward a
|
||||
live-logged workout would (see
|
||||
``WorkoutCreditMixin._apply_credit_for_written_entry``), regardless of
|
||||
whether it's dated today or back-dated — there's only one current
|
||||
shutdown config, so a back-dated sync still pushes it.
|
||||
"""
|
||||
push_pc_workouts(self.log_file)
|
||||
ingested = ingest_manual_records(
|
||||
self.log_file,
|
||||
pull_all_manual_records(),
|
||||
on_ingested=self._credit_ingested_manual_workout,
|
||||
)
|
||||
for record_id in ingested:
|
||||
_logger.info("Ingested synced manual workout: %s", record_id)
|
||||
self.workout_data = {}
|
||||
|
||||
def _credit_ingested_manual_workout(
|
||||
self, entry: dict[str, str], prior_entries: list[dict]
|
||||
) -> None:
|
||||
"""Apply the live-workout reward to a manual workout ingested via sync."""
|
||||
self.workout_data = entry
|
||||
credit = self._apply_credit_for_written_entry(prior_entries)
|
||||
if credit.shutdown_adjusted:
|
||||
_logger.info(
|
||||
"Synced manual workout pushed shutdown time +2h: %s",
|
||||
entry.get("source", ""),
|
||||
)
|
||||
elif credit.extra_bonus_delta:
|
||||
_logger.info(
|
||||
"Synced manual workout added +%dh shutdown time: %s",
|
||||
credit.extra_bonus_delta,
|
||||
entry.get("source", ""),
|
||||
)
|
||||
|
||||
def _auto_fill_week_runnerup_bonus(self) -> None:
|
||||
"""Auto-fill missed RunnerUp workouts and award any earned bonus."""
|
||||
prev_count = count_weekly_workouts(self.log_file)
|
||||
n_filled = self._scan_and_fill_week_runnerup(self.log_file)
|
||||
if not n_filled:
|
||||
return
|
||||
new_count = count_weekly_workouts(self.log_file)
|
||||
_logger.info("Auto-filled %d RunnerUp workout(s) from TCX exports.", n_filled)
|
||||
# Award +1h for each newly auto-filled workout above the minimum.
|
||||
bonus = max(0, new_count - max(WEEKLY_WORKOUT_MINIMUM, prev_count))
|
||||
if bonus > 0 and self._adjust_shutdown_time_by(bonus):
|
||||
_logger.info("Auto-fill extra bonus: +%dh shutdown time.", bonus)
|
||||
|
||||
def _check_heat_skip_exit(self) -> None:
|
||||
"""Exit early if today qualifies for the extreme-heat skip dialog.
|
||||
|
||||
Fail-closed by construction: a failed or timed-out temperature fetch
|
||||
falls straight through to the lock, same as "not hot enough" — the
|
||||
only difference is this logs *why* explicitly, so a fetch failure
|
||||
is never silently indistinguishable from a normal day in the logs.
|
||||
"""
|
||||
check = fetch_current_temp_with_status(HEAT_SKIP_CITY)
|
||||
if check.timed_out:
|
||||
_logger.warning(
|
||||
"Heat-skip temperature check timed out — defaulting to lock."
|
||||
)
|
||||
return
|
||||
if check.temp_celsius is None:
|
||||
_logger.warning(
|
||||
"Heat-skip temperature check failed (network/API error) — "
|
||||
"defaulting to lock."
|
||||
)
|
||||
return
|
||||
if check.temp_celsius < HEAT_SKIP_TEMP_THRESHOLD:
|
||||
return
|
||||
_logger.info(
|
||||
"Temperature %.0f°C exceeds threshold — showing heat-skip dialog.",
|
||||
check.temp_celsius,
|
||||
)
|
||||
if self._show_heat_skip_dialog(check.temp_celsius):
|
||||
self._save_heat_skip_log(check.temp_celsius)
|
||||
_logger.info(
|
||||
"User skipped workout due to heat (%.0f°C).", check.temp_celsius
|
||||
)
|
||||
sys.exit(0)
|
||||
|
||||
def _apply_weekly_shutdown_bonus(self) -> None:
|
||||
"""Layer this week's earned shutdown bonus back on top of the fresh base."""
|
||||
bonus = weekly_shutdown_bonus_hours(EXTRA_BENEFITS_FILE)
|
||||
if bonus > 0 and self._adjust_shutdown_time_by(bonus):
|
||||
_logger.info("Weekly bonus: +%dh shutdown time this week.", bonus)
|
||||
|
||||
def close(self) -> None:
|
||||
"""Close the application and exit."""
|
||||
if self._lock is not None:
|
||||
@ -350,41 +180,6 @@ class ScreenLocker(
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if "--status" in sys.argv:
|
||||
from screen_locker._status import run_status
|
||||
from screen_locker._cli import main
|
||||
|
||||
# Bypass __init__ (no UI) — only log_file and workout_data are needed.
|
||||
_sl = object.__new__(ScreenLocker)
|
||||
_sl.log_file = Path(__file__).resolve().parent / "workout_log.json"
|
||||
_sl.workout_data = {}
|
||||
run_status(_sl)
|
||||
|
||||
if "--sync-only" in sys.argv:
|
||||
# Headless sync for the timer unit: pull other devices' workouts and
|
||||
# apply their credit, with no Tk and no lock screen. Sync used to run
|
||||
# ONLY at process start, so a workout finished after login was not seen
|
||||
# until the next login — the timer closes that window.
|
||||
#
|
||||
# Same __init__ bypass as --status: this path touches only log_file and
|
||||
# workout_data, and constructing the real ScreenLocker would build a UI.
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(levelname)s %(name)s: %(message)s",
|
||||
)
|
||||
_sl = object.__new__(ScreenLocker)
|
||||
_sl.log_file = Path(__file__).resolve().parent / "workout_log.json"
|
||||
_sl.workout_data = {}
|
||||
_sl.sync_now()
|
||||
sys.exit(0)
|
||||
|
||||
demo_mode = True
|
||||
verify_only = "--verify-workout" in sys.argv
|
||||
|
||||
if "--production" in sys.argv:
|
||||
demo_mode = False
|
||||
|
||||
locker = ScreenLocker(
|
||||
demo_mode=demo_mode,
|
||||
verify_only=verify_only,
|
||||
)
|
||||
locker.run()
|
||||
main(ScreenLocker, sys.argv)
|
||||
|
||||
@ -93,9 +93,9 @@ def create_locker_relaxed_day(
|
||||
patch.object(ScreenLocker, "_is_early_bird_pending", return_value=False),
|
||||
patch.object(ScreenLocker, "_is_early_bird_time", return_value=False),
|
||||
patch.object(ScreenLocker, "_try_auto_upgrade_early_bird", return_value=False),
|
||||
patch("screen_locker.screen_lock.is_relaxed_day", return_value=True),
|
||||
patch("screen_locker._startup_checks.is_relaxed_day", return_value=True),
|
||||
patch(
|
||||
"screen_locker.screen_lock.has_weekly_minimum",
|
||||
"screen_locker._startup_checks.has_weekly_minimum",
|
||||
return_value=False,
|
||||
),
|
||||
patch.object(ScreenLocker, "_start_phone_check"),
|
||||
|
||||
@ -107,6 +107,9 @@ def _block_real_tk_and_exit() -> Iterator[None]:
|
||||
)
|
||||
)
|
||||
stack.enter_context(patch("screen_locker.screen_lock.sys.exit"))
|
||||
# The startup early-exit checks moved to _startup_checks; sys.exit is
|
||||
# bound there now, so it needs blocking separately.
|
||||
stack.enter_context(patch("screen_locker._startup_checks.sys.exit"))
|
||||
yield
|
||||
|
||||
|
||||
@ -188,7 +191,8 @@ def _isolate_extra_benefits(tmp_path: Path) -> Iterator[None]:
|
||||
target = tmp_path / "extra_benefits_state.json"
|
||||
with (
|
||||
patch("screen_locker._constants.EXTRA_BENEFITS_FILE", target),
|
||||
patch("screen_locker.screen_lock.EXTRA_BENEFITS_FILE", target),
|
||||
patch("screen_locker._startup_checks.EXTRA_BENEFITS_FILE", target),
|
||||
patch("screen_locker._sync_mixin.EXTRA_BENEFITS_FILE", target),
|
||||
patch("screen_locker._early_bird.EXTRA_BENEFITS_FILE", target),
|
||||
patch("screen_locker._status.EXTRA_BENEFITS_FILE", target),
|
||||
):
|
||||
@ -213,7 +217,7 @@ def _isolate_shutdown_base(tmp_path: Path) -> Iterator[None]:
|
||||
)
|
||||
with (
|
||||
patch("screen_locker._constants.SHUTDOWN_BASE_FILE", target),
|
||||
patch("screen_locker.screen_lock.SHUTDOWN_BASE_FILE", target),
|
||||
patch("screen_locker._startup_checks.SHUTDOWN_BASE_FILE", target),
|
||||
):
|
||||
yield
|
||||
|
||||
@ -224,7 +228,7 @@ def _isolate_sick_day_state(tmp_path: Path) -> Iterator[None]:
|
||||
target = tmp_path / "sick_day_state.json"
|
||||
with (
|
||||
patch("screen_locker._constants.SICK_DAY_STATE_FILE", target),
|
||||
patch("screen_locker.screen_lock.SICK_DAY_STATE_FILE", target),
|
||||
patch("screen_locker._startup_checks.SICK_DAY_STATE_FILE", target),
|
||||
):
|
||||
yield
|
||||
|
||||
@ -251,11 +255,11 @@ def _mock_weekly_logic() -> Iterator[None]:
|
||||
"""
|
||||
with (
|
||||
patch(
|
||||
"screen_locker.screen_lock.is_relaxed_day",
|
||||
"screen_locker._startup_checks.is_relaxed_day",
|
||||
return_value=False,
|
||||
),
|
||||
patch(
|
||||
"screen_locker.screen_lock.has_weekly_minimum",
|
||||
"screen_locker._startup_checks.has_weekly_minimum",
|
||||
return_value=False,
|
||||
),
|
||||
):
|
||||
|
||||
175
screen_locker/tests/test_cli.py
Normal file
175
screen_locker/tests/test_cli.py
Normal file
@ -0,0 +1,175 @@
|
||||
"""Tests for screen_locker._cli, the command-line entry-point dispatcher.
|
||||
|
||||
This logic used to live in ``screen_lock.py``'s ``if __name__ == "__main__":``
|
||||
block, which coverage excludes -- so it shipped untested. Splitting it into a
|
||||
real module for the 250-line cap made it testable; these tests cover it.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, ClassVar # ClassVar is needed at runtime
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from screen_locker import _cli
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Iterator
|
||||
|
||||
|
||||
class _FakeLocker:
|
||||
"""Stand-in for ScreenLocker recording how it was constructed."""
|
||||
|
||||
instances: ClassVar[list[_FakeLocker]] = []
|
||||
|
||||
def __init__(self, *, demo_mode: bool = True, verify_only: bool = False) -> None:
|
||||
self.demo_mode = demo_mode
|
||||
self.verify_only = verify_only
|
||||
self.ran = False
|
||||
self.log_file: Path | None = None
|
||||
self.workout_data: dict[str, str] | None = None
|
||||
self.synced = False
|
||||
_FakeLocker.instances.append(self)
|
||||
|
||||
def run(self) -> None:
|
||||
"""Record that the main loop was entered."""
|
||||
self.ran = True
|
||||
|
||||
def sync_now(self) -> None:
|
||||
"""Record that a sync pass was requested."""
|
||||
self.synced = True
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _reset_instances() -> Iterator[None]:
|
||||
"""Clear the recorded instances before each test."""
|
||||
_FakeLocker.instances = []
|
||||
yield
|
||||
_FakeLocker.instances = []
|
||||
|
||||
|
||||
class TestHeadlessLocker:
|
||||
"""_headless_locker bypasses __init__ so no UI is ever built."""
|
||||
|
||||
def test_bypasses_init_and_seeds_only_the_two_needed_fields(self) -> None:
|
||||
"""Bypassing __init__ leaves only log_file and workout_data set."""
|
||||
locker = _cli._headless_locker(_FakeLocker)
|
||||
|
||||
# __init__ never ran, so demo_mode/verify_only are absent entirely.
|
||||
assert not hasattr(locker, "demo_mode")
|
||||
assert not _FakeLocker.instances
|
||||
assert not locker.workout_data
|
||||
assert locker.log_file.name == "workout_log.json"
|
||||
|
||||
def test_log_file_sits_beside_the_package(self) -> None:
|
||||
"""The workout log is resolved relative to the package directory."""
|
||||
locker = _cli._headless_locker(_FakeLocker)
|
||||
|
||||
assert locker.log_file.parent == Path(_cli.__file__).resolve().parent
|
||||
|
||||
|
||||
class TestStatusMode:
|
||||
"""--status runs the status report against a headless locker."""
|
||||
|
||||
def test_calls_run_status_without_constructing_a_ui(self) -> None:
|
||||
"""--status reports via run_status and builds no locker."""
|
||||
# The real run_status ends in sys.exit(0), which is why main() needs
|
||||
# no `return` here; the fake reproduces that so the fall-through to
|
||||
# the UI locker is exercised exactly as it is in production.
|
||||
with (
|
||||
patch.object(_cli, "run_status", side_effect=SystemExit(0)) as run_status,
|
||||
pytest.raises(SystemExit) as excinfo,
|
||||
):
|
||||
_cli.main(_FakeLocker, ["screen_lock.py", "--status"])
|
||||
|
||||
assert not excinfo.value.code
|
||||
run_status.assert_called_once()
|
||||
passed = run_status.call_args[0][0]
|
||||
assert not passed.workout_data
|
||||
# No real locker was constructed for the status path.
|
||||
assert not _FakeLocker.instances
|
||||
|
||||
|
||||
class TestSyncOnlyMode:
|
||||
"""--sync-only runs one headless sync pass and exits 0.
|
||||
|
||||
The suite-wide conftest fixture patches ``sys.exit`` to a no-op so a stray
|
||||
exit cannot kill the test run, so these assert on the call rather than on
|
||||
a raised SystemExit.
|
||||
"""
|
||||
|
||||
def test_syncs_then_exits_zero(self) -> None:
|
||||
"""--sync-only configures logging and exits 0."""
|
||||
with (
|
||||
patch.object(_cli.logging, "basicConfig") as basic_config,
|
||||
patch.object(_cli.sys, "exit") as sys_exit,
|
||||
):
|
||||
_cli.main(_FakeLocker, ["screen_lock.py", "--sync-only"])
|
||||
|
||||
sys_exit.assert_called_once_with(0)
|
||||
basic_config.assert_called_once()
|
||||
|
||||
def test_actually_calls_sync_now(self) -> None:
|
||||
"""--sync-only runs exactly one sync pass."""
|
||||
synced: list[bool] = []
|
||||
|
||||
class _Recorder(_FakeLocker):
|
||||
def sync_now(self) -> None:
|
||||
"""Record the sync call."""
|
||||
synced.append(True)
|
||||
|
||||
with (
|
||||
patch.object(_cli.logging, "basicConfig"),
|
||||
patch.object(_cli.sys, "exit"),
|
||||
):
|
||||
_cli.main(_Recorder, ["screen_lock.py", "--sync-only"])
|
||||
|
||||
assert synced == [True]
|
||||
|
||||
|
||||
class TestLockMode:
|
||||
"""With no subcommand flag, the real locker is built and run."""
|
||||
|
||||
def test_defaults_to_demo_mode(self) -> None:
|
||||
"""With no flags the locker is built in demo mode and run."""
|
||||
_cli.main(_FakeLocker, ["screen_lock.py"])
|
||||
|
||||
locker = _FakeLocker.instances[-1]
|
||||
assert locker.demo_mode is True
|
||||
assert locker.verify_only is False
|
||||
assert locker.ran is True
|
||||
|
||||
def test_production_flag_disables_demo_mode(self) -> None:
|
||||
"""--production turns demo mode off."""
|
||||
_cli.main(_FakeLocker, ["screen_lock.py", "--production"])
|
||||
|
||||
locker = _FakeLocker.instances[-1]
|
||||
assert locker.demo_mode is False
|
||||
assert locker.verify_only is False
|
||||
|
||||
def test_verify_workout_flag_sets_verify_only(self) -> None:
|
||||
"""--verify-workout sets verify_only."""
|
||||
_cli.main(_FakeLocker, ["screen_lock.py", "--verify-workout"])
|
||||
|
||||
locker = _FakeLocker.instances[-1]
|
||||
assert locker.verify_only is True
|
||||
assert locker.demo_mode is True
|
||||
|
||||
def test_production_and_verify_combine(self) -> None:
|
||||
"""--production and --verify-workout apply together."""
|
||||
_cli.main(_FakeLocker, ["screen_lock.py", "--production", "--verify-workout"])
|
||||
|
||||
locker = _FakeLocker.instances[-1]
|
||||
assert locker.demo_mode is False
|
||||
assert locker.verify_only is True
|
||||
|
||||
|
||||
class TestScreenLockDelegation:
|
||||
"""screen_lock.py's __main__ block delegates here rather than duplicating."""
|
||||
|
||||
def test_main_is_exported(self) -> None:
|
||||
"""main is the module's public entry point."""
|
||||
assert _cli.__all__ == ["main"]
|
||||
assert callable(_cli.main)
|
||||
@ -41,16 +41,18 @@ class TestCheckNonVerifyExitsExtras:
|
||||
MagicMock(return_value=True),
|
||||
)
|
||||
with (
|
||||
patch("screen_locker.screen_lock.reset_to_base_if_new_day"),
|
||||
patch("screen_locker._startup_checks.reset_to_base_if_new_day"),
|
||||
patch(
|
||||
"screen_locker.screen_lock.count_weekly_workouts", side_effect=[0, 6]
|
||||
"screen_locker._sync_mixin.count_weekly_workouts", side_effect=[0, 6]
|
||||
),
|
||||
patch(
|
||||
"screen_locker.screen_lock.process_week_transition",
|
||||
"screen_locker._startup_checks.process_week_transition",
|
||||
return_value=[],
|
||||
),
|
||||
patch("screen_locker.screen_lock.is_relaxed_day", return_value=False),
|
||||
patch("screen_locker.screen_lock.has_weekly_minimum", return_value=True),
|
||||
patch("screen_locker._startup_checks.is_relaxed_day", return_value=False),
|
||||
patch(
|
||||
"screen_locker._startup_checks.has_weekly_minimum", return_value=True
|
||||
),
|
||||
patch("screen_locker.screen_lock.sys.exit"),
|
||||
):
|
||||
locker._check_non_verify_exits()
|
||||
@ -75,17 +77,19 @@ class TestCheckNonVerifyExitsExtras:
|
||||
MagicMock(return_value=False),
|
||||
)
|
||||
with (
|
||||
patch("screen_locker.screen_lock.reset_to_base_if_new_day"),
|
||||
patch("screen_locker._startup_checks.reset_to_base_if_new_day"),
|
||||
# prev=2, new=3 → bonus=max(0,3-max(5,2))=0 → no bonus logger call
|
||||
patch(
|
||||
"screen_locker.screen_lock.count_weekly_workouts", side_effect=[2, 3]
|
||||
"screen_locker._sync_mixin.count_weekly_workouts", side_effect=[2, 3]
|
||||
),
|
||||
patch(
|
||||
"screen_locker.screen_lock.process_week_transition",
|
||||
"screen_locker._startup_checks.process_week_transition",
|
||||
return_value=[],
|
||||
),
|
||||
patch("screen_locker.screen_lock.is_relaxed_day", return_value=False),
|
||||
patch("screen_locker.screen_lock.has_weekly_minimum", return_value=False),
|
||||
patch("screen_locker._startup_checks.is_relaxed_day", return_value=False),
|
||||
patch(
|
||||
"screen_locker._startup_checks.has_weekly_minimum", return_value=False
|
||||
),
|
||||
patch("screen_locker.screen_lock.sys.exit"),
|
||||
):
|
||||
locker._check_non_verify_exits()
|
||||
@ -104,13 +108,15 @@ class TestCheckNonVerifyExitsExtras:
|
||||
MagicMock(return_value=0),
|
||||
)
|
||||
with (
|
||||
patch("screen_locker.screen_lock.reset_to_base_if_new_day"),
|
||||
patch("screen_locker._startup_checks.reset_to_base_if_new_day"),
|
||||
patch(
|
||||
"screen_locker.screen_lock.process_week_transition",
|
||||
"screen_locker._startup_checks.process_week_transition",
|
||||
return_value=["🎉 +1h shutdown bonus for 5-workout week!"],
|
||||
),
|
||||
patch("screen_locker.screen_lock.is_relaxed_day", return_value=False),
|
||||
patch("screen_locker.screen_lock.has_weekly_minimum", return_value=True),
|
||||
patch("screen_locker._startup_checks.is_relaxed_day", return_value=False),
|
||||
patch(
|
||||
"screen_locker._startup_checks.has_weekly_minimum", return_value=True
|
||||
),
|
||||
patch("screen_locker.screen_lock.sys.exit"),
|
||||
):
|
||||
locker._check_non_verify_exits()
|
||||
@ -135,18 +141,21 @@ class TestCheckNonVerifyExitsExtras:
|
||||
)
|
||||
with (
|
||||
patch(
|
||||
"screen_locker.screen_lock.reset_to_base_if_new_day", return_value=True
|
||||
"screen_locker._startup_checks.reset_to_base_if_new_day",
|
||||
return_value=True,
|
||||
),
|
||||
patch(
|
||||
"screen_locker.screen_lock.process_week_transition",
|
||||
"screen_locker._startup_checks.process_week_transition",
|
||||
return_value=[],
|
||||
),
|
||||
patch(
|
||||
"screen_locker.screen_lock.weekly_shutdown_bonus_hours",
|
||||
"screen_locker._sync_mixin.weekly_shutdown_bonus_hours",
|
||||
return_value=2,
|
||||
),
|
||||
patch("screen_locker.screen_lock.is_relaxed_day", return_value=False),
|
||||
patch("screen_locker.screen_lock.has_weekly_minimum", return_value=True),
|
||||
patch("screen_locker._startup_checks.is_relaxed_day", return_value=False),
|
||||
patch(
|
||||
"screen_locker._startup_checks.has_weekly_minimum", return_value=True
|
||||
),
|
||||
patch("screen_locker.screen_lock.sys.exit"),
|
||||
):
|
||||
locker._check_non_verify_exits()
|
||||
@ -172,18 +181,21 @@ class TestCheckNonVerifyExitsExtras:
|
||||
)
|
||||
with (
|
||||
patch(
|
||||
"screen_locker.screen_lock.reset_to_base_if_new_day", return_value=False
|
||||
"screen_locker._startup_checks.reset_to_base_if_new_day",
|
||||
return_value=False,
|
||||
),
|
||||
patch(
|
||||
"screen_locker.screen_lock.process_week_transition",
|
||||
"screen_locker._startup_checks.process_week_transition",
|
||||
return_value=[],
|
||||
),
|
||||
patch(
|
||||
"screen_locker.screen_lock.weekly_shutdown_bonus_hours",
|
||||
"screen_locker._sync_mixin.weekly_shutdown_bonus_hours",
|
||||
return_value=2,
|
||||
),
|
||||
patch("screen_locker.screen_lock.is_relaxed_day", return_value=False),
|
||||
patch("screen_locker.screen_lock.has_weekly_minimum", return_value=True),
|
||||
patch("screen_locker._startup_checks.is_relaxed_day", return_value=False),
|
||||
patch(
|
||||
"screen_locker._startup_checks.has_weekly_minimum", return_value=True
|
||||
),
|
||||
patch("screen_locker.screen_lock.sys.exit"),
|
||||
):
|
||||
locker._check_non_verify_exits()
|
||||
@ -358,12 +370,12 @@ class TestSyncNow:
|
||||
locker = create_locker(mock_tk, tmp_path)
|
||||
with (
|
||||
patch(
|
||||
"screen_locker.screen_lock.pull_all_manual_records",
|
||||
"screen_locker._sync_mixin.pull_all_manual_records",
|
||||
return_value=[("manual:x", {})],
|
||||
),
|
||||
patch("screen_locker.screen_lock.push_pc_workouts") as push,
|
||||
patch("screen_locker._sync_mixin.push_pc_workouts") as push,
|
||||
patch(
|
||||
"screen_locker.screen_lock.ingest_manual_records",
|
||||
"screen_locker._sync_mixin.ingest_manual_records",
|
||||
return_value=["manual:x"],
|
||||
) as ingest,
|
||||
):
|
||||
|
||||
@ -272,11 +272,11 @@ class TestIngestSyncedManualWorkouts:
|
||||
locker = create_locker(mock_tk, tmp_path)
|
||||
with (
|
||||
patch(
|
||||
"screen_locker.screen_lock.pull_all_manual_records",
|
||||
"screen_locker._sync_mixin.pull_all_manual_records",
|
||||
return_value=[("manual:x", {})],
|
||||
),
|
||||
patch(
|
||||
"screen_locker.screen_lock.ingest_manual_records",
|
||||
"screen_locker._sync_mixin.ingest_manual_records",
|
||||
return_value=["manual:x"],
|
||||
) as ingest,
|
||||
):
|
||||
@ -289,11 +289,11 @@ class TestIngestSyncedManualWorkouts:
|
||||
locker = create_locker(mock_tk, tmp_path)
|
||||
with (
|
||||
patch(
|
||||
"screen_locker.screen_lock.pull_all_manual_records",
|
||||
"screen_locker._sync_mixin.pull_all_manual_records",
|
||||
return_value=[],
|
||||
),
|
||||
patch(
|
||||
"screen_locker.screen_lock.ingest_manual_records",
|
||||
"screen_locker._sync_mixin.ingest_manual_records",
|
||||
return_value=[],
|
||||
),
|
||||
):
|
||||
@ -310,11 +310,11 @@ class TestIngestSyncedManualWorkouts:
|
||||
locker.workout_data = {"stale": "state"}
|
||||
with (
|
||||
patch(
|
||||
"screen_locker.screen_lock.pull_all_manual_records",
|
||||
"screen_locker._sync_mixin.pull_all_manual_records",
|
||||
return_value=[],
|
||||
),
|
||||
patch(
|
||||
"screen_locker.screen_lock.ingest_manual_records",
|
||||
"screen_locker._sync_mixin.ingest_manual_records",
|
||||
return_value=[],
|
||||
) as ingest,
|
||||
):
|
||||
|
||||
@ -43,11 +43,11 @@ class TestRelaxedDayBranch:
|
||||
ScreenLocker, "_try_auto_upgrade_early_bird", return_value=False
|
||||
),
|
||||
patch(
|
||||
"screen_locker.screen_lock.is_relaxed_day",
|
||||
"screen_locker._startup_checks.is_relaxed_day",
|
||||
return_value=True,
|
||||
),
|
||||
patch(
|
||||
"screen_locker.screen_lock.has_weekly_minimum",
|
||||
"screen_locker._startup_checks.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(
|
||||
"screen_locker.screen_lock.is_relaxed_day",
|
||||
"screen_locker._startup_checks.is_relaxed_day",
|
||||
return_value=True,
|
||||
),
|
||||
patch(
|
||||
"screen_locker.screen_lock.has_weekly_minimum",
|
||||
"screen_locker._startup_checks.has_weekly_minimum",
|
||||
return_value=False,
|
||||
),
|
||||
patch("screen_locker.screen_lock.LockWindow") as mock_lock_window,
|
||||
@ -109,11 +109,11 @@ class TestRelaxedDayBranch:
|
||||
ScreenLocker, "_try_auto_upgrade_early_bird", return_value=False
|
||||
),
|
||||
patch(
|
||||
"screen_locker.screen_lock.is_relaxed_day",
|
||||
"screen_locker._startup_checks.is_relaxed_day",
|
||||
return_value=True,
|
||||
),
|
||||
patch(
|
||||
"screen_locker.screen_lock.has_weekly_minimum",
|
||||
"screen_locker._startup_checks.has_weekly_minimum",
|
||||
return_value=False,
|
||||
),
|
||||
patch("screen_locker.screen_lock.LockWindow") as mock_lock_window,
|
||||
@ -148,7 +148,7 @@ class TestWeeklyMinimumBranch:
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
with patch(
|
||||
"screen_locker.screen_lock.has_weekly_minimum",
|
||||
"screen_locker._startup_checks.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(
|
||||
"screen_locker.screen_lock.has_weekly_minimum",
|
||||
"screen_locker._startup_checks.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(
|
||||
"screen_locker.screen_lock.has_weekly_minimum",
|
||||
"screen_locker._startup_checks.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(
|
||||
"screen_locker.screen_lock.has_weekly_minimum",
|
||||
"screen_locker._startup_checks.has_weekly_minimum",
|
||||
) as mock_weekly:
|
||||
create_locker(mock_tk, tmp_path, has_logged=True)
|
||||
|
||||
|
||||
@ -215,9 +215,11 @@ class TestHeatSkipBranch:
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
with (
|
||||
patch("screen_locker.screen_lock.has_weekly_minimum", return_value=False),
|
||||
patch(
|
||||
"screen_locker.screen_lock.fetch_current_temp_with_status",
|
||||
"screen_locker._startup_checks.has_weekly_minimum", return_value=False
|
||||
),
|
||||
patch(
|
||||
"screen_locker._startup_checks.fetch_current_temp_with_status",
|
||||
return_value=TemperatureCheck(temp_celsius=20.0, timed_out=False),
|
||||
) as mock_hot,
|
||||
patch.object(ScreenLocker, "_show_heat_skip_dialog") as mock_dialog,
|
||||
@ -235,9 +237,11 @@ class TestHeatSkipBranch:
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
with (
|
||||
patch("screen_locker.screen_lock.has_weekly_minimum", return_value=False),
|
||||
patch(
|
||||
"screen_locker.screen_lock.fetch_current_temp_with_status",
|
||||
"screen_locker._startup_checks.has_weekly_minimum", return_value=False
|
||||
),
|
||||
patch(
|
||||
"screen_locker._startup_checks.fetch_current_temp_with_status",
|
||||
return_value=TemperatureCheck(temp_celsius=35.0, timed_out=False),
|
||||
),
|
||||
patch.object(ScreenLocker, "_show_heat_skip_dialog", return_value=True),
|
||||
@ -255,9 +259,11 @@ class TestHeatSkipBranch:
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
with (
|
||||
patch("screen_locker.screen_lock.has_weekly_minimum", return_value=False),
|
||||
patch(
|
||||
"screen_locker.screen_lock.fetch_current_temp_with_status",
|
||||
"screen_locker._startup_checks.has_weekly_minimum", return_value=False
|
||||
),
|
||||
patch(
|
||||
"screen_locker._startup_checks.fetch_current_temp_with_status",
|
||||
return_value=TemperatureCheck(temp_celsius=35.0, timed_out=False),
|
||||
),
|
||||
patch.object(ScreenLocker, "_show_heat_skip_dialog", return_value=False),
|
||||
@ -276,9 +282,11 @@ class TestHeatSkipBranch:
|
||||
) -> None:
|
||||
"""A timed-out temperature check must fail closed, not skip the lock."""
|
||||
with (
|
||||
patch("screen_locker.screen_lock.has_weekly_minimum", return_value=False),
|
||||
patch(
|
||||
"screen_locker.screen_lock.fetch_current_temp_with_status",
|
||||
"screen_locker._startup_checks.has_weekly_minimum", return_value=False
|
||||
),
|
||||
patch(
|
||||
"screen_locker._startup_checks.fetch_current_temp_with_status",
|
||||
return_value=TemperatureCheck(temp_celsius=None, timed_out=True),
|
||||
),
|
||||
patch.object(ScreenLocker, "_show_heat_skip_dialog") as mock_dialog,
|
||||
@ -296,9 +304,11 @@ class TestHeatSkipBranch:
|
||||
) -> None:
|
||||
"""A failed (non-timeout) temperature check must also fail closed."""
|
||||
with (
|
||||
patch("screen_locker.screen_lock.has_weekly_minimum", return_value=False),
|
||||
patch(
|
||||
"screen_locker.screen_lock.fetch_current_temp_with_status",
|
||||
"screen_locker._startup_checks.has_weekly_minimum", return_value=False
|
||||
),
|
||||
patch(
|
||||
"screen_locker._startup_checks.fetch_current_temp_with_status",
|
||||
return_value=TemperatureCheck(temp_celsius=None, timed_out=False),
|
||||
),
|
||||
patch.object(ScreenLocker, "_show_heat_skip_dialog") as mock_dialog,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user