test(import): dry_run полной выгрузки ЕЦП (8400 строк) укладывается в 60 с

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
This commit is contained in:
Andrey Epifancev 2026-10-04 23:37:54 +04:00
parent c793b42fba
commit c9cd839e59
3 changed files with 108 additions and 4 deletions

View File

@ -0,0 +1,104 @@
"""Полная выгрузка ЕЦП (8400 строк): `dry_run` укладывается в 60 с (spec directory-import, сценарий «Полная выгрузка»).
Тест идёт на отдельной временной БД с зафиксированным seed (`committed_client`): `dry_run` делает явный
`session.rollback()`, поэтому «таблицы не выросли» осмысленно только вне SAVEPOINT-обёртки `db_session` (см. docstring
`tests/conftest.py`). Файл выгрузки — из `ARM_KA_DOCS` либо соседнего `docs`; нет файла — пропуск с причиной.
"""
import os
import time
from collections import Counter
from pathlib import Path
from typing import Any
import pytest
from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
from src.models.audit_ex import AuditEvent
from src.models.directories import (
CompanyGroup,
CompanyGroupHistory,
LegalEntity,
LegalEntityHistory,
RegionalOffice,
RelatedGroup,
RelatedGroupMember,
)
from src.models.files import S3Object
from tests.conftest import AuthClientFactory, FakeStorage
ADMIN = "admin@armka.local"
EXPORT_RELATIVE = Path("source/biq/ecp-export-lagal-entity.xlsx")
XLSX_TYPE = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
HTTP_OK = 200
TIME_LIMIT_SECONDS = 60
EXPORT_ROWS = 8400
EXPORT_OFFICES = 53
EXPORT_GROUPS = 344 # R-15: из 345 ГК файла одна целиком состоит из строк-дублей и не создаётся
MIN_UNSUPPORTED_TYPE_ROWS = 500
TOUCHED_TABLES = (
RegionalOffice,
CompanyGroup,
CompanyGroupHistory,
LegalEntity,
LegalEntityHistory,
RelatedGroup,
RelatedGroupMember,
AuditEvent,
S3Object,
)
def _export_path() -> Path:
docs = os.environ.get("ARM_KA_DOCS")
return (Path(docs) if docs else Path(__file__).resolve().parents[3] / "docs") / EXPORT_RELATIVE
async def _counts(session: AsyncSession) -> dict[str, int]:
return {
model.__tablename__: (await session.execute(select(func.count()).select_from(model))).scalar_one()
for model in TOUCHED_TABLES
}
@pytest.mark.integration
@pytest.mark.fixtures
async def test_full_ecp_export_dry_run_within_time_limit(
committed_client: AuthClientFactory,
committed_sessionmaker: async_sessionmaker[AsyncSession],
committed_storage: FakeStorage,
) -> None:
"""`dry_run` выгрузки из 8400 строк: ≤ 60 с, ожидаемые счётчики и коды, ни одна таблица справочника не выросла."""
path = _export_path()
if not path.is_file():
pytest.skip(f"нет выгрузки ЕЦП: {path} (задайте ARM_KA_DOCS или положите docs рядом с arm-core)")
content = path.read_bytes()
client = await committed_client(ADMIN)
async with committed_sessionmaker() as session:
before = await _counts(session)
started = time.perf_counter()
response = await client.post(
"/directories/import",
files={"file": (path.name, content, XLSX_TYPE)},
data={"dry_run": "true", "on_existing": "skip"},
)
elapsed = time.perf_counter() - started
assert response.status_code == HTTP_OK, response.text
assert elapsed <= TIME_LIMIT_SECONDS, f"dry_run занял {elapsed:.1f} с (лимит {TIME_LIMIT_SECONDS} с)"
print(f"\nfull export dry_run: {elapsed:.2f} s") # noqa: T201 - замер времени виден при `pytest -s`
report: dict[str, Any] = response.json()
assert report["rows"] == EXPORT_ROWS
assert report["offices_created"] == EXPORT_OFFICES
assert report["groups_created"] == EXPORT_GROUPS
codes = Counter(line["code"] for line in report["lines"])
assert codes["RF_CREATED"] == EXPORT_OFFICES
assert codes["UNSUPPORTED_TYPE"] >= MIN_UNSUPPORTED_TYPE_ROWS
assert codes["INVALID_INN"] > 0
assert codes["DUPLICATE_IN_FILE"] > 0
async with committed_sessionmaker() as session:
assert await _counts(session) == before
assert committed_storage.objects == {}

