mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-17 20:39:13 +00:00
- _redact_credentials: blank scalar descendants of objects reached
through a credential-owned list (e.g. tokens: [{"value": "secret"}])
regardless of field name -- the existing name-based walk only
protected direct dict values under a credential key, not list items.
- wifi.py: reject enabled/force/auto_enable_ap_mode values
_parse_bool_ish can't recognize (400) instead of silently treating
them as False, which could disable Wi-Fi or the radio itself.
- Starlark manifest locking: lock a stable manifest.json.lock sidecar
instead of manifest.json itself, in both the standalone route path
(_starlark_manifest_lock) and the plugin path
(StarlarkAppsPlugin._save_manifest / _update_manifest_safe).
manifest.json is replaced by an atomic rename on every write, which
swaps in a fresh inode; a lock held on the old inode does not
exclude a second locker that opens the path afresh right after the
rename and gets the new inode, so two writers could race despite
each holding "a lock". A sidecar that no write ever touches always
resolves to the same inode for every locker.
Skipped as stale: the "serialize the complete manifest
read-modify-write" finding at api_v3/__init__.py -- every standalone
handler that calls _write_starlark_manifest is already wrapped in
_starlark_manifest_lock() on this branch.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
251 lines
9.8 KiB
Python
251 lines
9.8 KiB
Python
"""starlark-apps: display() must report what actually reached the panel.
|
|
|
|
The display controller skips a mode only on a boolean False. Returning True
|
|
after the frame update failed told it the mode had rendered, so it held a dead
|
|
frame for the whole display_duration instead of rotating on -- the same class
|
|
of defect as a display() that returns None.
|
|
"""
|
|
|
|
import json
|
|
import sys
|
|
import types
|
|
from pathlib import Path
|
|
from unittest.mock import MagicMock
|
|
|
|
import pytest
|
|
|
|
PLUGIN_DIR = Path(__file__).resolve().parent.parent / "plugin-repos" / "starlark-apps"
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def manager_module():
|
|
if not PLUGIN_DIR.exists():
|
|
pytest.skip("starlark-apps plugin is not checked out")
|
|
sys.path.insert(0, str(PLUGIN_DIR))
|
|
# fcntl is POSIX-only and the manager imports it at module scope for
|
|
# manifest locking. None of the display paths under test touch it, so a
|
|
# stub is what keeps these assertions running on a developer's machine
|
|
# rather than skipping everywhere but the Pi and CI.
|
|
injected_fcntl = "fcntl" not in sys.modules
|
|
if injected_fcntl:
|
|
stub = types.ModuleType("fcntl")
|
|
stub.LOCK_EX = 2
|
|
stub.LOCK_UN = 8
|
|
stub.flock = lambda *a, **kw: None
|
|
sys.modules["fcntl"] = stub
|
|
try:
|
|
import importlib
|
|
spec = importlib.util.spec_from_file_location(
|
|
"starlark_manager_under_test", PLUGIN_DIR / "manager.py")
|
|
module = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(module)
|
|
return module
|
|
except Exception as e: # noqa: BLE001 - optional deps (Pillow, pixlet) may be absent
|
|
pytest.skip(f"starlark-apps manager is not importable here: {e}")
|
|
finally:
|
|
sys.path.remove(str(PLUGIN_DIR))
|
|
if injected_fcntl:
|
|
sys.modules.pop("fcntl", None)
|
|
|
|
|
|
def _plugin(manager_module):
|
|
"""A manager with __init__ bypassed -- only display paths are under test."""
|
|
cls = manager_module.StarlarkAppsPlugin
|
|
inst = cls.__new__(cls)
|
|
inst.logger = MagicMock()
|
|
inst.display_manager = MagicMock()
|
|
inst.current_app = None
|
|
return inst
|
|
|
|
|
|
class _App:
|
|
def __init__(self, frames):
|
|
self.frames = frames
|
|
self.current_frame_index = 0
|
|
self.last_frame_time = 0.0
|
|
self.app_id = "app"
|
|
|
|
|
|
class TestDisplayFramePropagates:
|
|
def test_a_failed_update_returns_false(self, manager_module):
|
|
p = _plugin(manager_module)
|
|
p.current_app = _App([("frame", 100)])
|
|
p.display_manager.update_display.side_effect = RuntimeError("panel gone")
|
|
|
|
assert p._display_frame() is False
|
|
|
|
def test_a_good_update_returns_true(self, manager_module):
|
|
p = _plugin(manager_module)
|
|
p.current_app = _App([("frame", 100)])
|
|
|
|
assert p._display_frame() is True
|
|
|
|
def test_no_frames_returns_false(self, manager_module):
|
|
p = _plugin(manager_module)
|
|
p.current_app = _App([])
|
|
|
|
assert p._display_frame() is False
|
|
|
|
def test_display_reports_the_frame_failure(self, manager_module):
|
|
p = _plugin(manager_module)
|
|
p.current_app = _App([("frame", 100)])
|
|
p.display_manager.update_display.side_effect = RuntimeError("panel gone")
|
|
|
|
result = p.display()
|
|
|
|
assert result is False, "display() claimed success over a failed frame update"
|
|
assert isinstance(result, bool), "the controller only skips on a real bool"
|
|
|
|
|
|
class _SelectableApp(_App):
|
|
"""An app that display() can pick, render and show."""
|
|
|
|
def __init__(self, app_id, enabled=True):
|
|
super().__init__(frames=[("frame", 100)])
|
|
self.app_id = app_id
|
|
self._enabled = enabled
|
|
|
|
def is_enabled(self):
|
|
return self._enabled
|
|
|
|
|
|
def _plugin_with_apps(manager_module, *app_ids):
|
|
p = _plugin(manager_module)
|
|
p.apps = {app_id: _SelectableApp(app_id) for app_id in app_ids}
|
|
p._display_frame = lambda: True
|
|
return p
|
|
|
|
|
|
class TestDisplayModeSelectsTheApp:
|
|
"""The controller passes the mode it is rotating to; the plugin must use it.
|
|
|
|
It inspects display()'s signature to decide whether to pass display_mode at
|
|
all, so the parameter is the whole mechanism -- without it there is no way
|
|
to address one specific app, including from an on-demand request pinned to
|
|
one.
|
|
"""
|
|
|
|
def test_display_accepts_display_mode(self, manager_module):
|
|
import inspect
|
|
params = inspect.signature(manager_module.StarlarkAppsPlugin.display).parameters
|
|
assert "display_mode" in params, \
|
|
"the controller only passes display_mode to plugins whose signature accepts it"
|
|
|
|
def test_a_named_app_is_the_one_shown(self, manager_module):
|
|
p = _plugin_with_apps(manager_module, "aquarium", "printer", "nowplaying")
|
|
assert p.display(display_mode="printer") is True
|
|
assert p.current_app.app_id == "printer"
|
|
|
|
def test_a_named_app_wins_over_the_one_already_showing(self, manager_module):
|
|
p = _plugin_with_apps(manager_module, "aquarium", "printer")
|
|
p.display(display_mode="aquarium")
|
|
p.display(display_mode="printer")
|
|
assert p.current_app.app_id == "printer"
|
|
|
|
def test_an_unknown_mode_falls_back_to_rotation(self, manager_module):
|
|
"""The plugin id arrives here when no per-app modes are exposed."""
|
|
p = _plugin_with_apps(manager_module, "aquarium")
|
|
assert p.display(display_mode="starlark-apps") is True
|
|
assert p.current_app.app_id == "aquarium"
|
|
|
|
def test_force_clear_does_not_override_a_named_app(self, manager_module):
|
|
p = _plugin_with_apps(manager_module, "aquarium", "printer")
|
|
p.display(display_mode="printer", force_clear=True)
|
|
assert p.current_app.app_id == "printer"
|
|
|
|
|
|
class TestInstalledAppsTakeTurns:
|
|
"""Every installed app gets shown, not just whichever was picked first.
|
|
|
|
_select_next_app ran only while current_app was unset, so the first enabled
|
|
app was chosen once and displayed forever; the rest were rendered on
|
|
schedule and never reached the panel.
|
|
"""
|
|
|
|
def test_each_entry_to_the_mode_advances(self, manager_module):
|
|
p = _plugin_with_apps(manager_module, "a", "b", "c")
|
|
seen = []
|
|
for _ in range(3):
|
|
p.display(force_clear=True) # force_clear = "you are up"
|
|
seen.append(p.current_app.app_id)
|
|
assert seen == ["a", "b", "c"]
|
|
|
|
def test_the_rotation_wraps(self, manager_module):
|
|
p = _plugin_with_apps(manager_module, "a", "b")
|
|
seen = []
|
|
for _ in range(4):
|
|
p.display(force_clear=True)
|
|
seen.append(p.current_app.app_id)
|
|
assert seen == ["a", "b", "a", "b"]
|
|
|
|
def test_frames_within_one_slot_do_not_advance_the_app(self, manager_module):
|
|
"""Only entry to the mode rotates -- the high-FPS loop must not."""
|
|
p = _plugin_with_apps(manager_module, "a", "b", "c")
|
|
p.display(force_clear=True)
|
|
for _ in range(20):
|
|
p.display(force_clear=False)
|
|
assert p.current_app.app_id == "a"
|
|
|
|
def test_a_disabled_app_is_skipped(self, manager_module):
|
|
p = _plugin_with_apps(manager_module, "a", "b")
|
|
p.apps["a"]._enabled = False
|
|
p.display(force_clear=True)
|
|
assert p.current_app.app_id == "b"
|
|
|
|
def test_no_enabled_apps_reports_nothing_displayed(self, manager_module):
|
|
p = _plugin_with_apps(manager_module, "a")
|
|
p.apps["a"]._enabled = False
|
|
assert p.display(force_clear=True) is False
|
|
|
|
|
|
class TestManifestLockSidecarIsStable:
|
|
"""_save_manifest and _update_manifest_safe must lock a sidecar file that
|
|
a manifest write never replaces. manifest.json itself is swapped for a
|
|
fresh inode by every atomic write (temp file + rename); a lock taken on
|
|
manifest.json directly would not exclude a second locker whose fresh
|
|
os.open() lands on that new inode right after a write, so two writers
|
|
could still race each other despite each believing it held the lock.
|
|
web_interface._starlark_manifest_lock (web_interface/blueprints/api_v3)
|
|
must lock this same sidecar name for that guarantee to hold across both
|
|
the plugin-loaded and standalone paths.
|
|
"""
|
|
|
|
def _plugin_with_manifest_dir(self, manager_module, tmp_path):
|
|
p = _plugin(manager_module)
|
|
p.manifest_file = tmp_path / "manifest.json"
|
|
p.manifest_lock_file = tmp_path / "manifest.json.lock"
|
|
return p
|
|
|
|
def test_the_lock_sidecar_is_not_the_manifest_file(self, manager_module, tmp_path):
|
|
p = self._plugin_with_manifest_dir(manager_module, tmp_path)
|
|
assert p.manifest_lock_file != p.manifest_file
|
|
assert p.manifest_lock_file.name == "manifest.json.lock"
|
|
|
|
def test_the_locked_files_inode_survives_repeated_writes(self, manager_module, tmp_path):
|
|
p = self._plugin_with_manifest_dir(manager_module, tmp_path)
|
|
|
|
assert p._save_manifest({"apps": {}})
|
|
lock_ino_before = p.manifest_lock_file.stat().st_ino
|
|
|
|
for app_id in ("one", "two", "three"):
|
|
def _update(manifest, app_id=app_id):
|
|
manifest.setdefault("apps", {})[app_id] = {"enabled": True}
|
|
assert p._update_manifest_safe(_update)
|
|
|
|
lock_ino_after = p.manifest_lock_file.stat().st_ino
|
|
assert lock_ino_after == lock_ino_before, (
|
|
"the locked file's inode changed across writes -- a locker that "
|
|
"opened it before this write and one that opens it after would "
|
|
"no longer contend for the same lock")
|
|
|
|
manifest = json.loads(p.manifest_file.read_text())
|
|
assert set(manifest["apps"]) == {"one", "two", "three"}
|
|
|
|
|
|
class TestAnimationsRunAtFrameRate:
|
|
def test_the_plugin_asks_for_the_high_fps_loop(self, manager_module):
|
|
"""The controller reads this attribute; a multi-frame app is otherwise
|
|
called once per rotation slot and never advances past frame one."""
|
|
plugin = manager_module.StarlarkAppsPlugin
|
|
assert getattr(plugin, "enable_scrolling", False) is True
|