mirror of
https://github.com/denizsafak/abogen.git
synced 2026-07-18 13:40:27 +02:00
refactor(tests): add auto-discovery test system for TTS plugins
- Create tests/plugins/ with auto-discovery fixtures and generic tests - Add conftest.py with plugin_ids, loaded_plugin, host_context fixtures - Add test_all_plugins.py with 3 test classes: - TestAllPluginsManifest: validates manifest structure - TestAllPluginsEngine: validates engine lifecycle contract - TestAllPluginsCapabilities: validates capability implementation - Update docs/testing.md with auto-discovery documentation - Plugin-specific tests remain in tests/test_*_plugin.py for integration New plugins in plugins/ are now automatically tested without manual test creation.
This commit is contained in:
@@ -0,0 +1,169 @@
|
||||
"""Fixtures for auto-discovery plugin tests.
|
||||
|
||||
This module provides shared fixtures for testing all plugins:
|
||||
- plugin_ids: List of all plugin IDs from plugins/ directory
|
||||
- plugin_dir: Path to a specific plugin directory (parametrized)
|
||||
- loaded_plugin: Loaded plugin data (manifest, model_requirements, create_engine)
|
||||
- host_context: Test HostContext with fake HTTP client
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
from abogen.tts_plugin.host_context import HostContext
|
||||
from abogen.tts_plugin.loader import load_plugin_from_dir
|
||||
|
||||
|
||||
def _discover_plugin_ids() -> list[str]:
|
||||
"""Discover all plugin IDs from the plugins/ directory.
|
||||
|
||||
Returns:
|
||||
List of plugin IDs (directory names with __init__.py).
|
||||
"""
|
||||
plugins_dir = Path(__file__).parent.parent.parent / "plugins"
|
||||
plugin_ids = []
|
||||
if plugins_dir.exists():
|
||||
for item in sorted(plugins_dir.iterdir()):
|
||||
if item.is_dir() and not item.name.startswith("."):
|
||||
init_file = item / "__init__.py"
|
||||
if init_file.exists():
|
||||
plugin_ids.append(item.name)
|
||||
return plugin_ids
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def plugins_dir() -> Path:
|
||||
"""Return the path to the plugins directory."""
|
||||
return Path(__file__).parent.parent.parent / "plugins"
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def plugin_ids(plugins_dir: Path) -> list[str]:
|
||||
"""Return a list of all plugin IDs from the plugins/ directory.
|
||||
|
||||
This fixture discovers all subdirectories in plugins/ that contain
|
||||
an __init__.py file (i.e., valid plugin directories).
|
||||
|
||||
Returns:
|
||||
List of plugin IDs (directory names).
|
||||
"""
|
||||
return _discover_plugin_ids()
|
||||
|
||||
|
||||
@pytest.fixture(params=_discover_plugin_ids())
|
||||
def plugin_id(request: pytest.FixtureRequest) -> str:
|
||||
"""Parametrized fixture returning each plugin ID.
|
||||
|
||||
This fixture is parametrized to run tests for each plugin.
|
||||
"""
|
||||
return request.param
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def plugin_dir(plugins_dir: Path, plugin_id: str) -> Path:
|
||||
"""Return the path to a specific plugin directory.
|
||||
|
||||
Args:
|
||||
plugins_dir: Base plugins directory.
|
||||
plugin_id: The plugin ID (directory name).
|
||||
|
||||
Returns:
|
||||
Path to the plugin directory.
|
||||
"""
|
||||
return plugins_dir / plugin_id
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def loaded_plugin(plugin_dir: Path):
|
||||
"""Load a plugin and return its load result.
|
||||
|
||||
This fixture loads the plugin using the loader, providing access to:
|
||||
- manifest: PluginManifest
|
||||
- model_requirements: tuple[ModelManifest, ...]
|
||||
- create_engine: Callable
|
||||
- module: The loaded module
|
||||
|
||||
Args:
|
||||
plugin_dir: Path to the plugin directory.
|
||||
|
||||
Returns:
|
||||
PluginLoadResult from load_plugin_from_dir.
|
||||
|
||||
Raises:
|
||||
pytest.skip: If plugin fails to load (should not happen for valid plugins).
|
||||
"""
|
||||
from abogen.tts_plugin.loader import PluginLoadResult
|
||||
|
||||
result = load_plugin_from_dir(plugin_dir)
|
||||
if not result.success:
|
||||
pytest.fail(
|
||||
f"Plugin {plugin_dir.name} failed to load: "
|
||||
f"{result.error.errors if result.error else 'Unknown error'}"
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def host_context(tmp_path: Path) -> HostContext:
|
||||
"""Create a test HostContext with fake HTTP client.
|
||||
|
||||
Args:
|
||||
tmp_path: Pytest tmp_path fixture for config directory.
|
||||
|
||||
Returns:
|
||||
HostContext with fake HTTP client and test logger.
|
||||
"""
|
||||
class FakeHttpClient:
|
||||
"""Fake HTTP client for testing."""
|
||||
|
||||
def get(self, url: str, **kwargs: object) -> object:
|
||||
"""Fake GET request."""
|
||||
return None
|
||||
|
||||
def post(self, url: str, **kwargs: object) -> object:
|
||||
"""Fake POST request."""
|
||||
return None
|
||||
|
||||
return HostContext(
|
||||
config_dir=tmp_path,
|
||||
logger=logging.getLogger("test"),
|
||||
http_client=FakeHttpClient(),
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def engine_config() -> Any:
|
||||
"""Return a default EngineConfig for testing.
|
||||
|
||||
Returns:
|
||||
EngineConfig with device="cpu" for testing.
|
||||
"""
|
||||
from abogen.tts_plugin.types import EngineConfig
|
||||
return EngineConfig(device="cpu")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def create_engine(loaded_plugin, host_context, engine_config):
|
||||
"""Create an engine instance from a loaded plugin.
|
||||
|
||||
This fixture creates an engine and ensures proper cleanup via dispose.
|
||||
|
||||
Args:
|
||||
loaded_plugin: Loaded plugin result.
|
||||
host_context: Test HostContext.
|
||||
engine_config: EngineConfig for engine creation.
|
||||
|
||||
Yields:
|
||||
Engine instance.
|
||||
"""
|
||||
from abogen.tts_plugin.engine import Engine
|
||||
|
||||
engine = loaded_plugin.create_engine(host_context, None, engine_config)
|
||||
assert isinstance(engine, Engine)
|
||||
yield engine
|
||||
engine.dispose()
|
||||
Reference in New Issue
Block a user