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