Replace batch DAG system with streaming architecture
- Remove legacy_tasks.py, hybrid_state.py, render.py - Remove old task modules (analyze, execute, execute_sexp, orchestrate) - Add streaming interpreter from test repo - Add sexp_effects with primitives and video effects - Add streaming Celery task with CID-based asset resolution - Support both CID and friendly name references for assets - Add .dockerignore to prevent local clones from conflicting Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
236
sexp_effects/test_interpreter.py
Normal file
236
sexp_effects/test_interpreter.py
Normal file
@@ -0,0 +1,236 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test the S-expression effect interpreter.
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Add parent to path
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
from sexp_effects import (
|
||||
get_interpreter,
|
||||
load_effects_dir,
|
||||
run_effect,
|
||||
list_effects,
|
||||
parse,
|
||||
)
|
||||
|
||||
|
||||
def test_parser():
|
||||
"""Test S-expression parser."""
|
||||
print("Testing parser...")
|
||||
|
||||
# Simple expressions
|
||||
assert parse("42") == 42
|
||||
assert parse("3.14") == 3.14
|
||||
assert parse('"hello"') == "hello"
|
||||
assert parse("true") == True
|
||||
|
||||
# Lists
|
||||
assert parse("(+ 1 2)")[0].name == "+"
|
||||
assert parse("(+ 1 2)")[1] == 1
|
||||
|
||||
# Nested
|
||||
expr = parse("(define x (+ 1 2))")
|
||||
assert expr[0].name == "define"
|
||||
|
||||
print(" Parser OK")
|
||||
|
||||
|
||||
def test_interpreter_basics():
|
||||
"""Test basic interpreter operations."""
|
||||
print("Testing interpreter basics...")
|
||||
|
||||
interp = get_interpreter()
|
||||
|
||||
# Math
|
||||
assert interp.eval(parse("(+ 1 2)")) == 3
|
||||
assert interp.eval(parse("(* 3 4)")) == 12
|
||||
assert interp.eval(parse("(- 10 3)")) == 7
|
||||
|
||||
# Comparison
|
||||
assert interp.eval(parse("(< 1 2)")) == True
|
||||
assert interp.eval(parse("(> 1 2)")) == False
|
||||
|
||||
# Let binding
|
||||
assert interp.eval(parse("(let ((x 5)) x)")) == 5
|
||||
assert interp.eval(parse("(let ((x 5) (y 3)) (+ x y))")) == 8
|
||||
|
||||
# Lambda
|
||||
result = interp.eval(parse("((lambda (x) (* x 2)) 5)"))
|
||||
assert result == 10
|
||||
|
||||
# If
|
||||
assert interp.eval(parse("(if true 1 2)")) == 1
|
||||
assert interp.eval(parse("(if false 1 2)")) == 2
|
||||
|
||||
print(" Interpreter basics OK")
|
||||
|
||||
|
||||
def test_primitives():
|
||||
"""Test image primitives."""
|
||||
print("Testing primitives...")
|
||||
|
||||
interp = get_interpreter()
|
||||
|
||||
# Create test image
|
||||
img = np.zeros((100, 100, 3), dtype=np.uint8)
|
||||
img[50, 50] = [255, 128, 64]
|
||||
|
||||
interp.global_env.set('test_img', img)
|
||||
|
||||
# Width/height
|
||||
assert interp.eval(parse("(width test_img)")) == 100
|
||||
assert interp.eval(parse("(height test_img)")) == 100
|
||||
|
||||
# Pixel
|
||||
pixel = interp.eval(parse("(pixel test_img 50 50)"))
|
||||
assert pixel == [255, 128, 64]
|
||||
|
||||
# RGB
|
||||
color = interp.eval(parse("(rgb 100 150 200)"))
|
||||
assert color == [100, 150, 200]
|
||||
|
||||
# Luminance
|
||||
lum = interp.eval(parse("(luminance (rgb 100 100 100))"))
|
||||
assert abs(lum - 100) < 1
|
||||
|
||||
print(" Primitives OK")
|
||||
|
||||
|
||||
def test_effect_loading():
|
||||
"""Test loading effects from .sexp files."""
|
||||
print("Testing effect loading...")
|
||||
|
||||
# Load all effects
|
||||
effects_dir = Path(__file__).parent / "effects"
|
||||
load_effects_dir(str(effects_dir))
|
||||
|
||||
effects = list_effects()
|
||||
print(f" Loaded {len(effects)} effects: {', '.join(sorted(effects))}")
|
||||
|
||||
assert len(effects) > 0
|
||||
print(" Effect loading OK")
|
||||
|
||||
|
||||
def test_effect_execution():
|
||||
"""Test running effects on images."""
|
||||
print("Testing effect execution...")
|
||||
|
||||
# Create test image
|
||||
img = np.random.randint(0, 255, (100, 100, 3), dtype=np.uint8)
|
||||
|
||||
# Load effects
|
||||
effects_dir = Path(__file__).parent / "effects"
|
||||
load_effects_dir(str(effects_dir))
|
||||
|
||||
# Test each effect
|
||||
effects = list_effects()
|
||||
passed = 0
|
||||
failed = []
|
||||
|
||||
for name in sorted(effects):
|
||||
try:
|
||||
result, state = run_effect(name, img.copy(), {'_time': 0.5}, {})
|
||||
assert isinstance(result, np.ndarray)
|
||||
assert result.shape == img.shape
|
||||
passed += 1
|
||||
print(f" {name}: OK")
|
||||
except Exception as e:
|
||||
failed.append((name, str(e)))
|
||||
print(f" {name}: FAILED - {e}")
|
||||
|
||||
print(f" Passed: {passed}/{len(effects)}")
|
||||
if failed:
|
||||
print(f" Failed: {[f[0] for f in failed]}")
|
||||
|
||||
return passed, failed
|
||||
|
||||
|
||||
def test_ascii_fx_zone():
|
||||
"""Test ascii_fx_zone effect with zone expressions."""
|
||||
print("Testing ascii_fx_zone...")
|
||||
|
||||
interp = get_interpreter()
|
||||
|
||||
# Load the effect
|
||||
effects_dir = Path(__file__).parent / "effects"
|
||||
load_effects_dir(str(effects_dir))
|
||||
|
||||
# Create gradient test frame
|
||||
frame = np.zeros((120, 160, 3), dtype=np.uint8)
|
||||
for x in range(160):
|
||||
frame[:, x] = int(x / 160 * 255)
|
||||
frame = np.stack([frame[:,:,0]]*3, axis=2)
|
||||
|
||||
# Test 1: Basic without expressions
|
||||
result, _ = run_effect('ascii_fx_zone', frame, {'cols': 20}, {})
|
||||
assert result.shape == frame.shape
|
||||
print(" Basic run: OK")
|
||||
|
||||
# Test 2: With zone-lum expression
|
||||
expr = parse('(* zone-lum 180)')
|
||||
result, _ = run_effect('ascii_fx_zone', frame, {
|
||||
'cols': 20,
|
||||
'char_hue': expr
|
||||
}, {})
|
||||
assert result.shape == frame.shape
|
||||
print(" Zone-lum expression: OK")
|
||||
|
||||
# Test 3: With multiple expressions
|
||||
scale_expr = parse('(+ 0.5 (* zone-lum 0.5))')
|
||||
rot_expr = parse('(* zone-row-norm 30)')
|
||||
result, _ = run_effect('ascii_fx_zone', frame, {
|
||||
'cols': 20,
|
||||
'char_scale': scale_expr,
|
||||
'char_rotation': rot_expr
|
||||
}, {})
|
||||
assert result.shape == frame.shape
|
||||
print(" Multiple expressions: OK")
|
||||
|
||||
# Test 4: With numeric literals
|
||||
result, _ = run_effect('ascii_fx_zone', frame, {
|
||||
'cols': 20,
|
||||
'char_hue': 90,
|
||||
'char_scale': 1.2
|
||||
}, {})
|
||||
assert result.shape == frame.shape
|
||||
print(" Numeric literals: OK")
|
||||
|
||||
# Test 5: Zone position expressions
|
||||
col_expr = parse('(* zone-col-norm 360)')
|
||||
result, _ = run_effect('ascii_fx_zone', frame, {
|
||||
'cols': 20,
|
||||
'char_hue': col_expr
|
||||
}, {})
|
||||
assert result.shape == frame.shape
|
||||
print(" Zone position expression: OK")
|
||||
|
||||
print(" ascii_fx_zone OK")
|
||||
|
||||
|
||||
def main():
|
||||
print("=" * 60)
|
||||
print("S-Expression Effect Interpreter Tests")
|
||||
print("=" * 60)
|
||||
|
||||
test_parser()
|
||||
test_interpreter_basics()
|
||||
test_primitives()
|
||||
test_effect_loading()
|
||||
test_ascii_fx_zone()
|
||||
passed, failed = test_effect_execution()
|
||||
|
||||
print("=" * 60)
|
||||
if not failed:
|
||||
print("All tests passed!")
|
||||
else:
|
||||
print(f"Tests completed with {len(failed)} failures")
|
||||
print("=" * 60)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user