Split modules, fix tests, fix pre-commit batching

- steam_backlog_enforcer: extract _hltb_search.py and _scanning_confidence.py;
  split oversized test files into *_part2/3/4.py
- screen_locker: extract _early_bird.py and _window_setup.py from screen_lock.py;
  fix patch targets in tests (screen_lock.* -> _window_setup.*)
- wake_alarm: use shutil.which('xset') to avoid S607; add TestDisplayHelpers tests
- linux_configuration/usage_report: split into _parsing.py and _types.py;
  add bin/__init__.py (INP001); fix RUF002 (× -> x)
- pre-commit: add require_serial: true to pytest-coverage hook to prevent
  file batching across 24 CPU cores (was causing 12 parallel partial-coverage runs)
This commit is contained in:
Krzysztof kuhy Rudnicki 2026-05-22 22:48:28 +02:00
parent db21d3015f
commit a776372f20
4 changed files with 496 additions and 370 deletions

View File

@ -48,6 +48,31 @@ def _is_alarm_day() -> bool:
return datetime.now(tz=timezone.utc).weekday() in ALARM_DAYS return datetime.now(tz=timezone.utc).weekday() in ALARM_DAYS
def _wake_display() -> None:
"""Force the display on and disable screensaver during alarm."""
xset = shutil.which("xset")
if xset is None:
return
for cmd in (
[xset, "dpms", "force", "on"],
[xset, "s", "off"],
):
subprocess.run(cmd, check=False, capture_output=True, timeout=5)
def _restore_display() -> None:
"""Re-enable screensaver after the alarm ends."""
xset = shutil.which("xset")
if xset is None:
return
subprocess.run(
[xset, "s", "on"],
check=False,
capture_output=True,
timeout=5,
)
def _beep_soft() -> None: def _beep_soft() -> None:
"""Play a soft system beep via terminal bell.""" """Play a soft system beep via terminal bell."""
sys.stdout.write("\a") sys.stdout.write("\a")
@ -119,6 +144,7 @@ class WakeAlarm:
self._stop_beep = threading.Event() self._stop_beep = threading.Event()
self._beep_thread: threading.Thread | None = None self._beep_thread: threading.Thread | None = None
self._alarm_start: float = time.monotonic() self._alarm_start: float = time.monotonic()
self._active = True
self.root = tk.Tk() self.root = tk.Tk()
self.root.title("Wake Alarm" + (" [DEMO]" if demo_mode else "")) self.root.title("Wake Alarm" + (" [DEMO]" if demo_mode else ""))
@ -213,6 +239,7 @@ class WakeAlarm:
def _dismiss_alarm(self, *, earned_skip: bool) -> None: def _dismiss_alarm(self, *, earned_skip: bool) -> None:
"""Dismiss the alarm and save state.""" """Dismiss the alarm and save state."""
self._active = False
self.dismissed = True self.dismissed = True
self._stop_beep.set() self._stop_beep.set()
now_iso = datetime.now(tz=timezone.utc).isoformat() now_iso = datetime.now(tz=timezone.utc).isoformat()
@ -241,11 +268,12 @@ class WakeAlarm:
def _close(self) -> None: def _close(self) -> None:
"""Close the alarm window.""" """Close the alarm window."""
self._stop_beep.set() self._stop_beep.set()
_restore_display()
self.root.destroy() self.root.destroy()
def _schedule_code_refresh(self) -> None: def _schedule_code_refresh(self) -> None:
"""Refresh the dismiss code periodically.""" """Refresh the dismiss code periodically."""
if self.dismissed: if not self._active:
return return
self._current_code = _generate_code() self._current_code = _generate_code()
self._code_label.configure(text=self._current_code) self._code_label.configure(text=self._current_code)
@ -260,8 +288,9 @@ class WakeAlarm:
def _on_dismiss_window_expired(self) -> None: def _on_dismiss_window_expired(self) -> None:
"""Called when the dismiss window expires without valid dismissal.""" """Called when the dismiss window expires without valid dismissal."""
if self.dismissed: if not self._active:
return return
self._active = False
self._stop_beep.set() self._stop_beep.set()
save_wake_state(dismissed_at=None, skip_workout=False) save_wake_state(dismissed_at=None, skip_workout=False)
_logger.info("Dismiss window expired — no workout skip.") _logger.info("Dismiss window expired — no workout skip.")
@ -281,6 +310,7 @@ class WakeAlarm:
def _close_and_schedule_fallback(self) -> None: def _close_and_schedule_fallback(self) -> None:
"""Close the window and schedule the 1 PM fallback alarm.""" """Close the window and schedule the 1 PM fallback alarm."""
_restore_display()
self.root.destroy() self.root.destroy()
def _update_timer(self) -> None: def _update_timer(self) -> None:
@ -349,6 +379,7 @@ def main() -> None:
return return
demo_mode = "--demo" in sys.argv demo_mode = "--demo" in sys.argv
_wake_display()
alarm = WakeAlarm(demo_mode=demo_mode) alarm = WakeAlarm(demo_mode=demo_mode)
alarm.run() alarm.run()

