`workspace/` is runtime state (per-user folders, no-auth dev's `code/`) and shouldn't be in git. The same files were previously committed under both `workspace/code/` and `src/static/bundled-demos/`, which forced a Docker `diff -q` sync check and leaked user-scoped paths into version control. - /workspace/ added to .gitignore; all previously tracked files removed via `git rm --cached`. - src/static/bundled-demos/ becomes the single source of truth: panel16 demos, led_tutorial, led_patterns, neopixel demos, and main.py move here alongside the existing canonical demos. - New BUNDLED_DEMOS_DIR config; user_workspace seeders read from it. - main.py lifespan seeds WORKSPACE_ROOT/code/ on startup so a fresh clone running `pipenv run dev` still gets the full sample set (existing files never overwritten — user edits survive restarts). - Dockerfile drops `COPY workspace` and the diff sanity check. - README/LED_TUTORIAL repointed at the new canonical paths. - test_led_patterns loads led_patterns.py from bundled-demos. - test_api uses mkdir(exist_ok=True) for `code/` (startup pre-creates). Co-authored-by: Cursor <cursoragent@cursor.com>
73 lines
2.5 KiB
Python
73 lines
2.5 KiB
Python
import importlib.util
|
|
from pathlib import Path
|
|
|
|
|
|
def _load_patterns_module():
|
|
repo_root = Path(__file__).resolve().parents[1]
|
|
# Canonical home for shipped demos — `workspace/` is gitignored.
|
|
module_path = repo_root / "src" / "static" / "bundled-demos" / "led_patterns.py"
|
|
spec = importlib.util.spec_from_file_location("led_patterns", module_path)
|
|
module = importlib.util.module_from_spec(spec)
|
|
assert spec is not None and spec.loader is not None
|
|
spec.loader.exec_module(module)
|
|
return module
|
|
|
|
|
|
def test_rainbow_frame_shape_and_bounds():
|
|
patterns = _load_patterns_module()
|
|
frame = patterns.rainbow_frame(12, 3)
|
|
assert len(frame) == 12
|
|
for color in frame:
|
|
assert len(color) == 3
|
|
assert all(0 <= c <= 255 for c in color)
|
|
|
|
|
|
def test_chase_frame_has_head_and_tail():
|
|
patterns = _load_patterns_module()
|
|
frame = patterns.chase_frame(8, 5, color=(10, 20, 30), tail=(1, 2, 3))
|
|
assert len(frame) == 8
|
|
assert frame[5] == (10, 20, 30)
|
|
assert frame[4] == (1, 2, 3)
|
|
assert sum(1 for c in frame if c != (0, 0, 0)) == 2
|
|
|
|
|
|
def test_bounce_head_pingpongs_off_ends():
|
|
patterns = _load_patterns_module()
|
|
bounce = patterns._bounce_head_index
|
|
assert bounce(5, 0) == 0 and bounce(5, 4) == 4
|
|
assert bounce(5, 5) == 3 and bounce(5, 8) == 1 and bounce(5, 16) == bounce(5, 0)
|
|
|
|
|
|
def test_scanner_bounce_has_head_and_fading_tail():
|
|
patterns = _load_patterns_module()
|
|
frame = patterns.scanner_bounce_frame(
|
|
8, 20, head_color=(80, 10, 10), tail_color=(20, 50, 90), tail_len=4
|
|
)
|
|
assert len(frame) == 8
|
|
bright = max(range(8), key=lambda i: sum(frame[i]))
|
|
assert sum(frame[bright]) >= 80
|
|
|
|
|
|
def test_knight_rider_tail_falls_off():
|
|
patterns = _load_patterns_module()
|
|
fr = patterns.knight_rider_scanner_frame(16, 55, tail_len=8, falloff_gamma=3.0)
|
|
assert len(fr) == 16
|
|
reds_sorted = sorted(fr[i][0] for i in range(16) if sum(fr[i]) > 0)
|
|
assert len(reds_sorted) >= 2
|
|
assert reds_sorted[-1] >= 200
|
|
assert reds_sorted[0] < reds_sorted[-1] * 0.5
|
|
|
|
|
|
def test_twinkle_frame_is_deterministic_for_same_inputs():
|
|
patterns = _load_patterns_module()
|
|
a = patterns.twinkle_frame(20, frame=9, seed=777, sparkles=4)
|
|
b = patterns.twinkle_frame(20, frame=9, seed=777, sparkles=4)
|
|
assert a == b
|
|
|
|
|
|
def test_twinkle_frame_varies_between_frames():
|
|
patterns = _load_patterns_module()
|
|
a = patterns.twinkle_frame(20, frame=1, seed=777, sparkles=4)
|
|
b = patterns.twinkle_frame(20, frame=2, seed=777, sparkles=4)
|
|
assert a != b
|