Refs #14 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: cedd7915-2efe-41e5-988c-a9117033598b
90 lines
4.4 KiB
Python
90 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 document_ok(document: StatementDocument) -> bool:
|
|
"""Документ пригоден (done): критерий `StatementParser._is_ok`, выраженный полями результата."""
|
|
if document.document_type == "DISCLOSURE":
|
|
return True
|
|
checks = document.checks
|
|
return (
|
|
checks is not None
|
|
and bool(document.columns)
|
|
and bool(document.header.organization_name or document.header.inn)
|
|
and not checks.relations_failed
|
|
and not checks.duplicate_line_codes
|
|
)
|
|
|
|
|
|
def explained_needs_check(document: StatementDocument) -> bool:
|
|
"""Причина needs_check видна в результате: нарушенное соотношение, дубликаты кодов, нет колонок или шапки."""
|
|
checks = document.checks
|
|
return bool(
|
|
checks is None
|
|
or checks.relations_failed
|
|
or checks.duplicate_line_codes
|
|
or not document.columns
|
|
or not (document.header.organization_name or document.header.inn)
|
|
)
|
|
|
|
|
|
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):
|
|
codes = [line.line_code for line in document.lines]
|
|
assert len(codes) == len(set(codes)), "коды уникальны"
|
|
assert all(value.column_index < len(document.columns) 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.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)
|
|
# статус документа — по критерию парсера (statement.py::_is_ok), независимо от манифеста
|
|
assert document_ok(document) == (item.status == "done"), (item, document.header)
|
|
if item.status == "needs_check":
|
|
assert not document_ok(document)
|
|
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)
|