207 lines
7.4 KiB
Python
207 lines
7.4 KiB
Python
import tempfile
|
|
from pathlib import Path
|
|
from unittest.mock import AsyncMock, patch
|
|
|
|
import pytest
|
|
|
|
|
|
class TestFFProbe:
|
|
"""Test the ffprobe module functionality."""
|
|
|
|
def setup_method(self):
|
|
"""Set up test files."""
|
|
self.temp_dir = tempfile.mkdtemp()
|
|
self.test_file = Path(self.temp_dir) / "test_video.mp4"
|
|
self.test_file.touch()
|
|
|
|
def teardown_method(self):
|
|
"""Clean up test files."""
|
|
import shutil
|
|
shutil.rmtree(self.temp_dir, ignore_errors=True)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_ffprobe_with_existing_file(self):
|
|
"""Test ffprobe with an existing file."""
|
|
from app.library.ffprobe import FFProbeResult, ffprobe
|
|
|
|
# Mock subprocess to avoid actual ffprobe execution
|
|
with patch("asyncio.create_subprocess_exec") as mock_subprocess:
|
|
# Mock the subprocess result
|
|
mock_process = AsyncMock()
|
|
mock_process.wait.return_value = 0
|
|
mock_process.communicate.return_value = (b'{"format": {"duration": "10.0"}}', b"")
|
|
mock_subprocess.return_value = mock_process
|
|
|
|
with patch("anyio.open_file") as mock_open_file:
|
|
mock_file = AsyncMock()
|
|
mock_open_file.return_value.__aenter__.return_value = mock_file
|
|
|
|
result = await ffprobe(str(self.test_file))
|
|
assert isinstance(result, FFProbeResult)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_ffprobe_with_nonexistent_file(self):
|
|
"""Test ffprobe with a non-existent file."""
|
|
from app.library.ffprobe import ffprobe
|
|
|
|
nonexistent_file = Path(self.temp_dir) / "does_not_exist.mp4"
|
|
|
|
with pytest.raises(OSError, match="No such media file"):
|
|
await ffprobe(str(nonexistent_file))
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_ffprobe_caching_behavior(self):
|
|
"""Test that ffprobe results are cached with enhanced async timed_lru_cache."""
|
|
from app.library.ffprobe import ffprobe
|
|
|
|
# Test that the function has been decorated with caching
|
|
assert hasattr(ffprobe, "cache_clear"), "ffprobe should have cache_clear method from timed_lru_cache"
|
|
assert hasattr(ffprobe, "cache_info"), "ffprobe should have cache_info method from timed_lru_cache"
|
|
|
|
# Clear cache to start fresh
|
|
ffprobe.cache_clear()
|
|
|
|
# Mock subprocess to avoid actual ffprobe execution
|
|
call_count = 0
|
|
with patch("asyncio.create_subprocess_exec") as mock_subprocess:
|
|
def create_mock_process(*_args, **_kwargs):
|
|
nonlocal call_count
|
|
call_count += 1
|
|
mock_process = AsyncMock()
|
|
mock_process.wait.return_value = 0
|
|
mock_process.communicate.return_value = (b'{"format": {"duration": "10.0"}, "streams": []}', b"")
|
|
return mock_process
|
|
|
|
mock_subprocess.side_effect = create_mock_process
|
|
|
|
with patch("anyio.open_file") as mock_open_file:
|
|
mock_file = AsyncMock()
|
|
mock_open_file.return_value.__aenter__.return_value = mock_file
|
|
|
|
# First call should execute the function
|
|
result1 = await ffprobe(str(self.test_file))
|
|
assert result1 is not None
|
|
assert isinstance(result1.metadata, dict)
|
|
first_call_count = call_count
|
|
|
|
# Second call with same argument should use cached result
|
|
result2 = await ffprobe(str(self.test_file))
|
|
assert result2 is not None
|
|
assert isinstance(result2.metadata, dict)
|
|
|
|
# The subprocess should not be called again for the actual ffprobe execution
|
|
# (it may be called for the -h check, but the main execution should be cached)
|
|
assert call_count == first_call_count, "Second call should use cached result"
|
|
|
|
# Results should be equivalent (same data, may not be same object due to async nature)
|
|
assert result1.metadata == result2.metadata
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_ffprobe_with_path_object(self):
|
|
"""Test ffprobe with Path object input."""
|
|
from app.library.ffprobe import ffprobe
|
|
|
|
# Mock subprocess to avoid actual ffprobe execution
|
|
with patch("asyncio.create_subprocess_exec") as mock_subprocess:
|
|
mock_process = AsyncMock()
|
|
mock_process.wait.return_value = 0
|
|
mock_process.communicate.return_value = (b'{"format": {"duration": "10.0"}}', b"")
|
|
mock_subprocess.return_value = mock_process
|
|
|
|
with patch("anyio.open_file") as mock_open_file:
|
|
mock_file = AsyncMock()
|
|
mock_open_file.return_value.__aenter__.return_value = mock_file
|
|
|
|
# Test with Path object
|
|
result = await ffprobe(self.test_file)
|
|
assert result is not None
|
|
|
|
def test_ffprobe_result_properties(self):
|
|
"""Test FFProbeResult object properties."""
|
|
from app.library.ffprobe import FFProbeResult, FFStream
|
|
|
|
result = FFProbeResult()
|
|
|
|
# Test empty result
|
|
assert result.video == []
|
|
assert result.audio == []
|
|
assert result.subtitle == []
|
|
assert result.attachment == []
|
|
assert result.metadata == {}
|
|
|
|
# Test adding streams
|
|
video_stream = FFStream({
|
|
"index": 0,
|
|
"codec_type": "video",
|
|
"codec_name": "h264",
|
|
"width": 1920,
|
|
"height": 1080
|
|
})
|
|
|
|
audio_stream = FFStream({
|
|
"index": 1,
|
|
"codec_type": "audio",
|
|
"codec_name": "aac",
|
|
"channels": 2
|
|
})
|
|
|
|
result.video.append(video_stream)
|
|
result.audio.append(audio_stream)
|
|
|
|
assert len(result.video) == 1
|
|
assert len(result.audio) == 1
|
|
assert result.video[0].is_video()
|
|
assert result.audio[0].is_audio()
|
|
|
|
def test_stream_object_methods(self):
|
|
"""Test Stream object methods."""
|
|
from app.library.ffprobe import FFStream
|
|
|
|
# Test video stream
|
|
video_stream = FFStream({
|
|
"codec_type": "video",
|
|
"codec_name": "h264",
|
|
"width": 1920,
|
|
"height": 1080,
|
|
"duration": "10.5"
|
|
})
|
|
|
|
assert video_stream.is_video()
|
|
assert not video_stream.is_audio()
|
|
assert not video_stream.is_subtitle()
|
|
assert not video_stream.is_attachment()
|
|
|
|
# Test audio stream
|
|
audio_stream = FFStream({
|
|
"codec_type": "audio",
|
|
"codec_name": "aac",
|
|
"channels": 2,
|
|
"sample_rate": "44100"
|
|
})
|
|
|
|
assert not audio_stream.is_video()
|
|
assert audio_stream.is_audio()
|
|
assert not audio_stream.is_subtitle()
|
|
assert not audio_stream.is_attachment()
|
|
|
|
# Test subtitle stream
|
|
subtitle_stream = FFStream({
|
|
"codec_type": "subtitle",
|
|
"codec_name": "subrip"
|
|
})
|
|
|
|
assert not subtitle_stream.is_video()
|
|
assert not subtitle_stream.is_audio()
|
|
assert subtitle_stream.is_subtitle()
|
|
assert not subtitle_stream.is_attachment()
|
|
|
|
# Test attachment stream
|
|
attachment_stream = FFStream({
|
|
"codec_type": "attachment",
|
|
"codec_name": "ttf"
|
|
})
|
|
|
|
assert not attachment_stream.is_video()
|
|
assert not attachment_stream.is_audio()
|
|
assert not attachment_stream.is_subtitle()
|
|
assert attachment_stream.is_attachment()
|