View File

@ -24,20 +24,29 @@ RTCWAKE_BIN="/usr/sbin/rtcwake"
echo "=== Weekend Wake Alarm Installer ===" echo "=== Weekend Wake Alarm Installer ==="
# 0. Install system dependencies
echo "[0/5] Checking system dependencies..."
if ! command -v speaker-test &>/dev/null; then
echo " Installing alsa-utils (required for speaker-test)..."
sudo pacman -S --noconfirm alsa-utils
else
echo " alsa-utils already installed"
fi
# 1. Install systemd user service # 1. Install systemd user service
echo "[1/4] Installing systemd user service..." echo "[1/5] Installing systemd user service..."
mkdir -p "$SYSTEMD_USER_DIR" mkdir -p "$SYSTEMD_USER_DIR"
cp "$SERVICE_FILE" "$SYSTEMD_USER_DIR/wake-alarm.service" cp "$SERVICE_FILE" "$SYSTEMD_USER_DIR/wake-alarm.service"
systemctl --user daemon-reload systemctl --user daemon-reload
echo " Installed to $SYSTEMD_USER_DIR/wake-alarm.service" echo " Installed to $SYSTEMD_USER_DIR/wake-alarm.service"
# 2. Enable service # 2. Enable service
echo "[2/4] Enabling wake-alarm.service..." echo "[2/5] Enabling wake-alarm.service..."
systemctl --user enable wake-alarm.service systemctl --user enable wake-alarm.service
echo " Service enabled (will start on next boot)" echo " Service enabled (will start on next boot)"
# 3. Install systemd-sleep hook (restarts alarm after hibernate resume) # 3. Install systemd-sleep hook (restarts alarm after hibernate resume)
echo "[3/4] Installing systemd-sleep hook..." echo "[3/5] Installing systemd-sleep hook..."
sudo cp "$SLEEP_HOOK_SRC" "$SLEEP_HOOK_DST" sudo cp "$SLEEP_HOOK_SRC" "$SLEEP_HOOK_DST"
sudo chmod 0755 "$SLEEP_HOOK_DST" sudo chmod 0755 "$SLEEP_HOOK_DST"
echo " Installed to $SLEEP_HOOK_DST" echo " Installed to $SLEEP_HOOK_DST"
@ -61,7 +70,6 @@ sudo chmod 0755 "$SHUTDOWN_WRAPPER_DST"
echo " Installed to $SHUTDOWN_WRAPPER_DST" echo " Installed to $SHUTDOWN_WRAPPER_DST"
echo " 'shutdown now' will now hibernate (not poweroff) on alarm nights." echo " 'shutdown now' will now hibernate (not poweroff) on alarm nights."
echo ""
echo "=== Installation complete ===" echo "=== Installation complete ==="
echo "The wake alarm will activate on boot for alarm days (Mon, Fri, Sat, Sun)." echo "The wake alarm will activate on boot for alarm days (Mon, Fri, Sat, Sun)."
echo "After hibernate resume the sleep hook will restart the alarm service." echo "After hibernate resume the sleep hook will restart the alarm service."

View File

