"""Pytest configuration for E2E tests.""" from __future__ import annotations import logging import os import sys from importlib.util import find_spec from pathlib import Path from typing import TYPE_CHECKING import pytest if TYPE_CHECKING: from infra import ModelPool # Configure logging logging.basicConfig( level=logging.INFO, format="%(asctime)s [%(levelname)8s] %(name)s: %(message)s", ) logger = logging.getLogger(__name__) # Only add bindings/python to path if the wheel is not installed (for local development) # This ensures CI tests use the installed wheel which contains the Rust extension _ROOT = Path(__file__).resolve().parents[1] _SRC = _ROOT / "bindings" / "python" # Check if sglang_router is already installed with the Rust extension _wheel_installed = find_spec("sglang_router.sglang_router_rs") is not None # Only add bindings/python if wheel is not installed (development mode) if not _wheel_installed and str(_SRC) not in sys.path: sys.path.insert(0, str(_SRC)) # --------------------------------------------------------------------------- # Custom pytest markers # --------------------------------------------------------------------------- def pytest_configure(config: pytest.Config) -> None: """Register custom markers.""" config.addinivalue_line( "markers", "model(name): mark test to use a specific model from the model pool", ) config.addinivalue_line( "markers", "e2e: mark test as an end-to-end test requiring GPU workers", ) config.addinivalue_line( "markers", "slow: mark test as slow-running", ) # --------------------------------------------------------------------------- # Model pool fixtures (session-scoped) # --------------------------------------------------------------------------- # Global model pool instance _model_pool: "ModelPool | None" = None def _get_requested_models(config: pytest.Config) -> list[str]: """Determine which models are needed based on collected tests. This scans all test items for @pytest.mark.model() markers and returns the unique set of models requested. """ models = set() # This is called during collection, so we need to iterate items for item in config.pluginmanager.get_plugin("main").session.items: marker = item.get_closest_marker("model") if marker and marker.args: models.add(marker.args[0]) return list(models) @pytest.fixture(scope="session") def model_pool(request: pytest.FixtureRequest) -> "ModelPool": """Session-scoped fixture providing the model pool. The model pool pre-loads all models needed by tests in this session, running them in parallel across available GPUs. Usage: @pytest.mark.model("llama-8b") def test_chat(model_pool): client = model_pool.get_client("llama-8b") ... """ global _model_pool # Import here to avoid import errors when infra is not set up from infra import MODEL_SPECS, GPUAllocator, ModelPool if _model_pool is not None: return _model_pool # Check if we should skip model startup (e.g., for unit tests) if os.environ.get("SKIP_MODEL_POOL", "").lower() in ("1", "true", "yes"): logger.info("SKIP_MODEL_POOL is set, skipping model pool startup") _model_pool = ModelPool(GPUAllocator(gpus=[])) return _model_pool # Determine which models to start # For now, start models based on environment or a default set models_env = os.environ.get("E2E_MODELS", "") if models_env: model_ids = [m.strip() for m in models_env.split(",") if m.strip()] else: # Default: start commonly needed models model_ids = ["llama-8b", "qwen-7b"] # Filter to available specs model_ids = [m for m in model_ids if m in MODEL_SPECS] if not model_ids: logger.warning("No models specified, model pool will be empty") _model_pool = ModelPool(GPUAllocator(gpus=[])) return _model_pool logger.info("Starting model pool with models: %s", model_ids) # Create and start the pool allocator = GPUAllocator() _model_pool = ModelPool(allocator) grpc_mode = os.environ.get("E2E_GRPC_MODE", "").lower() in ("1", "true", "yes") startup_timeout = int(os.environ.get("E2E_STARTUP_TIMEOUT", "300")) _model_pool.startup( model_ids=model_ids, grpc_mode=grpc_mode, startup_timeout=startup_timeout, ) # Register cleanup request.addfinalizer(_model_pool.shutdown) return _model_pool @pytest.fixture def model_client(request: pytest.FixtureRequest, model_pool: "ModelPool"): """Get OpenAI client for the model specified by @pytest.mark.model(). Usage: @pytest.mark.model("llama-8b") def test_chat(model_client): response = model_client.chat.completions.create(...) """ marker = request.node.get_closest_marker("model") if marker is None: pytest.fail( "Test must be marked with @pytest.mark.model('model-id') to use model_client fixture" ) model_id = marker.args[0] try: return model_pool.get_client(model_id) except KeyError: pytest.skip(f"Model {model_id} not available in model pool") @pytest.fixture def model_base_url(request: pytest.FixtureRequest, model_pool: "ModelPool") -> str: """Get the base URL for the model specified by @pytest.mark.model(). Usage: @pytest.mark.model("llama-8b") def test_direct_http(model_base_url): response = httpx.get(f"{model_base_url}/health") """ marker = request.node.get_closest_marker("model") if marker is None: pytest.fail( "Test must be marked with @pytest.mark.model('model-id') to use model_base_url fixture" ) model_id = marker.args[0] try: return model_pool.get_base_url(model_id) except KeyError: pytest.skip(f"Model {model_id} not available in model pool")