|
6 | 6 |
|
7 | 7 | from src.tools import list_directory |
8 | 8 |
|
9 | | -# --------------------------------------------------------------------------- |
10 | | -# Helpers |
11 | | -# --------------------------------------------------------------------------- |
12 | | - |
13 | | - |
14 | | -def _make_tree(base: Path, structure: dict) -> None: |
15 | | - """Recursively create files/dirs described by *structure*. |
16 | | -
|
17 | | - Keys ending in '/' are directories; other keys are files. |
18 | | - Values for files are the byte-string content; values for dirs are |
19 | | - nested structure dicts. |
20 | | - """ |
21 | | - for name, content in structure.items(): |
22 | | - if name.endswith("/"): |
23 | | - child = base / name.rstrip("/") |
24 | | - child.mkdir(parents=True, exist_ok=True) |
25 | | - if isinstance(content, dict): |
26 | | - _make_tree(child, content) |
27 | | - else: |
28 | | - (base / name).write_bytes( |
29 | | - content if isinstance(content, bytes) else content.encode() |
30 | | - ) |
31 | | - |
32 | | - |
33 | 9 | # --------------------------------------------------------------------------- |
34 | 10 | # TestListDirectoryErrors |
35 | 11 | # --------------------------------------------------------------------------- |
@@ -181,6 +157,36 @@ def test_default_ignores_dotgit(self, tmp_path: Path) -> None: |
181 | 157 | names = [e["name"] for e in result["entries"]] |
182 | 158 | assert ".git" not in names |
183 | 159 |
|
| 160 | + def test_file_named_like_ignored_pattern_is_not_skipped( |
| 161 | + self, tmp_path: Path |
| 162 | + ) -> None: |
| 163 | + """A file named '__pycache__' must still appear in listings.""" |
| 164 | + (tmp_path / "__pycache__").write_text("I am a file") |
| 165 | + result = list_directory(str(tmp_path)) |
| 166 | + names = [e["name"] for e in result["entries"]] |
| 167 | + assert "__pycache__" in names |
| 168 | + |
| 169 | + def test_dir_named_like_ignored_pattern_is_still_skipped( |
| 170 | + self, tmp_path: Path |
| 171 | + ) -> None: |
| 172 | + """A directory named '__pycache__' must be skipped.""" |
| 173 | + (tmp_path / "__pycache__").mkdir() |
| 174 | + result = list_directory(str(tmp_path)) |
| 175 | + names = [e["name"] for e in result["entries"]] |
| 176 | + assert "__pycache__" not in names |
| 177 | + |
| 178 | + def test_child_count_includes_file_named_like_ignored_pattern( |
| 179 | + self, tmp_path: Path |
| 180 | + ) -> None: |
| 181 | + """child_count must count files whose name matches an ignored pattern.""" |
| 182 | + parent = tmp_path / "parent" |
| 183 | + parent.mkdir() |
| 184 | + (parent / "__pycache__").write_text("I am a file") |
| 185 | + (parent / "regular.txt").write_text("ok") |
| 186 | + result = list_directory(str(tmp_path)) |
| 187 | + entry = next(e for e in result["entries"] if e["name"] == "parent") |
| 188 | + assert entry["child_count"] == 2 |
| 189 | + |
184 | 190 | def test_empty_env_var_disables_all_ignore_patterns(self, tmp_path: Path) -> None: |
185 | 191 | """LARGEFILE_IGNORED_DIR_PATTERNS='' must not produce a spurious [''] pattern.""" |
186 | 192 | with patch.dict(os.environ, {"LARGEFILE_IGNORED_DIR_PATTERNS": ""}): |
@@ -283,14 +289,33 @@ def test_no_truncation_when_under_limit(self, tmp_path: Path) -> None: |
283 | 289 |
|
284 | 290 |
|
285 | 291 | class TestListDirectoryErrorPaths: |
286 | | - def test_permission_error_on_root_scan_returns_empty(self, tmp_path: Path) -> None: |
287 | | - """PermissionError on os.scandir(dir_path) returns empty entries (lines 698-699).""" |
| 292 | + def test_permission_error_on_root_scan_returns_error(self, tmp_path: Path) -> None: |
| 293 | + """PermissionError on os.scandir(dir_path) at root depth returns error dict.""" |
288 | 294 | with patch("src.tools.os.scandir", side_effect=PermissionError): |
289 | 295 | result = list_directory(str(tmp_path)) |
290 | | - assert result["entries"] == [] |
291 | | - assert result["total_files"] == 0 |
292 | | - assert result["total_dirs"] == 0 |
293 | | - assert result["truncated"] is False |
| 296 | + assert "error" in result |
| 297 | + assert "Permission denied" in result["error"] |
| 298 | + |
| 299 | + def test_permission_error_on_nested_scan_returns_empty( |
| 300 | + self, tmp_path: Path |
| 301 | + ) -> None: |
| 302 | + """PermissionError on a nested directory returns empty children gracefully.""" |
| 303 | + sub = tmp_path / "restricted" |
| 304 | + sub.mkdir() |
| 305 | + |
| 306 | + real_scandir = os.scandir |
| 307 | + call_count = {"n": 0} |
| 308 | + |
| 309 | + def patched_scandir(path: str) -> object: |
| 310 | + call_count["n"] += 1 |
| 311 | + if call_count["n"] == 3: # third call: recursing into restricted/ |
| 312 | + raise PermissionError("access denied") |
| 313 | + return real_scandir(path) |
| 314 | + |
| 315 | + with patch("src.tools.os.scandir", side_effect=patched_scandir): |
| 316 | + result = list_directory(str(tmp_path), max_depth=2) |
| 317 | + assert "error" not in result |
| 318 | + assert result["total_dirs"] == 1 |
294 | 319 |
|
295 | 320 | def test_recursive_truncation_stops_parent_iteration(self, tmp_path: Path) -> None: |
296 | 321 | """counter.truncated=True from recursive call breaks parent loop (line 703).""" |
|
0 commit comments