Манифест 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
205 lines
9.1 KiB
Python
205 lines
9.1 KiB
Python
"""`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
|