gabriel / muse public
test_core_refs.py python
210 lines 8.1 KB
Raw
sha256:b636f72dcba9e190afb980bece906fa5b717fbde014b76ef023df8cb96e01eb9 docs: expand cache plan with all seven testing tiers and do… Sonnet 4.6 131 days ago
1 """Tests for muse/core/refs.py — canonical ref-file reading primitives.
2
3 Coverage
4 --------
5 read_ref
6 - returns commit ID for a well-formed ref file
7 - returns None for a missing file
8 - returns None for an empty file
9 - returns None for a whitespace-only file
10 - strips leading/trailing whitespace and newlines
11 - returns None on PermissionError (graceful degradation)
12
13 iter_branch_refs
14 - empty heads dir → yields nothing
15 - missing heads dir → yields nothing
16 - single branch → yields (branch_name, commit_id)
17 - multiple branches → yields all (branch_name, commit_id) pairs
18 - skips symlinks
19 - skips non-file entries (subdirectories)
20 - skips empty ref files
21 - each yielded commit_id is non-empty string
22 - lazy — yields incrementally (returns an iterator, not a list)
23 - branch name is the filename, not the full path
24 """
25
26 from __future__ import annotations
27
28 import json
29 import pathlib
30
31 import pytest
32
33 from muse.core.refs import iter_branch_refs, read_ref
34 from muse.core._types import long_id
35
36
37 # ---------------------------------------------------------------------------
38 # Helpers
39 # ---------------------------------------------------------------------------
40
41
42 def _make_repo(tmp_path: pathlib.Path) -> pathlib.Path:
43 muse = tmp_path / ".muse"
44 for d in ("objects", "commits", "snapshots", "refs/heads"):
45 (muse / d).mkdir(parents=True, exist_ok=True)
46 (muse / "repo.json").write_text(json.dumps({"repo_id": "test-repo"}))
47 (muse / "HEAD").write_text("ref: refs/heads/main\n")
48 return tmp_path
49
50
51 def _write_branch_ref(repo: pathlib.Path, branch: str, commit_id: str) -> pathlib.Path:
52 """Write a branch ref file; return the path."""
53 ref_path = repo / ".muse" / "refs" / "heads" / branch
54 ref_path.parent.mkdir(parents=True, exist_ok=True)
55 ref_path.write_text(commit_id + "\n", encoding="utf-8")
56 return ref_path
57
58
59 _FAKE_CID = long_id("ab" * 32)
60 _FAKE_CID2 = long_id("cd" * 32)
61 _FAKE_CID3 = long_id("ef" * 32)
62
63
64 # ---------------------------------------------------------------------------
65 # read_ref
66 # ---------------------------------------------------------------------------
67
68
69 class TestReadRef:
70 def test_returns_commit_id_for_well_formed_ref(self, tmp_path: pathlib.Path) -> None:
71 ref = tmp_path / "myref"
72 ref.write_text(_FAKE_CID + "\n", encoding="utf-8")
73 assert read_ref(ref) == _FAKE_CID
74
75 def test_returns_none_for_missing_file(self, tmp_path: pathlib.Path) -> None:
76 assert read_ref(tmp_path / "nonexistent") is None
77
78 def test_returns_none_for_empty_file(self, tmp_path: pathlib.Path) -> None:
79 ref = tmp_path / "empty"
80 ref.write_text("", encoding="utf-8")
81 assert read_ref(ref) is None
82
83 def test_returns_none_for_whitespace_only(self, tmp_path: pathlib.Path) -> None:
84 ref = tmp_path / "ws"
85 ref.write_text(" \n \n", encoding="utf-8")
86 assert read_ref(ref) is None
87
88 def test_strips_whitespace_and_newlines(self, tmp_path: pathlib.Path) -> None:
89 ref = tmp_path / "ref"
90 ref.write_text(f" {_FAKE_CID} \n", encoding="utf-8")
91 assert read_ref(ref) == _FAKE_CID
92
93 def test_strips_trailing_newline_only(self, tmp_path: pathlib.Path) -> None:
94 ref = tmp_path / "ref"
95 ref.write_text(_FAKE_CID + "\n", encoding="utf-8")
96 assert read_ref(ref) == _FAKE_CID
97
98 def test_returns_none_on_permission_error(self, tmp_path: pathlib.Path) -> None:
99 ref = tmp_path / "locked"
100 ref.write_text(_FAKE_CID, encoding="utf-8")
101 ref.chmod(0o000)
102 try:
103 result = read_ref(ref)
104 assert result is None
105 finally:
106 ref.chmod(0o644)
107
108
109 # ---------------------------------------------------------------------------
110 # iter_branch_refs
111 # ---------------------------------------------------------------------------
112
113
114 class TestIterBranchRefs:
115 def test_missing_heads_dir_yields_nothing(self, tmp_path: pathlib.Path) -> None:
116 repo = tmp_path # no .muse directory at all
117 result = list(iter_branch_refs(repo))
118 assert result == []
119
120 def test_empty_heads_dir_yields_nothing(self, tmp_path: pathlib.Path) -> None:
121 repo = _make_repo(tmp_path)
122 result = list(iter_branch_refs(repo))
123 assert result == []
124
125 def test_single_branch_yields_name_and_commit_id(self, tmp_path: pathlib.Path) -> None:
126 repo = _make_repo(tmp_path)
127 _write_branch_ref(repo, "main", _FAKE_CID)
128 result = list(iter_branch_refs(repo))
129 assert len(result) == 1
130 name, cid = result[0]
131 assert name == "main"
132 assert cid == _FAKE_CID
133
134 def test_multiple_branches_yields_all(self, tmp_path: pathlib.Path) -> None:
135 repo = _make_repo(tmp_path)
136 _write_branch_ref(repo, "main", _FAKE_CID)
137 _write_branch_ref(repo, "dev", _FAKE_CID2)
138 _write_branch_ref(repo, "feat/x", _FAKE_CID3)
139 result = dict(iter_branch_refs(repo))
140 assert result == {"main": _FAKE_CID, "dev": _FAKE_CID2, "feat/x": _FAKE_CID3}
141
142 def test_skips_symlinks(self, tmp_path: pathlib.Path) -> None:
143 repo = _make_repo(tmp_path)
144 _write_branch_ref(repo, "main", _FAKE_CID)
145 heads_dir = repo / ".muse" / "refs" / "heads"
146 symlink = heads_dir / "alias"
147 symlink.symlink_to(heads_dir / "main")
148 names = [name for name, _ in iter_branch_refs(repo)]
149 assert "alias" not in names
150 assert "main" in names
151
152 def test_skips_empty_subdirectories(self, tmp_path: pathlib.Path) -> None:
153 """Empty subdirectories (no ref files) yield nothing for that subtree."""
154 repo = _make_repo(tmp_path)
155 _write_branch_ref(repo, "main", _FAKE_CID)
156 subdir = repo / ".muse" / "refs" / "heads" / "namespace"
157 subdir.mkdir()
158 names = [name for name, _ in iter_branch_refs(repo)]
159 assert "namespace" not in names
160 assert "main" in names
161
162 def test_hierarchical_branch_name_uses_posix_slash(self, tmp_path: pathlib.Path) -> None:
163 """Branch names with slashes (task/foo) are yielded as relative POSIX paths."""
164 repo = _make_repo(tmp_path)
165 _write_branch_ref(repo, "task/my-feature", _FAKE_CID)
166 result = list(iter_branch_refs(repo))
167 assert len(result) == 1
168 name, cid = result[0]
169 assert name == "task/my-feature"
170 assert cid == _FAKE_CID
171
172 def test_skips_empty_ref_files(self, tmp_path: pathlib.Path) -> None:
173 repo = _make_repo(tmp_path)
174 _write_branch_ref(repo, "main", _FAKE_CID)
175 (repo / ".muse" / "refs" / "heads" / "empty-branch").write_text("")
176 result = list(iter_branch_refs(repo))
177 names = [name for name, _ in result]
178 assert "empty-branch" not in names
179 assert "main" in names
180
181 def test_yields_non_empty_commit_ids(self, tmp_path: pathlib.Path) -> None:
182 repo = _make_repo(tmp_path)
183 _write_branch_ref(repo, "main", _FAKE_CID)
184 for name, cid in iter_branch_refs(repo):
185 assert cid # non-empty
186 assert isinstance(cid, str)
187
188 def test_returns_iterator_not_list(self, tmp_path: pathlib.Path) -> None:
189 """iter_branch_refs must return an iterator (lazy), not a pre-built list."""
190 import collections.abc
191 repo = _make_repo(tmp_path)
192 result = iter_branch_refs(repo)
193 assert isinstance(result, collections.abc.Iterator)
194
195 def test_branch_name_is_relative_posix_path(self, tmp_path: pathlib.Path) -> None:
196 """Branch name is relative to heads_dir — not an absolute filesystem path."""
197 repo = _make_repo(tmp_path)
198 _write_branch_ref(repo, "simple", _FAKE_CID)
199 result = list(iter_branch_refs(repo))
200 assert len(result) == 1
201 name, _ = result[0]
202 assert name == "simple"
203 assert str(tmp_path) not in name
204
205 def test_uses_generator_not_list_comprehension(self) -> None:
206 """iter_branch_refs must use a generator (structural check)."""
207 import inspect
208 from muse.core import refs as refs_module
209 source = inspect.getsource(refs_module.iter_branch_refs)
210 assert "yield" in source, "iter_branch_refs must use yield (generator)"
File History 2 commits
sha256:b636f72dcba9e190afb980bece906fa5b717fbde014b76ef023df8cb96e01eb9 docs: expand cache plan with all seven testing tiers and do… Sonnet 4.6 131 days ago
sha256:7f9e2ef5286aedad9c1e6011b4c46ca27f39dbdad6e3409357e36b26e46b3b7c docs: docstring sprint for-each-ref→hotspots — idiomatic ru… Sonnet 4.6 patch 137 days ago