EXPECTED_FILES 66 → 72, порог покрытия словарей, имя теста без числа, CLAUDE.md. Снимки — arm-ka-fixtures#7. make test-fixtures: 873 passed (ошибка только у roundtrip без NATS). Refs #10 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
64 lines
3.4 KiB
Python
64 lines
3.4 KiB
Python
"""Покрытие словарей `rules/*.yaml` (design D-10, ruling R-22): каждая запись распознавания срабатывает хотя бы раз на
|
||
реальных ОСВ и синтетике; мёртвый синоним — повод удалить его или добавить файл синтетики `s14_*`.
|
||
|
||
Запись — один `Pattern` словаря (`sheet_markers`, `statement_markers`, `groups`, `debit`, `credit`, `service_columns`,
|
||
`total_row`, `header_markers`, `mapping`, правила периодов, `year`, `end`, `header`/`row` типов данных,
|
||
`currency_aliases`). Перечень кодов `currency_codes` — список значений, не синонимы: в покрытие не входит. Записи
|
||
находятся обходом моделей `Rules`, поэтому новый паттерн в любом словаре попадает под проверку автоматически.
|
||
|
||
«Срабатывание» — `search`/`search_normalized` вернул совпадение. Счётчик — обёртка вокруг методов `Pattern` в тесте,
|
||
боевой код не меняется.
|
||
"""
|
||
|
||
from pathlib import Path
|
||
|
||
import pytest
|
||
from fixtures_helpers import osv_fixture_files, synthetic_files
|
||
from rules_coverage import DICTIONARIES, MatchCounter, iter_patterns, uncovered
|
||
|
||
from src.parser import get_parser
|
||
from src.parser.osv import OsvParser
|
||
from src.parser.rules import DEFAULT_RULES_DIR, Rules, load_rules
|
||
|
||
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, OsvParser)
|
||
parser = OsvParser(rules, max_bytes=base.max_bytes, timeout_seconds=base.timeout_seconds)
|
||
files = [*osv_fixture_files(fixtures_dir), *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())
|
||
assert sum(counter.hits.values()) > len(entries)
|
||
assert files >= 72 # noqa: PLR2004 - реальные ОСВ из fixtures
|
||
assert not any(path.startswith("currency_codes") for _, path, _ in entries)
|