"""Покрытие словарей `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)