fix: дедлайн при классификации листов и тесты путей отказа форм (#54)
I-1: plan_sheets проверяет дедлайн на каждом листе (регрессия против main). I-2: тесты исключения пакета на кандидате (RECOGNITION_FAILED с предупреждениями файла), TIMEOUT в адаптере и конвейере, RulesError в get_parser. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BJKMLHdxMwSUeYcdkwqX4W
This commit is contained in:
parent
be5ad8ce86
commit
2fce78080b
@ -143,7 +143,7 @@ class BookParser:
|
|||||||
if read.no_codepage:
|
if read.no_codepage:
|
||||||
warnings.append(_file_warning(make_warning(WarningCode.XLS_NO_CODEPAGE, NO_CODEPAGE_WARNING, rules)))
|
warnings.append(_file_warning(make_warning(WarningCode.XLS_NO_CODEPAGE, NO_CODEPAGE_WARNING, rules)))
|
||||||
sheets = read.sheets
|
sheets = read.sheets
|
||||||
plan = plan_sheets(sheets, lambda sheet: is_osv_sheet(sheet, self._rules), rules)
|
plan = plan_sheets(sheets, lambda sheet: is_osv_sheet(sheet, self._rules), rules, deadline)
|
||||||
osv_names = {sheet.name for sheet in plan.osv}
|
osv_names = {sheet.name for sheet in plan.osv}
|
||||||
other_names = {sheet.name for sheet in plan.other}
|
other_names = {sheet.name for sheet in plan.other}
|
||||||
reader_notes = _reader_notes(read)
|
reader_notes = _reader_notes(read)
|
||||||
|
|||||||
@ -53,7 +53,9 @@ class SheetPlan:
|
|||||||
other: tuple[Sheet, ...]
|
other: tuple[Sheet, ...]
|
||||||
|
|
||||||
|
|
||||||
def plan_sheets(sheets: Sequence[Sheet], is_osv: Callable[[Sheet], bool], rules: StatementRules) -> SheetPlan:
|
def plan_sheets(
|
||||||
|
sheets: Sequence[Sheet], is_osv: Callable[[Sheet], bool], rules: StatementRules, deadline: Deadline
|
||||||
|
) -> SheetPlan:
|
||||||
"""Листы по порядку книги: ОСВ; форма; продолжение (в том числе слабое — лист из 1–2 кодов сразу после листа
|
"""Листы по порядку книги: ОСВ; форма; продолжение (в том числе слабое — лист из 1–2 кодов сразу после листа
|
||||||
формы или продолжения); прочее. Лист ОСВ или «прочее» прерывает «сразу после»."""
|
формы или продолжения); прочее. Лист ОСВ или «прочее» прерывает «сразу после»."""
|
||||||
osv: list[Sheet] = []
|
osv: list[Sheet] = []
|
||||||
@ -61,6 +63,7 @@ def plan_sheets(sheets: Sequence[Sheet], is_osv: Callable[[Sheet], bool], rules:
|
|||||||
other: list[Sheet] = []
|
other: list[Sheet] = []
|
||||||
last_type: DocumentType | None = None # тип последнего блока последнего листа формы; None — формы «сразу до» нет
|
last_type: DocumentType | None = None # тип последнего блока последнего листа формы; None — формы «сразу до» нет
|
||||||
for sheet in sheets:
|
for sheet in sheets:
|
||||||
|
deadline.check(f"лист «{sheet.name}»: вид листа")
|
||||||
if is_osv(sheet):
|
if is_osv(sheet):
|
||||||
osv.append(sheet)
|
osv.append(sheet)
|
||||||
last_type = None
|
last_type = None
|
||||||
|
|||||||
@ -138,11 +138,11 @@ def test_plan_sheets_weak_continuation_only_after_form(rules: StatementRules) ->
|
|||||||
def is_osv(sheet: Sheet) -> bool:
|
def is_osv(sheet: Sheet) -> bool:
|
||||||
return sheet.name == "ОСВ"
|
return sheet.name == "ОСВ"
|
||||||
|
|
||||||
plan = plan_sheets([ofr, weak], is_osv, rules)
|
plan = plan_sheets([ofr, weak], is_osv, rules, NO_DEADLINE)
|
||||||
assert [(s.name, k) for s, k in plan.statements] == [("ОФР", STATEMENT), ("2400", CONTINUATION)]
|
assert [(s.name, k) for s, k in plan.statements] == [("ОФР", STATEMENT), ("2400", CONTINUATION)]
|
||||||
plan = plan_sheets([osv, weak], is_osv, rules)
|
plan = plan_sheets([osv, weak], is_osv, rules, NO_DEADLINE)
|
||||||
assert (plan.osv, plan.statements, plan.other) == ((osv,), (), (weak,))
|
assert (plan.osv, plan.statements, plan.other) == ((osv,), (), (weak,))
|
||||||
plan = plan_sheets([ofr, notes, weak], is_osv, rules)
|
plan = plan_sheets([ofr, notes, weak], is_osv, rules, NO_DEADLINE)
|
||||||
assert [s.name for s in plan.other] == ["Примечания", "2400"]
|
assert [s.name for s in plan.other] == ["Примечания", "2400"]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -536,6 +536,61 @@ def test_deadline_exceeded_is_timeout(rules: Rules) -> None:
|
|||||||
assert "превышен лимит" in outcome.error.message
|
assert "превышен лимит" in outcome.error.message
|
||||||
|
|
||||||
|
|
||||||
|
def test_sheet_kind_classification_is_interrupted_by_deadline(rules: Rules) -> None:
|
||||||
|
"""I-1: классификация листов (`plan_sheets`) проверяет дедлайн на каждом листе, книга из «прочего» — TIMEOUT."""
|
||||||
|
notes: list[Cells] = [["Примечания"], ["Текст без кодов и сумм"]]
|
||||||
|
clock = itertools.count(step=CLOCK_STEP)
|
||||||
|
content = xlsx({"Лист 1": notes, "Лист 2": notes, "Лист 3": notes})
|
||||||
|
|
||||||
|
outcome = book(rules, timeout_seconds=SHORT_TIMEOUT, clock=lambda: float(next(clock))).parse(content, "XLSX")
|
||||||
|
|
||||||
|
assert outcome.error is not None
|
||||||
|
assert outcome.error.code is ErrorCode.TIMEOUT
|
||||||
|
assert "вид листа" in outcome.error.message
|
||||||
|
|
||||||
|
|
||||||
|
def test_package_exception_on_candidate_is_recognition_failed_with_collected_warnings(
|
||||||
|
rules: Rules, monkeypatch: pytest.MonkeyPatch
|
||||||
|
) -> None:
|
||||||
|
"""I-2(а), P4-11: исключение пакета на кандидате — ошибка книги, предупреждения файла до него сохраняются."""
|
||||||
|
|
||||||
|
def broken(candidate: object, statement_rules: object) -> object:
|
||||||
|
raise ValueError("сбой пакета\nвторая строка")
|
||||||
|
|
||||||
|
monkeypatch.setattr("src.parser.statements.pipeline.normalize_candidate", broken)
|
||||||
|
notes: list[Cells] = [["Примечания"], ["Текст без кодов и сумм"]]
|
||||||
|
|
||||||
|
outcome = book(rules).parse(xlsx({"Примечания": notes, "ББ": BALANCE_ROWS}), "XLSX")
|
||||||
|
|
||||||
|
assert outcome.status is ResultStatus.ERROR
|
||||||
|
assert outcome.error is not None
|
||||||
|
assert outcome.error.code is ErrorCode.RECOGNITION_FAILED
|
||||||
|
assert outcome.error.message == "ValueError: сбой пакета вторая строка"
|
||||||
|
assert [(w.code, w.locator) for w in file_warnings_of(outcome)] == [("SHEET_OTHER", "sheet:Примечания")]
|
||||||
|
|
||||||
|
|
||||||
|
def test_deadline_on_forms_path_is_timeout_in_adapter(rules: Rules) -> None:
|
||||||
|
"""I-2(б): дедлайн истекает на сборке кандидатов адаптера (после классификации листа)."""
|
||||||
|
ticks = itertools.chain([0.0] * 3, itertools.repeat(1e9)) # старт, создание дедлайна, вид листа
|
||||||
|
|
||||||
|
outcome = book(rules, timeout_seconds=100, clock=lambda: next(ticks)).parse(xlsx({"ББ": BALANCE_ROWS}), "XLSX")
|
||||||
|
|
||||||
|
assert outcome.error is not None
|
||||||
|
assert outcome.error.code is ErrorCode.TIMEOUT
|
||||||
|
assert "форма" in outcome.error.message
|
||||||
|
|
||||||
|
|
||||||
|
def test_deadline_on_forms_path_is_timeout_in_pipeline(rules: Rules) -> None:
|
||||||
|
"""I-2(б): дедлайн истекает перед нормализацией кандидата в `StatementPipeline`."""
|
||||||
|
ticks = itertools.chain([0.0] * 4, itertools.repeat(1e9)) # + проверка адаптера
|
||||||
|
|
||||||
|
outcome = book(rules, timeout_seconds=100, clock=lambda: next(ticks)).parse(xlsx({"ББ": BALANCE_ROWS}), "XLSX")
|
||||||
|
|
||||||
|
assert outcome.error is not None
|
||||||
|
assert outcome.error.code is ErrorCode.TIMEOUT
|
||||||
|
assert "форма «sheet:ББ»" in outcome.error.message
|
||||||
|
|
||||||
|
|
||||||
def test_unexpected_exception_is_recognition_failed_in_one_line(rules: Rules, monkeypatch: pytest.MonkeyPatch) -> None:
|
def test_unexpected_exception_is_recognition_failed_in_one_line(rules: Rules, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
def broken(rows: Sequence[BodyRow], deadline: Deadline | None = None) -> dict[int, int] | None:
|
def broken(rows: Sequence[BodyRow], deadline: Deadline | None = None) -> dict[int, int] | None:
|
||||||
raise ValueError("сбой стратегии\nвторая строка")
|
raise ValueError("сбой стратегии\nвторая строка")
|
||||||
|
|||||||
@ -35,6 +35,7 @@ from src.contract import (
|
|||||||
from src.core.config import NATSSettings
|
from src.core.config import NATSSettings
|
||||||
from src.core.nats import NatsConnection, NatsPublishError
|
from src.core.nats import NatsConnection, NatsPublishError
|
||||||
from src.core.worker import RecognitionWorker, result_key
|
from src.core.worker import RecognitionWorker, result_key
|
||||||
|
from arm_statements import RulesError
|
||||||
from src.parser import NullParser, ParseOutcome, get_parser, require_statements_version
|
from src.parser import NullParser, ParseOutcome, get_parser, require_statements_version
|
||||||
|
|
||||||
API_DIR = Path(__file__).resolve().parents[2]
|
API_DIR = Path(__file__).resolve().parents[2]
|
||||||
@ -935,3 +936,15 @@ def test_require_statements_version_reads_the_marker(tmp_path: Path) -> None:
|
|||||||
|
|
||||||
assert require_statements_version(marker) == "abc1234"
|
assert require_statements_version(marker) == "abc1234"
|
||||||
assert require_statements_version() # копия пакета в репозитории размечена sync-statements
|
assert require_statements_version() # копия пакета в репозитории размечена sync-statements
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_parser_wraps_rules_error_into_runtime_error(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
"""I-2(в): ошибка словарей пакета при старте — `RuntimeError`, сервис не стартует."""
|
||||||
|
|
||||||
|
def broken() -> object:
|
||||||
|
raise RulesError("битый словарь форм")
|
||||||
|
|
||||||
|
monkeypatch.setattr("src.parser.default_rules", broken)
|
||||||
|
|
||||||
|
with pytest.raises(RuntimeError, match="битый словарь форм"):
|
||||||
|
get_parser()
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user