arm-tabular-parser/api/tests/unit/test_golden_sync.py
Andrey Epifancev 9b8b8ea727 fix: финальное ревью — лимиты чтения xlsx (I-1), лог traceback (I-2), ACCOUNT_RE (M-1), ИНН/КПП (M-3)
- I-1 (R-23): readers/_limits.py — сумма ZipInfo.file_size ≤ 10 × PARSE_MAX_BYTES (параметр read_book от
  OsvParser), потоковый скан <mergeCell ref> до openpyxl и площадь merged после чтения любого формата
  (MERGED_AREA_LIMIT = 200 000; реальный максимум 10 090) → UNSUPPORTED_FORMAT; дедлайн в циклах detect
  (поиск строки групп, дополнительные колонки); README «Лимиты» исправлен, «Известные ограничения» (defusedxml).
- I-2: OsvParser логирует стек непредвиденной ошибки (loguru ERROR), UNREADABLE — WARNING без стека.
- M-1: ACCOUNT_RE с re.ASCII | re.DOTALL (многострочное название после запятой, только цифры 0–9).
- M-3 (R-24): правило детектора ИНН не дополняет 9/11 цифр нулём; дополнение — только при извлечении
  (numbers.inn_digits) из колонки, выбранной по заголовку или по строгому правилу.
- T9.1: тесты R-21 (свёрнутый родитель на противоположной стороне; (0,0) при детях (X, X)).
- T1.1: хелперы фикстур пропускают скрытые файлы/каталоги и lock-файлы Excel (~$…).
- T12.3: golden-update не удаляет снимки части без исходного каталога (osv/synthetic или реальные файлы).

Golden-снимки не изменились (82/82).

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

180 lines
8.0 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.

