ComfyUI/tests-unit/assets_test/test_file_utils.py
Luke Mino-Altherr 7daf360dfa fix: follow symlinks in list_files_recursively with cycle detection
list_files_recursively now uses followlinks=True so symlinked
directories under input/ and output/ roots are traversed, matching
the existing behavior of folder_paths.recursive_search for models.

Tracks (st_dev, st_ino) pairs of visited directories to detect and
break circular symlink loops safely.

Amp-Thread-ID: https://ampcode.com/threads/T-019c9220-21b8-7678-b428-9215ff1bb011
Co-authored-by: Amp <amp@ampcode.com>
2026-02-24 18:57:55 -08:00

122 lines
3.8 KiB
Python

import os
import sys
import pytest
from app.assets.services.file_utils import is_visible, list_files_recursively
class TestIsVisible:
def test_visible_file(self):
assert is_visible("file.txt") is True
def test_hidden_file(self):
assert is_visible(".hidden") is False
def test_hidden_directory(self):
assert is_visible(".git") is False
def test_visible_directory(self):
assert is_visible("src") is True
def test_dotdot_is_hidden(self):
assert is_visible("..") is False
def test_dot_is_hidden(self):
assert is_visible(".") is False
class TestListFilesRecursively:
def test_skips_hidden_files(self, tmp_path):
(tmp_path / "visible.txt").write_text("a")
(tmp_path / ".hidden").write_text("b")
result = list_files_recursively(str(tmp_path))
assert len(result) == 1
assert result[0].endswith("visible.txt")
def test_skips_hidden_directories(self, tmp_path):
hidden_dir = tmp_path / ".hidden_dir"
hidden_dir.mkdir()
(hidden_dir / "file.txt").write_text("a")
visible_dir = tmp_path / "visible_dir"
visible_dir.mkdir()
(visible_dir / "file.txt").write_text("b")
result = list_files_recursively(str(tmp_path))
assert len(result) == 1
assert "visible_dir" in result[0]
assert ".hidden_dir" not in result[0]
def test_empty_directory(self, tmp_path):
result = list_files_recursively(str(tmp_path))
assert result == []
def test_nonexistent_directory(self, tmp_path):
result = list_files_recursively(str(tmp_path / "nonexistent"))
assert result == []
@pytest.mark.skipif(sys.platform == "win32", reason="symlinks need privileges on Windows")
def test_follows_symlinked_directories(self, tmp_path):
target = tmp_path / "real_dir"
target.mkdir()
(target / "model.safetensors").write_text("data")
root = tmp_path / "root"
root.mkdir()
(root / "link").symlink_to(target)
result = list_files_recursively(str(root))
assert len(result) == 1
assert result[0].endswith("model.safetensors")
assert "link" in result[0]
@pytest.mark.skipif(sys.platform == "win32", reason="symlinks need privileges on Windows")
def test_follows_symlinked_files(self, tmp_path):
real_file = tmp_path / "real.txt"
real_file.write_text("content")
root = tmp_path / "root"
root.mkdir()
(root / "link.txt").symlink_to(real_file)
result = list_files_recursively(str(root))
assert len(result) == 1
assert result[0].endswith("link.txt")
@pytest.mark.skipif(sys.platform == "win32", reason="symlinks need privileges on Windows")
def test_circular_symlinks_do_not_loop(self, tmp_path):
dir_a = tmp_path / "a"
dir_a.mkdir()
(dir_a / "file.txt").write_text("a")
# a/b -> a (circular)
(dir_a / "b").symlink_to(dir_a)
result = list_files_recursively(str(dir_a))
assert len(result) == 1
assert result[0].endswith("file.txt")
@pytest.mark.skipif(sys.platform == "win32", reason="symlinks need privileges on Windows")
def test_mutual_circular_symlinks(self, tmp_path):
dir_a = tmp_path / "a"
dir_b = tmp_path / "b"
dir_a.mkdir()
dir_b.mkdir()
(dir_a / "file_a.txt").write_text("a")
(dir_b / "file_b.txt").write_text("b")
# a/link_b -> b and b/link_a -> a
(dir_a / "link_b").symlink_to(dir_b)
(dir_b / "link_a").symlink_to(dir_a)
result = list_files_recursively(str(dir_a))
basenames = sorted(os.path.basename(p) for p in result)
assert "file_a.txt" in basenames
assert "file_b.txt" in basenames