Skip to content

Commit 863bc4f

Browse files
committed
relink.py: Add --dry-run option.
1 parent 57b6036 commit 863bc4f

2 files changed

Lines changed: 135 additions & 2 deletions

File tree

relink.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
logger = logging.getLogger(__name__)
2020

2121

22-
def find_and_replace_owned_files(source_dir, target_dir, username):
22+
def find_and_replace_owned_files(source_dir, target_dir, username, dry_run=False):
2323
"""
2424
Finds files owned by a specific user in a source directory tree,
2525
deletes them, and replaces them with symbolic links to the same
@@ -29,6 +29,7 @@ def find_and_replace_owned_files(source_dir, target_dir, username):
2929
source_dir (str): The root of the directory tree to search for files.
3030
target_dir (str): The root of the directory tree containing the new files.
3131
username (str): The name of the user whose files will be processed.
32+
dry_run (bool): If True, only show what would be done without making changes.
3233
"""
3334
source_dir = os.path.abspath(source_dir)
3435
target_dir = os.path.abspath(target_dir)
@@ -40,6 +41,9 @@ def find_and_replace_owned_files(source_dir, target_dir, username):
4041
logger.error("Error: User '%s' not found. Exiting.", username)
4142
return
4243

44+
if dry_run:
45+
logger.info("DRY RUN MODE - No changes will be made")
46+
4347
logger.info(
4448
"Searching for files owned by '%s' (UID: %s) in '%s'...",
4549
username,
@@ -81,6 +85,14 @@ def find_and_replace_owned_files(source_dir, target_dir, username):
8185
# Get the link name
8286
link_name = file_path
8387

88+
if dry_run:
89+
logger.info(
90+
"[DRY RUN] Would create symbolic link: %s -> %s",
91+
link_name,
92+
link_target,
93+
)
94+
continue
95+
8496
# Remove the original file
8597
try:
8698
os.rename(link_name, link_name + ".tmp")
@@ -168,6 +180,12 @@ def parse_arguments():
168180
help="Quiet mode (show only warnings and errors)",
169181
)
170182

183+
parser.add_argument(
184+
"--dry-run",
185+
action="store_true",
186+
help="Show what would be done without making any changes",
187+
)
188+
171189
return parser.parse_args()
172190

173191

@@ -188,4 +206,6 @@ def parse_arguments():
188206
my_username = os.environ["USER"]
189207

190208
# --- Execution ---
191-
find_and_replace_owned_files(args.source_root, args.target_root, my_username)
209+
find_and_replace_owned_files(
210+
args.source_root, args.target_root, my_username, dry_run=args.dry_run
211+
)

tests/test_relink.py

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -451,3 +451,116 @@ def test_verbose_and_quiet_short_flags_mutually_exclusive(self, mock_default_dir
451451
relink.parse_arguments()
452452
# Mutually exclusive arguments cause SystemExit with code 2
453453
assert exc_info.value.code == 2
454+
455+
def test_dry_run_flag(self, mock_default_dirs):
456+
"""Test that --dry-run flag is parsed correctly."""
457+
# pylint: disable=unused-argument
458+
with patch("sys.argv", ["relink.py", "--dry-run"]):
459+
args = relink.parse_arguments()
460+
assert args.dry_run is True
461+
462+
def test_dry_run_default(self, mock_default_dirs):
463+
"""Test that dry_run defaults to False."""
464+
# pylint: disable=unused-argument
465+
with patch("sys.argv", ["relink.py"]):
466+
args = relink.parse_arguments()
467+
assert args.dry_run is False
468+
469+
470+
class TestDryRun:
471+
"""Test suite for dry-run functionality."""
472+
473+
@pytest.fixture
474+
def temp_dirs(self):
475+
"""Create temporary source and target directories for testing."""
476+
source_dir = tempfile.mkdtemp(prefix="test_source_")
477+
target_dir = tempfile.mkdtemp(prefix="test_target_")
478+
479+
yield source_dir, target_dir
480+
481+
# Cleanup
482+
shutil.rmtree(source_dir, ignore_errors=True)
483+
shutil.rmtree(target_dir, ignore_errors=True)
484+
485+
def test_dry_run_no_changes(self, temp_dirs, caplog):
486+
"""Test that dry-run mode makes no actual changes."""
487+
source_dir, target_dir = temp_dirs
488+
username = os.environ["USER"]
489+
490+
# Create files
491+
source_file = os.path.join(source_dir, "test_file.txt")
492+
target_file = os.path.join(target_dir, "test_file.txt")
493+
494+
with open(source_file, "w", encoding="utf-8") as f:
495+
f.write("source content")
496+
with open(target_file, "w", encoding="utf-8") as f:
497+
f.write("target content")
498+
499+
# Get original file info
500+
with open(source_file, "r", encoding="utf-8") as f:
501+
original_content = f.read()
502+
original_is_link = os.path.islink(source_file)
503+
504+
# Run in dry-run mode
505+
with caplog.at_level(logging.INFO):
506+
relink.find_and_replace_owned_files(
507+
source_dir, target_dir, username, dry_run=True
508+
)
509+
510+
# Verify no changes were made
511+
assert os.path.isfile(source_file), "Original file should still exist"
512+
assert not os.path.islink(source_file), "File should not be a symlink"
513+
with open(source_file, "r", encoding="utf-8") as f:
514+
assert f.read() == original_content
515+
assert os.path.islink(source_file) == original_is_link
516+
517+
def test_dry_run_shows_message(self, temp_dirs, caplog):
518+
"""Test that dry-run mode shows what would be done."""
519+
source_dir, target_dir = temp_dirs
520+
username = os.environ["USER"]
521+
522+
# Create files
523+
source_file = os.path.join(source_dir, "test_file.txt")
524+
target_file = os.path.join(target_dir, "test_file.txt")
525+
526+
with open(source_file, "w", encoding="utf-8") as f:
527+
f.write("source")
528+
with open(target_file, "w", encoding="utf-8") as f:
529+
f.write("target")
530+
531+
# Run in dry-run mode
532+
with caplog.at_level(logging.INFO):
533+
relink.find_and_replace_owned_files(
534+
source_dir, target_dir, username, dry_run=True
535+
)
536+
537+
# Check that dry-run messages were logged
538+
assert "DRY RUN MODE" in caplog.text
539+
assert "[DRY RUN] Would create symbolic link:" in caplog.text
540+
assert f"{source_file} -> {target_file}" in caplog.text
541+
542+
def test_dry_run_no_delete_or_create_messages(self, temp_dirs, caplog):
543+
"""Test that dry-run doesn't show delete/create messages."""
544+
source_dir, target_dir = temp_dirs
545+
username = os.environ["USER"]
546+
547+
# Create files
548+
source_file = os.path.join(source_dir, "test_file.txt")
549+
target_file = os.path.join(target_dir, "test_file.txt")
550+
551+
with open(source_file, "w", encoding="utf-8") as f:
552+
f.write("source")
553+
with open(target_file, "w", encoding="utf-8") as f:
554+
f.write("target")
555+
556+
# Run in dry-run mode
557+
with caplog.at_level(logging.INFO):
558+
relink.find_and_replace_owned_files(
559+
source_dir, target_dir, username, dry_run=True
560+
)
561+
562+
# Verify actual operation messages are NOT logged
563+
assert "Deleted original file:" not in caplog.text
564+
assert "Created symbolic link:" not in caplog.text
565+
# But the dry-run message should be there
566+
assert "[DRY RUN] Would create symbolic link: " in caplog.text

0 commit comments

Comments
 (0)