- 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
180 lines
8.0 KiB
Python
180 lines
8.0 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_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
|