- Created QueueRepository adapter in src/server/services/queue_repository.py - Refactored DownloadService to use repository pattern instead of JSON - Updated application startup to initialize download service from database - Updated all test fixtures to use MockQueueRepository - All 1104 tests passing
394 lines
13 KiB
Python
394 lines
13 KiB
Python
"""
|
|
Download System Stress Testing.
|
|
|
|
This module tests the download queue and management system under
|
|
heavy load and stress conditions.
|
|
"""
|
|
|
|
import asyncio
|
|
from unittest.mock import AsyncMock, MagicMock
|
|
|
|
import pytest
|
|
|
|
from src.server.models.download import DownloadPriority, EpisodeIdentifier
|
|
from src.server.services.anime_service import AnimeService
|
|
from src.server.services.download_service import DownloadService
|
|
|
|
|
|
@pytest.mark.performance
|
|
class TestDownloadQueueStress:
|
|
"""Stress testing for download queue."""
|
|
|
|
@pytest.fixture
|
|
def mock_anime_service(self):
|
|
"""Create mock AnimeService."""
|
|
service = MagicMock(spec=AnimeService)
|
|
service.download = AsyncMock(return_value=True)
|
|
return service
|
|
|
|
@pytest.fixture
|
|
def download_service(self, mock_anime_service, tmp_path):
|
|
"""Create download service with mock repository."""
|
|
from tests.unit.test_download_service import MockQueueRepository
|
|
mock_repo = MockQueueRepository()
|
|
service = DownloadService(
|
|
anime_service=mock_anime_service,
|
|
max_retries=3,
|
|
queue_repository=mock_repo,
|
|
)
|
|
return service
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_concurrent_download_additions(
|
|
self, download_service
|
|
):
|
|
"""Test adding many downloads concurrently."""
|
|
num_downloads = 100
|
|
|
|
# Add downloads concurrently
|
|
tasks = [
|
|
download_service.add_to_queue(
|
|
serie_id=f"series-{i}",
|
|
serie_folder=f"series_{i}",
|
|
serie_name=f"Test Series {i}",
|
|
episodes=[EpisodeIdentifier(season=1, episode=1)],
|
|
priority=DownloadPriority.NORMAL,
|
|
)
|
|
for i in range(num_downloads)
|
|
]
|
|
|
|
results = await asyncio.gather(*tasks, return_exceptions=True)
|
|
|
|
# Count successful additions
|
|
successful = sum(
|
|
1 for r in results if not isinstance(r, Exception)
|
|
)
|
|
|
|
# Should handle at least 90% successfully
|
|
success_rate = (successful / num_downloads) * 100
|
|
assert (
|
|
success_rate >= 90.0
|
|
), f"Queue addition success rate too low: {success_rate}%"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_queue_capacity(self, download_service):
|
|
"""Test queue behavior at capacity."""
|
|
# Fill queue beyond reasonable capacity
|
|
num_downloads = 1000
|
|
|
|
for i in range(num_downloads):
|
|
try:
|
|
await download_service.add_to_queue(
|
|
serie_id=f"series-{i}",
|
|
serie_folder=f"series_folder",
|
|
serie_name=f"Test Series {i}",
|
|
episodes=[EpisodeIdentifier(season=1, episode=1)],
|
|
priority=DownloadPriority.NORMAL,
|
|
)
|
|
except Exception:
|
|
# Queue might have limits
|
|
pass
|
|
|
|
# Queue should still be functional
|
|
status = await download_service.get_queue_status()
|
|
assert status is not None, "Queue became non-functional"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_rapid_queue_operations(self, download_service):
|
|
"""Test rapid add/remove operations."""
|
|
num_operations = 200
|
|
|
|
operations = []
|
|
for i in range(num_operations):
|
|
if i % 2 == 0:
|
|
# Add operation
|
|
operations.append(
|
|
download_service.add_to_queue(
|
|
serie_id=f"series-{i}",
|
|
serie_folder=f"series_folder",
|
|
serie_name=f"Test Series {i}",
|
|
episodes=[EpisodeIdentifier(season=1, episode=1)],
|
|
priority=DownloadPriority.NORMAL,
|
|
)
|
|
)
|
|
else:
|
|
# Remove operation - get item IDs from pending queue
|
|
item_ids = list(
|
|
download_service._pending_items_by_id.keys()
|
|
)
|
|
if item_ids:
|
|
operations.append(
|
|
download_service.remove_from_queue([item_ids[0]])
|
|
)
|
|
|
|
results = await asyncio.gather(
|
|
*operations, return_exceptions=True
|
|
)
|
|
|
|
# Most operations should succeed
|
|
successful = sum(
|
|
1 for r in results if not isinstance(r, Exception)
|
|
)
|
|
success_rate = (successful / len(results)) * 100 if results else 0
|
|
|
|
assert success_rate >= 80.0, "Operation success rate too low"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_concurrent_queue_reads(self, download_service):
|
|
"""Test concurrent queue status reads."""
|
|
# Add some items to queue
|
|
for i in range(10):
|
|
await download_service.add_to_queue(
|
|
serie_id=f"series-{i}",
|
|
serie_folder=f"series_folder",
|
|
serie_name=f"Test Series {i}",
|
|
episodes=[EpisodeIdentifier(season=1, episode=1)],
|
|
priority=DownloadPriority.NORMAL,
|
|
)
|
|
|
|
# Perform many concurrent reads
|
|
num_reads = 100
|
|
tasks = [
|
|
download_service.get_queue_status() for _ in range(num_reads)
|
|
]
|
|
|
|
results = await asyncio.gather(*tasks, return_exceptions=True)
|
|
|
|
# All reads should succeed
|
|
successful = sum(
|
|
1 for r in results if not isinstance(r, Exception)
|
|
)
|
|
|
|
assert (
|
|
successful == num_reads
|
|
), "Some queue reads failed"
|
|
|
|
|
|
@pytest.mark.performance
|
|
class TestDownloadMemoryUsage:
|
|
"""Test memory usage under load."""
|
|
|
|
@pytest.fixture
|
|
def mock_anime_service(self):
|
|
"""Create mock AnimeService."""
|
|
service = MagicMock(spec=AnimeService)
|
|
service.download = AsyncMock(return_value=True)
|
|
return service
|
|
|
|
@pytest.fixture
|
|
def download_service(self, mock_anime_service, tmp_path):
|
|
"""Create download service with mock repository."""
|
|
from tests.unit.test_download_service import MockQueueRepository
|
|
mock_repo = MockQueueRepository()
|
|
service = DownloadService(
|
|
anime_service=mock_anime_service,
|
|
max_retries=3,
|
|
queue_repository=mock_repo,
|
|
)
|
|
return service
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_queue_memory_leak(self, download_service):
|
|
"""Test for memory leaks in queue operations."""
|
|
# This is a placeholder for memory profiling
|
|
# In real implementation, would use memory_profiler
|
|
# or similar tools
|
|
|
|
# Perform many operations
|
|
for i in range(1000):
|
|
await download_service.add_to_queue(
|
|
serie_id=f"series-{i}",
|
|
serie_folder=f"series_folder",
|
|
serie_name=f"Test Series {i}",
|
|
episodes=[EpisodeIdentifier(season=1, episode=1)],
|
|
priority=DownloadPriority.NORMAL,
|
|
)
|
|
|
|
if i % 100 == 0:
|
|
# Clear some items periodically
|
|
item_ids = list(download_service._pending_items_by_id.keys())
|
|
if item_ids:
|
|
await download_service.remove_from_queue([item_ids[0]])
|
|
|
|
# Service should still be functional
|
|
status = await download_service.get_queue_status()
|
|
assert status is not None
|
|
|
|
|
|
@pytest.mark.performance
|
|
class TestDownloadConcurrency:
|
|
"""Test concurrent download handling."""
|
|
|
|
@pytest.fixture
|
|
def mock_anime_service(self):
|
|
"""Create mock AnimeService with slow downloads."""
|
|
service = MagicMock(spec=AnimeService)
|
|
|
|
async def slow_download(*args, **kwargs):
|
|
# Simulate slow download
|
|
await asyncio.sleep(0.1)
|
|
return True
|
|
|
|
service.download = slow_download
|
|
return service
|
|
|
|
@pytest.fixture
|
|
def download_service(self, mock_anime_service, tmp_path):
|
|
"""Create download service with mock repository."""
|
|
from tests.unit.test_download_service import MockQueueRepository
|
|
mock_repo = MockQueueRepository()
|
|
service = DownloadService(
|
|
anime_service=mock_anime_service,
|
|
max_retries=3,
|
|
queue_repository=mock_repo,
|
|
)
|
|
return service
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_concurrent_download_execution(
|
|
self, download_service
|
|
):
|
|
"""Test executing multiple downloads concurrently."""
|
|
# Start multiple downloads
|
|
num_downloads = 20
|
|
tasks = [
|
|
download_service.add_to_queue(
|
|
serie_id=f"series-{i}",
|
|
serie_folder=f"series_folder",
|
|
serie_name=f"Test Series {i}",
|
|
episodes=[EpisodeIdentifier(season=1, episode=1)],
|
|
priority=DownloadPriority.NORMAL,
|
|
)
|
|
for i in range(num_downloads)
|
|
]
|
|
|
|
await asyncio.gather(*tasks)
|
|
|
|
# All downloads should be queued
|
|
status = await download_service.get_queue_status()
|
|
total = (
|
|
len(status.pending_queue) +
|
|
len(status.active_downloads) +
|
|
len(status.completed_downloads)
|
|
)
|
|
assert total <= num_downloads
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_download_priority_under_load(
|
|
self, download_service
|
|
):
|
|
"""Test that priority is respected under load."""
|
|
# Add downloads with different priorities
|
|
await download_service.add_to_queue(
|
|
serie_id="series-1",
|
|
serie_folder=f"series_folder",
|
|
serie_name="Test Series 1",
|
|
episodes=[EpisodeIdentifier(season=1, episode=1)],
|
|
priority=DownloadPriority.LOW,
|
|
)
|
|
await download_service.add_to_queue(
|
|
serie_id="series-2",
|
|
serie_folder=f"series_folder",
|
|
serie_name="Test Series 2",
|
|
episodes=[EpisodeIdentifier(season=1, episode=1)],
|
|
priority=DownloadPriority.HIGH,
|
|
)
|
|
await download_service.add_to_queue(
|
|
serie_id="series-3",
|
|
serie_folder=f"series_folder",
|
|
serie_name="Test Series 3",
|
|
episodes=[EpisodeIdentifier(season=1, episode=1)],
|
|
priority=DownloadPriority.NORMAL,
|
|
)
|
|
|
|
# High priority should be processed first
|
|
status = await download_service.get_queue_status()
|
|
assert status is not None
|
|
|
|
|
|
@pytest.mark.performance
|
|
class TestDownloadErrorHandling:
|
|
"""Test error handling under stress."""
|
|
|
|
@pytest.fixture
|
|
def mock_failing_anime_service(self):
|
|
"""Create mock AnimeService that fails downloads."""
|
|
service = MagicMock(spec=AnimeService)
|
|
service.download = AsyncMock(
|
|
side_effect=Exception("Download failed")
|
|
)
|
|
return service
|
|
|
|
@pytest.fixture
|
|
def download_service_failing(
|
|
self, mock_failing_anime_service, tmp_path
|
|
):
|
|
"""Create download service with failing mock."""
|
|
from tests.unit.test_download_service import MockQueueRepository
|
|
mock_repo = MockQueueRepository()
|
|
service = DownloadService(
|
|
anime_service=mock_failing_anime_service,
|
|
max_retries=3,
|
|
queue_repository=mock_repo,
|
|
)
|
|
return service
|
|
|
|
@pytest.fixture
|
|
def mock_anime_service(self):
|
|
"""Create mock AnimeService."""
|
|
service = MagicMock(spec=AnimeService)
|
|
service.download = AsyncMock(return_value=True)
|
|
return service
|
|
|
|
@pytest.fixture
|
|
def download_service(self, mock_anime_service, tmp_path):
|
|
"""Create download service with mock repository."""
|
|
from tests.unit.test_download_service import MockQueueRepository
|
|
mock_repo = MockQueueRepository()
|
|
service = DownloadService(
|
|
anime_service=mock_anime_service,
|
|
max_retries=3,
|
|
queue_repository=mock_repo,
|
|
)
|
|
return service
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_multiple_failed_downloads(
|
|
self, download_service_failing
|
|
):
|
|
"""Test handling of many failed downloads."""
|
|
# Add multiple downloads
|
|
for i in range(50):
|
|
await download_service_failing.add_to_queue(
|
|
serie_id=f"series-{i}",
|
|
serie_folder=f"series_folder",
|
|
serie_name=f"Test Series {i}",
|
|
episodes=[EpisodeIdentifier(season=1, episode=1)],
|
|
priority=DownloadPriority.NORMAL,
|
|
)
|
|
|
|
# Service should remain stable despite failures
|
|
status = await download_service_failing.get_queue_status()
|
|
assert status is not None
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_recovery_from_errors(self, download_service):
|
|
"""Test system recovery after errors."""
|
|
# Cause some errors
|
|
try:
|
|
await download_service.remove_from_queue(["nonexistent-id"])
|
|
except Exception:
|
|
pass
|
|
|
|
# System should still work
|
|
await download_service.add_to_queue(
|
|
serie_id="series-1",
|
|
serie_folder=f"series_folder",
|
|
serie_name="Test Series 1",
|
|
episodes=[EpisodeIdentifier(season=1, episode=1)],
|
|
priority=DownloadPriority.NORMAL,
|
|
)
|
|
|
|
status = await download_service.get_queue_status()
|
|
assert status is not None
|