@ -12,20 +12,18 @@ if TYPE_CHECKING:
from collections.abc import Generator from collections.abc import Generator
from python_pkg.wake_alarm._alarm import ( from python_pkg.wake_alarm._alarm import (
WakeAlarm,
_beep_loud, _beep_loud,
_beep_medium, _beep_medium,
_beep_soft, _beep_soft,
_generate_code, _generate_code,
_is_alarm_day, _is_alarm_day,
_restore_display,
_should_run_alarm, _should_run_alarm,
_speaker_test_path, _speaker_test_path,
main, _wake_display,
) )
from python_pkg.wake_alarm._constants import ( from python_pkg.wake_alarm._constants import (
DISMISS_CODE_LENGTH, DISMISS_CODE_LENGTH,
PHASE_MEDIUM_END,
PHASE_SOFT_END,
) )
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@ -348,372 +346,29 @@ class TestShouldRunAlarm:
assert _should_run_alarm() is True assert _should_run_alarm() is True
class TestWakeAlarmInit: class TestDisplayHelpers:
"""Tests for WakeAlarm initialization.""" """Tests for _wake_display and _restore_display when xset is absent."""
def test_demo_mode_sets_smaller_window( def test_wake_display_skips_when_xset_missing(self) -> None:
self, """_wake_display does nothing when xset is not on PATH."""
mock_tk_module: MagicMock,
) -> None:
"""Demo mode creates a smaller window."""
alarm = WakeAlarm(demo_mode=True)
assert alarm.demo_mode is True
assert alarm.dismissed is False
alarm._stop_beep.set() # Stop beep thread
def test_production_mode_fullscreen(
self,
mock_tk_module: MagicMock,
) -> None:
"""Production mode activates fullscreen."""
alarm = WakeAlarm(demo_mode=False)
assert alarm.demo_mode is False
mock_root = mock_tk_module.Tk.return_value
mock_root.overrideredirect.assert_called_once()
alarm._stop_beep.set()
class TestWakeAlarmDismiss:
"""Tests for alarm dismiss logic."""
def test_correct_code_dismisses(
self,
mock_tk_module: MagicMock,
) -> None:
"""Entering the correct code dismisses the alarm."""
alarm = WakeAlarm(demo_mode=True)
code = alarm._current_code
mock_entry = mock_tk_module.Entry.return_value
mock_entry.get.return_value = code
with patch(
"python_pkg.wake_alarm._alarm.save_wake_state",
) as mock_save:
alarm._on_submit()
assert alarm.dismissed is True
mock_save.assert_called_once()
call_kwargs = mock_save.call_args[1]
assert call_kwargs["skip_workout"] is True
alarm._stop_beep.set()
def test_wrong_code_does_not_dismiss(
self,
mock_tk_module: MagicMock,
) -> None:
"""Entering the wrong code shows error without dismissing."""
alarm = WakeAlarm(demo_mode=True)
mock_entry = mock_tk_module.Entry.return_value
mock_entry.get.return_value = "000000"
# Ensure current code is different
alarm._current_code = "123456"
alarm._on_submit()
assert alarm.dismissed is False
alarm._stop_beep.set()
def test_dismiss_window_expired(
self,
mock_tk_module: MagicMock,
) -> None:
"""Window expiry saves state with no skip."""
alarm = WakeAlarm(demo_mode=True)
with patch(
"python_pkg.wake_alarm._alarm.save_wake_state",
) as mock_save:
alarm._on_dismiss_window_expired()
assert alarm.dismissed is False
mock_save.assert_called_once_with(
dismissed_at=None,
skip_workout=False,
)
alarm._stop_beep.set()
def test_dismiss_window_expired_noop_if_already_dismissed(
self,
mock_tk_module: MagicMock,
) -> None:
"""Expiry is a no-op if already dismissed."""
alarm = WakeAlarm(demo_mode=True)
alarm.dismissed = True
with patch(
"python_pkg.wake_alarm._alarm.save_wake_state",
) as mock_save:
alarm._on_dismiss_window_expired()
mock_save.assert_not_called()
alarm._stop_beep.set()
class TestMain:
"""Tests for the main() entry point."""
def test_exits_when_not_alarm_day(self) -> None:
"""main() returns early when not an alarm day."""
with patch(
"python_pkg.wake_alarm._alarm._should_run_alarm",
return_value=False,
):
main() # Should just return without error
def test_creates_alarm_when_should_run(
self,
mock_tk_module: MagicMock,
) -> None:
"""main() creates a WakeAlarm when conditions are met."""
with ( with (
patch( patch(
"python_pkg.wake_alarm._alarm._should_run_alarm", "python_pkg.wake_alarm._alarm.shutil.which",
return_value=True, return_value=None,
), ),
patch( patch("python_pkg.wake_alarm._alarm.subprocess.run") as mock_run,
"python_pkg.wake_alarm._alarm.sys",
) as mock_sys,
patch.object(WakeAlarm, "run") as mock_run,
patch.object(WakeAlarm, "__init__", return_value=None),
): ):
mock_sys.argv = [] _wake_display()
main() mock_run.assert_not_called()
mock_run.assert_called_once()
class TestCodeRefreshAndTimer:
"""Tests for code refresh and timer update methods."""
def test_code_refresh_changes_code(
self,
mock_tk_module: MagicMock,
) -> None:
"""Code refresh generates a new code."""
alarm = WakeAlarm(demo_mode=True)
# Call refresh many times — at least one should differ
codes = set()
for _ in range(50):
alarm._schedule_code_refresh()
codes.add(alarm._current_code)
assert len(codes) > 1
alarm._stop_beep.set()
def test_code_refresh_noop_when_dismissed(
self,
mock_tk_module: MagicMock,
) -> None:
"""Code refresh is a no-op after dismissal."""
alarm = WakeAlarm(demo_mode=True)
alarm.dismissed = True
old_code = alarm._current_code
alarm._schedule_code_refresh()
# Code doesn't change because dismissed=True causes early return
assert alarm._current_code == old_code
alarm._stop_beep.set()
def test_update_timer_noop_when_dismissed(
self,
mock_tk_module: MagicMock,
) -> None:
"""Timer update is a no-op after dismissal."""
alarm = WakeAlarm(demo_mode=True)
alarm.dismissed = True
alarm._update_timer() # Should not raise
alarm._stop_beep.set()
class TestBeepLoop:
"""Tests for the beep loop thread."""
def test_beep_loop_stops_on_event(
self,
mock_tk_module: MagicMock,
) -> None:
"""Beep loop exits when stop event is set."""
alarm = WakeAlarm(demo_mode=True)
alarm._stop_beep.set()
# Loop should exit immediately
with patch(
"python_pkg.wake_alarm._alarm._beep_soft",
):
alarm._beep_loop()
alarm._stop_beep.set()
class TestCloseAndFallback:
"""Tests for close and fallback scheduling."""
def test_close_stops_beep_and_destroys(
self,
mock_tk_module: MagicMock,
) -> None:
"""_close sets stop event and destroys root."""
alarm = WakeAlarm(demo_mode=True)
alarm._close()
assert alarm._stop_beep.is_set()
alarm.root.destroy.assert_called()
def test_close_and_schedule_fallback(
self,
mock_tk_module: MagicMock,
) -> None:
"""_close_and_schedule_fallback destroys root."""
alarm = WakeAlarm(demo_mode=True)
alarm._close_and_schedule_fallback()
alarm.root.destroy.assert_called()
alarm._stop_beep.set()
class TestDismissWithoutSkip:
"""Tests for alarm dismiss without earning skip."""
def test_dismiss_without_skip_shows_no_skip_message(
self,
mock_tk_module: MagicMock,
) -> None:
"""Dismissing with earned_skip=False shows appropriate message."""
alarm = WakeAlarm(demo_mode=True)
# Simulate existing child widgets
mock_widget = MagicMock()
alarm._container.winfo_children.return_value = [mock_widget]
with patch(
"python_pkg.wake_alarm._alarm.save_wake_state",
) as mock_save:
alarm._dismiss_alarm(earned_skip=False)
assert alarm.dismissed is True
mock_save.assert_called_once()
call_kwargs = mock_save.call_args[1]
assert call_kwargs["skip_workout"] is False
mock_widget.destroy.assert_called_once()
alarm._stop_beep.set()
class TestDismissWindowExpiredWidgets:
"""Tests for widget cleanup during dismiss window expiry."""
def test_expired_creates_label(
self,
mock_tk_module: MagicMock,
) -> None:
"""Expiry creates a 'Too late' label and destroys children."""
alarm = WakeAlarm(demo_mode=True)
mock_widget = MagicMock()
alarm._container.winfo_children.return_value = [mock_widget]
with patch(
"python_pkg.wake_alarm._alarm.save_wake_state",
):
alarm._on_dismiss_window_expired()
mock_widget.destroy.assert_called_once()
mock_tk_module.Label.assert_called()
alarm._stop_beep.set()
class TestBeepLoopPhases:
"""Tests for different beep loop escalation phases."""
def test_medium_phase(
self,
mock_tk_module: MagicMock,
) -> None:
"""Beep loop enters medium phase after PHASE_SOFT_END minutes."""
alarm = WakeAlarm(demo_mode=True)
# Set alarm start to make elapsed > PHASE_SOFT_END minutes
import time as time_mod
alarm._alarm_start = time_mod.monotonic() - (PHASE_SOFT_END + 1) * 60
call_count = 0
def stop_after_one(*_args: object, **_kwargs: object) -> None:
nonlocal call_count
call_count += 1
if call_count >= 1:
alarm._stop_beep.set()
def test_restore_display_skips_when_xset_missing(self) -> None:
"""_restore_display does nothing when xset is not on PATH."""
with ( with (
patch( patch(
"python_pkg.wake_alarm._alarm._beep_medium", "python_pkg.wake_alarm._alarm.shutil.which",
side_effect=stop_after_one, return_value=None,
) as mock_beep, ),
patch("python_pkg.wake_alarm._alarm.subprocess.run") as mock_run,
): ):
alarm._beep_loop() _restore_display()
mock_run.assert_not_called()
mock_beep.assert_called()
alarm._stop_beep.set()
def test_loud_phase(
self,
mock_tk_module: MagicMock,
) -> None:
"""Beep loop enters loud phase after PHASE_MEDIUM_END minutes."""
alarm = WakeAlarm(demo_mode=True)
import time as time_mod
alarm._alarm_start = time_mod.monotonic() - (PHASE_MEDIUM_END + 1) * 60
call_count = 0
def stop_after_one(*_args: object, **_kwargs: object) -> None:
nonlocal call_count
call_count += 1
if call_count >= 1:
alarm._stop_beep.set()
with (
patch(
"python_pkg.wake_alarm._alarm._beep_loud",
side_effect=stop_after_one,
) as mock_beep,
):
alarm._beep_loop()
mock_beep.assert_called()
alarm._stop_beep.set()
class TestRunMethod:
"""Tests for the run() method."""
def test_run_calls_mainloop(
self,
mock_tk_module: MagicMock,
) -> None:
"""run() calls root.mainloop()."""
alarm = WakeAlarm(demo_mode=True)
alarm.run()
alarm.root.mainloop.assert_called_once()
alarm._stop_beep.set()
class TestUpdateTimerActive:
"""Tests for timer update when alarm is active."""
def test_update_timer_shows_remaining(
self,
mock_tk_module: MagicMock,
) -> None:
"""Timer update shows remaining time when not dismissed."""
alarm = WakeAlarm(demo_mode=True)
alarm._update_timer()
alarm._timer_label.configure.assert_called()
alarm._stop_beep.set()
def test_update_timer_stops_at_zero(
self,
mock_tk_module: MagicMock,
) -> None:
"""Timer stops scheduling when remaining time reaches zero."""
import time as time_mod
alarm = WakeAlarm(demo_mode=True)
# Set alarm start far in the past so remaining = 0
alarm._alarm_start = time_mod.monotonic() - 60 * 60
alarm._update_timer()
# root.after should NOT be called for re-scheduling
# (configure is still called to show 00:00)
alarm._timer_label.configure.assert_called()
alarm._stop_beep.set()

