93 lines
4.5 KiB
Python
93 lines
4.5 KiB
Python
"""Общие фикстуры тестов (spec data-model; design D-7, рулинг R-16).
|
||
|
||
БД-фикстуры — только для тестов с маркером `integration` и ленивые: пока тест их не запросил, PostgreSQL не
|
||
трогается (`make check` не требует БД). Тестовая БД сессии — отдельная свежая `armcore_pytest_<hex>` на сервере из
|
||
`.env.test`: мигрируется alembic один раз на сессию и удаляется в конце; рабочая БД `armka` не используется.
|
||
Изоляция тестов — внешняя транзакция с SAVEPOINT: `commit()` внутри теста ничего не фиксирует.
|
||
"""
|
||
|
||
import uuid
|
||
from collections.abc import AsyncIterator
|
||
from pathlib import Path
|
||
|
||
import asyncpg
|
||
import pytest
|
||
import pytest_asyncio
|
||
from sqlalchemy.ext.asyncio import AsyncConnection, AsyncEngine, AsyncSession, create_async_engine
|
||
|
||
from tests.fixtures_helpers import fixtures_skip_reason, resolve_fixtures_dir
|
||
from tests.pg_helpers import ( # scratch_db_url — реэкспорт фикстуры, чтобы pytest её нашёл
|
||
alembic_async,
|
||
create_scratch_db,
|
||
drop_scratch_db,
|
||
scratch_db_url, # noqa: F401
|
||
)
|
||
|
||
PG_UNAVAILABLE = "PostgreSQL недоступен (make up / .env.test): {error}"
|
||
|
||
|
||
def pytest_collection_modifyitems(config: pytest.Config, items: list[pytest.Item]) -> None:
|
||
"""Тесты с маркером `fixtures` пропускаются с явной причиной, если каталог fixtures недоступен."""
|
||
reason = fixtures_skip_reason()
|
||
if reason is None:
|
||
return
|
||
skip = pytest.mark.skip(reason=reason)
|
||
for item in items:
|
||
if item.get_closest_marker("fixtures") is not None:
|
||
item.add_marker(skip)
|
||
|
||
|
||
@pytest.fixture(scope="session")
|
||
def fixtures_dir() -> Path:
|
||
"""Каталог реальных данных (`ARM_KA_FIXTURES` или соседний `fixtures`); без него тест пропускается."""
|
||
reason = fixtures_skip_reason()
|
||
if reason is not None:
|
||
pytest.skip(reason)
|
||
return resolve_fixtures_dir()
|
||
|
||
|
||
@pytest_asyncio.fixture(scope="session")
|
||
async def migrated_db_url() -> AsyncIterator[str]:
|
||
"""URL свежей БД `armcore_pytest_<hex>` с применённой миграцией `alembic upgrade head`; удаляется в конце сессии."""
|
||
name = f"armcore_pytest_{uuid.uuid4().hex[:12]}"
|
||
try:
|
||
url = await create_scratch_db(name)
|
||
except (OSError, asyncpg.InterfaceError) as error: # соединение не установлено: сервер не запущен, порт закрыт
|
||
pytest.fail(PG_UNAVAILABLE.format(error=f"{type(error).__name__}: {error}"), pytrace=False)
|
||
except asyncpg.PostgresError as error: # сервер ответил отказом: учётные данные, роль, права на CREATE DATABASE
|
||
pytest.fail(
|
||
f"PostgreSQL отказал при создании тестовой БД ({type(error).__name__}: {error}); "
|
||
"проверьте учётные данные и право CREATE DATABASE в .env.test",
|
||
pytrace=False,
|
||
)
|
||
try:
|
||
rendered = url.render_as_string(hide_password=False)
|
||
await alembic_async(rendered, "upgrade", "head")
|
||
yield rendered
|
||
finally:
|
||
await drop_scratch_db(name)
|
||
|
||
|
||
@pytest_asyncio.fixture(scope="session")
|
||
async def db_engine(migrated_db_url: str) -> AsyncIterator[AsyncEngine]:
|
||
"""Async-движок на мигрированную БД сессии; `search_path = dkb`."""
|
||
engine = create_async_engine(migrated_db_url, connect_args={"server_settings": {"search_path": "dkb"}})
|
||
try:
|
||
yield engine
|
||
finally:
|
||
await engine.dispose()
|
||
|
||
|
||
@pytest_asyncio.fixture
|
||
async def db_session(db_engine: AsyncEngine) -> AsyncIterator[AsyncSession]:
|
||
"""Сессия в внешней транзакции с откатом: `commit()` в тесте освобождает лишь SAVEPOINT, данные не остаются."""
|
||
connection: AsyncConnection = await db_engine.connect()
|
||
transaction = await connection.begin()
|
||
session = AsyncSession(bind=connection, join_transaction_mode="create_savepoint", expire_on_commit=False)
|
||
try:
|
||
yield session
|
||
finally:
|
||
await session.close()
|
||
await transaction.rollback()
|
||
await connection.close()
|