screen-locker/screen_locker/tests/test_wake_shutdown.py
Krzysztof kuhy Rudnicki ec0946f64e Grade against 3.14 too, not just run on it
`requires-python` and the CI pin already said 3.14, but ruff, mypy and pylint
were still grading against 3.10 -- three tools judging an interpreter nothing
runs any more, and ruff withholding the pyupgrade fixes that only become safe
at the newer target. `pin_ci_python.py` rewrites exactly these three knobs.

What the stale target was hiding: 118 sites on `datetime.timezone.utc` instead
of `datetime.UTC` (UP017), two classes still written with `Generic[...]` rather
than PEP 695 syntax, and ruff's stable formatter applying PEP 758 --
`except (A, B):` becomes `except A, B:`. That last one makes the source
3.14-only at the syntax level, which is what `requires-python = ">=3.14"`
already promised; kuhy chose it deliberately over holding ruff a release back
(2026-08-29).

The PEP 695 fix needed a hand: ruff rewrote `class WidgetGroup(Generic[_W])`
to `class WidgetGroup[W: tk.Misc]` but left the body annotated with the
module-level `_W`, a *different* type variable -- mypy caught it as "a function
returning TypeVar should receive at least one argument containing the same
TypeVar". Renamed the body to the new parameter, gave `FrameGroup.__init__`
the concrete `list[tk.Frame]` it always wrapped, and deleted the two now-dead
module-level TypeVars.

ruff, ruff-format, mypy --strict and both pylint passes clean at the new target.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SfnJmxxsZ534AKk16rC2Gr
2026-08-29 11:42:47 +02:00

190 lines
5.9 KiB
Python

"""Tests for rtcwake integration in ShutdownMixin."""
from __future__ import annotations
from datetime import UTC
from typing import TYPE_CHECKING
from unittest.mock import MagicMock, patch
from screen_locker.tests.conftest import create_locker
if TYPE_CHECKING:
from pathlib import Path
class TestIsTomorrowAlarmDay:
"""Tests for _is_tomorrow_alarm_day."""
def test_sunday_evening_means_monday_alarm(
self,
mock_tk: MagicMock,
mock_sys_exit: MagicMock,
tmp_path: Path,
) -> None:
"""Sunday evening → Monday is alarm day (weekday=0)."""
locker = create_locker(mock_tk, tmp_path)
from datetime import datetime
# Sunday 2026-04-12 → tomorrow Monday
with patch(
"screen_locker._wake_alarm.datetime",
) as mock_dt:
mock_dt.now.return_value = datetime(2026, 4, 12, 23, 0, tzinfo=UTC)
mock_dt.side_effect = datetime
from datetime import timedelta
# Ensure timedelta works
with patch(
"screen_locker._wake_alarm.timedelta",
timedelta,
):
assert locker._is_tomorrow_alarm_day() is True
def test_monday_evening_is_not_alarm_next(
self,
mock_tk: MagicMock,
mock_sys_exit: MagicMock,
tmp_path: Path,
) -> None:
"""Monday evening → Tuesday is NOT an alarm day."""
locker = create_locker(mock_tk, tmp_path)
from datetime import datetime, timedelta
# Monday 2026-04-13 → tomorrow Tuesday (weekday=1)
with (
patch(
"screen_locker._wake_alarm.datetime",
) as mock_dt,
patch(
"screen_locker._wake_alarm.timedelta",
timedelta,
),
):
mock_dt.now.return_value = datetime(2026, 4, 13, 23, 0, tzinfo=UTC)
mock_dt.side_effect = datetime
assert locker._is_tomorrow_alarm_day() is False
def test_thursday_evening_friday_is_alarm(
self,
mock_tk: MagicMock,
mock_sys_exit: MagicMock,
tmp_path: Path,
) -> None:
"""Thursday evening → Friday is alarm day (weekday=4)."""
locker = create_locker(mock_tk, tmp_path)
from datetime import datetime, timedelta
# Thursday 2026-04-16 → tomorrow Friday (weekday=4)
with (
patch(
"screen_locker._wake_alarm.datetime",
) as mock_dt,
patch(
"screen_locker._wake_alarm.timedelta",
timedelta,
),
):
mock_dt.now.return_value = datetime(2026, 4, 16, 23, 0, tzinfo=UTC)
mock_dt.side_effect = datetime
assert locker._is_tomorrow_alarm_day() is True
class TestScheduleRtcwake:
"""Tests for _schedule_rtcwake."""
def test_success(
self,
mock_tk: MagicMock,
mock_sys_exit: MagicMock,
tmp_path: Path,
) -> None:
"""Successful rtcwake call returns True."""
locker = create_locker(mock_tk, tmp_path)
with patch(
"screen_locker._wake_alarm.subprocess.run",
) as mock_run:
mock_run.return_value = MagicMock(returncode=0)
assert locker._schedule_rtcwake() is True
mock_run.assert_called_once()
cmd = mock_run.call_args[0][0]
assert "rtcwake" in cmd[1]
def test_failure_returns_false(
self,
mock_tk: MagicMock,
mock_sys_exit: MagicMock,
tmp_path: Path,
) -> None:
"""Failed rtcwake call returns False."""
locker = create_locker(mock_tk, tmp_path)
import subprocess
with patch(
"screen_locker._wake_alarm.subprocess.run",
side_effect=subprocess.SubprocessError("rtcwake failed"),
):
assert locker._schedule_rtcwake() is False
class TestScheduleWakeIfNeeded:
"""Tests for schedule_wake_if_needed."""
def test_skips_when_not_alarm_day(
self,
mock_tk: MagicMock,
mock_sys_exit: MagicMock,
tmp_path: Path,
) -> None:
"""Returns False when tomorrow is not an alarm day."""
locker = create_locker(mock_tk, tmp_path)
with patch.object(locker, "_is_tomorrow_alarm_day", return_value=False):
assert locker.schedule_wake_if_needed() is False
def test_schedules_when_alarm_day(
self,
mock_tk: MagicMock,
mock_sys_exit: MagicMock,
tmp_path: Path,
) -> None:
"""Returns True when tomorrow is an alarm day and rtcwake succeeds."""
locker = create_locker(mock_tk, tmp_path)
with (
patch.object(locker, "_is_tomorrow_alarm_day", return_value=True),
patch.object(locker, "_schedule_rtcwake", return_value=True),
):
assert locker.schedule_wake_if_needed() is True
def test_returns_false_when_rtcwake_fails(
self,
mock_tk: MagicMock,
mock_sys_exit: MagicMock,
tmp_path: Path,
) -> None:
"""Returns False when rtcwake call fails."""
locker = create_locker(mock_tk, tmp_path)
with (
patch.object(locker, "_is_tomorrow_alarm_day", return_value=True),
patch.object(locker, "_schedule_rtcwake", return_value=False),
):
assert locker.schedule_wake_if_needed() is False
class TestComputeWakeTimestamp:
"""Tests for _compute_wake_timestamp."""
def test_returns_future_epoch(
self,
mock_tk: MagicMock,
mock_sys_exit: MagicMock,
tmp_path: Path,
) -> None:
"""Wake timestamp is roughly 8 hours from now."""
locker = create_locker(mock_tk, tmp_path)
import time
now = int(time.time())
wake = locker._compute_wake_timestamp()
# Should be ~8 hours ahead (within 60 second tolerance)
expected = now + 8 * 3600
assert abs(wake - expected) < 60