Files
2025-11-30 08:48:52 +08:00

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"])