mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-08-05 18:58:10 +00:00
Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
22f0a96cbb | ||
|
|
2a99cf6e64 | ||
|
|
b89653c836 | ||
|
|
d59a66133b | ||
|
|
d86dc5914b |
@@ -625,24 +625,14 @@ class DisplayController:
|
|||||||
# Check if per-day schedule is configured
|
# Check if per-day schedule is configured
|
||||||
days_config = schedule_config.get('days')
|
days_config = schedule_config.get('days')
|
||||||
|
|
||||||
# Determine which schedule to use. Respect an explicit 'mode' field
|
# Determine which schedule to use
|
||||||
# (like the dim schedule does) so a stray/legacy 'days' dict left over
|
|
||||||
# from config migration or a prior per-day setup can't silently
|
|
||||||
# override a user's Global schedule selection.
|
|
||||||
mode = schedule_config.get('mode')
|
|
||||||
mode_normalized = mode.replace('_', '-') if mode else None
|
|
||||||
|
|
||||||
use_per_day = False
|
use_per_day = False
|
||||||
if mode_normalized == 'global':
|
if days_config:
|
||||||
use_per_day = False
|
# Check if days dict is not empty and contains current day
|
||||||
elif mode_normalized == 'per-day':
|
if days_config and current_day in days_config:
|
||||||
use_per_day = bool(days_config and current_day in days_config)
|
|
||||||
elif days_config:
|
|
||||||
# No explicit mode recorded (legacy config) - fall back to
|
|
||||||
# inferring from presence of a 'days' dict for the current day.
|
|
||||||
if current_day in days_config:
|
|
||||||
use_per_day = True
|
use_per_day = True
|
||||||
else:
|
elif days_config:
|
||||||
|
# Days dict exists but doesn't have current day - fall back to global
|
||||||
logger.debug("Per-day schedule exists but %s not configured, using global schedule", current_day)
|
logger.debug("Per-day schedule exists but %s not configured, using global schedule", current_day)
|
||||||
|
|
||||||
if use_per_day:
|
if use_per_day:
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import json
|
|||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
import threading
|
import threading
|
||||||
import types
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Dict, List, Optional, Any
|
from typing import Dict, List, Optional, Any
|
||||||
import logging
|
import logging
|
||||||
@@ -744,18 +743,8 @@ class PluginManager:
|
|||||||
# If resource monitor exists, wrap the call
|
# If resource monitor exists, wrap the call
|
||||||
def monitored_update():
|
def monitored_update():
|
||||||
self.resource_monitor.monitor_call(plugin_id, plugin_instance.update)
|
self.resource_monitor.monitor_call(plugin_id, plugin_instance.update)
|
||||||
# SimpleNamespace stores `update` as an *instance*
|
|
||||||
# attribute, so attribute lookup returns the plain
|
|
||||||
# function object as-is. A dynamically-built class
|
|
||||||
# (`type(..., {'update': monitored_update})`) instead
|
|
||||||
# stores it as a *class* attribute, which the
|
|
||||||
# descriptor protocol turns into a bound method on
|
|
||||||
# access -- silently prepending the instance as an
|
|
||||||
# implicit first argument to a function that takes
|
|
||||||
# none, raising "monitored_update() takes 0
|
|
||||||
# positional arguments but 1 was given" on every call.
|
|
||||||
success = self.plugin_executor.execute_update(
|
success = self.plugin_executor.execute_update(
|
||||||
types.SimpleNamespace(update=monitored_update),
|
type('obj', (object,), {'update': monitored_update})(),
|
||||||
plugin_id
|
plugin_id
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -454,18 +454,6 @@ class VisualTestDisplayManager:
|
|||||||
"""Check if display is currently scrolling."""
|
"""Check if display is currently scrolling."""
|
||||||
return self._scrolling_state['is_scrolling']
|
return self._scrolling_state['is_scrolling']
|
||||||
|
|
||||||
def process_deferred_updates(self):
|
|
||||||
"""Process any deferred updates (no-op for testing).
|
|
||||||
|
|
||||||
Several ticker-style plugins (news, odds-ticker, leaderboard,
|
|
||||||
stock-news, stocks) call this unconditionally between
|
|
||||||
set_scrolling_state() and their scroll-position update, mirroring the
|
|
||||||
real display_manager's deferred-update queue. This double has no such
|
|
||||||
queue, so there is nothing to process — the no-op just lets those
|
|
||||||
plugins render under the harness instead of raising AttributeError.
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# Utility methods
|
# Utility methods
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
|
|||||||
@@ -172,16 +172,6 @@ class TestVisualDisplayManager:
|
|||||||
vdm.set_scrolling_state(False)
|
vdm.set_scrolling_state(False)
|
||||||
assert vdm.is_currently_scrolling() is False
|
assert vdm.is_currently_scrolling() is False
|
||||||
|
|
||||||
def test_process_deferred_updates_is_noop(self):
|
|
||||||
# Ticker-style plugins (news, odds-ticker, leaderboard, stock-news,
|
|
||||||
# stocks) call this unconditionally alongside set_scrolling_state();
|
|
||||||
# it must exist and be harmless so those plugins render under the
|
|
||||||
# harness instead of raising AttributeError.
|
|
||||||
vdm = VisualTestDisplayManager(width=128, height=32)
|
|
||||||
vdm.set_scrolling_state(True)
|
|
||||||
vdm.process_deferred_updates() # should not raise
|
|
||||||
assert vdm.is_currently_scrolling() is True
|
|
||||||
|
|
||||||
def test_format_date_with_ordinal(self):
|
def test_format_date_with_ordinal(self):
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
vdm = VisualTestDisplayManager(width=128, height=32)
|
vdm = VisualTestDisplayManager(width=128, height=32)
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ from unittest.mock import MagicMock, patch
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from src.plugin_system.plugin_manager import PluginManager
|
from src.plugin_system.plugin_manager import PluginManager
|
||||||
from src.plugin_system.plugin_state import PluginState
|
from src.plugin_system.plugin_state import PluginState
|
||||||
from src.plugin_system.resource_monitor import PluginResourceMonitor
|
|
||||||
|
|
||||||
class TestPluginManager:
|
class TestPluginManager:
|
||||||
"""Test PluginManager functionality."""
|
"""Test PluginManager functionality."""
|
||||||
@@ -79,54 +78,6 @@ class TestPluginManager:
|
|||||||
assert result is False
|
assert result is False
|
||||||
assert pm.state_manager.get_state("non_existent_plugin") == PluginState.ERROR
|
assert pm.state_manager.get_state("non_existent_plugin") == PluginState.ERROR
|
||||||
|
|
||||||
def test_run_scheduled_updates_calls_update_with_resource_monitor(
|
|
||||||
self, mock_config_manager, mock_display_manager, mock_cache_manager
|
|
||||||
):
|
|
||||||
"""Regression test: run_scheduled_updates() must actually call a
|
|
||||||
plugin's update() when self.resource_monitor is set (as it is in
|
|
||||||
every real deployment -- display_controller.py and web_interface/
|
|
||||||
app.py both assign a real PluginResourceMonitor after construction).
|
|
||||||
|
|
||||||
Previously, the resource_monitor branch wrapped the call in a
|
|
||||||
function stored as a *class* attribute on a dynamically-built type
|
|
||||||
(`type('obj', (object,), {'update': monitored_update})()`), which
|
|
||||||
the descriptor protocol turns into a bound method on access --
|
|
||||||
silently passing the synthetic instance as an implicit first
|
|
||||||
argument to monitored_update(), which takes none. Every plugin's
|
|
||||||
scheduled update failed with "monitored_update() takes 0 positional
|
|
||||||
arguments but 1 was given" and was silently swallowed into a
|
|
||||||
circuit-breaker retry loop that never succeeded, so plugin data
|
|
||||||
(scores, odds, etc.) never refreshed.
|
|
||||||
"""
|
|
||||||
with patch('src.plugin_system.plugin_manager.ensure_directory_permissions'):
|
|
||||||
pm = PluginManager(
|
|
||||||
plugins_dir="plugins",
|
|
||||||
config_manager=mock_config_manager,
|
|
||||||
display_manager=mock_display_manager,
|
|
||||||
cache_manager=mock_cache_manager
|
|
||||||
)
|
|
||||||
|
|
||||||
plugin_instance = MagicMock()
|
|
||||||
plugin_instance.enabled = True
|
|
||||||
plugin_instance.update = MagicMock()
|
|
||||||
|
|
||||||
pm.plugins["test_plugin"] = plugin_instance
|
|
||||||
pm.plugin_manifests["test_plugin"] = {"update_interval": 10}
|
|
||||||
pm.state_manager.set_state("test_plugin", PluginState.ENABLED)
|
|
||||||
# Plain MagicMock, not the mock_cache_manager fixture: this test
|
|
||||||
# is about run_scheduled_updates() actually invoking update()
|
|
||||||
# through the resource-monitor wrapper, not about
|
|
||||||
# PluginResourceMonitor's own cache-backed metrics persistence
|
|
||||||
# (which calls cache_manager.get(..., memory_ttl=...) --
|
|
||||||
# a kwarg the fixture's mock_get() doesn't accept).
|
|
||||||
pm.resource_monitor = PluginResourceMonitor(MagicMock())
|
|
||||||
|
|
||||||
pm.run_scheduled_updates(current_time=time.time())
|
|
||||||
|
|
||||||
plugin_instance.update.assert_called_once()
|
|
||||||
assert "test_plugin" in pm.plugin_last_update
|
|
||||||
assert pm.state_manager.get_state("test_plugin") == PluginState.ENABLED
|
|
||||||
|
|
||||||
|
|
||||||
class TestPluginLoader:
|
class TestPluginLoader:
|
||||||
"""Test PluginLoader functionality."""
|
"""Test PluginLoader functionality."""
|
||||||
|
|||||||
@@ -329,7 +329,6 @@ def save_schedule_config():
|
|||||||
}
|
}
|
||||||
|
|
||||||
mode = data.get('mode', 'global')
|
mode = data.get('mode', 'global')
|
||||||
schedule_config['mode'] = mode
|
|
||||||
|
|
||||||
if mode == 'global':
|
if mode == 'global':
|
||||||
# Simple global schedule
|
# Simple global schedule
|
||||||
|
|||||||
Reference in New Issue
Block a user