fix(display): Freigabe-Ziel lesbar, Tests schriftunabhaengig

Sechs Review-Befunde an e2d836a/2aab1b0.

C1: die Pixelschwellen in test_render.py waren gegen Pillow 12/Arial
kalibriert und auf dem Pi wie auf Debian 13 (DejaVu) rot - "> 150
schwarze Pixel in Zeile 11" sind dort nur 143. Die Zusagen ueber den
Kopfbalken sind jetzt Anteile und Symmetrien statt Pixelzahlen
(mindestens 55 % des Balkens schwarz, weisse Schrift links UND rechts
der Mitte, Balken ueber die volle Breite). Zwei weitere schriftabhaengige
Schwellen ebenso: die grosse Zeile hebt sich ueber das VERHAELTNIS der
Tintenhoehen vom Beiwerk ab, und die Randgrenze zaehlt Zeilenbaender
statt Tinten-Cluster.

I2: im Zustand "armed" stand "FREIGEGEBEN: <ziel>" in der Kopfzeile,
wo 112 px Platz sind und der Text 182 px misst - gezeichnet wurde
"FREIGEGEBE…", bei jedem Ziel gleich. Im Kopf steht jetzt nur noch das
Label, der Zielname als eigene Beiwerkzeile ueber die volle Breite; die
SSID-Zeile gibt dafuer in "armed" ihren Platz ab. Geprueft wird das
gezeichnete Bild (Pixelvergleich gegen eine Referenzzeile), nicht
lines().

I3: der Kommentar an display_handover() begruendete die
Rueckgabewert-Durchreichung damit, dass der Handgriff bei stummem Panel
rot werde. Das stimmt nicht - display.py beendet sich bei fehlendem
Panel/SPI/Pillow mit 0. Der Kommentar sagt jetzt, dass man die Revision
am Panel beurteilt und nicht am Exit-Code.

I4: zwischen "systemctl stop" und "systemctl start" gab es keinen
Signalhandler - ein Strg-C waehrend "display test" (der dokumentierte
Weg, die Revision zu bestimmen, bei falscher Revision haengt das Panel)
liess den Anzeigedienst gestoppt zurueck. trap auf INT/TERM/HUP, das
Wiederanfahren in display_service_resume(), Handler beenden selbst mit
128 + Signalnummer.

M2: HEADER_PHASE_MIN_WIDTH_PX bleibt als Sicherheitsnetz, ist jetzt aber
getestet statt toter Code.

M4: compose() baute je Aufruf fuenf Schriften neu. Prozess-Cache (mit
ImageFont-Modul und FONT_SEARCH_PATHS im Schluessel, damit die Tests
nicht verfaelscht werden) und faules Laden je Rolle: 25,0 ms -> 7,0 ms.

