From 17deb5ad88918dd3ef2bad62caadfa6db55140e8 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 9 Aug 2026 16:26:26 -0400 Subject: [PATCH 1/2] fix(sports): stop an empty mode holding a blank panel for its duration Reported for hockey: NHL recent and live sit on a blank screen for the whole display duration. Reproduced with the plugin's real August data, where ESPN returns no recent or live NHL games while upcoming still has the preseason schedule -- which is why only those two modes show it. The display controller skips a mode whose display() returns False. Both plugins ship a bundled sports.py that is a stale fork of the core's, and its three display() methods had lost their return values; the base was even annotated `-> None`. Every path returns None, and the manager's dispatcher treats a non-boolean as success ("Result is None or other - assume success"), so an empty mode reported content and was never skipped. Its no-games branch calls display_manager.clear(), so what it reported as content was a blank panel, held until the duration expired. Return True when a game was drawn and False when there was nothing to show. That is what the dispatcher already documents ("Manager returns True if it has content to show, False if no content"), what the core's SportsCore/SportsUpcoming/SportsRecent do, and what the six other sports plugins do -- hockey and lacrosse are the only two that had drifted. Verified both directions against the real plugin with no games (display() now returns False and draws nothing, so the mode is skipped) and with a game (returns True and draws it). The added tests exercise the methods against a stand-in self, needing no display, network or cache; they fail against the unfixed sports.py for all three classes. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Udr6MfaFLUPhX5Fgo67Jf5 --- plugins.json | 6 +- plugins/hockey-scoreboard/manifest.json | 8 +- plugins/hockey-scoreboard/sports.py | 54 ++++++-- .../test_empty_mode_signals_no_content.py | 125 ++++++++++++++++++ plugins/lacrosse-scoreboard/manifest.json | 8 +- plugins/lacrosse-scoreboard/sports.py | 54 ++++++-- .../test_empty_mode_signals_no_content.py | 125 ++++++++++++++++++ 7 files changed, 349 insertions(+), 31 deletions(-) create mode 100644 plugins/hockey-scoreboard/test_empty_mode_signals_no_content.py create mode 100644 plugins/lacrosse-scoreboard/test_empty_mode_signals_no_content.py diff --git a/plugins.json b/plugins.json index 3953885..99f8f19 100644 --- a/plugins.json +++ b/plugins.json @@ -1,6 +1,6 @@ { "version": "1.0.0", - "last_updated": "2026-08-06", + "last_updated": "2026-08-09", "plugins": [ { "id": "cricket-scoreboard", @@ -335,7 +335,7 @@ "last_updated": "2026-08-05", "verified": true, "screenshot": "", - "latest_version": "1.9.0", + "latest_version": "1.9.1", "icon": "fas fa-hockey-puck" }, { @@ -359,7 +359,7 @@ "last_updated": "2026-08-05", "verified": true, "screenshot": "", - "latest_version": "1.9.0", + "latest_version": "1.9.1", "icon": "fas fa-baseball-ball" }, { diff --git a/plugins/hockey-scoreboard/manifest.json b/plugins/hockey-scoreboard/manifest.json index 2b25c7d..b6fe31d 100644 --- a/plugins/hockey-scoreboard/manifest.json +++ b/plugins/hockey-scoreboard/manifest.json @@ -1,7 +1,7 @@ { "id": "hockey-scoreboard", "name": "Hockey Scoreboard", - "version": "1.9.0", + "version": "1.9.1", "author": "ChuckBuilds", "description": "Live, recent, and upcoming hockey games across NHL, NCAA Men's, and NCAA Women's hockey with real-time scores and schedules", "homepage": "https://github.com/ChuckBuilds/ledmatrix-plugins/tree/main/plugins/hockey-scoreboard", @@ -54,6 +54,12 @@ } ], "versions": [ + { + "version": "1.9.1", + "released": "2026-08-09", + "notes": "Stop an out-of-season or empty mode holding a blank panel for its full display duration. This plugin's bundled sports.py was a stale fork of the core's and its three display() methods had lost their return values (the base was annotated -> None). The manager's dispatcher treats a non-boolean as success, so every mode reported content whether or not it had any, and the display controller -- which skips a mode whose display() returns False -- never skipped it. The no-games branch clears the display, so the result was a blank screen held for the whole duration. Reported against NHL recent and live in August, when both are empty while upcoming still has the preseason schedule. The methods now return True when a game was drawn and False when there was nothing to show, matching the core and the six other sports plugins.", + "ledmatrix_min_version": "2.0.0" + }, { "version": "1.9.0", "released": "2026-08-06", diff --git a/plugins/hockey-scoreboard/sports.py b/plugins/hockey-scoreboard/sports.py index 8820996..f083224 100644 --- a/plugins/hockey-scoreboard/sports.py +++ b/plugins/hockey-scoreboard/sports.py @@ -209,10 +209,18 @@ def _draw_scorebug_layout(self, game: Dict, force_clear: bool = False) -> None: f"Error in base _draw_scorebug_layout: {e}", exc_info=True ) - def display(self, force_clear: bool = False) -> None: - """Common display method for all NCAA FB managers""" # Updated docstring + def display(self, force_clear: bool = False) -> bool: + """ + Common display method for all managers. + + Returns True when a game was drawn and False when there was nothing to + show. The caller uses that to skip an empty mode: returning None here + left the display controller unable to tell "drew a game" from "drew + nothing", so an out-of-season league held a blank panel for its whole + display duration instead of being rotated past. + """ if not self.is_enabled: # Check if module is enabled - return + return False if not self.current_game: # Clear the display so old content doesn't persist @@ -227,17 +235,19 @@ def display(self, force_clear: bool = False) -> None: f"No game data available to display in {self.__class__.__name__}" ) setattr(self, "_last_warning_time", current_time) - return + return False try: self._draw_scorebug_layout(self.current_game, force_clear) # display_manager.update_display() should be called within subclass draw methods # or after calling display() in the main loop. Let's keep it out of the base display. + return True except Exception as e: self.logger.error( f"Error during display call in {self.__class__.__name__}: {e}", exc_info=True, ) + return False def _load_custom_font_from_element_config( self, @@ -1480,10 +1490,16 @@ def _draw_scorebug_layout(self, game: Dict, force_clear: bool = False) -> None: f"Error displaying upcoming game: {e}", exc_info=True ) # Changed log prefix - def display(self, force_clear=False): - """Display upcoming games, handling switching.""" + def display(self, force_clear=False) -> bool: + """ + Display upcoming games, handling switching. + + Returns True when a game was drawn, False when there was nothing to + show, so the caller can rotate past an empty mode instead of holding a + blank panel for its full display duration. + """ if not self.is_enabled: - return + return False if not self.games_list: # Clear the display so old content doesn't persist @@ -1499,7 +1515,7 @@ def display(self, force_clear=False): "No upcoming games found for favorite teams to display." ) # Changed log prefix self.last_warning_time = current_time - return # Skip display update + return False # Skip display update try: current_time = time.time() @@ -1536,12 +1552,15 @@ def display(self, force_clear=False): if self.current_game: self._draw_scorebug_layout(self.current_game, force_clear) - # update_display() is called within _draw_scorebug_layout for upcoming + # update_display() is called within _draw_scorebug_layout + return True + return False except Exception as e: self.logger.error( f"Error in display loop: {e}", exc_info=True ) # Changed log prefix + return False class SportsRecent(SportsCore): @@ -1983,8 +2002,14 @@ def _draw_scorebug_layout(self, game: Dict, force_clear: bool = False) -> None: f"Error displaying recent game: {e}", exc_info=True ) # Changed log prefix - def display(self, force_clear=False): - """Display recent games, handling switching.""" + def display(self, force_clear=False) -> bool: + """ + Display recent games, handling switching. + + Returns True when a game was drawn, False when there was nothing to + show, so the caller can rotate past an empty mode instead of holding a + blank panel for its full display duration. + """ if not self.is_enabled or not self.games_list: # If disabled or no games, clear the display so old content doesn't persist if force_clear or not self.games_list: @@ -1992,7 +2017,7 @@ def display(self, force_clear=False): self.display_manager.update_display() if not self.games_list and self.current_game: self.current_game = None # Clear internal state if list becomes empty - return + return False try: current_time = time.time() @@ -2029,12 +2054,15 @@ def display(self, force_clear=False): if self.current_game: self._draw_scorebug_layout(self.current_game, force_clear) - # update_display() is called within _draw_scorebug_layout for recent + # update_display() is called within _draw_scorebug_layout + return True + return False except Exception as e: self.logger.error( f"Error in display loop: {e}", exc_info=True ) # Changed log prefix + return False class SportsLive(SportsCore): diff --git a/plugins/hockey-scoreboard/test_empty_mode_signals_no_content.py b/plugins/hockey-scoreboard/test_empty_mode_signals_no_content.py new file mode 100644 index 0000000..92e21ab --- /dev/null +++ b/plugins/hockey-scoreboard/test_empty_mode_signals_no_content.py @@ -0,0 +1,125 @@ +#!/usr/bin/env python3 +""" +Tests that a mode with no games reports "no content" instead of "displayed". + +Regression under test: this plugin's bundled sports.py was a stale fork of the +core's, and its three display() methods had lost their return values -- the +base was even annotated ``-> None``. The manager's dispatcher treats a +non-boolean as success ("Result is None or other - assume success"), so every +mode reported content whether or not it had any. + +The display controller skips a mode whose display() returns False. Reporting +success for an empty mode meant it was never skipped, so an out-of-season +league sat on a blank panel -- the no-games branch calls display_manager.clear() +-- for its entire display duration. Reported for NHL recent and live in August, +when those are empty while upcoming still has the preseason schedule. + +The methods are exercised against a stand-in ``self`` rather than a constructed +manager, so the test needs no display hardware, no network and no cache. + +Run: /bin/python plugins/hockey-scoreboard/test_empty_mode_signals_no_content.py +""" + +import sys +import threading +from pathlib import Path + +plugin_dir = Path(__file__).parent +sys.path.insert(0, str(plugin_dir)) + +import sports # noqa: E402 + + +class _Logger: + def warning(self, *a, **k): pass + def info(self, *a, **k): pass + def debug(self, *a, **k): pass + def error(self, *a, **k): pass + + +class _DisplayManager: + def __init__(self): + self.clears = 0 + self.updates = 0 + + def clear(self): + self.clears += 1 + + def update_display(self): + self.updates += 1 + + +class _Manager: + """A stand-in ``self`` carrying only what display() touches.""" + + def __init__(self, games, enabled=True): + self.is_enabled = enabled + self.games_list = list(games) + self.current_game = games[0] if games else None + self.display_manager = _DisplayManager() + self.logger = _Logger() + self.sport_key = 'nhl' + self._games_lock = threading.Lock() + self.current_game_index = 0 + self.last_game_switch = 0.0 + self.game_display_duration = 1e9 # never switch mid-test + self.last_warning_time = 0.0 + self.warning_cooldown = 1e9 + self._last_warning_time = 0.0 + self.draws = 0 + + def _draw_scorebug_layout(self, game, force_clear=False): + self.draws += 1 + self.display_manager.update_display() + + +GAME = {'id': 'g1', 'away_abbr': 'TB', 'home_abbr': 'BOS'} + +# SportsLive has no display() of its own; it inherits SportsCore's, so covering +# SportsCore covers the live mode the bug was reported against. +CASES = [ + ('SportsCore', sports.SportsCore.display), + ('SportsUpcoming', sports.SportsUpcoming.display), + ('SportsRecent', sports.SportsRecent.display), +] + +failures = [] + + +def check(name, actual, expected): + if actual == expected: + print(" PASS %s" % name) + else: + print(" FAIL %s: expected %r, got %r" % (name, expected, actual)) + failures.append(name) + + +print("an empty mode reports no content, so the controller can skip it") +for label, display in CASES: + mgr = _Manager([]) + result = display(mgr, force_clear=False) + check("%s with no games returns False" % label, result, False) + check("%s with no games draws nothing" % label, mgr.draws, 0) + +print("\nthe same is true when the manager is disabled") +for label, display in CASES: + mgr = _Manager([GAME], enabled=False) + check("%s disabled returns False" % label, display(mgr), False) + +print("\na mode that does have a game still reports success") +for label, display in CASES: + mgr = _Manager([GAME]) + result = display(mgr, force_clear=False) + check("%s with a game returns True" % label, result, True) + check("%s with a game draws it" % label, mgr.draws, 1) + +print("\nthe result is a real bool, not something merely truthy") +# The dispatcher branches on `result is True` / `result is False`, so a truthy +# non-bool would fall through to the "assume success" path and reintroduce this. +for label, display in CASES: + check("%s empty -> bool" % label, type(display(_Manager([]))), bool) + check("%s populated -> bool" % label, + type(display(_Manager([GAME]))), bool) + +print("\n%s" % ("FAILED: %d" % len(failures) if failures else "All checks passed")) +sys.exit(1 if failures else 0) diff --git a/plugins/lacrosse-scoreboard/manifest.json b/plugins/lacrosse-scoreboard/manifest.json index 5ee7d8f..6d20784 100644 --- a/plugins/lacrosse-scoreboard/manifest.json +++ b/plugins/lacrosse-scoreboard/manifest.json @@ -1,7 +1,7 @@ { "id": "lacrosse-scoreboard", "name": "Lacrosse Scoreboard", - "version": "1.9.0", + "version": "1.9.1", "author": "ChuckBuilds", "description": "Live, recent, and upcoming NCAA men's and women's lacrosse games with real-time scores and schedules", "homepage": "https://github.com/ChuckBuilds/ledmatrix-plugins/tree/main/plugins/lacrosse-scoreboard", @@ -50,6 +50,12 @@ } ], "versions": [ + { + "version": "1.9.1", + "released": "2026-08-09", + "notes": "Stop an out-of-season or empty mode holding a blank panel for its full display duration. This plugin's bundled sports.py was a stale fork of the core's and its three display() methods had lost their return values (the base was annotated -> None). The manager's dispatcher treats a non-boolean as success, so every mode reported content whether or not it had any, and the display controller -- which skips a mode whose display() returns False -- never skipped it. The no-games branch clears the display, so the result was a blank screen held for the whole duration. Found alongside the same fault in hockey-scoreboard, where it was reported against NHL recent and live. The methods now return True when a game was drawn and False when there was nothing to show, matching the core and the six other sports plugins.", + "ledmatrix_min_version": "2.0.0" + }, { "version": "1.9.0", "released": "2026-08-06", diff --git a/plugins/lacrosse-scoreboard/sports.py b/plugins/lacrosse-scoreboard/sports.py index ea991e1..31d4029 100644 --- a/plugins/lacrosse-scoreboard/sports.py +++ b/plugins/lacrosse-scoreboard/sports.py @@ -210,10 +210,18 @@ def _draw_scorebug_layout(self, game: Dict, force_clear: bool = False) -> None: f"Error in base _draw_scorebug_layout: {e}", exc_info=True ) - def display(self, force_clear: bool = False) -> None: - """Common display method for all NCAA FB managers""" # Updated docstring + def display(self, force_clear: bool = False) -> bool: + """ + Common display method for all managers. + + Returns True when a game was drawn and False when there was nothing to + show. The caller uses that to skip an empty mode: returning None here + left the display controller unable to tell "drew a game" from "drew + nothing", so an out-of-season league held a blank panel for its whole + display duration instead of being rotated past. + """ if not self.is_enabled: # Check if module is enabled - return + return False if not self.current_game: # Clear the display so old content doesn't persist @@ -228,17 +236,19 @@ def display(self, force_clear: bool = False) -> None: f"No game data available to display in {self.__class__.__name__}" ) setattr(self, "_last_warning_time", current_time) - return + return False try: self._draw_scorebug_layout(self.current_game, force_clear) # display_manager.update_display() should be called within subclass draw methods # or after calling display() in the main loop. Let's keep it out of the base display. + return True except Exception as e: self.logger.error( f"Error during display call in {self.__class__.__name__}: {e}", exc_info=True, ) + return False def _load_custom_font_from_element_config( self, @@ -1481,10 +1491,16 @@ def _draw_scorebug_layout(self, game: Dict, force_clear: bool = False) -> None: f"Error displaying upcoming game: {e}", exc_info=True ) # Changed log prefix - def display(self, force_clear=False): - """Display upcoming games, handling switching.""" + def display(self, force_clear=False) -> bool: + """ + Display upcoming games, handling switching. + + Returns True when a game was drawn, False when there was nothing to + show, so the caller can rotate past an empty mode instead of holding a + blank panel for its full display duration. + """ if not self.is_enabled: - return + return False if not self.games_list: # Clear the display so old content doesn't persist @@ -1500,7 +1516,7 @@ def display(self, force_clear=False): "No upcoming games found for favorite teams to display." ) # Changed log prefix self.last_warning_time = current_time - return # Skip display update + return False # Skip display update try: current_time = time.time() @@ -1537,12 +1553,15 @@ def display(self, force_clear=False): if self.current_game: self._draw_scorebug_layout(self.current_game, force_clear) - # update_display() is called within _draw_scorebug_layout for upcoming + # update_display() is called within _draw_scorebug_layout + return True + return False except Exception as e: self.logger.error( f"Error in display loop: {e}", exc_info=True ) # Changed log prefix + return False class SportsRecent(SportsCore): @@ -1982,8 +2001,14 @@ def _draw_scorebug_layout(self, game: Dict, force_clear: bool = False) -> None: f"Error displaying recent game: {e}", exc_info=True ) # Changed log prefix - def display(self, force_clear=False): - """Display recent games, handling switching.""" + def display(self, force_clear=False) -> bool: + """ + Display recent games, handling switching. + + Returns True when a game was drawn, False when there was nothing to + show, so the caller can rotate past an empty mode instead of holding a + blank panel for its full display duration. + """ if not self.is_enabled or not self.games_list: # If disabled or no games, clear the display so old content doesn't persist if force_clear or not self.games_list: @@ -1991,7 +2016,7 @@ def display(self, force_clear=False): self.display_manager.update_display() if not self.games_list and self.current_game: self.current_game = None # Clear internal state if list becomes empty - return + return False try: current_time = time.time() @@ -2028,12 +2053,15 @@ def display(self, force_clear=False): if self.current_game: self._draw_scorebug_layout(self.current_game, force_clear) - # update_display() is called within _draw_scorebug_layout for recent + # update_display() is called within _draw_scorebug_layout + return True + return False except Exception as e: self.logger.error( f"Error in display loop: {e}", exc_info=True ) # Changed log prefix + return False def _swrr_schedule(weighted_ids: List[Tuple[str, int]]) -> List[str]: diff --git a/plugins/lacrosse-scoreboard/test_empty_mode_signals_no_content.py b/plugins/lacrosse-scoreboard/test_empty_mode_signals_no_content.py new file mode 100644 index 0000000..e3bbdd2 --- /dev/null +++ b/plugins/lacrosse-scoreboard/test_empty_mode_signals_no_content.py @@ -0,0 +1,125 @@ +#!/usr/bin/env python3 +""" +Tests that a mode with no games reports "no content" instead of "displayed". + +Regression under test: this plugin's bundled sports.py was a stale fork of the +core's, and its three display() methods had lost their return values -- the +base was even annotated ``-> None``. The manager's dispatcher treats a +non-boolean as success ("Result is None or other - assume success"), so every +mode reported content whether or not it had any. + +The display controller skips a mode whose display() returns False. Reporting +success for an empty mode meant it was never skipped, so an out-of-season +league sat on a blank panel -- the no-games branch calls display_manager.clear() +-- for its entire display duration. Found alongside the same fault in hockey-scoreboard, which was reported for +NHL recent and live in August, when those are empty while upcoming is not. + +The methods are exercised against a stand-in ``self`` rather than a constructed +manager, so the test needs no display hardware, no network and no cache. + +Run: /bin/python plugins/lacrosse-scoreboard/test_empty_mode_signals_no_content.py +""" + +import sys +import threading +from pathlib import Path + +plugin_dir = Path(__file__).parent +sys.path.insert(0, str(plugin_dir)) + +import sports # noqa: E402 + + +class _Logger: + def warning(self, *a, **k): pass + def info(self, *a, **k): pass + def debug(self, *a, **k): pass + def error(self, *a, **k): pass + + +class _DisplayManager: + def __init__(self): + self.clears = 0 + self.updates = 0 + + def clear(self): + self.clears += 1 + + def update_display(self): + self.updates += 1 + + +class _Manager: + """A stand-in ``self`` carrying only what display() touches.""" + + def __init__(self, games, enabled=True): + self.is_enabled = enabled + self.games_list = list(games) + self.current_game = games[0] if games else None + self.display_manager = _DisplayManager() + self.logger = _Logger() + self.sport_key = 'ncaa_mens_lacrosse' + self._games_lock = threading.Lock() + self.current_game_index = 0 + self.last_game_switch = 0.0 + self.game_display_duration = 1e9 # never switch mid-test + self.last_warning_time = 0.0 + self.warning_cooldown = 1e9 + self._last_warning_time = 0.0 + self.draws = 0 + + def _draw_scorebug_layout(self, game, force_clear=False): + self.draws += 1 + self.display_manager.update_display() + + +GAME = {'id': 'g1', 'away_abbr': 'TB', 'home_abbr': 'BOS'} + +# SportsLive has no display() of its own; it inherits SportsCore's, so covering +# SportsCore covers the live mode the bug was reported against. +CASES = [ + ('SportsCore', sports.SportsCore.display), + ('SportsUpcoming', sports.SportsUpcoming.display), + ('SportsRecent', sports.SportsRecent.display), +] + +failures = [] + + +def check(name, actual, expected): + if actual == expected: + print(" PASS %s" % name) + else: + print(" FAIL %s: expected %r, got %r" % (name, expected, actual)) + failures.append(name) + + +print("an empty mode reports no content, so the controller can skip it") +for label, display in CASES: + mgr = _Manager([]) + result = display(mgr, force_clear=False) + check("%s with no games returns False" % label, result, False) + check("%s with no games draws nothing" % label, mgr.draws, 0) + +print("\nthe same is true when the manager is disabled") +for label, display in CASES: + mgr = _Manager([GAME], enabled=False) + check("%s disabled returns False" % label, display(mgr), False) + +print("\na mode that does have a game still reports success") +for label, display in CASES: + mgr = _Manager([GAME]) + result = display(mgr, force_clear=False) + check("%s with a game returns True" % label, result, True) + check("%s with a game draws it" % label, mgr.draws, 1) + +print("\nthe result is a real bool, not something merely truthy") +# The dispatcher branches on `result is True` / `result is False`, so a truthy +# non-bool would fall through to the "assume success" path and reintroduce this. +for label, display in CASES: + check("%s empty -> bool" % label, type(display(_Manager([]))), bool) + check("%s populated -> bool" % label, + type(display(_Manager([GAME]))), bool) + +print("\n%s" % ("FAILED: %d" % len(failures) if failures else "All checks passed")) +sys.exit(1 if failures else 0) From bf0b307180cb8390a1c5c88bd8a91302abc5da69 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 9 Aug 2026 16:31:07 -0400 Subject: [PATCH 2/2] test(sports): guard the empty-mode harness behind __main__ The module-scope harness ran on import and called sys.exit(), so any collector that imports test_*.py would execute it and take the process down with it. Wrap it in main() behind an __name__ guard; running the file directly is unchanged. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Udr6MfaFLUPhX5Fgo67Jf5 --- .../test_empty_mode_signals_no_content.py | 63 ++++++++++--------- .../test_empty_mode_signals_no_content.py | 63 ++++++++++--------- 2 files changed, 68 insertions(+), 58 deletions(-) diff --git a/plugins/hockey-scoreboard/test_empty_mode_signals_no_content.py b/plugins/hockey-scoreboard/test_empty_mode_signals_no_content.py index 92e21ab..fef0453 100644 --- a/plugins/hockey-scoreboard/test_empty_mode_signals_no_content.py +++ b/plugins/hockey-scoreboard/test_empty_mode_signals_no_content.py @@ -94,32 +94,37 @@ def check(name, actual, expected): failures.append(name) -print("an empty mode reports no content, so the controller can skip it") -for label, display in CASES: - mgr = _Manager([]) - result = display(mgr, force_clear=False) - check("%s with no games returns False" % label, result, False) - check("%s with no games draws nothing" % label, mgr.draws, 0) - -print("\nthe same is true when the manager is disabled") -for label, display in CASES: - mgr = _Manager([GAME], enabled=False) - check("%s disabled returns False" % label, display(mgr), False) - -print("\na mode that does have a game still reports success") -for label, display in CASES: - mgr = _Manager([GAME]) - result = display(mgr, force_clear=False) - check("%s with a game returns True" % label, result, True) - check("%s with a game draws it" % label, mgr.draws, 1) - -print("\nthe result is a real bool, not something merely truthy") -# The dispatcher branches on `result is True` / `result is False`, so a truthy -# non-bool would fall through to the "assume success" path and reintroduce this. -for label, display in CASES: - check("%s empty -> bool" % label, type(display(_Manager([]))), bool) - check("%s populated -> bool" % label, - type(display(_Manager([GAME]))), bool) - -print("\n%s" % ("FAILED: %d" % len(failures) if failures else "All checks passed")) -sys.exit(1 if failures else 0) +def main(): + print("an empty mode reports no content, so the controller can skip it") + for label, display in CASES: + mgr = _Manager([]) + result = display(mgr, force_clear=False) + check("%s with no games returns False" % label, result, False) + check("%s with no games draws nothing" % label, mgr.draws, 0) + + print("\nthe same is true when the manager is disabled") + for label, display in CASES: + mgr = _Manager([GAME], enabled=False) + check("%s disabled returns False" % label, display(mgr), False) + + print("\na mode that does have a game still reports success") + for label, display in CASES: + mgr = _Manager([GAME]) + result = display(mgr, force_clear=False) + check("%s with a game returns True" % label, result, True) + check("%s with a game draws it" % label, mgr.draws, 1) + + print("\nthe result is a real bool, not something merely truthy") + # The dispatcher branches on `result is True` / `result is False`, so a truthy + # non-bool would fall through to the "assume success" path and reintroduce this. + for label, display in CASES: + check("%s empty -> bool" % label, type(display(_Manager([]))), bool) + check("%s populated -> bool" % label, + type(display(_Manager([GAME]))), bool) + + print("\n%s" % ("FAILED: %d" % len(failures) if failures else "All checks passed")) + sys.exit(1 if failures else 0) + + +if __name__ == "__main__": + main() diff --git a/plugins/lacrosse-scoreboard/test_empty_mode_signals_no_content.py b/plugins/lacrosse-scoreboard/test_empty_mode_signals_no_content.py index e3bbdd2..d0e86a3 100644 --- a/plugins/lacrosse-scoreboard/test_empty_mode_signals_no_content.py +++ b/plugins/lacrosse-scoreboard/test_empty_mode_signals_no_content.py @@ -94,32 +94,37 @@ def check(name, actual, expected): failures.append(name) -print("an empty mode reports no content, so the controller can skip it") -for label, display in CASES: - mgr = _Manager([]) - result = display(mgr, force_clear=False) - check("%s with no games returns False" % label, result, False) - check("%s with no games draws nothing" % label, mgr.draws, 0) - -print("\nthe same is true when the manager is disabled") -for label, display in CASES: - mgr = _Manager([GAME], enabled=False) - check("%s disabled returns False" % label, display(mgr), False) - -print("\na mode that does have a game still reports success") -for label, display in CASES: - mgr = _Manager([GAME]) - result = display(mgr, force_clear=False) - check("%s with a game returns True" % label, result, True) - check("%s with a game draws it" % label, mgr.draws, 1) - -print("\nthe result is a real bool, not something merely truthy") -# The dispatcher branches on `result is True` / `result is False`, so a truthy -# non-bool would fall through to the "assume success" path and reintroduce this. -for label, display in CASES: - check("%s empty -> bool" % label, type(display(_Manager([]))), bool) - check("%s populated -> bool" % label, - type(display(_Manager([GAME]))), bool) - -print("\n%s" % ("FAILED: %d" % len(failures) if failures else "All checks passed")) -sys.exit(1 if failures else 0) +def main(): + print("an empty mode reports no content, so the controller can skip it") + for label, display in CASES: + mgr = _Manager([]) + result = display(mgr, force_clear=False) + check("%s with no games returns False" % label, result, False) + check("%s with no games draws nothing" % label, mgr.draws, 0) + + print("\nthe same is true when the manager is disabled") + for label, display in CASES: + mgr = _Manager([GAME], enabled=False) + check("%s disabled returns False" % label, display(mgr), False) + + print("\na mode that does have a game still reports success") + for label, display in CASES: + mgr = _Manager([GAME]) + result = display(mgr, force_clear=False) + check("%s with a game returns True" % label, result, True) + check("%s with a game draws it" % label, mgr.draws, 1) + + print("\nthe result is a real bool, not something merely truthy") + # The dispatcher branches on `result is True` / `result is False`, so a truthy + # non-bool would fall through to the "assume success" path and reintroduce this. + for label, display in CASES: + check("%s empty -> bool" % label, type(display(_Manager([]))), bool) + check("%s populated -> bool" % label, + type(display(_Manager([GAME]))), bool) + + print("\n%s" % ("FAILED: %d" % len(failures) if failures else "All checks passed")) + sys.exit(1 if failures else 0) + + +if __name__ == "__main__": + main()