arm-tabular-parser/api/tests/unit/test_parser_factory.py
Andrey Epifancev e705d75520 feat(parser): контрольные суммы §1.4, статусы, OsvParser как парсер по умолчанию, лимиты
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
2026-10-05 14:37:21 +04:00

85 lines
3.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Фабрика парсера и заглушка (design D-3, D-7, D-8; spec «Парсер — заменяемый интерфейс», «Словари — данные»).
`get_parser()` по умолчанию отдаёт `OsvParser` со словарями `api/rules/` и лимитами из настроек; битый словарь —
падение при создании (старт сервиса), а не при первом файле. `NullParser` остаётся для тестов воркера:
`NOT_IMPLEMENTED` для любого типа файла, входные байты не читаются.
"""
import shutil
from collections.abc import Callable
from pathlib import Path
from typing import Any, get_args
import pytest
from src.contract import ErrorCode, FileType, MessageError, ResultStatus
from src.core.config import Settings
from src.parser import NullParser, ParseOutcome, Parser, get_parser
from src.parser.osv import OsvParser
from src.parser.rules import DEFAULT_RULES_DIR, HEADERS_FILE
DEFAULT_MAX_BYTES = 50 * 1024 * 1024
DEFAULT_TIMEOUT_SECONDS = 120
class UntouchableBytes(bytes):
"""Байты, любое обращение к содержимому которых — ошибка теста."""
def _fail(self, *_args: Any, **_kwargs: Any) -> Any:
raise AssertionError("NullParser прикоснулся к входным байтам")
__iter__ = __len__ = __getitem__ = __bytes__ = __contains__ = __eq__ = __hash__ = _fail # type: ignore[assignment]
@pytest.mark.parametrize("file_type", get_args(FileType))
def test_null_parser_returns_not_implemented_without_touching_input(file_type: FileType) -> None:
data = UntouchableBytes(b"content")
outcome = NullParser().parse(data, file_type)
assert outcome.status is ResultStatus.ERROR
assert outcome.error == MessageError(
code=ErrorCode.NOT_IMPLEMENTED,
message="разбор XLS/XLSX/XLSB реализуется в Срезе 2",
)
assert outcome.documents == ()
assert outcome.metrics is None
def test_get_parser_returns_osv_parser_with_default_limits() -> None:
parser = get_parser()
assert isinstance(parser, OsvParser)
assert isinstance(parser, Parser)
assert (parser.max_bytes, parser.timeout_seconds) == (DEFAULT_MAX_BYTES, DEFAULT_TIMEOUT_SECONDS)
def test_get_parser_takes_limits_from_settings(minimal_settings: Callable[..., Settings]) -> None:
settings = minimal_settings("PARSE_MAX_BYTES=1024\nPARSE_TIMEOUT_SECONDS=7\n")
parser = get_parser(settings)
assert isinstance(parser, OsvParser)
assert (parser.max_bytes, parser.timeout_seconds) == (1024, 7) # noqa: PLR2004 - значения из окружения теста
def test_get_parser_fails_on_broken_dictionary(tmp_path: Path) -> None:
"""Spec «Словари — данные»: ошибка словаря — падение старта с указанием файла и записи."""
broken = tmp_path / "rules"
shutil.copytree(DEFAULT_RULES_DIR, broken)
headers = broken / HEADERS_FILE
headers.write_text(headers.read_text(encoding="utf-8").replace("total_row:", "total_rows:"), encoding="utf-8")
with pytest.raises(RuntimeError, match=r"osv_headers\.yaml.*total_row"):
get_parser(rules_dir=broken)
def test_outcome_error_requires_error_payload() -> None:
with pytest.raises(ValueError, match="error"):
ParseOutcome(status=ResultStatus.ERROR)
def test_outcome_success_requires_documents() -> None:
with pytest.raises(ValueError, match="documents"):
ParseOutcome(status=ResultStatus.DONE)