"""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")