ci: run the whole test tree and make the plugin-safety job assert something real
The unit-tests CI job ran an explicit 24-file allowlist that had rotted: 63 of 90 test files (display, vegas, store manager, web API, web_interface) never ran on a PR. The job now runs all of test/ (minus test/plugins, which the plugin-safety job owns) so new test files are enrolled by default and any exclusion needs a visible, commented --ignore. The plugin-safety job was a green no-op: plugins/ is empty in CI, so every test skipped with 'Manifest not found'. It now renders a bundled deterministic fixture plugin (test/fixtures/plugins/ci-fixture-plugin, golden images included for all 8 default sizes) via LEDMATRIX_PLUGINS_DIR, and sets LEDMATRIX_REQUIRE_PLUGINS=1 so discovering zero plugins fails loudly instead of skipping green. The per-plugin suites document that they target dev machines with real plugins installed. Coverage is now measured and enforced in exactly one place — the CI unit-tests step (--cov=src --cov=web_interface --cov-fail-under=45, from a measured 47% baseline). pytest.ini previously declared --cov-fail-under=30 but CI always passed --no-cov, so the gate had never run anywhere; local pytest is now coverage-free and fast. Enabling the 63 unenrolled files surfaced three cases of test rot, fixed here: test_display_controller_vegas_tick.py could not collect without the hardware rgbmatrix module (now uses the emulator convention), the state-reconciliation unrecoverable-cache tests broke when production added the is_plugin_uninstalled tombstone check (bare Mock returned truthy), and test_get_system_status assumed the optional psutil dependency (now installed via requirements-test.txt and guarded by importorskip). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NohXi78cwsAKtN1sCfxjUh
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "CI Fixture Plugin",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
},
|
||||
"display_duration": {
|
||||
"type": "number",
|
||||
"default": 5
|
||||
},
|
||||
"border_color": {
|
||||
"type": "array",
|
||||
"items": {"type": "integer", "minimum": 0, "maximum": 255},
|
||||
"minItems": 3,
|
||||
"maxItems": 3,
|
||||
"default": [0, 255, 0],
|
||||
"description": "RGB color of the border rectangle."
|
||||
},
|
||||
"diagonal_color": {
|
||||
"type": "array",
|
||||
"items": {"type": "integer", "minimum": 0, "maximum": 255},
|
||||
"minItems": 3,
|
||||
"maxItems": 3,
|
||||
"default": [255, 0, 0],
|
||||
"description": "RGB color of the diagonals."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
"""
|
||||
CI fixture plugin.
|
||||
|
||||
Exists so the plugin safety harness (test/plugins/test_plugin_matrix.py and
|
||||
the plugin-safety CI job) always has at least one real plugin to load and
|
||||
render — without it, an empty plugins/ directory turns the whole job into a
|
||||
green no-op. The render is deliberately trivial and fully deterministic:
|
||||
a border rectangle plus both diagonals, sized from the display manager's
|
||||
declared dimensions. No fonts, no network, no time dependence, so golden
|
||||
images are stable across platforms.
|
||||
"""
|
||||
|
||||
from PIL import ImageDraw
|
||||
|
||||
from src.plugin_system.base_plugin import BasePlugin
|
||||
|
||||
|
||||
class CIFixturePlugin(BasePlugin):
|
||||
def update(self) -> None:
|
||||
"""Nothing to fetch — the render is self-contained."""
|
||||
|
||||
def display(self, force_clear: bool = False) -> None:
|
||||
width = self.display_manager.matrix.width
|
||||
height = self.display_manager.matrix.height
|
||||
border = tuple(self.config.get("border_color", [0, 255, 0]))
|
||||
diagonal = tuple(self.config.get("diagonal_color", [255, 0, 0]))
|
||||
|
||||
image = self.display_manager.image
|
||||
draw = ImageDraw.Draw(image)
|
||||
# Blank only the declared panel area, then draw edge-to-edge content:
|
||||
# the border proves the plugin reads dynamic dimensions (any overflow
|
||||
# or underfill at any size is a harness bug or a dimensions bug), the
|
||||
# diagonals make golden comparisons sensitive to size/offset drift.
|
||||
draw.rectangle([0, 0, width - 1, height - 1], fill=(0, 0, 0))
|
||||
draw.rectangle([0, 0, width - 1, height - 1], outline=border)
|
||||
draw.line([0, 0, width - 1, height - 1], fill=diagonal)
|
||||
draw.line([0, height - 1, width - 1, 0], fill=diagonal)
|
||||
self.display_manager.update_display()
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"id": "ci-fixture-plugin",
|
||||
"name": "CI Fixture Plugin",
|
||||
"version": "1.0.0",
|
||||
"description": "Bundled test fixture so the plugin safety harness always has at least one real plugin to render in CI. Draws a deterministic border + diagonals pattern at any panel size. Not installable from the store and never shipped to devices.",
|
||||
"author": "LEDMatrix",
|
||||
"entry_point": "manager.py",
|
||||
"class_name": "CIFixturePlugin",
|
||||
"display_modes": ["ci-fixture"],
|
||||
"update_interval": 3600,
|
||||
"min_ledmatrix_version": "2.0.0",
|
||||
"compatible_versions": [">=2.0.0"]
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
# No dependencies — the fixture must load in any environment.
|
||||
|
After Width: | Height: | Size: 359 B |
|
After Width: | Height: | Size: 586 B |
|
After Width: | Height: | Size: 849 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 395 B |
|
After Width: | Height: | Size: 319 B |
|
After Width: | Height: | Size: 466 B |
|
After Width: | Height: | Size: 454 B |