66import sys
77import tempfile
88import logging
9+ from unittest .mock import patch
10+ from contextlib import contextmanager
911
1012# Add parent directory to path to import relink module
1113sys .path .insert (
1517import relink # noqa: E402
1618
1719
20+ class MockDirEntry :
21+ """Wrapper for DirEntry that allows mocking stat() for specific files."""
22+
23+ # pylint: disable=missing-function-docstring
24+
25+ def __init__ (self , entry , uid_override = None ):
26+ """
27+ Initialize MockDirEntry.
28+
29+ Args:
30+ entry: The original DirEntry object.
31+ uid_override: Dict mapping filename to UID to override in stat results.
32+ """
33+ self ._entry = entry
34+ self ._uid_override = uid_override or {}
35+
36+ def __getattr__ (self , name ):
37+ return getattr (self ._entry , name )
38+
39+ def stat (self , * args , ** kwargs ):
40+ stat_result = self ._entry .stat (* args , ** kwargs )
41+ if self ._entry .name in self ._uid_override :
42+ # Create a modified stat result with different UID
43+ modified_stat = os .stat_result (
44+ (
45+ stat_result .st_mode ,
46+ stat_result .st_ino ,
47+ stat_result .st_dev ,
48+ stat_result .st_nlink ,
49+ self ._uid_override [self ._entry .name ], # Override UID
50+ stat_result .st_gid ,
51+ stat_result .st_size ,
52+ stat_result .st_atime ,
53+ stat_result .st_mtime ,
54+ stat_result .st_ctime ,
55+ )
56+ )
57+ return modified_stat
58+ return stat_result
59+
60+ def is_file (self , * args , ** kwargs ):
61+ return self ._entry .is_file (* args , ** kwargs )
62+
63+ def is_dir (self , * args , ** kwargs ):
64+ return self ._entry .is_dir (* args , ** kwargs )
65+
66+ def is_symlink (self ):
67+ return self ._entry .is_symlink ()
68+
69+
70+ def create_mock_scandir (uid_override = None ):
71+ """
72+ Create a mock scandir function that wraps entries with MockDirEntry.
73+
74+ Args:
75+ uid_override: Dict mapping filename to UID to override in stat results.
76+
77+ Returns:
78+ A context manager function that can be used with patch.
79+ """
80+ original_scandir = os .scandir
81+
82+ @contextmanager
83+ def mock_scandir (path ):
84+ with original_scandir (path ) as entries :
85+ wrapped_entries = [MockDirEntry (entry , uid_override ) for entry in entries ]
86+ yield iter (wrapped_entries )
87+
88+ return mock_scandir
89+
90+
1891def test_find_owned_files_basic (temp_dirs ):
1992 """Test basic functionality: find files owned by user."""
2093 source_dir , _ = temp_dirs
@@ -82,7 +155,7 @@ def test_skip_symlinks(temp_dirs, caplog):
82155 os .symlink (dummy_target , symlink_path )
83156
84157 # Find owned files with logging
85- with caplog .at_level (logging .INFO ):
158+ with caplog .at_level (logging .DEBUG ):
86159 found_files = list (relink .find_owned_files_scandir (source_dir , user_uid ))
87160
88161 # Verify only regular file was found
@@ -95,6 +168,48 @@ def test_skip_symlinks(temp_dirs, caplog):
95168 assert symlink_path in caplog .text
96169
97170
171+ def test_skip_symlinks_owned_by_different_user (temp_dirs , caplog ):
172+ """Test that symlinks owned by different users are not logged.
173+
174+ Since find_owned_files_scandir filters by UID first, symlinks owned
175+ by other users should never reach the symlink check and thus should
176+ not generate a "Skipping symlink" log message.
177+ """
178+ source_dir , _ = temp_dirs
179+ user_uid = os .stat (source_dir ).st_uid
180+
181+ # Use a different UID
182+ different_uid = user_uid + 1000
183+
184+ # Create a regular file owned by current user
185+ regular_file = os .path .join (source_dir , "regular.txt" )
186+ with open (regular_file , "w" , encoding = "utf-8" ) as f :
187+ f .write ("content" )
188+
189+ # Create a symlink
190+ symlink_path = os .path .join (source_dir , "other_user_link.txt" )
191+ dummy_target = os .path .join (tempfile .gettempdir (), "somewhere" )
192+ os .symlink (dummy_target , symlink_path )
193+
194+ # Mock DirEntry.stat to return different UID for the symlink
195+ uid_override = {"other_user_link.txt" : different_uid }
196+ mock_scandir = create_mock_scandir (uid_override )
197+
198+ with patch ("os.scandir" , side_effect = mock_scandir ):
199+ with caplog .at_level (logging .INFO ):
200+ found_files = list (relink .find_owned_files_scandir (source_dir , user_uid ))
201+
202+ # Verify only regular file was found
203+ assert len (found_files ) == 1
204+ assert regular_file in found_files
205+ assert symlink_path not in found_files
206+
207+ # Check that "Skipping symlink" message was NOT logged for the other user's symlink
208+ # (it should be filtered out by UID check before reaching symlink check)
209+ if "Skipping symlink:" in caplog .text :
210+ assert symlink_path not in caplog .text
211+
212+
98213def test_empty_directory (temp_dirs ):
99214 """Test with empty directory."""
100215 source_dir , _ = temp_dirs
0 commit comments