Squashed 'core/' content from commit 4957443

git-subtree-dir: core
git-subtree-split: 4957443184ae0eb6323635a90a19acffb3e01d07
This commit is contained in:
giles
2026-02-24 23:09:39 +00:00
commit cc2dcbddd4
80 changed files with 25711 additions and 0 deletions

View 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)