arm-tabular-parser/api/tests/unit/test_golden_sync.py
Andrey Epifancev 7738221b5a docs: README/CLAUDE.md Среза 2a, tasks.md, спека-дельты
- README: парсер по модулям, статусы и коды, словари и добавление синонима, лимиты, golden-процесс, настройки PARSE_*
- CLAUDE.md: раздел «Добавлено в Срезе 2a» (модули, правила расширения, линтер, R-21); api/rules/README.md: ключи словарей
- openspec: tasks.md (5.1), design.md (D-1…D-7, O-7/O-8), спека-дельты osv-parsing (R-12…R-22) и tabular-recognition-service («Результат по контракту»)
- tests: check_snapshot в golden_helpers + тесты «нет снимка»/«регрессия»; locator в property-тесте; пример needs_check синхронизирован с docs

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

134 lines
5.6 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 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_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
monkeypatch.setenv("ARM_KA_FIXTURES", str(root / "нет"))
capsys.readouterr()
assert golden_update.main([]) == golden_update.EXIT_NO_FIXTURES
assert "каталог фикстур не найден" in capsys.readouterr().err