- Modified BackgroundLoaderService to use multiple workers (default: 5) - Anime additions now process in parallel without blocking - Added comprehensive unit tests for concurrent behavior - Updated integration tests for compatibility - Updated architecture documentation
284 lines
9.1 KiB
Python
284 lines
9.1 KiB
Python
"""
|
|
Integration tests for asynchronous series data loading.
|
|
Tests the complete flow from API to database with WebSocket notifications.
|
|
|
|
Note: These tests focus on the integration between components and proper async behavior.
|
|
Mocking is used to avoid dependencies on external services (TMDB, Aniworld, etc.).
|
|
"""
|
|
import asyncio
|
|
from unittest.mock import AsyncMock, Mock
|
|
|
|
import pytest
|
|
from httpx import ASGITransport, AsyncClient
|
|
|
|
from src.server.fastapi_app import app
|
|
from src.server.services.background_loader_service import (
|
|
BackgroundLoaderService,
|
|
LoadingStatus,
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
async def client():
|
|
"""Create an async test client."""
|
|
transport = ASGITransport(app=app)
|
|
async with AsyncClient(transport=transport, base_url="http://test") as ac:
|
|
yield ac
|
|
|
|
|
|
class TestBackgroundLoaderIntegration:
|
|
"""Integration tests for BackgroundLoaderService."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_loader_initialization(self):
|
|
"""Test that background loader initializes correctly."""
|
|
mock_websocket = AsyncMock()
|
|
mock_anime_service = AsyncMock()
|
|
mock_series_app = Mock()
|
|
|
|
loader = BackgroundLoaderService(
|
|
websocket_service=mock_websocket,
|
|
anime_service=mock_anime_service,
|
|
series_app=mock_series_app
|
|
)
|
|
|
|
assert loader.websocket_service == mock_websocket
|
|
assert loader.anime_service == mock_anime_service
|
|
assert loader.series_app == mock_series_app
|
|
assert loader.task_queue is not None
|
|
assert loader.active_tasks == {}
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_loader_start_stop(self):
|
|
"""Test starting and stopping the background loader."""
|
|
mock_websocket = AsyncMock()
|
|
mock_anime_service = AsyncMock()
|
|
mock_series_app = Mock()
|
|
|
|
loader = BackgroundLoaderService(
|
|
websocket_service=mock_websocket,
|
|
anime_service=mock_anime_service,
|
|
series_app=mock_series_app
|
|
)
|
|
|
|
# Start loader
|
|
await loader.start()
|
|
assert loader.worker_task is not None
|
|
assert not loader.worker_task.done()
|
|
|
|
# Stop loader
|
|
await loader.stop()
|
|
assert loader.worker_task.done()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_add_series_loading_task(self):
|
|
"""Test adding a series loading task to the queue."""
|
|
mock_websocket = AsyncMock()
|
|
mock_anime_service = AsyncMock()
|
|
mock_series_app = Mock()
|
|
|
|
loader = BackgroundLoaderService(
|
|
websocket_service=mock_websocket,
|
|
anime_service=mock_anime_service,
|
|
series_app=mock_series_app
|
|
)
|
|
|
|
await loader.start()
|
|
|
|
try:
|
|
# Add a task
|
|
await loader.add_series_loading_task(
|
|
key="test-series",
|
|
folder="test_folder",
|
|
name="Test Series"
|
|
)
|
|
|
|
# Wait a moment for task to be processed
|
|
await asyncio.sleep(0.2)
|
|
|
|
# Verify task was added
|
|
assert "test-series" in loader.active_tasks
|
|
task = loader.active_tasks["test-series"]
|
|
assert task.key == "test-series"
|
|
assert task.name == "Test Series"
|
|
assert task.folder == "test_folder"
|
|
finally:
|
|
await loader.stop()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_multiple_tasks_concurrent(self):
|
|
"""Test loader handles multiple concurrent tasks."""
|
|
mock_websocket = AsyncMock()
|
|
mock_anime_service = AsyncMock()
|
|
mock_series_app = Mock()
|
|
|
|
# Setup mocks to simulate async work
|
|
mock_anime_service.scan_series_episodes = AsyncMock()
|
|
mock_anime_service.update_series = AsyncMock()
|
|
mock_series_app.nfo_service = Mock()
|
|
mock_series_app.nfo_service.create_nfo_async = AsyncMock()
|
|
|
|
loader = BackgroundLoaderService(
|
|
websocket_service=mock_websocket,
|
|
anime_service=mock_anime_service,
|
|
series_app=mock_series_app
|
|
)
|
|
|
|
await loader.start()
|
|
|
|
try:
|
|
# Add multiple tasks quickly
|
|
series_count = 3
|
|
for i in range(series_count):
|
|
await loader.add_series_loading_task(
|
|
key=f"series-{i}",
|
|
folder=f"folder_{i}",
|
|
name=f"Series {i}"
|
|
)
|
|
|
|
# Wait for processing
|
|
await asyncio.sleep(0.5)
|
|
|
|
# Verify all tasks were registered
|
|
assert len(loader.active_tasks) >= series_count
|
|
finally:
|
|
await loader.stop()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_graceful_shutdown(self):
|
|
"""Test graceful shutdown with pending tasks."""
|
|
mock_websocket = AsyncMock()
|
|
mock_anime_service = AsyncMock()
|
|
mock_series_app = Mock()
|
|
|
|
loader = BackgroundLoaderService(
|
|
websocket_service=mock_websocket,
|
|
anime_service=mock_anime_service,
|
|
series_app=mock_series_app
|
|
)
|
|
|
|
await loader.start()
|
|
|
|
# Add tasks
|
|
for i in range(5):
|
|
await loader.add_series_loading_task(
|
|
key=f"series-{i}",
|
|
folder=f"folder_{i}",
|
|
name=f"Series {i}"
|
|
)
|
|
|
|
# Stop should complete without exceptions
|
|
try:
|
|
await loader.stop()
|
|
shutdown_success = True
|
|
except Exception:
|
|
shutdown_success = False
|
|
|
|
assert shutdown_success
|
|
# Check all worker tasks are done
|
|
assert all(task.done() for task in loader.worker_tasks)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_no_duplicate_tasks(self):
|
|
"""Test that duplicate tasks for same series are handled."""
|
|
mock_websocket = AsyncMock()
|
|
mock_anime_service = AsyncMock()
|
|
mock_series_app = Mock()
|
|
|
|
loader = BackgroundLoaderService(
|
|
websocket_service=mock_websocket,
|
|
anime_service=mock_anime_service,
|
|
series_app=mock_series_app
|
|
)
|
|
|
|
await loader.start()
|
|
|
|
try:
|
|
# Try to add same series multiple times
|
|
for _ in range(3):
|
|
await loader.add_series_loading_task(
|
|
key="test-series",
|
|
folder="test_folder",
|
|
name="Test Series"
|
|
)
|
|
|
|
await asyncio.sleep(0.2)
|
|
|
|
# Should only have one task for this series
|
|
series_tasks = [k for k in loader.active_tasks if k == "test-series"]
|
|
assert len(series_tasks) == 1
|
|
finally:
|
|
await loader.stop()
|
|
|
|
|
|
class TestLoadingStatusEnum:
|
|
"""Tests for LoadingStatus enum."""
|
|
|
|
def test_loading_status_values(self):
|
|
"""Test LoadingStatus enum has expected values."""
|
|
assert LoadingStatus.PENDING.value == "pending"
|
|
assert LoadingStatus.LOADING_EPISODES.value == "loading_episodes"
|
|
assert LoadingStatus.LOADING_NFO.value == "loading_nfo"
|
|
assert LoadingStatus.LOADING_LOGO.value == "loading_logo"
|
|
assert LoadingStatus.LOADING_IMAGES.value == "loading_images"
|
|
assert LoadingStatus.COMPLETED.value == "completed"
|
|
assert LoadingStatus.FAILED.value == "failed"
|
|
|
|
def test_loading_status_string_repr(self):
|
|
"""Test LoadingStatus can be used as strings."""
|
|
status = LoadingStatus.LOADING_EPISODES
|
|
# The enum string representation includes the class name
|
|
assert status.value == "loading_episodes"
|
|
assert status == LoadingStatus.LOADING_EPISODES
|
|
|
|
|
|
class TestAsyncBehavior:
|
|
"""Tests to verify async and non-blocking behavior."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_adding_tasks_is_fast(self):
|
|
"""Test that adding tasks doesn't block."""
|
|
mock_websocket = AsyncMock()
|
|
mock_anime_service = AsyncMock()
|
|
mock_series_app = Mock()
|
|
|
|
# Make operations slow
|
|
async def slow_operation(*args, **kwargs):
|
|
await asyncio.sleep(0.5)
|
|
|
|
mock_anime_service.scan_series_episodes = AsyncMock(side_effect=slow_operation)
|
|
mock_anime_service.update_series = AsyncMock()
|
|
mock_series_app.nfo_service = Mock()
|
|
mock_series_app.nfo_service.create_nfo_async = AsyncMock(side_effect=slow_operation)
|
|
|
|
loader = BackgroundLoaderService(
|
|
websocket_service=mock_websocket,
|
|
anime_service=mock_anime_service,
|
|
series_app=mock_series_app
|
|
)
|
|
|
|
await loader.start()
|
|
|
|
try:
|
|
# Add multiple series quickly
|
|
start_time = asyncio.get_event_loop().time()
|
|
|
|
for i in range(5):
|
|
await loader.add_series_loading_task(
|
|
key=f"series-{i}",
|
|
folder=f"folder_{i}",
|
|
name=f"Series {i}"
|
|
)
|
|
|
|
add_time = asyncio.get_event_loop().time() - start_time
|
|
|
|
# Adding tasks should be fast (< 0.1s for 5 series)
|
|
# even though processing is slow
|
|
assert add_time < 0.1
|
|
|
|
# Verify all were queued
|
|
await asyncio.sleep(0.1)
|
|
assert len(loader.active_tasks) == 5
|
|
finally:
|
|
await loader.stop()
|