"""Сценарии спеки `statement-parsing` на синтетических файлах (Срез 2b, Task 13). Ожидания выведены из сценариев спеки и из содержимого генератора, а не из текущего вывода парсера: файл — вход, оракулом он не служит. Без каталога fixtures тесты пропускаются (маркер `fixtures`). """ from decimal import Decimal from pathlib import Path from typing import cast import pytest from result_contract import assert_valid_result from src.contract import FileType, ResultStatus, StatementDocument from src.contract.result import StatementLine from src.parser import ParseOutcome, get_parser pytestmark = pytest.mark.fixtures @pytest.fixture(autouse=True) def _require_synthetic(fixtures_dir: Path) -> None: """T13.1: fixtures есть, а `statements-xlsx/synthetic` нет/пуст — skip, а не падение.""" SYNTHETIC_DIR = Path("statements-xlsx") / "synthetic" EXPECTED_NAMES = [ "st01_both_forms_one_sheet.xlsx", "st02_csv_like.xlsx", "st03_no_codes.xlsx", "st04_mixed_book.xlsx", "st05_continuation_only.xlsx", ] ORG = 'ООО "Тестовая организация"' D = Decimal EXPECTED_DOCS_COUNT = 2 EXPECTED_STATEMENT_COUNT = 1 EXPECTED_LOCATORS_COUNT = 1 EXPECTED_LINES_COUNT = 0 def synthetic_path(fixtures_dir: Path, name: str) -> Path: return fixtures_dir / SYNTHETIC_DIR / name def parse_path(path: Path) -> ParseOutcome: return get_parser().parse(path.read_bytes(), cast(FileType, path.suffix.lstrip(".").upper())) def parse_named(fixtures_dir: Path, name: str) -> ParseOutcome: path = synthetic_path(fixtures_dir, name) if not path.is_file(): pytest.fail(f"нет файла {name}: запустите генератор (см. README fixtures)") return parse_path(path) def statement_documents(outcome: ParseOutcome) -> list[StatementDocument]: """Документы форм результата.""" assert outcome.error is None, outcome.error return [doc for doc in outcome.documents if isinstance(doc, StatementDocument)] def one_statement(outcome: ParseOutcome) -> StatementDocument: (doc,) = statement_documents(outcome) return doc def warnings_of(outcome: ParseOutcome) -> list[str]: assert outcome.metrics is not None assert outcome.metrics.warnings is not None return outcome.metrics.warnings def by_code(doc: StatementDocument) -> dict[str, StatementLine]: """Строки по коду показателя.""" lines = {line.line_code: line for line in doc.lines} return lines def get_line_value(line: StatementLine, column_index: int = 0) -> D | None: """Получить значение из строки по индексу колонки.""" for value in line.values: if value.column_index == column_index: return value.amount return None # --- Состав набора и общие ожидания --- def test_synthetic_set_is_complete(fixtures_dir: Path) -> None: """В `fixtures/statements-xlsx/synthetic/` лежат файлы набора st01…st05.""" present = sorted(path.name for path in (fixtures_dir / SYNTHETIC_DIR).glob("st*") if path.is_file()) assert present == EXPECTED_NAMES def test_every_synthetic_file_is_listed(statement_synthetic_file: Path) -> None: """Каждый файл каталога — один из ожидаемых.""" assert statement_synthetic_file.name in EXPECTED_NAMES # --- Сценарии спеки --- def test_st01_both_forms_one_sheet(fixtures_dir: Path) -> None: """st01: ББ и ОФР на одном листе; два документа с одним `locator`.""" outcome = parse_named(fixtures_dir, "st01_both_forms_one_sheet.xlsx") stmt_docs = statement_documents(outcome) assert len(stmt_docs) == EXPECTED_DOCS_COUNT, ( f"ожидается {EXPECTED_DOCS_COUNT} документа, получено {len(stmt_docs)}" ) # Оба документа имеют один locator locators = {doc.locator for doc in stmt_docs} assert len(locators) == EXPECTED_LOCATORS_COUNT, ( f"ожидается {EXPECTED_LOCATORS_COUNT} уникальный locator, получено {len(locators)}" ) # Проверяем типы документов doc_types = {doc.document_type for doc in stmt_docs} assert "BALANCE" in doc_types and "INCOME" in doc_types, f"ожидаются типы BALANCE и INCOME, получены {doc_types}" def test_st02_csv_like_format(fixtures_dir: Path) -> None: """st02: CSV-подобный лист; организация и ИНН найдены, units None с warning.""" outcome = parse_named(fixtures_dir, "st02_csv_like.xlsx") doc = one_statement(outcome) # Проверяем организацию assert doc.header.organization_name == 'ООО "Синтетика"' assert doc.header.inn == "7700000001" # Проверяем units = None с warning assert doc.header.units is None warnings = warnings_of(outcome) assert any("unit" in w.lower() or "измер" in w.lower() for w in warnings), ( f"ожидается warning о единицах, получены: {warnings}" ) # Проверяем что документ содержит данные assert len(doc.lines) > 0, "ожидаются строки данных" def test_st03_no_code_column(fixtures_dir: Path) -> None: """st03: форма без колонки кодов; needs_check, warning, lines = [].""" outcome = parse_named(fixtures_dir, "st03_no_codes.xlsx") doc = one_statement(outcome) # Статус needs_check assert outcome.status is ResultStatus.NEEDS_CHECK # Нет найденных строк assert len(doc.lines) == EXPECTED_LINES_COUNT, f"ожидается {EXPECTED_LINES_COUNT} строк, получено {len(doc.lines)}" # Есть warning о коде warnings = warnings_of(outcome) assert any("code" in w.lower() or "код" in w.lower() for w in warnings), ( f"ожидается warning о коде, получены: {warnings}" ) def test_st04_mixed_book_osv_and_statement(fixtures_dir: Path) -> None: """st04: книга с листом ОСВ и листом баланса; OsvDocument + StatementDocument, статус done.""" outcome = parse_named(fixtures_dir, "st04_mixed_book.xlsx") stmt_docs = statement_documents(outcome) assert len(stmt_docs) == EXPECTED_STATEMENT_COUNT, ( f"ожидается {EXPECTED_STATEMENT_COUNT} StatementDocument, получено {len(stmt_docs)}" ) # Проверяем что есть также OsvDocument (проверяя общее количество документов) assert len(outcome.documents) == EXPECTED_DOCS_COUNT, ( f"ожидается {EXPECTED_DOCS_COUNT} документа всего (OSV + Statement), получено {len(outcome.documents)}" ) # Статус done assert outcome.status is ResultStatus.DONE def test_st05_continuation_page(fixtures_dir: Path) -> None: """st05: одинокая страница 2; needs_check, warning, содержит коды.""" outcome = parse_named(fixtures_dir, "st05_continuation_only.xlsx") doc = one_statement(outcome) # Статус needs_check assert outcome.status is ResultStatus.NEEDS_CHECK # Есть warning о continuation warnings = warnings_of(outcome) assert any( "continuation" in w.lower() or "страниц" in w.lower() or "страница" in w.lower() or "page" in w.lower() for w in warnings ), f"ожидается warning о continuation, получены: {warnings}" # Проверяем что есть коды codes = {line.line_code for line in doc.lines if line.line_code} assert len(codes) > 0, "ожидаются коды, получены пустое множество" # --- Валидация контракта --- def test_all_synthetic_results_are_valid(statement_synthetic_file: Path) -> None: """Все результаты валидны по контракту (общая проверка для всех синтетических файлов).""" outcome = parse_path(statement_synthetic_file) assert_valid_result(outcome)