"""Unit tests for FFmpegExecutor utility methods in execution.py.""" from pathlib import Path import pytest from fansifter_clipper.execution import FFmpegExecutor from fansifter_clipper.models import CropKeyframe def make_kf(time, x, y, scene_change=False, width=720, height=1280): return CropKeyframe(time=time, x=x, y=y, width=width, height=height, scene_change=scene_change) class TestSplitKeyframesByScene: def setup_method(self): self.executor = FFmpegExecutor() def test_empty_keyframes(self): result = self.executor._split_keyframes_by_scene([]) assert result == [] def test_no_scene_changes(self): kfs = [make_kf(0.0, 100, 50), make_kf(1.0, 110, 60), make_kf(2.0, 120, 70)] result = self.executor._split_keyframes_by_scene(kfs) assert len(result) == 1 assert len(result[0]) == 3 def test_single_scene_change(self): kfs = [ make_kf(0.0, 100, 50), make_kf(1.0, 110, 60), make_kf(2.0, 800, 600, scene_change=True), make_kf(3.0, 810, 610), ] result = self.executor._split_keyframes_by_scene(kfs) assert len(result) == 2 assert len(result[0]) == 2 # t=0, t=1 assert len(result[1]) == 2 # t=2 (scene_change), t=3 def test_multiple_scene_changes(self): kfs = [ make_kf(0.0, 100, 50), make_kf(2.0, 800, 600, scene_change=True), make_kf(4.0, 300, 400, scene_change=True), make_kf(6.0, 310, 410), ] result = self.executor._split_keyframes_by_scene(kfs) assert len(result) == 3 def test_scene_change_on_first_keyframe_not_split(self): # scene_change=True on the very first keyframe should NOT create an empty first group kfs = [ make_kf(0.0, 100, 50, scene_change=True), make_kf(1.0, 110, 60), ] result = self.executor._split_keyframes_by_scene(kfs) assert len(result) == 1 assert len(result[0]) == 2 def test_all_keyframes_preserved(self): kfs = [ make_kf(0.0, 100, 50), make_kf(1.0, 110, 60), make_kf(2.0, 800, 600, scene_change=True), make_kf(3.0, 810, 610), make_kf(4.0, 300, 400, scene_change=True), ] result = self.executor._split_keyframes_by_scene(kfs) total_kfs = sum(len(group) for group in result) assert total_kfs == len(kfs) class TestGetOverlayOutputPath: def setup_method(self): self.executor = FFmpegExecutor() def test_appends_overlay_suffix(self): path = Path("/output/2026-01-22/clip_1_s10.0-22.0.mp4") overlay_path = self.executor._get_overlay_output_path(path) assert overlay_path.name == "clip_1_s10.0-22.0_overlay.mp4" def test_same_parent_directory(self): path = Path("/output/clips/my_clip.mp4") overlay_path = self.executor._get_overlay_output_path(path) assert overlay_path.parent == path.parent def test_preserves_extension(self): path = Path("/output/clip.mp4") overlay_path = self.executor._get_overlay_output_path(path) assert overlay_path.suffix == ".mp4" def test_stem_with_dots(self): # Filename with dots in the stem (like timestamps) path = Path("/out/clip_1_s134.0-145.0.mp4") overlay_path = self.executor._get_overlay_output_path(path) assert "_overlay" in overlay_path.name assert overlay_path.suffix == ".mp4" def test_different_extensions(self): path = Path("/out/video.mov") overlay_path = self.executor._get_overlay_output_path(path) assert overlay_path.suffix == ".mov" assert overlay_path.name == "video_overlay.mov" class TestBuildNestedIfExpression: """Test build_nested_if_expression from ffmpeg_utils.""" def test_empty_segments_returns_default(self): from fansifter_clipper.ffmpeg_utils import build_nested_if_expression result = build_nested_if_expression([], default_value=100) assert result == "100" def test_single_segment(self): from fansifter_clipper.ffmpeg_utils import build_nested_if_expression segments = [(0.0, 5.0, "interp_expr")] result = build_nested_if_expression(segments, default_value=200) assert "if(lt(t,5.0)" in result assert "interp_expr" in result assert "200" in result def test_multiple_segments_nested(self): from fansifter_clipper.ffmpeg_utils import build_nested_if_expression segments = [ (0.0, 5.0, "expr1"), (5.0, 10.0, "expr2"), ] result = build_nested_if_expression(segments, default_value=300) # Both expressions should appear assert "expr1" in result assert "expr2" in result assert "300" in result # Should be nested: two if() calls assert result.count("if(lt(t,") == 2 def test_default_value_as_float(self): from fansifter_clipper.ffmpeg_utils import build_nested_if_expression result = build_nested_if_expression([], default_value=150.7) assert result == "150" # Converted to int def test_build_order_first_to_last(self): from fansifter_clipper.ffmpeg_utils import build_nested_if_expression # First segment's time should appear before second's in expression segments = [ (0.0, 3.0, "first"), (3.0, 6.0, "second"), ] result = build_nested_if_expression(segments, default_value=0) first_pos = result.find("first") second_pos = result.find("second") assert first_pos < second_pos