gabriel / muse public
test_cmd_stress.py python
455 lines 16.1 KB
Raw
sha256:b636f72dcba9e190afb980bece906fa5b717fbde014b76ef023df8cb96e01eb9 docs: expand cache plan with all seven testing tiers and do… Sonnet 4.6 140 days ago
1 """Stress and scale tests for Muse commands.
2
3 These tests exercise 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 json
27 import pathlib
28
29 from tests.cli_test_helper import CliRunner
30
31 cli = None # argparse migration — CliRunner ignores this arg
32 from muse.core._types import blob_id, fake_id
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 fake_id(tag)
47
48
49 def _sha_bytes(data: bytes) -> str:
50 return blob_id(data)
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(
103 repo_id="stress-repo",
104 parent_ids=parent_ids,
105 snapshot_id=sid,
106 message=message,
107 committed_at_iso=committed_at.isoformat(),
108 author="stress-tester",)
109 write_commit(
110 repo,
111 CommitRecord(
112 commit_id=real_cid,
113 repo_id="stress-repo",
114 created_on_branch=branch,
115 snapshot_id=sid,
116 message=message,
117 committed_at=committed_at,
118 author="stress-tester",
119 parent_commit_id=parent,
120 parent2_commit_id=parent2,
121 ),
122 )
123 return real_cid
124
125
126 def _set_branch(repo: pathlib.Path, branch: str, cid: str) -> None:
127 ref = repo / ".muse" / "refs" / "heads" / branch
128 ref.parent.mkdir(parents=True, exist_ok=True)
129 ref.write_text(cid, encoding="utf-8")
130
131
132 def _linear_chain(repo: pathlib.Path, n: int, sid: str, branch: str = "main") -> list[str]:
133 """Build a linear chain of n commits. Returns real commit IDs root→tip."""
134 cids: list[str] = []
135 parent: str | None = None
136 for i in range(n):
137 real_cid = _commit_raw(repo, "", sid, f"commit {i}", branch=branch, parent=parent)
138 cids.append(real_cid)
139 parent = real_cid
140 _set_branch(repo, branch, cids[-1])
141 return cids
142
143
144 def _obj(repo: pathlib.Path, tag: str) -> str:
145 content = tag.encode()
146 oid = _sha_bytes(content)
147 write_object(repo, oid, content)
148 return oid
149
150
151 # ---------------------------------------------------------------------------
152 # Stress: commit-graph
153 # ---------------------------------------------------------------------------
154
155
156 class TestCommitGraphStress:
157 def test_500_commit_linear_chain_full_traversal(self, tmp_path: pathlib.Path) -> None:
158 repo = _init_repo(tmp_path)
159 sid = _snap(repo)
160 cids = _linear_chain(repo, 500, sid)
161 result = runner.invoke(cli, ["commit-graph", "--json"], env=_env(repo))
162 assert result.exit_code == 0, result.output
163 data = json.loads(result.stdout)
164 assert data["count"] == 500
165 assert data["truncated"] is False
166
167 def test_500_commit_chain_stop_at_midpoint(self, tmp_path: pathlib.Path) -> None:
168 repo = _init_repo(tmp_path)
169 sid = _snap(repo)
170 cids = _linear_chain(repo, 500, sid)
171 result = runner.invoke(
172 cli,
173 ["commit-graph", "--json", "--tip", cids[499], "--stop-at", cids[249]],
174 env=_env(repo),
175 )
176 assert result.exit_code == 0
177 data = json.loads(result.stdout)
178 assert data["count"] == 250
179
180 def test_count_flag_on_500_commits(self, tmp_path: pathlib.Path) -> None:
181 repo = _init_repo(tmp_path)
182 sid = _snap(repo)
183 _linear_chain(repo, 500, sid)
184 result = runner.invoke(cli, ["commit-graph", "--count", "--json"], env=_env(repo))
185 assert result.exit_code == 0
186 data = json.loads(result.stdout)
187 assert data["count"] == 500
188 assert "commits" not in data # --count suppresses node list
189
190
191 # ---------------------------------------------------------------------------
192 # Stress: merge-base
193 # ---------------------------------------------------------------------------
194
195
196 class TestMergeBaseStress:
197 def test_merge_base_300_deep_shared_root(self, tmp_path: pathlib.Path) -> None:
198 repo = _init_repo(tmp_path)
199 sid = _snap(repo)
200
201 # Shared root
202 root_cid = _commit_raw(repo, "", sid, "root")
203
204 # Two 150-commit chains from the same root
205 main_chain = [root_cid]
206 feat_chain = [root_cid]
207 for i in range(150):
208 mc = _commit_raw(repo, "", sid, f"main-{i}", branch="main", parent=main_chain[-1])
209 main_chain.append(mc)
210 fc = _commit_raw(repo, "", sid, f"feat-{i}", branch="feat", parent=feat_chain[-1])
211 feat_chain.append(fc)
212
213 _set_branch(repo, "main", main_chain[-1])
214 _set_branch(repo, "feat", feat_chain[-1])
215 (repo / ".muse" / "HEAD").write_text("ref: refs/heads/main", encoding="utf-8")
216
217 result = runner.invoke(
218 cli, ["merge-base", "--json", "main", "feat"], env=_env(repo)
219 )
220 assert result.exit_code == 0
221 data = json.loads(result.stdout)
222 assert data["merge_base"] == root_cid
223
224
225 # ---------------------------------------------------------------------------
226 # Stress: name-rev
227 # ---------------------------------------------------------------------------
228
229
230 class TestNameRevStress:
231 def test_name_rev_200_commit_chain_all_named(self, tmp_path: pathlib.Path) -> None:
232 repo = _init_repo(tmp_path)
233 sid = _snap(repo)
234 cids = _linear_chain(repo, 200, sid)
235
236 result = runner.invoke(cli, ["name-rev", "--json", *cids], env=_env(repo))
237 assert result.exit_code == 0
238 data = json.loads(result.stdout)
239 assert len(data["results"]) == 200
240 for entry in data["results"]:
241 assert not entry["undefined"]
242
243 def test_name_rev_tip_has_no_tilde_suffix(self, tmp_path: pathlib.Path) -> None:
244 """distance=0 means the tip is the branch tip itself; name is bare branch name."""
245 repo = _init_repo(tmp_path)
246 sid = _snap(repo)
247 cids = _linear_chain(repo, 10, sid)
248 tip = cids[-1]
249
250 result = runner.invoke(cli, ["name-rev", "--json", tip], env=_env(repo))
251 assert result.exit_code == 0
252 entry = json.loads(result.stdout)["results"][0]
253 # name-rev emits "<branch>" (no ~0) for the exact branch tip.
254 assert entry["name"] == "main"
255 assert entry["distance"] == 0
256
257
258 # ---------------------------------------------------------------------------
259 # Stress: snapshot-diff
260 # ---------------------------------------------------------------------------
261
262
263 class TestSnapshotDiffStress:
264 def test_diff_2000_file_manifests(self, tmp_path: pathlib.Path) -> None:
265 repo = _init_repo(tmp_path)
266 oid = _sha("shared-blob")
267
268 # Manifest A: 2000 files
269 manifest_a = {f"track_{i:04d}.mid": oid for i in range(2000)}
270 # Manifest B: same 2000 files but first 200 have new IDs (modified)
271 new_oid = _sha("new-blob")
272 manifest_b = {f"track_{i:04d}.mid": (new_oid if i < 200 else oid) for i in range(2000)}
273
274 sid_a = compute_snapshot_id(manifest_a)
275 sid_b = compute_snapshot_id(manifest_b)
276 write_snapshot(
277 repo,
278 SnapshotRecord(
279 snapshot_id=sid_a,
280 manifest=manifest_a,
281 created_at=datetime.datetime(2026, 1, 1, tzinfo=datetime.timezone.utc),
282 ),
283 )
284 write_snapshot(
285 repo,
286 SnapshotRecord(
287 snapshot_id=sid_b,
288 manifest=manifest_b,
289 created_at=datetime.datetime(2026, 1, 2, tzinfo=datetime.timezone.utc),
290 ),
291 )
292
293 result = runner.invoke(cli, ["snapshot-diff", "--json", sid_a, sid_b], env=_env(repo))
294 assert result.exit_code == 0
295 data = json.loads(result.stdout)
296 assert data["total_changes"] == 200
297 assert len(data["modified"]) == 200
298 assert data["added"] == []
299 assert data["deleted"] == []
300
301
302 # ---------------------------------------------------------------------------
303 # Stress: verify-object
304 # ---------------------------------------------------------------------------
305
306
307 class TestVerifyObjectStress:
308 def test_200_objects_all_verified(self, tmp_path: pathlib.Path) -> None:
309 repo = _init_repo(tmp_path)
310 oids = [_obj(repo, f"stress-obj-{i}") for i in range(200)]
311 result = runner.invoke(cli, ["verify-object", "--json", *oids], env=_env(repo))
312 assert result.exit_code == 0
313 data = json.loads(result.stdout)
314 assert data["all_ok"] is True
315 assert data["checked"] == 200
316 assert data["failed"] == 0
317
318 def test_verify_1mib_object_no_crash(self, tmp_path: pathlib.Path) -> None:
319 repo = _init_repo(tmp_path)
320 content = b"Z" * (1024 * 1024)
321 oid = _sha_bytes(content)
322 write_object(repo, oid, content)
323 result = runner.invoke(cli, ["verify-object", "--json", oid], env=_env(repo))
324 assert result.exit_code == 0
325 assert json.loads(result.stdout)["all_ok"] is True
326
327
328 # ---------------------------------------------------------------------------
329 # Stress: ls-files
330 # ---------------------------------------------------------------------------
331
332
333 class TestLsFilesStress:
334 def test_ls_files_2000_file_snapshot(self, tmp_path: pathlib.Path) -> None:
335 repo = _init_repo(tmp_path)
336 oid = _sha("common-oid")
337 manifest = {f"track_{i:04d}.mid": oid for i in range(2000)}
338 sid = _snap(repo, manifest, "big")
339 cid = _commit_raw(repo, "", sid, "big manifest", branch="main")
340 _set_branch(repo, "main", cid)
341
342 result = runner.invoke(cli, ["ls-files", "--json"], env=_env(repo))
343 assert result.exit_code == 0
344 data = json.loads(result.stdout)
345 assert data["file_count"] == 2000
346
347
348 # ---------------------------------------------------------------------------
349 # Stress: for-each-ref and show-ref
350 # ---------------------------------------------------------------------------
351
352
353 class TestRefCommandsStress:
354 def _build_100_branches(self, repo: pathlib.Path) -> None:
355 sid = _snap(repo, tag="multi-branch")
356 for i in range(100):
357 branch = f"feature-{i:03d}"
358 cid = _commit_raw(repo, "", sid, f"tip of {branch}", branch=branch)
359 _set_branch(repo, branch, cid)
360
361 def test_for_each_ref_100_branches(self, tmp_path: pathlib.Path) -> None:
362 repo = _init_repo(tmp_path)
363 self._build_100_branches(repo)
364 result = runner.invoke(cli, ["for-each-ref", "--json"], env=_env(repo))
365 assert result.exit_code == 0
366 data = json.loads(result.stdout)
367 assert len(data["refs"]) == 100
368
369 def test_show_ref_100_branches(self, tmp_path: pathlib.Path) -> None:
370 repo = _init_repo(tmp_path)
371 self._build_100_branches(repo)
372 result = runner.invoke(cli, ["show-ref", "--json"], env=_env(repo))
373 assert result.exit_code == 0
374 data = json.loads(result.stdout)
375 assert data["count"] == 100
376
377 def test_for_each_ref_pattern_filter_on_100(self, tmp_path: pathlib.Path) -> None:
378 repo = _init_repo(tmp_path)
379 self._build_100_branches(repo)
380 result = runner.invoke(
381 cli,
382 ["for-each-ref", "--json", "--pattern", "refs/heads/feature-00*"],
383 env=_env(repo),
384 )
385 assert result.exit_code == 0
386 data = json.loads(result.stdout)
387 # feature-000 through feature-009 = 10 branches
388 assert len(data["refs"]) == 10
389
390
391 # ---------------------------------------------------------------------------
392 # Stress: pack-objects → unpack-objects
393 # ---------------------------------------------------------------------------
394
395
396 class TestPackUnpackStress:
397 def test_100_commit_100_object_round_trip(self, tmp_path: pathlib.Path) -> None:
398 from muse.core.object_store import has_object
399 from muse.core.store import read_commit
400
401 src = _init_repo(tmp_path / "src")
402 dst = _init_repo(tmp_path / "dst")
403
404 # Build 100 objects
405 oids = [_obj(src, f"blob-{i}") for i in range(100)]
406 manifest = {f"f{i}.mid": oids[i] for i in range(100)}
407 sid = _snap(src, manifest, "big-pack")
408
409 # Build 100-commit linear chain referencing that snapshot
410 parent: str | None = None
411 cids: list[str] = []
412 for i in range(100):
413 cid = _commit_raw(src, "", sid, f"pack-{i}", parent=parent)
414 cids.append(cid)
415 parent = cid
416 _set_branch(src, "main", cids[-1])
417
418 # Pack tip → unpack into dst
419 pack_result = runner.invoke(
420 cli, ["pack-objects", cids[-1]], env=_env(src)
421 )
422 assert pack_result.exit_code == 0
423
424 unpack_result = runner.invoke(
425 cli,
426 ["unpack-objects", "--json"],
427 input=pack_result.stdout_bytes,
428 env=_env(dst),
429 )
430 assert unpack_result.exit_code == 0
431 counts = json.loads(unpack_result.stdout)
432 assert counts["commits_written"] == 100
433 assert counts["objects_written"] == 100
434
435 for cid in cids:
436 assert read_commit(dst, cid) is not None
437 for oid in oids:
438 assert has_object(dst, oid)
439
440
441 # ---------------------------------------------------------------------------
442 # Stress: read-commit sequential
443 # ---------------------------------------------------------------------------
444
445
446 class TestReadCommitStress:
447 def test_200_commits_all_readable(self, tmp_path: pathlib.Path) -> None:
448 repo = _init_repo(tmp_path)
449 sid = _snap(repo)
450 cids = _linear_chain(repo, 200, sid)
451 for cid in cids:
452 result = runner.invoke(cli, ["read-commit", "--json", cid], env=_env(repo))
453 assert result.exit_code == 0
454 data = json.loads(result.stdout)
455 assert data["commit_id"] == cid
File History 3 commits
sha256:b636f72dcba9e190afb980bece906fa5b717fbde014b76ef023df8cb96e01eb9 docs: expand cache plan with all seven testing tiers and do… Sonnet 4.6 140 days ago
sha256:7f9e2ef5286aedad9c1e6011b4c46ca27f39dbdad6e3409357e36b26e46b3b7c docs: docstring sprint for-each-ref→hotspots — idiomatic ru… Sonnet 4.6 patch 146 days ago
sha256:a09b1b4f6838754495547f200aa0ce88e2f56ffc5b20b900f6f0cff2c3cdede9 fix(cursorignore): remove git-ism (.git/worktrees) Human minor 149 days ago