arm-core/api/arm_statements/tests/test_golden.py
Andrey Epifancev 46ac9367ee chore(arm_statements): копия пакета arm-statements@6bce80b (#128)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 19:38:30 +04:00

172 lines
8.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Golden на обезличенных входах Review Focus 1–5 (план 1, задача 20).
Входы `golden/*.candidate.json` собраны из `builders` один раз и лежат файлами; мусорный текст ячеек — дословно из
реальных распознаваний, названия, ИНН и суммы — обезличенные и синтетические. Эталон `*.expected.json` —
`to_jsonable(normalize_candidate(...).document)`; пересборка — `make golden-update` (`GOLDEN_UPDATE=1`).
"""
import json
import os
from collections.abc import Callable
from decimal import Decimal as D
from pathlib import Path
import pytest
from arm_statements import (
document_status,
normalize_candidate,
status_input,
status_input_from_contract,
to_contract,
)
from arm_statements.loader import Rules
from arm_statements.model import DocStatus, DocumentType, FormCandidate, LineStatus, StatementDocument
from arm_statements.serialize import candidate_from_dict, to_jsonable
from arm_statements.tests.builders import hdr, ocr
GOLDEN = Path(__file__).parent / "golden"
FULL_CONFIDENCE = 100.0
NAMES = ["aep3_balance", "aep3_income", "glued_balance", "kody_title_balance", "npz_income"]
HEAD = ["Наименование показателя", "Код", "На 30.09.2025"]
FULL_HEADER = hdr(organization_name="ООО «Пример»", inn="7700000000", report_date="30.09.2025", units="тыс. руб.")
def run(name: str, rules: Rules) -> StatementDocument:
data = json.loads((GOLDEN / f"{name}.candidate.json").read_text(encoding="utf-8"))
result = normalize_candidate(candidate_from_dict(data), rules)
assert result.document is not None
return result.document
@pytest.mark.parametrize("name", NAMES)
def test_golden_snapshot(name: str, rules: Rules) -> None:
actual = to_jsonable(run(name, rules))
target = GOLDEN / f"{name}.expected.json"
if os.environ.get("GOLDEN_UPDATE") == "1":
target.write_text(json.dumps(actual, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
pytest.fail(f"эталон {target.name} пересобран — просмотрите diff и запустите без GOLDEN_UPDATE")
assert actual == json.loads(target.read_text(encoding="utf-8"))
def assert_status_is_function_of_contract(doc: StatementDocument) -> None:
contract = json.loads(json.dumps(to_contract(doc), ensure_ascii=False))
assert status_input_from_contract(contract) == status_input(doc)
assert document_status(status_input(doc)) is doc.status
@pytest.mark.parametrize("name", NAMES)
def test_status_is_function_of_contract_document(name: str, rules: Rules) -> None:
assert_status_is_function_of_contract(run(name, rules))
def _disclosure() -> FormCandidate:
return ocr([["Текст"]], type_hint=DocumentType.DISCLOSURE)
def _without_lines() -> FormCandidate:
return ocr([["Итого", "15 000"]], type_hint=DocumentType.BALANCE, header=FULL_HEADER)
def _absolute_column_without_report_date() -> FormCandidate:
rows = [
HEAD,
["Запасы", "1210", "60"],
["Дебиторы", "1230", "40"],
["Итого II", "1200", "100"],
]
header = hdr(organization_name="ООО «Пример»", inn="7700000000", units="тыс. руб.")
return ocr(rows, type_hint=DocumentType.BALANCE, header=header)
EXTRA: dict[str, Callable[[], FormCandidate]] = {
"disclosure": _disclosure,
"without_lines": _without_lines,
"absolute_column_without_report_date": _absolute_column_without_report_date,
}
@pytest.mark.parametrize("case", sorted(EXTRA))
def test_status_is_function_of_contract_document_edge_cases(case: str, rules: Rules) -> None:
doc = normalize_candidate(EXTRA[case](), rules).document
assert doc is not None
assert_status_is_function_of_contract(doc)
def test_edge_cases_cover_their_branch(rules: Rules) -> None:
disclosure = normalize_candidate(_disclosure(), rules).document
assert disclosure is not None and disclosure.document_type is DocumentType.DISCLOSURE
assert disclosure.status is DocStatus.DONE
empty = normalize_candidate(_without_lines(), rules).document
assert empty is not None and empty.lines == ()
assert [w.code for w in empty.warnings if w.code == "CODE_COLUMN_NOT_FOUND"] == ["CODE_COLUMN_NOT_FOUND"]
assert status_input(empty).layout_found is False and empty.status is DocStatus.NEEDS_CHECK
absolute = normalize_candidate(_absolute_column_without_report_date(), rules).document
assert absolute is not None and absolute.header.report_date.value is None
assert status_input(absolute).period_known is True
def test_aep3_junk_codes(rules: Rules) -> None: # Review Focus 1
doc = run("aep3_balance", rules)
by_text = {ln.line_code_text: ln for ln in doc.lines}
assert (by_text["| 140"].line_status, by_text["| 140"].line_code) == (LineStatus.OK, "1140")
confidence = by_text["| 140"].line_code_confidence
assert confidence is not None and confidence < FULL_CONFIDENCE
for junk in ("НИ", "изо :"):
assert by_text[junk].line_status is LineStatus.DETAIL and by_text[junk].parent_ref is not None
refs = [ln.ref for ln in doc.lines]
assert len(refs) == len(set(refs)) and [ln.row_index for ln in doc.lines] == list(range(len(doc.lines)))
assert by_text["НИ"].parent_ref == by_text["1110"].ref and by_text["изо :"].parent_ref == by_text["| 140"].ref
assert len(doc.lines) == len(json.loads((GOLDEN / "aep3_balance.candidate.json").read_text("utf-8"))["rows"]) - 1
def test_aep3_dirty_income_amounts(rules: Rules) -> None: # Review Focus 2
doc = run("aep3_income", rules)
values = {v.text: v for ln in doc.lines for v in ln.values if v.text}
assert (values["(7744536}"].amount, values["(7744536}"].needs_review) == (D("-7744536"), False)
assert (values["211(1 |"].amount, values["211(1 |"].needs_review) == (None, True)
assert (values["42831 1"].amount, values["42831 1"].needs_review) == (D("428311"), False)
assert doc.checks is not None and doc.checks.relations_failed == ()
assert doc.status is DocStatus.NEEDS_CHECK
def test_glued_rows(rules: Rules) -> None: # Review Focus 3
doc = run("glued_balance", rules)
split = [ln for ln in doc.lines if "#" in ln.ref]
assert [(ln.line_code, ln.values[0].amount) for ln in split] == [("1500", D("917775")), ("1700", D("2371333"))]
assert [ln.ref for ln in split] == ["row:13#1", "row:13#2"]
assert len({ln.ref for ln in doc.lines}) == len(doc.lines)
assert [w.code for w in doc.warnings if w.code == "ROW_SPLIT"] == ["ROW_SPLIT"]
assert not [v for ln in doc.lines for v in ln.values if v.needs_review]
assert not [ln for ln in doc.lines if ln.needs_review]
def test_kody_title_block(rules: Rules) -> None: # Review Focus 4
doc = run("kody_title_balance", rules)
assert "12267" not in [ln.line_code for ln in doc.lines]
assert doc.header.okopf.value == "12267" and doc.status is DocStatus.DONE
assert not [w for w in doc.warnings if w.code == "CODE_COLUMN_NOT_FOUND"]
assert all(ln.ref.startswith("Баланс!C") for ln in doc.lines)
def test_npz_continuation_single_code(rules: Rules) -> None: # Review Focus 5
doc = run("npz_income", rules)
(line_2400,) = [ln for ln in doc.lines if ln.line_code == "2400"]
assert [v.column_index for v in line_2400.values] == [c.column_index for c in doc.columns]
assert all(v.amount is not None for v in line_2400.values)
assert [v.amount for v in line_2400.values] == [D("264"), D("238")]
assert "," in doc.locator and not [w for w in doc.warnings if w.code.startswith("SEGMENT_")]
assert doc.status is DocStatus.DONE
@pytest.mark.parametrize("name", NAMES)
def test_document_warnings_reference_document_parts(name: str, rules: Rules) -> None:
"""I-2: line_refs предупреждения — ref строк документа; locator нет; column_index — из колонок."""
doc = run(name, rules)
line_refs = {ln.ref for ln in doc.lines}
column_indexes = {c.column_index for c in doc.columns}
for warning in doc.warnings:
assert set(warning.line_refs) <= line_refs, warning
assert warning.locator is None, warning
assert warning.column_index is None or warning.column_index in column_indexes, warning