checks.py: тождество сальдо по каждой строке, суммы родителя = Σ детей того же data_type (CURRENCY/QUANTITY исключены; обороты — по колонкам, сальдо — по колонкам или по нетто при свёрнутом сальдо родителя), «Итого» = корневой счёт (R-13: нет корня — «Итого» пусто/0; R-20: нет «Итого» — false и предупреждение). osv.py: OsvParser — лимит размера, чтение по сигнатуре, листы по содержимому (формы ББ/ОФР и прочие — пропуск с предупреждением), карта исходов D-6, кооперативный дедлайн, metrics (rows без TOTAL, warnings с «sheet=<имя> hierarchy=<стратегия>» первой записью по листу). get_parser(settings, rules_dir=…) → OsvParser (словари грузятся при создании: битый словарь — падение старта); PARSE_MAX_BYTES / PARSE_TIMEOUT_SECONDS в Settings и .env.*; app.py передаёт настройки. test_null_parser.py → test_parser_factory.py. Тесты: юнит-карта статусов, «Испорченная сумма», «Пустая ОСВ», два листа ОСВ, новый синоним без релиза кода (R-8); property-тест на 66 реальных файлах (все done, < 5 с, схема v1.1). test_app и интеграционный round-trip ждут ошибку реального парсера вместо NOT_IMPLEMENTED. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
54 lines
2.5 KiB
Python
54 lines
2.5 KiB
Python
"""Property-тест парсера на всех реальных ОСВ `../fixtures/osv/**` (design D-10; spec «Золотые снимки и
|
||
property-тесты», «Контрольные суммы определяют статус», «Пустая ОСВ», «Форматы, лимиты, время»).
|
||
|
||
Инварианты проверяются независимо от снимков: статус `done`, один документ, контрольные суммы §1.4 сошлись, корень =
|
||
счёт шапки, период распознан, разбор быстрее 5 с, результат валиден по контракту v1.1. Пустая ОСВ (R-13) определяется
|
||
свойством — в `rows` только TOTAL, — а не списком файлов.
|
||
"""
|
||
|
||
import time
|
||
from pathlib import Path
|
||
from typing import cast
|
||
|
||
import pytest
|
||
from result_contract import assert_valid_result
|
||
|
||
from src.contract import FileType, OsvDocument, ResultStatus
|
||
from src.parser import get_parser
|
||
|
||
MAX_PARSE_SECONDS = 5
|
||
|
||
|
||
@pytest.mark.fixtures
|
||
def test_real_osv_parses_done_with_invariants(osv_file: Path) -> None:
|
||
parser = get_parser()
|
||
data = osv_file.read_bytes()
|
||
|
||
started = time.monotonic()
|
||
outcome = parser.parse(data, cast(FileType, osv_file.suffix.lstrip(".").upper()))
|
||
seconds = time.monotonic() - started
|
||
|
||
assert outcome.error is None, outcome.error
|
||
assert outcome.status is ResultStatus.DONE, outcome.metrics
|
||
(document,) = outcome.documents
|
||
assert isinstance(document, OsvDocument)
|
||
checks = document.checks
|
||
assert checks is not None
|
||
assert checks.total_row_matches_root is True
|
||
assert checks.balance_identity_violations == []
|
||
assert checks.parent_sum_violations == []
|
||
assert document.header.period_quarter is not None
|
||
data_rows = [row for row in document.rows if row.group_type != "TOTAL"]
|
||
assert outcome.metrics is not None
|
||
assert outcome.metrics.rows == len(data_rows)
|
||
if data_rows:
|
||
assert outcome.metrics.rows > 0
|
||
assert document.header.account_code_raw == document.rows[0].account_code_raw
|
||
else:
|
||
# Пустая ОСВ (R-13): только «Итого»
|
||
assert [row.group_type for row in document.rows] == ["TOTAL"]
|
||
assert outcome.metrics.warnings is not None
|
||
assert "нет строк данных" in outcome.metrics.warnings
|
||
assert seconds < MAX_PARSE_SECONDS, f"{osv_file.name}: {seconds:.2f} с"
|
||
assert_valid_result(outcome)
|