Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BJKMLHdxMwSUeYcdkwqX4W
84 lines
4.4 KiB
Python
84 lines
4.4 KiB
Python
"""Инварианты форм на всех `../fixtures/statements-xlsx/**` по манифесту (spec «Словари форм, координаты, golden»)."""
|
||
|
||
import time
|
||
from pathlib import Path
|
||
from typing import cast
|
||
|
||
import pytest
|
||
from fixtures_helpers import statement_files
|
||
from result_contract import assert_valid_result
|
||
from statement_helpers import load_manifest, relative_key
|
||
|
||
from src.contract import FileType, ResultStatus, StatementDocument
|
||
from src.parser import get_parser
|
||
|
||
pytestmark = pytest.mark.fixtures
|
||
EXPECTED_FILES = 18
|
||
MAX_PARSE_SECONDS = 5
|
||
|
||
|
||
def explained_needs_check(document: StatementDocument) -> bool:
|
||
"""Причина needs_check видна в результате: предупреждение error, нарушенное соотношение, пометка проверки
|
||
у строки, значения или шапки, нечитаемая или пропущенная строка."""
|
||
checks = document.checks
|
||
return bool(
|
||
any(warning.severity == "error" for warning in document.warnings)
|
||
or (checks is not None and checks.relations_failed)
|
||
or document.header.needs_review_fields
|
||
or any(
|
||
line.needs_review
|
||
or line.line_status in ("unreadable", "missing")
|
||
or any(value.needs_review for value in line.values)
|
||
for line in document.lines
|
||
)
|
||
)
|
||
|
||
|
||
def test_manifest_covers_every_file(fixtures_dir: Path) -> None:
|
||
files = statement_files(fixtures_dir)
|
||
assert len(files) == EXPECTED_FILES
|
||
assert {relative_key(fixtures_dir, path) for path in files} == set(load_manifest())
|
||
|
||
|
||
def test_statement_file_matches_manifest(fixtures_dir: Path, statement_file: Path) -> None:
|
||
expectation = load_manifest()[relative_key(fixtures_dir, statement_file)]
|
||
started = time.monotonic()
|
||
file_type = cast(FileType, statement_file.suffix.lstrip(".").upper())
|
||
outcome = get_parser().parse(statement_file.read_bytes(), file_type)
|
||
assert time.monotonic() - started < MAX_PARSE_SECONDS
|
||
assert outcome.error is None, outcome.error
|
||
documents = [document for document in outcome.documents if isinstance(document, StatementDocument)]
|
||
assert [document.document_type for document in documents] == [item.type for item in expectation.documents]
|
||
all_done = all(item.status == "done" for item in expectation.documents)
|
||
assert outcome.status is (ResultStatus.DONE if all_done else ResultStatus.NEEDS_CHECK)
|
||
for document, item in zip(documents, expectation.documents, strict=True):
|
||
# повторы кодов строк законны (v2.0); ссылки на строки уникальны, значения ссылаются на колонки документа
|
||
refs = [line.ref for line in document.lines]
|
||
assert len(refs) == len(set(refs)), "ref уникальны"
|
||
column_indexes = {column.column_index for column in document.columns}
|
||
assert all(value.column_index in column_indexes for line in document.lines for value in line.values)
|
||
if item.sheets:
|
||
assert document.locator.count(",") == item.sheets - 1
|
||
failed = {relation.relation for relation in document.checks.relations_failed} if document.checks else set()
|
||
if item.failed_relations is not None:
|
||
assert failed == set(item.failed_relations)
|
||
if item.error_warnings is not None:
|
||
errors = {warning.code for warning in document.warnings if warning.severity == "error"}
|
||
assert errors == set(item.error_warnings)
|
||
if item.complete:
|
||
assert document.checks is not None
|
||
assert document.checks.relations_failed == [], document.checks
|
||
assert document.columns
|
||
assert all(column.period_date is not None for column in document.columns)
|
||
assert document.status == item.status, (item, document.header)
|
||
if item.status == "needs_check":
|
||
assert explained_needs_check(document), "needs_check должен объясняться полями результата"
|
||
if expectation.organization:
|
||
assert document.header.organization_name == expectation.organization
|
||
if expectation.inn:
|
||
if document.document_type in expectation.inn_optional_for:
|
||
assert document.header.inn in {None, expectation.inn}
|
||
else:
|
||
assert document.header.inn == expectation.inn
|
||
assert_valid_result(outcome)
|