View File

@ -172,9 +172,9 @@ async def run(self, *, filename: str, content: bytes, dry_run: bool, on_existing
- Create: `api/tests/test_import_full_export.py`
- Modify (при необходимости): `api/src/services/directory_import.py` (пакетные вставки `insert(...).values([...])`, если > 60 с)
- [ ] **Step 1: Тест** `@pytest.mark.integration @pytest.mark.fixtures`: путь `Path(os.environ.get("ARM_KA_DOCS") or Path(__file__).resolve().parents[3] / "docs") / "source/biq/ecp-export-lagal-entity.xlsx"`; нет файла → `pytest.skip("нет выгрузки ЕЦП: …")`. `dry_run=true` под `admin@armka.local`; `time.perf_counter()` ≤ 60 с; `rows == 8400`; `offices_created == 53`; среди кодов есть `UNSUPPORTED_TYPE` (≥ 500 строк), `INVALID_INN`, `DUPLICATE_IN_FILE` (`LE_IN_OTHER_GROUP` на пустой БД невозможен — не проверять); `groups_created == 344` (R-15); таблицы после теста не выросли.
- [ ] **Step 2:** запустить; при превышении 60 с — профилировать (`--durations`), перевести вставки на `session.execute(insert(Model), [dict, ...])` пакетами по 1000, повторить.
- [ ] **Step 3: Commit** `test(import): dry_run полной выгрузки ЕЦП (8400 строк) укладывается в 60 с`.
- [x] **Step 1: Тест** `@pytest.mark.integration @pytest.mark.fixtures`: путь `Path(os.environ.get("ARM_KA_DOCS") or Path(__file__).resolve().parents[3] / "docs") / "source/biq/ecp-export-lagal-entity.xlsx"`; нет файла → `pytest.skip("нет выгрузки ЕЦП: …")`. `dry_run=true` под `admin@armka.local`; `time.perf_counter()` ≤ 60 с; `rows == 8400`; `offices_created == 53`; среди кодов есть `UNSUPPORTED_TYPE` (≥ 500 строк), `INVALID_INN`, `DUPLICATE_IN_FILE` (`LE_IN_OTHER_GROUP` на пустой БД невозможен — не проверять); `groups_created == 344` (R-15); таблицы после теста не выросли.
- [x] **Step 2:** запустить; при превышении 60 с — профилировать (`--durations`), перевести вставки на `session.execute(insert(Model), [dict, ...])` пакетами по 1000, повторить.
- [x] **Step 3: Commit** `test(import): dry_run полной выгрузки ЕЦП (8400 строк) укладывается в 60 с`.
---

View File

@ -57,7 +57,7 @@ test-integration`). Пути — от корня репозитория. Доп
+ `s3_object`), `POST /api/v1/directories/import` (multipart, `IMPORT_MAX_BYTES` = 20 МБ → 413, `DIRECTORY_ADMIN`),
`schemas/directory_import.py`. Интеграционные тесты: сценарии спеки `directory-import` (срез РБ, идемпотентность, `update`,
`LE_IN_OTHER_GROUP`, dry_run без следов, `MISSING_COLUMNS`); S3 — через `app.state.storage` с заглушкой в тестах.
- [ ] 3.3 **Полная выгрузка (fixtures-тест).** Тест с маркером `fixtures`: `ARM_KA_DOCS/source/biq/ecp-export-lagal-entity.xlsx`,
- [x] 3.3 **Полная выгрузка (fixtures-тест).** Тест с маркером `fixtures`: `ARM_KA_DOCS/source/biq/ecp-export-lagal-entity.xlsx`,
`dry_run`, `rows = 8400`, `RF_CREATED = 53`, `groups_created = 344`, присутствие кодов, время ≤ 60 с; пропуск с причиной, если файла нет.
Профилирование: при превышении — пакетные вставки (`insert().values([...])`).