mirror of
https://github.com/kuhyx/wake-alarm.git
synced 2026-11-11 17:43:49 +01:00
CI caught what a developer machine cannot: the hook died with "No module named" its own package. The package is pip-installed locally, so running the script by path found it anyway; a fresh checkout has no install, and `python3 scripts/x.py` puts only `scripts/` on sys.path. `python3 -m scripts.verify_screen_fits` puts the repo root there instead, which is the checkout in CI and locally alike. The xvfb re-exec runs the same way, with an explicit cwd so the child does not depend on where the parent was invoked from. pylint had the same blind spot for the same reason: it infers sys.path from the files it is handed, so a script outside the package resolved its imports only when a package file happened to be in the same batch -- passing or failing by what else was staged. An init-hook puts the repo root on the path unconditionally. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BsXnTHyuuCUZsqetTaajcF
178 lines
5.6 KiB
Python
Executable File
178 lines
5.6 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Assert the alarm's screens fit a 1366x768 display.
|
|
|
|
The alarm ``place``s a centred frame, and a ``place``d frame takes its
|
|
*requested* size: when it is taller than the screen the parent shears equal
|
|
amounts off the top **and** the bottom -- which here means losing "WAKE UP!"
|
|
and the answer entry at once, on a screen whose entire purpose is to be
|
|
answered before it will stop. There is no scrollbar and nothing to indicate
|
|
anything is missing, so "it fits" is the only thing keeping it usable.
|
|
|
|
Runs the real builders against a real (throwaway) X display, because widget
|
|
heights come from the font engine and nothing short of rendering can answer
|
|
this honestly:
|
|
|
|
cd ~/wake-alarm && python3 -m scripts.verify_screen_fits
|
|
|
|
Run as a *module*, not a path: ``-m`` puts the repo root on
|
|
``sys.path`` so ``wake_alarm`` resolves from the checkout. Running it as a
|
|
path only works where the package happens to be pip-installed, which is
|
|
why it passed locally and died in CI.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import os
|
|
from pathlib import Path
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import tkinter as tk
|
|
from typing import TYPE_CHECKING
|
|
|
|
from gatelock import LockConfig, measure_fit, report_fit
|
|
|
|
from wake_alarm._alarm_view import _build_alarm_view
|
|
from wake_alarm._challenges import (
|
|
_make_flash_challenge,
|
|
_make_math_challenge,
|
|
_make_reverse_challenge,
|
|
_make_sequence_challenge,
|
|
_make_sort_challenge,
|
|
)
|
|
from wake_alarm._escape_flow import EscapeCallbacks, EscapeDialog, build_tracker
|
|
|
|
if TYPE_CHECKING:
|
|
from collections.abc import Callable
|
|
|
|
from gatelock import FitResult, ScrollableSurface
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
INNER_ENV = "_SCREEN_FITS_CHECK_INNER"
|
|
|
|
# The panel this app has to fit: the machine it runs on.
|
|
WIDTH = 1366
|
|
HEIGHT = 768
|
|
|
|
|
|
def reexec_under_xvfb() -> int:
|
|
"""Re-run this script on a throwaway X display, returning its exit code."""
|
|
xvfb_run = shutil.which("xvfb-run")
|
|
if xvfb_run is None:
|
|
_logger.error(
|
|
"xvfb-run not found. Install it with: sudo pacman -S --needed "
|
|
"xorg-server-xvfb"
|
|
)
|
|
return 1
|
|
env = dict(os.environ, **{INNER_ENV: "1"})
|
|
return subprocess.run(
|
|
[
|
|
xvfb_run,
|
|
"-a",
|
|
# Bigger than the size under test, so the surface is sized by the
|
|
# geometry the harness sets rather than clamped by the display.
|
|
"-s",
|
|
"-screen 0 1600x1200x24",
|
|
sys.executable,
|
|
"-m",
|
|
"scripts.verify_screen_fits",
|
|
],
|
|
check=False,
|
|
cwd=Path(__file__).resolve().parent.parent,
|
|
env=env,
|
|
).returncode
|
|
|
|
|
|
def _placed_child(parent: tk.Misc) -> tk.Misc:
|
|
"""Return the ``place``d descendant that carries the screen's height.
|
|
|
|
``place`` does not propagate a child's size to its parent, so measuring
|
|
the parent would report an empty screen. Falls back to the parent itself
|
|
when nothing is placed, which measures correctly for packed layouts.
|
|
"""
|
|
for child in parent.winfo_children():
|
|
if child.winfo_manager() == "place":
|
|
return child
|
|
found = _placed_child(child)
|
|
if found is not child:
|
|
return found
|
|
return parent
|
|
|
|
|
|
# Every challenge kind, not a random one: the flash kind renders its code at
|
|
# 2.25x the display token where the others use 1.5x, and the sequence kind's
|
|
# display string is the longest. A hook that picked one at random would pass
|
|
# or fail by dice roll -- which is worse than no hook.
|
|
_CHALLENGE_KINDS = (
|
|
("math", _make_math_challenge),
|
|
("flash", _make_flash_challenge),
|
|
("sort", _make_sort_challenge),
|
|
("sequence", _make_sequence_challenge),
|
|
("reverse", _make_reverse_challenge),
|
|
)
|
|
|
|
|
|
def _build_alarm(surface: ScrollableSurface, make: Callable[[], object]) -> tk.Misc:
|
|
"""Paint one dismiss challenge and return the frame to measure."""
|
|
_build_alarm_view(
|
|
surface.content,
|
|
make(),
|
|
LockConfig(),
|
|
on_submit=lambda _event: None,
|
|
entry_var=tk.StringVar(master=surface.content),
|
|
)
|
|
return _placed_child(surface.content)
|
|
|
|
|
|
def _build_escape(surface: ScrollableSurface) -> tk.Misc:
|
|
"""Paint the escape-hatch form and return the frame to measure."""
|
|
dialog = EscapeDialog(
|
|
surface.content,
|
|
LockConfig(),
|
|
build_tracker(),
|
|
EscapeCallbacks(on_granted=lambda: None, on_cancel=lambda: None),
|
|
)
|
|
dialog.frame.pack()
|
|
return dialog.frame
|
|
|
|
|
|
def measure_all() -> list[FitResult]:
|
|
"""Measure every screen at the supported size."""
|
|
results = [
|
|
measure_fit(
|
|
f"dismiss-challenge:{kind}",
|
|
lambda surface, make=make: _build_alarm(surface, make),
|
|
width=WIDTH,
|
|
height=HEIGHT,
|
|
)
|
|
for kind, make in _CHALLENGE_KINDS
|
|
]
|
|
results.append(
|
|
measure_fit("escape-hatch", _build_escape, width=WIDTH, height=HEIGHT)
|
|
)
|
|
return results
|
|
|
|
|
|
def main() -> int:
|
|
"""Measure every screen and fail if any of them overflows."""
|
|
logging.basicConfig(level=logging.INFO, format="%(message)s", stream=sys.stdout)
|
|
if os.environ.get(INNER_ENV) != "1":
|
|
return reexec_under_xvfb()
|
|
|
|
_logger.info("Measuring every alarm screen against a %dx%d panel:", WIDTH, HEIGHT)
|
|
if report_fit(measure_all()):
|
|
_logger.info("\nOK: every screen fits.")
|
|
return 0
|
|
_logger.error(
|
|
"\nAt least one screen is taller than the display. A place-centred "
|
|
"surface clips at BOTH edges when that happens, so the overflow is "
|
|
"not merely hidden -- it is unrecoverable."
|
|
)
|
|
return 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|