"""`OcrNormalizer`: набор json ИС → `NormalizeOutcome` (статус, ошибка набора, предупреждения файла, метрики, документы контракта 2.0). Реальные ответы ИС — `test_golden.py` / `test_real_answers.py` (задача 6).""" from typing import Any import pytest import src.normalizer.ocr as ocr_module import src.normalizer.ocr.answer_set as answer_set_module from arm_statements import default_rules from src.contract import ErrorCode, FileWarning, ResultStatus, Severity, StatementDocument from src.normalizer import OcrNormalizer from src.normalizer.ocr.answer_set import NO_TABLES_MESSAGE from tests.unit.normalizer.ocr_builders import BAD, BALANCE, HEAD, INCOME, OK, merged, ocr_table, response from tests.worker_helpers import SUCCESS_ANSWER VERSION = "abc1234" def normalizer() -> OcrNormalizer: return OcrNormalizer(default_rules(), version=VERSION) def two_forms() -> dict[str, Any]: return response(ocr_table("Бухгалтерский баланс", BALANCE), ocr_table("Отчет о финансовых результатах", INCOME)) def test_done_with_documents_metrics_and_version() -> None: outcome = normalizer().normalize(merged(("j.json", response(ocr_table("Бухгалтерский баланс", BALANCE))))) assert outcome.status is ResultStatus.DONE and outcome.error is None and outcome.warnings == [] assert [d.document_type for d in outcome.documents] == ["BALANCE"] assert all(isinstance(d, StatementDocument) for d in outcome.documents) assert outcome.metrics is not None assert outcome.metrics.rows == sum(len(d.lines) for d in outcome.documents) > 0 assert outcome.statements_version == VERSION def test_one_document_needs_check_makes_set_needs_check() -> None: """Синтетический ОФР без строки 2300 не сходится по §7: документ needs_check, набор — тоже.""" outcome = normalizer().normalize(merged(("j.json", two_forms()))) assert [d.document_type for d in outcome.documents] == ["BALANCE", "INCOME"] assert [d.status for d in outcome.documents] == ["done", "needs_check"] assert outcome.status is ResultStatus.NEEDS_CHECK and outcome.warnings == [] def test_lost_json_keeps_documents_and_gives_needs_check() -> None: outcome = normalizer().normalize( merged(("j.json", response(ocr_table("Бухгалтерский баланс", BALANCE))), ("j_ofr.json", response(error=BAD))) ) assert outcome.status is ResultStatus.NEEDS_CHECK and len(outcome.documents) == 1 [warning] = outcome.warnings assert isinstance(warning, FileWarning) assert (warning.code, warning.severity, warning.locator) == ("JSON_FAILED", Severity.ERROR, "file:j_ofr.json") assert warning.message == "INVALID_DOCUMENT: Не удалось определить тип документа." def test_all_json_failed_takes_first_code() -> None: outcome = normalizer().normalize(merged(("j.json", response(error=BAD)), ("j_ofr.json", "не объект"))) assert outcome.status is ResultStatus.ERROR and outcome.error is not None assert (outcome.error.code, outcome.error.message) == (ErrorCode.INVALID_DOCUMENT, BAD["message"]) assert outcome.documents == [] and outcome.metrics is None and outcome.statements_version is None assert [w.locator for w in outcome.warnings] == ["file:j.json", "file:j_ofr.json"] def test_no_tables_is_recognition_failed() -> None: outcome = normalizer().normalize(merged(("j.json", {"error": OK, "data_ocr": {**HEAD, "table_structure": []}}))) assert outcome.status is ResultStatus.ERROR and outcome.error is not None assert (outcome.error.code, outcome.error.message) == (ErrorCode.RECOGNITION_FAILED, NO_TABLES_MESSAGE) assert [(w.code, w.message) for w in outcome.warnings] == [("JSON_FAILED", "в ответе ИС нет таблиц")] def test_table_exception_becomes_table_parse_error(monkeypatch: pytest.MonkeyPatch) -> None: real = answer_set_module.normalize_candidate def flaky(candidate: Any, rules: Any = None) -> Any: if candidate.locator.endswith("table:1"): raise RuntimeError("сломалось") return real(candidate, rules) monkeypatch.setattr(answer_set_module, "normalize_candidate", flaky) outcome = normalizer().normalize(merged(("j.json", two_forms()))) assert outcome.status is ResultStatus.NEEDS_CHECK and len(outcome.documents) == 1 [warning] = outcome.warnings assert (warning.code, warning.locator) == ("TABLE_PARSE_ERROR", "file:j.json;table:1") assert warning.message == "BACKEND_PARSING_ERROR: RuntimeError: сломалось" @pytest.mark.parametrize("raw", [None, {"job_id": "j"}, {"job_id": "j", "files": []}]) def test_not_merged_structure_is_invalid_format(raw: object) -> None: outcome = normalizer().normalize(raw) assert outcome.status is ResultStatus.ERROR and outcome.error is not None assert outcome.error.code is ErrorCode.INVALID_FORMAT and outcome.warnings == [] def test_success_answer_helper_from_worker_tests() -> None: outcome = normalizer().normalize(merged(("j.json", SUCCESS_ANSWER))) assert outcome.status is not ResultStatus.ERROR and len(outcome.documents) == 1 def test_default_rules_and_version_from_package() -> None: created = OcrNormalizer() assert created.statements_version == ocr_module.statements_version() def test_requires_statements_version(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr(ocr_module, "statements_version", lambda path=None: None) with pytest.raises(ValueError, match="без VERSION"): OcrNormalizer(default_rules()) assert OcrNormalizer(default_rules(), version="x").statements_version == "x"