mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-08-07 11:48:06 +00:00
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
98 lines
4.0 KiB
Python
98 lines
4.0 KiB
Python
"""
|
|
Regression tests for DisplayController._tick_plugin_updates_for_vegas().
|
|
|
|
PR #299 added logic to detect which plugins actually got fresh data on a
|
|
scheduled-update tick and notify Vegas mode via
|
|
vegas_coordinator.mark_plugin_updated(), so a live score change reaches the
|
|
scroll within seconds instead of waiting for a full cycle. PR #330's
|
|
multi-display sync refactor deleted this method (folding the callback back
|
|
to the plain _tick_plugin_updates(), which reports nothing), silently
|
|
orphaning VegasModeCoordinator.mark_plugin_updated() -- it has had zero
|
|
callers since.
|
|
"""
|
|
|
|
import os
|
|
from typing import Dict, List, Optional
|
|
from unittest.mock import MagicMock
|
|
|
|
# display_controller imports display_manager, which imports the hardware
|
|
# rgbmatrix module unless EMULATOR=true was set before import. Use the
|
|
# emulator (same convention as test_display_dirty_tracking.py and
|
|
# test/plugins/conftest.py) so this file collects on machines without the
|
|
# hardware library — and so display_manager gets the emulator binding no
|
|
# matter which test module imports it first.
|
|
os.environ.setdefault("EMULATOR", "true")
|
|
|
|
from src.display_controller import DisplayController
|
|
|
|
|
|
def _make_controller(updated: Optional[List[str]] = None, vegas_coordinator: Optional[MagicMock] = None) -> DisplayController:
|
|
dc = object.__new__(DisplayController)
|
|
dc.plugin_manager = MagicMock()
|
|
dc.plugin_manager.run_scheduled_updates_with_changes.return_value = list(updated or [])
|
|
dc.vegas_coordinator = vegas_coordinator
|
|
return dc
|
|
|
|
|
|
class TestTickPluginUpdatesForVegas:
|
|
def test_marks_only_plugins_whose_timestamp_advanced(self):
|
|
vc = MagicMock()
|
|
dc = _make_controller(updated=["stock-news"], vegas_coordinator=vc)
|
|
|
|
dc._tick_plugin_updates_for_vegas()
|
|
|
|
vc.mark_plugin_updated.assert_called_once_with("stock-news")
|
|
|
|
def test_no_advance_marks_nothing(self):
|
|
vc = MagicMock()
|
|
dc = _make_controller(updated=[], vegas_coordinator=vc)
|
|
|
|
dc._tick_plugin_updates_for_vegas()
|
|
|
|
vc.mark_plugin_updated.assert_not_called()
|
|
|
|
def test_no_vegas_coordinator_does_not_raise(self):
|
|
dc = _make_controller(updated=["stock-news"], vegas_coordinator=None)
|
|
|
|
dc._tick_plugin_updates_for_vegas() # must not raise
|
|
|
|
def test_mark_plugin_updated_exception_does_not_propagate(self):
|
|
"""One plugin's mark_plugin_updated failing must not stop the tick
|
|
or crash the update loop it runs in."""
|
|
vc = MagicMock()
|
|
vc.mark_plugin_updated.side_effect = [RuntimeError("boom"), None]
|
|
dc = _make_controller(updated=["a", "b"], vegas_coordinator=vc)
|
|
|
|
dc._tick_plugin_updates_for_vegas() # must not raise
|
|
|
|
assert vc.mark_plugin_updated.call_count == 2
|
|
|
|
|
|
class TestVegasCoordinatorCallbackWiring:
|
|
def test_initialize_wires_vegas_aware_tick_as_update_callback(self):
|
|
"""The Vegas coordinator must be given the Vegas-aware
|
|
_tick_plugin_updates_for_vegas as its update callback, not the plain
|
|
_tick_plugin_updates() -- that's the exact wiring PR #330 dropped."""
|
|
dc = object.__new__(DisplayController)
|
|
dc.config = {"display": {"vegas_scroll": {"enabled": True}}, "sync": {}}
|
|
dc.display_manager = MagicMock()
|
|
dc.plugin_manager = MagicMock()
|
|
dc.sync_manager = MagicMock()
|
|
dc._check_live_priority = MagicMock()
|
|
dc._check_vegas_interrupt = MagicMock(return_value=False)
|
|
|
|
fake_coordinator = MagicMock()
|
|
|
|
import src.display_controller as dc_module
|
|
original_imported = dc_module._vegas_mode_imported
|
|
original_class = dc_module.VegasModeCoordinator
|
|
try:
|
|
dc_module._vegas_mode_imported = True
|
|
dc_module.VegasModeCoordinator = MagicMock(return_value=fake_coordinator)
|
|
dc._initialize_vegas_mode()
|
|
finally:
|
|
dc_module._vegas_mode_imported = original_imported
|
|
dc_module.VegasModeCoordinator = original_class
|
|
|
|
fake_coordinator.set_update_callback.assert_called_once_with(dc._tick_plugin_updates_for_vegas)
|