- golden_helpers, scripts/golden_update.py (--check), test_golden на 66 ОСВ и синтетике; make golden-update/golden-check/test-fixtures - тест покрытия словарей (R-22): счётчик срабатываний Pattern; из osv_headers.yaml удалены неиспользуемые service_columns kpp/code/quantity/currency - синтетика s14–s16 в генераторе и таблице ожиданий; skip вместо падения при отсутствии osv/synthetic (T11.1); комментарий R-21 в s07 (T11.2) - линтер: предел адреса 10 вместо 4 (T3.1), обращение cell(3, 4) (T3.8); реальные файлы не фиксируют BU/RUB (T3.9) - README: golden-процесс; отмечены Task 12 и пункты 4.2, 4.3 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
78 lines
3.1 KiB
Python
78 lines
3.1 KiB
Python
"""Помощники теста покрытия словарей (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",
|
|
}
|
|
|
|
|
|
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
|
|
]
|