arm-tabular-parser/api/tests/unit/test_golden_sync.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

205 lines
9.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.

"""`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_statement_snapshots_are_kept_without_statement_sources_and_removed_with_them(root: Path) -> None:
"""Снимки форм (`expected_statement_parse`) лишние только там, где есть исходные файлы форм своей части."""
parser = get_parser()
kept = root / "golden" / "expected_statement_parse" / "npz" / "2025-Q3" / "книга.json"
kept.parent.mkdir(parents=True)
kept.write_text("{}\n", encoding="utf-8")
report = sync_snapshots(root, files_of(root), parser, write=True)
assert report.orphans == []
assert report.protected == [kept.relative_to(root)]
assert kept.is_file()
form = root / "statements-xlsx" / "templates" / "ББ.xlsx"
form.parent.mkdir(parents=True)
form.write_bytes(files_of(root)[0].read_bytes())
report = sync_snapshots(root, [*files_of(root), form], parser, write=False)
assert report.orphans == [kept.relative_to(root)]
assert report.protected == []
assert [path for path in report.created if "expected_statement_parse" in path.parts] == [
golden_path(root, form).relative_to(root)
]
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