Skip to content

Commit af221e5

Browse files
authored
Merge pull request #13 from samsrabin/reduce-noise
Reduce noise
2 parents 0027181 + 9319a84 commit af221e5

3 files changed

Lines changed: 128 additions & 14 deletions

File tree

relink.py

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -52,21 +52,20 @@ def find_owned_files_scandir(directory, user_uid):
5252
with os.scandir(directory) as entries:
5353
for entry in entries:
5454
try:
55-
# Check if it's a file (not following symlinks)
56-
if entry.is_file(follow_symlinks=False):
57-
# Get stat info (cached by scandir, very efficient)
58-
stat_info = entry.stat(follow_symlinks=False)
59-
60-
if stat_info.st_uid == user_uid:
61-
yield entry.path
62-
6355
# Recursively process directories (not following symlinks)
64-
elif entry.is_dir(follow_symlinks=False):
56+
if entry.is_dir(follow_symlinks=False):
6557
yield from find_owned_files_scandir(entry.path, user_uid)
6658

67-
# Skip symlinks
68-
elif entry.is_symlink():
69-
logger.info("Skipping symlink: %s", entry.path)
59+
# Is this owned by the user?
60+
elif entry.stat(follow_symlinks=False).st_uid == user_uid:
61+
62+
# Return if it's a file (not following symlinks)
63+
if entry.is_file(follow_symlinks=False):
64+
yield entry.path
65+
66+
# Skip symlinks
67+
elif entry.is_symlink():
68+
logger.debug("Skipping symlink: %s", entry.path)
7069

7170
except (OSError, PermissionError) as e:
7271
logger.debug("Error accessing %s: %s. Skipping.", entry.path, e)

tests/relink/test_find_owned_files_scandir.py

Lines changed: 116 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
import sys
77
import tempfile
88
import logging
9+
from unittest.mock import patch
10+
from contextlib import contextmanager
911

1012
# Add parent directory to path to import relink module
1113
sys.path.insert(
@@ -15,6 +17,77 @@
1517
import 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+
1891
def 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+
98213
def test_empty_directory(temp_dirs):
99214
"""Test with empty directory."""
100215
source_dir, _ = temp_dirs

tests/relink/test_replace_files_with_symlinks.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ def test_skip_existing_symlinks(temp_dirs, current_user, caplog):
8888
stat_before = os.lstat(source_link)
8989

9090
# Run the function
91-
with caplog.at_level(logging.INFO):
91+
with caplog.at_level(logging.DEBUG):
9292
relink.replace_files_with_symlinks(source_dir, target_dir, username)
9393

9494
# Verify the symlink is unchanged (same inode means it wasn't deleted/recreated)

0 commit comments

Comments
 (0)