arm-tabular-parser/api/tests/rules_coverage.py
Andrey Epifancev be5ad8ce86 refactor: удалён собственный конвейер форм и словари форм — шаги 2–8 в arm_statements (#54)
Удалены модули statements/{checks,columns,detect,header,lines,merge,statement,legacy_models}.py, словари
rules/statement_{headers,periods,checks}.yaml и их тесты. Словари форм и шаги 2–8 — в пакете arm_statements.
Rules без полей statement_*; покрытие словарей ОСВ (DICTIONARIES) без словарей форм. Документация сервиса
переписана под адаптер форм, контракт 2.0 и коды OSV_*.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BJKMLHdxMwSUeYcdkwqX4W
2026-10-18 21:53:29 +04:00

82 lines
3.5 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.

"""Помощники теста покрытия словарей (R-22): обход записей `Rules` и счётчик срабатываний `Pattern`.
Вынесены из `tests/fixtures/test_rules_coverage.py`, чтобы сам механизм проверялся без каталога fixtures
(`tests/unit/test_rules_coverage_helpers.py`).
"""
import re
from collections import Counter
from collections.abc import Iterator
from typing import Any
import pytest
from pydantic import BaseModel
from src.parser.rules import Pattern, Rules
DICTIONARIES = {
"headers": "osv_headers.yaml",
"groups": "osv_groups.yaml",
"checks": "osv_checks.yaml",
"periods": "periods.yaml",
"data_types": "data_types.yaml",
}
# Словарь без записей распознавания: список счетов проверок ОСВ — коды счетов, а не `Pattern` (в покрытии не участвует).
# Словари форм ББ/ОФР живут в пакете `arm_statements` и его покрытие не проверяет этот тест.
PATTERNLESS = frozenset({"osv_checks.yaml"})
def iter_patterns(rules: Rules) -> Iterator[tuple[str, str, Pattern]]:
"""(файл словаря, путь ключа, Pattern) для каждой записи распознавания; порядок — как в моделях."""
for attribute, file_name in DICTIONARIES.items():
yield from _walk(file_name, attribute_value=getattr(rules, attribute), path="")
def _walk(file_name: str, attribute_value: Any, path: str) -> Iterator[tuple[str, str, Pattern]]:
if isinstance(attribute_value, Pattern):
yield file_name, path, attribute_value
elif isinstance(attribute_value, BaseModel):
for name in type(attribute_value).model_fields:
yield from _walk(file_name, getattr(attribute_value, name), f"{path}.{name}" if path else name)
elif isinstance(attribute_value, dict):
for key, inner in attribute_value.items():
yield from _walk(file_name, inner, f"{path}.{key}")
elif isinstance(attribute_value, list | tuple):
for index, inner in enumerate(attribute_value):
yield from _walk(file_name, inner, f"{path}[{index}]")
class MatchCounter:
"""Считает совпадения `Pattern.search`/`search_normalized` по идентичности объекта паттерна."""
def __init__(self) -> None:
self.hits: Counter[int] = Counter()
def install(self, monkeypatch: pytest.MonkeyPatch) -> None:
original_search = Pattern.search
original_normalized = Pattern.search_normalized
counter = self
def search(pattern: Pattern, text: str) -> re.Match[str] | None:
found = original_search(pattern, text)
if found is not None:
counter.hits[id(pattern)] += 1
return found
def search_normalized(pattern: Pattern, normalized: str) -> re.Match[str] | None:
found = original_normalized(pattern, normalized)
if found is not None:
counter.hits[id(pattern)] += 1
return found
monkeypatch.setattr(Pattern, "search", search)
monkeypatch.setattr(Pattern, "search_normalized", search_normalized)
def uncovered(rules: Rules, counter: MatchCounter) -> list[str]:
return [
f"{file_name}: {path}: {pattern.source!r}"
for file_name, path, pattern in iter_patterns(rules)
if counter.hits[id(pattern)] == 0
]