mirror of
https://github.com/kuhyx/screen-locker.git
synced 2026-11-10 20:27:13 +01:00
`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
190 lines
5.9 KiB
Python
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
|