Die Pixeltests laufen zusaetzlich unter Pillow 11.1 mit echtem DejaVu
und unter fuenf weiteren Schriften. Sieben Mutationsproben angewandt,
angesehen und zurueckgenommen. tools/test_host.sh gruen.
This commit is contained in:
Michael Sauer
2026-09-14 15:12:45 +02:00
parent e77d3da292
commit f105a0eb69
4 changed files with 516 additions and 49 deletions
+80
View File
@@ -198,11 +198,35 @@ setup_sandbox() {
cp "$STATION_DIR/products.py" "$sandbox/lib/products.py"
cat >"$sandbox/lib/display/display.py" <<EOF
import os
import signal
import sys
with open("$sandbox/cmd.log", "a") as fh:
fh.write("STUB display.py %s (EPD_REVISION=%s)\n"
% (" ".join(sys.argv[1:]), os.environ.get("EPD_REVISION", "<unset>")))
# Testschalter fuer Fall D2c: stellt das Strg-C des Abnehmers nach, waehrend display.py
# laeuft (bei falscher Panel-Revision kehrt ReadBusy() nie zurueck - das ist der
# dokumentierte Weg, die Revision zu bestimmen, und damit der Regelfall fuer ein Strg-C).
#
# Nachgestellt wird, was das Terminal wirklich tut: das Signal geht an die GANZE
# Vordergrund-Prozessgruppe, also an die ninlight-station-Shell UND an diesen Prozess.
# Der Elternprozess ist dabei die Shell selbst ("sudo" und "env" sind Stub bzw.
# exec-Aufruf, der Python-Prozess haengt also direkt unter ihr).
#
# Dass dieser Prozess AM SIGNAL STIRBT (Standardbehandlung) statt sich mit 128+n zu
# beenden, ist kein Detail: eine Shell unterscheidet beides. Beendet sich das Kind
# normal, verwirft /bin/sh (bash im sh-Modus, macOS) das inzwischen zugestellte SIGINT
# und laeuft weiter - der Testfall waere dann auch ohne Signalhandler in
# display_handover() gruen und wuerde gar nichts pruefen (beim Schreiben dieses Tests
# genau so passiert).
signal_name = os.environ.get("FAKE_DISPLAY_SIGNAL", "")
if signal_name:
number = getattr(signal, "SIG" + signal_name)
os.kill(os.getppid(), number)
signal.signal(number, signal.SIG_DFL)
os.kill(os.getpid(), number)
sys.exit(int(os.environ.get("FAKE_DISPLAY_EXIT", "0")))
EOF
@@ -672,6 +696,62 @@ set -e
[ "$RC" -eq 1 ] && pass "Exit-Code 1 wird durchgereicht" || fail "Exit-Code $RC statt 1: $OUT"
grep -Fq "STUB systemctl start ninlight-display" "$SBX/cmd.log" && pass "Anzeigedienst laeuft trotz Panel-Fehler wieder" || fail "Anzeigedienst blieb nach dem Fehler aus: $(cat "$SBX/cmd.log")"
echo
echo "=== Fall D2c: Strg-C waehrend 'display test' laesst den Anzeigedienst NICHT gestoppt zurueck ==="
# Der Ablauf aus der Abnahme: bei falscher Revision kehrt ReadBusy() nie zurueck
# (display.py-Docstring), der Abnehmer bricht mit Strg-C ab. Ohne Signalhandler zwischen
# "stop" und "start" stirbt display_handover mit - der Anzeigedienst bleibt aus, das
# Panel schwarz, und das sieht mitten in der Lampenabnahme aus wie "Panel kaputt".
# Geprueft wird fuer alle drei Signale, die das ausloesen koennen.
for SIG_NAME in INT TERM HUP; do
case "$SIG_NAME" in
INT) EXPECT_RC=130 ;;
TERM) EXPECT_RC=143 ;;
HUP) EXPECT_RC=129 ;;
esac
SBX="$TMPROOT/tD2c$SIG_NAME"
setup_sandbox "$SBX"
: >"$SBX/cmd.log"
set +e
OUT=$(FAKE_DISPLAY_SIGNAL="$SIG_NAME" run_cli "$SBX" display test 2>&1)
RC=$?
set -e
grep -Fq "STUB systemctl stop ninlight-display" "$SBX/cmd.log" \
&& pass "SIG$SIG_NAME: der Anzeigedienst war fuer den Handgriff gestoppt (Vorbedingung)" \
|| fail "SIG$SIG_NAME: Vorbedingung fehlt, der Dienst wurde gar nicht erst gestoppt: $(cat "$SBX/cmd.log")"
# GENAU einmal gestartet. Die Anzahl, nicht nur das Vorhandensein: der Handler muss
# selbst beenden (in diesem Projekt schon einmal ein Befund, setup_pi.sh). Ohne "exit"
# laeuft die Shell hinter dem unterbrochenen Kommando einfach weiter und faehrt den
# Dienst ein zweites Mal an - der Exit-Code sieht dabei zufaellig richtig aus (er ist
# dann der des am Signal gestorbenen Kindes), die doppelte Startzeile nicht. Genau
# diese Mutation ("trap ohne exit") war gegen eine blosse Vorhandensein-Pruefung
# gruen.
START_COUNT=$(grep -c "STUB systemctl start ninlight-display" "$SBX/cmd.log" || true)
[ "$START_COUNT" -eq 1 ] \
&& pass "SIG$SIG_NAME: der Anzeigedienst wird trotz Abbruch genau einmal wieder gestartet" \
|| fail "SIG$SIG_NAME: $START_COUNT Startversuche (1 erwartet) - Anzeigedienst bleibt aus oder der Handler beendet nicht selbst: $(cat "$SBX/cmd.log")"
[ "$RC" -eq "$EXPECT_RC" ] \
&& pass "SIG$SIG_NAME: Exit-Code $EXPECT_RC (128 + Signalnummer)" \
|| fail "SIG$SIG_NAME: Exit-Code $RC statt $EXPECT_RC: $OUT"
done
echo
echo "=== Fall D2d: war der Anzeigedienst aus, startet ihn auch ein Strg-C nicht ==="
# Spiegelbild zu D2a: der Signalhandler darf den Zustand nicht anders behandeln als der
# normale Ausgang - sonst schaltete ein abgebrochener Handgriff die Anzeige ein, die
# 'off' zuvor bewusst ausgeschaltet hat.
SBX="$TMPROOT/tD2d"
setup_sandbox "$SBX"
: >"$SBX/cmd.log"
set +e
OUT=$(FAKE_DISPLAY_SERVICE_ACTIVE_EXIT=3 FAKE_DISPLAY_SIGNAL=INT run_cli "$SBX" display test 2>&1)
RC=$?
set -e
[ "$RC" -eq 130 ] && pass "Exit-Code 130 auch ohne laufenden Anzeigedienst" || fail "Exit-Code $RC statt 130: $OUT"
grep -Fq "STUB systemctl start ninlight-display" "$SBX/cmd.log" \
&& fail "der Signalhandler hat den Anzeigedienst eingeschaltet, obwohl er aus war: $(cat "$SBX/cmd.log")" \
|| pass "kein 'start' fuer einen Dienst, der gar nicht lief"
echo
echo "=== Fall D3 (Task 6b, Punkt 4): 'display off' stoppt den Dienst VOR dem Leeren des Panels ==="
# Umgekehrte Reihenfolge waere wirkungslos: der Daemon fragt alle 5 s nach und