"""`sync_snapshots` и `scripts/golden_update.py`: запись, сравнение, diff, лишние снимки (на временном каталоге)."""
import shutil
import sys
from pathlib import Path
import pytest
from golden_helpers import check_snapshot, golden_path, render, snapshot_text, sync_snapshots
from src.parser import get_parser
from tests.synthetic.generate import generate_all
FILES = 2 # s01 и s02
API_ROOT = Path(__file__).resolve().parents[2]
@pytest.fixture(scope="module")
def fixtures_root(tmp_path_factory: pytest.TempPathFactory) -> Path:
root = tmp_path_factory.mktemp("fixtures")
generate_all(root / "osv" / "synthetic")
return root
@pytest.fixture
def root(fixtures_root: Path, tmp_path: Path) -> Path:
"""Копия дерева `osv/` на каждый тест: снимки не пересекаются между тестами."""
copy = tmp_path / "fx"
(copy / "osv" / "synthetic").mkdir(parents=True)
for source in sorted((fixtures_root / "osv" / "synthetic").glob("s0[12]*")):
(copy / "osv" / "synthetic" / source.name).write_bytes(source.read_bytes())
return copy
def files_of(root: Path) -> list[Path]:
return sorted((root / "osv" / "synthetic").glob("s*"))
def test_first_run_creates_snapshots_equal_to_render(root: Path) -> None:
parser = get_parser()
report = sync_snapshots(root, files_of(root), parser, write=True)
assert len(report.created) == FILES
assert report.changed == [] and report.orphans == [] and report.unchanged == 0
for file in files_of(root):
assert golden_path(root, file).read_text(encoding="utf-8") == snapshot_text(render(parser, file))
assert (root / "golden" / "expected_osv_parse" / "synthetic" / "s01_variant2_header.json").is_file()
def test_second_run_reports_nothing_and_check_does_not_write(root: Path) -> None:
parser = get_parser()
sync_snapshots(root, files_of(root), parser, write=True)
again = sync_snapshots(root, files_of(root), parser, write=False)
assert again.unchanged == FILES
assert not again.differs
assert again.diffs == []
def test_changed_snapshot_gives_diff_and_check_leaves_it_untouched(root: Path) -> None:
parser = get_parser()
sync_snapshots(root, files_of(root), parser, write=True)
target = golden_path(root, files_of(root)[0])
original = target.read_text(encoding="utf-8")
target.write_text(original.replace('"done"', '"needs_check"', 1), encoding="utf-8")
checked = sync_snapshots(root, files_of(root), parser, write=False)
assert checked.changed == [target.relative_to(root)]
assert '- "status": "needs_check"' in checked.diffs[0]
assert '+ "status": "done"' in checked.diffs[0]
assert target.read_text(encoding="utf-8") != original
written = sync_snapshots(root, files_of(root), parser, write=True)
assert written.changed == [target.relative_to(root)]
assert target.read_text(encoding="utf-8") == original
def test_orphan_snapshot_is_reported_and_removed_on_write(root: Path) -> None:
parser = get_parser()
sync_snapshots(root, files_of(root), parser, write=True)
orphan = root / "golden" / "expected_osv_parse" / "synthetic" / "s99_gone.json"
orphan.write_text("{}\n", encoding="utf-8")
checked = sync_snapshots(root, files_of(root), parser, write=False)
assert checked.orphans == [orphan.relative_to(root)]
assert orphan.exists()
sync_snapshots(root, files_of(root), parser, write=True)
assert not orphan.exists()
def test_snapshots_are_kept_when_their_source_directory_is_absent(root: Path) -> None:
"""T12.3: без `osv/synthetic` (или без реальных файлов) снимки этой части — не «лишние»: запись их не удаляет,
сверка о них не сообщает как о расхождении, но перечисляет как сохранённые."""
parser = get_parser()
sync_snapshots(root, files_of(root), parser, write=True)
synthetic_snapshots = sorted((root / "golden" / "expected_osv_parse" / "synthetic").glob("*.json"))
real_snapshot = root / "golden" / "expected_osv_parse" / "rb" / "2024-Q1" / "сч 60.json"
real_snapshot.parent.mkdir(parents=True)
real_snapshot.write_text("{}\n", encoding="utf-8")
shutil.rmtree(root / "osv" / "synthetic")
report = sync_snapshots(root, [], parser, write=True)
assert report.orphans == []
assert not report.differs
assert report.protected == sorted(
[*(path.relative_to(root) for path in synthetic_snapshots), real_snapshot.relative_to(root)]
)
assert all(path.is_file() for path in synthetic_snapshots)
assert real_snapshot.is_file()
def test_orphan_is_removed_only_within_a_present_source_directory(root: Path) -> None:
"""Реальных файлов нет, синтетика есть: лишний синтетический снимок удаляется, реальные снимки сохраняются."""
parser = get_parser()
real_snapshot = root / "golden" / "expected_osv_parse" / "rb" / "сч 60.json"
real_snapshot.parent.mkdir(parents=True)
real_snapshot.write_text("{}\n", encoding="utf-8")
orphan = root / "golden" / "expected_osv_parse" / "synthetic" / "s99_gone.json"
orphan.parent.mkdir(parents=True)
orphan.write_text("{}\n", encoding="utf-8")
report = sync_snapshots(root, files_of(root), parser, write=True)
assert report.orphans == [orphan.relative_to(root)]
assert report.protected == [real_snapshot.relative_to(root)]
assert not orphan.exists()
assert real_snapshot.is_file()
def test_golden_test_fails_with_hint_when_snapshot_is_missing(root: Path) -> None:
"""Spec «Новый файл без снимка»: golden-тест сообщает «нет снимка: выполните make golden-update»."""
file = files_of(root)[0]
with pytest.raises(pytest.fail.Exception, match=r"нет снимка: выполните make golden-update"):
check_snapshot(root, file, get_parser())
def test_golden_test_fails_with_diff_when_parse_result_changed(root: Path) -> None:
"""Spec «Регрессия»: расхождение разбора со снимком — падение с diff по этому файлу."""
parser = get_parser()
sync_snapshots(root, files_of(root), parser, write=True)
file = files_of(root)[0]
check_snapshot(root, file, parser) # свежий снимок совпадает с разбором
target = golden_path(root, file)
target.write_text(target.read_text(encoding="utf-8").replace('"level": 1', '"level": 7', 1), encoding="utf-8")
with pytest.raises(pytest.fail.Exception, match=r"отличается от снимка") as failure:
check_snapshot(root, file, parser)
diff_lines = str(failure.value).splitlines()
assert any(line.startswith("-") and '"level": 7' in line for line in diff_lines)
assert any(line.startswith("+") and '"level": 1' in line for line in diff_lines)
def test_script_check_exit_codes(
root: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
) -> None:
monkeypatch.setenv("ARM_KA_FIXTURES", str(root))
monkeypatch.syspath_prepend(str(API_ROOT / "scripts"))
sys.modules.pop("golden_update", None)
import golden_update # noqa: PLC0415 - модуль-скрипт
assert golden_update.main(["--check"]) == 1
assert golden_update.main([]) == 0
assert golden_update.main(["--check"]) == 0
shutil.rmtree(root / "osv" / "synthetic")
capsys.readouterr()
assert golden_update.main([]) == 0
assert "снимок сохранён (нет исходного каталога)" in capsys.readouterr().err
assert (root / "golden" / "expected_osv_parse" / "synthetic" / "s01_variant2_header.json").is_file()
monkeypatch.setenv("ARM_KA_FIXTURES", str(root / "нет"))
capsys.readouterr()
assert golden_update.main([]) == golden_update.EXIT_NO_FIXTURES
assert "каталог фикстур не найден" in capsys.readouterr().err