gabriel / muse public
test_cli_log.py python
286 lines 10.3 KB
Raw
1 """Tests for muse log — commit history display and filters."""
2
3 import pathlib
4
5 import pytest
6 from tests.cli_test_helper import CliRunner
7
8 cli = None # argparse migration — CliRunner ignores this arg
9 from muse.cli.commands.log import _parse_date
10
11 runner = CliRunner()
12
13
14 @pytest.fixture
15 def repo(tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch) -> pathlib.Path:
16 monkeypatch.chdir(tmp_path)
17 monkeypatch.setenv("MUSE_REPO_ROOT", str(tmp_path))
18 result = runner.invoke(cli, ["init"])
19 assert result.exit_code == 0, result.output
20 return tmp_path
21
22
23 def _write(repo: pathlib.Path, filename: str, content: str = "data") -> None:
24 (repo / filename).write_text(content)
25
26
27 def _commit(msg: str, **flags: str) -> None:
28 runner.invoke(cli, ["code", "add", "."])
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.py")
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.py")
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.py")
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.py")
134 _commit("first")
135 _write(repo, "b.py")
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.py")
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.py")
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.py")
157 _commit("first")
158 _write(repo, "b.py")
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.py")
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.py")
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.py")
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.py")
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.py")
199 _commit("add beat")
200 result = runner.invoke(cli, ["log", "--stat"])
201 assert result.exit_code == 0
202 assert "beat.py" 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.py")
207 _commit("add")
208 result = runner.invoke(cli, ["log", "--stat"])
209 assert "added" in result.output
210
211
212 class TestLogFilters:
213 def test_limit_n(self, repo: pathlib.Path) -> None:
214 for i in range(5):
215 _write(repo, f"f{i}.py", str(i))
216 _commit(f"msg-{i}")
217 result = runner.invoke(cli, ["log", "--limit", "2"])
218 assert result.exit_code == 0
219 # With limit 2, we should see the 2 newest but not the oldest
220 assert "msg-0" not in result.output
221 assert "msg-1" not in result.output
222 assert "msg-2" not in result.output
223
224 def test_filter_author(self, repo: pathlib.Path) -> None:
225 _write(repo, "a.py")
226 _commit("by gabriel", author="Gabriel")
227 _write(repo, "b.py")
228 _commit("by alice", author="Alice")
229 result = runner.invoke(cli, ["log", "--author", "Gabriel"])
230 assert result.exit_code == 0
231 assert "by gabriel" in result.output
232 assert "by alice" not in result.output
233
234 def test_filter_author_case_insensitive(self, repo: pathlib.Path) -> None:
235 _write(repo, "a.py")
236 _commit("authored", author="Gabriel")
237 result = runner.invoke(cli, ["log", "--author", "gabriel"])
238 assert "authored" in result.output
239
240 def test_filter_section(self, repo: pathlib.Path) -> None:
241 _write(repo, "a.py")
242 _commit("verse part", section="verse")
243 _write(repo, "b.py")
244 _commit("chorus part", section="chorus")
245 result = runner.invoke(cli, ["log", "--section", "verse"])
246 assert result.exit_code == 0
247 assert "verse part" in result.output
248 assert "chorus part" not in result.output
249
250 def test_filter_track(self, repo: pathlib.Path) -> None:
251 _write(repo, "a.py")
252 _commit("drums commit", track="drums")
253 _write(repo, "b.py")
254 _commit("bass commit", track="bass")
255 result = runner.invoke(cli, ["log", "--track", "drums"])
256 assert "drums commit" in result.output
257 assert "bass commit" not in result.output
258
259 def test_filter_emotion(self, repo: pathlib.Path) -> None:
260 _write(repo, "a.py")
261 _commit("happy commit", emotion="joyful")
262 _write(repo, "b.py")
263 _commit("sad commit", emotion="melancholic")
264 result = runner.invoke(cli, ["log", "--emotion", "joyful"])
265 assert "happy commit" in result.output
266 assert "sad commit" not in result.output
267
268 def test_filter_since_future_returns_nothing(self, repo: pathlib.Path) -> None:
269 _write(repo, "a.py")
270 _commit("old commit")
271 result = runner.invoke(cli, ["log", "--since", "2099-01-01"])
272 assert result.exit_code == 0
273 assert "no commits" in result.output.lower() or "(no commits)" in result.output
274
275 def test_filter_until_past_returns_nothing(self, repo: pathlib.Path) -> None:
276 _write(repo, "a.py")
277 _commit("recent commit")
278 result = runner.invoke(cli, ["log", "--until", "2000-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_no_matches_shows_no_commits(self, repo: pathlib.Path) -> None:
283 _write(repo, "a.py")
284 _commit("only commit", author="Gabriel")
285 result = runner.invoke(cli, ["log", "--author", "nobody"])
286 assert "(no commits)" in result.output
File History 1 commit