first commit
This commit is contained in:
34
tests/conftest.py
Normal file
34
tests/conftest.py
Normal file
@ -0,0 +1,34 @@
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
from main import create_app # Import the app factory function
|
||||
from config import settings # Import settings to monkeypatch
|
||||
import os
|
||||
|
||||
@pytest.fixture
|
||||
def tmp_audio_dir(tmp_path):
|
||||
"""Provides a temporary directory for audio caching for each test."""
|
||||
audio_dir = tmp_path / "test_audio_cache"
|
||||
audio_dir.mkdir()
|
||||
return audio_dir
|
||||
|
||||
@pytest.fixture
|
||||
def app_instance(tmp_audio_dir, monkeypatch):
|
||||
"""
|
||||
Provides a fresh FastAPI application instance for each test,
|
||||
with its AUDIO_CACHE_DIR redirected to a temporary location.
|
||||
"""
|
||||
monkeypatch.setattr(settings, "AUDIO_CACHE_DIR", str(tmp_audio_dir))
|
||||
app = create_app()
|
||||
return app
|
||||
|
||||
@pytest.fixture
|
||||
def app_client(app_instance):
|
||||
"""Provides a TestClient instance for the FastAPI application."""
|
||||
with TestClient(app_instance) as client:
|
||||
yield client
|
||||
|
||||
@pytest.fixture
|
||||
def piper_engine(app_instance):
|
||||
"""Provides the PiperEngine instance from the registry of the fresh app instance."""
|
||||
# Access the engine registry from the app_instance
|
||||
return app_instance.ENGINE_REGISTRY.get("piper")
|
||||
123
tests/test_api.py
Normal file
123
tests/test_api.py
Normal file
@ -0,0 +1,123 @@
|
||||
from fastapi.testclient import TestClient
|
||||
import pytest
|
||||
# from main import app, ENGINE_REGISTRY # No longer needed, using fixtures
|
||||
import shutil
|
||||
import asyncio
|
||||
|
||||
TEST_TEXT = "Dies ist ein NovaAi Test."
|
||||
# Ensure the engine being tested is in the default .env config
|
||||
ENGINE = "piper"
|
||||
MODEL = "de_DE-thorsten-high"
|
||||
FORMAT = "ogg"
|
||||
|
||||
# === Happy Path Tests ===
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_tts_link_success(app_client):
|
||||
"""Tests successful synthesis returning a URL."""
|
||||
resp = app_client.post("/tts", json={
|
||||
"text": TEST_TEXT,
|
||||
"engine": ENGINE,
|
||||
"model": MODEL,
|
||||
"format": FORMAT
|
||||
})
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert "audio_url" in data
|
||||
|
||||
# Also test the audio download endpoint
|
||||
audio_url = data['audio_url']
|
||||
audio_resp = app_client.get(audio_url)
|
||||
assert audio_resp.status_code == 200
|
||||
assert len(audio_resp.content) > 1000
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_tts_base64_success(app_client):
|
||||
"""Tests successful synthesis returning base64 data."""
|
||||
resp = app_client.post("/tts?as=true", json={
|
||||
"text": TEST_TEXT,
|
||||
"engine": ENGINE,
|
||||
"model": MODEL,
|
||||
"format": FORMAT
|
||||
})
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert "audio_base64" in data
|
||||
assert len(data["audio_base64"]) > 1000
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_health_and_engines_endpoints(app_client):
|
||||
"""Tests the /health and /engines endpoints for correct responses."""
|
||||
resp_engines = app_client.get("/engines")
|
||||
assert resp_engines.status_code == 200
|
||||
engines = resp_engines.json()
|
||||
assert ENGINE in engines
|
||||
|
||||
resp_health = app_client.get("/health")
|
||||
assert resp_health.status_code == 200
|
||||
health = resp_health.json()
|
||||
assert health["status"][ENGINE] == "ok"
|
||||
|
||||
# === Failure Path Tests ===
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_tts_fails_with_invalid_engine(app_client):
|
||||
"""Tests that a request with a non-existent engine fails with HTTP 404."""
|
||||
response = app_client.post("/tts", json={
|
||||
"text": TEST_TEXT,
|
||||
"engine": "non_existent_engine",
|
||||
"model": MODEL,
|
||||
})
|
||||
assert response.status_code == 404
|
||||
assert "Engine 'non_existent_engine' not found" in response.json()["detail"]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_tts_fails_with_invalid_model(app_client):
|
||||
"""Tests that a request with a non-existent model fails with HTTP 400."""
|
||||
response = app_client.post("/tts", json={
|
||||
"text": TEST_TEXT,
|
||||
"engine": ENGINE,
|
||||
"model": "non_existent_model",
|
||||
})
|
||||
assert response.status_code == 400
|
||||
assert "Model 'non_existent_model' not found" in response.json()["detail"]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_tts_fails_with_invalid_speaker(app_client, piper_engine):
|
||||
"""Tests that a request with a non-existent speaker fails with HTTP 400."""
|
||||
# This model has speakers, so requesting a non-existent one should fail.
|
||||
# We need to pick a model that is known to have multiple speakers
|
||||
# The default piper engine models should have this.
|
||||
model_with_speakers = "en_US-kristin-medium" # Example model with speakers
|
||||
|
||||
# First, check if the model is actually available to test against
|
||||
if model_with_speakers not in piper_engine.list_models():
|
||||
pytest.skip(f"Model '{model_with_speakers}' not available for '{ENGINE}' to test against.")
|
||||
|
||||
response = app_client.post("/tts", json={
|
||||
"text": "This is a test.",
|
||||
"engine": ENGINE,
|
||||
"model": model_with_speakers,
|
||||
"speaker": "non_existent_speaker"
|
||||
})
|
||||
assert response.status_code == 400
|
||||
assert "Speaker 'non_existent_speaker' not found" in response.json()["detail"]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_tts_fails_with_unavailable_engine(app_client, piper_engine, monkeypatch):
|
||||
"""Tests that a request fails with HTTP 503 if an engine is unhealthy."""
|
||||
# Simulate the piper executable being not found by monkeypatching the specific instance
|
||||
monkeypatch.setattr(piper_engine, "piper_executable", None)
|
||||
|
||||
response = app_client.post("/tts", json={
|
||||
"text": TEST_TEXT,
|
||||
"engine": ENGINE,
|
||||
"model": MODEL,
|
||||
})
|
||||
|
||||
assert response.status_code == 503
|
||||
assert "is not available" in response.json()["detail"]
|
||||
|
||||
|
||||
|
||||
|
||||
70
tests/test_f5_tts.py
Normal file
70
tests/test_f5_tts.py
Normal file
@ -0,0 +1,70 @@
|
||||
import unittest
|
||||
import os
|
||||
import shutil
|
||||
import asyncio
|
||||
from importlib.resources import files
|
||||
from engines.f5_tts import F5TTSEngine
|
||||
|
||||
class TestF5TTSEngine(unittest.TestCase):
|
||||
async def asyncSetUp(self):
|
||||
self.engine = F5TTSEngine()
|
||||
self.voices_dir = "engines/f5-tts-voices"
|
||||
self.test_speaker_name = "test_speaker"
|
||||
self.test_speaker_wav = os.path.join(self.voices_dir, f"{self.test_speaker_name}.wav")
|
||||
self.test_speaker_txt = os.path.join(self.voices_dir, f"{self.test_speaker_name}.txt")
|
||||
|
||||
# Create a dummy speaker for testing
|
||||
if not await asyncio.to_thread(os.path.exists, self.test_speaker_wav):
|
||||
default_wav_path = str(files("f5_tts").joinpath("infer/examples/basic/basic_ref_en.wav"))
|
||||
await asyncio.to_thread(shutil.copy, default_wav_path, self.test_speaker_wav)
|
||||
if not await asyncio.to_thread(os.path.exists, self.test_speaker_txt):
|
||||
await asyncio.to_thread(lambda: open(self.test_speaker_txt, "w").write("Some call me nature, others call me mother nature."))
|
||||
|
||||
# Reload speakers to include the new test speaker
|
||||
self.engine._load_speakers()
|
||||
|
||||
async def asyncTearDown(self):
|
||||
# Clean up the dummy speaker files
|
||||
if await asyncio.to_thread(os.path.exists, self.test_speaker_wav):
|
||||
await asyncio.to_thread(os.remove, self.test_speaker_wav)
|
||||
if await asyncio.to_thread(os.path.exists, self.test_speaker_txt):
|
||||
await asyncio.to_thread(os.remove, self.test_speaker_txt)
|
||||
|
||||
async def test_synthesize_default_wav(self):
|
||||
if not self.engine.model:
|
||||
self.skipTest("F5-TTS model not initialized.")
|
||||
|
||||
text = "Hello, this is a test with the default voice."
|
||||
audio_file = await self.engine.synthesize(text, fmt="wav")
|
||||
|
||||
self.assertTrue(await asyncio.to_thread(os.path.exists, audio_file))
|
||||
self.assertTrue(await asyncio.to_thread(os.path.getsize, audio_file) > 0)
|
||||
|
||||
await asyncio.to_thread(os.remove, audio_file)
|
||||
|
||||
async def test_synthesize_custom_speaker_wav(self):
|
||||
if not self.engine.model:
|
||||
self.skipTest("F5-TTS model not initialized.")
|
||||
|
||||
text = "Hello, this is a test with a custom voice."
|
||||
audio_file = await self.engine.synthesize(text, speaker=self.test_speaker_name, fmt="wav")
|
||||
|
||||
self.assertTrue(await asyncio.to_thread(os.path.exists, audio_file))
|
||||
self.assertTrue(await asyncio.to_thread(os.path.getsize, audio_file) > 0)
|
||||
|
||||
await asyncio.to_thread(os.remove, audio_file)
|
||||
|
||||
async def test_synthesize_ogg(self):
|
||||
if not self.engine.model:
|
||||
self.skipTest("F5-TTS model not initialized.")
|
||||
|
||||
text = "Hello, this is a test in ogg format."
|
||||
audio_file = await self.engine.synthesize(text, fmt="ogg")
|
||||
|
||||
self.assertTrue(await asyncio.to_thread(os.path.exists, audio_file))
|
||||
self.assertTrue(await asyncio.to_thread(os.path.getsize, audio_file) > 0)
|
||||
|
||||
await asyncio.to_thread(os.remove, audio_file)
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user