#!/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)