Skip to content

fix(plugin-harness): add no-op process_deferred_updates to test double - #391

Merged
ChuckBuilds merged 1 commit into
mainfrom
fix/harness-process-deferred-updates
Jul 11, 2026
Merged

fix(plugin-harness): add no-op process_deferred_updates to test double#391
ChuckBuilds merged 1 commit into
mainfrom
fix/harness-process-deferred-updates

Conversation

@ChuckBuilds

@ChuckBuildsChuckBuilds commented Jul 10, 2026

Copy link
Copy Markdown
Owner

Summary

The plugin-safety harness's VisualTestDisplayManager (base class of
BoundsCheckingDisplayManager) doesn't implement process_deferred_updates().
Five first-party plugins in ledmatrix-plugins call it unconditionally,
right alongside set_scrolling_state(), before updating scroll position:
news, odds-ticker, ledmatrix-leaderboard, stock-news, and
ledmatrix-stocks. Any of them fails the harness with AttributeError
the instant it's touched, even though the call is completely safe against
the real production display_manager.py (which has always had this
method).

This surfaced concretely in ledmatrix-plugins#177,
which had to add a local hasattr(self.display_manager, "process_deferred_updates")
guard in ledmatrix-stocks/manager.py just to get CI green — a workaround
for this exact gap, not a real plugin bug. The other 4 plugins are still
unguarded and will hit the same failure the next time any of them is
touched.

Fix

Add process_deferred_updates() to VisualTestDisplayManager as a no-op,
mirroring the existing "no-op for testing" pattern already used a few lines
above for set_scrolling_state(). This double has no deferred-update queue
to process, so there's nothing to do — the point is just to stop raising
AttributeError for callers that assume the method exists (as production
code always can).

Test plan

  • Added test_process_deferred_updates_is_noop to test/plugins/test_visual_rendering.py,
    next to the existing test_scrolling_state test, asserting the call
    doesn't raise and doesn't disturb scrolling state.
  • python3 -m py_compile on both touched files.
  • Read through test/plugins/test_harness.py, test_visual_rendering.py,
    and test_plugin_matrix.py to confirm none of them do exhaustive
    method-set introspection that this addition would break.
  • Existing CI (test.ymlpytest test/plugins/test_harness.py test/plugins/test_visual_rendering.py test/plugins/test_plugin_matrix.py)
    will exercise this on the PR.

Follow-up (not in this PR)

Once this merges, the local hasattr guard added in
ledmatrix-plugins#177 becomes unnecessary (though harmless to leave). The
other 4 affected plugins (news, odds-ticker, leaderboard, stock-news) need
no changes at all — they'll simply stop failing the harness next time
they're touched.

Summary by CodeRabbit

  • Bug Fixes

    • Improved compatibility with ticker-style visual plugins by safely handling deferred update calls during scrolling.
    • Prevented test rendering failures when deferred updates are requested.
  • Tests

    • Added coverage confirming deferred update handling is harmless and preserves scrolling state.

The safety harness's VisualTestDisplayManager (base of
BoundsCheckingDisplayManager) doesn't implement process_deferred_updates(),
which 5 first-party ledmatrix-plugins call unconditionally between
set_scrolling_state() and their scroll-position update: news, odds-ticker,
ledmatrix-leaderboard, stock-news, and ledmatrix-stocks. Any of them fails
the harness with AttributeError the moment it's touched (surfaced when
ledmatrix-plugins#177 had to add a local hasattr guard in ledmatrix-stocks
just to pass CI). Add the method as a no-op, mirroring the existing
"no-op for testing" pattern already used for set_scrolling_state, so these
plugins render under the harness without every touching PR needing its own
guard.
@coderabbitai

coderabbitaiBot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4c5dfa31-4e8c-4d22-b10c-6da25fdb2974

📥 Commits

Reviewing files that changed from the base of the PR and between 978a03b and 0c7cf09.

📒 Files selected for processing (2)
  • src/plugin_system/testing/visual_display_manager.py
  • test/plugins/test_visual_rendering.py

📝 Walkthrough

Walkthrough

VisualTestDisplayManager now implements a no-op process_deferred_updates() hook, with a test confirming the call does not alter scrolling state.

Changes

Visual display deferred updates

