Files
LEDMatrix/test/test_vegas_coordinator_iteration.py
T
ChuckandClaude Opus 5.5 559ef14a5c feat(vegas): keep live games in the ticker by default
display.vegas_scroll.live_in_ticker now defaults to true: through a live
game the marquee keeps running and the live scoreboard takes extra turns in
it -- its cards updating in place while they scroll -- instead of the ticker
giving way to the full-screen scoreboard.

The new default would reach nobody on its own: every existing config holds
an explicit false copied from the template (there was no control for it),
and the template merge only adds missing keys. ConfigManager therefore turns
a stored false on once, with a backup, and records live_in_ticker_migrated
so a false chosen afterwards stays. The marker is never in the template.

A "Keep live games in the ticker" checkbox under Vegas mode sets it. Tests
that pin the full-screen takeover now say live_in_ticker=false.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 18:36:39 -04:00

141 lines
5.3 KiB
Python

"""run_iteration() does no per-iteration static-mode bookkeeping.
Every iteration used to rebuild a set of STATIC-mode plugins -- asking each
plugin for its display mode and logging the result at INFO -- that nothing
ever read. The static pause is driven by _check_static_plugin_trigger(), which
looks at the next segment, not at that set.
"""
import logging
import threading
from types import SimpleNamespace
from unittest.mock import MagicMock
from src.plugin_system.base_plugin import VegasDisplayMode
from src.vegas_mode.config import VegasModeConfig
from src.vegas_mode.coordinator import VegasModeCoordinator
def _coordinator(plugins):
coord = VegasModeCoordinator.__new__(VegasModeCoordinator)
coord.vegas_config = VegasModeConfig.from_config({'display': {'vegas_scroll': {
'enabled': True, 'max_cycle_duration': 0}}})
coord.render_pipeline = MagicMock()
# The loop paces itself from these (#628); a MagicMock can't be compared.
coord.render_pipeline.frame_interval = 0.0
coord.render_pipeline.target_fps = 90
coord.stream_manager = MagicMock()
coord.display_manager = MagicMock()
coord.plugin_manager = SimpleNamespace(plugins=plugins, get_plugin=plugins.get)
coord.stats = {'cycles_completed': 0, 'interruptions': 0}
coord._state_lock = threading.Lock()
coord._is_active = True
coord._is_paused = False
coord._should_stop = False
coord._fps_last_health_log = 0.0
coord._fps_was_degraded = False
coord._live_priority_check = None
coord._live_priority_active = False
coord._interrupt_check = None
coord._interrupt_check_interval = 10
coord._update_callback = None
coord._update_tick_running = False
coord._check_static_plugin_trigger = lambda: None
coord.run_frame = lambda: True
return coord
def test_an_iteration_does_not_poll_every_plugin_for_its_mode(caplog):
static = MagicMock()
static.get_vegas_display_mode.return_value = VegasDisplayMode.STATIC
coord = _coordinator({'static-plugin': static})
with caplog.at_level(logging.INFO, logger='src.vegas_mode.coordinator'):
assert coord.run_iteration() is True
static.get_vegas_display_mode.assert_not_called()
assert not any('Static mode plugins' in r.getMessage() for r in caplog.records)
def _live_coordinator(live):
"""A coordinator running the real run_frame(), with a switchable live check.
live_in_ticker off: these pin the full-screen takeover, which is no longer
the default.
"""
coord = _coordinator({})
del coord.run_frame # the real one: it is what refuses frames while paused
coord.vegas_config = VegasModeConfig.from_config({'display': {'vegas_scroll': {
'enabled': True, 'max_cycle_duration': 0, 'continuous_scroll': True,
'live_in_ticker': False}}})
coord.render_pipeline.has_deferred.return_value = False
coord.render_pipeline.needs_extension.return_value = False
coord.render_pipeline.render_frame.return_value = True
coord._pending_config_update = False
coord._last_live_check = float('-inf')
coord._live_priority_check = lambda: live[0]
return coord
def test_vegas_resumes_after_a_live_priority_pause():
"""The pause for live content was only ever lifted from inside run_frame(),
which returns before reaching that check while paused -- so once live
content had interrupted the ticker, it never came back until a restart.
The display controller only calls run_iteration() when nothing preempts
Vegas, so an iteration starting while paused for live content resumes."""
live = ['nfl_live']
coord = _live_coordinator(live)
assert coord.run_iteration() is False
assert coord._is_paused and coord._live_priority_active
coord.render_pipeline.render_frame.assert_not_called()
live[0] = None
coord._last_live_check = float('-inf')
assert coord.run_iteration() is True
assert not coord._is_paused and not coord._live_priority_active
coord.render_pipeline.render_frame.assert_called()
def test_stop_clears_a_live_priority_pause():
coord = _live_coordinator(['nfl_live'])
coord._start_time = None
coord._restore_switch_interval = lambda: None
coord._remove_render_gate = lambda: None
coord.run_iteration()
assert coord._is_paused
coord.stop()
assert not coord._is_paused and not coord._live_priority_active
def test_iteration_length_ignores_wall_clock_steps(monkeypatch):
"""No RTC: the wall clock jumps when NTP first syncs. A forward step used
to end the iteration on its first frame."""
from src.vegas_mode import coordinator as coordinator_module
coord = _coordinator({})
coord.vegas_config = VegasModeConfig.from_config({'display': {'vegas_scroll': {
'enabled': True, 'max_cycle_duration': 10}}})
frames = [0]
def frame():
frames[0] += 1
return True
coord.run_frame = frame
clock = [1000.0]
wall = [1000.0]
def monotonic():
clock[0] += 0.5
return clock[0]
def wall_time():
wall[0] += 3600.0 # NTP stepping the wall clock forward
return wall[0]
monkeypatch.setattr(coordinator_module.time, 'monotonic', monotonic)
monkeypatch.setattr(coordinator_module.time, 'time', wall_time)
monkeypatch.setattr(coordinator_module.time, 'sleep', lambda s: None)
assert coord.run_iteration() is True
assert frames[0] > 1