gabriel / muse public
test_plumbing_stress.py python
449 lines 15.8 KB
Raw
sha256:660fcac1df3ab28f61862e961890bd2ca8b754fa0242079d93ca1e25037ec8a6 chore(tests): add docstring to tests/__init__.py so rc14 tr… Human 26 days ago
1 """Stress and scale tests for the Muse plumbing layer.
2
3 These tests exercise plumbing commands at a scale that would reveal
4 O(n²) performance regressions, memory leaks, and missing edge-case
5 handling. Every test in this module is designed to complete in under
6 10 seconds on a modern laptop when running from an in-memory temp
7 directory — if any test consistently takes longer, it signals a
8 performance regression worth investigating.
9
10 Scenarios:
11 - commit-graph BFS on a 500-commit linear history
12 - merge-base on a 300-deep dag (shared ancestor at the root)
13 - name-rev multi-source BFS on a 200-commit diamond graph
14 - snapshot-diff on manifests with 2000 files each
15 - verify-object on 200 objects
16 - ls-files on a 2000-file snapshot
17 - for-each-ref on 100 branches
18 - show-ref on 100 branches
19 - pack-objects → unpack-objects with 100 commits and 100 objects
20 - read-commit on 200 sequential commits
21 """
22
23 from __future__ import annotations
24
25 import datetime
26 import hashlib
27 import json
28 import pathlib
29
30 from tests.cli_test_helper import CliRunner
31
32 cli = None # argparse migration — CliRunner ignores this arg
33 from muse.core.object_store import write_object
34 from muse.core.snapshot import compute_commit_id, compute_snapshot_id
35 from muse.core.store import CommitRecord, SnapshotRecord, write_commit, write_snapshot
36
37 runner = CliRunner()
38
39
40 # ---------------------------------------------------------------------------
41 # Helpers
42 # ---------------------------------------------------------------------------
43
44
45 def _sha(tag: str) -> str:
46 return hashlib.sha256(tag.encode()).hexdigest()
47
48
49 def _sha_bytes(data: bytes) -> str:
50 return hashlib.sha256(data).hexdigest()
51
52
53 def _init_repo(path: pathlib.Path) -> pathlib.Path:
54 muse = path / ".muse"
55 (muse / "commits").mkdir(parents=True)
56 (muse / "snapshots").mkdir(parents=True)
57 (muse / "objects").mkdir(parents=True)
58 (muse / "refs" / "heads").mkdir(parents=True)
59 (muse / "HEAD").write_text("ref: refs/heads/main", encoding="utf-8")
60 (muse / "repo.json").write_text(
61 json.dumps({"repo_id": "stress-repo", "domain": "midi"}), encoding="utf-8"
62 )
63 return path
64
65
66 def _env(repo: pathlib.Path) -> Manifest:
67 return {"MUSE_REPO_ROOT": str(repo)}
68
69
70 def _snap(repo: pathlib.Path, manifest: Manifest | None = None, tag: str = "s") -> str:
71 """Write a snapshot with a real content-addressed ID and return it."""
72 m = manifest or {}
73 sid = compute_snapshot_id(m)
74 write_snapshot(
75 repo,
76 SnapshotRecord(
77 snapshot_id=sid,
78 manifest=m,
79 created_at=datetime.datetime(2026, 1, 1, tzinfo=datetime.timezone.utc),
80 ),
81 )
82 return sid
83
84
85 def _commit_raw(
86 repo: pathlib.Path,
87 cid: str,
88 sid: str,
89 message: str,
90 branch: str = "main",
91 parent: str | None = None,
92 parent2: str | None = None,
93 ) -> str:
94 """Write a commit with a real content-addressed ID and return it.
95
96 The *cid* parameter is ignored — the real commit ID is derived from
97 *sid*, *message*, *committed_at*, and the parent IDs using the same
98 algorithm that :func:`muse.core.snapshot.compute_commit_id` uses.
99 """
100 committed_at = datetime.datetime(2026, 1, 1, tzinfo=datetime.timezone.utc)
101 parent_ids = [p for p in (parent, parent2) if p is not None]
102 real_cid = compute_commit_id(parent_ids, sid, message, committed_at.isoformat())
103 write_commit(
104 repo,
105 CommitRecord(
106 commit_id=real_cid,
107 repo_id="stress-repo",
108 branch=branch,
109 snapshot_id=sid,
110 message=message,
111 committed_at=committed_at,
112 author="stress-tester",
113 parent_commit_id=parent,
114 parent2_commit_id=parent2,
115 ),
116 )
117 return real_cid
118
119
120 def _set_branch(repo: pathlib.Path, branch: str, cid: str) -> None:
121 ref = repo / ".muse" / "refs" / "heads" / branch
122 ref.parent.mkdir(parents=True, exist_ok=True)
123 ref.write_text(cid, encoding="utf-8")
124
125
126 def _linear_chain(repo: pathlib.Path, n: int, sid: str, branch: str = "main") -> list[str]:
127 """Build a linear chain of n commits. Returns real commit IDs root→tip."""
128 cids: list[str] = []
129 parent: str | None = None
130 for i in range(n):
131 real_cid = _commit_raw(repo, "", sid, f"commit {i}", branch=branch, parent=parent)
132 cids.append(real_cid)
133 parent = real_cid
134 _set_branch(repo, branch, cids[-1])
135 return cids
136
137
138 def _obj(repo: pathlib.Path, tag: str) -> str:
139 content = tag.encode()
140 oid = _sha_bytes(content)
141 write_object(repo, oid, content)
142 return oid
143
144
145 # ---------------------------------------------------------------------------
146 # Stress: commit-graph
147 # ---------------------------------------------------------------------------
148
149
150 class TestCommitGraphStress:
151 def test_500_commit_linear_chain_full_traversal(self, tmp_path: pathlib.Path) -> None:
152 repo = _init_repo(tmp_path)
153 sid = _snap(repo)
154 cids = _linear_chain(repo, 500, sid)
155 result = runner.invoke(cli, ["commit-graph"], env=_env(repo))
156 assert result.exit_code == 0, result.output
157 data = json.loads(result.stdout)
158 assert data["count"] == 500
159 assert data["truncated"] is False
160
161 def test_500_commit_chain_stop_at_midpoint(self, tmp_path: pathlib.Path) -> None:
162 repo = _init_repo(tmp_path)
163 sid = _snap(repo)
164 cids = _linear_chain(repo, 500, sid)
165 result = runner.invoke(
166 cli,
167 ["commit-graph", "--tip", cids[499], "--stop-at", cids[249]],
168 env=_env(repo),
169 )
170 assert result.exit_code == 0
171 data = json.loads(result.stdout)
172 assert data["count"] == 250
173
174 def test_count_flag_on_500_commits(self, tmp_path: pathlib.Path) -> None:
175 repo = _init_repo(tmp_path)
176 sid = _snap(repo)
177 _linear_chain(repo, 500, sid)
178 result = runner.invoke(cli, ["commit-graph", "--count"], env=_env(repo))
179 assert result.exit_code == 0
180 data = json.loads(result.stdout)
181 assert data["count"] == 500
182 assert "commits" not in data # --count suppresses node list
183
184
185 # ---------------------------------------------------------------------------
186 # Stress: merge-base
187 # ---------------------------------------------------------------------------
188
189
190 class TestMergeBaseStress:
191 def test_merge_base_300_deep_shared_root(self, tmp_path: pathlib.Path) -> None:
192 repo = _init_repo(tmp_path)
193 sid = _snap(repo)
194
195 # Shared root
196 root_cid = _commit_raw(repo, "", sid, "root")
197
198 # Two 150-commit chains from the same root
199 main_chain = [root_cid]
200 feat_chain = [root_cid]
201 for i in range(150):
202 mc = _commit_raw(repo, "", sid, f"main-{i}", branch="main", parent=main_chain[-1])
203 main_chain.append(mc)
204 fc = _commit_raw(repo, "", sid, f"feat-{i}", branch="feat", parent=feat_chain[-1])
205 feat_chain.append(fc)
206
207 _set_branch(repo, "main", main_chain[-1])
208 _set_branch(repo, "feat", feat_chain[-1])
209 (repo / ".muse" / "HEAD").write_text("ref: refs/heads/main", encoding="utf-8")
210
211 result = runner.invoke(
212 cli, ["merge-base", "main", "feat"], env=_env(repo)
213 )
214 assert result.exit_code == 0
215 data = json.loads(result.stdout)
216 assert data["merge_base"] == root_cid
217
218
219 # ---------------------------------------------------------------------------
220 # Stress: name-rev
221 # ---------------------------------------------------------------------------
222
223
224 class TestNameRevStress:
225 def test_name_rev_200_commit_chain_all_named(self, tmp_path: pathlib.Path) -> None:
226 repo = _init_repo(tmp_path)
227 sid = _snap(repo)
228 cids = _linear_chain(repo, 200, sid)
229
230 result = runner.invoke(cli, ["name-rev", *cids], env=_env(repo))
231 assert result.exit_code == 0
232 data = json.loads(result.stdout)
233 assert len(data["results"]) == 200
234 for entry in data["results"]:
235 assert not entry["undefined"]
236
237 def test_name_rev_tip_has_no_tilde_suffix(self, tmp_path: pathlib.Path) -> None:
238 """distance=0 means the tip is the branch tip itself; name is bare branch name."""
239 repo = _init_repo(tmp_path)
240 sid = _snap(repo)
241 cids = _linear_chain(repo, 10, sid)
242 tip = cids[-1]
243
244 result = runner.invoke(cli, ["name-rev", tip], env=_env(repo))
245 assert result.exit_code == 0
246 entry = json.loads(result.stdout)["results"][0]
247 # name-rev emits "<branch>" (no ~0) for the exact branch tip.
248 assert entry["name"] == "main"
249 assert entry["distance"] == 0
250
251
252 # ---------------------------------------------------------------------------
253 # Stress: snapshot-diff
254 # ---------------------------------------------------------------------------
255
256
257 class TestSnapshotDiffStress:
258 def test_diff_2000_file_manifests(self, tmp_path: pathlib.Path) -> None:
259 repo = _init_repo(tmp_path)
260 oid = _sha("shared-blob")
261
262 # Manifest A: 2000 files
263 manifest_a = {f"track_{i:04d}.mid": oid for i in range(2000)}
264 # Manifest B: same 2000 files but first 200 have new IDs (modified)
265 new_oid = _sha("new-blob")
266 manifest_b = {f"track_{i:04d}.mid": (new_oid if i < 200 else oid) for i in range(2000)}
267
268 sid_a = compute_snapshot_id(manifest_a)
269 sid_b = compute_snapshot_id(manifest_b)
270 write_snapshot(
271 repo,
272 SnapshotRecord(
273 snapshot_id=sid_a,
274 manifest=manifest_a,
275 created_at=datetime.datetime(2026, 1, 1, tzinfo=datetime.timezone.utc),
276 ),
277 )
278 write_snapshot(
279 repo,
280 SnapshotRecord(
281 snapshot_id=sid_b,
282 manifest=manifest_b,
283 created_at=datetime.datetime(2026, 1, 2, tzinfo=datetime.timezone.utc),
284 ),
285 )
286
287 result = runner.invoke(cli, ["snapshot-diff", sid_a, sid_b], env=_env(repo))
288 assert result.exit_code == 0
289 data = json.loads(result.stdout)
290 assert data["total_changes"] == 200
291 assert len(data["modified"]) == 200
292 assert data["added"] == []
293 assert data["deleted"] == []
294
295
296 # ---------------------------------------------------------------------------
297 # Stress: verify-object
298 # ---------------------------------------------------------------------------
299
300
301 class TestVerifyObjectStress:
302 def test_200_objects_all_verified(self, tmp_path: pathlib.Path) -> None:
303 repo = _init_repo(tmp_path)
304 oids = [_obj(repo, f"stress-obj-{i}") for i in range(200)]
305 result = runner.invoke(cli, ["verify-object", *oids], env=_env(repo))
306 assert result.exit_code == 0
307 data = json.loads(result.stdout)
308 assert data["all_ok"] is True
309 assert data["checked"] == 200
310 assert data["failed"] == 0
311
312 def test_verify_1mib_object_no_crash(self, tmp_path: pathlib.Path) -> None:
313 repo = _init_repo(tmp_path)
314 content = b"Z" * (1024 * 1024)
315 oid = _sha_bytes(content)
316 write_object(repo, oid, content)
317 result = runner.invoke(cli, ["verify-object", oid], env=_env(repo))
318 assert result.exit_code == 0
319 assert json.loads(result.stdout)["all_ok"] is True
320
321
322 # ---------------------------------------------------------------------------
323 # Stress: ls-files
324 # ---------------------------------------------------------------------------
325
326
327 class TestLsFilesStress:
328 def test_ls_files_2000_file_snapshot(self, tmp_path: pathlib.Path) -> None:
329 repo = _init_repo(tmp_path)
330 oid = _sha("common-oid")
331 manifest = {f"track_{i:04d}.mid": oid for i in range(2000)}
332 sid = _snap(repo, manifest, "big")
333 cid = _commit_raw(repo, "", sid, "big manifest", branch="main")
334 _set_branch(repo, "main", cid)
335
336 result = runner.invoke(cli, ["ls-files"], env=_env(repo))
337 assert result.exit_code == 0
338 data = json.loads(result.stdout)
339 assert data["file_count"] == 2000
340
341
342 # ---------------------------------------------------------------------------
343 # Stress: for-each-ref and show-ref
344 # ---------------------------------------------------------------------------
345
346
347 class TestRefCommandsStress:
348 def _build_100_branches(self, repo: pathlib.Path) -> None:
349 sid = _snap(repo, tag="multi-branch")
350 for i in range(100):
351 branch = f"feature-{i:03d}"
352 cid = _commit_raw(repo, "", sid, f"tip of {branch}", branch=branch)
353 _set_branch(repo, branch, cid)
354
355 def test_for_each_ref_100_branches(self, tmp_path: pathlib.Path) -> None:
356 repo = _init_repo(tmp_path)
357 self._build_100_branches(repo)
358 result = runner.invoke(cli, ["for-each-ref"], env=_env(repo))
359 assert result.exit_code == 0
360 data = json.loads(result.stdout)
361 assert len(data["refs"]) == 100
362
363 def test_show_ref_100_branches(self, tmp_path: pathlib.Path) -> None:
364 repo = _init_repo(tmp_path)
365 self._build_100_branches(repo)
366 result = runner.invoke(cli, ["show-ref"], env=_env(repo))
367 assert result.exit_code == 0
368 data = json.loads(result.stdout)
369 assert data["count"] == 100
370
371 def test_for_each_ref_pattern_filter_on_100(self, tmp_path: pathlib.Path) -> None:
372 repo = _init_repo(tmp_path)
373 self._build_100_branches(repo)
374 result = runner.invoke(
375 cli,
376 ["for-each-ref", "--pattern", "refs/heads/feature-00*"],
377 env=_env(repo),
378 )
379 assert result.exit_code == 0
380 data = json.loads(result.stdout)
381 # feature-000 through feature-009 = 10 branches
382 assert len(data["refs"]) == 10
383
384
385 # ---------------------------------------------------------------------------
386 # Stress: pack-objects → unpack-objects
387 # ---------------------------------------------------------------------------
388
389
390 class TestPackUnpackStress:
391 def test_100_commit_100_object_round_trip(self, tmp_path: pathlib.Path) -> None:
392 from muse.core.object_store import has_object
393 from muse.core.store import read_commit
394
395 src = _init_repo(tmp_path / "src")
396 dst = _init_repo(tmp_path / "dst")
397
398 # Build 100 objects
399 oids = [_obj(src, f"blob-{i}") for i in range(100)]
400 manifest = {f"f{i}.mid": oids[i] for i in range(100)}
401 sid = _snap(src, manifest, "big-pack")
402
403 # Build 100-commit linear chain referencing that snapshot
404 parent: str | None = None
405 cids: list[str] = []
406 for i in range(100):
407 cid = _commit_raw(src, "", sid, f"pack-{i}", parent=parent)
408 cids.append(cid)
409 parent = cid
410 _set_branch(src, "main", cids[-1])
411
412 # Pack tip → unpack into dst
413 pack_result = runner.invoke(
414 cli, ["pack-objects", cids[-1]], env=_env(src)
415 )
416 assert pack_result.exit_code == 0
417
418 unpack_result = runner.invoke(
419 cli,
420 ["unpack-objects"],
421 input=pack_result.stdout_bytes,
422 env=_env(dst),
423 )
424 assert unpack_result.exit_code == 0
425 counts = json.loads(unpack_result.stdout)
426 assert counts["commits_written"] == 100
427 assert counts["objects_written"] == 100
428
429 for cid in cids:
430 assert read_commit(dst, cid) is not None
431 for oid in oids:
432 assert has_object(dst, oid)
433
434
435 # ---------------------------------------------------------------------------
436 # Stress: read-commit sequential
437 # ---------------------------------------------------------------------------
438
439
440 class TestReadCommitStress:
441 def test_200_commits_all_readable(self, tmp_path: pathlib.Path) -> None:
442 repo = _init_repo(tmp_path)
443 sid = _snap(repo)
444 cids = _linear_chain(repo, 200, sid)
445 for cid in cids:
446 result = runner.invoke(cli, ["read-commit", cid], env=_env(repo))
447 assert result.exit_code == 0
448 data = json.loads(result.stdout)
449 assert data["commit_id"] == cid
File History 3 commits
sha256:660fcac1df3ab28f61862e961890bd2ca8b754fa0242079d93ca1e25037ec8a6 chore(tests): add docstring to tests/__init__.py so rc14 tr… Human 26 days ago
sha256:d8316ffae901be06347e16ab55be11868eb519dd16ade3e8aa16a99e662f7e62 baseline: rc14 re-baseline after rc3 store corruption recovery Human patch 26 days ago
sha256:1c4b3e3a9a1f300774c3ee662b572a698d5fd405bf765a71e6011a2e9c3eaaaa feat: Muse — version control for the agent era Human 100 days ago