Initial commit

This commit is contained in:
Zhongwei Li
2025-11-30 08:48:52 +08:00
commit 6ec3196ecc
434 changed files with 125248 additions and 0 deletions

View File

@@ -0,0 +1,2 @@
pytest>=7.4.0
pytest-cov>=4.1.0

View File

@@ -0,0 +1,372 @@
#!/usr/bin/env python3
"""Tests for batch_resize.py"""
import sys
from pathlib import Path
from unittest.mock import MagicMock, call, patch
import pytest
# Add parent directory to path
sys.path.insert(0, str(Path(__file__).parent.parent))
from batch_resize import ImageResizer, collect_images
class TestImageResizer:
"""Test ImageResizer class."""
def setup_method(self):
"""Set up test fixtures."""
self.resizer = ImageResizer(verbose=False, dry_run=False)
@patch("subprocess.run")
def test_check_imagemagick_available(self, mock_run):
"""Test ImageMagick availability check."""
mock_run.return_value = MagicMock(returncode=0)
assert self.resizer.check_imagemagick() is True
@patch("subprocess.run")
def test_check_imagemagick_unavailable(self, mock_run):
"""Test when ImageMagick is not available."""
mock_run.side_effect = FileNotFoundError()
assert self.resizer.check_imagemagick() is False
def test_build_resize_command_fit_strategy(self):
"""Test command building for 'fit' strategy."""
cmd = self.resizer.build_resize_command(
Path("input.jpg"),
Path("output.jpg"),
width=800,
height=600,
strategy="fit",
quality=85
)
assert "magick" in cmd
assert str(Path("input.jpg")) in cmd
assert "-resize" in cmd
assert "800x600" in cmd
assert "-quality" in cmd
assert "85" in cmd
assert "-strip" in cmd
def test_build_resize_command_fill_strategy(self):
"""Test command building for 'fill' strategy."""
cmd = self.resizer.build_resize_command(
Path("input.jpg"),
Path("output.jpg"),
width=800,
height=600,
strategy="fill",
quality=85
)
assert "-resize" in cmd
assert "800x600^" in cmd
assert "-gravity" in cmd
assert "center" in cmd
assert "-extent" in cmd
def test_build_resize_command_thumbnail_strategy(self):
"""Test command building for 'thumbnail' strategy."""
cmd = self.resizer.build_resize_command(
Path("input.jpg"),
Path("output.jpg"),
width=200,
height=None,
strategy="thumbnail",
quality=85
)
assert "200x200^" in cmd
assert "-gravity" in cmd
assert "center" in cmd
def test_build_resize_command_with_watermark(self):
"""Test command building with watermark."""
watermark = Path("watermark.png")
cmd = self.resizer.build_resize_command(
Path("input.jpg"),
Path("output.jpg"),
width=800,
height=None,
strategy="fit",
quality=85,
watermark=watermark
)
assert str(watermark) in cmd
assert "-gravity" in cmd
assert "southeast" in cmd
assert "-composite" in cmd
def test_build_resize_command_exact_strategy(self):
"""Test command building for 'exact' strategy."""
cmd = self.resizer.build_resize_command(
Path("input.jpg"),
Path("output.jpg"),
width=800,
height=600,
strategy="exact",
quality=85
)
assert "800x600!" in cmd
def test_build_resize_command_fill_requires_dimensions(self):
"""Test that 'fill' strategy requires both dimensions."""
with pytest.raises(ValueError):
self.resizer.build_resize_command(
Path("input.jpg"),
Path("output.jpg"),
width=800,
height=None,
strategy="fill",
quality=85
)
@patch("subprocess.run")
def test_resize_image_success(self, mock_run):
"""Test successful image resize."""
mock_run.return_value = MagicMock(returncode=0)
result = self.resizer.resize_image(
Path("input.jpg"),
Path("output/output.jpg"),
width=800,
height=None,
strategy="fit",
quality=85
)
assert result is True
mock_run.assert_called_once()
@patch("subprocess.run")
def test_resize_image_dry_run(self, mock_run):
"""Test resize in dry-run mode."""
resizer = ImageResizer(dry_run=True)
result = resizer.resize_image(
Path("input.jpg"),
Path("output.jpg"),
width=800,
height=None
)
assert result is True
mock_run.assert_not_called()
@patch("subprocess.run")
def test_resize_image_failure(self, mock_run):
"""Test resize failure handling."""
mock_run.side_effect = Exception("Resize failed")
result = self.resizer.resize_image(
Path("input.jpg"),
Path("output.jpg"),
width=800,
height=None
)
assert result is False
class TestCollectImages:
"""Test image collection functionality."""
def test_collect_images_from_file(self, tmp_path):
"""Test collecting a single image file."""
img_file = tmp_path / "test.jpg"
img_file.touch()
images = collect_images([img_file])
assert len(images) == 1
assert images[0] == img_file
def test_collect_images_from_directory(self, tmp_path):
"""Test collecting images from directory."""
(tmp_path / "image1.jpg").touch()
(tmp_path / "image2.png").touch()
(tmp_path / "text.txt").touch()
images = collect_images([tmp_path])
assert len(images) == 2
assert all(img.suffix.lower() in {'.jpg', '.png'} for img in images)
def test_collect_images_recursive(self, tmp_path):
"""Test recursive image collection."""
subdir = tmp_path / "subdir"
subdir.mkdir()
(tmp_path / "image1.jpg").touch()
(subdir / "image2.jpg").touch()
images = collect_images([tmp_path], recursive=True)
assert len(images) == 2
images_non_recursive = collect_images([tmp_path], recursive=False)
assert len(images_non_recursive) == 1
def test_collect_images_filters_extensions(self, tmp_path):
"""Test that only image files are collected."""
(tmp_path / "image.jpg").touch()
(tmp_path / "doc.pdf").touch()
(tmp_path / "text.txt").touch()
images = collect_images([tmp_path])
assert len(images) == 1
assert images[0].suffix.lower() == '.jpg'
def test_collect_images_multiple_paths(self, tmp_path):
"""Test collecting from multiple paths."""
dir1 = tmp_path / "dir1"
dir2 = tmp_path / "dir2"
dir1.mkdir()
dir2.mkdir()
(dir1 / "image1.jpg").touch()
(dir2 / "image2.png").touch()
images = collect_images([dir1, dir2])
assert len(images) == 2
class TestBatchResize:
"""Test batch resize functionality."""
def setup_method(self):
"""Set up test fixtures."""
self.resizer = ImageResizer(verbose=False, dry_run=False)
@patch.object(ImageResizer, "resize_image")
def test_batch_resize_success(self, mock_resize, tmp_path):
"""Test successful batch resize."""
mock_resize.return_value = True
input_images = [
tmp_path / "image1.jpg",
tmp_path / "image2.jpg"
]
for img in input_images:
img.touch()
output_dir = tmp_path / "output"
success, fail = self.resizer.batch_resize(
input_images,
output_dir,
width=800,
height=None,
strategy="fit"
)
assert success == 2
assert fail == 0
assert mock_resize.call_count == 2
@patch.object(ImageResizer, "resize_image")
def test_batch_resize_with_failures(self, mock_resize, tmp_path):
"""Test batch resize with some failures."""
mock_resize.side_effect = [True, False, True]
input_images = [
tmp_path / "image1.jpg",
tmp_path / "image2.jpg",
tmp_path / "image3.jpg"
]
for img in input_images:
img.touch()
output_dir = tmp_path / "output"
success, fail = self.resizer.batch_resize(
input_images,
output_dir,
width=800,
height=None
)
assert success == 2
assert fail == 1
@patch.object(ImageResizer, "resize_image")
def test_batch_resize_format_conversion(self, mock_resize, tmp_path):
"""Test batch resize with format conversion."""
mock_resize.return_value = True
input_images = [tmp_path / "image.png"]
input_images[0].touch()
output_dir = tmp_path / "output"
self.resizer.batch_resize(
input_images,
output_dir,
width=800,
height=None,
format_ext="jpg"
)
# Check that resize_image was called with .jpg extension
call_args = mock_resize.call_args[0]
assert call_args[1].suffix == ".jpg"
class TestResizeStrategies:
"""Test different resize strategies."""
def setup_method(self):
"""Set up test fixtures."""
self.resizer = ImageResizer()
def test_fit_strategy_maintains_aspect(self):
"""Test that 'fit' strategy maintains aspect ratio."""
cmd = self.resizer.build_resize_command(
Path("input.jpg"),
Path("output.jpg"),
width=800,
height=600,
strategy="fit",
quality=85
)
# Should have resize without ^ or !
resize_idx = cmd.index("-resize")
geometry = cmd[resize_idx + 1]
assert "^" not in geometry
assert "!" not in geometry
def test_cover_strategy_fills_dimensions(self):
"""Test that 'cover' strategy fills dimensions."""
cmd = self.resizer.build_resize_command(
Path("input.jpg"),
Path("output.jpg"),
width=800,
height=600,
strategy="cover",
quality=85
)
resize_idx = cmd.index("-resize")
geometry = cmd[resize_idx + 1]
assert "^" in geometry
def test_exact_strategy_ignores_aspect(self):
"""Test that 'exact' strategy ignores aspect ratio."""
cmd = self.resizer.build_resize_command(
Path("input.jpg"),
Path("output.jpg"),
width=800,
height=600,
strategy="exact",
quality=85
)
resize_idx = cmd.index("-resize")
geometry = cmd[resize_idx + 1]
assert "!" in geometry
if __name__ == "__main__":
pytest.main([__file__, "-v"])

