"""Tests for the local filesystem storage adapter.""" import pytest from snapshot.storage import LocalFileStorage, create_storage, validate_key from snapshot.storage.base import StorageAdapter def _contract(adapter: StorageAdapter) -> None: """Run the shared storage contract against an adapter instance.""" adapter.put("a/b/file1.txt", b"hello") adapter.put("a/b/file2.txt", b"world") adapter.put("c/file3.txt", b"!") assert adapter.get("a/b/file1.txt") == b"hello" assert adapter.exists("a/b/file1.txt") assert not adapter.exists("a/b/missing.txt") with pytest.raises(KeyError): adapter.get("a/b/missing.txt") assert adapter.list_keys() == [ "a/b/file1.txt", "a/b/file2.txt", "c/file3.txt", ] assert adapter.list_keys("a/b") == [ "a/b/file1.txt", "a/b/file2.txt", ] adapter.delete("a/b/file1.txt") adapter.delete("a/b/file1.txt") # idempotent assert not adapter.exists("a/b/file1.txt") assert adapter.list_keys() == [ "a/b/file2.txt", "c/file3.txt", ] def test_contract(tmp_path): """Validate the adapter contract on a temporary directory.""" adapter = LocalFileStorage(tmp_path) _contract(adapter) def test_put_creates_parent_directories(tmp_path): """put() must create intermediate directories.""" adapter = LocalFileStorage(tmp_path) adapter.put("deep/nested/key.bin", b"data") assert (tmp_path / "deep" / "nested" / "key.bin").read_bytes() == b"data" def test_atomic_put_leaves_no_tmp_files(tmp_path): """Atomic writes must not leak temporary files.""" adapter = LocalFileStorage(tmp_path) adapter.put("x/y/z.txt", b"payload") tmp_files = list(tmp_path.rglob("*.tmp-*")) assert tmp_files == [] def test_validate_key_rejects_bad_keys(): """validate_key must reject unsafe key strings.""" with pytest.raises(ValueError): validate_key("") with pytest.raises(ValueError): validate_key("/absolute/path") with pytest.raises(ValueError): validate_key("../escape") with pytest.raises(ValueError): validate_key("a\\b") def test_get_missing_raises_key_error(tmp_path): """Reading a missing key must raise KeyError.""" adapter = LocalFileStorage(tmp_path) with pytest.raises(KeyError): adapter.get("missing.bin") def test_create_storage_custom_local_root(tmp_path): """create_storage must honor an explicit local_root directory.""" custom_dir = tmp_path / "custom_versions" adapter = create_storage( "local", root_dir=tmp_path, local_root=str(custom_dir), ) adapter.put("notebook.ipynb", b"data") assert (custom_dir / "notebook.ipynb").read_bytes() == b"data" def test_create_storage_default_local_root(tmp_path): """create_storage must fall back to root_dir/.notebook_versions.""" adapter = create_storage("local", root_dir=tmp_path) adapter.put("notebook.ipynb", b"data") expected = tmp_path / ".notebook_versions" / "notebook.ipynb" assert expected.read_bytes() == b"data"