Files
grossanzeige/test_main.py
T
admin 142a621066 Dauerbetrieb: Autostart (main.py) und Watchdog
MicroPython fuehrt main.py nach jedem Reset selbst aus -- nach Stromausfall oder
Watchdog-Neustart laeuft die Anzeige damit ohne Notebook wieder an. Bisher gab es
die Datei bewusst nicht, weil am Aufbau gearbeitet wurde.

**Das Abbruchfenster ist kein Komfort, sondern die Notbremse.** Ohne es waere ein
Geraet, dessen Programm beim Start haengt, nur noch durch Neuflashen erreichbar:
Die Endlosschleife belegt die REPL sofort nach dem Reset. main.py wartet deshalb
erst AUTOSTART_DELAY (3 s), in denen Ctrl-C zurueck an die REPL fuehrt -- dann
laeuft auch kein Watchdog.

Watchdog (WDT_TIMEOUT = 120 s): startet das Geraet neu, wenn die Poll-Schleife
stehenbleibt. Gefuettert wird zu Beginn *jedes* Durchlaufs, auch nach einem
Fehler -- "KEIN LINK" ist kein Stillstand, die Schleife laeuft ja weiter. Der
Wert muss ueber allem liegen, was regulaer blockieren darf (WLAN 20 s,
Montierung 3 s, dazu ein MQTT-Versuch); knapper waere er eine Neustart-Schleife.

Ein gestarteter Watchdog laesst sich nicht mehr abschalten. Deshalb legt ihn nur
main.py an -- im Handbetrieb ueber die REPL startete das Geraet sonst neu,
waehrend man nachdenkt.

Weiteres:
- Nach einem Watchdog-Neustart meldet main.py das beim Start
  (machine.reset_cause()), sonst saehe er aus wie ein Stromausfall.
- Eine Ausnahme faengt main.py ab und startet nach RESTART_DELAY (10 s) neu,
  statt in die REPL zu fallen und die Anzeige einzufrieren. Die Pause verhindert
  eine Neustart-Schleife im Sekundentakt.
- Autostart faehrt mit Helligkeitsregelung (with_ldr=True, Nutzerentscheidung).

Am Geraet bestaetigt: Autostart nach Reset laeuft durch (Watchdog aktiv, WLAN,
MQTT), ein absichtlich herbeigefuehrter Stillstand loest den Watchdog aus und das
Geraet kommt von allein wieder hoch -- mit der Meldung "Vorheriger Lauf wurde vom
Watchdog beendet". Ctrl-C im Startfenster gibt die REPL frei.

Beim Geraetetest aufgefallen: make_watchdog wurde ohne log aufgerufen, im
Startprotokoll stand also nichts vom Watchdog. Behoben und durch einen Test
festgehalten. 178 Tests gruen (neu: test_main.py).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 18:41:33 +02:00

181 lines
5.8 KiB
Python

