"""`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