arm-tabular-parser/api/tests/rules_coverage.py
Andrey Epifancev 07d57cbd07 test(statements): манифест реальных форм, property-тест, golden форм, покрытие словарей форм
Манифест statements_manifest.yaml по 18 реальным файлам statements-xlsx (сверен
прогоном парсера, R-13, OQ-137), property-тест по манифесту, снимки форм в
golden_helpers/golden_update/test_golden, словари форм в покрытии, синтетика
st06 для вариантов заголовков. Мёртвые записи словаря удалены (name_caption,
метки okved и okud_label) либо покрыты st06.

Refs #14

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: cedd7915-2efe-41e5-988c-a9117033598b
2026-10-06 20:50:25 +04:00

83 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",
"periods": "periods.yaml",
"data_types": "data_types.yaml",
"statement_headers": "statement_headers.yaml",
"statement_periods": "statement_periods.yaml",
"statement_checks": "statement_checks.yaml",
}
# Словари без записей распознавания: соотношения §7 — выражения над кодами, а не `Pattern` (в покрытии не участвуют)
PATTERNLESS = frozenset({"statement_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
]