mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-17 20:39:13 +00:00
CodeRabbit raised six; all six were real. The test double had drifted ahead of production. VisualTestDisplayManager accepted set_scrolling_state(frame_hold=...) while DisplayManager did not, so such a call passed every harness run and would raise TypeError on the panel -- the one failure a safety harness exists to prevent. frame_hold belongs to the change that adds it to DisplayManager (#523), so it moves there and the double matches main again. The harness swallowed exceptions from re-rendered frames. _settle_loop re-renders a mode that came back blank, to give a scroll time to draw; returning silently on a crash meant a mode that renders one good frame and then explodes was reported as passing. Recorded on result.error now, keeping the captured frame so the failure stays inspectable. starlark-apps display() returned True after _display_frame() failed, so the controller held a dead frame for the whole display_duration instead of rotating on. _display_frame now returns bool on all three paths. run_plugin_tests.py used env.setdefault for PYTHONPATH and LEDMATRIX_CORE, so an inherited value won and the subprocess imported a different core than the one under test -- ledmatrix-plugins#467 exactly. Prepends PROJECT_ROOT and sets LEDMATRIX_CORE unconditionally. The on-demand mailbox is polled after every frame, ~125x/second on a scrolling mode, and the read is deliberately uncached, so it was that many disk reads per second to find nothing. Floored at 250ms, which is imperceptible for a web-UI click. Consuming it also deleted whatever was present rather than what had just been processed, so a request posted while the previous one was in flight was thrown away and never ran; the delete is now keyed by request_id. That narrows the window rather than closing it -- a true atomic claim needs a primitive the cache layer does not offer, and the code says so rather than implying otherwise. Codacy's 2 criticals were bandit B404/B603 on the subprocess call added to run_plugin_tests.py. Fixed interpreter, argument list, no shell; annotated with the repo's existing nosec convention. Bandit is clean on the file. Adds test/test_on_demand_mailbox.py (8), test_starlark_display_contract.py (4) and two settle cases in test_harness_empty_claimed.py. 4, 4 and 2 of those fail against the pre-fix code. Full suite: 3961 passed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014RRtqXDCnvnY6EQwhT5CV9
107 lines
4.4 KiB
Python
107 lines
4.4 KiB
Python
"""The on-demand request mailbox: how often it is read, and how it is consumed.
|
|
|
|
The mailbox is a cache key the web process writes and the display process
|
|
reads. Two properties matter and neither is obvious from the call site:
|
|
|
|
* it is polled after every rendered frame, so an uncached read here is a
|
|
disk read at frame rate;
|
|
* consuming it must not throw away a request that arrived while the previous
|
|
one was being processed.
|
|
"""
|
|
|
|
from unittest.mock import MagicMock
|
|
|
|
import pytest
|
|
|
|
|
|
class TestPollingIsBounded:
|
|
"""_poll_on_demand_requests runs ~125x/second on a scrolling mode.
|
|
|
|
The read is deliberately uncached (memory_ttl=0) because a cached one
|
|
pinned the first request for an hour. That makes the call a real disk read,
|
|
so it needs a floor -- without one it was ~125 reads per second to find
|
|
nothing at all.
|
|
"""
|
|
|
|
def test_first_call_always_reads(self, test_display_controller):
|
|
c = test_display_controller
|
|
c.cache_manager.get = MagicMock(return_value=None)
|
|
c._poll_on_demand_requests()
|
|
assert c.cache_manager.get.call_count == 1
|
|
|
|
def test_immediate_second_call_does_not_read(self, test_display_controller):
|
|
c = test_display_controller
|
|
c.cache_manager.get = MagicMock(return_value=None)
|
|
c._poll_on_demand_requests()
|
|
for _ in range(50):
|
|
c._poll_on_demand_requests()
|
|
assert c.cache_manager.get.call_count == 1, "polling was not bounded"
|
|
|
|
def test_reads_again_once_the_interval_has_passed(self, test_display_controller, monkeypatch):
|
|
c = test_display_controller
|
|
c.cache_manager.get = MagicMock(return_value=None)
|
|
clock = {"t": 1000.0}
|
|
monkeypatch.setattr("src.display_controller.time.monotonic", lambda: clock["t"])
|
|
|
|
c._poll_on_demand_requests()
|
|
clock["t"] += c.ON_DEMAND_POLL_INTERVAL / 2
|
|
c._poll_on_demand_requests()
|
|
assert c.cache_manager.get.call_count == 1, "read before the interval elapsed"
|
|
|
|
clock["t"] += c.ON_DEMAND_POLL_INTERVAL
|
|
c._poll_on_demand_requests()
|
|
assert c.cache_manager.get.call_count == 2
|
|
|
|
def test_the_interval_is_short_enough_to_feel_instant(self, test_display_controller):
|
|
# A person clicking in the web UI must not notice the floor.
|
|
assert test_display_controller.ON_DEMAND_POLL_INTERVAL <= 0.5
|
|
|
|
|
|
class TestMailboxIsConsumedByIdentity:
|
|
"""Deleting whatever is in the mailbox loses a request that raced in."""
|
|
|
|
def _arrange(self, controller, first, later):
|
|
"""Mailbox returns `first`, then `later` on the pre-delete re-read."""
|
|
controller.on_demand_active = False
|
|
controller.on_demand_request_id = None
|
|
controller._last_on_demand_poll = None
|
|
reads = iter([first, later])
|
|
|
|
def fake_get(key, *a, **kw):
|
|
if key == 'display_on_demand_request':
|
|
return next(reads, later)
|
|
return None # processed-id lookup
|
|
|
|
controller.cache_manager.get = MagicMock(side_effect=fake_get)
|
|
controller.cache_manager.set = MagicMock()
|
|
controller.cache_manager.delete = MagicMock()
|
|
controller._activate_on_demand = MagicMock()
|
|
|
|
REQ_A = {'request_id': 'A', 'action': 'start', 'plugin_id': 'p', 'mode': 'm'}
|
|
REQ_B = {'request_id': 'B', 'action': 'start', 'plugin_id': 'p', 'mode': 'm'}
|
|
|
|
def test_own_request_is_deleted(self, test_display_controller):
|
|
c = test_display_controller
|
|
self._arrange(c, self.REQ_A, self.REQ_A)
|
|
c._poll_on_demand_requests()
|
|
c.cache_manager.delete.assert_called_once_with('display_on_demand_request')
|
|
|
|
def test_a_newer_request_is_left_for_the_next_poll(self, test_display_controller):
|
|
c = test_display_controller
|
|
self._arrange(c, self.REQ_A, self.REQ_B)
|
|
c._poll_on_demand_requests()
|
|
assert c.cache_manager.delete.call_count == 0, \
|
|
"request B was deleted without ever being processed"
|
|
|
|
def test_an_already_empty_mailbox_is_still_cleared(self, test_display_controller):
|
|
c = test_display_controller
|
|
self._arrange(c, self.REQ_A, None)
|
|
c._poll_on_demand_requests()
|
|
c.cache_manager.delete.assert_called_once_with('display_on_demand_request')
|
|
|
|
def test_the_request_is_still_processed(self, test_display_controller):
|
|
c = test_display_controller
|
|
self._arrange(c, self.REQ_A, self.REQ_B)
|
|
c._poll_on_demand_requests()
|
|
c._activate_on_demand.assert_called_once()
|