Files
LEDMatrix/docs/plugin-safety-harness.md
T
ChuckandClaude Opus 5 df3524305f docs(plugins): one display-size contract: display_manager.width/height
CLAUDE.md (#580) says to read display_manager.width/height because
matrix is None when hardware init fails; the development guide, the
safety-harness doc and two DisplayManager docstrings still recommended
matrix.width/height. The bundled starlark-apps plugin read matrix.width
unguarded, so its magnify recommendation and frame scaling raised in
fallback mode (e.g. after the Pi 5 hardware refusal).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 08:56:12 -04:00

138 lines
5.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Plugin Safety Harness
Renders a plugin across **every declared screen (mode)** and **a spread of
matrix sizes**, and fails if any combination crashes, draws past the panel edge,
or — for plugins that ship golden images — drifts visually. The goal: change a
plugin without breaking a size or screen you didn't think to test.
## Sizes: a sample, not a fixed list
There is **no fixed set of supported panel sizes** — an RGB matrix build can be
any width/height and configuration (square, rectangle, 2×2, 4×4, 8×2, long
strips, tall stacks). Plugins are expected to read dimensions dynamically
(`self.display_manager.width/height` — not `matrix.width/height`, since
`matrix` is `None` when hardware init fails) and lay themselves out
accordingly, so a hardcoded coordinate or unscaled font shows up as a failure
here.
The harness therefore renders against a **representative sample** that spans the
axes of variation (`DEFAULT_TEST_SIZES` in `src/plugin_system/testing/sizes.py`),
not an authoritative list:
Each module is 64×32; entries are real panel-grid arrangements (cols × rows):
| Size | Grid | Why it's in the sample |
|---------|------|--------------------------------------------|
| 64×32 | 1×1 | single panel — tightest common rectangle |
| 128×32 | 2×1 | the baseline most plugins are tuned for |
| 64×64 | 1×2 | stacked — tall-narrow centering |
| 128×64 | 2×2 | block — icon scaling / vertical centering |
| 256×32 | 4×1 | long strip — wide horizontal layout |
| 128×96 | 2×3 | tall — vertical overflow |
| 256×128 | 4×4 | large block — both dimensions big at once |
**Override the sizes entirely** to test your actual hardware (or any shape):
```bash
# CLI — one-off:
python scripts/check_plugin.py --plugin clock-simple --sizes 8x16,64x64,256x32
# pytest — force every plugin onto your panel(s):
LEDMATRIX_TEST_SIZES="8x16,128x128" pytest test/plugins/test_plugin_matrix.py
# Per-plugin — declare the shapes a plugin targets in its test/harness.json:
# { "sizes": [[8, 16], [64, 64]] }
```
Precedence: `LEDMATRIX_TEST_SIZES` env (global) → per-plugin `harness.json`
`sizes` → the default sample. Bounds checking adapts to whatever sizes a run
uses — the backing canvas is padded out to the **largest** panel in the run, so
a coordinate meant for a big build is still caught when rendering a small one.
## Quick start
```bash
# Functional + bounds check across all sizes/screens:
python scripts/check_plugin.py --plugin clock-simple
# Every discovered plugin:
python scripts/check_plugin.py --all
# Dump PNGs to eyeball each size/screen:
python scripts/check_plugin.py --plugin ledmatrix-weather --out-dir /tmp/preview
```
Exit code is non-zero if any `(plugin, size, screen)` fails. Plugins are
discovered in `plugin-repos/` and `plugins/` (override with `--plugin-dir`).
## What it checks (Phase 1 — always on)
1. **Loads** and builds its mode list.
2. **Renders every screen** at every size without raising. `update()` may fail
(no network in CI) and is tolerated; a crash in `display()` is a failure —
`display()` must handle the no-data state.
3. **Bounds**: nothing is drawn past the right/bottom edge. Implemented by
`BoundsCheckingDisplayManager`, which backs the declared panel with an
oversized canvas and flags any pixels that land in the margin. (Left/top
overflow at negative coordinates and BDF text are not flagged — golden images
cover those.)
## Golden images (Phase 2 — opt-in per plugin)
A plugin opts in by committing reference PNGs and (usually) a small harness spec:
```
plugins/<id>/test/harness.json # how to render deterministically
plugins/<id>/test/fixtures/mock.json # optional cached data
plugins/<id>/test/golden/<WxH>/<mode>.png
```
`test/harness.json` keys (all optional):
```json
{
"config": { "timezone": "UTC" },
"mock_data": "fixtures/mock.json",
"freeze_time": "2025-08-01 15:25:00",
"skip_update": false,
"sizes": [[128, 32], [128, 64]]
}
```
Generate / refresh goldens after an intentional visual change, then review the
diff before committing:
```bash
python scripts/check_plugin.py --plugin clock-simple --update-golden \
--config '{"timezone":"UTC"}' --freeze-time "2025-08-01 15:25:00"
```
Comparison is exact by default (`compare_images` in `harness.py` accepts a
tolerance for known anti-aliasing noise). Determinism requires a pinned Pillow
and the bundled fonts — keep both stable when regenerating goldens.
## Tests & CI
- `test/plugins/test_harness.py` — unit tests for bounds detection, image
comparison, and mode enumeration (run anywhere).
- `test/plugins/test_plugin_matrix.py` — parametrized over discovered plugins ×
sizes × screens; honors each plugin's `test/harness.json` and goldens. Skips
when no plugins are present (e.g. a fresh core checkout); set
`LEDMATRIX_REQUIRE_PLUGINS=1` in a pipeline where plugins must be present to
turn an empty discovery into a hard failure instead. Point it at the monorepo
with `LEDMATRIX_PLUGINS_DIR=/path/to/ledmatrix-plugins/plugins`.
- `.github/workflows/test.yml` — runs the harness + visual tests on every PR.
The plugin monorepo has its own `Plugin Safety` workflow that runs this harness
against changed plugins on every PR.
## Developer workflow
1. Change the plugin on a branch.
2. `python scripts/check_plugin.py --plugin <id> --out-dir /tmp/preview` and
eyeball the PNGs.
3. Intentional visual change? `--update-golden`, review diffs, commit goldens.
4. (Monorepo) bump `manifest.json` version and let the pre-commit hook sync
`plugins.json`.
5. Push — CI re-runs the harness across all sizes and gates the PR.