Files
portal/test/test_patterns.py
T
jimmyandCursor 3d444d1cb5 Ship the portal FastAPI controller with presets, sequences, and live audio.
Move the app under src/, add ring-spanning patterns with shared fonts and
beat-driven motion, and wire global audio settings into the web UI.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-09 00:59:03 +12:00

251 lines
7.6 KiB
Python

"""Pattern plugin smoke tests."""
from __future__ import annotations
import sys
from pathlib import Path
SRC = Path(__file__).resolve().parent.parent / "src"
if str(SRC) not in sys.path:
sys.path.insert(0, str(SRC))
from leds.panel_udp import HeadlessMatrix
from leds.pattern import get_registry, merge_params
NEW_PATTERN_IDS = (
"aurora",
"meteor",
"embers",
"tide",
"vortex",
"serpent",
"lattice",
"eclipse",
)
def test_registry_loads_bundled_patterns() -> None:
ids = get_registry().ids()
assert "rainbow" in ids
assert "scanner" in ids
for pattern_id in NEW_PATTERN_IDS:
assert pattern_id in ids, pattern_id
assert len(ids) >= 18
def test_scanner_draws_pixels() -> None:
cls = get_registry()["scanner"]
inst = cls()
params = merge_params(cls, {"colors": {"beam": "#00ff00"}})
matrix = HeadlessMatrix(20, 9, brightness=1.0)
inst.draw(matrix, 0.5, params)
assert any(matrix.rgb_bytes())
def test_ancient_text_patterns_draw() -> None:
registry = get_registry()
for pattern_id in (
"inscription",
"runes",
"glyphs",
"oracle",
"procession",
"rite",
"braziers",
"constellation",
"veil",
*NEW_PATTERN_IDS,
):
assert pattern_id in registry
cls = registry[pattern_id]
matrix = HeadlessMatrix(45, 9, brightness=1.0)
cls().draw(matrix, 1.25, merge_params(cls))
assert max(matrix.rgb_bytes()) > 40, pattern_id
def test_portal_ring_spans_panels() -> None:
from leds.array_config import PANEL_COUNT, panel_layout
from leds.portal_span import inject_portal, portal_ring_width
cls = get_registry()["procession"]
inst = cls()
params = merge_params(cls)
lit_global = set()
for index in range(PANEL_COUNT):
width, height = panel_layout(index)
matrix = HeadlessMatrix(width, height, brightness=1.0)
inject_portal(params, index)
inst.draw(matrix, 2.5, params)
rgb = matrix.rgb_bytes()
offset = params["portal"]["x_offset"]
for x in range(width):
# Middle row brightness sample.
i = ((height // 2) * width + x) * 3
if rgb[i] + rgb[i + 1] + rgb[i + 2] > 30:
lit_global.add(offset + x)
# Spanning pattern should light pixels across more than one panel's width.
assert len(lit_global) > 50
assert params["portal"]["ring_width"] == portal_ring_width()
def test_ancient_patterns_have_no_preset_bpm() -> None:
for pattern_id in (
"inscription",
"runes",
"glyphs",
"oracle",
"rite",
"veil",
"procession",
"braziers",
"constellation",
"aurora",
"vortex",
"serpent",
"lattice",
"eclipse",
"tide",
"embers",
):
names = {p["name"] for p in get_registry()[pattern_id].schema()["params"]}
assert "bpm" not in names, pattern_id
def test_resolve_bpm_uses_settings_fallback() -> None:
from leds.bpm_limits import resolve_bpm
params = merge_params(get_registry()["rite"])
params["beat"] = {"active": False, "fallback_bpm": 140}
assert resolve_bpm(params, fallback=120) == 140
params["extras"]["beat_detect"] = True
params["beat"] = {"active": True, "bpm": 128, "fallback_bpm": 140}
assert resolve_bpm(params, fallback=120) == 128
params["extras"]["beat_detect"] = False
assert resolve_bpm(params, fallback=120) == 140
def test_no_synthetic_pulse_when_beat_detect_off() -> None:
from leds.bpm_limits import resolve_beat_wave, resolve_pulse
params = merge_params(get_registry()["rite"])
assert params["extras"]["beat_detect"] is False
params["beat"] = {"active": True, "bpm": 128, "pulse": 0.9, "fallback_bpm": 120}
assert resolve_pulse(params, 0.25, fallback_bpm=120) == 0.0
assert resolve_beat_wave(params, 0.25, fallback_bpm=120) == 0.5
def test_patterns_prefer_live_beat_pulse() -> None:
from leds.bpm_limits import resolve_pulse
cls = get_registry()["rite"]
params = merge_params(cls)
params["extras"]["beat_detect"] = True
params["beat"] = {
"active": True,
"bpm": 128,
"pulse": 0.9,
"beat_seq": 12,
"kick": 0.8,
"bass": 0.6,
"energy": 0.7,
"flux": 0.2,
"hat": 0.1,
"snare": 0.3,
"level": 0.4,
}
assert resolve_pulse(params, 0.0, fallback_bpm=120) == 0.9
matrix = HeadlessMatrix(45, 9, brightness=1.0)
cls().draw(matrix, 1.0, params)
assert max(matrix.rgb_bytes()) > 40
def test_inscription_accepts_custom_text() -> None:
cls = get_registry()["inscription"]
schema = cls.schema()
text_param = next(p for p in schema["params"] if p["name"] == "text")
assert text_param["type"] == "text"
params = merge_params(cls, {"extras": {"text": "ave"}})
assert params["extras"]["text"] == "ave"
matrix = HeadlessMatrix(45, 9, brightness=1.0)
cls().draw(matrix, 0.0, params)
assert max(matrix.rgb_bytes()) > 40
def test_oracle_splits_custom_words() -> None:
from patterns.oracle import _words
assert _words("lux / nox") == ("LUX", "NOX")
cls = get_registry()["oracle"]
params = merge_params(cls, {"extras": {"text": "LUX", "beats_per_word": 3.0}})
assert params["extras"]["text"] == "LUX"
assert params["extras"]["beats_per_word"] == 3.0
matrix = HeadlessMatrix(45, 9, brightness=1.0)
cls().draw(matrix, 0.5, params)
assert max(matrix.rgb_bytes()) > 40
def test_preset_normalize_keeps_text() -> None:
from leds.presets import _normalize
preset = _normalize(
{
"name": "Hello",
"pattern": "inscription",
"brightness": 0.4,
"speed": 1.0,
"colors": {"ink": "#e0b44a", "stone": "#2a1c10"},
"extras": {"text": "HELLO"},
}
)
assert preset["extras"]["text"] == "HELLO"
def test_new_pattern_presets_normalize() -> None:
from leds.presets import _normalize
preset = _normalize(
{
"name": "Vortex",
"pattern": "vortex",
"brightness": 0.42,
"speed": 1.0,
"colors": {"core": "#6a20ff", "rim": "#f0c060", "void": "#05030a"},
"extras": {"twist": 0.75, "bg_brightness": 0.4},
}
)
assert preset["pattern"] == "vortex"
assert preset["extras"]["twist"] == 0.75
def test_text_and_background_brightness() -> None:
cls = get_registry()["inscription"]
names = {p["name"] for p in cls.schema()["params"]}
assert "text_brightness" in names
assert "bg_brightness" in names
bright = merge_params(cls, {"extras": {"text": "AVE", "text_brightness": 1.0, "bg_brightness": 0.0}})
dimmed = merge_params(cls, {"extras": {"text": "AVE", "text_brightness": 0.0, "bg_brightness": 0.0}})
a = HeadlessMatrix(45, 9, brightness=1.0)
b = HeadlessMatrix(45, 9, brightness=1.0)
cls().draw(a, 0.0, bright)
cls().draw(b, 0.0, dimmed)
assert sum(a.rgb_bytes()) > sum(b.rgb_bytes())
if __name__ == "__main__":
test_registry_loads_bundled_patterns()
test_scanner_draws_pixels()
test_ancient_text_patterns_draw()
test_portal_ring_spans_panels()
test_ancient_patterns_have_no_preset_bpm()
test_resolve_bpm_uses_settings_fallback()
test_no_synthetic_pulse_when_beat_detect_off()
test_patterns_prefer_live_beat_pulse()
test_inscription_accepts_custom_text()
test_oracle_splits_custom_words()
test_preset_normalize_keeps_text()
test_text_and_background_brightness()
test_new_pattern_presets_normalize()
print("ok")