arm-tabular-parser/api/tests/fixtures/test_rules_coverage.py
Andrey Epifancev 1abe212524 fix(statements): финальная волна ревью — I-1 (O-9), M-10, I-2, I-3, M-5, M-8, M-11, T13.5, M-6, M-7, T15.1, T15.2
I-1: лист с 1–2 кодами после листа формы — продолжение (min_codes_continuation в словаре).
M-10: тип формы маркера продолжения запрещает присоединение к документу иного типа.
I-2: одиночный месяц не задаёт границы при дате на число/квартале (not_single_month).
I-3: название строки из ближайшей текстовой ячейки левее кодов, пустое имя без confidence.
M-5: units_thousands и okud_code в словаре, без type: ignore в detect.py.
M-8: строка нумерации и нули не считаются строкой без кода с суммами.
M-11: предупреждение о дробных значениях — одно на документ.
T13.5: _upper_header_row возвращает верхнюю строку при склейке хотя бы одной колонки.
M-6/M-7/T15.1/T15.2: остатки statement_markers, описание fractional_share_percent, README, тест воркера.

Refs #14

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EAKRDNzXtRrwmWz9z6PZu3
2026-10-06 21:58:24 +04:00

88 lines
4.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Покрытие словарей `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 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.statement import StatementParser
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)
parser = BookParser(
rules,
osv=OsvParser(rules),
statements=StatementParser(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)