974 lines
35 KiB
Python
974 lines
35 KiB
Python
"""Unit tests for BackgroundLoaderService.
|
|
|
|
Tests cover:
|
|
- Task queuing and worker orchestration
|
|
- Loading status tracking and progress reporting
|
|
- Concurrent task processing
|
|
- WebSocket broadcasting
|
|
- Error handling and recovery
|
|
- Resource cleanup
|
|
- Missing data detection
|
|
"""
|
|
import asyncio
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
from src.server.services.background_loader_service import (
|
|
BackgroundLoaderService,
|
|
LoadingStatus,
|
|
SeriesLoadingTask,
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_websocket_service():
|
|
"""Mock WebSocket service."""
|
|
service = AsyncMock()
|
|
service.broadcast = AsyncMock()
|
|
return service
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_anime_service():
|
|
"""Mock AnimeService."""
|
|
service = AsyncMock()
|
|
service.sync_episodes_to_db = AsyncMock()
|
|
service.sync_single_series_after_scan = AsyncMock()
|
|
return service
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_series_app():
|
|
"""Mock SeriesApp."""
|
|
app = MagicMock()
|
|
app.directory_to_search = "/anime"
|
|
app.nfo_service = AsyncMock()
|
|
app.nfo_service.has_nfo = MagicMock(return_value=False)
|
|
app.nfo_service.create_tvshow_nfo = AsyncMock(return_value="/anime/TestSeries/tvshow.nfo")
|
|
app.serie_scanner = MagicMock()
|
|
app.serie_scanner.scan_single_series = MagicMock(return_value={
|
|
"Season 1": ["episode1.mp4", "episode2.mp4"]
|
|
})
|
|
return app
|
|
|
|
|
|
@pytest.fixture
|
|
def background_loader_service(mock_websocket_service, mock_anime_service, mock_series_app):
|
|
"""Create BackgroundLoaderService instance."""
|
|
return BackgroundLoaderService(
|
|
websocket_service=mock_websocket_service,
|
|
anime_service=mock_anime_service,
|
|
series_app=mock_series_app,
|
|
max_concurrent_loads=3
|
|
)
|
|
|
|
|
|
class TestSeriesLoadingTask:
|
|
"""Tests for SeriesLoadingTask data class."""
|
|
|
|
def test_task_initialization(self):
|
|
"""Test task is initialized with correct default values."""
|
|
task = SeriesLoadingTask(
|
|
key="test_series",
|
|
folder="test_folder",
|
|
name="Test Series",
|
|
year=2020
|
|
)
|
|
|
|
assert task.key == "test_series"
|
|
assert task.folder == "test_folder"
|
|
assert task.name == "Test Series"
|
|
assert task.year == 2020
|
|
assert task.status == LoadingStatus.PENDING
|
|
assert task.progress == {
|
|
"episodes": False,
|
|
"nfo": False,
|
|
"logo": False,
|
|
"images": False
|
|
}
|
|
assert task.started_at is None
|
|
assert task.completed_at is None
|
|
assert task.error is None
|
|
|
|
def test_task_with_minimal_fields(self):
|
|
"""Test task creation with minimal required fields."""
|
|
task = SeriesLoadingTask(
|
|
key="minimal",
|
|
folder="folder",
|
|
name="Name"
|
|
)
|
|
|
|
assert task.key == "minimal"
|
|
assert task.year is None
|
|
assert task.status == LoadingStatus.PENDING
|
|
|
|
def test_task_progress_tracking(self):
|
|
"""Test progress tracking updates correctly."""
|
|
task = SeriesLoadingTask(
|
|
key="test",
|
|
folder="Test",
|
|
name="Test"
|
|
)
|
|
|
|
task.progress["episodes"] = True
|
|
assert task.progress["episodes"] is True
|
|
assert not task.progress["nfo"]
|
|
|
|
|
|
class TestLoadingStatus:
|
|
"""Tests for LoadingStatus enumeration."""
|
|
|
|
def test_all_status_values(self):
|
|
"""Test all loading status values."""
|
|
assert LoadingStatus.PENDING.value == "pending"
|
|
assert LoadingStatus.LOADING_EPISODES.value == "loading_episodes"
|
|
assert LoadingStatus.LOADING_NFO.value == "loading_nfo"
|
|
assert LoadingStatus.COMPLETED.value == "completed"
|
|
assert LoadingStatus.FAILED.value == "failed"
|
|
|
|
|
|
class TestBackgroundLoaderServiceInitialization:
|
|
"""Tests for service initialization."""
|
|
|
|
def test_service_initialization(self, mock_websocket_service, mock_anime_service, mock_series_app):
|
|
"""Test service initializes with correct configuration."""
|
|
service = BackgroundLoaderService(
|
|
websocket_service=mock_websocket_service,
|
|
anime_service=mock_anime_service,
|
|
series_app=mock_series_app,
|
|
max_concurrent_loads=5
|
|
)
|
|
|
|
assert service.websocket_service is mock_websocket_service
|
|
assert service.anime_service is mock_anime_service
|
|
assert service.series_app is mock_series_app
|
|
assert service.max_concurrent_loads == 5
|
|
assert isinstance(service.task_queue, asyncio.Queue)
|
|
assert service.active_tasks == {}
|
|
assert service.worker_tasks == []
|
|
assert service._shutdown is False
|
|
|
|
def test_default_max_concurrent_loads(self, mock_websocket_service, mock_anime_service, mock_series_app):
|
|
"""Test default max_concurrent_loads is 5."""
|
|
service = BackgroundLoaderService(
|
|
websocket_service=mock_websocket_service,
|
|
anime_service=mock_anime_service,
|
|
series_app=mock_series_app
|
|
)
|
|
|
|
assert service.max_concurrent_loads == 5
|
|
|
|
|
|
class TestStartStopService:
|
|
"""Tests for service startup and shutdown."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_service_start(self, background_loader_service):
|
|
"""Test service starts worker tasks."""
|
|
await background_loader_service.start()
|
|
|
|
assert len(background_loader_service.worker_tasks) == 3
|
|
assert all(not task.done() for task in background_loader_service.worker_tasks)
|
|
|
|
await background_loader_service.stop()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_service_stop(self, background_loader_service):
|
|
"""Test service stops all worker tasks gracefully."""
|
|
await background_loader_service.start()
|
|
assert len(background_loader_service.worker_tasks) == 3
|
|
|
|
await background_loader_service.stop()
|
|
|
|
assert background_loader_service.worker_tasks == []
|
|
assert background_loader_service._shutdown is True
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_service_stop_when_not_started(self, background_loader_service):
|
|
"""Test stopping a service that was never started."""
|
|
await background_loader_service.stop()
|
|
assert background_loader_service.worker_tasks == []
|
|
|
|
|
|
class TestAddSeriesLoadingTask:
|
|
"""Tests for adding tasks to the queue."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_add_series_loading_task(self, background_loader_service):
|
|
"""Test adding a series loading task to the queue."""
|
|
await background_loader_service.add_series_loading_task(
|
|
key="test_series",
|
|
folder="test_folder",
|
|
name="Test Series",
|
|
year=2020
|
|
)
|
|
|
|
assert "test_series" in background_loader_service.active_tasks
|
|
task = background_loader_service.active_tasks["test_series"]
|
|
|
|
assert task.key == "test_series"
|
|
assert task.folder == "test_folder"
|
|
assert task.name == "Test Series"
|
|
assert task.year == 2020
|
|
assert task.status == LoadingStatus.PENDING
|
|
assert task.started_at is not None
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_add_duplicate_task_skips_addition(self, background_loader_service):
|
|
"""Test adding duplicate task skips the new one."""
|
|
await background_loader_service.add_series_loading_task(
|
|
key="test_series",
|
|
folder="test_folder1",
|
|
name="Test Series 1"
|
|
)
|
|
|
|
initial_task = background_loader_service.active_tasks["test_series"]
|
|
|
|
await background_loader_service.add_series_loading_task(
|
|
key="test_series",
|
|
folder="test_folder2",
|
|
name="Test Series 2"
|
|
)
|
|
|
|
assert background_loader_service.active_tasks["test_series"] is initial_task
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_add_task_broadcasts_status(self, background_loader_service, mock_websocket_service):
|
|
"""Test adding task broadcasts initial status."""
|
|
await background_loader_service.add_series_loading_task(
|
|
key="test_series",
|
|
folder="test_folder",
|
|
name="Test Series"
|
|
)
|
|
|
|
mock_websocket_service.broadcast.assert_called_once()
|
|
call_args = mock_websocket_service.broadcast.call_args[0][0]
|
|
|
|
assert call_args["type"] == "series_loading_update"
|
|
assert call_args["key"] == "test_series"
|
|
assert call_args["status"] == LoadingStatus.PENDING.value
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_add_multiple_tasks(self, background_loader_service):
|
|
"""Test adding multiple different tasks."""
|
|
await background_loader_service.add_series_loading_task(
|
|
key="series1",
|
|
folder="folder1",
|
|
name="Series 1"
|
|
)
|
|
await background_loader_service.add_series_loading_task(
|
|
key="series2",
|
|
folder="folder2",
|
|
name="Series 2"
|
|
)
|
|
|
|
assert len(background_loader_service.active_tasks) == 2
|
|
assert "series1" in background_loader_service.active_tasks
|
|
assert "series2" in background_loader_service.active_tasks
|
|
|
|
|
|
class TestCheckMissingData:
|
|
"""Tests for checking missing data for a series."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_check_missing_data_no_series_in_db(self, background_loader_service):
|
|
"""Test checking missing data when series doesn't exist in DB."""
|
|
mock_db = AsyncMock()
|
|
|
|
with patch("src.server.database.service.AnimeSeriesService") as mock_service_class:
|
|
mock_service_class.get_by_key = AsyncMock(return_value=None)
|
|
|
|
missing = await background_loader_service.check_missing_data(
|
|
key="new_series",
|
|
folder="new_folder",
|
|
anime_directory="/anime",
|
|
db=mock_db
|
|
)
|
|
|
|
assert all(missing.values())
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_check_missing_data_partial_loaded(self, background_loader_service):
|
|
"""Test checking missing data for partially loaded series."""
|
|
mock_db = AsyncMock()
|
|
mock_series = MagicMock()
|
|
mock_series.episodes_loaded = True
|
|
mock_series.has_nfo = True
|
|
mock_series.logo_loaded = False
|
|
mock_series.images_loaded = False
|
|
|
|
with patch("src.server.database.service.AnimeSeriesService") as mock_service_class:
|
|
with patch("src.server.utils.media.check_media_files") as mock_check:
|
|
mock_service_class.get_by_key = AsyncMock(return_value=mock_series)
|
|
mock_check.return_value = {
|
|
"poster": False,
|
|
"logo": False,
|
|
"fanart": False,
|
|
"nfo": True
|
|
}
|
|
|
|
missing = await background_loader_service.check_missing_data(
|
|
key="partial_series",
|
|
folder="partial_folder",
|
|
anime_directory="/anime",
|
|
db=mock_db
|
|
)
|
|
|
|
assert missing["episodes"] is False
|
|
assert missing["nfo"] is False
|
|
assert missing["logo"] is True
|
|
assert missing["images"] is True
|
|
|
|
|
|
class TestBroadcastStatus:
|
|
"""Tests for WebSocket status broadcasting."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_broadcast_status_pending(self, background_loader_service, mock_websocket_service):
|
|
"""Test broadcasting pending status."""
|
|
task = SeriesLoadingTask(
|
|
key="test",
|
|
folder="folder",
|
|
name="Test",
|
|
status=LoadingStatus.PENDING
|
|
)
|
|
|
|
await background_loader_service._broadcast_status(task)
|
|
|
|
call_args = mock_websocket_service.broadcast.call_args[0][0]
|
|
assert call_args["status"] == LoadingStatus.PENDING.value
|
|
assert "Queued" in call_args["message"]
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_broadcast_status_completed(self, background_loader_service, mock_websocket_service):
|
|
"""Test broadcasting completed status."""
|
|
task = SeriesLoadingTask(
|
|
key="test",
|
|
folder="folder",
|
|
name="Test",
|
|
status=LoadingStatus.COMPLETED
|
|
)
|
|
|
|
await background_loader_service._broadcast_status(task)
|
|
|
|
call_args = mock_websocket_service.broadcast.call_args[0][0]
|
|
assert call_args["status"] == LoadingStatus.COMPLETED.value
|
|
assert "successfully" in call_args["message"]
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_broadcast_status_failed(self, background_loader_service, mock_websocket_service):
|
|
"""Test broadcasting failed status with error message."""
|
|
task = SeriesLoadingTask(
|
|
key="test",
|
|
folder="folder",
|
|
name="Test",
|
|
status=LoadingStatus.FAILED,
|
|
error="Test error"
|
|
)
|
|
|
|
await background_loader_service._broadcast_status(task)
|
|
|
|
call_args = mock_websocket_service.broadcast.call_args[0][0]
|
|
assert call_args["status"] == LoadingStatus.FAILED.value
|
|
assert call_args["error"] == "Test error"
|
|
assert "failed" in call_args["message"]
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_broadcast_status_custom_message(self, background_loader_service, mock_websocket_service):
|
|
"""Test broadcasting with custom message."""
|
|
task = SeriesLoadingTask(
|
|
key="test",
|
|
folder="folder",
|
|
name="Test"
|
|
)
|
|
|
|
await background_loader_service._broadcast_status(task, "Custom message")
|
|
|
|
call_args = mock_websocket_service.broadcast.call_args[0][0]
|
|
assert call_args["message"] == "Custom message"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_broadcast_status_includes_metadata(self, background_loader_service, mock_websocket_service):
|
|
"""Test broadcast includes all required metadata."""
|
|
task = SeriesLoadingTask(
|
|
key="test_key",
|
|
folder="test_folder",
|
|
name="Test Name"
|
|
)
|
|
task.progress = {"episodes": True, "nfo": False, "logo": False, "images": False}
|
|
|
|
await background_loader_service._broadcast_status(task)
|
|
|
|
call_args = mock_websocket_service.broadcast.call_args[0][0]
|
|
assert call_args["type"] == "series_loading_update"
|
|
assert call_args["key"] == "test_key"
|
|
assert call_args["series_key"] == "test_key"
|
|
assert call_args["folder"] == "test_folder"
|
|
assert call_args["progress"] == {"episodes": True, "nfo": False, "logo": False, "images": False}
|
|
assert "timestamp" in call_args
|
|
|
|
|
|
class TestFindSeriesDirectory:
|
|
"""Tests for finding series directory."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_find_series_directory_exists(self, background_loader_service, tmp_path):
|
|
"""Test finding series directory when it exists."""
|
|
series_dir = tmp_path / "TestSeries"
|
|
series_dir.mkdir()
|
|
|
|
background_loader_service.series_app.directory_to_search = str(tmp_path)
|
|
|
|
task = SeriesLoadingTask(
|
|
key="test",
|
|
folder="TestSeries",
|
|
name="Test"
|
|
)
|
|
|
|
result = await background_loader_service._find_series_directory(task)
|
|
|
|
assert result is not None
|
|
assert result.name == "TestSeries"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_find_series_directory_not_found(self, background_loader_service, tmp_path):
|
|
"""Test finding series directory when it doesn't exist."""
|
|
background_loader_service.series_app.directory_to_search = str(tmp_path)
|
|
|
|
task = SeriesLoadingTask(
|
|
key="test",
|
|
folder="NonExistentSeries",
|
|
name="Test"
|
|
)
|
|
|
|
result = await background_loader_service._find_series_directory(task)
|
|
|
|
assert result is None
|
|
|
|
|
|
class TestScanSeriesEpisodes:
|
|
"""Tests for scanning episodes in a series directory."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_scan_series_episodes(self, background_loader_service, tmp_path):
|
|
"""Test scanning episodes from series directory."""
|
|
season1 = tmp_path / "Season 1"
|
|
season1.mkdir()
|
|
(season1 / "episode1.mp4").touch()
|
|
(season1 / "episode2.mp4").touch()
|
|
|
|
season2 = tmp_path / "Season 2"
|
|
season2.mkdir()
|
|
(season2 / "episode1.mp4").touch()
|
|
|
|
task = SeriesLoadingTask(
|
|
key="test",
|
|
folder="TestSeries",
|
|
name="Test"
|
|
)
|
|
|
|
result = await background_loader_service._scan_series_episodes(tmp_path, task)
|
|
|
|
assert "Season 1" in result
|
|
assert "Season 2" in result
|
|
assert len(result["Season 1"]) == 2
|
|
assert len(result["Season 2"]) == 1
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_scan_series_episodes_ignores_non_mp4(self, background_loader_service, tmp_path):
|
|
"""Test that only .mp4 files are scanned."""
|
|
season = tmp_path / "Season 1"
|
|
season.mkdir()
|
|
(season / "episode1.mp4").touch()
|
|
(season / "episode2.txt").touch()
|
|
(season / "episode3.avi").touch()
|
|
|
|
task = SeriesLoadingTask(
|
|
key="test",
|
|
folder="TestSeries",
|
|
name="Test"
|
|
)
|
|
|
|
result = await background_loader_service._scan_series_episodes(tmp_path, task)
|
|
|
|
assert len(result["Season 1"]) == 1
|
|
assert result["Season 1"][0] == "episode1.mp4"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_scan_series_episodes_empty_directory(self, background_loader_service, tmp_path):
|
|
"""Test scanning empty directory."""
|
|
task = SeriesLoadingTask(
|
|
key="test",
|
|
folder="TestSeries",
|
|
name="Test"
|
|
)
|
|
|
|
result = await background_loader_service._scan_series_episodes(tmp_path, task)
|
|
|
|
assert result == {}
|
|
|
|
|
|
class TestLoadNfoAndImages:
|
|
"""Tests for loading NFO files and images."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_load_nfo_creates_new_nfo(self, background_loader_service, mock_websocket_service):
|
|
"""Test creating new NFO file when it doesn't exist."""
|
|
mock_db = AsyncMock()
|
|
mock_series = MagicMock()
|
|
mock_series.has_nfo = False
|
|
|
|
task = SeriesLoadingTask(
|
|
key="test",
|
|
folder="test_folder",
|
|
name="Test Series",
|
|
year=2020
|
|
)
|
|
|
|
with patch("src.server.database.service.AnimeSeriesService") as mock_service_class:
|
|
mock_service_class.get_by_key = AsyncMock(return_value=mock_series)
|
|
|
|
result = await background_loader_service._load_nfo_and_images(task, mock_db)
|
|
|
|
assert result is True
|
|
assert task.progress["nfo"] is True
|
|
assert task.progress["logo"] is True
|
|
assert task.progress["images"] is True
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_load_nfo_uses_existing(self, background_loader_service):
|
|
"""Test using existing NFO file when it already exists."""
|
|
background_loader_service.series_app.nfo_service.has_nfo = MagicMock(return_value=True)
|
|
|
|
mock_db = AsyncMock()
|
|
mock_series = MagicMock()
|
|
mock_series.has_nfo = True
|
|
|
|
task = SeriesLoadingTask(
|
|
key="test",
|
|
folder="test_folder",
|
|
name="Test Series"
|
|
)
|
|
|
|
with patch("src.server.database.service.AnimeSeriesService") as mock_service_class:
|
|
mock_service_class.get_by_key = AsyncMock(return_value=mock_series)
|
|
|
|
result = await background_loader_service._load_nfo_and_images(task, mock_db)
|
|
|
|
assert result is False
|
|
assert task.progress["nfo"] is True
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_load_nfo_without_nfo_service(self, background_loader_service):
|
|
"""Test graceful handling when NFO service not available."""
|
|
background_loader_service.series_app.nfo_service = None
|
|
|
|
mock_db = AsyncMock()
|
|
task = SeriesLoadingTask(
|
|
key="test",
|
|
folder="test_folder",
|
|
name="Test Series"
|
|
)
|
|
|
|
result = await background_loader_service._load_nfo_and_images(task, mock_db)
|
|
|
|
assert result is False
|
|
assert task.progress["nfo"] is False
|
|
assert task.progress["logo"] is False
|
|
|
|
|
|
class TestScanMissingEpisodes:
|
|
"""Tests for scanning missing episodes."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_scan_missing_episodes(self, background_loader_service):
|
|
"""Test scanning for missing episodes."""
|
|
mock_db = AsyncMock()
|
|
mock_series = MagicMock()
|
|
|
|
task = SeriesLoadingTask(
|
|
key="test",
|
|
folder="test_folder",
|
|
name="Test Series"
|
|
)
|
|
|
|
with patch("src.server.database.service.AnimeSeriesService") as mock_service_class:
|
|
mock_service_class.get_by_key = AsyncMock(return_value=mock_series)
|
|
|
|
await background_loader_service._scan_missing_episodes(task, mock_db)
|
|
|
|
assert task.progress["episodes"] is True
|
|
background_loader_service.anime_service.sync_single_series_after_scan.assert_called_once_with("test")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_scan_missing_episodes_no_scanner(self, background_loader_service):
|
|
"""Test handling when scanner not available."""
|
|
background_loader_service.series_app.serie_scanner = None
|
|
|
|
mock_db = AsyncMock()
|
|
task = SeriesLoadingTask(
|
|
key="test",
|
|
folder="test_folder",
|
|
name="Test Series"
|
|
)
|
|
|
|
await background_loader_service._scan_missing_episodes(task, mock_db)
|
|
|
|
|
|
class TestConcurrentProcessing:
|
|
"""Tests for concurrent task processing."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_multiple_concurrent_tasks(self, background_loader_service):
|
|
"""Test processing multiple tasks concurrently."""
|
|
await background_loader_service.add_series_loading_task("series1", "folder1", "Series 1")
|
|
await background_loader_service.add_series_loading_task("series2", "folder2", "Series 2")
|
|
await background_loader_service.add_series_loading_task("series3", "folder3", "Series 3")
|
|
|
|
assert len(background_loader_service.active_tasks) == 3
|
|
assert background_loader_service.task_queue.qsize() == 3
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_max_concurrent_load_limit(self, mock_websocket_service, mock_anime_service, mock_series_app):
|
|
"""Test respecting maximum concurrent loads setting."""
|
|
service = BackgroundLoaderService(
|
|
websocket_service=mock_websocket_service,
|
|
anime_service=mock_anime_service,
|
|
series_app=mock_series_app,
|
|
max_concurrent_loads=2
|
|
)
|
|
|
|
await service.start()
|
|
|
|
assert len(service.worker_tasks) == 2
|
|
|
|
await service.stop()
|
|
|
|
|
|
class TestLoadSeriesData:
|
|
"""Tests for complete series data loading process."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_load_series_data_successful(self, background_loader_service):
|
|
"""Test successful series data loading."""
|
|
mock_db = AsyncMock()
|
|
mock_series = MagicMock()
|
|
|
|
task = SeriesLoadingTask(
|
|
key="test",
|
|
folder="test_folder",
|
|
name="Test Series"
|
|
)
|
|
|
|
with patch("src.server.database.connection.get_db_session"):
|
|
with patch("src.server.database.service.AnimeSeriesService") as mock_service_class:
|
|
mock_service_class.get_by_key = AsyncMock(return_value=mock_series)
|
|
|
|
with patch.object(background_loader_service, "check_missing_data", return_value={
|
|
"episodes": True,
|
|
"nfo": True,
|
|
"logo": True,
|
|
"images": True
|
|
}):
|
|
with patch.object(background_loader_service, "_load_nfo_and_images", return_value=True):
|
|
with patch.object(background_loader_service, "_scan_missing_episodes"):
|
|
with patch.object(background_loader_service, "_broadcast_status"):
|
|
await background_loader_service._load_series_data(task)
|
|
|
|
assert task.status == LoadingStatus.COMPLETED
|
|
assert task.completed_at is not None
|
|
assert "test" not in background_loader_service.active_tasks
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_load_series_data_handles_error(self, background_loader_service):
|
|
"""Test error handling during series data loading."""
|
|
mock_db = AsyncMock()
|
|
|
|
task = SeriesLoadingTask(
|
|
key="test",
|
|
folder="test_folder",
|
|
name="Test Series"
|
|
)
|
|
|
|
with patch("src.server.database.connection.get_db_session"):
|
|
with patch("src.server.database.service.AnimeSeriesService") as mock_service_class:
|
|
mock_service_class.get_by_key = AsyncMock(return_value=None)
|
|
|
|
with patch.object(background_loader_service, "check_missing_data", side_effect=Exception("Test error")):
|
|
with patch.object(background_loader_service, "_broadcast_status"):
|
|
await background_loader_service._load_series_data(task)
|
|
|
|
assert task.status == LoadingStatus.FAILED
|
|
assert task.error == "Test error"
|
|
assert task.completed_at is not None
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_load_series_data_with_partial_missing(self, background_loader_service):
|
|
"""Test loading series when only some data is missing."""
|
|
mock_db = AsyncMock()
|
|
mock_series = MagicMock()
|
|
|
|
task = SeriesLoadingTask(
|
|
key="test",
|
|
folder="test_folder",
|
|
name="Test Series"
|
|
)
|
|
|
|
with patch("src.server.database.connection.get_db_session"):
|
|
with patch("src.server.database.service.AnimeSeriesService") as mock_service_class:
|
|
mock_service_class.get_by_key = AsyncMock(return_value=mock_series)
|
|
|
|
with patch.object(background_loader_service, "check_missing_data", return_value={
|
|
"episodes": False,
|
|
"nfo": False,
|
|
"logo": False,
|
|
"images": False
|
|
}):
|
|
with patch.object(background_loader_service, "_load_nfo_and_images", return_value=False):
|
|
with patch.object(background_loader_service, "_scan_missing_episodes"):
|
|
with patch.object(background_loader_service, "_broadcast_status"):
|
|
await background_loader_service._load_series_data(task)
|
|
|
|
assert task.status == LoadingStatus.COMPLETED
|
|
# When nothing is missing, nfo/logo/images get marked true, but episodes marked false by scan
|
|
assert task.progress["nfo"] is True or task.progress["nfo"] is False
|
|
|
|
|
|
class TestWorkerExecution:
|
|
"""Tests for background worker execution."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_worker_processes_task_from_queue(self, background_loader_service):
|
|
"""Test worker processes task from queue."""
|
|
# Create a task
|
|
task = SeriesLoadingTask(
|
|
key="test",
|
|
folder="test_folder",
|
|
name="Test Series"
|
|
)
|
|
|
|
# Add it to queue
|
|
await background_loader_service.task_queue.put(task)
|
|
|
|
# Start service
|
|
await background_loader_service.start()
|
|
|
|
# Let worker process briefly
|
|
await asyncio.sleep(0.1)
|
|
|
|
# Stop service
|
|
await background_loader_service.stop()
|
|
|
|
# Queue should be empty
|
|
assert background_loader_service.task_queue.empty()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_worker_timeout_loop(self, background_loader_service):
|
|
"""Test worker timeout loop works correctly."""
|
|
await background_loader_service.start()
|
|
|
|
# Let worker loop a few times with no tasks
|
|
await asyncio.sleep(0.2)
|
|
|
|
# Workers should still be running
|
|
assert all(not task.done() for task in background_loader_service.worker_tasks)
|
|
|
|
await background_loader_service.stop()
|
|
|
|
|
|
class TestBroadcastStatusMessages:
|
|
"""Tests for various status message broadcasts."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_broadcast_loading_episodes_status(self, background_loader_service, mock_websocket_service):
|
|
"""Test broadcasting loading episodes status."""
|
|
task = SeriesLoadingTask(
|
|
key="test",
|
|
folder="folder",
|
|
name="Test",
|
|
status=LoadingStatus.LOADING_EPISODES
|
|
)
|
|
|
|
await background_loader_service._broadcast_status(task)
|
|
|
|
call_args = mock_websocket_service.broadcast.call_args[0][0]
|
|
assert "loading" in call_args["message"].lower() or "episode" in call_args["message"].lower()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_broadcast_loading_nfo_status(self, background_loader_service, mock_websocket_service):
|
|
"""Test broadcasting loading NFO status."""
|
|
task = SeriesLoadingTask(
|
|
key="test",
|
|
folder="folder",
|
|
name="Test",
|
|
status=LoadingStatus.LOADING_NFO
|
|
)
|
|
|
|
await background_loader_service._broadcast_status(task)
|
|
|
|
call_args = mock_websocket_service.broadcast.call_args[0][0]
|
|
assert "nfo" in call_args["message"].lower() or "loading" in call_args["message"].lower()
|
|
|
|
|
|
class TestErrorHandling:
|
|
"""Tests for error handling scenarios."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_load_nfo_error_handling(self, background_loader_service):
|
|
"""Test error handling during NFO creation."""
|
|
background_loader_service.series_app.nfo_service.has_nfo = MagicMock(return_value=False)
|
|
background_loader_service.series_app.nfo_service.create_tvshow_nfo = AsyncMock(
|
|
side_effect=Exception("API error")
|
|
)
|
|
|
|
mock_db = AsyncMock()
|
|
task = SeriesLoadingTask(
|
|
key="test",
|
|
folder="test_folder",
|
|
name="Test Series"
|
|
)
|
|
|
|
result = await background_loader_service._load_nfo_and_images(task, mock_db)
|
|
|
|
assert result is False
|
|
assert task.progress["nfo"] is False
|
|
assert task.progress["logo"] is False
|
|
assert task.progress["images"] is False
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_scan_missing_episodes_error_handling(self, background_loader_service):
|
|
"""Test error handling during episode scan."""
|
|
background_loader_service.series_app.serie_scanner.scan_single_series = MagicMock(
|
|
side_effect=Exception("Scan error")
|
|
)
|
|
|
|
mock_db = AsyncMock()
|
|
task = SeriesLoadingTask(
|
|
key="test",
|
|
folder="test_folder",
|
|
name="Test Series"
|
|
)
|
|
|
|
await background_loader_service._scan_missing_episodes(task, mock_db)
|
|
|
|
assert task.progress["episodes"] is False
|
|
|
|
|
|
class TestDirectoryScanning:
|
|
"""Tests for directory operations."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_find_series_directory_error_handling(self, background_loader_service):
|
|
"""Test error handling when directory lookup fails."""
|
|
background_loader_service.series_app.directory_to_search = "/invalid/path"
|
|
|
|
task = SeriesLoadingTask(
|
|
key="test",
|
|
folder="TestSeries",
|
|
name="Test"
|
|
)
|
|
|
|
result = await background_loader_service._find_series_directory(task)
|
|
|
|
assert result is None
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_scan_series_episodes_error_handling(self, background_loader_service):
|
|
"""Test error handling during episode scanning."""
|
|
task = SeriesLoadingTask(
|
|
key="test",
|
|
folder="TestSeries",
|
|
name="Test"
|
|
)
|
|
|
|
# Pass a non-existent path
|
|
result = await background_loader_service._scan_series_episodes(Path("/nonexistent"), task)
|
|
|
|
assert result == {}
|
|
|
|
|
|
class TestCheckMissingDataEdgeCases:
|
|
"""Tests for edge cases in missing data checking."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_check_missing_data_all_loaded(self, background_loader_service):
|
|
"""Test checking missing data when all data is already loaded."""
|
|
mock_db = AsyncMock()
|
|
mock_series = MagicMock()
|
|
mock_series.episodes_loaded = True
|
|
mock_series.has_nfo = True
|
|
mock_series.logo_loaded = True
|
|
mock_series.images_loaded = True
|
|
|
|
with patch("src.server.database.service.AnimeSeriesService") as mock_service_class:
|
|
with patch("src.server.utils.media.check_media_files") as mock_check:
|
|
mock_service_class.get_by_key = AsyncMock(return_value=mock_series)
|
|
mock_check.return_value = {
|
|
"poster": True,
|
|
"logo": True,
|
|
"fanart": True,
|
|
"nfo": True
|
|
}
|
|
|
|
missing = await background_loader_service.check_missing_data(
|
|
key="complete_series",
|
|
folder="complete_folder",
|
|
anime_directory="/anime",
|
|
db=mock_db
|
|
)
|
|
|
|
assert not any(missing.values())
|
|
|
|
|
|
class TestTaskProgressTracking:
|
|
"""Tests for task progress tracking."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_task_progress_updates(self, background_loader_service):
|
|
"""Test that task progress is properly updated."""
|
|
task = SeriesLoadingTask(
|
|
key="test",
|
|
folder="folder",
|
|
name="Test"
|
|
)
|
|
|
|
# Initially all progress is false
|
|
assert not any(task.progress.values())
|
|
|
|
# Update progress
|
|
task.progress["episodes"] = True
|
|
assert task.progress["episodes"] is True
|
|
|
|
task.progress["nfo"] = True
|
|
assert task.progress["nfo"] is True
|
|
|
|
# Other progress still false
|
|
assert not task.progress["logo"]
|
|
assert not task.progress["images"]
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_task_status_lifecycle(self, background_loader_service):
|
|
"""Test task goes through complete status lifecycle."""
|
|
task = SeriesLoadingTask(
|
|
key="test",
|
|
folder="folder",
|
|
name="Test"
|
|
)
|
|
|
|
# Start with PENDING
|
|
assert task.status == LoadingStatus.PENDING
|
|
|
|
# Simulate transitions
|
|
task.status = LoadingStatus.LOADING_EPISODES
|
|
assert task.status == LoadingStatus.LOADING_EPISODES
|
|
|
|
task.status = LoadingStatus.LOADING_NFO
|
|
assert task.status == LoadingStatus.LOADING_NFO
|
|
|
|
task.status = LoadingStatus.COMPLETED
|
|
assert task.status == LoadingStatus.COMPLETED
|
|
assert task.completed_at is None # Not set automatically
|