mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-17 20:39:13 +00:00
test_plugin_dynamic_update_interval.py asserts that _get_plugin_update_interval() returns the number the plugin asked for. That is not the same claim as "the plugin gets updated more often", and the gap between those two is exactly where the original bug lived: the plugin knew it wanted 15s, said so in live_update_interval, and nothing downstream acted on it. So this ticks the real run_scheduled_updates() through a simulated hour and counts dispatches. Against pre-fix core it reports "10 updates in 10 minutes of a live game" -- the 60s manifest cadence, matching what was measured on a rig during the reported game. Against the fix it reports ~40. Also pins the regression that would be worse than the bug: an idle hour must still be ~60 updates, not 240. Asking for the live interval year-round would poll ESPN four times a minute all summer. Scope note, since it is easy to over-read this fix: the *switch* display path already refreshed the manager immediately before drawing, via _try_manager_display() -> _ensure_manager_updated(), which honours the manager's own 15s interval. So a switch-mode card was already <=15s stale at draw time before this change. What this fixes is the background cadence, which is what live-priority detection, Vegas content and scroll preparation all read. Full suite: 4,288 passed, 68 skipped. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014RRtqXDCnvnY6EQwhT5CV9
160 lines
5.8 KiB
Python
160 lines
5.8 KiB
Python
"""The scheduler actually polls faster while a plugin reports live content.
|
|
|
|
test_plugin_dynamic_update_interval.py proves _get_plugin_update_interval()
|
|
returns the plugin's requested number. That is not the same claim as "the
|
|
plugin gets updated more often", and the gap between those two is exactly where
|
|
the original bug lived: the plugin knew it wanted 15s, said so in
|
|
live_update_interval, and nothing downstream acted on it.
|
|
|
|
So this drives the real run_scheduled_updates() over a simulated hour of ticks
|
|
and counts dispatches. It is the test that would have caught the reported bug:
|
|
|
|
"the football plugin with live games only updates the live game in
|
|
progress if I restart the display"
|
|
|
|
Measured on a rig before the fix, during an NFL fourth quarter, ESPN was polled
|
|
at 23:21:49 / 23:22:50 / 23:23:50 / 23:24:50 -- exactly the manifest's 60s,
|
|
never the configured 15.
|
|
"""
|
|
|
|
from unittest.mock import MagicMock
|
|
|
|
import pytest
|
|
|
|
from src.plugin_system.plugin_manager import PluginManager
|
|
from src.plugin_system.plugin_state import PluginState
|
|
|
|
|
|
class _StateManager:
|
|
"""Enough of the real state machine for reservation to behave."""
|
|
|
|
def __init__(self):
|
|
self.states = {}
|
|
|
|
def can_execute(self, plugin_id):
|
|
return self.states.get(plugin_id, PluginState.ENABLED) != PluginState.RUNNING
|
|
|
|
def set_state(self, plugin_id, state):
|
|
self.states[plugin_id] = state
|
|
|
|
|
|
class _Plugin:
|
|
"""A sports plugin: fast while something is live, quiet otherwise."""
|
|
|
|
enabled = True
|
|
|
|
def __init__(self, live_interval=15):
|
|
self.live = False
|
|
self._live_interval = live_interval
|
|
|
|
def get_update_interval(self):
|
|
return self._live_interval if self.live else None
|
|
|
|
def update(self):
|
|
pass
|
|
|
|
|
|
@pytest.fixture
|
|
def scheduler():
|
|
pm = PluginManager.__new__(PluginManager)
|
|
import threading
|
|
pm._update_interval_cache = {}
|
|
pm.plugin_manifests = {"sports": {"update_interval": 60}}
|
|
pm.config_manager = None
|
|
pm.logger = MagicMock()
|
|
pm.state_manager = _StateManager()
|
|
pm.health_tracker = None
|
|
pm._synchronous_updates = False
|
|
pm._reservation_lock = threading.Lock()
|
|
pm._plugin_last_update_lock = threading.Lock()
|
|
pm.plugin_last_update = {}
|
|
pm.dispatched = []
|
|
|
|
# Stand in for the background worker: record the dispatch and complete it,
|
|
# so the next tick can reserve the plugin again.
|
|
def _enqueue(plugin_id, scheduled_time):
|
|
pm.dispatched.append(scheduled_time)
|
|
pm.plugin_last_update[plugin_id] = scheduled_time
|
|
pm.state_manager.set_state(plugin_id, PluginState.ENABLED)
|
|
|
|
pm._enqueue_update = _enqueue
|
|
return pm
|
|
|
|
|
|
def _run_for(pm, plugin, seconds, start=1_000_000.0, step=1.0):
|
|
"""Tick the real scheduler once a second for `seconds`."""
|
|
pm.plugins = {"sports": plugin}
|
|
t = start
|
|
end = start + seconds
|
|
while t < end:
|
|
pm.run_scheduled_updates(current_time=t)
|
|
t += step
|
|
return len(pm.dispatched)
|
|
|
|
|
|
class TestTheReportedBug:
|
|
def test_a_live_game_is_polled_at_the_live_interval(self, scheduler):
|
|
"""15s while live, not the manifest's 60s. This is the fix."""
|
|
plugin = _Plugin(live_interval=15)
|
|
plugin.live = True
|
|
count = _run_for(scheduler, plugin, seconds=600) # ten minutes
|
|
# 600s / 15s = 40, allowing one for the first tick's free run.
|
|
assert 38 <= count <= 41, (
|
|
f"{count} updates in 10 minutes of a live game; expected ~40 at 15s. "
|
|
f"At the manifest's 60s it would be ~10 -- the reported bug.")
|
|
|
|
def test_the_same_window_without_the_hook_is_the_old_behaviour(self, scheduler):
|
|
"""Pin what the bug actually looked like, so the contrast is asserted."""
|
|
plugin = _Plugin()
|
|
plugin.live = True
|
|
plugin.get_update_interval = lambda: None # pre-fix: no opinion
|
|
count = _run_for(scheduler, plugin, seconds=600)
|
|
assert 9 <= count <= 11, (
|
|
f"{count} updates in 10 minutes; expected ~10 at the manifest's 60s")
|
|
|
|
def test_idle_polling_is_unchanged(self, scheduler):
|
|
"""The regression that would be worse than the bug.
|
|
|
|
Asking for 15s year-round would poll ESPN four times a minute all
|
|
summer. Idle must stay exactly on the manifest.
|
|
"""
|
|
plugin = _Plugin()
|
|
plugin.live = False
|
|
count = _run_for(scheduler, plugin, seconds=3600) # a full hour idle
|
|
assert 59 <= count <= 61, (
|
|
f"{count} updates in an idle hour; expected ~60 at the manifest's 60s")
|
|
|
|
|
|
class TestTheCadenceTracksTheGame:
|
|
def test_it_speeds_up_when_a_game_starts_and_slows_when_it_ends(self, scheduler):
|
|
"""No reload, no restart -- the whole point of a per-tick hook."""
|
|
plugin = _Plugin(live_interval=15)
|
|
pm = scheduler
|
|
|
|
_run_for(pm, plugin, seconds=300, start=1_000_000.0)
|
|
idle_before = len(pm.dispatched)
|
|
|
|
plugin.live = True
|
|
_run_for(pm, plugin, seconds=300, start=1_000_300.0)
|
|
during = len(pm.dispatched) - idle_before
|
|
|
|
plugin.live = False
|
|
_run_for(pm, plugin, seconds=300, start=1_000_600.0)
|
|
idle_after = len(pm.dispatched) - idle_before - during
|
|
|
|
assert during > idle_before * 3, (
|
|
f"live window got {during} updates vs {idle_before} idle; "
|
|
"the hook did not speed anything up")
|
|
assert idle_after <= idle_before + 1, (
|
|
f"{idle_after} updates after the game ended vs {idle_before} before; "
|
|
"the fast cadence leaked past the live window")
|
|
|
|
|
|
def test_a_broken_hook_does_not_stop_the_plugin_updating(scheduler):
|
|
"""A scheduler that propagates one plugin's bug stops every other plugin."""
|
|
plugin = _Plugin()
|
|
plugin.get_update_interval = MagicMock(side_effect=RuntimeError("boom"))
|
|
count = _run_for(scheduler, plugin, seconds=600)
|
|
assert 9 <= count <= 11, (
|
|
f"{count} updates; a raising hook should fall back to the manifest's 60s")
|