mirror of
https://github.com/kuhyx/screen-locker.git
synced 2026-11-10 20:07:16 +01:00
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
pre-commit / pre-commit (push) Waiting to run
Tests / test (push) Waiting to run
Tests / lock-popup-safety (push) Waiting to run
release-apk / release (APK) (push) Waiting to run
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
215 lines
8.2 KiB
Python
215 lines
8.2 KiB
Python
"""Tests for _armed_state: is the locker actually schedulable.
|
|
|
|
The distinction these tests defend is the one that cost thirteen days in
|
|
2026-08: ``is-enabled`` says "enabled" for a timer whose job systemd deleted to
|
|
break an ordering cycle, so enabled alone is not armed.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import subprocess
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
from screen_locker._armed_state import (
|
|
REQUIRED_SERVICES,
|
|
REQUIRED_TIMERS,
|
|
TimerState,
|
|
collect_states,
|
|
locker_unit_active,
|
|
systemctl_available,
|
|
)
|
|
|
|
_ALL_UNITS = (*REQUIRED_TIMERS, *REQUIRED_SERVICES)
|
|
_PKG = "screen_locker._armed_state"
|
|
|
|
_LISTED = """NEXT LEFT LAST PASSED UNIT
|
|
Sun 2026-08-30 08:30:00 CEST 10h - - early-bird-workout-check.timer
|
|
Sat 2026-08-29 23:00:00 CEST 30min - - workout-locker.timer
|
|
"""
|
|
|
|
|
|
def _completed(stdout: str, code: int = 0) -> MagicMock:
|
|
"""Build a stand-in for a finished subprocess.
|
|
|
|
Args:
|
|
stdout: What the command printed.
|
|
code: Its return code.
|
|
|
|
Returns:
|
|
An object with the two attributes ``_run`` reads.
|
|
"""
|
|
proc = MagicMock()
|
|
proc.returncode = code
|
|
proc.stdout = stdout
|
|
return proc
|
|
|
|
|
|
class TestTimerState:
|
|
"""armed is the conjunction; describe() names every missing half."""
|
|
|
|
def test_enabled_and_scheduled_is_armed(self) -> None:
|
|
"""Both halves present means the timer will really fire."""
|
|
state = TimerState(
|
|
"t.timer", enabled=True, enabled_raw="enabled", scheduled=True
|
|
)
|
|
assert state.armed is True
|
|
assert "OK" in state.describe()
|
|
|
|
def test_enabled_but_unscheduled_is_not_armed(self) -> None:
|
|
"""The exact 2026-08 failure: enabled, but the job was deleted."""
|
|
state = TimerState(
|
|
"t.timer", enabled=True, enabled_raw="enabled", scheduled=False
|
|
)
|
|
assert state.armed is False
|
|
assert "no next trigger" in state.describe()
|
|
|
|
def test_scheduled_but_disabled_is_not_armed(self) -> None:
|
|
"""A transient start does not survive a reboot."""
|
|
state = TimerState(
|
|
"t.timer", enabled=False, enabled_raw="disabled", scheduled=True
|
|
)
|
|
assert state.armed is False
|
|
assert "not enabled (is-enabled=disabled)" in state.describe()
|
|
|
|
def test_unknown_enabled_state_is_named(self) -> None:
|
|
"""An empty is-enabled reads as unknown, not as a blank."""
|
|
state = TimerState("t.timer", enabled=False, enabled_raw="", scheduled=False)
|
|
assert "is-enabled=unknown" in state.describe()
|
|
|
|
|
|
class TestSystemctlAvailable:
|
|
""" "Could not check" must be distinguishable from "checked and fine"."""
|
|
|
|
def test_present(self) -> None:
|
|
"""systemctl on PATH means the check can run."""
|
|
with patch(f"{_PKG}.shutil.which", return_value="/usr/bin/systemctl"):
|
|
assert systemctl_available() is True
|
|
|
|
def test_absent(self) -> None:
|
|
"""No systemctl (containers, CI) is reported, never assumed fine."""
|
|
with patch(f"{_PKG}.shutil.which", return_value=None):
|
|
assert systemctl_available() is False
|
|
|
|
|
|
class TestCollectStates:
|
|
"""collect_states reads systemd's own view, not the unit files."""
|
|
|
|
def test_all_armed(self) -> None:
|
|
"""Every timer enabled and listed, and the login run anchored."""
|
|
with patch(f"{_PKG}.subprocess.run") as run:
|
|
run.side_effect = [
|
|
_completed(_LISTED),
|
|
_completed("active\n"),
|
|
*[_completed("enabled\n") for _ in _ALL_UNITS],
|
|
]
|
|
states = collect_states()
|
|
assert [s.name for s in states] == list(_ALL_UNITS)
|
|
assert all(s.armed for s in states)
|
|
|
|
def test_the_login_run_is_checked_too(self) -> None:
|
|
"""The 2026-08-30 gap: timers fine, login run silently disabled.
|
|
|
|
Enforcement read "armed" through a 24-minute unenforced window
|
|
because only the timers were ever looked at.
|
|
"""
|
|
with patch(f"{_PKG}.subprocess.run") as run:
|
|
run.side_effect = [
|
|
_completed(_LISTED),
|
|
_completed("active\n"),
|
|
*[_completed("enabled\n") for _ in REQUIRED_TIMERS],
|
|
_completed("disabled\n", code=1),
|
|
]
|
|
states = collect_states()
|
|
service = states[-1]
|
|
assert service.name == REQUIRED_SERVICES[0]
|
|
assert service.armed is False
|
|
assert not all(s.armed for s in states)
|
|
|
|
def test_an_inactive_login_target_disarms_the_service(self) -> None:
|
|
"""Enabled is not enough: i3 may never activate the target."""
|
|
with patch(f"{_PKG}.subprocess.run") as run:
|
|
run.side_effect = [
|
|
_completed(_LISTED),
|
|
_completed("inactive\n", code=3),
|
|
*[_completed("enabled\n") for _ in _ALL_UNITS],
|
|
]
|
|
states = collect_states()
|
|
service = states[-1]
|
|
assert service.enabled is True
|
|
assert service.armed is False
|
|
assert "is not active" in service.describe()
|
|
|
|
def test_timer_listed_with_no_next_trigger_is_unscheduled(self) -> None:
|
|
"""An "n/a" NEXT column means systemd will never run it."""
|
|
listed = (
|
|
"NEXT LEFT LAST PASSED UNIT\n n/a - - - workout-locker.timer\n"
|
|
)
|
|
with patch(f"{_PKG}.subprocess.run") as run:
|
|
run.side_effect = [
|
|
_completed(listed),
|
|
_completed("inactive\n", code=3),
|
|
*[_completed("enabled\n") for _ in _ALL_UNITS],
|
|
]
|
|
states = collect_states()
|
|
assert all(not s.scheduled for s in states)
|
|
|
|
def test_disabled_timer_is_reported(self) -> None:
|
|
"""A non-zero is-enabled marks the timer disabled."""
|
|
with patch(f"{_PKG}.subprocess.run") as run:
|
|
run.side_effect = [
|
|
_completed(_LISTED),
|
|
_completed("active\n"),
|
|
*[_completed("disabled\n", code=1) for _ in _ALL_UNITS],
|
|
]
|
|
states = collect_states()
|
|
assert all(not s.enabled for s in states)
|
|
|
|
def test_a_check_that_cannot_run_is_not_a_pass(self) -> None:
|
|
"""When systemctl cannot be executed, nothing reads as armed."""
|
|
with patch(f"{_PKG}.subprocess.run", side_effect=OSError("no exec")):
|
|
states = collect_states()
|
|
assert all(not s.armed for s in states)
|
|
|
|
def test_a_timeout_is_not_a_pass_either(self) -> None:
|
|
"""A hung systemctl must fail closed, same as a missing one."""
|
|
with patch(
|
|
f"{_PKG}.subprocess.run",
|
|
side_effect=subprocess.TimeoutExpired(cmd="systemctl", timeout=10),
|
|
):
|
|
states = collect_states()
|
|
assert all(not s.armed for s in states)
|
|
|
|
|
|
class TestLockerUnitActive:
|
|
""" "Would lock" says nothing about whether anything is enforcing it."""
|
|
|
|
def test_an_active_unit_reads_as_running(self) -> None:
|
|
"""The ordinary case while a lock is up."""
|
|
with patch(f"{_PKG}.subprocess.run", return_value=_completed("active\n")):
|
|
assert locker_unit_active() is True
|
|
|
|
def test_an_inactive_unit_reads_as_not_running(self) -> None:
|
|
"""The 2026-08-30 screenshot: decided to lock, nothing running."""
|
|
with patch(
|
|
f"{_PKG}.subprocess.run", return_value=_completed("inactive\n", code=3)
|
|
):
|
|
assert locker_unit_active() is False
|
|
|
|
def test_no_systemctl_is_unknown_not_false(self) -> None:
|
|
""" "Could not ask" must never render as "not running"."""
|
|
with patch(f"{_PKG}.shutil.which", return_value=None):
|
|
assert locker_unit_active() is None
|
|
|
|
def test_a_failed_probe_is_unknown_not_false(self) -> None:
|
|
"""A systemctl that cannot run answers nothing, not "no"."""
|
|
with patch(f"{_PKG}.subprocess.run", side_effect=OSError("no exec")):
|
|
assert locker_unit_active() is None
|
|
|
|
def test_an_armed_login_run_says_what_anchors_it(self) -> None:
|
|
"""The Health row must name the target, since that is what drifts."""
|
|
state = TimerState(
|
|
REQUIRED_SERVICES[0], enabled=True, enabled_raw="enabled", scheduled=True
|
|
)
|
|
assert state.armed is True
|
|
assert "graphical-session.target" in state.describe()
|