Repository navigation
refactor(display): run() stage 1 - golden traces and extracted helpers, no behaviour change - #704
Conversation
Characterisation tests for DisplayController.run() before it is restructured. test/_run_loop_harness.py builds a real controller on fake plugins, cache, config, sync and plugin manager, swaps the module's time and datetime for one fake clock, and runs the real loop until a horizon. The event log is folded into screens (mode, start, duration, exit reason, frames, force_clear) and compared with test/fixtures/run_loop_golden/. Fourteen scenarios: plain rotation, empty and missing modes, an all-empty rotation, plugin errors and the circuit breaker, dynamic duration, live priority and live round-robin, on-demand start/stop/expiry, pinned on-demand, the on/off and dim schedules, a WiFi notice, sync follower, and Vegas (a contract fake of the coordinator) with and without live_in_ticker. All 15 tests run in under a second. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…change Stage 1 of restructuring DisplayController.run() (docs/RUN_LOOP_REDESIGN.md). Each block moves verbatim into a method named for what the later Arbiter / ScreenRunner needs from it: - _blank_while_scheduled_off: the schedule-off pass - _run_follower_frame: the sync-follower frame (Follower source) - _show_wifi_notice: the WiFi status message pass (Wifi source) - _resolve_active_mode: on-demand mode vs rotation mode (OnDemand source) - _plugin_for_mode: mode -> plugin, with the circuit-breaker skip - _dispatch_first_frame: the executor-bound first frame and lock release - _skip_failed_plugin_modes: skipping a failed plugin's other modes - _track_dynamic_cycle / _resolve_durations / _clamp_to_on_demand: the screen's min/max duration (the plan's bounds) - _needs_high_fps: the frame-rate decision (future frame_policy) - _advance_after_screen: next mode after a screen (rotation / live hold) The frame loops, the Vegas branch and every early exit stay in run(). The two `not wifi_status_data` tests are dropped: past the WiFi block it was always None (a shown message ends the pass, a failed one is reset). The golden traces in test/fixtures/run_loop_golden/ are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…-off stretch Two gaps a mutation check of the extracted helpers found: nothing exercised _resolve_active_mode moving the panel onto an on-demand mode (it only happens when a session is resumed after a restart), and the schedule scenario's off stretch was always cut short before the 60 s dwell ended. Both goldens were generated from main's run(), before the extraction, and pass unchanged after it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s stages docs/RUN_LOOP_REDESIGN.md: what run() does today, the target design, stages 1-5 with how each is verified (golden traces, mutation check, suite diff, ledpi soak), and seven current behaviours the traces pin down that look wrong and should each get their own PR. CHANGELOG Tooling entry; a pointer from ARCHITECTURE.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… new _resolve_active_mode can return None (no current mode), and annotating that made every screen-path helper call an Optional-to-str error that run() never had. It is left unannotated, as the inline code was, and _advance_after_screen takes Optional[str]. mypy on display_controller.py (not yet on the ratchet): 42 errors, main has 43. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Navigate logical layers of code changes, visualize relationships, and explore their blast radius. No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (21)
Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review. 📝 WalkthroughWalkthrough
ChangesDisplay run-loop restructuring
Priority: ⬇️ Low Estimated code review effort: 4 (Complex) | ~45 minutes Change: Refactor Merge Risk: ⚪ Minimal · up to No actionable defect is established in this behavior-preserving extraction. Complete the planned hardware soak before merging, as requested by the author. 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
Full details: Docstring CoverageExplanation Docstring coverage is 18.18% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 121 functions across 3 files. (18 skipped: 18 unsupported.)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Up to standards ✅🟢 Issues
|
Hardware check on ledpi (Pi 4, 192x48, 2026-10-01): partial, the Vegas half is still to doBranch Normal rotation (football and baseball scoreboards, all static
On-demand, on the branch:
Not tested yet:
🤖 Generated with Claude Code |
# Conflicts: # src/display_controller.py
|
Hardware check on hdpi: PASS (no regression) Rig: Pi 4, 512x64 (4x 128x64, chain 4), pwm_bits 8, rebuilt binding that releases the GIL, ~90 Hz. Vegas was on (it already was in this rig's config). Each phase ran
🤖 Generated with Claude Code |
Stage 1 of restructuring
DisplayController.run()into an Arbiter / ScreenRunner / Sources (roadmap theme T1, item 8). No behaviour change. The plan for the rest is indocs/RUN_LOOP_REDESIGN.md.What's in it
1. Golden trace tests
test/_run_loop_harness.pybuilds a realDisplayControllerthrough__init__and runs the realrun()on fakes:timeanddatetime. Sleeping only advances it, and scripted events (requests, WiFi files, live windows) fire as it passes them.The first frame of each screen still goes through the real
PluginExecutorand the per-plugin locks. The run stops at a horizon via aBaseException, sorun()'sexcept Exceptioncan't swallow it, and the harness fails ifrun()returns early or spins.Each trace row is
[start, mode, duration, exit_reason, frames, force_clear], and notable events (requests, live switches, schedule, brightness) are listed with their time. The traces are intest/fixtures/run_loop_golden/. There are 15 scenarios:plain_rotationdisplay_durationsoverride, a plugin's own duration, a high-FPS scroller (1252 frames / 10 s), a legacydisplay()withoutdisplay_modeempty_modesall_emptyplugin_errordisplay()raising inside the executor counts as an empty passdynamic_durationget_cycle_duration, global cap, cycle resetslive_prioritylive_round_robinon_demandon_demand_pinnedon_demand_restoredschedulewifi_noticefollowervegasvegas_live_in_tickerVegas uses
FakeVegas, a contract fake. It returns True after its cycle and False when the interrupt or live check asks it to yield, checking at the real coordinator's cadence. Driving the real coordinator on the fake clock is stage 4 work, as the doc explains.All goldens were generated from main's
run()before any code moved. They pass unchanged on this branch.2. Helpers extracted from
run(), verbatim_blank_while_scheduled_off_run_follower_frame_show_wifi_notice_resolve_active_mode_plugin_for_mode_dispatch_first_frame_skip_failed_plugin_modes_track_dynamic_cycle_resolve_durations_clamp_to_on_demand_needs_high_fpsframe_policy(stage 5)_advance_after_screenThe two frame loops, the Vegas branch and every early exit stay in
run(). That keeps frame timing untouched and leaves the rest of the file alone. The only non-mechanical edit drops twoand not wifi_status_dataconditions. Past the WiFi block that variable was alwaysNone: a shown message ends the pass, and a failed one is reset.3.
docs/RUN_LOOP_REDESIGN.mdThe doc covers the target design and stages 2-5, each with how it is verified. It also lists seven current behaviours the traces pin down that look wrong. They are left as they are here, and each should get its own PR with a golden diff:
display()raises inside the executor is recorded as a breaker success.CHANGELOG has a Tooling entry, and
docs/ARCHITECTURE.mdhas a one-line pointer to the doc.Stage-2 plan
ScreenPlanandArbiter.decide(state, inputs, now). It is pure, with the ScheduledOff gate and the Follower and Wifi Sources only. Everything else returns aLEGACYplan.run()gathers inputs after its bookkeeping, callsdecide(), and dispatches onplan.source. ALEGACYplan takes the existing path.decide(). The golden traces must not change, including the missed WiFi notice, which gets its own fix PR.Stage 3 adds the ScreenRunner, FrameClock and the
PREEMPTEDexit reason, which replaces the five remainingcurrent_display_mode != active_modere-checks. Stage 4 makes Vegas a Source driven byrun_frame(). Stage 5 addsframe_policy.Verification
Golden tests: 16 passed in each of 5 consecutive runs, 0.8-1.3 s per run.
Goldens against main's code: with main's
src/display_controller.pyswapped into this branch, all 16 pass.Mutation check: I broke each extracted helper once, and every mutation failed at least one trace:
_resolve_durationsuses max of the caps_advance_after_screendrops the live hold_dispatch_first_framenever clears force_change_needs_high_fpsignores enable_scrolling_resolve_active_modeskips the forced clear_clamp_to_on_demanddoesn't clamp_skip_failed_plugin_modesnever skips_show_wifi_noticedwells 1 s_run_follower_frameat 30 fps_track_dynamic_cyclenever resets_plugin_for_modeignores the breaker_blank_while_scheduled_offdwells 30 sFull suite (
python -m pytest -m "not hardware" test/ --ignore=test/plugins) on this Windows host, compared with a baseline run of main (f4bda507) in a separate worktree. Both runs had the same 65 FAILED/ERROR ids (59 failed + 6 errors, the known Windows set), so nothing is new. Passes went from 6934 to 6950, which is the 16 new tests.scripts/check_types.py: clean (82 modules).display_controller.pyis not on the ratchet. mypy on it reports 42 errors here and 43 on main, with none new.ledpi soak needed before merge
The frame loops did not move, but every screen now goes through extra method calls, so check on ledpi:
python3 scripts/frame_soak.py --previewfor 10 minutes on a scrolling rotation (no live game holding a static screen), main vs this branch. Alternate which build goes first. Compare the late-frame % and freezes.I didn't touch either Pi.
Coordination
claude/ipc-control-socket) adds an on-demand hook. This PR only touchesrun()and the new helpers placed just above it._poll_on_demand_requestsand the rest of the on-demand code are unchanged.Do not merge until the soak is done.
🤖 Generated with Claude Code
Summary by CodeRabbit