"""Shared test fixtures for Lyria Studio backend.""" import asyncio import os import sys from pathlib import Path import pytest import pytest_asyncio from httpx import ASGITransport, AsyncClient # ── Path setup: ensure src/ is importable ────────────────────── BACKEND_SRC = Path(__file__).resolve().parent.parent / "src" sys.path.insert(0, str(BACKEND_SRC)) @pytest.fixture(scope="session") def event_loop(): """Create a single event loop for the entire test session.""" loop = asyncio.new_event_loop() yield loop loop.close() @pytest_asyncio.fixture async def app_client(tmp_path): """Provide an httpx AsyncClient wired to the FastAPI app with a fresh temp database and data directories. Each test gets its own isolated DB and file directories. """ # Create temp data directories data_dir = tmp_path / "data" data_dir.mkdir() clips_dir = data_dir / "clips" clips_dir.mkdir() waveforms_dir = data_dir / "waveforms" waveforms_dir.mkdir() artwork_dir = data_dir / "artwork" artwork_dir.mkdir() test_db = data_dir / "lyria.db" # Set env vars BEFORE import so module-level Path() calls use test dirs # (image_gen.py reads ARTWORK_DIR from env, main.py uses __file__-relative paths) os.environ["ARTWORK_DIR"] = str(artwork_dir) # Import modules AFTER setting env vars import image_gen as ig_mod import database as db_mod import waveform as wf_mod import main # Save originals for restoration orig_db_path = db_mod.DB_PATH orig_clips_dir = main.CLIPS_DIR orig_artwork_dir = main.ARTWORK_DIR orig_wf_clips = wf_mod.CLIPS_DIR orig_wf_wave = wf_mod.WAVEFORM_DIR orig_ig_artwork = ig_mod.ARTWORK_DIR # Patch module-level path constants to test directories db_mod.DB_PATH = test_db main.CLIPS_DIR = clips_dir main.ARTWORK_DIR = artwork_dir wf_mod.CLIPS_DIR = clips_dir wf_mod.WAVEFORM_DIR = waveforms_dir ig_mod.ARTWORK_DIR = artwork_dir # Initialize fresh database await db_mod.init_db() transport = ASGITransport(app=main.app) async with AsyncClient(transport=transport, base_url="http://test") as c: yield { "client": c, "db": test_db, "clips_dir": clips_dir, "waveforms_dir": waveforms_dir, "artwork_dir": artwork_dir, "data_dir": data_dir, } # Restore originals db_mod.DB_PATH = orig_db_path main.CLIPS_DIR = orig_clips_dir main.ARTWORK_DIR = orig_artwork_dir wf_mod.CLIPS_DIR = orig_wf_clips wf_mod.WAVEFORM_DIR = orig_wf_wave ig_mod.ARTWORK_DIR = orig_ig_artwork # Clean up env var del os.environ["ARTWORK_DIR"] # ── Helper: create a clip record + dummy MP3 ─────────────────── async def seed_test_clip(app_client, prompt="test prompt", model="lyria-3-clip-preview", duration=30): """Create a clip record in the test DB with a dummy MP3 file.""" import aiosqlite from database import create_clip clip_id = await create_clip(prompt, model, duration) filename = f"{clip_id}.mp3" mp3_path = app_client["clips_dir"] / filename mp3_path.write_bytes(b"\xff\xfb\x90\x00" + b"\x00" * 4096) async with aiosqlite.connect(app_client["db"]) as db: await db.execute( "UPDATE clips SET status = ?, filename = ? WHERE id = ?", ("ready", filename, clip_id), ) await db.commit() return clip_id # ── Helper: create a real (ffmpeg-generated) MP3 ──────────────── def create_real_mp3(output_path, duration=2): """Generate a real MP3 file using ffmpeg. Returns True if successful, False if ffmpeg is unavailable. """ import subprocess try: subprocess.run( [ "ffmpeg", "-y", "-f", "lavfi", "-i", f"anullsrc=r=22050:cl=mono", "-t", str(duration), "-q:a", "9", str(output_path) ], capture_output=True, check=True, ) return True except (FileNotFoundError, subprocess.CalledProcessError): return False