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:
rin.kagamine committed 2026-05-31 08:26:01 -04:00
1 parent 3500c73167
commit 6c1e1b4509
7 files changed
+742 -1

No files matched your search

+139
View File
@@ -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