mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-17 20:39:13 +00:00
test(harness): keep the visual double's signature tied to production
Moves set_scrolling_state's frame_hold into the test double here, where DisplayManager gains it, rather than in #534 where it arrived a PR early. CodeRabbit flagged the #534 version correctly: a double that accepts an argument production does not lets the call pass every harness run and raise TypeError on the panel, which is the one failure a safety harness exists to prevent. The drift has now gone both ways across two branches -- double behind production on this branch, double ahead of it on #534 -- so it is pinned instead of remembered. test_display_double_parity.py compares the two signatures and fails with the direction of the drift named. It reads the files with ast rather than importing them, because display_manager imports rgbmatrix at module scope and this check should hold on a laptop and in CI as well as on a Pi. Plugins begin passing frame_hold in ledmatrix-plugins#462, which is why production and the double both need it before that lands. Full suite: 3889 passed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014RRtqXDCnvnY6EQwhT5CV9
This commit is contained in:
co-authored by
Claude Opus 5
parent
571cc6fdac
commit
c17c58003c
@@ -503,9 +503,18 @@ class VisualTestDisplayManager:
|
|||||||
# Scrolling state (no-op interface compat)
|
# Scrolling state (no-op interface compat)
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
def set_scrolling_state(self, is_scrolling: bool):
|
def set_scrolling_state(self, is_scrolling: bool, frame_hold: int = 1):
|
||||||
"""Set the current scrolling state (no-op for testing)."""
|
"""Set the current scrolling state (no-op for testing).
|
||||||
|
|
||||||
|
``frame_hold`` mirrors the DisplayManager signature this change adds.
|
||||||
|
The two are kept in step deliberately: a double that accepts arguments
|
||||||
|
production does not lets a call pass every harness run and then raise
|
||||||
|
TypeError on the panel, and a double that lacks one production has
|
||||||
|
fails every render of a plugin that legitimately paces its scroll.
|
||||||
|
Plugins begin passing it in ledmatrix-plugins#462.
|
||||||
|
"""
|
||||||
self._scrolling_state['is_scrolling'] = is_scrolling
|
self._scrolling_state['is_scrolling'] = is_scrolling
|
||||||
|
self._scrolling_state['frame_hold'] = frame_hold
|
||||||
if is_scrolling:
|
if is_scrolling:
|
||||||
self._scrolling_state['last_scroll_activity'] = time.time()
|
self._scrolling_state['last_scroll_activity'] = time.time()
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
"""The visual-test double must not drift from the real DisplayManager.
|
||||||
|
|
||||||
|
Both directions of drift are silent and both are damaging:
|
||||||
|
|
||||||
|
* the double accepts an argument production does not -- the call passes
|
||||||
|
every harness run and raises TypeError on the panel, which is precisely
|
||||||
|
the failure a safety harness exists to prevent;
|
||||||
|
* the double lacks an argument production has -- every plugin that
|
||||||
|
legitimately uses it fails every render, and the harness blames the
|
||||||
|
plugin.
|
||||||
|
|
||||||
|
`set_scrolling_state` has been each of those in turn across two branches, so
|
||||||
|
the parity is asserted rather than remembered.
|
||||||
|
|
||||||
|
Read with ast rather than imported: src/display_manager.py imports rgbmatrix
|
||||||
|
at module scope, which is absent anywhere without the panel library, and this
|
||||||
|
check should hold on a laptop and in CI as well as on a Pi.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import ast
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parent.parent
|
||||||
|
REAL = ROOT / "src" / "display_manager.py"
|
||||||
|
DOUBLE = ROOT / "src" / "plugin_system" / "testing" / "visual_display_manager.py"
|
||||||
|
|
||||||
|
#: Methods a plugin calls on whichever manager it is handed.
|
||||||
|
SHARED_METHODS = ["set_scrolling_state", "is_currently_scrolling"]
|
||||||
|
|
||||||
|
|
||||||
|
def _signature(path: Path, class_hint: str, method: str):
|
||||||
|
"""(name, default-repr) pairs for `method`, or None if it is absent."""
|
||||||
|
tree = ast.parse(path.read_text(encoding="utf-8"))
|
||||||
|
for node in ast.walk(tree):
|
||||||
|
if not isinstance(node, ast.ClassDef) or class_hint not in node.name:
|
||||||
|
continue
|
||||||
|
for item in node.body:
|
||||||
|
if isinstance(item, (ast.FunctionDef, ast.AsyncFunctionDef)) and item.name == method:
|
||||||
|
args = [a.arg for a in item.args.args if a.arg != "self"]
|
||||||
|
pad = [None] * (len(args) - len(item.args.defaults))
|
||||||
|
defaults = pad + [ast.unparse(d) for d in item.args.defaults]
|
||||||
|
return list(zip(args, defaults))
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("method", SHARED_METHODS)
|
||||||
|
def test_the_double_matches_production(method):
|
||||||
|
real = _signature(REAL, "DisplayManager", method)
|
||||||
|
double = _signature(DOUBLE, "DisplayManager", method)
|
||||||
|
|
||||||
|
assert real is not None, f"DisplayManager lost {method}"
|
||||||
|
assert double is not None, \
|
||||||
|
f"the test double is missing {method}, so every plugin using it fails to render"
|
||||||
|
assert double == real, (
|
||||||
|
f"{method} has drifted: production takes {real}, the double takes {double}. "
|
||||||
|
"A double that is more permissive hides a production TypeError; one that "
|
||||||
|
"is less permissive fails plugins that are actually correct."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_frame_hold_is_accepted_by_both():
|
||||||
|
"""The specific argument that has drifted twice."""
|
||||||
|
for path, label in ((REAL, "DisplayManager"), (DOUBLE, "VisualTestDisplayManager")):
|
||||||
|
params = dict(_signature(path, "DisplayManager", "set_scrolling_state") or [])
|
||||||
|
assert "frame_hold" in params, f"{label} does not accept frame_hold"
|
||||||
|
assert params["frame_hold"] == "1", \
|
||||||
|
f"{label} must default frame_hold to 1 so existing callers are unaffected"
|
||||||
Reference in New Issue
Block a user