Squashed 'core/' content from commit 4957443
git-subtree-dir: core git-subtree-split: 4957443184ae0eb6323635a90a19acffb3e01d07
This commit is contained in:
146
artdag/sexp/test_ffmpeg_compiler.py
Normal file
146
artdag/sexp/test_ffmpeg_compiler.py
Normal file
@@ -0,0 +1,146 @@
|
||||
"""
|
||||
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)
|
||||
Reference in New Issue
Block a user