Skip to main content
Glama
irskep

persistproc

by irskep
conftest.py7.7 kB
import logging import os import random import signal import socket import sys import uuid from collections.abc import Iterable from datetime import datetime from pathlib import Path import pytest from tests.helpers import start_persistproc, stop_run LOG_PATTERN = "persistproc.run.*.log" ENV_PORT = "PERSISTPROC_PORT" ENV_DATA_DIR = "PERSISTPROC_DATA_DIR" def _find_latest_log(dirs: Iterable[Path]) -> Path | None: """Return the most recently modified log file among *dirs* (recursive).""" latest: Path | None = None for base in dirs: if not base.exists(): continue for path in base.rglob(LOG_PATTERN): if latest is None or path.stat().st_mtime > latest.stat().st_mtime: latest = path return latest @pytest.hookimpl(hookwrapper=True, tryfirst=True) def pytest_runtest_makereport(item, call): # noqa: D401 – pytest hook # Let pytest perform its normal processing first. outcome = yield rep = outcome.get_result() # Only act after the *call* phase and when the test has failed. if rep.when != "call" or rep.passed: return # Collect candidate directories to search. candidate_dirs: list[Path] = [] # Common temporary directory fixtures. for fixture_name in ("tmp_path", "tmp_path_factory"): if fixture_name in item.funcargs: fixture_val = item.funcargs[fixture_name] if isinstance(fixture_val, Path): candidate_dirs.append(fixture_val) elif hasattr(fixture_val, "getbasetemp"): # tmp_path_factory candidate_dirs.append(Path(fixture_val.getbasetemp())) # Environment override allows tests to specify additional locations. extra_dir = item.config.getoption("--persistproc-data-dir", default=None) if extra_dir: candidate_dirs.append(Path(extra_dir)) # Always include repository-level artifacts directory if present. repo_artifacts = Path(__file__).parent / "_artifacts" candidate_dirs.append(repo_artifacts) latest_log = _find_latest_log(candidate_dirs) if latest_log is None: rep.sections.append(("persistproc-log", "[no log file found]")) return try: contents = latest_log.read_text(encoding="utf-8") except Exception as exc: # pragma: no cover – best-effort contents = f"<error reading log file {latest_log}: {exc}>" # Attach as an additional section so `-vv` shows it nicely; also write to # stderr immediately so it appears even with minimal verbosity. rep.sections.append(("persistproc-log", contents)) sys.stderr.write("\n==== persistproc server log (latest) ====\n") sys.stderr.write(contents) sys.stderr.write("\n==== end of persistproc server log ====\n\n") @pytest.fixture(autouse=True) def _enforce_timeout(request): """Fail tests that run longer than the allowed time. Default timeout is 30 seconds unless a test is marked with ``@pytest.mark.timeout(N)`` specifying a custom limit. """ marker = request.node.get_closest_marker("timeout") timeout = int(marker.args[0]) if marker and marker.args else 30 # Skip if timeout is non-positive or SIGALRM unavailable (e.g. Windows). if timeout <= 0 or sys.platform.startswith("win"): yield return def _alarm_handler(signum, frame): # noqa: D401 – signal handler pytest.fail(f"Test timed out after {timeout} seconds", pytrace=False) previous = signal.signal(signal.SIGALRM, _alarm_handler) # type: ignore[arg-type] signal.alarm(timeout) try: yield finally: signal.alarm(0) signal.signal(signal.SIGALRM, previous) # type: ignore[arg-type] @pytest.fixture(autouse=True) def _persistproc_env(monkeypatch): """Configure default data dir and port for *persistproc* CLI/tests. This eliminates the need for per-test boilerplate – the CLI picks up these settings via environment variables automatically. """ # ------------------------------------------------------------------ # Data directory – keep within repository under *tests/_artifacts* so # developers can inspect logs easily. Ensure uniqueness to avoid clashes # between concurrent/parameterised tests. # ------------------------------------------------------------------ artifacts_root = Path(__file__).parent / "_artifacts" artifacts_root.mkdir(parents=True, exist_ok=True) unique = ( f"data_{datetime.now().strftime('%Y%m%d_%H%M%S_%f')}_{uuid.uuid4().hex[:6]}" ) data_dir = artifacts_root / unique data_dir.mkdir(parents=True, exist_ok=True) monkeypatch.setenv(ENV_DATA_DIR, str(data_dir)) # ------------------------------------------------------------------ # Port – select a currently-available TCP port. # ------------------------------------------------------------------ def _choose_free_port() -> int: for _ in range(50): # try up to 50 random ports port = random.randint(20000, 50000) with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: try: sock.bind(("127.0.0.1", port)) except OSError: continue # in use – pick another return port # Fallback – let the OS pick an unused port (port 0) then reuse it. with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: sock.bind(("127.0.0.1", 0)) return sock.getsockname()[1] port = _choose_free_port() monkeypatch.setenv(ENV_PORT, str(port)) # ------------------------------------------------------------------ # Format – ensure tests get JSON output for parsing # ------------------------------------------------------------------ monkeypatch.setenv("PERSISTPROC_FORMAT", "json") yield # Cleanup: monkeypatch context manager restores env automatically. @pytest.fixture def persistproc_data_dir() -> Path: """Return the data directory configured for *persistproc* in this test.""" val = os.environ.get(ENV_DATA_DIR) if not val: raise RuntimeError("PERSISTPROC_DATA_DIR not set by _persistproc_env") return Path(val) @pytest.fixture def persistproc_port() -> int: """Return the TCP port allocated for *persistproc* in this test.""" val = os.environ.get(ENV_PORT) if not val: raise RuntimeError("PERSISTPROC_PORT not set by _persistproc_env") return int(val) # --------------------------------------------------------------------------- # Helper fixtures for starting/stopping the persistproc server used in e2e tests # --------------------------------------------------------------------------- @pytest.fixture def persistproc_server(): """Start a persistproc server for the duration of one test.""" proc = start_persistproc() yield proc stop_run(proc) @pytest.fixture def server(persistproc_server): # alias for convenience return persistproc_server @pytest.fixture(autouse=True) def logging_config(): """Silence overly verbose third-party loggers during tests.""" # These loggers are very verbose and don't provide useful info for debugging persistproc issues noisy_loggers = [ "httpcore.http11", "mcp.server.streamable_http", "mcp.server.streamable_http_manager", "mcp.server.lowlevel.server", "sse_starlette.sse", "asyncio", "FastMCP.fastmcp.server.server", "uvicorn.access", "uvicorn.error", ] for logger_name in noisy_loggers: logging.getLogger(logger_name).setLevel(logging.WARNING)

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/irskep/persistproc'

If you have feedback or need assistance with the MCP directory API, please join our Discord server