View File

@@ -0,0 +1,259 @@
#!/usr/bin/env python3
"""Tests for media_convert.py"""
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 media_convert import (
build_audio_command,
build_image_command,
build_video_command,
check_dependencies,
convert_file,
detect_media_type,
)
class TestMediaTypeDetection:
"""Test media type detection."""
def test_detect_video_formats(self):
"""Test video format detection."""
assert detect_media_type(Path("test.mp4")) == "video"
assert detect_media_type(Path("test.mkv")) == "video"
assert detect_media_type(Path("test.avi")) == "video"
assert detect_media_type(Path("test.mov")) == "video"
def test_detect_audio_formats(self):
"""Test audio format detection."""
assert detect_media_type(Path("test.mp3")) == "audio"
assert detect_media_type(Path("test.aac")) == "audio"
assert detect_media_type(Path("test.flac")) == "audio"
assert detect_media_type(Path("test.wav")) == "audio"
def test_detect_image_formats(self):
"""Test image format detection."""
assert detect_media_type(Path("test.jpg")) == "image"
assert detect_media_type(Path("test.png")) == "image"
assert detect_media_type(Path("test.gif")) == "image"
assert detect_media_type(Path("test.webp")) == "image"
def test_detect_unknown_format(self):
"""Test unknown format detection."""
assert detect_media_type(Path("test.txt")) == "unknown"
assert detect_media_type(Path("test.doc")) == "unknown"
def test_case_insensitive(self):
"""Test case-insensitive detection."""
assert detect_media_type(Path("TEST.MP4")) == "video"
assert detect_media_type(Path("TEST.JPG")) == "image"
class TestCommandBuilding:
"""Test command building functions."""
def test_build_video_command_web_preset(self):
"""Test video command with web preset."""
cmd = build_video_command(
Path("input.mp4"),
Path("output.mp4"),
preset="web"
)
assert "ffmpeg" in cmd
assert "-i" in cmd
assert str(Path("input.mp4")) in cmd
assert "-c:v" in cmd
assert "libx264" in cmd
assert "-crf" in cmd
assert "23" in cmd
assert "-preset" in cmd
assert "medium" in cmd
assert str(Path("output.mp4")) in cmd
def test_build_video_command_archive_preset(self):
"""Test video command with archive preset."""
cmd = build_video_command(
Path("input.mp4"),
Path("output.mp4"),
preset="archive"
)
assert "18" in cmd # CRF for archive
assert "slow" in cmd # Preset for archive
def test_build_audio_command_mp3(self):
"""Test audio command for MP3 output."""
cmd = build_audio_command(
Path("input.wav"),
Path("output.mp3"),
preset="web"
)
assert "ffmpeg" in cmd
assert "-c:a" in cmd
assert "libmp3lame" in cmd
assert "-b:a" in cmd
def test_build_audio_command_flac(self):
"""Test audio command for FLAC (lossless)."""
cmd = build_audio_command(
Path("input.wav"),
Path("output.flac"),
preset="web"
)
assert "flac" in cmd
assert "-b:a" not in cmd # No bitrate for lossless
def test_build_image_command(self):
"""Test image command building."""
cmd = build_image_command(
Path("input.png"),
Path("output.jpg"),
preset="web"
)
assert "magick" in cmd
assert str(Path("input.png")) in cmd
assert "-quality" in cmd
assert "85" in cmd
assert "-strip" in cmd
assert str(Path("output.jpg")) in cmd
class TestDependencyCheck:
"""Test dependency checking."""
@patch("subprocess.run")
def test_check_dependencies_both_available(self, mock_run):
"""Test when both tools are available."""
mock_run.return_value = MagicMock(returncode=0)
ffmpeg_ok, magick_ok = check_dependencies()
assert ffmpeg_ok is True
assert magick_ok is True
@patch("subprocess.run")
def test_check_dependencies_ffmpeg_only(self, mock_run):
"""Test when only FFmpeg is available."""
def side_effect(*args, **kwargs):
if "ffmpeg" in args[0]:
return MagicMock(returncode=0)
return MagicMock(returncode=1)
mock_run.side_effect = side_effect
ffmpeg_ok, magick_ok = check_dependencies()
assert ffmpeg_ok is True
assert magick_ok is False
class TestFileConversion:
"""Test file conversion functionality."""
@patch("subprocess.run")
@patch("media_convert.detect_media_type")
def test_convert_video_file_dry_run(self, mock_detect, mock_run):
"""Test video conversion in dry-run mode."""
mock_detect.return_value = "video"
result = convert_file(
Path("input.mp4"),
Path("output.mp4"),
preset="web",
dry_run=True
)
assert result is True
mock_run.assert_not_called()
@patch("subprocess.run")
@patch("media_convert.detect_media_type")
def test_convert_image_file_success(self, mock_detect, mock_run):
"""Test successful image conversion."""
mock_detect.return_value = "image"
mock_run.return_value = MagicMock(returncode=0)
result = convert_file(
Path("input.png"),
Path("output.jpg"),
preset="web"
)
assert result is True
mock_run.assert_called_once()
@patch("subprocess.run")
@patch("media_convert.detect_media_type")
def test_convert_file_error(self, mock_detect, mock_run):
"""Test conversion error handling."""
mock_detect.return_value = "video"
mock_run.side_effect = Exception("Conversion failed")
result = convert_file(
Path("input.mp4"),
Path("output.mp4")
)
assert result is False
@patch("media_convert.detect_media_type")
def test_convert_unknown_format(self, mock_detect):
"""Test conversion with unknown format."""
mock_detect.return_value = "unknown"
result = convert_file(
Path("input.txt"),
Path("output.txt")
)
assert result is False
class TestQualityPresets:
"""Test quality preset functionality."""
def test_web_preset_settings(self):
"""Test web preset values."""
cmd = build_video_command(
Path("input.mp4"),
Path("output.mp4"),
preset="web"
)
cmd_str = " ".join(cmd)
assert "23" in cmd_str # CRF
assert "128k" in cmd_str # Audio bitrate
def test_archive_preset_settings(self):
"""Test archive preset values."""
cmd = build_video_command(
Path("input.mp4"),
Path("output.mp4"),
preset="archive"
)
cmd_str = " ".join(cmd)
assert "18" in cmd_str # Higher quality CRF
assert "192k" in cmd_str # Higher audio bitrate
def test_mobile_preset_settings(self):
"""Test mobile preset values."""
cmd = build_video_command(
Path("input.mp4"),
Path("output.mp4"),
preset="mobile"
)
cmd_str = " ".join(cmd)
assert "26" in cmd_str # Lower quality CRF
assert "96k" in cmd_str # Lower audio bitrate
if __name__ == "__main__":
pytest.main([__file__, "-v"])

View File

@@ -0,0 +1,397 @@
#!/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"])