df779c9e6d
* fix: Sync issues - AudioDB warmig + automatic sync skips * progress ui/ux + discovery and album fixes * artist fixes * several request level fixes and improvements * handle request fails + artist refresh + resilience fixes * fix format * fix stop sync fail + last.fn mbid issues + failures/validation reworks
154 lines
5.1 KiB
Python
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', generation=0)
|
|
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', generation=0)
|
|
assert True
|