test(normalizer): golden на реальных ответах ИС, make golden-update (D-8)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
This commit is contained in:
parent
2a7ceca334
commit
ebbeff58bd
7
Makefile
7
Makefile
@ -7,7 +7,7 @@ COMPOSE_FILE ?= ../docs/dev/docker-compose.yml
|
||||
COMPOSE_PROJECT ?= arm-ka
|
||||
IMAGE ?= scan-gateway:dev
|
||||
|
||||
.PHONY: help venv lint test test-integration check docker-build check-full up emulator
|
||||
.PHONY: help venv lint test test-integration check docker-build check-full up emulator golden-update
|
||||
|
||||
help:
|
||||
@echo "venv - окружение api/.venv (Python 3.11) с зависимостями без raisa_*"
|
||||
@ -19,6 +19,7 @@ help:
|
||||
@echo "check-full - check + test-integration + docker-build (по порядку, до первой ошибки)"
|
||||
@echo "up - поднять NATS/MinIO/PostgreSQL общим compose-проектом arm-ka и создать бакет (minio-init)"
|
||||
@echo "emulator - запустить эмулятор ИС Сканирование (python -m src.scan_emulator)"
|
||||
@echo "golden-update - пересобрать эталоны нормализатора ../fixtures/ocr/expected_normalize/ (diff смотреть в fixtures)"
|
||||
|
||||
# Python 3.11 (версия платформы); внутренние пакеты raisa_* вне банка недоступны — пропускаем их
|
||||
venv:
|
||||
@ -62,3 +63,7 @@ up:
|
||||
# Эмулятор ИС Сканирование (параметры — SCAN_HOTFOLDER_PATH, SCAN_FIXTURES_DIR, SCAN_EMULATOR_DELAY из api/.env)
|
||||
emulator:
|
||||
cd api && $(BIN)/python -m src.scan_emulator
|
||||
|
||||
# Эталоны нормализатора на реальных ответах ИС: пишет ../fixtures/ocr/expected_normalize/<name>.json; diff смотреть в fixtures
|
||||
golden-update:
|
||||
cd api && $(BIN)/python -m tests.golden_update
|
||||
|
||||
73
api/tests/golden_update.py
Normal file
73
api/tests/golden_update.py
Normal file
@ -0,0 +1,73 @@
|
||||
"""Golden нормализатора: сбор склейки реального ответа и запись эталонов (`make golden-update`).
|
||||
|
||||
Набор задания `<name>` — `<name>.json` + `<name>_*.json` из `../fixtures/ocr` (как у эмулятора); эталон —
|
||||
`../fixtures/ocr/expected_normalize/<name>.json`. Запуск: `cd api && .venv/bin/python -m tests.golden_update`.
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from src.gateway.hotfolder import AnswerFile
|
||||
from src.gateway.merge import merge_answers
|
||||
from src.normalizer import NormalizeOutcome, OcrNormalizer
|
||||
from src.normalizer.rules import load_rules
|
||||
|
||||
API_DIR = Path(__file__).resolve().parents[1]
|
||||
REAL_FIXTURES = API_DIR.parent.parent / "fixtures" / "ocr"
|
||||
GOLDEN_DIR = REAL_FIXTURES / "expected_normalize"
|
||||
JOB = "3f1c2b7a-0000-4000-8000-000000000001"
|
||||
|
||||
|
||||
def golden_cases() -> list[str]:
|
||||
"""Имена `<name>` без суффикса `_*`: `aep_3`, `aep_6`, … (по одному на набор)."""
|
||||
names = sorted(p.name[: -len(".json")] for p in REAL_FIXTURES.glob("*.json") if p.is_file())
|
||||
stems = {name for name in names if not any(name.startswith(f"{other}_") for other in names if other != name)}
|
||||
return sorted(stems)
|
||||
|
||||
|
||||
def real_set(name: str) -> dict[str, Any]:
|
||||
files = []
|
||||
for path in sorted(REAL_FIXTURES.glob(f"{name}*.json")):
|
||||
base = path.name[: -len(".json")]
|
||||
if base == name:
|
||||
target = f"{JOB}.json"
|
||||
elif base.startswith(f"{name}_"):
|
||||
target = f"{JOB}_{base[len(name) + 1 :]}.json"
|
||||
else:
|
||||
continue
|
||||
files.append(AnswerFile(target, path.read_bytes()))
|
||||
return dict(merge_answers(JOB, files))
|
||||
|
||||
|
||||
def outcome_to_json(outcome: NormalizeOutcome) -> dict[str, Any]:
|
||||
return {
|
||||
"status": outcome.status.value,
|
||||
"warnings": list(outcome.warnings),
|
||||
"metrics": outcome.metrics.model_dump(mode="json", exclude_none=True) if outcome.metrics else None,
|
||||
"error": outcome.error.model_dump(mode="json", exclude_none=True) if outcome.error else None,
|
||||
"documents": [doc.model_dump(mode="json", exclude_none=True) for doc in outcome.documents],
|
||||
}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if not REAL_FIXTURES.is_dir():
|
||||
print(f"нет каталога {REAL_FIXTURES}", file=sys.stderr)
|
||||
return 2
|
||||
rules = load_rules(API_DIR / "rules")
|
||||
GOLDEN_DIR.mkdir(exist_ok=True)
|
||||
for name in golden_cases():
|
||||
payload = outcome_to_json(OcrNormalizer(rules).normalize(real_set(name)))
|
||||
(GOLDEN_DIR / f"{name}.json").write_text(
|
||||
json.dumps(payload, ensure_ascii=False, indent=2, sort_keys=True) + "\n", encoding="utf-8"
|
||||
)
|
||||
print(
|
||||
f"{name}: {payload['status']}, документов {len(payload['documents'])}, "
|
||||
f"предупреждений {len(payload['warnings'])}"
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
32
api/tests/unit/normalizer/test_golden.py
Normal file
32
api/tests/unit/normalizer/test_golden.py
Normal file
@ -0,0 +1,32 @@
|
||||
"""Golden нормализатора на реальных ответах ИС (D-8).
|
||||
|
||||
`../fixtures/ocr/<name>.json` → `../fixtures/ocr/expected_normalize/<name>.json`.
|
||||
Обновление эталонов — `make golden-update` (не автоматически)."""
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from src.normalizer import OcrNormalizer
|
||||
from src.normalizer.rules import load_rules
|
||||
from tests.golden_update import GOLDEN_DIR, REAL_FIXTURES, golden_cases, outcome_to_json, real_set
|
||||
|
||||
API_DIR = Path(__file__).resolve().parents[3]
|
||||
RULES = load_rules(API_DIR / "rules")
|
||||
pytestmark = pytest.mark.fixtures
|
||||
|
||||
|
||||
def _cases() -> list[str]:
|
||||
return golden_cases() if REAL_FIXTURES.is_dir() else []
|
||||
|
||||
|
||||
@pytest.mark.parametrize("name", _cases() or ["<нет fixtures>"])
|
||||
def test_matches_golden(name: str) -> None:
|
||||
if not REAL_FIXTURES.is_dir():
|
||||
pytest.skip("fixtures недоступен: положите репозиторий fixtures рядом с workspace")
|
||||
expected_path = GOLDEN_DIR / f"{name}.json"
|
||||
assert expected_path.is_file(), f"нет эталона {expected_path}: выполните make golden-update и проверьте diff"
|
||||
expected = json.loads(expected_path.read_text(encoding="utf-8"))
|
||||
actual = outcome_to_json(OcrNormalizer(RULES).normalize(real_set(name)))
|
||||
assert actual == expected, f"эталон {name} разошёлся: make golden-update и git diff в fixtures"
|
||||
Loading…
x
Reference in New Issue
Block a user