"""Фабрика парсера и заглушка (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)