gabriel / muse public
test_cmd_shortlog.py python
226 lines 7.5 KB
1ba7f7b1 feat(porcelain): implement 9 gap-fill porcelain commands with full test… Gabriel Cardona <gabriel@tellurstori.com> 2d ago
1 """Tests for ``muse shortlog``.
2
3 Covers: empty repo, single author, multiple authors, --numbered sort,
4 --email flag, --format json, --all branches, --limit, short flags,
5 stress: 200 commits across 3 authors.
6 """
7
8 from __future__ import annotations
9
10 import datetime
11 import hashlib
12 import json
13 import pathlib
14
15 import pytest
16 from typer.testing import CliRunner
17
18 from muse.cli.app import cli
19 from muse.core.object_store import write_object
20 from muse.core.snapshot import compute_snapshot_id
21 from muse.core.store import CommitRecord, SnapshotRecord, write_commit, write_snapshot
22
23 runner = CliRunner()
24
25 _REPO_ID = "shortlog-test"
26
27
28 # ---------------------------------------------------------------------------
29 # Helpers
30 # ---------------------------------------------------------------------------
31
32
33 def _sha(data: bytes) -> str:
34 return hashlib.sha256(data).hexdigest()
35
36
37 def _init_repo(path: pathlib.Path) -> pathlib.Path:
38 muse = path / ".muse"
39 for d in ("commits", "snapshots", "objects", "refs/heads"):
40 (muse / d).mkdir(parents=True, exist_ok=True)
41 (muse / "HEAD").write_text("ref: refs/heads/main", encoding="utf-8")
42 (muse / "repo.json").write_text(
43 json.dumps({"repo_id": _REPO_ID, "domain": "midi"}), encoding="utf-8"
44 )
45 return path
46
47
48 def _env(repo: pathlib.Path) -> dict[str, str]:
49 return {"MUSE_REPO_ROOT": str(repo)}
50
51
52 _counter = 0
53
54 # Per-branch tracking of the latest commit so tests can chain automatically.
55 _branch_heads: dict[str, str] = {}
56
57
58 def _make_commit(
59 root: pathlib.Path,
60 author: str = "Alice",
61 parent_id: str | None = None,
62 branch: str = "main",
63 ) -> str:
64 """Create a commit, automatically chaining to the previous commit on the branch."""
65 global _counter
66 _counter += 1
67 # Auto-chain: if no explicit parent, use the last commit on this branch.
68 if parent_id is None:
69 parent_id = _branch_heads.get(f"{str(root)}:{branch}")
70 content = f"content-{_counter}".encode()
71 obj_id = _sha(content)
72 write_object(root, obj_id, content)
73 manifest = {f"file_{_counter}.txt": obj_id}
74 snap_id = compute_snapshot_id(manifest)
75 write_snapshot(root, SnapshotRecord(snapshot_id=snap_id, manifest=manifest))
76 committed_at = datetime.datetime.now(datetime.timezone.utc)
77 commit_id = _sha(f"{_counter}:{author}:{snap_id}".encode())
78 write_commit(root, CommitRecord(
79 commit_id=commit_id,
80 repo_id=_REPO_ID,
81 branch=branch,
82 snapshot_id=snap_id,
83 message=f"commit by {author} #{_counter}",
84 committed_at=committed_at,
85 parent_commit_id=parent_id,
86 author=author,
87 ))
88 (root / ".muse" / "refs" / "heads" / branch).write_text(commit_id, encoding="utf-8")
89 _branch_heads[f"{str(root)}:{branch}"] = commit_id
90 return commit_id
91
92
93 # ---------------------------------------------------------------------------
94 # Unit: empty repo
95 # ---------------------------------------------------------------------------
96
97
98 def test_shortlog_empty_repo(tmp_path: pathlib.Path) -> None:
99 _init_repo(tmp_path)
100 result = runner.invoke(cli, ["shortlog"], env=_env(tmp_path))
101 assert result.exit_code == 0
102 assert "no commits" in result.output.lower()
103
104
105 def test_shortlog_help() -> None:
106 result = runner.invoke(cli, ["shortlog", "--help"])
107 assert result.exit_code == 0
108 assert "--numbered" in result.output or "-n" in result.output
109
110
111 # ---------------------------------------------------------------------------
112 # Unit: single author
113 # ---------------------------------------------------------------------------
114
115
116 def test_shortlog_single_author(tmp_path: pathlib.Path) -> None:
117 _init_repo(tmp_path)
118 _make_commit(tmp_path, author="Alice")
119 _make_commit(tmp_path, author="Alice")
120 result = runner.invoke(cli, ["shortlog"], env=_env(tmp_path))
121 assert result.exit_code == 0
122 assert "Alice" in result.output
123 assert "(2)" in result.output
124
125
126 # ---------------------------------------------------------------------------
127 # Unit: multiple authors
128 # ---------------------------------------------------------------------------
129
130
131 def test_shortlog_multiple_authors(tmp_path: pathlib.Path) -> None:
132 _init_repo(tmp_path)
133 _make_commit(tmp_path, author="Alice")
134 _make_commit(tmp_path, author="Bob")
135 _make_commit(tmp_path, author="Alice")
136 result = runner.invoke(cli, ["shortlog"], env=_env(tmp_path))
137 assert result.exit_code == 0
138 assert "Alice" in result.output
139 assert "Bob" in result.output
140
141
142 # ---------------------------------------------------------------------------
143 # Unit: --numbered sorts by count
144 # ---------------------------------------------------------------------------
145
146
147 def test_shortlog_numbered(tmp_path: pathlib.Path) -> None:
148 _init_repo(tmp_path)
149 _make_commit(tmp_path, author="Bob")
150 _make_commit(tmp_path, author="Alice")
151 _make_commit(tmp_path, author="Alice")
152 _make_commit(tmp_path, author="Alice")
153 result = runner.invoke(cli, ["shortlog", "--numbered"], env=_env(tmp_path))
154 assert result.exit_code == 0
155 alice_pos = result.output.index("Alice")
156 bob_pos = result.output.index("Bob")
157 assert alice_pos < bob_pos # Alice has more commits, should appear first
158
159
160 # ---------------------------------------------------------------------------
161 # Unit: --format json
162 # ---------------------------------------------------------------------------
163
164
165 def test_shortlog_json_output(tmp_path: pathlib.Path) -> None:
166 _init_repo(tmp_path)
167 _make_commit(tmp_path, author="Charlie")
168 result = runner.invoke(cli, ["shortlog", "--format", "json"], env=_env(tmp_path))
169 assert result.exit_code == 0
170 data = json.loads(result.output)
171 assert isinstance(data, list)
172 assert len(data) >= 1
173 assert data[0]["author"] == "Charlie"
174 assert data[0]["count"] >= 1
175
176
177 # ---------------------------------------------------------------------------
178 # Unit: --limit
179 # ---------------------------------------------------------------------------
180
181
182 def test_shortlog_limit(tmp_path: pathlib.Path) -> None:
183 _init_repo(tmp_path)
184 for _ in range(20):
185 _make_commit(tmp_path, author="Dave")
186 result = runner.invoke(cli, ["shortlog", "--limit", "5", "--format", "json"], env=_env(tmp_path))
187 assert result.exit_code == 0
188 data = json.loads(result.output)
189 total_commits = sum(g["count"] for g in data)
190 assert total_commits <= 5
191
192
193 # ---------------------------------------------------------------------------
194 # Unit: short flags
195 # ---------------------------------------------------------------------------
196
197
198 def test_shortlog_short_flags(tmp_path: pathlib.Path) -> None:
199 _init_repo(tmp_path)
200 _make_commit(tmp_path, author="Eve")
201 result = runner.invoke(cli, ["shortlog", "-n", "-f", "json"], env=_env(tmp_path))
202 assert result.exit_code == 0
203 data = json.loads(result.output)
204 assert len(data) >= 1
205
206
207 # ---------------------------------------------------------------------------
208 # Stress: 200 commits across 3 authors
209 # ---------------------------------------------------------------------------
210
211
212 def test_shortlog_stress_200_commits(tmp_path: pathlib.Path) -> None:
213 _init_repo(tmp_path)
214 authors = ["Frank", "Grace", "Heidi"]
215 for i in range(200):
216 _make_commit(tmp_path, author=authors[i % 3])
217
218 result = runner.invoke(cli, ["shortlog", "--format", "json"], env=_env(tmp_path))
219 assert result.exit_code == 0
220 data = json.loads(result.output)
221 total = sum(g["count"] for g in data)
222 assert total == 200
223 names = {g["author"] for g in data}
224 assert "Frank" in names
225 assert "Grace" in names
226 assert "Heidi" in names