#!/usr/bin/env python3
"""Tests fuer den Autostart (main.py).
python3 -m unittest -v test_main
main.py startet beim Import die Schleife -- deshalb wird es hier nicht importiert,
sondern die Quelle geladen und in einer eigenen Umgebung ausgefuehrt, in der
time.sleep, run_esp32 und machine durch Attrappen ersetzt sind.
Der wichtigste Test ist das Abbruchfenster: Ohne es waere ein Geraet mit
fehlerhaftem main.py nur noch durch Neuflashen erreichbar.
"""
import unittest
import config
class FakeRunEsp32:
def __init__(self, wirft=None):
self.aufrufe = []
self.watchdogs = 0
self.log_beim_watchdog = None
self._wirft = wirft
def make_watchdog(self, timeout=None, log=None):
self.watchdogs += 1
self.log_beim_watchdog = log
return lambda: None
def main(self, **kwargs):
self.aufrufe.append(kwargs)
if self._wirft is not None:
raise self._wirft
class FakeMachine:
WDT_RESET = 3
PWRON_RESET = 1
def __init__(self, ursache=1):
self.resets = 0
self._ursache = ursache
def reset(self):
self.resets += 1
def reset_cause(self):
return self._ursache
def lade_main(run_esp32, machine=None, sleep_wirft=None):
"""main.py in einer kontrollierten Umgebung ausfuehren.
sleep_wirft: Ausnahme, die time.sleep beim n-ten Aufruf wirft -- damit
laesst sich Ctrl-C im Abbruchfenster nachstellen.
"""
import sys
import types
schlaefe = []
class FakeTime:
@staticmethod
def sleep(s):
schlaefe.append(s)
if sleep_wirft is not None and len(schlaefe) == 1:
raise sleep_wirft
ausgaben = []
umgebung = {
"__name__": "main",
"print": lambda *a: ausgaben.append(" ".join(str(x) for x in a)),
}
gemerkt = {name: sys.modules.get(name)
for name in ("time", "run_esp32", "machine")}
try:
sys.modules["time"] = FakeTime
sys.modules["run_esp32"] = run_esp32
if machine is not None:
sys.modules["machine"] = machine
else:
sys.modules.pop("machine", None)
with open("main.py", encoding="utf-8") as f:
quelle = f.read()
exec(compile(quelle, "main.py", "exec"), umgebung)
finally:
for name, modul in gemerkt.items():
if modul is None:
sys.modules.pop(name, None)
else:
sys.modules[name] = modul
return ausgaben, schlaefe
class TestAbbruchfenster(unittest.TestCase):
"""Die Notbremse: Ctrl-C im Startfenster fuehrt zurueck an die REPL."""
def test_ohne_abbruch_startet_die_schleife(self):
r = FakeRunEsp32()
ausgaben, schlaefe = lade_main(r)
self.assertEqual(len(r.aufrufe), 1)
self.assertEqual(schlaefe[0], config.AUTOSTART_DELAY)
def test_ctrl_c_im_startfenster_bricht_ab(self):
"""Ohne das waere ein Geraet mit kaputtem main.py nur noch flashbar."""
r = FakeRunEsp32()
ausgaben, _ = lade_main(r, sleep_wirft=KeyboardInterrupt())
self.assertEqual(r.aufrufe, [])
self.assertEqual(r.watchdogs, 0, "Watchdog darf dann nicht laufen")
self.assertTrue(any("abgebrochen" in a for a in ausgaben))
def test_wartezeit_ist_lang_genug_zum_eingreifen(self):
self.assertGreaterEqual(config.AUTOSTART_DELAY, 2)
class TestStart(unittest.TestCase):
def test_watchdog_wird_angelegt_und_durchgereicht(self):
r = FakeRunEsp32()
lade_main(r)
self.assertEqual(r.watchdogs, 1)
self.assertIn("feed", r.aufrufe[0])
self.assertIsNotNone(r.aufrufe[0]["feed"])
def test_watchdog_meldet_sich_im_startprotokoll(self):
"""Sonst bliebe unsichtbar, ob er ueberhaupt laeuft (am Geraet aufgefallen)."""
r = FakeRunEsp32()
lade_main(r)
self.assertIsNotNone(r.log_beim_watchdog,
"make_watchdog wurde ohne log aufgerufen")
def test_helligkeitsregelung_ist_an(self):
"""Nutzerentscheidung 2026-07-27: Autostart faehrt mit LDR."""
r = FakeRunEsp32()
lade_main(r)
self.assertTrue(r.aufrufe[0]["with_ldr"])
def test_watchdog_neustart_wird_gemeldet(self):
"""Sonst sieht ein Watchdog-Neustart aus wie ein Stromausfall."""
r = FakeRunEsp32()
m = FakeMachine(ursache=FakeMachine.WDT_RESET)
ausgaben, _ = lade_main(r, machine=m)
self.assertTrue(any("Watchdog" in a for a in ausgaben))
def test_gewoehnlicher_start_meldet_keinen_watchdog(self):
r = FakeRunEsp32()
m = FakeMachine(ursache=FakeMachine.PWRON_RESET)
ausgaben, _ = lade_main(r, machine=m)
self.assertFalse(any("Watchdog beendet" in a for a in ausgaben))
class TestAbsturz(unittest.TestCase):
"""Eine Ausnahme darf nicht in der REPL enden -- die Anzeige stuende still."""
def test_absturz_loest_neustart_aus(self):
r = FakeRunEsp32(wirft=RuntimeError("kaputt"))
m = FakeMachine()
ausgaben, schlaefe = lade_main(r, machine=m)
self.assertEqual(m.resets, 1)
self.assertIn(config.RESTART_DELAY, schlaefe)
self.assertTrue(any("Absturz" in a for a in ausgaben))
def test_neustartpause_verhindert_dauerschleife(self):
"""Ist etwas dauerhaft kaputt, soll nicht im Sekundentakt neu gestartet
werden -- die Pause gibt auch Zeit, mit Ctrl-C einzugreifen."""
self.assertGreaterEqual(config.RESTART_DELAY, 5)
def test_ctrl_c_im_betrieb_startet_nicht_neu(self):
"""Von Hand beendet heisst: die REPL soll frei werden."""
r = FakeRunEsp32(wirft=KeyboardInterrupt())
m = FakeMachine()
ausgaben, _ = lade_main(r, machine=m)
self.assertEqual(m.resets, 0)
self.assertTrue(any("beendet" in a for a in ausgaben))
if __name__ == "__main__":
unittest.main(verbosity=2)