Layer / File(s)Summary
Add and validate deferred-update hook
src/plugin_system/testing/visual_display_manager.py, test/plugins/test_visual_rendering.py
VisualTestDisplayManager provides a no-op process_deferred_updates() method, and its test confirms the method is harmless while scrolling remains enabled.

Estimated code review effort: 1 (Trivial) | ~5 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly states the main change: adding a no-op process_deferred_updates method to the test double.
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/harness-process-deferred-updates

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codacy-production

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics0 complexity · 0 duplication

MetricResults
Complexity0
Duplication0

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

@ChuckBuilds
ChuckBuilds merged commit 2ffc57c into mainJul 11, 2026
7 checks passed
@ChuckBuilds
ChuckBuilds deleted the fix/harness-process-deferred-updates branch July 11, 2026 12:55
ChuckBuilds added a commit that referenced this pull request Sep 8, 2026
… size (#539)
* fix(display): pin one text layout engine, and give the 5x7 face a size
Two ways a font could render differently on two machines running the same
code, both found while diagnosing four plugins whose golden images passed on
the machine that generated them and failed everywhere else.
**Layout engine.** `ImageFont.truetype` picks its engine at load time: Raqm
where the host Pillow was built with libraqm, Basic otherwise. The two round
fractional glyph advances differently. `PressStart2P-Regular.ttf` at 8px has
whole-pixel advances, so they agree — which is why most of the fleet matched
everywhere and hid this. `4x6-font.ttf` at 6px does not: glyph positions drift
cumulatively along a run, and the four plugins that draw body text in it
(geochron, of-the-day, christmas-countdown, ledmatrix-weather's almanac) are
exactly the four whose goldens travelled badly.
Every core font load now goes through `src/common/font_layout.load_truetype`,
which pins the Basic engine, so a render depends on the font file and the size
and nothing else. Basic gives up complex-script shaping and kerning pairs;
neither applies to bitmap-grid faces on an LED panel. Output is unchanged on a
host without libraqm.
**Zero font height.** `DisplayManager` built the 5x7 BDF face with
`freetype.Face(path)` and never called `set_char_size`, so `face.size.height`
stayed 0 and `get_font_height()` returned 0 for it — callers stacking rows by
`prev_y + prev_height + gap` drew two lines on top of each other. The
start-up line `Calendar font size: 0 pixels` has been printing the symptom all
along. `font_manager._load_bdf_font` already called `set_char_size`, so
whether measurement worked depended on which path loaded the face.
`DisplayManager` now sets it too, and `get_font_height()` falls back to the
strike the file declares rather than returning a zero line height.
FixesChuckBuilds/ledmatrix-plugins#397
Refs ChuckBuilds/ledmatrix-plugins#371, #375, #378, #391
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(display): give the startup banner a rung that fits a full address at 64px
CI caught what pinning the layout engine exposed rather than caused.
`_fitting_font` walks PressStart2P then 4x6 at 6px, and "255.255.255.255" --
the widest thing the startup banner ever shows -- measures 66px at 4x6/6px
against the 62 a 64x32 panel has to give. It used to squeak in only because
the measurement depended on which layout engine the host Pillow happened to
have; with the engine pinned it does not, so the rung the worst case actually
needs is now in the ladder instead of implied: 4x6 at 5px, which measures 51.
The fallback was wrong in the same place. When nothing in the ladder fit, it
returned `self.font` -- the *widest* option, and precisely how "Initializing"
came to run off the side of a 64px panel to begin with. It returns the
narrowest face that loaded now.
test/test_initializing_screen.py: 34 passed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(display): name the exceptions the BDF strike read can raise
Codacy flagged the try/except/pass. It was already narrow in intent -- a
malformed strike table on the measurement path must degrade to "size unknown"
rather than take the display down -- but a bare `except Exception: pass` says
neither of those things and hides a genuinely broken font behind a silent 8px
fallback. It now catches what reading `available_sizes` can actually raise and
logs which face failed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* chore: drop logo PNGs the render harness downloaded into the worktree
These are fetched at runtime by the logo cache; they are not source, and they
rode in on a `git add -A` while I was running check_plugin.py against this
branch. Nothing in the change needs them.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@ChuckBuilds