Files
rose-ash/artdag/sexp/test_ffmpeg_compiler.py
giles cc2dcbddd4 Squashed 'core/' content from commit 4957443
git-subtree-dir: core
git-subtree-split: 4957443184ae0eb6323635a90a19acffb3e01d07
2026-02-24 23:09:39 +00:00

147 lines
4.7 KiB
Python

"""
Tests for FFmpeg filter compilation.
Validates that each filter mapping produces valid FFmpeg commands.
"""
import subprocess
import tempfile
from pathlib import Path
from .ffmpeg_compiler import FFmpegCompiler, EFFECT_MAPPINGS
def test_filter_syntax(filter_str: str, duration: float = 0.1, is_complex: bool = False) -> tuple[bool, str]:
"""
Test if an FFmpeg filter string is valid by running it on a test pattern.
Args:
filter_str: The filter string to test
duration: Duration of test video
is_complex: If True, use -filter_complex instead of -vf
Returns (success, error_message)
"""
with tempfile.NamedTemporaryFile(suffix='.mp4', delete=False) as f:
output_path = f.name
try:
if is_complex:
# Complex filter graph needs -filter_complex and explicit output mapping
cmd = [
'ffmpeg', '-y',
'-f', 'lavfi', '-i', f'testsrc=duration={duration}:size=64x64:rate=10',
'-f', 'lavfi', '-i', f'sine=frequency=440:duration={duration}',
'-filter_complex', f'[0:v]{filter_str}[out]',
'-map', '[out]', '-map', '1:a',
'-c:v', 'libx264', '-preset', 'ultrafast',
'-c:a', 'aac',
'-t', str(duration),
output_path
]
else:
# Simple filter uses -vf
cmd = [
'ffmpeg', '-y',
'-f', 'lavfi', '-i', f'testsrc=duration={duration}:size=64x64:rate=10',
'-f', 'lavfi', '-i', f'sine=frequency=440:duration={duration}',
'-vf', filter_str,
'-c:v', 'libx264', '-preset', 'ultrafast',
'-c:a', 'aac',
'-t', str(duration),
output_path
]
result = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
if result.returncode == 0:
return True, ""
else:
# Extract relevant error
stderr = result.stderr
for line in stderr.split('\n'):
if 'Error' in line or 'error' in line or 'Invalid' in line:
return False, line.strip()
return False, stderr[-500:] if len(stderr) > 500 else stderr
except subprocess.TimeoutExpired:
return False, "Timeout"
except Exception as e:
return False, str(e)
finally:
Path(output_path).unlink(missing_ok=True)
def run_all_tests():
"""Test all effect mappings."""
compiler = FFmpegCompiler()
results = []
for effect_name, mapping in EFFECT_MAPPINGS.items():
filter_name = mapping.get("filter")
# Skip effects with no FFmpeg equivalent (external tools or python primitives)
if filter_name is None:
reason = "No FFmpeg equivalent"
if mapping.get("external_tool"):
reason = f"External tool: {mapping['external_tool']}"
elif mapping.get("python_primitive"):
reason = f"Python primitive: {mapping['python_primitive']}"
results.append((effect_name, "SKIP", reason))
continue
# Check if complex filter
is_complex = mapping.get("complex", False)
# Build filter string
if "static" in mapping:
filter_str = f"{filter_name}={mapping['static']}"
else:
filter_str = filter_name
# Test it
success, error = test_filter_syntax(filter_str, is_complex=is_complex)
if success:
results.append((effect_name, "PASS", filter_str))
else:
results.append((effect_name, "FAIL", f"{filter_str} -> {error}"))
return results
def print_results(results):
"""Print test results."""
passed = sum(1 for _, status, _ in results if status == "PASS")
failed = sum(1 for _, status, _ in results if status == "FAIL")
skipped = sum(1 for _, status, _ in results if status == "SKIP")
print(f"\n{'='*60}")
print(f"FFmpeg Filter Tests: {passed} passed, {failed} failed, {skipped} skipped")
print(f"{'='*60}\n")
# Print failures first
if failed > 0:
print("FAILURES:")
for name, status, msg in results:
if status == "FAIL":
print(f" {name}: {msg}")
print()
# Print passes
print("PASSED:")
for name, status, msg in results:
if status == "PASS":
print(f" {name}: {msg}")
# Print skips
if skipped > 0:
print("\nSKIPPED (Python fallback):")
for name, status, msg in results:
if status == "SKIP":
print(f" {name}")
if __name__ == "__main__":
results = run_all_tests()
print_results(results)