cgcardona / muse public
test_cli_log.py python
293 lines 10.6 KB
c8984819 test: bring core VCS coverage from 60% to 91% Gabriel Cardona <gabriel@tellurstori.com> 3d ago
1 """Tests for muse log — commit history display and filters."""
2 from __future__ import annotations
3
4 import pathlib
5
6 import pytest
7 from typer.testing import CliRunner
8
9 from muse.cli.app import cli
10 from muse.cli.commands.log import _parse_date
11
12 runner = CliRunner()
13
14
15 @pytest.fixture
16 def repo(tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch) -> pathlib.Path:
17 monkeypatch.chdir(tmp_path)
18 monkeypatch.setenv("MUSE_REPO_ROOT", str(tmp_path))
19 result = runner.invoke(cli, ["init"])
20 assert result.exit_code == 0, result.output
21 return tmp_path
22
23
24 def _write(repo: pathlib.Path, filename: str, content: str = "data") -> None:
25 (repo / "muse-work" / filename).write_text(content)
26
27
28 def _commit(msg: str, **flags: str) -> None:
29 args = ["commit", "-m", msg]
30 for k, v in flags.items():
31 args += [f"--{k}", v]
32 result = runner.invoke(cli, args)
33 assert result.exit_code == 0, result.output
34
35
36 # ---------------------------------------------------------------------------
37 # _parse_date unit tests
38 # ---------------------------------------------------------------------------
39
40
41 class TestParseDate:
42 def test_today(self) -> None:
43 from datetime import datetime, timezone
44 dt = _parse_date("today")
45 assert dt.tzinfo == timezone.utc
46 now = datetime.now(timezone.utc)
47 assert dt.date() == now.date()
48
49 def test_yesterday(self) -> None:
50 from datetime import datetime, timedelta, timezone
51 dt = _parse_date("yesterday")
52 expected = (datetime.now(timezone.utc) - timedelta(days=1)).date()
53 assert dt.date() == expected
54
55 def test_n_days_ago(self) -> None:
56 from datetime import datetime, timedelta, timezone
57 dt = _parse_date("3 days ago")
58 expected = (datetime.now(timezone.utc) - timedelta(days=3)).date()
59 assert dt.date() == expected
60
61 def test_n_weeks_ago(self) -> None:
62 from datetime import datetime, timedelta, timezone
63 dt = _parse_date("2 weeks ago")
64 expected = (datetime.now(timezone.utc) - timedelta(weeks=2)).date()
65 assert dt.date() == expected
66
67 def test_n_months_ago(self) -> None:
68 from datetime import datetime, timedelta, timezone
69 dt = _parse_date("1 month ago")
70 expected = (datetime.now(timezone.utc) - timedelta(days=30)).date()
71 assert dt.date() == expected
72
73 def test_n_years_ago(self) -> None:
74 from datetime import datetime, timedelta, timezone
75 dt = _parse_date("1 year ago")
76 expected = (datetime.now(timezone.utc) - timedelta(days=365)).date()
77 assert dt.date() == expected
78
79 def test_iso_date(self) -> None:
80 dt = _parse_date("2025-01-15")
81 assert dt.year == 2025
82 assert dt.month == 1
83 assert dt.day == 15
84
85 def test_iso_datetime(self) -> None:
86 dt = _parse_date("2025-01-15T10:30:00")
87 assert dt.hour == 10
88 assert dt.minute == 30
89
90 def test_iso_datetime_space(self) -> None:
91 dt = _parse_date("2025-01-15 10:30:00")
92 assert dt.hour == 10
93
94 def test_invalid_raises(self) -> None:
95 with pytest.raises(ValueError, match="Cannot parse date"):
96 _parse_date("not-a-date")
97
98
99 # ---------------------------------------------------------------------------
100 # Log output modes
101 # ---------------------------------------------------------------------------
102
103
104 class TestLogEmpty:
105 def test_empty_repo_shows_no_commits(self, repo: pathlib.Path) -> None:
106 result = runner.invoke(cli, ["log"])
107 assert result.exit_code == 0
108 assert "no commits" in result.output.lower() or "(no commits)" in result.output
109
110
111 class TestLogDefault:
112 def test_shows_commit_line(self, repo: pathlib.Path) -> None:
113 _write(repo, "beat.mid")
114 _commit("first commit")
115 result = runner.invoke(cli, ["log"])
116 assert result.exit_code == 0
117 assert "commit" in result.output
118 assert "first commit" in result.output
119
120 def test_shows_date(self, repo: pathlib.Path) -> None:
121 _write(repo, "beat.mid")
122 _commit("dated")
123 result = runner.invoke(cli, ["log"])
124 assert "Date:" in result.output
125
126 def test_shows_author_when_set(self, repo: pathlib.Path) -> None:
127 _write(repo, "beat.mid")
128 _commit("authored", author="Gabriel")
129 result = runner.invoke(cli, ["log"])
130 assert "Gabriel" in result.output
131
132 def test_multiple_commits_newest_first(self, repo: pathlib.Path) -> None:
133 _write(repo, "a.mid")
134 _commit("first")
135 _write(repo, "b.mid")
136 _commit("second")
137 result = runner.invoke(cli, ["log"])
138 assert result.output.index("second") < result.output.index("first")
139
140 def test_shows_head_label(self, repo: pathlib.Path) -> None:
141 _write(repo, "beat.mid")
142 _commit("only")
143 result = runner.invoke(cli, ["log"])
144 assert "HEAD" in result.output
145
146 def test_shows_metadata(self, repo: pathlib.Path) -> None:
147 _write(repo, "beat.mid")
148 _commit("versed", section="verse")
149 result = runner.invoke(cli, ["log"])
150 assert "verse" in result.output
151 assert "Meta:" in result.output
152
153
154 class TestLogOneline:
155 def test_one_line_per_commit(self, repo: pathlib.Path) -> None:
156 _write(repo, "a.mid")
157 _commit("first")
158 _write(repo, "b.mid")
159 _commit("second")
160 result = runner.invoke(cli, ["log", "--oneline"])
161 assert result.exit_code == 0
162 lines = [l for l in result.output.strip().splitlines() if l.strip()]
163 assert len(lines) == 2
164
165 def test_oneline_format(self, repo: pathlib.Path) -> None:
166 _write(repo, "beat.mid")
167 _commit("a message")
168 result = runner.invoke(cli, ["log", "--oneline"])
169 # short id + message on one line
170 assert "a message" in result.output
171 lines = [l for l in result.output.strip().splitlines() if l]
172 assert len(lines) == 1
173
174 def test_oneline_shows_head_label(self, repo: pathlib.Path) -> None:
175 _write(repo, "beat.mid")
176 _commit("only")
177 result = runner.invoke(cli, ["log", "--oneline"])
178 assert "HEAD" in result.output
179
180
181 class TestLogGraph:
182 def test_graph_prefix(self, repo: pathlib.Path) -> None:
183 _write(repo, "beat.mid")
184 _commit("graphed")
185 result = runner.invoke(cli, ["log", "--graph"])
186 assert result.exit_code == 0
187 assert "* " in result.output
188
189 def test_graph_shows_message(self, repo: pathlib.Path) -> None:
190 _write(repo, "beat.mid")
191 _commit("graph msg")
192 result = runner.invoke(cli, ["log", "--graph"])
193 assert "graph msg" in result.output
194
195
196 class TestLogStat:
197 def test_stat_shows_added_files(self, repo: pathlib.Path) -> None:
198 _write(repo, "beat.mid")
199 _commit("add beat")
200 result = runner.invoke(cli, ["log", "--stat"])
201 assert result.exit_code == 0
202 assert "beat.mid" in result.output
203 assert "+" in result.output
204
205 def test_stat_shows_summary_line(self, repo: pathlib.Path) -> None:
206 _write(repo, "beat.mid")
207 _commit("add")
208 result = runner.invoke(cli, ["log", "--stat"])
209 assert "added" in result.output
210
211 def test_patch_shows_files(self, repo: pathlib.Path) -> None:
212 _write(repo, "beat.mid")
213 _commit("patched")
214 result = runner.invoke(cli, ["log", "--patch"])
215 assert result.exit_code == 0
216 assert "beat.mid" in result.output
217
218
219 class TestLogFilters:
220 def test_limit_n(self, repo: pathlib.Path) -> None:
221 for i in range(5):
222 _write(repo, f"f{i}.mid", str(i))
223 _commit(f"msg-{i}")
224 result = runner.invoke(cli, ["log", "-n", "2"])
225 assert result.exit_code == 0
226 # With limit 2, we should see the 2 newest but not the oldest
227 assert "msg-0" not in result.output
228 assert "msg-1" not in result.output
229 assert "msg-2" not in result.output
230
231 def test_filter_author(self, repo: pathlib.Path) -> None:
232 _write(repo, "a.mid")
233 _commit("by gabriel", author="Gabriel")
234 _write(repo, "b.mid")
235 _commit("by alice", author="Alice")
236 result = runner.invoke(cli, ["log", "--author", "Gabriel"])
237 assert result.exit_code == 0
238 assert "by gabriel" in result.output
239 assert "by alice" not in result.output
240
241 def test_filter_author_case_insensitive(self, repo: pathlib.Path) -> None:
242 _write(repo, "a.mid")
243 _commit("authored", author="Gabriel")
244 result = runner.invoke(cli, ["log", "--author", "gabriel"])
245 assert "authored" in result.output
246
247 def test_filter_section(self, repo: pathlib.Path) -> None:
248 _write(repo, "a.mid")
249 _commit("verse part", section="verse")
250 _write(repo, "b.mid")
251 _commit("chorus part", section="chorus")
252 result = runner.invoke(cli, ["log", "--section", "verse"])
253 assert result.exit_code == 0
254 assert "verse part" in result.output
255 assert "chorus part" not in result.output
256
257 def test_filter_track(self, repo: pathlib.Path) -> None:
258 _write(repo, "a.mid")
259 _commit("drums commit", track="drums")
260 _write(repo, "b.mid")
261 _commit("bass commit", track="bass")
262 result = runner.invoke(cli, ["log", "--track", "drums"])
263 assert "drums commit" in result.output
264 assert "bass commit" not in result.output
265
266 def test_filter_emotion(self, repo: pathlib.Path) -> None:
267 _write(repo, "a.mid")
268 _commit("happy commit", emotion="joyful")
269 _write(repo, "b.mid")
270 _commit("sad commit", emotion="melancholic")
271 result = runner.invoke(cli, ["log", "--emotion", "joyful"])
272 assert "happy commit" in result.output
273 assert "sad commit" not in result.output
274
275 def test_filter_since_future_returns_nothing(self, repo: pathlib.Path) -> None:
276 _write(repo, "a.mid")
277 _commit("old commit")
278 result = runner.invoke(cli, ["log", "--since", "2099-01-01"])
279 assert result.exit_code == 0
280 assert "no commits" in result.output.lower() or "(no commits)" in result.output
281
282 def test_filter_until_past_returns_nothing(self, repo: pathlib.Path) -> None:
283 _write(repo, "a.mid")
284 _commit("recent commit")
285 result = runner.invoke(cli, ["log", "--until", "2000-01-01"])
286 assert result.exit_code == 0
287 assert "no commits" in result.output.lower() or "(no commits)" in result.output
288
289 def test_no_matches_shows_no_commits(self, repo: pathlib.Path) -> None:
290 _write(repo, "a.mid")
291 _commit("only commit", author="Gabriel")
292 result = runner.invoke(cli, ["log", "--author", "nobody"])
293 assert "(no commits)" in result.output