arm-core/api/tests/fixtures_helpers.py
Andrey Epifancev 234e830476 test(statements): закрыты отложенные minor-находки ревью задач 1-12 (T1.1, T2.1, T3.1, T3.3, T4.x, T5.5, T7.2, T7.3, T8.4, T10.1, T11.3, T12.4)
Проверки §7 по колонкам, допуск, abs_terms, M-9, граница округления; граничные случаи маппинга колонок и
уверенности; шапка, смешанные снимки, статус при задании needs_check, предупреждение о повторе формы и
идемпотентный повторный маппинг форм; редакция формы, пара ББ/ОФР, 403 без права, подтверждение формы без шапки
и без дат; комментарий без видимости ЮЛ; справочник пользователей; documents_count; CSV чек-листа.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
2026-10-07 14:00:09 +04:00

124 lines
5.8 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Поиск каталога реальных данных `fixtures` (соседний репозиторий workspace) для тестов с маркером `fixtures`."""
import json
import os
import uuid
from collections.abc import Callable
from datetime import UTC, datetime
from pathlib import Path
from typing import TYPE_CHECKING
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
if TYPE_CHECKING:
from src.contract.result import RecognitionResult
from src.models.directories import LegalEntity
from src.models.enums import RecognitionStatus
from src.models.files import Document
FIXTURES_ENV = "ARM_KA_FIXTURES"
GOLDEN_STATEMENTS = Path("golden") / "expected_statement_parse"
OCR_EXPECTED = Path("ocr") / "expected_normalize"
def resolve_fixtures_dir() -> Path:
"""Каталог fixtures: `ARM_KA_FIXTURES`, иначе соседний `../../fixtures` относительно `arm-core/api`.
Не проверяет существование: это делает вызывающий код (пропуск теста с причиной).
"""
explicit = os.environ.get(FIXTURES_ENV)
if explicit:
return Path(explicit)
return Path(__file__).resolve().parents[3] / "fixtures"
def fixtures_skip_reason() -> str | None:
"""Причина пропуска тестов с маркером `fixtures` или `None`, если каталог доступен."""
path = resolve_fixtures_dir()
if path.is_dir():
return None
return (
f"ARM_KA_FIXTURES недоступен: нет каталога {path} "
f"(задайте {FIXTURES_ENV} или положите fixtures рядом с arm-core)"
)
def ocr_result(path: Path, *, job_id: uuid.UUID, file_id: uuid.UUID) -> "RecognitionResult":
"""Эталон нормализатора scan-gateway (`ocr/expected_normalize/<name>.json`)
как `RecognitionResult` контракта v1.2.
"""
from src.contract.result import RecognitionResult # noqa: PLC0415 - цикл с conftest
data = json.loads(path.read_text("utf-8"))
payload: dict[str, object] = {
"schema_version": "1.2",
"job_id": str(job_id),
"file_id": str(file_id),
"service": {"name": "scan-gateway", "version": "golden"},
"status": data["status"],
"documents": data["documents"],
}
if data.get("error") is not None:
payload["error"] = data["error"]
if data.get("metrics") is not None:
payload["metrics"] = data["metrics"]
return RecognitionResult.model_validate(payload)
def golden_result(path: Path, *, job_id: uuid.UUID, file_id: uuid.UUID) -> "RecognitionResult":
"""Golden-снимок парсера как `RecognitionResult`: подставляет `job_id`/`file_id` и версию сервиса."""
from src.contract.result import RecognitionResult # noqa: PLC0415 - тяжёлые импорты лениво: цикл с conftest
data = json.loads(path.read_text("utf-8"))
data["service"] = {"version": "golden", **data["service"]}
return RecognitionResult.model_validate(data | {"job_id": str(job_id), "file_id": str(file_id)})
async def map_golden_snapshot( # noqa: PLR0913 - все входы именованные
session: AsyncSession,
*,
entity: "LegalEntity",
path: Path,
uploaded_by: uuid.UUID | None,
result_builder: Callable[..., "RecognitionResult"] | None = None,
job_status: "RecognitionStatus | None" = None,
) -> "list[Document]":
"""Golden-снимок `path` → файл ЮЛ `entity` и его документы в БД теста (`flush`, без commit).
Как маппер: файл `PROCESSING` с текущим заданием `done`, затем боевой `map_file` с планом счетов seed и
настоящим `CounterpartyIndex` (справочник читается на момент вызова — ЮЛ и алиасы должны быть созданы
до маппинга). Возвращает документы файла отсортированные по (document_type, osv_account_code).
`result_builder` — функция создания RecognitionResult (по умолчанию golden_result); `job_status` — статус
текущего задания (по умолчанию `done`; `needs_check` — как после результата с `status = needs_check`).
"""
# ленивые импорты: `conftest` импортирует этот модуль раньше `tests.factories` (цикл)
from src.models.accounting import ChartOfAccount # noqa: PLC0415
from src.models.enums import FileStatus, RecognitionStatus # noqa: PLC0415
from src.models.files import Document # noqa: PLC0415
from src.repositories.counterparties import CounterpartyIndex # noqa: PLC0415
from src.services.result_mapping import map_file # noqa: PLC0415
from tests.factories import make_stored_file # noqa: PLC0415
file, job = await make_stored_file(
session,
entity=entity,
status=FileStatus.PROCESSING,
job_status=job_status or RecognitionStatus.done,
original_name=path.name,
uploaded_by=uploaded_by,
)
chart = {
code: account_id for code, account_id in await session.execute(select(ChartOfAccount.code, ChartOfAccount.id))
}
index = await CounterpartyIndex.load(session)
builder = result_builder or golden_result
result = builder(path, job_id=job.id, file_id=file.id)
await map_file(session, job=job, file=file, result=result, chart=chart, index=index, now=datetime.now(UTC))
documents = await session.execute(
select(Document)
.where(Document.stored_file_id == file.id)
.order_by(Document.document_type, Document.osv_account_code)
)
return list(documents.scalars())