Add test suite for Lyria Studio backend
- conftest.py: isolated async test fixtures with temp DB and directories - test_waveform.py: waveform generation, downsampling, caching, SVG output - test_api.py: full API endpoint tests (health, models, clips CRUD, waveform endpoint, admin cache clearing) - test_analysis.py: BPM detection, key detection, clip analysis - test_models.py: request validation, model registry, prompt building - Fixed off-by-one in waveform.py downsampling (linspace upper bound) - All 40 tests passing
This commit is contained in:
1 parent
3500c73167
commit
6c1e1b4509
7 files changed
+742
-1
No files matched your search
@@ -0,0 +1,139 @@
|
||||
"""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
|
||||
Reference in new issue
Block a user