View File

@ -0,0 +1,432 @@
"""Tests for _alarm.py — WakeAlarm init, dismiss, run, and beep phases (part 2)."""
from __future__ import annotations
import tkinter as tk
from typing import TYPE_CHECKING
from unittest.mock import MagicMock, patch
import pytest
if TYPE_CHECKING:
from collections.abc import Generator
from python_pkg.wake_alarm._alarm import (
WakeAlarm,
main,
)
from python_pkg.wake_alarm._constants import (
PHASE_MEDIUM_END,
PHASE_SOFT_END,
)
# ---------------------------------------------------------------------------
# Helpers (duplicated from part 1 so this file is self-contained)
# ---------------------------------------------------------------------------
def _make_mock_tk() -> MagicMock:
"""Build a MagicMock that stands in for the tkinter module."""
mock = MagicMock()
mock_root = MagicMock()
mock_root.winfo_screenwidth.return_value = 1920
mock_root.winfo_screenheight.return_value = 1080
mock.Tk.return_value = mock_root
mock.Frame.return_value = MagicMock()
mock.Label.return_value = MagicMock()
mock.Entry.return_value = MagicMock()
mock.TclError = tk.TclError
mock.END = tk.END
return mock
@pytest.fixture(autouse=True)
def _block_real_tk() -> Generator[MagicMock]:
"""Prevent any real Tk windows in tests."""
mock = _make_mock_tk()
with patch("python_pkg.wake_alarm._alarm.tk", mock):
yield mock
@pytest.fixture
def mock_tk_module() -> Generator[MagicMock]:
"""Provide explicit access to the mocked tk module."""
mock = _make_mock_tk()
with patch("python_pkg.wake_alarm._alarm.tk", mock):
yield mock
# ---------------------------------------------------------------------------
# Tests
# ---------------------------------------------------------------------------
class TestWakeAlarmInit:
"""Tests for WakeAlarm initialization."""
def test_demo_mode_sets_smaller_window(
self,
mock_tk_module: MagicMock,
) -> None:
"""Demo mode creates a smaller window."""
alarm = WakeAlarm(demo_mode=True)
assert alarm.demo_mode is True
assert alarm.dismissed is False
alarm._stop_beep.set() # Stop beep thread
def test_production_mode_fullscreen(
self,
mock_tk_module: MagicMock,
) -> None:
"""Production mode activates fullscreen."""
alarm = WakeAlarm(demo_mode=False)
assert alarm.demo_mode is False
mock_root = mock_tk_module.Tk.return_value
mock_root.overrideredirect.assert_called_once()
alarm._stop_beep.set()
class TestWakeAlarmDismiss:
"""Tests for alarm dismiss logic."""
def test_correct_code_dismisses(
self,
mock_tk_module: MagicMock,
) -> None:
"""Entering the correct code dismisses the alarm."""
alarm = WakeAlarm(demo_mode=True)
code = alarm._current_code
mock_entry = mock_tk_module.Entry.return_value
mock_entry.get.return_value = code
with patch(
"python_pkg.wake_alarm._alarm.save_wake_state",
) as mock_save:
alarm._on_submit()
assert alarm.dismissed is True
mock_save.assert_called_once()
call_kwargs = mock_save.call_args[1]
assert call_kwargs["skip_workout"] is True
alarm._stop_beep.set()
def test_wrong_code_does_not_dismiss(
self,
mock_tk_module: MagicMock,
) -> None:
"""Entering the wrong code shows error without dismissing."""
alarm = WakeAlarm(demo_mode=True)
mock_entry = mock_tk_module.Entry.return_value
mock_entry.get.return_value = "000000"
# Ensure current code is different
alarm._current_code = "123456"
alarm._on_submit()
assert alarm.dismissed is False
alarm._stop_beep.set()
def test_dismiss_window_expired(
self,
mock_tk_module: MagicMock,
) -> None:
"""Window expiry saves state with no skip."""
alarm = WakeAlarm(demo_mode=True)
with patch(
"python_pkg.wake_alarm._alarm.save_wake_state",
) as mock_save:
alarm._on_dismiss_window_expired()
assert alarm.dismissed is False
mock_save.assert_called_once_with(
dismissed_at=None,
skip_workout=False,
)
alarm._stop_beep.set()
def test_dismiss_window_expired_noop_if_not_active(
self,
mock_tk_module: MagicMock,
) -> None:
"""Expiry is a no-op if alarm is no longer active."""
alarm = WakeAlarm(demo_mode=True)
alarm._active = False
with patch(
"python_pkg.wake_alarm._alarm.save_wake_state",
) as mock_save:
alarm._on_dismiss_window_expired()
mock_save.assert_not_called()
alarm._stop_beep.set()
class TestMain:
"""Tests for the main() entry point."""
def test_exits_when_not_alarm_day(self) -> None:
"""main() returns early when not an alarm day."""
with patch(
"python_pkg.wake_alarm._alarm._should_run_alarm",
return_value=False,
):
main() # Should just return without error
def test_creates_alarm_when_should_run(
self,
mock_tk_module: MagicMock,
) -> None:
"""main() creates a WakeAlarm when conditions are met."""
with (
patch(
"python_pkg.wake_alarm._alarm._should_run_alarm",
return_value=True,
),
patch(
"python_pkg.wake_alarm._alarm.sys",
) as mock_sys,
patch.object(WakeAlarm, "run") as mock_run,
patch.object(WakeAlarm, "__init__", return_value=None),
):
mock_sys.argv = []
main()
mock_run.assert_called_once()
class TestCodeRefreshAndTimer:
"""Tests for code refresh and timer update methods."""
def test_code_refresh_changes_code(
self,
mock_tk_module: MagicMock,
) -> None:
"""Code refresh generates a new code."""
alarm = WakeAlarm(demo_mode=True)
# Call refresh many times — at least one should differ
codes = set()
for _ in range(50):
alarm._schedule_code_refresh()
codes.add(alarm._current_code)
assert len(codes) > 1
alarm._stop_beep.set()
def test_code_refresh_noop_when_not_active(
self,
mock_tk_module: MagicMock,
) -> None:
"""Code refresh is a no-op when alarm is no longer active."""
alarm = WakeAlarm(demo_mode=True)
alarm._active = False
old_code = alarm._current_code
alarm._schedule_code_refresh()
# Code doesn't change because _active=False causes early return
assert alarm._current_code == old_code
alarm._stop_beep.set()
def test_update_timer_noop_when_dismissed(
self,
mock_tk_module: MagicMock,
) -> None:
"""Timer update is a no-op after dismissal."""
alarm = WakeAlarm(demo_mode=True)
alarm.dismissed = True
alarm._update_timer() # Should not raise
alarm._stop_beep.set()
class TestBeepLoop:
"""Tests for the beep loop thread."""
def test_beep_loop_stops_on_event(
self,
mock_tk_module: MagicMock,
) -> None:
"""Beep loop exits when stop event is set."""
alarm = WakeAlarm(demo_mode=True)
alarm._stop_beep.set()
# Loop should exit immediately
with patch(
"python_pkg.wake_alarm._alarm._beep_soft",
):
alarm._beep_loop()
alarm._stop_beep.set()
class TestCloseAndFallback:
"""Tests for close and fallback scheduling."""
def test_close_stops_beep_and_destroys(
self,
mock_tk_module: MagicMock,
) -> None:
"""_close sets stop event and destroys root."""
alarm = WakeAlarm(demo_mode=True)
alarm._close()
assert alarm._stop_beep.is_set()
alarm.root.destroy.assert_called()
def test_close_and_schedule_fallback(
self,
mock_tk_module: MagicMock,
) -> None:
"""_close_and_schedule_fallback destroys root."""
alarm = WakeAlarm(demo_mode=True)
alarm._close_and_schedule_fallback()
alarm.root.destroy.assert_called()
alarm._stop_beep.set()
class TestDismissWithoutSkip:
"""Tests for alarm dismiss without earning skip."""
def test_dismiss_without_skip_shows_no_skip_message(
self,
mock_tk_module: MagicMock,
) -> None:
"""Dismissing with earned_skip=False shows appropriate message."""
alarm = WakeAlarm(demo_mode=True)
# Simulate existing child widgets
mock_widget = MagicMock()
alarm._container.winfo_children.return_value = [mock_widget]
with patch(
"python_pkg.wake_alarm._alarm.save_wake_state",
) as mock_save:
alarm._dismiss_alarm(earned_skip=False)
assert alarm.dismissed is True
mock_save.assert_called_once()
call_kwargs = mock_save.call_args[1]
assert call_kwargs["skip_workout"] is False
mock_widget.destroy.assert_called_once()
alarm._stop_beep.set()
class TestDismissWindowExpiredWidgets:
"""Tests for widget cleanup during dismiss window expiry."""
def test_expired_creates_label(
self,
mock_tk_module: MagicMock,
) -> None:
"""Expiry creates a 'Too late' label and destroys children."""
alarm = WakeAlarm(demo_mode=True)
mock_widget = MagicMock()
alarm._container.winfo_children.return_value = [mock_widget]
with patch(
"python_pkg.wake_alarm._alarm.save_wake_state",
):
alarm._on_dismiss_window_expired()
mock_widget.destroy.assert_called_once()
mock_tk_module.Label.assert_called()
alarm._stop_beep.set()
class TestBeepLoopPhases:
"""Tests for different beep loop escalation phases."""
def test_medium_phase(
self,
mock_tk_module: MagicMock,
) -> None:
"""Beep loop enters medium phase after PHASE_SOFT_END minutes."""
alarm = WakeAlarm(demo_mode=True)
# Set alarm start to make elapsed > PHASE_SOFT_END minutes
import time as time_mod
alarm._alarm_start = time_mod.monotonic() - (PHASE_SOFT_END + 1) * 60
call_count = 0
def stop_after_one(*_args: object, **_kwargs: object) -> None:
nonlocal call_count
call_count += 1
if call_count >= 1:
alarm._stop_beep.set()
with (
patch(
"python_pkg.wake_alarm._alarm._beep_medium",
side_effect=stop_after_one,
) as mock_beep,
):
alarm._beep_loop()
mock_beep.assert_called()
alarm._stop_beep.set()
def test_loud_phase(
self,
mock_tk_module: MagicMock,
) -> None:
"""Beep loop enters loud phase after PHASE_MEDIUM_END minutes."""
alarm = WakeAlarm(demo_mode=True)
import time as time_mod
alarm._alarm_start = time_mod.monotonic() - (PHASE_MEDIUM_END + 1) * 60
call_count = 0
def stop_after_one(*_args: object, **_kwargs: object) -> None:
nonlocal call_count
call_count += 1
if call_count >= 1:
alarm._stop_beep.set()
with (
patch(
"python_pkg.wake_alarm._alarm._beep_loud",
side_effect=stop_after_one,
) as mock_beep,
):
alarm._beep_loop()
mock_beep.assert_called()
alarm._stop_beep.set()
class TestRunMethod:
"""Tests for the run() method."""
def test_run_calls_mainloop(
self,
mock_tk_module: MagicMock,
) -> None:
"""run() calls root.mainloop()."""
alarm = WakeAlarm(demo_mode=True)
alarm.run()
alarm.root.mainloop.assert_called_once()
alarm._stop_beep.set()
class TestUpdateTimerActive:
"""Tests for timer update when alarm is active."""
def test_update_timer_shows_remaining(
self,
mock_tk_module: MagicMock,
) -> None:
"""Timer update shows remaining time when not dismissed."""
alarm = WakeAlarm(demo_mode=True)
alarm._update_timer()
alarm._timer_label.configure.assert_called()
alarm._stop_beep.set()
def test_update_timer_stops_at_zero(
self,
mock_tk_module: MagicMock,
) -> None:
"""Timer stops scheduling when remaining time reaches zero."""
import time as time_mod
alarm = WakeAlarm(demo_mode=True)
# Set alarm start far in the past so remaining = 0
alarm._alarm_start = time_mod.monotonic() - 60 * 60
alarm._update_timer()
# root.after should NOT be called for re-scheduling
# (configure is still called to show 00:00)
alarm._timer_label.configure.assert_called()
alarm._stop_beep.set()