diff --git a/.github/workflows/python-tests.yml b/.github/workflows/python-tests.yml index 8869410..5bcd0fe 100644 --- a/.github/workflows/python-tests.yml +++ b/.github/workflows/python-tests.yml @@ -45,6 +45,9 @@ jobs: sudo apt-get update sudo apt-get install -y xvfb python3-tk pip install -r requirements.txt + # The script imports screen_locker directly rather than through + # pytest, which would put the rootdir on sys.path for it. + pip install -e . --no-deps - name: Sport selector survives gatelock's recovery tick run: python3 scripts/verify_lock_popup_safety.py diff --git a/pyproject.toml b/pyproject.toml index 9cf66b5..cfd95ec 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ requires-python = ">=3.10" # crdt-sync: shared CRDT merge scheme + GitHub-Contents-API sync transport, # also used by diet_guard; here for phone-workout GitHub sync (Workstream C) dependencies = [ - "gatelock @ git+https://github.com/kuhyx/utils@gatelock-v0.2.0#subdirectory=gatelock", + "gatelock @ git+https://github.com/kuhyx/utils@gatelock-v0.2.1#subdirectory=gatelock", "crdt-sync @ git+https://github.com/kuhyx/utils@crdt-sync-v0.1.0#subdirectory=crdt-sync", ] diff --git a/requirements.txt b/requirements.txt index 2351a96..2e7f9bd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ bandit>=1.7.0 codespell>=2.2.0 coverage>=7.4.0 crdt-sync @ git+https://github.com/kuhyx/utils@crdt-sync-v0.1.0#subdirectory=crdt-sync -gatelock @ git+https://github.com/kuhyx/utils@gatelock-v0.2.0#subdirectory=gatelock +gatelock @ git+https://github.com/kuhyx/utils@gatelock-v0.2.1#subdirectory=gatelock mcp>=1.9 mypy>=1.8.0 pre-commit>=3.6.0 diff --git a/scripts/verify_lock_popup_safety.py b/scripts/verify_lock_popup_safety.py index e83e1ed..2a22e46 100755 --- a/scripts/verify_lock_popup_safety.py +++ b/scripts/verify_lock_popup_safety.py @@ -97,10 +97,13 @@ class HostileLock: def _tick(self) -> None: """Lift the surface and re-take the grab, exactly as gatelock does.""" - self.ticks += 1 self.root.lift() if not self._loop.holds_grab(): self.root.grab_set_global() + # Counted only after the work, so a raising tick does not count. Tk + # swallows exceptions from `after` callbacks, and this harness once + # reported OK while every tick was dying on an AttributeError. + self.ticks += 1 self.root.after(TICK_MS, self._tick) @@ -120,13 +123,21 @@ def run_ticks( nonlocal verdict verdict = judge() - HostileLock(root) + lock = HostileLock(root) if setup is not None: root.after(200, setup) root.after(TICK_MS * TICKS + 400, decide) root.after(TICK_MS * TICKS + 600, root.quit) root.mainloop() root.destroy() + if lock.ticks < TICKS: + _logger.error( + " only %d of %d hostile ticks completed -- the harness itself is " + "broken, so this result proves nothing", + lock.ticks, + TICKS, + ) + return False return verdict @@ -269,6 +280,14 @@ def main() -> int: if os.environ.get(INNER_ENV) != "1": return reexec_under_xvfb() + 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):")