ytptube/app/tests/test_ffprobe.py
2025-09-15 19:34:03 +03:00

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()