- C-1 (№30): сумма с более чем двумя знаками после запятой не принимается (amount=null, string_value, is_recognized=false); confidence ячеек и шапки приводится к [0, 100], NaN и бесконечность дают 0 - F-A (№28): ошибочный файл набора при успешном понижает статус до needs_check - F-B: «ИНН не распознан» только когда ИНН не извлечён, иначе «ИНН распознан неуверенно» - №29: предупреждения «строк с нечисловым кодом пропущено: N» и «строк без кода с суммами пропущено: N» - I-3 (№31): тексты дат длиннее 200 символов не разбираются и не проходят fix_lookalikes - T1.3 (№33): загрузчик yaml отклоняет повторяющиеся ключи; тавтологичный тест дублей заменён настоящим - гигиена: докстринг amounts.py описывает фактическое поведение (все точки удаляются), мёртвые noqa в ocr/__init__.py Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
100 lines
4.1 KiB
Python
100 lines
4.1 KiB
Python
"""Шапка документа из `data_ocr` (§11.1 п. 8) на реальных `aep_3.json` (мусор) и `aep_6.json` (чисто)."""
|
||
|
||
import json
|
||
import time
|
||
from datetime import date
|
||
from pathlib import Path
|
||
|
||
import pytest
|
||
|
||
from src.normalizer.ocr.header import FileHeader, parse_file_header
|
||
|
||
API_DIR = Path(__file__).resolve().parents[3]
|
||
REAL_FIXTURES = API_DIR.parent.parent / "fixtures" / "ocr"
|
||
LOOKALIKES = {"О": "0", "о": "0", "З": "3", "l": "1", "I": "1", "|": "1", "б": "6", "S": "5", "Ч": "4"}
|
||
|
||
|
||
def _real(name: str) -> dict:
|
||
if not REAL_FIXTURES.is_dir():
|
||
pytest.skip("fixtures недоступен: положите репозиторий fixtures рядом с workspace")
|
||
return dict(json.loads((REAL_FIXTURES / name).read_text(encoding="utf-8"))["data_ocr"])
|
||
|
||
|
||
def _field(text: str, confidence: float) -> dict:
|
||
return {"form_name": text, "confidence": confidence}
|
||
|
||
|
||
def test_clean_header() -> None:
|
||
header = parse_file_header(
|
||
{
|
||
"organization": _field("ООО Ромашка", 97.5),
|
||
"inn": _field("7700000000", 100),
|
||
"units": _field("тыс. руб.", 100),
|
||
"date": _field("на 31 декабря 2024 г.", 100),
|
||
},
|
||
LOOKALIKES,
|
||
)
|
||
assert header == FileHeader(
|
||
"ООО Ромашка",
|
||
97.5,
|
||
"7700000000",
|
||
100.0,
|
||
"тыс. руб.",
|
||
"на 31 декабря 2024 г.",
|
||
date(2024, 12, 31),
|
||
True,
|
||
)
|
||
|
||
|
||
def test_noisy_inn_caps_confidence() -> None:
|
||
header = parse_file_header({"inn": _field("3|1 21 2| о! О' 0 0; 3: 51", 100)}, LOOKALIKES)
|
||
assert header.inn == "3121200351"
|
||
assert header.inn_confidence == 99.0
|
||
assert header.organization_name is None and header.organization_confidence is None
|
||
|
||
|
||
@pytest.mark.parametrize(("raw", "expected"), [(float("nan"), 0.0), (150, 100.0), (-5, 0.0), (float("inf"), 0.0)])
|
||
def test_header_confidence_is_clamped(raw: float, expected: float) -> None:
|
||
"""Решение №30: уверенность полей шапки — в [0, 100], NaN → 0 (контракт требует 0..100)."""
|
||
header = parse_file_header(
|
||
{"organization": _field("ООО Ромашка", raw), "inn": _field("7700000000", raw), "date": _field("2024", raw)},
|
||
LOOKALIKES,
|
||
)
|
||
assert header.organization_confidence == expected and header.inn_confidence == expected
|
||
assert header.report_date_recognized is (expected == 100.0)
|
||
|
||
|
||
@pytest.mark.parametrize(
|
||
"raw", ["1" + " " * 4000 + "b", "1" + "\n" * 4000, "1" + "О" * 5000], ids=["spaces", "nl", "look"]
|
||
)
|
||
def test_pathological_report_date_is_fast(raw: str) -> None:
|
||
"""Решение №31: длинный текст даты не разбирается и не проходит `fix_lookalikes` (иначе — секунды и минуты)."""
|
||
start = time.perf_counter()
|
||
header = parse_file_header({"date": _field(raw, 100)}, LOOKALIKES)
|
||
assert time.perf_counter() - start < 1.0
|
||
assert header.report_date is None and header.report_date_recognized is False
|
||
assert header.report_date_text == raw.strip() # исходный текст не теряется
|
||
|
||
|
||
def test_missing_fields() -> None:
|
||
header = parse_file_header({}, LOOKALIKES)
|
||
assert header == FileHeader(None, None, None, None, None, None, None, False)
|
||
|
||
|
||
@pytest.mark.fixtures
|
||
def test_aep6_header() -> None:
|
||
header = parse_file_header(_real("aep_6.json"), LOOKALIKES)
|
||
assert header.inn is None # текст «+»
|
||
assert header.units == "тыс. руб."
|
||
assert header.report_date is None # в поле date — название организации
|
||
assert header.report_date_text is not None and header.report_date_recognized is False
|
||
assert header.organization_name is not None and "Дорстроймонтаж" in header.organization_name
|
||
|
||
|
||
@pytest.mark.fixtures
|
||
def test_aep3_header() -> None:
|
||
header = parse_file_header(_real("aep_3.json"), LOOKALIKES)
|
||
assert header.inn == "3121200351" and header.inn_confidence is not None and header.inn_confidence < 100
|
||
assert header.report_date == date(2024, 12, 31)
|
||
assert header.report_date_recognized is False # confidence 75
|