"""Помощники интеграционных тестов с PostgreSQL: временная БД внутри того же сервера, что и `OPENBAO__DATABASE`. Фикстура `scratch_db_url` создаёт пустую БД `armcore_scratch_`, отдаёт её SQLAlchemy-URL (драйвер asyncpg) и в конце удаляет. Рабочую БД (`armka`) тесты не трогают. Подключается плагином в `conftest.py`. """ import asyncio import subprocess import sys import uuid from collections.abc import AsyncIterator from pathlib import Path import asyncpg import pytest_asyncio from sqlalchemy.engine import URL, make_url from src.core.config import settings API_DIR = Path(__file__).resolve().parents[1] def base_url() -> URL: """SQLAlchemy-URL рабочей БД из настроек (`.env.test`).""" return make_url(settings.OPENBAO.DATABASE.DATABASE_URL) def _asyncpg_dsn(url: URL) -> str: return url.set(drivername="postgresql").render_as_string(hide_password=False) def run_alembic(url: str, *args: str) -> str: """`alembic -x dburl= ` из каталога api/ отдельным процессом; вернуть stdout+stderr. `env.py` вызывает `asyncio.run(...)` — внутри event loop pytest-asyncio это невозможно. При ошибке — `AssertionError` с выводом процесса (`ENV_FILE`/`APP_ENV` наследуются из окружения pytest-env). """ result = subprocess.run( [sys.executable, "-m", "alembic", "-x", f"dburl={url}", *args], cwd=API_DIR, capture_output=True, text=True, check=False, ) output = result.stdout + result.stderr assert result.returncode == 0, f"alembic {' '.join(args)} завершился с кодом {result.returncode}:\n{output}" return output async def alembic_async(url: str, *args: str) -> str: """`run_alembic` в потоке, чтобы не блокировать event loop.""" return await asyncio.to_thread(run_alembic, url, *args) async def create_scratch_db(name: str) -> URL: """Создать пустую БД `name` на сервере рабочей БД; вернуть SQLAlchemy-URL на неё.""" url = base_url() conn = await asyncpg.connect(_asyncpg_dsn(url)) try: await conn.execute(f'CREATE DATABASE "{name}"') finally: await conn.close() return url.set(database=name) async def drop_scratch_db(name: str) -> None: """Удалить БД `name` (закрывая оставшиеся подключения).""" conn = await asyncpg.connect(_asyncpg_dsn(base_url())) try: await conn.execute(f'DROP DATABASE IF EXISTS "{name}" WITH (FORCE)') finally: await conn.close() @pytest_asyncio.fixture async def scratch_db_url() -> AsyncIterator[str]: """URL пустой временной БД (asyncpg); БД удаляется после теста.""" name = f"armcore_scratch_{uuid.uuid4().hex[:12]}" url = await create_scratch_db(name) try: yield url.render_as_string(hide_password=False) finally: await drop_scratch_db(name)