mirror of
https://github.com/kuhyx/screen-locker.git
synced 2026-11-10 20:07:16 +01:00
verify_lock_popup_safety.py (318) -> _xvfb_reexec.py shared re-exec
_popup_form_check.py HostileLock, ticks,
the real-form check
verify_screen_fits.py (266) -> uses _xvfb_reexec too
conftest.py (329) -> _tk_mocks.py the tk mock + the
module patch list
_opt_in_fixtures.py requestable fixtures
three test modules -> _part4/_part5/_part15 siblings
conftest keeps every autouse fixture. pytest applies autouse fixtures from
conftest only, so one moved to a sibling would silently stop running and take
its safety guard with it -- verified by asserting the fixtures are still
listed, not just that the suite is green. The five near-identical _isolate_*
fixtures collapse into one table-driven _isolate_state_files, which also picks
up the _shutdown_sick_state binding an earlier split created.
scripts/ becomes a real package so the shared helpers are importable. Both
gates now run as `python3 -m scripts.<name>`, including CI -- the path form put
scripts/ on sys.path instead of the repo root, and the alternative was a
try/except ImportError that this repo's no-silent-failures hook rightly
rejects. Each gate was run in its exact invocation form.
Note: verify_lock_popup_safety still fails on
`RecoveryLoop.__init__() takes 3 positional arguments but 6 were given`. That
is pre-existing -- confirmed by running it at HEAD in a clean worktree before
touching it -- a gatelock API drift, not this refactor. Left as found.
978 tests at 100% branch coverage. Gate: 41 -> 22 files, all remaining ones
Dart plus DESIGN_AUDIT_TODO.md.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EveB813BQ8abAP68VSqZ5Y
130 lines
4.6 KiB
Python
Executable File
130 lines
4.6 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Prove the lock-screen sport selector survives gatelock's recovery tick.
|
|
|
|
``_manual_workout_dialog._mw_sport_row`` has the mechanism and the incident.
|
|
``screen_locker/tests/test_no_popup_widgets.py`` keeps popups out of the lock
|
|
path statically; this script is the *behavioural* half. It builds the real
|
|
``ManualWorkoutDialogMixin`` form with real Tk, runs gatelock's two hostile
|
|
behaviours for five ticks with the "Other" sport selected, and checks the
|
|
selection and the swapped-in fields are still there. It also re-runs the
|
|
original failure against a ``tk.OptionMenu``, so the mechanism stays
|
|
demonstrated rather than merely described -- if that check ever stops
|
|
reproducing, this harness has drifted from what gatelock actually does.
|
|
|
|
Fidelity gap, deliberate: ``HostileLock`` replays the two behaviours rather
|
|
than calling ``RecoveryLoop.tick()``, which would need a real ``SurfaceSet``
|
|
managing real per-output windows on this throwaway display. It borrows the
|
|
real ``holds_grab()`` so the predicate cannot drift; a THIRD hostile behaviour
|
|
added to ``tick()`` would not be covered here.
|
|
|
|
Not a pytest test because ``screen_locker/tests/conftest.py`` replaces the
|
|
whole ``tk`` module for every test so the suite can never reach a real
|
|
display. This script re-executes itself under Xvfb instead, so a global grab
|
|
can never freeze the developer's own session. Run it directly, or let CI:
|
|
|
|
python3 -m scripts.verify_lock_popup_safety # from the repo root
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import os
|
|
import sys
|
|
import tkinter as tk
|
|
|
|
from gatelock import RecoveryLoop
|
|
|
|
from scripts._popup_form_check import (
|
|
check_real_form_survives,
|
|
grab_holder,
|
|
leaf,
|
|
run_ticks,
|
|
)
|
|
from scripts._xvfb_reexec import reexec_under_xvfb
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
TICK_MS = 1000
|
|
TICKS = 5
|
|
INNER_ENV = "_LOCK_POPUP_CHECK_INNER"
|
|
|
|
|
|
def check_optionmenu_still_fails() -> bool:
|
|
"""Show the original mechanism: a posted menu gets covered by the lift."""
|
|
root = tk.Tk()
|
|
root.geometry("800x600+0+0")
|
|
surface = tk.Toplevel(root)
|
|
surface.overrideredirect(boolean=True)
|
|
surface.geometry("800x600+0+0")
|
|
var = tk.StringVar(value="Table tennis")
|
|
option = tk.OptionMenu(surface, var, "Table tennis", "Other")
|
|
option.pack()
|
|
root.update()
|
|
|
|
menu = surface.nametowidget(option["menu"])
|
|
|
|
def open_menu() -> None:
|
|
menu.tk_popup(100, 100)
|
|
root.update()
|
|
|
|
def judge() -> bool:
|
|
"""A click where the menu appears must no longer reach the menu."""
|
|
centre_x = menu.winfo_rootx() + menu.winfo_width() // 2
|
|
centre_y = menu.winfo_rooty() + menu.winfo_height() // 2
|
|
hit = root.winfo_containing(centre_x, centre_y)
|
|
_logger.info(
|
|
" OptionMenu after %d ticks: grab=%s click_lands_on=%s",
|
|
TICKS,
|
|
grab_holder(root),
|
|
leaf(hit),
|
|
)
|
|
return hit is not menu
|
|
|
|
return run_ticks(root, judge, setup=open_menu)
|
|
|
|
|
|
def main() -> int:
|
|
"""Run both checks under Xvfb and report which held."""
|
|
logging.basicConfig(level=logging.INFO, format="%(message)s", stream=sys.stdout)
|
|
if os.environ.get(INNER_ENV) != "1":
|
|
return reexec_under_xvfb(
|
|
inner_env=INNER_ENV, module="scripts.verify_lock_popup_safety"
|
|
)
|
|
|
|
if not hasattr(RecoveryLoop, "holds_grab"):
|
|
_logger.error(
|
|
"The installed gatelock has no RecoveryLoop.holds_grab, so this "
|
|
"check cannot replay the real grab predicate. Bump the gatelock "
|
|
"pin in requirements.txt to a version that has it."
|
|
)
|
|
return 1
|
|
|
|
_logger.info("The mechanism (a popup on a lock surface):")
|
|
mechanism_reproduced = check_optionmenu_still_fails()
|
|
_logger.info("The fix (radio buttons in the production form):")
|
|
fix_holds = check_real_form_survives()
|
|
|
|
if not mechanism_reproduced:
|
|
_logger.error(
|
|
"The OptionMenu was NOT covered by the recovery tick. This check no "
|
|
"longer reproduces the failure it exists to guard against -- either "
|
|
"gatelock stopped lifting its surfaces, or this harness has drifted."
|
|
)
|
|
if not fix_holds:
|
|
_logger.error(
|
|
"The production sport selector did NOT survive %d recovery ticks. "
|
|
"The 2026-07-26 lock-screen bug is back.",
|
|
TICKS,
|
|
)
|
|
if mechanism_reproduced and fix_holds:
|
|
_logger.info(
|
|
"\nOK: a popup still dies under the tick; the shipped radio-button "
|
|
"selector survives it."
|
|
)
|
|
return 0
|
|
return 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|