- копия схем и примеров контракта v2.0 (запрос остаётся v1.0), модели result/common/message/request; ResultWarning/FileWarning, статус и предупреждения у документов, строки forms v2.0, service.statements_version - statements/pipeline.py: кандидаты адаптера -> normalize_candidate -> to_contract / warning_to_contract - BookParser: статус книги через envelope_status, SHEET_OTHER / FILE_TYPE_MISMATCH / XLS_NO_CODEPAGE как предупреждения файла, OSV_* и OSV_XLSB_NO_OUTLINE в документах ОСВ, тексты иерархии по спеке - воркер, app и CLI получают statements_version из require_statements_version() - старые модели документа формы вынесены в statements/legacy_models.py до удаления старого конвейера (задача 9) - OpenSpec: MODIFIED «Парсер — заменяемый интерфейс» в дельте tabular-recognition-service Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BJKMLHdxMwSUeYcdkwqX4W
54 lines
2.6 KiB
Python
54 lines
2.6 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)
|
||
assert document.locator == "sheet:Лист_1" # spec «Один лист — одна ОСВ»: имя листа берётся из файла, не ищется
|
||
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 "нет строк данных" in [warning.message for warning in document.warnings]
|
||
assert seconds < MAX_PARSE_SECONDS, f"{osv_file.name}: {seconds:.2f} с"
|
||
assert_valid_result(outcome)
|