Files
musicseerr/backend/tests/test_audiodb_parallel.py
T
Harvey e84f2d6127 feat: robust library sync with adaptive watchdog, resume-on-failure &… (#22)
* feat: robust library sync with adaptive watchdog, resume-on-failure & parallel pre-warming

* update copy
2026-04-05 15:36:42 +01:00

154 lines
5.1 KiB
Python

"""Tests for AudioDB parallel prewarm with semaphore gating."""
import asyncio
import tempfile
from pathlib import Path
import pytest
from unittest.mock import AsyncMock, MagicMock
from services.precache.audiodb_phase import AudioDBPhase
def _make_settings(concurrency=4, delay=0.0, enabled=True):
s = MagicMock()
s.audiodb_enabled = enabled
s.audiodb_name_search_fallback = False
s.audiodb_prewarm_concurrency = concurrency
s.audiodb_prewarm_delay = delay
return s
def _make_prefs(settings=None):
if settings is None:
settings = _make_settings()
prefs = MagicMock()
prefs.get_advanced_settings.return_value = settings
return prefs
def _make_status_service():
status = MagicMock()
status.update_phase = AsyncMock()
status.update_progress = AsyncMock()
status.persist_progress = AsyncMock()
status.skip_phase = AsyncMock()
status.is_cancelled.return_value = False
return status
def _make_cover_repo(tmpdir):
repo = AsyncMock()
repo.cache_dir = Path(tmpdir)
return repo
class TestAudioDBParallel:
@pytest.mark.asyncio
async def test_concurrent_processing(self):
"""Multiple artists should be processed concurrently up to the semaphore limit."""
concurrency = 2
prefs = _make_prefs(_make_settings(concurrency=concurrency, delay=0.0))
audiodb_svc = AsyncMock()
audiodb_svc.get_cached_artist_images = AsyncMock(return_value=None)
audiodb_svc.get_cached_album_images = AsyncMock(return_value=None)
audiodb_svc.fetch_and_cache_artist_images = AsyncMock(return_value=None)
with tempfile.TemporaryDirectory() as tmpdir:
phase = AudioDBPhase(
cover_repo=_make_cover_repo(tmpdir),
preferences_service=prefs,
audiodb_image_service=audiodb_svc,
)
artists = [{"mbid": f"mbid-{i:04d}", "name": f"Artist {i}"} for i in range(6)]
status = _make_status_service()
await phase.precache_audiodb_data(artists, [], status)
assert audiodb_svc.fetch_and_cache_artist_images.call_count == 6
assert status.update_progress.call_count == 6
assert True
@pytest.mark.asyncio
async def test_concurrency_respects_setting(self):
"""The concurrency semaphore should limit parallel execution."""
max_concurrent = 0
current_concurrent = 0
lock = asyncio.Lock()
prefs = _make_prefs(_make_settings(concurrency=2, delay=0.0))
audiodb_svc = AsyncMock()
audiodb_svc.get_cached_artist_images = AsyncMock(return_value=None)
audiodb_svc.get_cached_album_images = AsyncMock(return_value=None)
async def track_concurrency(*args, **kwargs):
nonlocal max_concurrent, current_concurrent
async with lock:
current_concurrent += 1
if current_concurrent > max_concurrent:
max_concurrent = current_concurrent
await asyncio.sleep(0.05)
async with lock:
current_concurrent -= 1
return None
audiodb_svc.fetch_and_cache_artist_images = AsyncMock(side_effect=track_concurrency)
with tempfile.TemporaryDirectory() as tmpdir:
phase = AudioDBPhase(
cover_repo=_make_cover_repo(tmpdir),
preferences_service=prefs,
audiodb_image_service=audiodb_svc,
)
artists = [{"mbid": f"mbid-{i:04d}", "name": f"Artist {i}"} for i in range(10)]
status = _make_status_service()
await phase.precache_audiodb_data(artists, [], status)
assert max_concurrent <= 2
assert max_concurrent >= 1
assert True
@pytest.mark.asyncio
async def test_disabled_audiodb_skips(self):
"""When audiodb_enabled is False, phase should be skipped."""
prefs = _make_prefs(_make_settings(enabled=False))
phase = AudioDBPhase(
cover_repo=AsyncMock(),
preferences_service=prefs,
audiodb_image_service=AsyncMock(),
)
status = _make_status_service()
await phase.precache_audiodb_data([], [], status)
status.skip_phase.assert_called_once_with('audiodb_prewarm')
assert True
@pytest.mark.asyncio
async def test_all_cached_skips(self):
"""When all items are cached, phase should be skipped."""
prefs = _make_prefs(_make_settings())
audiodb_svc = AsyncMock()
audiodb_svc.get_cached_artist_images = AsyncMock(return_value={"some": "data"})
audiodb_svc.get_cached_album_images = AsyncMock(return_value={"some": "data"})
phase = AudioDBPhase(
cover_repo=AsyncMock(),
preferences_service=prefs,
audiodb_image_service=audiodb_svc,
)
artists = [{"mbid": "mbid-0001", "name": "Artist 1"}]
status = _make_status_service()
await phase.precache_audiodb_data(artists, [], status)
status.skip_phase.assert_called_once_with('audiodb_prewarm')
assert True