mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-08-08 04:08:06 +00:00
Nine fixes for bugs surfaced while writing coverage for previously untested modules (plus the bool-duration quirk pinned in PR #441): - base_plugin.get_display_duration: exclude bools from both numeric branches — display_duration=True no longer reads as a 1-second slot; it falls through to config, then the 15.0 default. - display_helper: draw_error_message/draw_no_data_message called _draw_centered_text with the wrong arguments and crashed with AttributeError — both now delegate to draw_centered_text. draw_scorebug_layout drew status and clock at the same y, overprinting each other — they now share one combined top line. draw_ticker_layout drew its text starting at x=display_width (fully off-canvas), returning a blank frame every time — now draws at x=0; scroll_speed stays accepted-but-unused and is documented as such. - api_helper.clear_cache guarded on a nonexistent CacheManager.clear() method, silently never clearing anything; it now uses the real surface (clear_cache/delete/list_cache_files) and no-ops safely otherwise. - base_odds_manager._extract_espn_data raised AttributeError when ESPN sent explicit JSON nulls ("homeTeamOdds": null) — every level now null-safes with 'or {}'. format_odds_summary gated on is_odds_available, which deliberately ignores money lines, so ML-only odds formatted as "No odds available" — it now gates only on empty/no_odds data and formats money lines. - logging_config.ContextualFormatter mutated record.msg in place, so a second handler prepended the context prefix twice; it now formats a copy. log_error hardcoded exc_info=True and raised TypeError when the caller passed exc_info — now kwargs.setdefault. - dynamic_team_resolver wrote its "shared" class cache through self, creating instance shadows — the cache was per-instance and every scoreboard refetched rankings. Writes now go through the class. - saved_repositories cleaned URLs with an unanchored .replace('.git','') that mangled URLs merely containing '.git' (my.github.io -> myhub.io); now strips only a trailing suffix. add/remove also roll back the in-memory list when the save fails, so memory always matches disk. - config_helper.merge_configs shallow-copied the base, aliasing every un-overridden nested dict into the result — now deep-copies. - startup_validator.validate_all accumulated errors/warnings across calls — now resets both lists per run. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NohXi78cwsAKtN1sCfxjUh
125 lines
4.7 KiB
Python
125 lines
4.7 KiB
Python
"""
|
|
Tests for BasePlugin.get_display_duration — ~100 lines of type coercion that
|
|
every plugin's rotation slot depends on, previously untested.
|
|
|
|
The contract: a positive number wins wherever it comes from; everything else
|
|
falls through instance attr → config → the 15.0 default, logging on the way.
|
|
"""
|
|
|
|
from unittest.mock import MagicMock
|
|
|
|
import pytest
|
|
|
|
from src.plugin_system.base_plugin import BasePlugin
|
|
|
|
|
|
class _MinimalPlugin(BasePlugin):
|
|
def update(self):
|
|
pass
|
|
|
|
def display(self, force_clear=False):
|
|
pass
|
|
|
|
|
|
def make_plugin(config=None, instance_duration="__unset__"):
|
|
plugin = _MinimalPlugin(
|
|
plugin_id="duration-test",
|
|
config=config or {},
|
|
display_manager=MagicMock(),
|
|
cache_manager=MagicMock(),
|
|
plugin_manager=MagicMock(),
|
|
)
|
|
if instance_duration != "__unset__":
|
|
plugin.display_duration = instance_duration
|
|
return plugin
|
|
|
|
|
|
class TestInstanceVariable:
|
|
def test_positive_int_wins(self):
|
|
assert make_plugin(instance_duration=30).get_display_duration() == 30.0
|
|
|
|
def test_positive_float_wins(self):
|
|
assert make_plugin(instance_duration=12.5).get_display_duration() == 12.5
|
|
|
|
def test_returns_float_type(self):
|
|
result = make_plugin(instance_duration=30).get_display_duration()
|
|
assert isinstance(result, float)
|
|
|
|
def test_numeric_string_wins(self):
|
|
assert make_plugin(instance_duration="25").get_display_duration() == 25.0
|
|
|
|
def test_zero_falls_through_to_config(self):
|
|
plugin = make_plugin(config={"display_duration": 20},
|
|
instance_duration=0)
|
|
assert plugin.get_display_duration() == 20.0
|
|
|
|
def test_negative_falls_through_to_config(self):
|
|
plugin = make_plugin(config={"display_duration": 20},
|
|
instance_duration=-5)
|
|
assert plugin.get_display_duration() == 20.0
|
|
|
|
def test_none_falls_through_to_config(self):
|
|
plugin = make_plugin(config={"display_duration": 20},
|
|
instance_duration=None)
|
|
assert plugin.get_display_duration() == 20.0
|
|
|
|
def test_garbage_string_falls_through(self):
|
|
plugin = make_plugin(config={"display_duration": 20},
|
|
instance_duration="abc")
|
|
assert plugin.get_display_duration() == 20.0
|
|
|
|
def test_non_positive_string_falls_through(self):
|
|
plugin = make_plugin(config={"display_duration": 20},
|
|
instance_duration="0")
|
|
assert plugin.get_display_duration() == 20.0
|
|
|
|
def test_unexpected_type_falls_through(self):
|
|
plugin = make_plugin(config={"display_duration": 20},
|
|
instance_duration=[30])
|
|
assert plugin.get_display_duration() == 20.0
|
|
|
|
def test_bool_true_falls_through_like_any_non_number(self):
|
|
# bool is an int subclass, but a boolean is not a duration: True
|
|
# must NOT read as 1 second — it falls through to config/default.
|
|
assert make_plugin(instance_duration=True).get_display_duration() == 15.0
|
|
|
|
def test_bool_true_falls_through_to_config(self):
|
|
plugin = make_plugin(config={"display_duration": 20},
|
|
instance_duration=True)
|
|
assert plugin.get_display_duration() == 20.0
|
|
|
|
def test_bool_false_still_falls_through(self):
|
|
plugin = make_plugin(config={"display_duration": 20},
|
|
instance_duration=False)
|
|
assert plugin.get_display_duration() == 20.0
|
|
|
|
|
|
class TestConfigFallback:
|
|
def test_config_number(self):
|
|
assert make_plugin({"display_duration": 20}).get_display_duration() == 20.0
|
|
|
|
def test_config_numeric_string(self):
|
|
assert make_plugin({"display_duration": "12.5"}).get_display_duration() == 12.5
|
|
|
|
def test_missing_config_uses_default(self):
|
|
assert make_plugin({}).get_display_duration() == 15.0
|
|
|
|
def test_config_zero_uses_default(self):
|
|
assert make_plugin({"display_duration": 0}).get_display_duration() == 15.0
|
|
|
|
def test_config_negative_uses_default(self):
|
|
assert make_plugin({"display_duration": -10}).get_display_duration() == 15.0
|
|
|
|
def test_config_garbage_string_uses_default(self):
|
|
assert make_plugin({"display_duration": "soon"}).get_display_duration() == 15.0
|
|
|
|
def test_config_unexpected_type_uses_default(self):
|
|
assert make_plugin({"display_duration": {"s": 5}}).get_display_duration() == 15.0
|
|
|
|
def test_config_none_uses_default(self):
|
|
assert make_plugin({"display_duration": None}).get_display_duration() == 15.0
|
|
|
|
def test_config_bool_uses_default(self):
|
|
assert make_plugin({"display_duration": True}).get_display_duration() == 15.0
|
|
assert make_plugin({"display_duration": False}).get_display_duration() == 15.0
|