"""Инварианты форм на всех `../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)