"""Shared pytest fixtures for FATHOM env tests.""" from __future__ import annotations import json import os import signal import socket import subprocess import sys import time from pathlib import Path from typing import Iterator import pytest from fastapi.testclient import TestClient @pytest.fixture() def fresh_test_client() -> Iterator[TestClient]: """A TestClient bound to a FRESH FastAPI app — avoids single-_ENV state bleed between tests.""" import importlib import env.server.app as app_mod importlib.reload(app_mod) yield TestClient(app_mod.app) @pytest.fixture(scope="session") def fixture_rows() -> list[dict]: """Parse env/data/phase0_fixture.jsonl once per test session.""" path = Path("env/data/phase0_fixture.jsonl") rows = [] with path.open("r", encoding="utf-8") as f: for line in f: line = line.strip() if line: rows.append(json.loads(line)) assert len(rows) >= 3, f"Expected >=3 fixture rows, got {len(rows)}" return rows def _find_free_port() -> int: with socket.socket() as s: s.bind(("127.0.0.1", 0)) return s.getsockname()[1] @pytest.fixture() def live_uvicorn_server() -> Iterator[str]: """Launch uvicorn in a subprocess; yield base_url; terminate on teardown. Skipped when subprocess uvicorn fails to come up (e.g., flaky Windows CI). """ port = _find_free_port() env = os.environ.copy() env["PYTHONDONTWRITEBYTECODE"] = "1" proc = subprocess.Popen( [sys.executable, "-m", "uvicorn", "env.server.app:app", "--host", "127.0.0.1", "--port", str(port), "--log-level", "warning"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env, ) base_url = f"http://127.0.0.1:{port}" try: deadline = time.monotonic() + 10.0 ready = False import httpx while time.monotonic() < deadline: try: r = httpx.get(base_url + "/healthz", timeout=0.5) if r.status_code == 200: ready = True break except Exception: time.sleep(0.2) if not ready: out = proc.stdout.read().decode("utf-8", errors="replace")[:2000] if proc.stdout else "" err = proc.stderr.read().decode("utf-8", errors="replace")[:2000] if proc.stderr else "" pytest.skip(f"uvicorn failed to come up in 10s. stderr: {err}. stdout: {out}") yield base_url finally: if proc.poll() is None: try: if sys.platform != "win32": proc.send_signal(signal.SIGTERM) else: proc.terminate() proc.wait(timeout=5) except Exception: proc.kill()