#!/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()