diff --git a/tools/station/tests/test_render.py b/tools/station/tests/test_render.py index 3146573..2b43f0e 100644 --- a/tools/station/tests/test_render.py +++ b/tools/station/tests/test_render.py @@ -1988,3 +1988,79 @@ def test_display_main_passes_the_final_kind_through(monkeypatch): assert len(drawn) == 1 expected = render.compose({"mode": "final", "final": kind}) assert list(drawn[0].getdata()) == list(expected.getdata()), kind + + +# --- Die Entbehrlichkeitszusage der Anzeige (Abschlussreview, I1) -------------------- +# +# Zusage aus dem Modul-Docstring, ueber fuenf Fixrunden erkaempft: fehlt Panel, SPI, +# Pillow oder Schrift, gibt es GENAU EINE Protokollzeile und sys.exit(0). Der Grund ist +# Restart=on-failure mit RestartSec=5 in ninlight-display.service - ein Exit != 0 macht +# daraus auf dem Zero W eine Neustartschleife, die das Journal flutet, und der Sinn der +# Zusage ("ein Fehler in der Anzeige darf niemals eine Migration stoeren") faellt still +# weg. +# +# Das Abschlussreview hat belegt, dass diese Zusage ungetestet war: sys.exit(0) -> +# sys.exit(1) liess alle 225 Tests gruen. Die vorhandenen Tests pruefen nur, dass die +# richtigen Ausnahmetypen bis main() DURCHKOMMEN - niemand hat main() selbst in den +# except-Zweig getrieben und auf den Rueckgabewert gesehen. Beim Tasterdienst ist +# dasselbe laengst getestet (tests/test_button.py, test_missing_gpiozero_...). +# +# Alle vier Modi, weil jeder sein eigenes run_*() hat: --test, --clear, --final und der +# Daemon. Alle vier bauen ihr Panel selbst, ein Panel(), das schon im Konstruktor wirft, +# deckt daher jeden der vier Einstiege ab. + +_DISPLAY_MODES = [ + pytest.param(["display.py", "--test"], id="test"), + pytest.param(["display.py", "--clear"], id="clear"), + pytest.param(["display.py", "--final", "poweroff"], id="final"), + pytest.param(["display.py"], id="daemon"), +] + + +def _panel_raising(exc): + class _Panel: + def __init__(self, revision): + raise exc + return _Panel + + +@pytest.mark.parametrize("argv", _DISPLAY_MODES) +def test_missing_panel_logs_exactly_one_line_and_exits_zero(argv, monkeypatch, caplog): + """Kein Panel/SPI -> DisplayUnavailable -> eine Zeile, Exit 0 - in JEDEM Modus.""" + monkeypatch.setattr(display, "Panel", + _panel_raising(epdconfig.DisplayUnavailable("kein SPI-Geraet"))) + monkeypatch.setattr(sys, "argv", argv) + monkeypatch.setenv("EPD_REVISION", "V3") + + with caplog.at_level("INFO", logger="ninlight-display"): + with pytest.raises(SystemExit) as excinfo: + display.main() + + assert excinfo.value.code == 0, ( + f"{argv[1:] or ['daemon']}: Exit {excinfo.value.code} statt 0 - " + "Restart=on-failure macht daraus eine Neustartschleife") + assert len(caplog.records) == 1, \ + f"erwartet genau eine Protokollzeile, bekommen: {[r.getMessage() for r in caplog.records]}" + assert "kein SPI-Geraet" in caplog.records[0].getMessage() + + +@pytest.mark.parametrize("argv", _DISPLAY_MODES) +def test_missing_pillow_logs_exactly_one_line_and_exits_zero(argv, monkeypatch, caplog): + """Der zweite Ast derselben Zusage (Task 6a, C4): Pillow kommt auf dem Ziel-Pi erst + per "apt install python3-pil" nach, render.compose() importiert es erst beim + Zeichnen. Ein ModuleNotFoundError (ImportError-Unterklasse) muss deshalb genauso + enden wie ein fehlendes Panel - und nicht als Absturz.""" + monkeypatch.setattr(display, "Panel", + _panel_raising(ModuleNotFoundError("No module named 'PIL'"))) + monkeypatch.setattr(sys, "argv", argv) + monkeypatch.setenv("EPD_REVISION", "V3") + + with caplog.at_level("INFO", logger="ninlight-display"): + with pytest.raises(SystemExit) as excinfo: + display.main() + + assert excinfo.value.code == 0, ( + f"{argv[1:] or ['daemon']}: Exit {excinfo.value.code} statt 0") + assert len(caplog.records) == 1, \ + f"erwartet genau eine Protokollzeile, bekommen: {[r.getMessage() for r in caplog.records]}" + assert "PIL" in caplog.records[0].getMessage()