test_object_store_algo_layout.py
python
sha256:b636f72dcba9e190afb980bece906fa5b717fbde014b76ef023df8cb96e01eb9
docs: expand cache plan with all seven testing tiers and do…
Sonnet 4.6
131 days ago
| 1 | """Tests: algorithm-directory layout for the object store. |
| 2 | |
| 3 | Canonical layout:: |
| 4 | |
| 5 | .muse/objects/sha256/<prefix>/<remainder> |
| 6 | |
| 7 | Also covers ``iter_stored_objects`` — the single canonical walker that |
| 8 | replaces the six inline ``iterdir`` loops scattered across gc, maintenance, |
| 9 | prune, count_objects, verify_object, and object_store itself. |
| 10 | """ |
| 11 | |
| 12 | from __future__ import annotations |
| 13 | |
| 14 | import pathlib |
| 15 | |
| 16 | import pytest |
| 17 | |
| 18 | from muse.core._types import blob_id, DEFAULT_HASH_ALGO, split_id |
| 19 | from muse.core.object_store import ( |
| 20 | _object_path_with_fallback, |
| 21 | cleanup_stale_object_temps, |
| 22 | has_object, |
| 23 | iter_stored_objects, |
| 24 | object_path, |
| 25 | objects_dir, |
| 26 | read_object, |
| 27 | write_object, |
| 28 | ) |
| 29 | |
| 30 | |
| 31 | # --------------------------------------------------------------------------- |
| 32 | # Helpers |
| 33 | # --------------------------------------------------------------------------- |
| 34 | |
| 35 | |
| 36 | def _repo(tmp_path: pathlib.Path) -> pathlib.Path: |
| 37 | (tmp_path / ".muse").mkdir() |
| 38 | return tmp_path |
| 39 | |
| 40 | |
| 41 | # --------------------------------------------------------------------------- |
| 42 | # 1. object_path — algo directory |
| 43 | # --------------------------------------------------------------------------- |
| 44 | |
| 45 | |
| 46 | class TestObjectPathAlgoDirectory: |
| 47 | """object_path must embed the algorithm as a directory component.""" |
| 48 | |
| 49 | def test_path_contains_sha256_directory(self, tmp_path: pathlib.Path) -> None: |
| 50 | """object_path returns .muse/objects/sha256/<prefix>/<rest>.""" |
| 51 | repo = _repo(tmp_path) |
| 52 | oid = blob_id(b"hello") |
| 53 | p = object_path(repo, oid) |
| 54 | # algo dir is the first component under objects/ |
| 55 | assert p.parent.parent.name == "sha256" |
| 56 | |
| 57 | def test_algo_directory_is_inside_objects(self, tmp_path: pathlib.Path) -> None: |
| 58 | """sha256/ sits directly under .muse/objects/.""" |
| 59 | repo = _repo(tmp_path) |
| 60 | oid = blob_id(b"world") |
| 61 | p = object_path(repo, oid) |
| 62 | assert p.parent.parent.parent == objects_dir(repo) |
| 63 | |
| 64 | def test_shard_prefix_still_correct(self, tmp_path: pathlib.Path) -> None: |
| 65 | """The 2-char shard prefix is the first 2 hex chars of the hash.""" |
| 66 | repo = _repo(tmp_path) |
| 67 | data = b"shard-check" |
| 68 | oid = blob_id(data) |
| 69 | p = object_path(repo, oid) |
| 70 | assert p.parent.name == oid[len("sha256:"):len("sha256:") + 2] |
| 71 | |
| 72 | def test_filename_is_remaining_hex(self, tmp_path: pathlib.Path) -> None: |
| 73 | """Object filename is the last 62 hex chars of the hash.""" |
| 74 | repo = _repo(tmp_path) |
| 75 | data = b"filename-check" |
| 76 | oid = blob_id(data) |
| 77 | p = object_path(repo, oid) |
| 78 | assert p.name == split_id(oid)[1][2:] |
| 79 | |
| 80 | def test_four_char_prefix_still_nested_under_sha256( |
| 81 | self, tmp_path: pathlib.Path |
| 82 | ) -> None: |
| 83 | """prefix_len=4 still places the shard under sha256/.""" |
| 84 | repo = _repo(tmp_path) |
| 85 | oid = blob_id(b"four-char") |
| 86 | p = object_path(repo, oid, prefix_len=4) |
| 87 | assert p.parent.parent.name == DEFAULT_HASH_ALGO |
| 88 | assert p.parent.name == split_id(oid)[1][:4] |
| 89 | assert p.name == split_id(oid)[1][4:] |
| 90 | |
| 91 | def test_write_object_lands_in_sha256_dir(self, tmp_path: pathlib.Path) -> None: |
| 92 | """write_object places the file under .muse/objects/sha256/.""" |
| 93 | repo = _repo(tmp_path) |
| 94 | data = b"write-check" |
| 95 | oid = blob_id(data) |
| 96 | write_object(repo, oid, data) |
| 97 | p = object_path(repo, oid) |
| 98 | assert p.exists() |
| 99 | assert p.parent.parent.name == "sha256" |
| 100 | |
| 101 | |
| 102 | # --------------------------------------------------------------------------- |
| 103 | # 2. iter_stored_objects — new layout |
| 104 | # --------------------------------------------------------------------------- |
| 105 | |
| 106 | |
| 107 | class TestIterStoredObjectsNewLayout: |
| 108 | """iter_stored_objects yields (prefixed_id, path) from the new layout.""" |
| 109 | |
| 110 | def test_empty_store_yields_nothing(self, tmp_path: pathlib.Path) -> None: |
| 111 | repo = _repo(tmp_path) |
| 112 | assert list(iter_stored_objects(repo)) == [] |
| 113 | |
| 114 | def test_yields_written_object(self, tmp_path: pathlib.Path) -> None: |
| 115 | repo = _repo(tmp_path) |
| 116 | data = b"single object" |
| 117 | oid = blob_id(data) |
| 118 | write_object(repo, oid, data) |
| 119 | results = list(iter_stored_objects(repo)) |
| 120 | assert len(results) == 1 |
| 121 | yielded_id, yielded_path = results[0] |
| 122 | assert yielded_id == oid |
| 123 | |
| 124 | def test_yielded_path_exists(self, tmp_path: pathlib.Path) -> None: |
| 125 | repo = _repo(tmp_path) |
| 126 | oid = blob_id(b"path-exists") |
| 127 | write_object(repo, oid, b"path-exists") |
| 128 | _, p = list(iter_stored_objects(repo))[0] |
| 129 | assert p.exists() |
| 130 | assert p.is_file() |
| 131 | |
| 132 | def test_yields_all_objects(self, tmp_path: pathlib.Path) -> None: |
| 133 | repo = _repo(tmp_path) |
| 134 | written = set() |
| 135 | for i in range(10): |
| 136 | data = f"obj-{i}".encode() |
| 137 | oid = blob_id(data) |
| 138 | write_object(repo, oid, data) |
| 139 | written.add(oid) |
| 140 | yielded = {oid for oid, _ in iter_stored_objects(repo)} |
| 141 | assert yielded == written |
| 142 | |
| 143 | def test_ids_are_sha256_prefixed(self, tmp_path: pathlib.Path) -> None: |
| 144 | """All yielded IDs carry the sha256: prefix.""" |
| 145 | repo = _repo(tmp_path) |
| 146 | write_object(repo, blob_id(b"prefix-check"), b"prefix-check") |
| 147 | for oid, _ in iter_stored_objects(repo): |
| 148 | assert oid.startswith("sha256:") |
| 149 | |
| 150 | def test_no_duplicates(self, tmp_path: pathlib.Path) -> None: |
| 151 | repo = _repo(tmp_path) |
| 152 | data = b"idempotent" |
| 153 | oid = blob_id(data) |
| 154 | write_object(repo, oid, data) |
| 155 | write_object(repo, oid, data) # second write is no-op |
| 156 | results = list(iter_stored_objects(repo)) |
| 157 | assert len(results) == 1 |
| 158 | |
| 159 | def test_skips_symlinks(self, tmp_path: pathlib.Path) -> None: |
| 160 | """Symlinked files inside shard dirs are not yielded.""" |
| 161 | repo = _repo(tmp_path) |
| 162 | oid = blob_id(b"real") |
| 163 | write_object(repo, oid, b"real") |
| 164 | p = object_path(repo, oid) |
| 165 | link = p.parent / ("symlink" + "a" * 60) |
| 166 | link.symlink_to(p) |
| 167 | results = list(iter_stored_objects(repo)) |
| 168 | ids = [r[0] for r in results] |
| 169 | assert len(ids) == 1 |
| 170 | assert oid in ids |
| 171 | |
| 172 | def test_skips_non_hex_filenames(self, tmp_path: pathlib.Path) -> None: |
| 173 | """Stray files (DS_Store, editor temps) with non-hex names are skipped.""" |
| 174 | repo = _repo(tmp_path) |
| 175 | oid = blob_id(b"real-obj") |
| 176 | write_object(repo, oid, b"real-obj") |
| 177 | p = object_path(repo, oid) |
| 178 | (p.parent / ".DS_Store").write_bytes(b"") |
| 179 | (p.parent / "editor.tmp").write_bytes(b"") |
| 180 | results = list(iter_stored_objects(repo)) |
| 181 | assert len(results) == 1 |
| 182 | |
| 183 | |
| 184 | # --------------------------------------------------------------------------- |
| 185 | # 3. _object_path_with_fallback — shard-prefix fallback only |
| 186 | # --------------------------------------------------------------------------- |
| 187 | |
| 188 | |
| 189 | class TestFallbackShardPrefix: |
| 190 | """_object_path_with_fallback handles the 2-char/4-char shard-prefix migration.""" |
| 191 | |
| 192 | def test_finds_canonical_layout(self, tmp_path: pathlib.Path) -> None: |
| 193 | repo = _repo(tmp_path) |
| 194 | data = b"canonical find" |
| 195 | oid = blob_id(data) |
| 196 | write_object(repo, oid, data) |
| 197 | p = _object_path_with_fallback(repo, oid) |
| 198 | assert p.exists() |
| 199 | assert p.parent.parent.name == "sha256" |
| 200 | |
| 201 | def test_has_object_canonical(self, tmp_path: pathlib.Path) -> None: |
| 202 | repo = _repo(tmp_path) |
| 203 | data = b"has-object canonical" |
| 204 | oid = blob_id(data) |
| 205 | write_object(repo, oid, data) |
| 206 | assert has_object(repo, oid) |
| 207 | |
| 208 | def test_read_object_canonical(self, tmp_path: pathlib.Path) -> None: |
| 209 | repo = _repo(tmp_path) |
| 210 | data = b"read-object canonical" |
| 211 | oid = blob_id(data) |
| 212 | write_object(repo, oid, data) |
| 213 | assert read_object(repo, oid) == data |
| 214 | |
| 215 | |
| 216 | # --------------------------------------------------------------------------- |
| 217 | # 4. cleanup_stale_object_temps |
| 218 | # --------------------------------------------------------------------------- |
| 219 | |
| 220 | |
| 221 | class TestCleanupStaleTempNewLayout: |
| 222 | """cleanup_stale_object_temps handles the algo-directory structure.""" |
| 223 | |
| 224 | def test_cleanup_finds_temps_in_sha256_shards( |
| 225 | self, tmp_path: pathlib.Path |
| 226 | ) -> None: |
| 227 | """Stale .obj-tmp-* files inside sha256/<shard>/ are cleaned up.""" |
| 228 | import time |
| 229 | repo = _repo(tmp_path) |
| 230 | # Create a temp file in the new layout shard directory. |
| 231 | shard = objects_dir(repo) / "sha256" / "ab" |
| 232 | shard.mkdir(parents=True) |
| 233 | stale = shard / ".obj-tmp-stale" |
| 234 | stale.write_bytes(b"stale") |
| 235 | # Back-date so it exceeds the min-age threshold. |
| 236 | old_time = time.time() - 120 |
| 237 | os.utime(stale, (old_time, old_time)) |
| 238 | removed = cleanup_stale_object_temps(repo) |
| 239 | assert removed >= 1 |
| 240 | assert not stale.exists() |
| 241 | |
| 242 | |
| 243 | import os # noqa: E402 — needed by the last test |
File History
2 commits
sha256:b636f72dcba9e190afb980bece906fa5b717fbde014b76ef023df8cb96e01eb9
docs: expand cache plan with all seven testing tiers and do…
Sonnet 4.6
131 days ago
sha256:7f9e2ef5286aedad9c1e6011b4c46ca27f39dbdad6e3409357e36b26e46b3b7c
docs: docstring sprint for-each-ref→hotspots — idiomatic ru…
Sonnet 4.6
patch
138 days ago