- копия схем и примеров контракта 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
91 lines
4.3 KiB
Python
91 lines
4.3 KiB
Python
"""Покрытие словарей `rules/*.yaml` (design D-10, ruling R-22): каждая запись распознавания срабатывает хотя бы раз на
|
||
реальных ОСВ и синтетике; мёртвый синоним — повод удалить его или добавить файл синтетики `s14_*`.
|
||
|
||
Запись — один `Pattern` словаря (`sheet_markers`, `form_titles`/`okud` форм, `groups`, `debit`, `credit`,
|
||
`service_columns`,
|
||
`total_row`, `header_markers`, `mapping`, правила периодов, `year`, `end`, `header`/`row` типов данных,
|
||
`currency_aliases`). Перечень кодов `currency_codes` — список значений, не синонимы: в покрытие не входит. Записи
|
||
находятся обходом моделей `Rules`, поэтому новый паттерн в любом словаре попадает под проверку автоматически.
|
||
|
||
Файлы — реальные ОСВ `osv/**`, пакет ДКБ `osv-dkb/**` (диапазон месяцев, «Итого развернутое», шапка без меток) и
|
||
синтетика.
|
||
|
||
«Срабатывание» — `search`/`search_normalized` вернул совпадение. Счётчик — обёртка вокруг методов `Pattern` в тесте,
|
||
боевой код не меняется.
|
||
"""
|
||
|
||
from pathlib import Path
|
||
|
||
import pytest
|
||
from fixtures_helpers import (
|
||
osv_dkb_files,
|
||
osv_fixture_files,
|
||
statement_files,
|
||
statement_synthetic_files,
|
||
synthetic_files,
|
||
)
|
||
from rules_coverage import DICTIONARIES, PATTERNLESS, MatchCounter, iter_patterns, uncovered
|
||
|
||
from arm_statements import default_rules
|
||
from src.parser import get_parser
|
||
from src.parser.book import BookParser
|
||
from src.parser.osv import OsvParser
|
||
from src.parser.rules import DEFAULT_RULES_DIR, Rules, load_rules
|
||
from src.parser.statements.pipeline import StatementPipeline
|
||
|
||
pytestmark = pytest.mark.fixtures
|
||
|
||
|
||
@pytest.fixture(scope="module")
|
||
def coverage(fixtures_dir: Path) -> tuple[Rules, MatchCounter, int]:
|
||
monkeypatch = pytest.MonkeyPatch()
|
||
try:
|
||
counter = MatchCounter()
|
||
counter.install(monkeypatch)
|
||
rules = load_rules(DEFAULT_RULES_DIR)
|
||
base = get_parser()
|
||
assert isinstance(base, BookParser)
|
||
statement_rules = default_rules()
|
||
parser = BookParser(
|
||
rules,
|
||
osv=OsvParser(rules),
|
||
statements=StatementPipeline(statement_rules),
|
||
statement_rules=statement_rules,
|
||
max_bytes=base.max_bytes,
|
||
timeout_seconds=base.timeout_seconds,
|
||
)
|
||
files = [
|
||
*osv_fixture_files(fixtures_dir),
|
||
*osv_dkb_files(fixtures_dir),
|
||
*synthetic_files(fixtures_dir),
|
||
*statement_files(fixtures_dir),
|
||
*statement_synthetic_files(fixtures_dir),
|
||
]
|
||
for file in files:
|
||
parser.parse(file.read_bytes(), file.suffix.lstrip(".").upper()) # type: ignore[arg-type]
|
||
return rules, counter, len(files)
|
||
finally:
|
||
monkeypatch.undo()
|
||
|
||
|
||
def test_every_dictionary_entry_fires_on_fixtures_and_synthetic(coverage: tuple[Rules, MatchCounter, int]) -> None:
|
||
rules, counter, files = coverage
|
||
|
||
dead = uncovered(rules, counter)
|
||
|
||
assert dead == [], (
|
||
f"записи словарей, не сработавшие ни на одном из {files} файлов (удалите синоним или добавьте синтетику "
|
||
"s14_*):\n" + "\n".join(dead)
|
||
)
|
||
|
||
|
||
def test_coverage_sees_every_dictionary_entry_and_all_files(coverage: tuple[Rules, MatchCounter, int]) -> None:
|
||
"""Счётчик не пуст и обход видит записи всех семи словарей (защита от «покрытия» на пустом месте)."""
|
||
rules, counter, files = coverage
|
||
entries = list(iter_patterns(rules))
|
||
|
||
assert {file_name for file_name, _, _ in entries} == set(DICTIONARIES.values()) - PATTERNLESS
|
||
assert sum(counter.hits.values()) > len(entries)
|
||
assert files >= 95 # noqa: PLR2004 - реальные ОСВ, формы и синтетика из fixtures
|
||
assert not any(path.startswith("currency_codes") for _, path, _ in entries)
|