"""Shared fixtures. Postgres tests run when ``NOETA_TEST_POSTGRES_DSN`` is set (e.g. ``postgresql://postgres:pw@localhost:55432/postgres``) and skip otherwise; each test gets its own schema, dropped afterwards.""" from __future__ import annotations import os import threading import uuid from collections.abc import Iterator from typing import Any import pytest PG_DSN = os.environ.get("NOETA_TEST_POSTGRES_DSN", "") # The tests' own HTTP clients reach their servers on this machine directly, # past a proxy the environment or macOS's system settings name (noeta's own # clients do: noeta.toolkit.http). os.environ["NO_PROXY"] = ",".join( x for x in ("localhost,127.0.0.1,::1", os.environ.get("NO_PROXY", "")) if x) class Pg: def __init__(self, dsn: str, schema: str) -> None: self.dsn = dsn self.schema = schema def store(self, **kw: Any) -> Any: from noeta.backends.stores.postgres import PostgresStore return PostgresStore(self.dsn, schema=self.schema, **kw) @pytest.fixture def pg() -> Iterator[Pg]: if not PG_DSN: pytest.skip("set NOETA_TEST_POSTGRES_DSN to run the postgres tests") import psycopg schema = f"t_{uuid.uuid4().hex[:12]}" yield Pg(PG_DSN, schema) with psycopg.connect(PG_DSN, autocommit=True) as conn: conn.execute(f'DROP SCHEMA IF EXISTS "{schema}" CASCADE') #: The real stderr, kept before output capture takes fd 2 over. _STDERR_FD: list[int] = [] def pytest_configure(config: pytest.Config) -> None: _STDERR_FD.append(os.dup(2)) def _dump_pending_tasks(nodeid: str) -> None: """Every unfinished asyncio task with its stack: faulthandler's thread stacks show a stuck loop, not the coroutine it waits on.""" import asyncio.tasks lines = [f"\n--- asyncio tasks still pending in {nodeid} ---\n"] for _ in range(3): # read from another thread: the set may change under us try: pending = [t for t in list(asyncio.tasks._scheduled_tasks) if not t.done()] break except RuntimeError: pending = [] for task in pending: lines.append(f"{task!r}\n") awaited: Any = task.get_coro() while awaited is not None: # down the await chain, outermost first frame = getattr(awaited, "cr_frame", None) or getattr(awaited, "gi_frame", None) if frame is None: lines.append(f" awaiting {awaited!r}\n") break lines.append(f" {frame.f_code.co_filename}:{frame.f_lineno} in " f"{frame.f_code.co_name}\n") awaited = getattr(awaited, "cr_await", None) or getattr(awaited, "gi_yieldfrom", None) os.write(_STDERR_FD[0], "".join(lines).encode()) @pytest.hookimpl(wrapper=True) def pytest_runtest_protocol(item: pytest.Item, nextitem: pytest.Item | None) -> Any: """With ``faulthandler_timeout`` set, a test still running then also has its pending asyncio tasks printed.""" after = float(item.config.getini("faulthandler_timeout") or 0) if after <= 0: return (yield) timer = threading.Timer(after, _dump_pending_tasks, args=(item.nodeid,)) timer.daemon = True timer.start() try: return (yield) finally: timer.cancel() @pytest.fixture(autouse=True) def noeta_home_in_tmp(monkeypatch: pytest.MonkeyPatch, tmp_path_factory: pytest.TempPathFactory ) -> None: """noeta's home is a fresh directory for each test, so what a task keeps there (plans, pasted images, transcripts, the server token) never lands in the user's: the variable for subprocesses, ``home_dir.HOME`` (read once at import) for this one.""" from noeta.cli import home_dir home = tmp_path_factory.mktemp("noeta-home") monkeypatch.setenv("NOETA_HOME", str(home)) monkeypatch.setattr(home_dir, "HOME", home) @pytest.fixture(autouse=True) def no_shared_agents_skills(monkeypatch: pytest.MonkeyPatch, tmp_path_factory: pytest.TempPathFactory) -> None: """The ``noeta`` command reads ``~/.agents/skills``; a test never sees the user's.""" from noeta.cli import home_dir monkeypatch.setattr(home_dir, "AGENTS_SKILLS", tmp_path_factory.mktemp("agents") / "skills") @pytest.fixture(autouse=True) def no_model_listings(monkeypatch: pytest.MonkeyPatch) -> None: """No test reads a provider's model listing over the network (a front end built from settings would, for a model the catalog does not know); a test of listings passes its own transport or listing.""" from noeta.backends.providers import listing monkeypatch.setattr(listing, "fetch_listing", lambda *a, **kw: []) @pytest.fixture(autouse=True) def fold_snapshots_both_ways(monkeypatch: pytest.MonkeyPatch) -> None: """Every task log any test loads is folded both ways and compared: from the start, and from a snapshot taken at several seqs plus the events after it (``tests/snapshot_check.py``). Opt out with ``NOETA_TEST_NO_SNAPSHOT_CHECK=1``.""" if os.environ.get("NOETA_TEST_NO_SNAPSHOT_CHECK"): return from noeta.core.runner.task import TaskRun from tests.snapshot_check import check_both_ways original = TaskRun.load async def load(self: TaskRun) -> None: await original(self) await check_both_ways(self) monkeypatch.setattr(TaskRun, "load", load)