"""Unit tests for FFmpeg utilities.""" from fansifter_clipper.ffmpeg_utils import build_crop_expression from fansifter_clipper.models import CropKeyframe def test_crop_expression_format_single_keyframe(): """Test static crop expression format.""" kf = CropKeyframe(time=0.0, x=100, y=50, width=720, height=1280) expr = build_crop_expression([kf]) # Static values should be unquoted numbers assert expr == "crop=w=720:h=1280:x=100:y=50" def test_crop_expression_format_multiple_keyframes(): """Test dynamic crop expression format (no quotes, escaped commas).""" kf1 = CropKeyframe(time=0.0, x=100, y=50, width=720, height=1280) kf2 = CropKeyframe(time=2.0, x=200, y=150, width=720, height=1280) expr = build_crop_expression([kf1, kf2]) # Dynamic expressions should NOT be quoted assert "x='" not in expr, "x expression must not be quoted" assert "y='" not in expr, "y expression must not be quoted" # Should contain unquoted if expressions assert "x=if" in expr, "x should have unquoted if expression" assert "y=if" in expr, "y should have unquoted if expression" # Should contain proper FFmpeg expression syntax with escaped commas assert "lt(t\\," in expr, "Should have escaped commas in expressions" assert "crop=w=720:h=1280:" in expr, "Should have crop dimensions" # Should NOT have unescaped commas (except in dimension parameters) # Count escaped vs unescaped commas expr_without_dimensions = expr.split("x=")[1] # Skip w=720:h=1280 assert "\\," in expr_without_dimensions, "Should have escaped commas" def test_crop_expression_interpolation(): """Test that interpolation expressions are correctly formatted.""" kf1 = CropKeyframe(time=10.0, x=100, y=50, width=720, height=1280) kf2 = CropKeyframe(time=12.0, x=200, y=150, width=720, height=1280) expr = build_crop_expression([kf1, kf2]) # Time should be offset-adjusted (10.0 - 10.0 = 0.0) assert "t-0.0" in expr or "t-0" in expr, "Should use time offset" # Should interpolate from kf1 to kf2 positions assert "100+" in expr or "100 +" in expr, "Should start from x=100" assert "200-100" in expr, "Should interpolate to x=200" def test_crop_expression_ffmpeg_escape_characters(): """Verify expressions have proper FFmpeg filtergraph escaping.""" kf1 = CropKeyframe(time=0.0, x=100, y=50, width=720, height=1280) kf2 = CropKeyframe(time=2.0, x=200, y=150, width=720, height=1280) expr = build_crop_expression([kf1, kf2]) # Should contain unescaped parentheses (FFmpeg doesn't need them escaped) assert "(" in expr, "Should contain unescaped parentheses" assert ")" in expr, "Should contain unescaped parentheses" # Should have ESCAPED commas for FFmpeg filtergraph parser assert "\\," in expr, "Should have escaped commas for FFmpeg" # Parentheses should NOT be escaped (FFmpeg doesn't require it) assert "\\(" not in expr, "Should not have escaped parentheses" assert "\\)" not in expr, "Should not have escaped parentheses" def test_crop_expression_three_keyframes(): """Test nested if expressions with three keyframes.""" kf1 = CropKeyframe(time=0.0, x=100, y=50, width=720, height=1280) kf2 = CropKeyframe(time=5.0, x=200, y=150, width=720, height=1280) kf3 = CropKeyframe(time=10.0, x=300, y=250, width=720, height=1280) expr = build_crop_expression([kf1, kf2, kf3]) # Should have nested if expressions (2 segments = 2 ifs per axis = 4 total) # Note: commas are escaped, so search for escaped version if_count = expr.count("if(lt(t\\,") expected = 4 # 2 segments × 2 axes (x and y) assert if_count == expected, f"Expected {expected} if expressions, got {if_count}" # Should NOT have quotes around expressions assert "x='" not in expr, "x expression must not be quoted" assert "y='" not in expr, "y expression must not be quoted" def test_crop_expression_unsorted_keyframes(): """Test that keyframes are automatically sorted by time.""" kf1 = CropKeyframe(time=10.0, x=100, y=50, width=720, height=1280) kf2 = CropKeyframe(time=5.0, x=200, y=150, width=720, height=1280) kf3 = CropKeyframe(time=15.0, x=300, y=250, width=720, height=1280) # Pass unsorted keyframes expr = build_crop_expression([kf1, kf2, kf3]) # Should still generate valid expression (sorted internally) assert "crop=w=720:h=1280:" in expr # Commas are escaped in the expression assert "if(lt(t\\," in expr # Should NOT have quotes assert "x='" not in expr assert "y='" not in expr def test_crop_expression_no_colon_escaping(): """Test that colons are NOT escaped in expressions (critical bug fix test).""" kf1 = CropKeyframe(time=0.0, x=100, y=50, width=720, height=1280) kf2 = CropKeyframe(time=2.0, x=200, y=150, width=720, height=1280) expr = build_crop_expression([kf1, kf2]) # Colons should NOT be escaped with backslashes # The only colons should be parameter separators (crop=w=720:h=1280:x=...:y=...) assert "\\:" not in expr, "Colons should NOT be escaped in the expression" # Should have unescaped colons as parameter separators assert ":h=" in expr, "Should have unescaped colon before height parameter" assert ":x=" in expr, "Should have unescaped colon before x parameter" assert ":y=" in expr, "Should have unescaped colon before y parameter" def test_crop_expression_comma_escaping_only(): """Test that ONLY commas are escaped, not colons.""" kf1 = CropKeyframe(time=0.0, x=100, y=50, width=720, height=1280) kf2 = CropKeyframe(time=2.0, x=200, y=150, width=720, height=1280) expr = build_crop_expression([kf1, kf2]) # Commas in if() expressions SHOULD be escaped assert "\\," in expr, "Commas should be escaped in the expression" # Colons should NOT be escaped assert "\\:" not in expr, "Colons should NOT be escaped" # Expression should be properly formatted assert expr.startswith("crop=w="), "Should start with crop=w=" parts = expr.split(":") assert len(parts) == 4, "Should have 4 parts separated by colons (w, h, x, y)"