mirror of
https://github.com/kuhyx/screen-locker.git
synced 2026-11-10 19:07:16 +01:00
Real fixes: split over-long functions (lock-decision chain, manual sync, validation, phone verification, status), hoist a dead lazy import, context-manage Xvfb and temp dirs, add encodings, share the silent-failure argv loop. lock_grab_handoff read lock._recovery, which gatelock's LockWindow does not have (its public property is recovery); a contract test now checks every attribute the handoff reads against the real class. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WzxdWYdwRAtMg7ki6TKxuU
150 lines
4.8 KiB
Python
150 lines
4.8 KiB
Python
"""Tests for lending the lock screen's grab to the workout app.
|
|
|
|
The invariant: the grab moves, nothing else does. VT switching stays disabled
|
|
and the arbiter claim stays held, because ``close()`` would undo both and hand
|
|
the user Ctrl+Alt+F2 out of the obligation the lock exists to enforce.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import tkinter as tk
|
|
from unittest.mock import MagicMock
|
|
|
|
from gatelock import LockWindow
|
|
|
|
from screen_locker._workout_handoff import lock_grab_handoff, start_workout
|
|
|
|
|
|
class _FakeRecovery:
|
|
"""Stands in for gatelock's watchdog, which re-grabs every 1000ms."""
|
|
|
|
def __init__(self) -> None:
|
|
self.running = True
|
|
self.events: list[str] = []
|
|
|
|
def stop(self) -> None:
|
|
self.running = False
|
|
self.events.append("stop")
|
|
|
|
|
|
class _FakeLock:
|
|
"""A lock window that records the order of grab operations."""
|
|
|
|
def __init__(self, *, release_raises: bool = False) -> None:
|
|
self.recovery = _FakeRecovery()
|
|
self.events: list[str] = []
|
|
self.closed = False
|
|
self._release_raises = release_raises
|
|
outer = self
|
|
|
|
class _Root:
|
|
def grab_release(self) -> None:
|
|
outer.events.append("grab_release")
|
|
if outer._release_raises:
|
|
msg = "no grab to release"
|
|
raise tk.TclError(msg)
|
|
|
|
self.root = _Root()
|
|
|
|
def report_grab_held(self, *, held: bool) -> None:
|
|
"""gatelock 0.11: what leisure-watch reads via ``held_grabs``."""
|
|
self.events.append(f"held={held}")
|
|
|
|
def grab_input(self) -> None:
|
|
self.events.append("grab_input")
|
|
self.recovery.running = True
|
|
|
|
def close(self) -> None:
|
|
self.closed = True
|
|
|
|
|
|
class TestLockGrabHandoff:
|
|
"""Releasing and re-taking, in the only safe order."""
|
|
|
|
def test_stops_the_watchdog_before_releasing(self) -> None:
|
|
"""A tick between release and the app's own grab would steal it back."""
|
|
lock = _FakeLock()
|
|
lock_grab_handoff(lock).release()
|
|
assert lock.recovery.events == ["stop"]
|
|
assert lock.events == ["held=False", "grab_release"]
|
|
assert not lock.recovery.running
|
|
|
|
def test_reacquire_restores_the_grab_and_the_watchdog(self) -> None:
|
|
handoff = lock_grab_handoff(lock := _FakeLock())
|
|
handoff.release()
|
|
handoff.reacquire()
|
|
assert lock.events == ["held=False", "grab_release", "grab_input"]
|
|
assert lock.recovery.running
|
|
|
|
def test_never_closes_the_lock_window(self) -> None:
|
|
"""close() also restores VT switching -- that is the escape hatch."""
|
|
handoff = lock_grab_handoff(lock := _FakeLock())
|
|
handoff.release()
|
|
handoff.reacquire()
|
|
assert not lock.closed
|
|
|
|
def test_tolerates_having_no_grab_to_release(self) -> None:
|
|
"""Tk raises when releasing a grab we do not hold; not fatal."""
|
|
handoff = lock_grab_handoff(lock := _FakeLock(release_raises=True))
|
|
handoff.release()
|
|
handoff.reacquire()
|
|
assert lock.events == ["held=False", "grab_release", "grab_input"]
|
|
|
|
|
|
class TestStartWorkout:
|
|
"""The button handler never raises onto a screen with no way out."""
|
|
|
|
def test_reports_a_failed_launch_as_a_message(self, monkeypatch: object) -> None:
|
|
from screen_locker import _workout_handoff
|
|
|
|
class _Result:
|
|
launched = False
|
|
reason = "not built"
|
|
|
|
monkeypatch.setattr(
|
|
_workout_handoff, "launch_workout_app", lambda _h: _Result()
|
|
)
|
|
message = start_workout(_FakeLock())
|
|
assert "Could not start the workout" in message
|
|
assert "not built" in message
|
|
|
|
def test_reports_a_finished_run(self, monkeypatch: object) -> None:
|
|
from screen_locker import _workout_handoff
|
|
|
|
class _Result:
|
|
launched = True
|
|
reason = "exited with code 0"
|
|
|
|
monkeypatch.setattr(
|
|
_workout_handoff, "launch_workout_app", lambda _h: _Result()
|
|
)
|
|
assert "checking whether it counted" in start_workout(_FakeLock())
|
|
|
|
|
|
class _Recorder:
|
|
"""A lock stand-in that records every attribute the handoff reads."""
|
|
|
|
def __init__(self) -> None:
|
|
self.names: set[str] = set()
|
|
|
|
def __getattr__(self, name: str) -> MagicMock:
|
|
self.names.add(name)
|
|
return MagicMock()
|
|
|
|
|
|
class TestAgainstTheRealGatelockApi:
|
|
"""The fakes above cannot notice a renamed or private gatelock attribute."""
|
|
|
|
def test_the_handoff_only_reads_attributes_a_real_lock_window_has(self) -> None:
|
|
"""``lock._recovery`` raised AttributeError live while fakes passed."""
|
|
spy = _Recorder()
|
|
handoff = lock_grab_handoff(spy)
|
|
handoff.release()
|
|
handoff.reacquire()
|
|
missing = {
|
|
name
|
|
for name in spy.names
|
|
if name != "root" and not hasattr(LockWindow, name)
|
|
}
|
|
assert not missing
|