gabriel / muse public
test_object_store_algo_layout.py python
243 lines 8.7 KB
Raw
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