test(station): run_daemon selbst treiben statt eine Kopie der Schleife

Die Verdrahtung zwischen RefreshPlanner und Panel - Aktion auf Panel-Methode,
was_full=upgraded, record_sleep(), Reihenfolge decide/ausfuehren/zurueckmelden -
ueberlebte jede Mutation, weil der Test eine nachgebaute Schleife trieb. Am
schlimmsten: der "full"-Zweig auf draw_partial() umgebogen haette bedeutet, dass
es auf einem wachen Panel nie wieder einen echten Vollaufbau gibt.

run_daemon() nimmt jetzt Uhr, Panel und Zustandsquelle entgegen (Vorgaben
unveraendert) und wird im Test mit Fake-Uhr und protokollierendem Panel-Doppel
direkt getrieben; die Nachbildung _run_schedule() entfaellt ersatzlos.
Dazu: RefreshPlanner liest die Intervallkonstanten wieder pro Entscheidung statt
sie als Default-Argument zur Importzeit zu binden, record_sleep() verliert den
toten now-Parameter.
This commit is contained in:
Michael Sauer
2026-09-13 22:20:11 +02:00
parent b8c7febb8f
commit 22f92988dd
2 changed files with 214 additions and 93 deletions
+42 -13
View File
@@ -328,10 +328,15 @@ class RefreshPlanner:
_NO_MODE = object() # garantiert ungleich jedem echten Modus beim ersten Lauf
def __init__(self, now: float,
full_refresh_interval_s: float = FULL_REFRESH_INTERVAL_S,
sleep_after_idle_s: float = SLEEP_AFTER_IDLE_S) -> None:
self.full_refresh_interval_s = full_refresh_interval_s
self.sleep_after_idle_s = sleep_after_idle_s
full_refresh_interval_s: float | None = None,
sleep_after_idle_s: float | None = None) -> None:
# Bewusst None statt der Modulkonstanten als Vorgabewert (Fixrunde 5): ein
# Default-Argument wird zur Importzeit ausgewertet, ein spaeteres
# monkeypatch.setattr(display, "FULL_REFRESH_INTERVAL_S", ...) haette dann keine
# Wirkung mehr auf neu gebaute Planer - die alte Schleife las die Konstanten pro
# Runde. Die Properties unten lesen sie weiterhin bei jeder Entscheidung.
self._full_refresh_interval_s = full_refresh_interval_s
self._sleep_after_idle_s = sleep_after_idle_s
self.last_lines: list[str] | None = None
self.last_mode: object = self._NO_MODE
self.last_full = now
@@ -363,6 +368,16 @@ class RefreshPlanner:
self.partials_since_full = 0
self.asleep = False
@property
def full_refresh_interval_s(self) -> float:
return (FULL_REFRESH_INTERVAL_S if self._full_refresh_interval_s is None
else self._full_refresh_interval_s)
@property
def sleep_after_idle_s(self) -> float:
return (SLEEP_AFTER_IDLE_S if self._sleep_after_idle_s is None
else self._sleep_after_idle_s)
def decide(self, now: float, lines: list[str], mode: object) -> str:
"""Was jetzt zu tun ist. Aendert nichts - das Ergebnis wird per record_*()
zurueckgemeldet."""
@@ -395,7 +410,10 @@ class RefreshPlanner:
else:
self.partials_since_full += 1
def record_sleep(self, now: float) -> None:
def record_sleep(self) -> None:
"""Kein `now`-Parameter (Fixrunde 5): das Einschlafen verschiebt weder die
30-Minuten-Uhr noch last_draw - es merkt sich nur, dass ein weiteres
panel.sleep() nichts mehr zu tun haette."""
self.asleep = True
@@ -434,20 +452,31 @@ def run_clear(revision: str) -> None:
log.info("Panel geleert (EPD_REVISION=%s)", revision)
def run_daemon(revision: str) -> None:
def run_daemon(revision: str, clock=time, panel: "Panel | None" = None,
read_state=build_state) -> None:
"""Endlosschleife: alle POLL_INTERVAL_S Sekunden den Zustand einlesen und die von
RefreshPlanner entschiedene Aktion ausfuehren. Hier steht bewusst KEINE Zeitlogik mehr
(Fixrunde 4) - die steckt vollstaendig in RefreshPlanner und ist dort ohne Schleife,
ohne time.sleep() und ohne Hardware pruefbar."""
panel = Panel(revision)
ohne time.sleep() und ohne Hardware pruefbar.
`clock` (braucht monotonic() und sleep()), `panel` und `read_state` sind einschleusbar,
damit die VERDRAHTUNG hier - welche Aktion welche Panel-Methode ruft, dass die
Selbst-Aufwertung von draw_partial() zurueckgemeldet wird, dass das Einschlafen
zurueckgemeldet wird, und die Reihenfolge decide -> ausfuehren -> zurueckmelden -
selbst pruefbar ist und nicht nur eine Kopie davon (Fixrunde 5, N2: alle drei
Mutationen an genau diesen Zeilen blieben vorher gruen, weil der Test eine nachgebaute
Schleife statt dieser hier trieb). Im Betrieb bleiben die Vorgaben: echte Uhr, echtes
Panel, echtes build_state()."""
if panel is None:
panel = Panel(revision)
panel.clear()
planner = RefreshPlanner(time.monotonic())
planner = RefreshPlanner(clock.monotonic())
while True:
state = build_state()
state = read_state()
current_lines = render.lines(state)
mode = state.get("mode")
now = time.monotonic()
now = clock.monotonic()
action = planner.decide(now, current_lines, mode)
if action == "full":
@@ -460,9 +489,9 @@ def run_daemon(revision: str) -> None:
planner.record_draw(now, current_lines, mode, was_full=upgraded)
elif action == "sleep":
panel.sleep()
planner.record_sleep(now)
planner.record_sleep()
time.sleep(POLL_INTERVAL_S)
clock.sleep(POLL_INTERVAL_S)
def main() -> None: