screen-locker/screen_locker/tests/test_workout_app.py
Krzysztof kuhy Rudnicki aa3e290391 Document every test, split files at the cap
Real one-line docstrings stating what each test pins down. Files that
would pass 250 lines moved whole classes into _part2 siblings
(sick_panel, earned, poke_server, shutdown_reading,
manual_workout_dialog).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WzxdWYdwRAtMg7ki6TKxuU
2026-11-01 21:11:34 +01:00

242 lines
8.6 KiB
Python

"""Tests for supervising the desktop workout app as the lock surface.
The invariant every test here defends: the X grab is released ONLY once the
app has signalled that its own fullscreen window is up. Releasing it to an app
that never started would leave the machine unlocked with the obligation unmet.
"""
from __future__ import annotations
from pathlib import Path
from typing import TYPE_CHECKING
from screen_locker._workout_app import (
READY_MARKER,
GrabHandoff,
ProcessHooks,
WorkoutAppResult,
launch_workout_app,
workout_app_binary,
)
if TYPE_CHECKING:
from collections.abc import Sequence
class _FakeStdout:
"""A pipe that yields fixed lines, then EOF."""
def __init__(self, lines: Sequence[str]) -> None:
self._it = iter(lines)
def readline(self) -> str:
"""Return the next fixed line, then empty string at EOF."""
return next(self._it, "")
class _FakeChild:
"""A stand-in for the Flutter process."""
def __init__(self, lines: Sequence[str], *, returncode: int = 0) -> None:
self.stdout = _FakeStdout(lines)
self.returncode = returncode
self.terminated = False
self.waited = False
def terminate(self) -> None:
"""Record that the child was asked to terminate."""
self.terminated = True
def wait(self) -> int:
"""Record that the child was waited on and return its exit code."""
self.waited = True
return self.returncode
class _Recorder:
"""Records grab release/reacquire in call order."""
def __init__(self) -> None:
self.calls: list[str] = []
def release(self) -> None:
"""Record a grab release."""
self.calls.append("release")
def reacquire(self) -> None:
"""Record a grab reacquire."""
self.calls.append("reacquire")
def _always_readable(
rlist: object, _w: object, _x: object, _t: float
) -> tuple[object, list[object], list[object]]:
"""Stand in for select(): the fake pipe is always ready to read."""
return rlist, [], []
def _run(
lines: Sequence[str],
recorder: _Recorder,
*,
child: _FakeChild | None = None,
**kwargs: object,
) -> object:
kid = child if child is not None else _FakeChild(lines)
return launch_workout_app(
GrabHandoff(release=recorder.release, reacquire=recorder.reacquire),
binary=Path("/nonexistent/workout_app"),
hooks=ProcessHooks(popen=lambda *_a, **_k: kid, selector=_always_readable),
**kwargs,
)
class TestLaunchWorkoutApp:
"""The handoff, and every way it can fail safely."""
def test_releases_the_grab_once_the_app_is_ready(self) -> None:
"""The grab is released once the ready line arrives and reacquired afterwards."""
recorder = _Recorder()
result = _run([f"{READY_MARKER}\n"], recorder)
assert result.launched
assert recorder.calls == ["release", "reacquire"]
def test_tolerates_output_before_the_ready_line(self) -> None:
"""Flutter prints Mesa/Impeller noise before anything of ours."""
recorder = _Recorder()
result = _run(["MESA-EGL: warning\n", f"{READY_MARKER}\n"], recorder)
assert result.launched
assert "release" in recorder.calls
def test_never_releases_when_the_app_dies_before_ready(self) -> None:
"""The invariant: a dead app must not be handed the screen."""
recorder = _Recorder()
result = _run([], recorder)
assert not result.launched
assert "release" not in recorder.calls
assert "exited before signalling ready" in result.reason
def test_terminates_an_app_that_never_signals_ready(self) -> None:
"""A child that never signals ready is terminated."""
recorder = _Recorder()
child = _FakeChild([])
_run([], recorder, child=child)
assert child.terminated
def test_reports_a_hang_differently_from_a_crash(self) -> None:
"""A slow first sync and a broken build need different fixes."""
recorder = _Recorder()
# Never EOFs and never readies: readline keeps returning noise.
child = _FakeChild(["noise\n"] * 10_000)
result = _run([], recorder, child=child, ready_timeout=0.0)
assert not result.launched
assert "never signalled ready" in result.reason
assert "release" not in recorder.calls
def test_times_out_on_an_app_that_starts_but_prints_nothing(self) -> None:
"""The regression: a silent live app must not hold the lock forever.
``readline()`` on a real pipe blocks, so the timeout can only be
enforced by waiting on the pipe rather than polling the clock.
"""
recorder = _Recorder()
child = _FakeChild([])
def _never_readable(
_r: object, _w: object, _x: object, _t: float
) -> tuple[list[object], list[object], list[object]]:
return [], [], []
result = launch_workout_app(
GrabHandoff(release=recorder.release, reacquire=recorder.reacquire),
binary=Path("/nonexistent/workout_app"),
hooks=ProcessHooks(popen=lambda *_a, **_k: child, selector=_never_readable),
ready_timeout=5.0,
)
assert not result.launched
assert "never signalled ready" in result.reason
assert "release" not in recorder.calls
assert child.terminated
def test_reacquires_the_grab_even_when_the_app_crashes(self) -> None:
"""However the app exits, something must hold the screen after."""
recorder = _Recorder()
child = _FakeChild([f"{READY_MARKER}\n"], returncode=-15)
result = _run([], recorder, child=child)
assert recorder.calls == ["release", "reacquire"]
assert "-15" in result.reason
def test_waits_for_the_app_to_exit(self) -> None:
"""The launcher waits for the app process to exit."""
recorder = _Recorder()
child = _FakeChild([f"{READY_MARKER}\n"])
_run([], recorder, child=child)
assert child.waited
def test_treats_a_missing_stdout_pipe_as_a_dead_app(self) -> None:
"""No pipe means no ready line can ever arrive -- never release."""
recorder = _Recorder()
child = _FakeChild([])
child.stdout = None
result = _run([], recorder, child=child)
assert not result.launched
assert "release" not in recorder.calls
assert child.terminated
def test_says_so_when_the_app_is_not_built(self) -> None:
"""A missing built binary is reported as not built and the grab is untouched."""
recorder = _Recorder()
result = launch_workout_app(
GrabHandoff(release=recorder.release, reacquire=recorder.reacquire),
resolve_binary=lambda: None,
hooks=ProcessHooks(
popen=lambda *_a, **_k: _FakeChild([]), selector=_always_readable
),
)
assert not result.launched
assert "not built" in result.reason
assert not recorder.calls
class TestWorkoutAppBinary:
"""Locating the built bundle."""
def test_returns_none_when_nothing_is_built(self, tmp_path: Path) -> None:
"""No built bundle yields None."""
assert workout_app_binary(tmp_path) is None
def test_defaults_to_the_real_repo_root(self) -> None:
"""The no-argument call is what the lock screen actually uses."""
found = workout_app_binary()
assert found is None or found.name == "workout_app"
def test_prefers_release_over_debug(self, tmp_path: Path) -> None:
"""A release bundle wins over a debug one."""
base = tmp_path / "stronglift_replacement/workout_app/build/linux/x64"
for flavour in ("debug", "release"):
binary = base / flavour / "bundle" / "workout_app"
binary.parent.mkdir(parents=True)
binary.touch()
assert workout_app_binary(tmp_path).parent.parent.name == "release"
def test_falls_back_to_debug(self, tmp_path: Path) -> None:
"""With only a debug bundle built, that one is used."""
binary = (
tmp_path
/ "stronglift_replacement/workout_app/build/linux/x64/debug/bundle"
/ "workout_app"
)
binary.parent.mkdir(parents=True)
binary.touch()
assert workout_app_binary(tmp_path) == binary
class TestWorkoutAppResult:
"""The result carries a reason a human can act on."""
def test_repr_shows_the_outcome_and_reason(self) -> None:
"""Logged on failure paths, so it must name why, not just False."""
result = WorkoutAppResult(launched=False, reason="not built")
assert "launched=False" in repr(result)
assert "not built" in repr(result)