398 lines
12 KiB
Python
398 lines
12 KiB
Python
#!/usr/bin/env python3
|
|
"""Tests for video_optimize.py"""
|
|
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
# Add parent directory to path
|
|
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
|
|
from video_optimize import VideoInfo, VideoOptimizer
|
|
|
|
|
|
class TestVideoOptimizer:
|
|
"""Test VideoOptimizer class."""
|
|
|
|
def setup_method(self):
|
|
"""Set up test fixtures."""
|
|
self.optimizer = VideoOptimizer(verbose=False, dry_run=False)
|
|
|
|
@patch("subprocess.run")
|
|
def test_check_ffmpeg_available(self, mock_run):
|
|
"""Test FFmpeg availability check."""
|
|
mock_run.return_value = MagicMock(returncode=0)
|
|
assert self.optimizer.check_ffmpeg() is True
|
|
|
|
@patch("subprocess.run")
|
|
def test_check_ffmpeg_unavailable(self, mock_run):
|
|
"""Test when FFmpeg is not available."""
|
|
mock_run.side_effect = FileNotFoundError()
|
|
assert self.optimizer.check_ffmpeg() is False
|
|
|
|
@patch("subprocess.run")
|
|
def test_get_video_info_success(self, mock_run):
|
|
"""Test successful video info extraction."""
|
|
mock_data = {
|
|
"streams": [
|
|
{
|
|
"codec_type": "video",
|
|
"codec_name": "h264",
|
|
"width": 1920,
|
|
"height": 1080,
|
|
"r_frame_rate": "30/1"
|
|
},
|
|
{
|
|
"codec_type": "audio",
|
|
"codec_name": "aac",
|
|
"bit_rate": "128000"
|
|
}
|
|
],
|
|
"format": {
|
|
"duration": "120.5",
|
|
"bit_rate": "5000000",
|
|
"size": "75000000"
|
|
}
|
|
}
|
|
|
|
mock_run.return_value = MagicMock(
|
|
stdout=json.dumps(mock_data).encode(),
|
|
returncode=0
|
|
)
|
|
|
|
info = self.optimizer.get_video_info(Path("test.mp4"))
|
|
|
|
assert info is not None
|
|
assert info.width == 1920
|
|
assert info.height == 1080
|
|
assert info.fps == 30.0
|
|
assert info.codec == "h264"
|
|
assert info.audio_codec == "aac"
|
|
|
|
@patch("subprocess.run")
|
|
def test_get_video_info_failure(self, mock_run):
|
|
"""Test video info extraction failure."""
|
|
mock_run.side_effect = Exception("ffprobe failed")
|
|
|
|
info = self.optimizer.get_video_info(Path("test.mp4"))
|
|
assert info is None
|
|
|
|
def test_calculate_target_resolution_no_constraints(self):
|
|
"""Test resolution calculation without constraints."""
|
|
width, height = self.optimizer.calculate_target_resolution(
|
|
1920, 1080, None, None
|
|
)
|
|
assert width == 1920
|
|
assert height == 1080
|
|
|
|
def test_calculate_target_resolution_width_constraint(self):
|
|
"""Test resolution calculation with width constraint."""
|
|
width, height = self.optimizer.calculate_target_resolution(
|
|
1920, 1080, 1280, None
|
|
)
|
|
assert width == 1280
|
|
assert height == 720
|
|
|
|
def test_calculate_target_resolution_height_constraint(self):
|
|
"""Test resolution calculation with height constraint."""
|
|
width, height = self.optimizer.calculate_target_resolution(
|
|
1920, 1080, None, 720
|
|
)
|
|
assert width == 1280
|
|
assert height == 720
|
|
|
|
def test_calculate_target_resolution_both_constraints(self):
|
|
"""Test resolution calculation with both constraints."""
|
|
width, height = self.optimizer.calculate_target_resolution(
|
|
1920, 1080, 1280, 720
|
|
)
|
|
assert width == 1280
|
|
assert height == 720
|
|
|
|
def test_calculate_target_resolution_even_dimensions(self):
|
|
"""Test that dimensions are always even."""
|
|
width, height = self.optimizer.calculate_target_resolution(
|
|
1920, 1080, 1279, None # Odd width
|
|
)
|
|
assert width % 2 == 0
|
|
assert height % 2 == 0
|
|
|
|
def test_calculate_target_resolution_no_upscale(self):
|
|
"""Test that small videos are not upscaled."""
|
|
width, height = self.optimizer.calculate_target_resolution(
|
|
640, 480, 1920, 1080
|
|
)
|
|
assert width == 640
|
|
assert height == 480
|
|
|
|
@patch("subprocess.run")
|
|
@patch.object(VideoOptimizer, "get_video_info")
|
|
def test_optimize_video_dry_run(self, mock_get_info, mock_run):
|
|
"""Test video optimization in dry-run mode."""
|
|
mock_info = VideoInfo(
|
|
path=Path("input.mp4"),
|
|
duration=120.0,
|
|
width=1920,
|
|
height=1080,
|
|
bitrate=5000000,
|
|
fps=30.0,
|
|
size=75000000,
|
|
codec="h264",
|
|
audio_codec="aac",
|
|
audio_bitrate=128000
|
|
)
|
|
mock_get_info.return_value = mock_info
|
|
|
|
optimizer = VideoOptimizer(dry_run=True)
|
|
result = optimizer.optimize_video(
|
|
Path("input.mp4"),
|
|
Path("output.mp4"),
|
|
max_width=1280
|
|
)
|
|
|
|
assert result is True
|
|
mock_run.assert_not_called()
|
|
|
|
@patch("subprocess.run")
|
|
@patch.object(VideoOptimizer, "get_video_info")
|
|
def test_optimize_video_resolution_reduction(self, mock_get_info, mock_run):
|
|
"""Test video optimization with resolution reduction."""
|
|
mock_info = VideoInfo(
|
|
path=Path("input.mp4"),
|
|
duration=120.0,
|
|
width=1920,
|
|
height=1080,
|
|
bitrate=5000000,
|
|
fps=30.0,
|
|
size=75000000,
|
|
codec="h264",
|
|
audio_codec="aac",
|
|
audio_bitrate=128000
|
|
)
|
|
mock_get_info.return_value = mock_info
|
|
mock_run.return_value = MagicMock(returncode=0)
|
|
|
|
result = self.optimizer.optimize_video(
|
|
Path("input.mp4"),
|
|
Path("output.mp4"),
|
|
max_width=1280,
|
|
max_height=720
|
|
)
|
|
|
|
assert result is True
|
|
mock_run.assert_called_once()
|
|
|
|
# Check that scale filter is applied
|
|
cmd = mock_run.call_args[0][0]
|
|
assert "-vf" in cmd
|
|
filter_idx = cmd.index("-vf")
|
|
assert "scale=1280:720" in cmd[filter_idx + 1]
|
|
|
|
@patch("subprocess.run")
|
|
@patch.object(VideoOptimizer, "get_video_info")
|
|
def test_optimize_video_fps_reduction(self, mock_get_info, mock_run):
|
|
"""Test video optimization with FPS reduction."""
|
|
mock_info = VideoInfo(
|
|
path=Path("input.mp4"),
|
|
duration=120.0,
|
|
width=1920,
|
|
height=1080,
|
|
bitrate=5000000,
|
|
fps=60.0,
|
|
size=75000000,
|
|
codec="h264",
|
|
audio_codec="aac",
|
|
audio_bitrate=128000
|
|
)
|
|
mock_get_info.return_value = mock_info
|
|
mock_run.return_value = MagicMock(returncode=0)
|
|
|
|
result = self.optimizer.optimize_video(
|
|
Path("input.mp4"),
|
|
Path("output.mp4"),
|
|
target_fps=30.0
|
|
)
|
|
|
|
assert result is True
|
|
|
|
# Check that FPS filter is applied
|
|
cmd = mock_run.call_args[0][0]
|
|
assert "-r" in cmd
|
|
fps_idx = cmd.index("-r")
|
|
assert "30.0" in cmd[fps_idx + 1]
|
|
|
|
@patch("subprocess.run")
|
|
@patch.object(VideoOptimizer, "get_video_info")
|
|
def test_optimize_video_two_pass(self, mock_get_info, mock_run):
|
|
"""Test two-pass encoding."""
|
|
mock_info = VideoInfo(
|
|
path=Path("input.mp4"),
|
|
duration=120.0,
|
|
width=1920,
|
|
height=1080,
|
|
bitrate=5000000,
|
|
fps=30.0,
|
|
size=75000000,
|
|
codec="h264",
|
|
audio_codec="aac",
|
|
audio_bitrate=128000
|
|
)
|
|
mock_get_info.return_value = mock_info
|
|
mock_run.return_value = MagicMock(returncode=0)
|
|
|
|
result = self.optimizer.optimize_video(
|
|
Path("input.mp4"),
|
|
Path("output.mp4"),
|
|
two_pass=True
|
|
)
|
|
|
|
assert result is True
|
|
# Should be called twice (pass 1 and pass 2)
|
|
assert mock_run.call_count == 2
|
|
|
|
# Check pass 1 command
|
|
pass1_cmd = mock_run.call_args_list[0][0][0]
|
|
assert "-pass" in pass1_cmd
|
|
assert "1" in pass1_cmd
|
|
|
|
# Check pass 2 command
|
|
pass2_cmd = mock_run.call_args_list[1][0][0]
|
|
assert "-pass" in pass2_cmd
|
|
assert "2" in pass2_cmd
|
|
|
|
@patch("subprocess.run")
|
|
@patch.object(VideoOptimizer, "get_video_info")
|
|
def test_optimize_video_crf_encoding(self, mock_get_info, mock_run):
|
|
"""Test CRF-based encoding (single pass)."""
|
|
mock_info = VideoInfo(
|
|
path=Path("input.mp4"),
|
|
duration=120.0,
|
|
width=1920,
|
|
height=1080,
|
|
bitrate=5000000,
|
|
fps=30.0,
|
|
size=75000000,
|
|
codec="h264",
|
|
audio_codec="aac",
|
|
audio_bitrate=128000
|
|
)
|
|
mock_get_info.return_value = mock_info
|
|
mock_run.return_value = MagicMock(returncode=0)
|
|
|
|
result = self.optimizer.optimize_video(
|
|
Path("input.mp4"),
|
|
Path("output.mp4"),
|
|
crf=23,
|
|
two_pass=False
|
|
)
|
|
|
|
assert result is True
|
|
mock_run.assert_called_once()
|
|
|
|
# Check CRF parameter
|
|
cmd = mock_run.call_args[0][0]
|
|
assert "-crf" in cmd
|
|
crf_idx = cmd.index("-crf")
|
|
assert "23" in cmd[crf_idx + 1]
|
|
|
|
@patch("subprocess.run")
|
|
@patch.object(VideoOptimizer, "get_video_info")
|
|
def test_optimize_video_failure(self, mock_get_info, mock_run):
|
|
"""Test optimization failure handling."""
|
|
mock_info = VideoInfo(
|
|
path=Path("input.mp4"),
|
|
duration=120.0,
|
|
width=1920,
|
|
height=1080,
|
|
bitrate=5000000,
|
|
fps=30.0,
|
|
size=75000000,
|
|
codec="h264",
|
|
audio_codec="aac",
|
|
audio_bitrate=128000
|
|
)
|
|
mock_get_info.return_value = mock_info
|
|
mock_run.side_effect = Exception("FFmpeg failed")
|
|
|
|
result = self.optimizer.optimize_video(
|
|
Path("input.mp4"),
|
|
Path("output.mp4")
|
|
)
|
|
|
|
assert result is False
|
|
|
|
|
|
class TestVideoInfo:
|
|
"""Test VideoInfo dataclass."""
|
|
|
|
def test_video_info_creation(self):
|
|
"""Test creating VideoInfo object."""
|
|
info = VideoInfo(
|
|
path=Path("test.mp4"),
|
|
duration=120.5,
|
|
width=1920,
|
|
height=1080,
|
|
bitrate=5000000,
|
|
fps=30.0,
|
|
size=75000000,
|
|
codec="h264",
|
|
audio_codec="aac",
|
|
audio_bitrate=128000
|
|
)
|
|
|
|
assert info.width == 1920
|
|
assert info.height == 1080
|
|
assert info.fps == 30.0
|
|
assert info.codec == "h264"
|
|
|
|
|
|
class TestCompareVideos:
|
|
"""Test video comparison functionality."""
|
|
|
|
@patch.object(VideoOptimizer, "get_video_info")
|
|
def test_compare_videos_success(self, mock_get_info, capsys):
|
|
"""Test video comparison output."""
|
|
orig_info = VideoInfo(
|
|
path=Path("original.mp4"),
|
|
duration=120.0,
|
|
width=1920,
|
|
height=1080,
|
|
bitrate=5000000,
|
|
fps=30.0,
|
|
size=75000000,
|
|
codec="h264",
|
|
audio_codec="aac",
|
|
audio_bitrate=128000
|
|
)
|
|
|
|
opt_info = VideoInfo(
|
|
path=Path("optimized.mp4"),
|
|
duration=120.0,
|
|
width=1280,
|
|
height=720,
|
|
bitrate=2500000,
|
|
fps=30.0,
|
|
size=37500000,
|
|
codec="h264",
|
|
audio_codec="aac",
|
|
audio_bitrate=128000
|
|
)
|
|
|
|
mock_get_info.side_effect = [orig_info, opt_info]
|
|
|
|
optimizer = VideoOptimizer()
|
|
optimizer.compare_videos(Path("original.mp4"), Path("optimized.mp4"))
|
|
|
|
captured = capsys.readouterr()
|
|
assert "Resolution" in captured.out
|
|
assert "1920x1080" in captured.out
|
|
assert "1280x720" in captured.out
|
|
assert "50.0%" in captured.out # Size reduction
|
|
|
|
|
|
if __name__ == "__main__":
|
|
pytest.main([__file__, "-v"])
|