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