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:
parent
c793b42fba
commit
c9cd839e59
104
api/tests/test_import_full_export.py
Normal file
104
api/tests/test_import_full_export.py
Normal 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 == {}
|
||||||
@ -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`
|
- Create: `api/tests/test_import_full_export.py`
|
||||||
- Modify (при необходимости): `api/src/services/directory_import.py` (пакетные вставки `insert(...).values([...])`, если > 60 с)
|
- 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); таблицы после теста не выросли.
|
- [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); таблицы после теста не выросли.
|
||||||
- [ ] **Step 2:** запустить; при превышении 60 с — профилировать (`--durations`), перевести вставки на `session.execute(insert(Model), [dict, ...])` пакетами по 1000, повторить.
|
- [x] **Step 2:** запустить; при превышении 60 с — профилировать (`--durations`), перевести вставки на `session.execute(insert(Model), [dict, ...])` пакетами по 1000, повторить.
|
||||||
- [ ] **Step 3: Commit** `test(import): dry_run полной выгрузки ЕЦП (8400 строк) укладывается в 60 с`.
|
- [x] **Step 3: Commit** `test(import): dry_run полной выгрузки ЕЦП (8400 строк) укладывается в 60 с`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@ -57,7 +57,7 @@ test-integration`). Пути — от корня репозитория. Доп
|
|||||||
+ `s3_object`), `POST /api/v1/directories/import` (multipart, `IMPORT_MAX_BYTES` = 20 МБ → 413, `DIRECTORY_ADMIN`),
|
+ `s3_object`), `POST /api/v1/directories/import` (multipart, `IMPORT_MAX_BYTES` = 20 МБ → 413, `DIRECTORY_ADMIN`),
|
||||||
`schemas/directory_import.py`. Интеграционные тесты: сценарии спеки `directory-import` (срез РБ, идемпотентность, `update`,
|
`schemas/directory_import.py`. Интеграционные тесты: сценарии спеки `directory-import` (срез РБ, идемпотентность, `update`,
|
||||||
`LE_IN_OTHER_GROUP`, dry_run без следов, `MISSING_COLUMNS`); S3 — через `app.state.storage` с заглушкой в тестах.
|
`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 с; пропуск с причиной, если файла нет.
|
`dry_run`, `rows = 8400`, `RF_CREATED = 53`, `groups_created = 344`, присутствие кодов, время ≤ 60 с; пропуск с причиной, если файла нет.
|
||||||
Профилирование: при превышении — пакетные вставки (`insert().values([...])`).
|
Профилирование: при превышении — пакетные вставки (`insert().values([...])`).
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user