From 8899dce900368ca4cd9eb1896d1c6c8240b8f265 Mon Sep 17 00:00:00 2001 From: Sam Rabin Date: Thu, 15 Jan 2026 14:47:58 -0700 Subject: [PATCH 1/2] relink.py: Timing now shows in quiet mode. --- relink.py | 16 +++++++++++++++- tests/relink/test_timing.py | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/relink.py b/relink.py index 67bcc8b..4ae1004 100644 --- a/relink.py +++ b/relink.py @@ -19,6 +19,20 @@ # Set up logger logger = logging.getLogger(__name__) +# Define a custom log level that always prints +ALWAYS = logging.CRITICAL * 2 +logging.addLevelName(ALWAYS, "ALWAYS") + + +def always(self, message, *args, **kwargs): + """Log message that always appears regardless of log level.""" + if self.isEnabledFor(ALWAYS): + # pylint: disable=protected-access + self._log(ALWAYS, message, args, **kwargs) + + +logging.Logger.always = always + def find_and_replace_owned_files(source_dir, target_dir, username, dry_run=False): """ @@ -234,7 +248,7 @@ def main(): if args.timing: elapsed_time = time.time() - start_time - logger.info("Execution time: %.2f seconds", elapsed_time) + logger.always("Execution time: %.2f seconds", elapsed_time) if __name__ == "__main__": diff --git a/tests/relink/test_timing.py b/tests/relink/test_timing.py index a60e949..e071944 100644 --- a/tests/relink/test_timing.py +++ b/tests/relink/test_timing.py @@ -56,3 +56,40 @@ def test_timing_logging(tmp_path, caplog, use_timing, should_log_timing): assert "seconds" in caplog.text else: assert "Execution time:" not in caplog.text + + +def test_timing_shows_in_quiet_mode(tmp_path, caplog): + """Test that timing message is shown even when --quiet flag is used.""" + # Create real directories + source_dir = tmp_path / "source" + target_dir = tmp_path / "target" + source_dir.mkdir() + target_dir.mkdir() + + # Create a file + source_file = source_dir / "test_file.txt" + target_file = target_dir / "test_file.txt" + source_file.write_text("source") + target_file.write_text("target") + + # Build argv with both --timing and --quiet flags + test_argv = [ + "relink.py", + "--source-root", + str(source_dir), + "--target-root", + str(target_dir), + "--timing", + "--quiet", + ] + + with patch("sys.argv", test_argv): + with caplog.at_level(logging.WARNING): + # Call main() which includes the timing logic + relink.main() + + # Verify timing message appears even in quiet mode + assert "Execution time:" in caplog.text + assert "seconds" in caplog.text + # Verify that INFO messages are suppressed + assert "Searching for files owned by" not in caplog.text From ecc9a4a5dd3a4e37b7bccd58acfd6f3285d5f99e Mon Sep 17 00:00:00 2001 From: Sam Rabin Date: Thu, 15 Jan 2026 15:13:56 -0700 Subject: [PATCH 2/2] relink.py: Use os.scandir() instead of os.walk() for efficiency. os.scandir() caches stat information during directory traversal, so this reduces system calls. Results in a speedup of about 84% on Derecho, from ~230 seconds to ~37 (n=1 of each). --- relink.py | 156 ++++++++------ tests/relink/test_dryrun.py | 6 +- tests/relink/test_find_owned_files_scandir.py | 199 ++++++++++++++++++ ...py => test_replace_files_with_symlinks.py} | 61 ++---- tests/relink/test_verbosity.py | 10 +- 5 files changed, 313 insertions(+), 119 deletions(-) create mode 100644 tests/relink/test_find_owned_files_scandir.py rename tests/relink/{test_find_and_replace_owned_files.py => test_replace_files_with_symlinks.py} (83%) diff --git a/relink.py b/relink.py index 4ae1004..4778ad7 100644 --- a/relink.py +++ b/relink.py @@ -34,7 +34,49 @@ def always(self, message, *args, **kwargs): logging.Logger.always = always -def find_and_replace_owned_files(source_dir, target_dir, username, dry_run=False): +def find_owned_files_scandir(directory, user_uid): + """ + Efficiently find all files owned by a specific user using os.scandir(). + + This is more efficient than os.walk() because os.scandir() caches stat + information during directory traversal, reducing system calls. + + Args: + directory (str): The root directory to search. + user_uid (int): The UID of the user whose files to find. + + Yields: + str: Absolute paths to files owned by the user. + """ + try: + with os.scandir(directory) as entries: + for entry in entries: + try: + # Check if it's a file (not following symlinks) + if entry.is_file(follow_symlinks=False): + # Get stat info (cached by scandir, very efficient) + stat_info = entry.stat(follow_symlinks=False) + + if stat_info.st_uid == user_uid: + yield entry.path + + # Recursively process directories (not following symlinks) + elif entry.is_dir(follow_symlinks=False): + yield from find_owned_files_scandir(entry.path, user_uid) + + # Skip symlinks + elif entry.is_symlink(): + logger.info("Skipping symlink: %s", entry.path) + + except (OSError, PermissionError) as e: + logger.debug("Error accessing %s: %s. Skipping.", entry.path, e) + continue + + except (OSError, PermissionError) as e: + logger.debug("Error accessing %s: %s. Skipping.", directory, e) + + +def replace_files_with_symlinks(source_dir, target_dir, username, dry_run=False): """ Finds files owned by a specific user in a source directory tree, deletes them, and replaces them with symbolic links to the same @@ -66,70 +108,52 @@ def find_and_replace_owned_files(source_dir, target_dir, username, dry_run=False source_dir, ) - for dirpath, _, filenames in os.walk(source_dir): - for filename in filenames: - file_path = os.path.join(dirpath, filename) - - # Use os.stat().st_uid to get the file's owner UID - try: - if os.path.islink(file_path): - logger.info("Skipping symlink: %s", file_path) - continue - - file_uid = os.stat(file_path).st_uid - except FileNotFoundError: - continue # Skip if file was deleted during traversal - - if file_uid == user_uid: - logger.info("Found owned file: %s", file_path) - - # Determine the relative path and the new link's destination - relative_path = os.path.relpath(file_path, source_dir) - link_target = os.path.join(target_dir, relative_path) - - # Check if the target file actually exists - if not os.path.exists(link_target): - logger.warning( - "Warning: Corresponding file not found in '%s' " - "for '%s'. Skipping.", - target_dir, - file_path, - ) - continue - - # Get the link name - link_name = file_path - - if dry_run: - logger.info( - "[DRY RUN] Would create symbolic link: %s -> %s", - link_name, - link_target, - ) - continue - - # Remove the original file - try: - os.rename(link_name, link_name + ".tmp") - logger.info("Deleted original file: %s", link_name) - except OSError as e: - logger.error("Error deleting file %s: %s. Skipping.", link_name, e) - continue - - # Create the symbolic link, handling necessary parent directories - try: - # Create parent directories for the link if they don't exist - os.makedirs(os.path.dirname(link_name), exist_ok=True) - os.symlink(link_target, link_name) - os.remove(link_name + ".tmp") - logger.info( - "Created symbolic link: %s -> %s", link_name, link_target - ) - except OSError as e: - os.rename(link_name + ".tmp", link_name) - logger.error( - "Error creating symlink for %s: %s. Skipping.", link_name, e - ) + # Use efficient scandir-based search + for file_path in find_owned_files_scandir(source_dir, user_uid): + logger.info("Found owned file: %s", file_path) + + # Determine the relative path and the new link's destination + relative_path = os.path.relpath(file_path, source_dir) + link_target = os.path.join(target_dir, relative_path) + + # Check if the target file actually exists + if not os.path.exists(link_target): + logger.warning( + "Warning: Corresponding file not found in '%s' for '%s'. Skipping.", + target_dir, + file_path, + ) + continue + + # Get the link name + link_name = file_path + + if dry_run: + logger.info( + "[DRY RUN] Would create symbolic link: %s -> %s", + link_name, + link_target, + ) + continue + + # Remove the original file + try: + os.rename(link_name, link_name + ".tmp") + logger.info("Deleted original file: %s", link_name) + except OSError as e: + logger.error("Error deleting file %s: %s. Skipping.", link_name, e) + continue + + # Create the symbolic link, handling necessary parent directories + try: + # Create parent directories for the link if they don't exist + os.makedirs(os.path.dirname(link_name), exist_ok=True) + os.symlink(link_target, link_name) + os.remove(link_name + ".tmp") + logger.info("Created symbolic link: %s -> %s", link_name, link_target) + except OSError as e: + os.rename(link_name + ".tmp", link_name) + logger.error("Error creating symlink for %s: %s. Skipping.", link_name, e) def validate_directory(path): @@ -242,7 +266,7 @@ def main(): start_time = time.time() # --- Execution --- - find_and_replace_owned_files( + replace_files_with_symlinks( args.source_root, args.target_root, my_username, dry_run=args.dry_run ) diff --git a/tests/relink/test_dryrun.py b/tests/relink/test_dryrun.py index c844d56..b18c676 100644 --- a/tests/relink/test_dryrun.py +++ b/tests/relink/test_dryrun.py @@ -45,7 +45,7 @@ def test_dry_run_no_changes(dry_run_setup, caplog): # Run in dry-run mode with caplog.at_level(logging.INFO): - relink.find_and_replace_owned_files( + relink.replace_files_with_symlinks( source_dir, target_dir, username, dry_run=True ) @@ -63,7 +63,7 @@ def test_dry_run_shows_message(dry_run_setup, caplog): # Run in dry-run mode with caplog.at_level(logging.INFO): - relink.find_and_replace_owned_files( + relink.replace_files_with_symlinks( source_dir, target_dir, username, dry_run=True ) @@ -79,7 +79,7 @@ def test_dry_run_no_delete_or_create_messages(dry_run_setup, caplog): # Run in dry-run mode with caplog.at_level(logging.INFO): - relink.find_and_replace_owned_files( + relink.replace_files_with_symlinks( source_dir, target_dir, username, dry_run=True ) diff --git a/tests/relink/test_find_owned_files_scandir.py b/tests/relink/test_find_owned_files_scandir.py new file mode 100644 index 0000000..d0718a3 --- /dev/null +++ b/tests/relink/test_find_owned_files_scandir.py @@ -0,0 +1,199 @@ +""" +Tests of find_owned_files_scandir() in relink.py +""" + +import os +import sys +import tempfile +import logging + +# Add parent directory to path to import relink module +sys.path.insert( + 0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +) +# pylint: disable=wrong-import-position +import relink # noqa: E402 + + +def test_find_owned_files_basic(temp_dirs): + """Test basic functionality: find files owned by user.""" + source_dir, _ = temp_dirs + user_uid = os.stat(source_dir).st_uid + + # Create files + file1 = os.path.join(source_dir, "file1.txt") + file2 = os.path.join(source_dir, "file2.txt") + + with open(file1, "w", encoding="utf-8") as f: + f.write("content1") + with open(file2, "w", encoding="utf-8") as f: + f.write("content2") + + # Find owned files + found_files = list(relink.find_owned_files_scandir(source_dir, user_uid)) + + # Verify both files were found + assert len(found_files) == 2 + assert file1 in found_files + assert file2 in found_files + + +def test_find_owned_files_nested(temp_dirs): + """Test finding files in nested directory structures.""" + source_dir, _ = temp_dirs + user_uid = os.stat(source_dir).st_uid + + # Create nested directories + nested_path = os.path.join("subdir1", "subdir2") + os.makedirs(os.path.join(source_dir, nested_path)) + + # Create files at different levels + file1 = os.path.join(source_dir, "root_file.txt") + file2 = os.path.join(source_dir, "subdir1", "level1_file.txt") + file3 = os.path.join(source_dir, nested_path, "level2_file.txt") + + for f in [file1, file2, file3]: + with open(f, "w", encoding="utf-8") as fp: + fp.write("content") + + # Find owned files + found_files = list(relink.find_owned_files_scandir(source_dir, user_uid)) + + # Verify all files were found + assert len(found_files) == 3 + assert file1 in found_files + assert file2 in found_files + assert file3 in found_files + + +def test_skip_symlinks(temp_dirs, caplog): + """Test that symlinks are skipped and logged.""" + source_dir, _ = temp_dirs + user_uid = os.stat(source_dir).st_uid + + # Create a regular file + regular_file = os.path.join(source_dir, "regular.txt") + with open(regular_file, "w", encoding="utf-8") as f: + f.write("content") + + # Create a symlink + symlink_path = os.path.join(source_dir, "link.txt") + dummy_target = os.path.join(tempfile.gettempdir(), "somewhere") + os.symlink(dummy_target, symlink_path) + + # Find owned files with logging + with caplog.at_level(logging.INFO): + found_files = list(relink.find_owned_files_scandir(source_dir, user_uid)) + + # Verify only regular file was found + assert len(found_files) == 1 + assert regular_file in found_files + assert symlink_path not in found_files + + # Check that "Skipping symlink" message was logged + assert "Skipping symlink:" in caplog.text + assert symlink_path in caplog.text + + +def test_empty_directory(temp_dirs): + """Test with empty directory.""" + source_dir, _ = temp_dirs + user_uid = os.stat(source_dir).st_uid + + # Find owned files in empty directory + found_files = list(relink.find_owned_files_scandir(source_dir, user_uid)) + + # Should return empty list + assert len(found_files) == 0 + + +def test_permission_error_handling(temp_dirs, caplog): + """Test that permission errors are handled gracefully.""" + source_dir, _ = temp_dirs + user_uid = os.stat(source_dir).st_uid + + # Create a file + file1 = os.path.join(source_dir, "accessible.txt") + with open(file1, "w", encoding="utf-8") as f: + f.write("content") + + # Create a subdirectory + subdir = os.path.join(source_dir, "subdir") + os.makedirs(subdir) + file2 = os.path.join(subdir, "file_in_subdir.txt") + with open(file2, "w", encoding="utf-8") as f: + f.write("content") + + # Remove read permission from subdirectory + os.chmod(subdir, 0o000) + + try: + # Find owned files with debug logging + with caplog.at_level(logging.DEBUG): + found_files = list(relink.find_owned_files_scandir(source_dir, user_uid)) + + # Should find the accessible file but skip the inaccessible directory + assert file1 in found_files + assert file2 not in found_files + + # Check that error was logged at DEBUG level + assert "Error accessing" in caplog.text + finally: + # Restore permissions for cleanup + os.chmod(subdir, 0o755) + + +def test_only_files_not_directories(temp_dirs): + """Test that only files are returned, not directories.""" + source_dir, _ = temp_dirs + user_uid = os.stat(source_dir).st_uid + + # Create files and directories + file1 = os.path.join(source_dir, "file.txt") + with open(file1, "w", encoding="utf-8") as f: + f.write("content") + + subdir = os.path.join(source_dir, "subdir") + os.makedirs(subdir) + + # Find owned files + found_files = list(relink.find_owned_files_scandir(source_dir, user_uid)) + + # Should only find the file, not the directory + assert len(found_files) == 1 + assert file1 in found_files + assert subdir not in found_files + + +def test_does_not_follow_symlink_directories(temp_dirs): + """Test that symlinked directories are not followed.""" + source_dir, _ = temp_dirs + user_uid = os.stat(source_dir).st_uid + + # Create a real directory with a file + real_dir = os.path.join(source_dir, "real_dir") + os.makedirs(real_dir) + file_in_real = os.path.join(real_dir, "file.txt") + with open(file_in_real, "w", encoding="utf-8") as f: + f.write("content") + + # Create a symlink to a directory outside source_dir + external_dir = tempfile.mkdtemp() + try: + external_file = os.path.join(external_dir, "external.txt") + with open(external_file, "w", encoding="utf-8") as f: + f.write("external content") + + symlink_dir = os.path.join(source_dir, "link_to_external") + os.symlink(external_dir, symlink_dir) + + # Find owned files + found_files = list(relink.find_owned_files_scandir(source_dir, user_uid)) + + # Should find file in real directory but not in symlinked directory + assert file_in_real in found_files + assert external_file not in found_files + finally: + # Cleanup + os.remove(external_file) + os.rmdir(external_dir) diff --git a/tests/relink/test_find_and_replace_owned_files.py b/tests/relink/test_replace_files_with_symlinks.py similarity index 83% rename from tests/relink/test_find_and_replace_owned_files.py rename to tests/relink/test_replace_files_with_symlinks.py index 781a0f7..3d670fe 100644 --- a/tests/relink/test_find_and_replace_owned_files.py +++ b/tests/relink/test_replace_files_with_symlinks.py @@ -1,5 +1,5 @@ """ -Tests of find_and_replace_owned_files() in relink.py +Tests of replace_files_with_symlinks() in relink.py """ import os @@ -33,7 +33,7 @@ def test_basic_file_replacement(temp_dirs, current_user): f.write("target content") # Run the function - relink.find_and_replace_owned_files(source_dir, target_dir, username) + relink.replace_files_with_symlinks(source_dir, target_dir, username) # Verify the source file is now a symlink assert os.path.islink(source_file), "Source file should be a symlink" @@ -62,7 +62,7 @@ def test_nested_directory_structure(temp_dirs, current_user): f.write("nested target") # Run the function - relink.find_and_replace_owned_files(source_dir, target_dir, username) + relink.replace_files_with_symlinks(source_dir, target_dir, username) # Verify assert os.path.islink(source_file), "Nested file should be a symlink" @@ -89,7 +89,7 @@ def test_skip_existing_symlinks(temp_dirs, current_user, caplog): # Run the function with caplog.at_level(logging.INFO): - relink.find_and_replace_owned_files(source_dir, target_dir, username) + relink.replace_files_with_symlinks(source_dir, target_dir, username) # Verify the symlink is unchanged (same inode means it wasn't deleted/recreated) stat_after = os.lstat(source_link) @@ -120,7 +120,7 @@ def test_missing_target_file(temp_dirs, current_user, caplog): # Run the function with caplog.at_level(logging.INFO): - relink.find_and_replace_owned_files(source_dir, target_dir, username) + relink.replace_files_with_symlinks(source_dir, target_dir, username) # Verify the file is NOT converted to symlink assert not os.path.islink(source_file), "File should not be a symlink" @@ -145,7 +145,7 @@ def test_invalid_username(temp_dirs, caplog): # Run the function with caplog.at_level(logging.INFO): - relink.find_and_replace_owned_files(source_dir, target_dir, invalid_username) + relink.replace_files_with_symlinks(source_dir, target_dir, invalid_username) # Check error message assert "Error: User" in caplog.text @@ -168,7 +168,7 @@ def test_multiple_files(temp_dirs, current_user): f.write(f"target {i}") # Run the function - relink.find_and_replace_owned_files(source_dir, target_dir, username) + relink.replace_files_with_symlinks(source_dir, target_dir, username) # Verify all files are symlinks for i in range(5): @@ -200,7 +200,7 @@ def test_absolute_paths(temp_dirs, current_user): rel_target = os.path.basename(target_dir) # Run with relative paths - relink.find_and_replace_owned_files(rel_source, rel_target, username) + relink.replace_files_with_symlinks(rel_source, rel_target, username) # Verify it still works assert os.path.islink(source_file) @@ -215,7 +215,7 @@ def test_print_searching_message(temp_dirs, current_user, caplog): # Run the function with caplog.at_level(logging.INFO): - relink.find_and_replace_owned_files(source_dir, target_dir, username) + relink.replace_files_with_symlinks(source_dir, target_dir, username) # Check that searching message was logged assert f"Searching for files owned by '{username}'" in caplog.text @@ -238,7 +238,7 @@ def test_print_found_owned_file(temp_dirs, current_user, caplog): # Run the function with caplog.at_level(logging.INFO): - relink.find_and_replace_owned_files(source_dir, target_dir, username) + relink.replace_files_with_symlinks(source_dir, target_dir, username) # Check that "Found owned file" message was logged assert "Found owned file:" in caplog.text @@ -261,7 +261,7 @@ def test_print_deleted_and_created_messages(temp_dirs, current_user, caplog): # Run the function with caplog.at_level(logging.INFO): - relink.find_and_replace_owned_files(source_dir, target_dir, username) + relink.replace_files_with_symlinks(source_dir, target_dir, username) # Check messages assert "Deleted original file:" in caplog.text @@ -269,35 +269,6 @@ def test_print_deleted_and_created_messages(temp_dirs, current_user, caplog): assert f"{source_file} -> {target_file}" in caplog.text -def test_handles_file_deleted_during_traversal(temp_dirs, current_user, caplog): - """Test that FileNotFoundError during stat is handled gracefully.""" - source_dir, target_dir = temp_dirs - username = current_user - - # Create files - source_file = os.path.join(source_dir, "disappearing.txt") - with open(source_file, "w", encoding="utf-8") as f: - f.write("content") - - # Mock os.stat to raise FileNotFoundError for this specific file - original_stat = os.stat - - def mock_stat(path, *args, **kwargs): - if path == source_file: - raise FileNotFoundError(f"Simulated: {path} deleted during traversal") - return original_stat(path, *args, **kwargs) - - with patch("os.stat", side_effect=mock_stat): - with caplog.at_level(logging.INFO): - # Should not crash, should continue processing - relink.find_and_replace_owned_files(source_dir, target_dir, username) - - # Should complete without errors (file was skipped) - # No error message should be logged (it's silently skipped via continue) - assert "Error" not in caplog.text - assert "disappearing.txt" not in caplog.text - - def test_error_creating_symlink(temp_dirs, caplog): """Test error message when symlink creation fails.""" source_dir, target_dir = temp_dirs @@ -319,7 +290,7 @@ def mock_symlink(src, dst): with patch("os.symlink", side_effect=mock_symlink): # Run the function with caplog.at_level(logging.INFO): - relink.find_and_replace_owned_files(source_dir, target_dir, username) + relink.replace_files_with_symlinks(source_dir, target_dir, username) # Check error message assert "Error creating symlink" in caplog.text @@ -332,7 +303,7 @@ def test_empty_directories(temp_dirs): username = os.environ["USER"] # Run with empty directories (should not crash) - relink.find_and_replace_owned_files(source_dir, target_dir, username) + relink.replace_files_with_symlinks(source_dir, target_dir, username) # Should complete without errors assert True @@ -353,7 +324,7 @@ def test_file_with_spaces_in_name(temp_dirs): f.write("target content") # Run the function - relink.find_and_replace_owned_files(source_dir, target_dir, username) + relink.replace_files_with_symlinks(source_dir, target_dir, username) # Verify assert os.path.islink(source_file) @@ -376,7 +347,7 @@ def test_file_with_special_characters(temp_dirs): f.write("target content") # Run the function - relink.find_and_replace_owned_files(source_dir, target_dir, username) + relink.replace_files_with_symlinks(source_dir, target_dir, username) # Verify assert os.path.islink(source_file) @@ -404,7 +375,7 @@ def mock_rename(src, dst): with patch("os.rename", side_effect=mock_rename): # Run the function with caplog.at_level(logging.INFO): - relink.find_and_replace_owned_files(source_dir, target_dir, username) + relink.replace_files_with_symlinks(source_dir, target_dir, username) # Check error message assert "Error deleting file" in caplog.text diff --git a/tests/relink/test_verbosity.py b/tests/relink/test_verbosity.py index d96dbd6..073f7ca 100644 --- a/tests/relink/test_verbosity.py +++ b/tests/relink/test_verbosity.py @@ -37,7 +37,7 @@ def test_quiet_mode_suppresses_info_messages(temp_dirs, caplog): # Run the function with WARNING level (quiet mode) with caplog.at_level(logging.WARNING): - relink.find_and_replace_owned_files(source_dir, target_dir, username) + relink.replace_files_with_symlinks(source_dir, target_dir, username) # Verify INFO messages are NOT in the log assert "Searching for files owned by" not in caplog.text @@ -59,7 +59,7 @@ def test_quiet_mode_shows_warnings(temp_dirs, caplog): # Run the function with WARNING level (quiet mode) with caplog.at_level(logging.WARNING): - relink.find_and_replace_owned_files(source_dir, target_dir, username) + relink.replace_files_with_symlinks(source_dir, target_dir, username) # Verify WARNING message IS in the log assert "Warning: Corresponding file not found" in caplog.text @@ -73,7 +73,7 @@ def test_quiet_mode_shows_errors(temp_dirs, caplog): # Test 1: Invalid username error invalid_username = "nonexistent_user_12345" with caplog.at_level(logging.WARNING): - relink.find_and_replace_owned_files(source_dir, target_dir, invalid_username) + relink.replace_files_with_symlinks(source_dir, target_dir, invalid_username) assert "Error: User" in caplog.text assert "not found" in caplog.text @@ -94,7 +94,7 @@ def mock_rename(src, dst): with patch("os.rename", side_effect=mock_rename): with caplog.at_level(logging.WARNING): - relink.find_and_replace_owned_files(source_dir, target_dir, username) + relink.replace_files_with_symlinks(source_dir, target_dir, username) assert "Error deleting file" in caplog.text # Clear the log for next test @@ -114,5 +114,5 @@ def mock_symlink(src, dst): with patch("os.symlink", side_effect=mock_symlink): with caplog.at_level(logging.WARNING): - relink.find_and_replace_owned_files(source_dir, target_dir, username) + relink.replace_files_with_symlinks(source_dir, target_dir, username) assert "Error creating symlink" in caplog.text