""" Smoke-test pattern generators with a fake driver (no hardware). Run from repo root (CPython or MicroPython): python3 led-driver/tests/pattern_smoke.py Requires only the stdlib; loads ``preset.py`` and ``patterns/*.py`` from ``led-driver/src`` via import paths (MicroPython: copy ``src`` tree to the device and run the same command if ``importlib.util`` is available, or set ``PYTHONPATH`` to ``led-driver/src`` and use ``python3`` on the host). Exit code 0 on success, 1 on any failure. """ import importlib.util import sys from pathlib import Path # ----------------------------------------------------------------------------- # Host-only ``utime`` so patterns import on CPython. # ----------------------------------------------------------------------------- _UTIME_MS = [0] def utime_advance(ms): _UTIME_MS[0] += int(ms) def _install_utime_shim(): if "utime" in sys.modules: return import types m = types.ModuleType("utime") def ticks_ms(): return _UTIME_MS[0] def ticks_diff(a, b): return a - b def ticks_add(a, b): return a + b m.ticks_ms = ticks_ms m.ticks_diff = ticks_diff m.ticks_add = ticks_add sys.modules["utime"] = m # ----------------------------------------------------------------------------- # Load ``preset`` and pattern modules from ``led-driver/src`` (no sys.path). # ----------------------------------------------------------------------------- _SRC = Path(__file__).resolve().parent.parent / "src" _TESTS = Path(__file__).resolve().parent def _load_module(name, path): spec = importlib.util.spec_from_file_location(name, path) if spec is None or spec.loader is None: raise RuntimeError("no spec for %s" % path) mod = importlib.util.module_from_spec(spec) spec.loader.exec_module(mod) return mod def _pattern_class_from_module(mod): for attr_name in dir(mod): attr = getattr(mod, attr_name) if isinstance(attr, type) and hasattr(attr, "run"): return attr return None def _load_preset_class(): preset_mod = _load_module("preset", _SRC / "preset.py") return preset_mod.Preset def _list_pattern_basenames(): pat_dir = _SRC / "patterns" out = [] for p in sorted(pat_dir.iterdir()): if p.suffix != ".py": continue if p.name in ("__init__.py", "main.py"): continue out.append(p.stem) return out def _default_preset_dict(basename): """Reasonable defaults so each pattern's ``run()`` can start.""" base = { "p": basename, "d": 100, "b": 200, "a": True, "n1": 5, "n2": 8, "n3": 5, "n4": 4, } if basename in ("rainbow", "colour_cycle"): base["c"] = [] base["n1"] = 2 elif basename == "transition": base["c"] = [(255, 0, 0), (0, 255, 0), (0, 0, 255)] base["d"] = 200 elif basename in ("chase", "circle"): base["c"] = [(255, 0, 0), (0, 0, 255)] elif basename == "pulse": base["c"] = [(0, 200, 100)] base["n1"] = 50 base["n2"] = 40 base["n3"] = 50 base["d"] = 30 elif basename in ("blink", "flicker"): base["c"] = [(255, 100, 0)] base["d"] = 80 elif basename == "flame": base["c"] = [] base["d"] = 40 base["n1"] = 40 base["n2"] = 2000 base["n3"] = -1 base["n4"] = 0 else: base["c"] = [(200, 200, 200)] return base def _run_pattern_ticks(Preset, driver, basename, steps, ms_per_tick): _install_utime_shim() mod = _load_module("patterns.%s" % basename, _SRC / "patterns" / (basename + ".py")) cls = _pattern_class_from_module(mod) if cls is None: raise RuntimeError("no pattern class in %s" % basename) preset = Preset(_default_preset_dict(basename)) gen = cls(driver).run(preset) for _ in range(steps): utime_advance(ms_per_tick) next(gen) def main(): failures = [] Preset = _load_preset_class() fake_mod = _load_module("fake_driver", _TESTS / "fake_driver.py") FakePresets = fake_mod.FakePresets for basename in _list_pattern_basenames(): d = FakePresets(16) try: _run_pattern_ticks(Preset, d, basename, steps=80, ms_per_tick=50) print("ok patterns.%s" % basename) except Exception as exc: print("FAIL patterns.%s: %r" % (basename, exc)) failures.append((basename, exc)) if failures: print("%d pattern(s) failed" % len(failures)) return 1 print("all %d pattern smoke tests passed" % len(_list_pattern_basenames())) return 0 if __name__ == "__main__": try: sys.exit(main()) except KeyboardInterrupt: sys.exit(130)