Skip to content

Commit 9d8624d

Browse files
committed
relink.py: Add verbosity level flags.
1 parent cbb88db commit 9d8624d

2 files changed

Lines changed: 193 additions & 5 deletions

File tree

relink.py

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -101,8 +101,8 @@ def parse_arguments():
101101
Parse command-line arguments.
102102
103103
Returns:
104-
argparse.Namespace: Parsed arguments containing source_root
105-
and target_root.
104+
argparse.Namespace: Parsed arguments containing source_root,
105+
target_root, and verbosity settings.
106106
"""
107107
parser = argparse.ArgumentParser(
108108
description=(
@@ -125,19 +125,41 @@ def parse_arguments():
125125
)
126126
)
127127

128+
# Verbosity options (mutually exclusive)
129+
verbosity_group = parser.add_mutually_exclusive_group()
130+
verbosity_group.add_argument(
131+
'-v', '--verbose',
132+
action='store_true',
133+
help='Enable verbose output'
134+
)
135+
verbosity_group.add_argument(
136+
'-q', '--quiet',
137+
action='store_true',
138+
help='Quiet mode (show only warnings and errors)'
139+
)
140+
128141
return parser.parse_args()
129142

130143
if __name__ == '__main__':
131144
# Configure logging to display INFO and above to console (stdout)
132145
import sys
146+
# --- Configuration ---
147+
args = parse_arguments()
148+
149+
# Configure logging based on verbosity flags
150+
if args.quiet:
151+
LOG_LEVEL = logging.WARNING
152+
elif args.verbose:
153+
LOG_LEVEL = logging.DEBUG
154+
else:
155+
LOG_LEVEL = logging.INFO
156+
133157
logging.basicConfig(
134-
level=logging.INFO,
158+
level=LOG_LEVEL,
135159
format='%(message)s',
136160
stream=sys.stdout
137161
)
138162

139-
# --- Configuration ---
140-
args = parse_arguments()
141163
my_username = os.environ['USER']
142164

143165
# --- Execution ---

tests/test_relink.py

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,172 @@ def test_both_custom_paths(self):
336336
assert args.source_root == source_path
337337
assert args.target_root == target_path
338338

339+
def test_verbose_flag(self):
340+
"""Test that --verbose flag is parsed correctly."""
341+
with patch("sys.argv", ["relink.py", "--verbose"]):
342+
args = relink.parse_arguments()
343+
assert args.verbose is True
344+
assert args.quiet is False
345+
346+
def test_quiet_flag(self):
347+
"""Test that --quiet flag is parsed correctly."""
348+
with patch("sys.argv", ["relink.py", "--quiet"]):
349+
args = relink.parse_arguments()
350+
assert args.quiet is True
351+
assert args.verbose is False
352+
353+
def test_verbose_short_flag(self):
354+
"""Test that -v flag is parsed correctly."""
355+
with patch("sys.argv", ["relink.py", "-v"]):
356+
args = relink.parse_arguments()
357+
assert args.verbose is True
358+
359+
def test_quiet_short_flag(self):
360+
"""Test that -q flag is parsed correctly."""
361+
with patch("sys.argv", ["relink.py", "-q"]):
362+
args = relink.parse_arguments()
363+
assert args.quiet is True
364+
365+
def test_default_verbosity(self):
366+
"""Test that default verbosity has both flags as False."""
367+
with patch("sys.argv", ["relink.py"]):
368+
args = relink.parse_arguments()
369+
assert args.verbose is False
370+
assert args.quiet is False
371+
372+
def test_verbose_and_quiet_mutually_exclusive(self):
373+
"""Test that --verbose and --quiet cannot be used together."""
374+
with patch("sys.argv", ["relink.py", "--verbose", "--quiet"]):
375+
with pytest.raises(SystemExit) as exc_info:
376+
relink.parse_arguments()
377+
# Mutually exclusive arguments cause SystemExit with code 2
378+
assert exc_info.value.code == 2
379+
380+
def test_verbose_and_quiet_short_flags_mutually_exclusive(self):
381+
"""Test that -v and -q cannot be used together."""
382+
with patch("sys.argv", ["relink.py", "-v", "-q"]):
383+
with pytest.raises(SystemExit) as exc_info:
384+
relink.parse_arguments()
385+
# Mutually exclusive arguments cause SystemExit with code 2
386+
assert exc_info.value.code == 2
387+
388+
389+
class TestVerbosityLevels:
390+
"""Test suite for verbosity level behavior."""
391+
392+
@pytest.fixture
393+
def temp_dirs(self):
394+
"""Create temporary source and target directories for testing."""
395+
source_dir = tempfile.mkdtemp(prefix="test_source_")
396+
target_dir = tempfile.mkdtemp(prefix="test_target_")
397+
398+
yield source_dir, target_dir
399+
400+
# Cleanup
401+
shutil.rmtree(source_dir, ignore_errors=True)
402+
shutil.rmtree(target_dir, ignore_errors=True)
403+
404+
def test_quiet_mode_suppresses_info_messages(self, temp_dirs, caplog):
405+
"""Test that quiet mode suppresses INFO level messages."""
406+
source_dir, target_dir = temp_dirs
407+
username = os.environ["USER"]
408+
409+
# Create files
410+
source_file = os.path.join(source_dir, "test_file.txt")
411+
target_file = os.path.join(target_dir, "test_file.txt")
412+
413+
with open(source_file, "w", encoding="utf-8") as f:
414+
f.write("source")
415+
with open(target_file, "w", encoding="utf-8") as f:
416+
f.write("target")
417+
418+
# Create a symlink to test "Skipping symlink" message
419+
source_link = os.path.join(source_dir, "existing_link.txt")
420+
dummy_target = os.path.join(tempfile.gettempdir(), "somewhere")
421+
os.symlink(dummy_target, source_link)
422+
423+
# Run the function with WARNING level (quiet mode)
424+
with caplog.at_level(logging.WARNING):
425+
relink.find_and_replace_owned_files(source_dir, target_dir, username)
426+
427+
# Verify INFO messages are NOT in the log
428+
assert "Searching for files owned by" not in caplog.text
429+
assert "Skipping symlink:" not in caplog.text
430+
assert "Found owned file:" not in caplog.text
431+
assert "Deleted original file:" not in caplog.text
432+
assert "Created symbolic link:" not in caplog.text
433+
434+
def test_quiet_mode_shows_warnings(self, temp_dirs, caplog):
435+
"""Test that quiet mode still shows WARNING level messages."""
436+
source_dir, target_dir = temp_dirs
437+
username = os.environ["USER"]
438+
439+
# Create only source file (no corresponding target) to trigger warning
440+
source_file = os.path.join(source_dir, "orphan.txt")
441+
with open(source_file, "w", encoding="utf-8") as f:
442+
f.write("orphan content")
443+
444+
# Run the function with WARNING level (quiet mode)
445+
with caplog.at_level(logging.WARNING):
446+
relink.find_and_replace_owned_files(source_dir, target_dir, username)
447+
448+
# Verify WARNING message IS in the log
449+
assert "Warning: Corresponding file not found" in caplog.text
450+
451+
def test_quiet_mode_shows_errors(self, temp_dirs, caplog):
452+
"""Test that quiet mode still shows ERROR level messages."""
453+
source_dir, target_dir = temp_dirs
454+
username = os.environ["USER"]
455+
456+
# Test 1: Invalid username error
457+
invalid_username = "nonexistent_user_12345"
458+
with caplog.at_level(logging.WARNING):
459+
relink.find_and_replace_owned_files(
460+
source_dir, target_dir, invalid_username
461+
)
462+
assert "Error: User" in caplog.text
463+
assert "not found" in caplog.text
464+
465+
# Clear the log for next test
466+
caplog.clear()
467+
468+
# Test 2: Error deleting file
469+
source_file = os.path.join(source_dir, "test.txt")
470+
target_file = os.path.join(target_dir, "test.txt")
471+
472+
with open(source_file, "w", encoding="utf-8") as f:
473+
f.write("source")
474+
with open(target_file, "w", encoding="utf-8") as f:
475+
f.write("target")
476+
477+
def mock_rename(src, dst):
478+
raise OSError("Simulated rename error")
479+
480+
with patch("os.rename", side_effect=mock_rename):
481+
with caplog.at_level(logging.WARNING):
482+
relink.find_and_replace_owned_files(source_dir, target_dir, username)
483+
assert "Error deleting file" in caplog.text
484+
485+
# Clear the log for next test
486+
caplog.clear()
487+
488+
# Test 3: Error creating symlink
489+
source_file2 = os.path.join(source_dir, "test2.txt")
490+
target_file2 = os.path.join(target_dir, "test2.txt")
491+
492+
with open(source_file2, "w", encoding="utf-8") as f:
493+
f.write("source2")
494+
with open(target_file2, "w", encoding="utf-8") as f:
495+
f.write("target2")
496+
497+
def mock_symlink(src, dst):
498+
raise OSError("Simulated symlink error")
499+
500+
with patch("os.symlink", side_effect=mock_symlink):
501+
with caplog.at_level(logging.WARNING):
502+
relink.find_and_replace_owned_files(source_dir, target_dir, username)
503+
assert "Error creating symlink" in caplog.text
504+
339505

340506
class TestEdgeCases:
341507
"""Test edge cases and error handling."""

0 commit comments

Comments
 (0)