From da4254a675cde3b1633094670316e401f9db03a3 Mon Sep 17 00:00:00 2001 From: Sam Rabin Date: Thu, 15 Jan 2026 13:28:00 -0700 Subject: [PATCH 1/5] Split relink tests across multiple files. --- tests/relink/__init__.py | 0 tests/relink/test_args.py | 294 +++++ tests/relink/test_cmdline.py | 155 +++ tests/relink/test_dryrun.py | 136 +++ .../test_find_and_replace_owned_files.py | 492 ++++++++ tests/relink/test_timing.py | 103 ++ tests/relink/test_verbosity.py | 171 +++ tests/test_relink.py | 1026 ----------------- 8 files changed, 1351 insertions(+), 1026 deletions(-) create mode 100644 tests/relink/__init__.py create mode 100644 tests/relink/test_args.py create mode 100644 tests/relink/test_cmdline.py create mode 100644 tests/relink/test_dryrun.py create mode 100644 tests/relink/test_find_and_replace_owned_files.py create mode 100644 tests/relink/test_timing.py create mode 100644 tests/relink/test_verbosity.py delete mode 100644 tests/test_relink.py diff --git a/tests/relink/__init__.py b/tests/relink/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/relink/test_args.py b/tests/relink/test_args.py new file mode 100644 index 0000000..f6e4bde --- /dev/null +++ b/tests/relink/test_args.py @@ -0,0 +1,294 @@ +""" +Tests for relink.py script. +""" + +import os +import sys +import tempfile +import shutil +import logging +import argparse +from unittest.mock import patch + +import pytest + +# 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 + + +@pytest.fixture(scope="function", autouse=True) +def configure_logging(): + """Configure logging to output to stdout for all tests.""" + # Configure logging before each test + logging.basicConfig( + level=logging.INFO, + format="%(message)s", + stream=sys.stdout, + force=True, # Force reconfiguration + ) + yield + # Clean up logging handlers after each test + logging.getLogger().handlers.clear() + + +@pytest.fixture(scope="function", name="mock_default_dirs") +def fixture_mock_default_dirs(): + """Mock the default directories to use temporary directories.""" + source_dir = tempfile.mkdtemp(prefix="test_default_source_") + target_dir = tempfile.mkdtemp(prefix="test_default_target_") + + with patch.object(relink, "DEFAULT_SOURCE_ROOT", source_dir): + with patch.object(relink, "DEFAULT_TARGET_ROOT", target_dir): + yield source_dir, target_dir + + # Cleanup + shutil.rmtree(source_dir, ignore_errors=True) + shutil.rmtree(target_dir, ignore_errors=True) + + +@pytest.fixture(scope="function", name="temp_dirs") +def fixture_temp_dirs(): + """Create temporary source and target directories for testing.""" + source_dir = tempfile.mkdtemp(prefix="test_source_") + target_dir = tempfile.mkdtemp(prefix="test_target_") + + yield source_dir, target_dir + + # Cleanup + shutil.rmtree(source_dir, ignore_errors=True) + shutil.rmtree(target_dir, ignore_errors=True) + + +class TestParseArguments: + """Test suite for parse_arguments function.""" + + def test_default_arguments(self, mock_default_dirs): + """Test that default arguments are used when none provided.""" + source_dir, target_dir = mock_default_dirs + with patch("sys.argv", ["relink.py"]): + args = relink.parse_arguments() + assert args.source_root == source_dir + assert args.target_root == target_dir + + def test_custom_source_root(self, mock_default_dirs, tmp_path): + """Test custom source root argument.""" + _, target_dir = mock_default_dirs + custom_source = tmp_path / "custom_source" + custom_source.mkdir() + with patch("sys.argv", ["relink.py", "--source-root", str(custom_source)]): + args = relink.parse_arguments() + assert args.source_root == str(custom_source.resolve()) + assert args.target_root == target_dir + + def test_custom_target_root(self, mock_default_dirs, tmp_path): + """Test custom target root argument.""" + source_dir, _ = mock_default_dirs + custom_target = tmp_path / "custom_target" + custom_target.mkdir() + with patch("sys.argv", ["relink.py", "--target-root", str(custom_target)]): + args = relink.parse_arguments() + assert args.source_root == source_dir + assert args.target_root == str(custom_target.resolve()) + + def test_both_custom_paths(self, tmp_path): + """Test both custom source and target roots.""" + source_path = tmp_path / "custom_source" + target_path = tmp_path / "custom_target" + source_path.mkdir() + target_path.mkdir() + with patch( + "sys.argv", + [ + "relink.py", + "--source-root", + str(source_path), + "--target-root", + str(target_path), + ], + ): + args = relink.parse_arguments() + assert args.source_root == str(source_path.resolve()) + assert args.target_root == str(target_path.resolve()) + + def test_verbose_flag(self, mock_default_dirs): # pylint: disable=unused-argument + """Test that --verbose flag is parsed correctly.""" + with patch("sys.argv", ["relink.py", "--verbose"]): + args = relink.parse_arguments() + assert args.verbose is True + assert args.quiet is False + + def test_quiet_flag(self, mock_default_dirs): # pylint: disable=unused-argument + """Test that --quiet flag is parsed correctly.""" + with patch("sys.argv", ["relink.py", "--quiet"]): + args = relink.parse_arguments() + assert args.quiet is True + assert args.verbose is False + + def test_verbose_short_flag( + self, mock_default_dirs + ): # pylint: disable=unused-argument + """Test that -v flag is parsed correctly.""" + with patch("sys.argv", ["relink.py", "-v"]): + args = relink.parse_arguments() + assert args.verbose is True + + def test_quiet_short_flag( + self, mock_default_dirs + ): # pylint: disable=unused-argument + """Test that -q flag is parsed correctly.""" + with patch("sys.argv", ["relink.py", "-q"]): + args = relink.parse_arguments() + assert args.quiet is True + + def test_default_verbosity( + self, mock_default_dirs + ): # pylint: disable=unused-argument + """Test that default verbosity has both flags as False.""" + with patch("sys.argv", ["relink.py"]): + args = relink.parse_arguments() + assert args.verbose is False + assert args.quiet is False + + def test_verbose_and_quiet_mutually_exclusive(self, mock_default_dirs): + """Test that --verbose and --quiet cannot be used together.""" + # pylint: disable=unused-argument + with patch("sys.argv", ["relink.py", "--verbose", "--quiet"]): + with pytest.raises(SystemExit) as exc_info: + relink.parse_arguments() + # Mutually exclusive arguments cause SystemExit with code 2 + assert exc_info.value.code == 2 + + def test_verbose_and_quiet_short_flags_mutually_exclusive(self, mock_default_dirs): + """Test that -v and -q cannot be used together.""" + # pylint: disable=unused-argument + with patch("sys.argv", ["relink.py", "-v", "-q"]): + with pytest.raises(SystemExit) as exc_info: + relink.parse_arguments() + # Mutually exclusive arguments cause SystemExit with code 2 + assert exc_info.value.code == 2 + + def test_dry_run_flag(self, mock_default_dirs): + """Test that --dry-run flag is parsed correctly.""" + # pylint: disable=unused-argument + with patch("sys.argv", ["relink.py", "--dry-run"]): + args = relink.parse_arguments() + assert args.dry_run is True + + def test_dry_run_default(self, mock_default_dirs): + """Test that dry_run defaults to False.""" + # pylint: disable=unused-argument + with patch("sys.argv", ["relink.py"]): + args = relink.parse_arguments() + assert args.dry_run is False + + def test_timing_flag(self, mock_default_dirs): + """Test that --timing flag is parsed correctly.""" + # pylint: disable=unused-argument + with patch("sys.argv", ["relink.py", "--timing"]): + args = relink.parse_arguments() + assert args.timing is True + + def test_timing_default(self, mock_default_dirs): + """Test that timing defaults to False.""" + # pylint: disable=unused-argument + with patch("sys.argv", ["relink.py"]): + args = relink.parse_arguments() + assert args.timing is False + + +class TestValidateDirectory: + """Test suite for validate_directory function.""" + + def test_valid_directory(self, tmp_path): + """Test that valid directory is accepted and returns absolute path.""" + test_dir = tmp_path / "valid_dir" + test_dir.mkdir() + + result = relink.validate_directory(str(test_dir)) + assert result == str(test_dir.resolve()) + + def test_nonexistent_directory(self): + """Test that nonexistent directory raises ArgumentTypeError.""" + nonexistent = os.path.join(os.sep, "nonexistent", "directory", "12345") + + with pytest.raises(argparse.ArgumentTypeError) as exc_info: + relink.validate_directory(nonexistent) + + assert "does not exist" in str(exc_info.value) + assert nonexistent in str(exc_info.value) + + def test_file_instead_of_directory(self, tmp_path): + """Test that a file path raises ArgumentTypeError.""" + test_file = tmp_path / "test_file.txt" + test_file.write_text("content") + + with pytest.raises(argparse.ArgumentTypeError) as exc_info: + relink.validate_directory(str(test_file)) + + assert "not a directory" in str(exc_info.value) + + def test_relative_path_converted_to_absolute(self, tmp_path): + """Test that relative paths are converted to absolute.""" + test_dir = tmp_path / "relative_test" + test_dir.mkdir() + + # Change to parent directory and use relative path + cwd = os.getcwd() + try: + os.chdir(str(tmp_path)) + result = relink.validate_directory("relative_test") + assert os.path.isabs(result) + assert result == str(test_dir.resolve()) + finally: + os.chdir(cwd) + + def test_symlink_to_directory(self, tmp_path): + """Test that symlink to a directory is accepted.""" + real_dir = tmp_path / "real_dir" + real_dir.mkdir() + + link_dir = tmp_path / "link_dir" + link_dir.symlink_to(real_dir) + + result = relink.validate_directory(str(link_dir)) + # validate_directory returns absolute path of the symlink itself + assert result == str(link_dir.absolute()) + # Verify it's still a symlink + assert os.path.islink(result) + + +class TestProcessArgs: + """Test suite for process_args function.""" + + # pylint: disable=no-member + + def test_process_args_quiet_sets_warning_level(self): + """Test that quiet flag sets log level to WARNING.""" + args = argparse.Namespace(quiet=True, verbose=False) + relink.process_args(args) + assert args.log_level == logging.WARNING + + def test_process_args_verbose_sets_debug_level(self): + """Test that verbose flag sets log level to DEBUG.""" + args = argparse.Namespace(quiet=False, verbose=True) + relink.process_args(args) + assert args.log_level == logging.DEBUG + + def test_process_args_default_sets_info_level(self): + """Test that default (no flags) sets log level to INFO.""" + args = argparse.Namespace(quiet=False, verbose=False) + relink.process_args(args) + assert args.log_level == logging.INFO + + def test_process_args_modifies_args_in_place(self): + """Test that process_args modifies the args object in place.""" + args = argparse.Namespace(quiet=False, verbose=False) + original_args = args + relink.process_args(args) + # Should be the same object, modified in place + assert args is original_args + assert hasattr(args, "log_level") diff --git a/tests/relink/test_cmdline.py b/tests/relink/test_cmdline.py new file mode 100644 index 0000000..214a931 --- /dev/null +++ b/tests/relink/test_cmdline.py @@ -0,0 +1,155 @@ +""" +Tests for relink.py script as called from command line +""" + +import os +import sys +import tempfile +import shutil +import logging +import subprocess +from unittest.mock import patch + +import pytest + +# 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 + + +@pytest.fixture(scope="function", autouse=True) +def configure_logging(): + """Configure logging to output to stdout for all tests.""" + # Configure logging before each test + logging.basicConfig( + level=logging.INFO, + format="%(message)s", + stream=sys.stdout, + force=True, # Force reconfiguration + ) + yield + # Clean up logging handlers after each test + logging.getLogger().handlers.clear() + + +@pytest.fixture(scope="function", name="mock_default_dirs") +def fixture_mock_default_dirs(): + """Mock the default directories to use temporary directories.""" + source_dir = tempfile.mkdtemp(prefix="test_default_source_") + target_dir = tempfile.mkdtemp(prefix="test_default_target_") + + with patch.object(relink, "DEFAULT_SOURCE_ROOT", source_dir): + with patch.object(relink, "DEFAULT_TARGET_ROOT", target_dir): + yield source_dir, target_dir + + # Cleanup + shutil.rmtree(source_dir, ignore_errors=True) + shutil.rmtree(target_dir, ignore_errors=True) + + +@pytest.fixture(scope="function", name="temp_dirs") +def fixture_temp_dirs(): + """Create temporary source and target directories for testing.""" + source_dir = tempfile.mkdtemp(prefix="test_source_") + target_dir = tempfile.mkdtemp(prefix="test_target_") + + yield source_dir, target_dir + + # Cleanup + shutil.rmtree(source_dir, ignore_errors=True) + shutil.rmtree(target_dir, ignore_errors=True) + + +class TestCommandLineExecution: + """Test suite for command-line execution of relink.py.""" + + @pytest.fixture + def mock_dirs(self, tmp_path): + """Create temporary directories and files for command-line testing.""" + source_dir = tmp_path / "source" + target_dir = tmp_path / "target" + source_dir.mkdir() + target_dir.mkdir() + + # Create a test file + source_file = source_dir / "test_file.txt" + target_file = target_dir / "test_file.txt" + source_file.write_text("source content") + target_file.write_text("target content") + + return source_dir, target_dir, source_file, target_file + + def test_command_line_execution_dry_run(self, mock_dirs): + """Test executing relink.py from command line with --dry-run flag.""" + source_dir, target_dir, source_file, _ = mock_dirs + + # Get the path to relink.py + relink_script = os.path.join( + os.path.dirname( + os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + ), + "relink.py", + ) + + # Build the command + command = [ + sys.executable, + relink_script, + "--source-root", + str(source_dir), + "--target-root", + str(target_dir), + "--dry-run", + ] + + # Execute the command + result = subprocess.run(command, capture_output=True, text=True, check=False) + + # Verify the command executed successfully + assert result.returncode == 0, f"Command failed with stderr: {result.stderr}" + + # Verify dry-run messages in output + assert "DRY RUN MODE" in result.stdout + assert "[DRY RUN] Would create symbolic link:" in result.stdout + + # Verify no actual changes were made + assert source_file.is_file() + assert not source_file.is_symlink() + + def test_command_line_execution_actual_run(self, mock_dirs): + """Test executing relink.py from command line without dry-run.""" + source_dir, target_dir, source_file, target_file = mock_dirs + + # Get the path to relink.py + relink_script = os.path.join( + os.path.dirname( + os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + ), + "relink.py", + ) + + # Build the command + command = [ + sys.executable, + relink_script, + "--source-root", + str(source_dir), + "--target-root", + str(target_dir), + ] + + # Execute the command + result = subprocess.run(command, capture_output=True, text=True, check=False) + + # Verify the command executed successfully + assert result.returncode == 0, f"Command failed with stderr: {result.stderr}" + + # Verify the file was converted to a symlink + assert source_file.is_symlink() + assert os.readlink(str(source_file)) == str(target_file) + + # Verify success messages in output + assert "Created symbolic link:" in result.stdout diff --git a/tests/relink/test_dryrun.py b/tests/relink/test_dryrun.py new file mode 100644 index 0000000..d43088d --- /dev/null +++ b/tests/relink/test_dryrun.py @@ -0,0 +1,136 @@ +""" +Tests for relink.py script. +""" + +import os +import sys +import tempfile +import shutil +import logging +from unittest.mock import patch + +import pytest + +# 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 + + +@pytest.fixture(scope="function", autouse=True) +def configure_logging(): + """Configure logging to output to stdout for all tests.""" + # Configure logging before each test + logging.basicConfig( + level=logging.INFO, + format="%(message)s", + stream=sys.stdout, + force=True, # Force reconfiguration + ) + yield + # Clean up logging handlers after each test + logging.getLogger().handlers.clear() + + +@pytest.fixture(scope="function", name="mock_default_dirs") +def fixture_mock_default_dirs(): + """Mock the default directories to use temporary directories.""" + source_dir = tempfile.mkdtemp(prefix="test_default_source_") + target_dir = tempfile.mkdtemp(prefix="test_default_target_") + + with patch.object(relink, "DEFAULT_SOURCE_ROOT", source_dir): + with patch.object(relink, "DEFAULT_TARGET_ROOT", target_dir): + yield source_dir, target_dir + + # Cleanup + shutil.rmtree(source_dir, ignore_errors=True) + shutil.rmtree(target_dir, ignore_errors=True) + + +@pytest.fixture(scope="function", name="temp_dirs") +def fixture_temp_dirs(): + """Create temporary source and target directories for testing.""" + source_dir = tempfile.mkdtemp(prefix="test_source_") + target_dir = tempfile.mkdtemp(prefix="test_target_") + + yield source_dir, target_dir + + # Cleanup + shutil.rmtree(source_dir, ignore_errors=True) + shutil.rmtree(target_dir, ignore_errors=True) + + +@pytest.fixture(name="dry_run_setup") +def fixture_dry_run_setup(temp_dirs): + """Set up directories and files for dry-run tests.""" + source_dir, target_dir = temp_dirs + username = os.environ["USER"] + + # Create files + source_file = os.path.join(source_dir, "test_file.txt") + target_file = os.path.join(target_dir, "test_file.txt") + + with open(source_file, "w", encoding="utf-8") as f: + f.write("source content") + with open(target_file, "w", encoding="utf-8") as f: + f.write("target content") + + return source_dir, target_dir, source_file, target_file, username + + +def test_dry_run_no_changes(dry_run_setup, caplog): + """Test that dry-run mode makes no actual changes.""" + source_dir, target_dir, source_file, _, username = dry_run_setup + + # Get original file info + with open(source_file, "r", encoding="utf-8") as f: + original_content = f.read() + original_is_link = os.path.islink(source_file) + + # Run in dry-run mode + with caplog.at_level(logging.INFO): + relink.find_and_replace_owned_files( + source_dir, target_dir, username, dry_run=True + ) + + # Verify no changes were made + assert os.path.isfile(source_file), "Original file should still exist" + assert not os.path.islink(source_file), "File should not be a symlink" + with open(source_file, "r", encoding="utf-8") as f: + assert f.read() == original_content + assert os.path.islink(source_file) == original_is_link + + +def test_dry_run_shows_message(dry_run_setup, caplog): + """Test that dry-run mode shows what would be done.""" + source_dir, target_dir, source_file, target_file, username = dry_run_setup + + # Run in dry-run mode + with caplog.at_level(logging.INFO): + relink.find_and_replace_owned_files( + source_dir, target_dir, username, dry_run=True + ) + + # Check that dry-run messages were logged + assert "DRY RUN MODE" in caplog.text + assert "[DRY RUN] Would create symbolic link:" in caplog.text + assert f"{source_file} -> {target_file}" in caplog.text + + +def test_dry_run_no_delete_or_create_messages(dry_run_setup, caplog): + """Test that dry-run doesn't show delete/create messages.""" + source_dir, target_dir, _, _, username = dry_run_setup + + # Run in dry-run mode + with caplog.at_level(logging.INFO): + relink.find_and_replace_owned_files( + source_dir, target_dir, username, dry_run=True + ) + + # Verify actual operation messages are NOT logged + assert "Deleted original file:" not in caplog.text + assert "Created symbolic link:" not in caplog.text + # But the dry-run message should be there + assert "[DRY RUN] Would create symbolic link: " in caplog.text diff --git a/tests/relink/test_find_and_replace_owned_files.py b/tests/relink/test_find_and_replace_owned_files.py new file mode 100644 index 0000000..cea7d72 --- /dev/null +++ b/tests/relink/test_find_and_replace_owned_files.py @@ -0,0 +1,492 @@ +""" +Tests for relink.py script. +""" + +import os +import sys +import tempfile +import shutil +import pwd +import logging +from unittest.mock import patch + +import pytest + +# 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 + + +@pytest.fixture(scope="function", autouse=True) +def configure_logging(): + """Configure logging to output to stdout for all tests.""" + # Configure logging before each test + logging.basicConfig( + level=logging.INFO, + format="%(message)s", + stream=sys.stdout, + force=True, # Force reconfiguration + ) + yield + # Clean up logging handlers after each test + logging.getLogger().handlers.clear() + + +@pytest.fixture(scope="function", name="mock_default_dirs") +def fixture_mock_default_dirs(): + """Mock the default directories to use temporary directories.""" + source_dir = tempfile.mkdtemp(prefix="test_default_source_") + target_dir = tempfile.mkdtemp(prefix="test_default_target_") + + with patch.object(relink, "DEFAULT_SOURCE_ROOT", source_dir): + with patch.object(relink, "DEFAULT_TARGET_ROOT", target_dir): + yield source_dir, target_dir + + # Cleanup + shutil.rmtree(source_dir, ignore_errors=True) + shutil.rmtree(target_dir, ignore_errors=True) + + +@pytest.fixture(scope="function", name="temp_dirs") +def fixture_temp_dirs(): + """Create temporary source and target directories for testing.""" + source_dir = tempfile.mkdtemp(prefix="test_source_") + target_dir = tempfile.mkdtemp(prefix="test_target_") + + yield source_dir, target_dir + + # Cleanup + shutil.rmtree(source_dir, ignore_errors=True) + shutil.rmtree(target_dir, ignore_errors=True) + + +@pytest.fixture(name="current_user") +def fixture_current_user(): + """Get the current user's username.""" + username = os.environ["USER"] + return username + + +def test_basic_file_replacement(temp_dirs, current_user): + """Test basic functionality: replace owned file with symlink.""" + source_dir, target_dir = temp_dirs + username = current_user + + # Create a file in source directory + source_file = os.path.join(source_dir, "test_file.txt") + with open(source_file, "w", encoding="utf-8") as f: + f.write("source content") + + # Create corresponding file in target directory + target_file = os.path.join(target_dir, "test_file.txt") + with open(target_file, "w", encoding="utf-8") as f: + f.write("target content") + + # Run the function + relink.find_and_replace_owned_files(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" + assert ( + os.readlink(source_file) == target_file + ), "Symlink should point to target file" + + +def test_nested_directory_structure(temp_dirs, current_user): + """Test with nested directory structures.""" + source_dir, target_dir = temp_dirs + username = current_user + + # Create nested directories + nested_path = os.path.join("subdir1", "subdir2") + os.makedirs(os.path.join(source_dir, nested_path)) + os.makedirs(os.path.join(target_dir, nested_path)) + + # Create files in nested directories + source_file = os.path.join(source_dir, nested_path, "nested_file.txt") + target_file = os.path.join(target_dir, nested_path, "nested_file.txt") + + with open(source_file, "w", encoding="utf-8") as f: + f.write("nested source") + with open(target_file, "w", encoding="utf-8") as f: + f.write("nested target") + + # Run the function + relink.find_and_replace_owned_files(source_dir, target_dir, username) + + # Verify + assert os.path.islink(source_file), "Nested file should be a symlink" + assert os.readlink(source_file) == target_file + + +def test_skip_existing_symlinks(temp_dirs, current_user, caplog): + """Test that existing symlinks are skipped.""" + source_dir, target_dir = temp_dirs + username = current_user + + # Create a target file + target_file = os.path.join(target_dir, "target.txt") + with open(target_file, "w", encoding="utf-8") as f: + f.write("target") + + # Create a symlink in source (pointing somewhere else) + source_link = os.path.join(source_dir, "existing_link.txt") + dummy_target = os.path.join(tempfile.gettempdir(), "somewhere") + os.symlink(dummy_target, source_link) + + # Get the inode and mtime before running the function + stat_before = os.lstat(source_link) + + # Run the function + with caplog.at_level(logging.INFO): + relink.find_and_replace_owned_files(source_dir, target_dir, username) + + # Verify the symlink is unchanged (same inode means it wasn't deleted/recreated) + stat_after = os.lstat(source_link) + assert ( + stat_before.st_ino == stat_after.st_ino + ), "Symlink should not have been recreated" + assert ( + stat_before.st_mtime == stat_after.st_mtime + ), "Symlink mtime should be unchanged" + assert ( + os.readlink(source_link) == dummy_target + ), "Symlink target should be unchanged" + + # Check that "Skipping symlink" message was logged + assert "Skipping symlink:" in caplog.text + assert source_link in caplog.text + + +def test_missing_target_file(temp_dirs, current_user, caplog): + """Test behavior when target file doesn't exist.""" + source_dir, target_dir = temp_dirs + username = current_user + + # Create only source file (no corresponding target) + source_file = os.path.join(source_dir, "orphan.txt") + with open(source_file, "w", encoding="utf-8") as f: + f.write("orphan content") + + # Run the function + with caplog.at_level(logging.INFO): + relink.find_and_replace_owned_files(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" + assert os.path.isfile(source_file), "Original file should still exist" + + # Check warning message + assert "Warning: Corresponding file not found" in caplog.text + + +def test_invalid_username(temp_dirs, caplog): + """Test behavior with invalid username.""" + source_dir, target_dir = temp_dirs + + # Use a username that doesn't exist + invalid_username = "nonexistent_user_12345" + try: + pwd.getpwnam(invalid_username).pw_uid + except KeyError: + pass + else: + raise RuntimeError(f"{invalid_username=} DOES actually exist") + + # Run the function + with caplog.at_level(logging.INFO): + relink.find_and_replace_owned_files(source_dir, target_dir, invalid_username) + + # Check error message + assert "Error: User" in caplog.text + assert "not found" in caplog.text + + +def test_multiple_files(temp_dirs, current_user): + """Test with multiple files in the directory.""" + source_dir, target_dir = temp_dirs + username = current_user + + # Create multiple files + for i in range(5): + source_file = os.path.join(source_dir, f"file_{i}.txt") + target_file = os.path.join(target_dir, f"file_{i}.txt") + + with open(source_file, "w", encoding="utf-8") as f: + f.write(f"source {i}") + with open(target_file, "w", encoding="utf-8") as f: + f.write(f"target {i}") + + # Run the function + relink.find_and_replace_owned_files(source_dir, target_dir, username) + + # Verify all files are symlinks + for i in range(5): + source_file = os.path.join(source_dir, f"file_{i}.txt") + target_file = os.path.join(target_dir, f"file_{i}.txt") + assert os.path.islink(source_file) + assert os.readlink(source_file) == target_file + + +def test_absolute_paths(temp_dirs, current_user): + """Test that function handles relative paths by converting to absolute.""" + source_dir, target_dir = temp_dirs + username = current_user + + # Create test files + source_file = os.path.join(source_dir, "test.txt") + target_file = os.path.join(target_dir, "test.txt") + + with open(source_file, "w", encoding="utf-8") as f: + f.write("test") + with open(target_file, "w", encoding="utf-8") as f: + f.write("test target") + + # Use relative paths (if possible) + cwd = os.getcwd() + try: + os.chdir(os.path.dirname(source_dir)) + rel_source = os.path.basename(source_dir) + rel_target = os.path.basename(target_dir) + + # Run with relative paths + relink.find_and_replace_owned_files(rel_source, rel_target, username) + + # Verify it still works + assert os.path.islink(source_file) + finally: + os.chdir(cwd) + + +def test_print_searching_message(temp_dirs, current_user, caplog): + """Test that searching message is printed.""" + source_dir, target_dir = temp_dirs + username = current_user + + # Run the function + with caplog.at_level(logging.INFO): + relink.find_and_replace_owned_files(source_dir, target_dir, username) + + # Check that searching message was logged + assert f"Searching for files owned by '{username}'" in caplog.text + assert f"in '{os.path.abspath(source_dir)}'" in caplog.text + + +def test_print_found_owned_file(temp_dirs, current_user, caplog): + """Test that 'Found owned file' message is printed.""" + source_dir, target_dir = temp_dirs + username = current_user + + # Create a file owned by current user + source_file = os.path.join(source_dir, "owned_file.txt") + target_file = os.path.join(target_dir, "owned_file.txt") + + with open(source_file, "w", encoding="utf-8") as f: + f.write("content") + with open(target_file, "w", encoding="utf-8") as f: + f.write("target content") + + # Run the function + with caplog.at_level(logging.INFO): + relink.find_and_replace_owned_files(source_dir, target_dir, username) + + # Check that "Found owned file" message was logged + assert "Found owned file:" in caplog.text + assert source_file in caplog.text + + +def test_print_deleted_and_created_messages(temp_dirs, current_user, caplog): + """Test that deleted and created symlink messages are printed.""" + source_dir, target_dir = temp_dirs + username = current_user + + # Create files + source_file = os.path.join(source_dir, "test_file.txt") + target_file = os.path.join(target_dir, "test_file.txt") + + with open(source_file, "w", encoding="utf-8") as f: + f.write("source") + with open(target_file, "w", encoding="utf-8") as f: + f.write("target") + + # Run the function + with caplog.at_level(logging.INFO): + relink.find_and_replace_owned_files(source_dir, target_dir, username) + + # Check messages + assert "Deleted original file:" in caplog.text + assert "Created symbolic link:" in caplog.text + 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 + username = os.environ["USER"] + + # Create source file + source_file = os.path.join(source_dir, "test.txt") + target_file = os.path.join(target_dir, "test.txt") + + with open(source_file, "w", encoding="utf-8") as f: + f.write("source") + with open(target_file, "w", encoding="utf-8") as f: + f.write("target") + + # Mock os.symlink to raise an error + def mock_symlink(src, dst): + raise OSError("Simulated symlink error") + + 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) + + # Check error message + assert "Error creating symlink" in caplog.text + assert source_file in caplog.text + + +def test_empty_directories(temp_dirs): + """Test with empty directories.""" + source_dir, target_dir = temp_dirs + username = os.environ["USER"] + + # Run with empty directories (should not crash) + relink.find_and_replace_owned_files(source_dir, target_dir, username) + + # Should complete without errors + assert True + + +def test_file_with_spaces_in_name(temp_dirs): + """Test files with spaces in their names.""" + source_dir, target_dir = temp_dirs + username = os.environ["USER"] + + # Create files with spaces + source_file = os.path.join(source_dir, "file with spaces.txt") + target_file = os.path.join(target_dir, "file with spaces.txt") + + with open(source_file, "w", encoding="utf-8") as f: + f.write("content") + with open(target_file, "w", encoding="utf-8") as f: + f.write("target content") + + # Run the function + relink.find_and_replace_owned_files(source_dir, target_dir, username) + + # Verify + assert os.path.islink(source_file) + assert os.readlink(source_file) == target_file + + +def test_file_with_special_characters(temp_dirs): + """Test files with special characters in names.""" + source_dir, target_dir = temp_dirs + username = os.environ["USER"] + + # Create files with special chars (that are valid in filenames) + filename = "file-with_special.chars@123.txt" + source_file = os.path.join(source_dir, filename) + target_file = os.path.join(target_dir, filename) + + with open(source_file, "w", encoding="utf-8") as f: + f.write("content") + with open(target_file, "w", encoding="utf-8") as f: + f.write("target content") + + # Run the function + relink.find_and_replace_owned_files(source_dir, target_dir, username) + + # Verify + assert os.path.islink(source_file) + assert os.readlink(source_file) == target_file + + +def test_error_deleting_file(temp_dirs, caplog): + """Test error message when file deletion fails.""" + source_dir, target_dir = temp_dirs + username = os.environ["USER"] + + # Create files + source_file = os.path.join(source_dir, "test.txt") + target_file = os.path.join(target_dir, "test.txt") + + with open(source_file, "w", encoding="utf-8") as f: + f.write("source") + with open(target_file, "w", encoding="utf-8") as f: + f.write("target") + + # Mock os.rename to raise an error + def mock_rename(src, dst): + raise OSError("Simulated rename error") + + 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) + + # Check error message + assert "Error deleting file" in caplog.text + assert source_file in caplog.text + + +def test_error_creating_symlink(temp_dirs, caplog): + """Test error message when symlink creation fails.""" + source_dir, target_dir = temp_dirs + username = os.environ["USER"] + + # Create source file + source_file = os.path.join(source_dir, "test.txt") + target_file = os.path.join(target_dir, "test.txt") + + with open(source_file, "w", encoding="utf-8") as f: + f.write("source") + with open(target_file, "w", encoding="utf-8") as f: + f.write("target") + + # Mock os.symlink to raise an error + def mock_symlink(src, dst): + raise OSError("Simulated symlink error") + + 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) + + # Check error message + assert "Error creating symlink" in caplog.text + assert source_file in caplog.text diff --git a/tests/relink/test_timing.py b/tests/relink/test_timing.py new file mode 100644 index 0000000..1ff8ae5 --- /dev/null +++ b/tests/relink/test_timing.py @@ -0,0 +1,103 @@ +""" +Tests for relink.py script. +""" + +import os +import sys +import tempfile +import shutil +import logging +from unittest.mock import patch + +import pytest + +# 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 + + +@pytest.fixture(scope="function", autouse=True) +def configure_logging(): + """Configure logging to output to stdout for all tests.""" + # Configure logging before each test + logging.basicConfig( + level=logging.INFO, + format="%(message)s", + stream=sys.stdout, + force=True, # Force reconfiguration + ) + yield + # Clean up logging handlers after each test + logging.getLogger().handlers.clear() + + +@pytest.fixture(scope="function", name="mock_default_dirs") +def fixture_mock_default_dirs(): + """Mock the default directories to use temporary directories.""" + source_dir = tempfile.mkdtemp(prefix="test_default_source_") + target_dir = tempfile.mkdtemp(prefix="test_default_target_") + + with patch.object(relink, "DEFAULT_SOURCE_ROOT", source_dir): + with patch.object(relink, "DEFAULT_TARGET_ROOT", target_dir): + yield source_dir, target_dir + + # Cleanup + shutil.rmtree(source_dir, ignore_errors=True) + shutil.rmtree(target_dir, ignore_errors=True) + + +@pytest.fixture(scope="function", name="temp_dirs") +def fixture_temp_dirs(): + """Create temporary source and target directories for testing.""" + source_dir = tempfile.mkdtemp(prefix="test_source_") + target_dir = tempfile.mkdtemp(prefix="test_target_") + + yield source_dir, target_dir + + # Cleanup + shutil.rmtree(source_dir, ignore_errors=True) + shutil.rmtree(target_dir, ignore_errors=True) + + +@pytest.mark.parametrize( + "use_timing, should_log_timing", [(True, True), (False, False)] +) +def test_timing_logging(tmp_path, caplog, use_timing, should_log_timing): + """Test that timing message is logged only when --timing 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 or without --timing flag + test_argv = [ + "relink.py", + "--source-root", + str(source_dir), + "--target-root", + str(target_dir), + ] + if use_timing: + test_argv.append("--timing") + + with patch("sys.argv", test_argv): + with caplog.at_level(logging.INFO): + # Call main() which includes the timing logic + relink.main() + + # Verify timing message presence based on flag + if should_log_timing: + assert "Execution time:" in caplog.text + assert "seconds" in caplog.text + else: + assert "Execution time:" not in caplog.text diff --git a/tests/relink/test_verbosity.py b/tests/relink/test_verbosity.py new file mode 100644 index 0000000..efe1777 --- /dev/null +++ b/tests/relink/test_verbosity.py @@ -0,0 +1,171 @@ +""" +Tests for relink.py script. +""" + +import os +import sys +import tempfile +import shutil +import logging +from unittest.mock import patch + +import pytest + +# 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 + + +@pytest.fixture(scope="function", autouse=True) +def configure_logging(): + """Configure logging to output to stdout for all tests.""" + # Configure logging before each test + logging.basicConfig( + level=logging.INFO, + format="%(message)s", + stream=sys.stdout, + force=True, # Force reconfiguration + ) + yield + # Clean up logging handlers after each test + logging.getLogger().handlers.clear() + + +@pytest.fixture(scope="function", name="mock_default_dirs") +def fixture_mock_default_dirs(): + """Mock the default directories to use temporary directories.""" + source_dir = tempfile.mkdtemp(prefix="test_default_source_") + target_dir = tempfile.mkdtemp(prefix="test_default_target_") + + with patch.object(relink, "DEFAULT_SOURCE_ROOT", source_dir): + with patch.object(relink, "DEFAULT_TARGET_ROOT", target_dir): + yield source_dir, target_dir + + # Cleanup + shutil.rmtree(source_dir, ignore_errors=True) + shutil.rmtree(target_dir, ignore_errors=True) + + +@pytest.fixture(scope="function", name="temp_dirs") +def fixture_temp_dirs(): + """Create temporary source and target directories for testing.""" + source_dir = tempfile.mkdtemp(prefix="test_source_") + target_dir = tempfile.mkdtemp(prefix="test_target_") + + yield source_dir, target_dir + + # Cleanup + shutil.rmtree(source_dir, ignore_errors=True) + shutil.rmtree(target_dir, ignore_errors=True) + + +@pytest.fixture(name="current_user") +def fixture_current_user(): + """Get the current user's username.""" + username = os.environ["USER"] + return username + + +def test_quiet_mode_suppresses_info_messages(temp_dirs, caplog): + """Test that quiet mode suppresses INFO level messages.""" + source_dir, target_dir = temp_dirs + username = os.environ["USER"] + + # Create files + source_file = os.path.join(source_dir, "test_file.txt") + target_file = os.path.join(target_dir, "test_file.txt") + + with open(source_file, "w", encoding="utf-8") as f: + f.write("source") + with open(target_file, "w", encoding="utf-8") as f: + f.write("target") + + # Create a symlink to test "Skipping symlink" message + source_link = os.path.join(source_dir, "existing_link.txt") + dummy_target = os.path.join(tempfile.gettempdir(), "somewhere") + os.symlink(dummy_target, source_link) + + # 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) + + # Verify INFO messages are NOT in the log + assert "Searching for files owned by" not in caplog.text + assert "Skipping symlink:" not in caplog.text + assert "Found owned file:" not in caplog.text + assert "Deleted original file:" not in caplog.text + assert "Created symbolic link:" not in caplog.text + + +def test_quiet_mode_shows_warnings(temp_dirs, caplog): + """Test that quiet mode still shows WARNING level messages.""" + source_dir, target_dir = temp_dirs + username = os.environ["USER"] + + # Create only source file (no corresponding target) to trigger warning + source_file = os.path.join(source_dir, "orphan.txt") + with open(source_file, "w", encoding="utf-8") as f: + f.write("orphan content") + + # 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) + + # Verify WARNING message IS in the log + assert "Warning: Corresponding file not found" in caplog.text + + +def test_quiet_mode_shows_errors(temp_dirs, caplog): + """Test that quiet mode still shows ERROR level messages.""" + source_dir, target_dir = temp_dirs + username = os.environ["USER"] + + # 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) + assert "Error: User" in caplog.text + assert "not found" in caplog.text + + # Clear the log for next test + caplog.clear() + + # Test 2: Error deleting file + source_file = os.path.join(source_dir, "test.txt") + target_file = os.path.join(target_dir, "test.txt") + + with open(source_file, "w", encoding="utf-8") as f: + f.write("source") + with open(target_file, "w", encoding="utf-8") as f: + f.write("target") + + def mock_rename(src, dst): + raise OSError("Simulated rename error") + + 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) + assert "Error deleting file" in caplog.text + + # Clear the log for next test + caplog.clear() + + # Test 3: Error creating symlink + source_file2 = os.path.join(source_dir, "test2.txt") + target_file2 = os.path.join(target_dir, "test2.txt") + + with open(source_file2, "w", encoding="utf-8") as f: + f.write("source2") + with open(target_file2, "w", encoding="utf-8") as f: + f.write("target2") + + def mock_symlink(src, dst): + raise OSError("Simulated symlink error") + + 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) + assert "Error creating symlink" in caplog.text diff --git a/tests/test_relink.py b/tests/test_relink.py deleted file mode 100644 index 1c4e5bf..0000000 --- a/tests/test_relink.py +++ /dev/null @@ -1,1026 +0,0 @@ -""" -Tests for relink.py script. -""" - -import os -import sys -import tempfile -import shutil -import pwd -import logging -import argparse -import subprocess -from unittest.mock import patch - -import pytest - -# Add parent directory to path to import relink module -sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) -# pylint: disable=wrong-import-position -import relink # noqa: E402 - - -@pytest.fixture(scope="function", autouse=True) -def configure_logging(): - """Configure logging to output to stdout for all tests.""" - # Configure logging before each test - logging.basicConfig( - level=logging.INFO, - format="%(message)s", - stream=sys.stdout, - force=True, # Force reconfiguration - ) - yield - # Clean up logging handlers after each test - logging.getLogger().handlers.clear() - - -@pytest.fixture(scope="function", name="mock_default_dirs") -def fixture_mock_default_dirs(): - """Mock the default directories to use temporary directories.""" - source_dir = tempfile.mkdtemp(prefix="test_default_source_") - target_dir = tempfile.mkdtemp(prefix="test_default_target_") - - with patch.object(relink, "DEFAULT_SOURCE_ROOT", source_dir): - with patch.object(relink, "DEFAULT_TARGET_ROOT", target_dir): - yield source_dir, target_dir - - # Cleanup - shutil.rmtree(source_dir, ignore_errors=True) - shutil.rmtree(target_dir, ignore_errors=True) - - -@pytest.fixture(scope="function", name="temp_dirs") -def fixture_temp_dirs(): - """Create temporary source and target directories for testing.""" - source_dir = tempfile.mkdtemp(prefix="test_source_") - target_dir = tempfile.mkdtemp(prefix="test_target_") - - yield source_dir, target_dir - - # Cleanup - shutil.rmtree(source_dir, ignore_errors=True) - shutil.rmtree(target_dir, ignore_errors=True) - - -class TestFindAndReplaceOwnedFiles: - """Test suite for find_and_replace_owned_files function.""" - - @pytest.fixture - def current_user(self): - """Get the current user's username.""" - username = os.environ["USER"] - return username - - def test_basic_file_replacement(self, temp_dirs, current_user): - """Test basic functionality: replace owned file with symlink.""" - source_dir, target_dir = temp_dirs - username = current_user - - # Create a file in source directory - source_file = os.path.join(source_dir, "test_file.txt") - with open(source_file, "w", encoding="utf-8") as f: - f.write("source content") - - # Create corresponding file in target directory - target_file = os.path.join(target_dir, "test_file.txt") - with open(target_file, "w", encoding="utf-8") as f: - f.write("target content") - - # Run the function - relink.find_and_replace_owned_files(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" - assert ( - os.readlink(source_file) == target_file - ), "Symlink should point to target file" - - def test_nested_directory_structure(self, temp_dirs, current_user): - """Test with nested directory structures.""" - source_dir, target_dir = temp_dirs - username = current_user - - # Create nested directories - nested_path = os.path.join("subdir1", "subdir2") - os.makedirs(os.path.join(source_dir, nested_path)) - os.makedirs(os.path.join(target_dir, nested_path)) - - # Create files in nested directories - source_file = os.path.join(source_dir, nested_path, "nested_file.txt") - target_file = os.path.join(target_dir, nested_path, "nested_file.txt") - - with open(source_file, "w", encoding="utf-8") as f: - f.write("nested source") - with open(target_file, "w", encoding="utf-8") as f: - f.write("nested target") - - # Run the function - relink.find_and_replace_owned_files(source_dir, target_dir, username) - - # Verify - assert os.path.islink(source_file), "Nested file should be a symlink" - assert os.readlink(source_file) == target_file - - def test_skip_existing_symlinks(self, temp_dirs, current_user, caplog): - """Test that existing symlinks are skipped.""" - source_dir, target_dir = temp_dirs - username = current_user - - # Create a target file - target_file = os.path.join(target_dir, "target.txt") - with open(target_file, "w", encoding="utf-8") as f: - f.write("target") - - # Create a symlink in source (pointing somewhere else) - source_link = os.path.join(source_dir, "existing_link.txt") - dummy_target = os.path.join(tempfile.gettempdir(), "somewhere") - os.symlink(dummy_target, source_link) - - # Get the inode and mtime before running the function - stat_before = os.lstat(source_link) - - # Run the function - with caplog.at_level(logging.INFO): - relink.find_and_replace_owned_files(source_dir, target_dir, username) - - # Verify the symlink is unchanged (same inode means it wasn't deleted/recreated) - stat_after = os.lstat(source_link) - assert ( - stat_before.st_ino == stat_after.st_ino - ), "Symlink should not have been recreated" - assert ( - stat_before.st_mtime == stat_after.st_mtime - ), "Symlink mtime should be unchanged" - assert ( - os.readlink(source_link) == dummy_target - ), "Symlink target should be unchanged" - - # Check that "Skipping symlink" message was logged - assert "Skipping symlink:" in caplog.text - assert source_link in caplog.text - - def test_missing_target_file(self, temp_dirs, current_user, caplog): - """Test behavior when target file doesn't exist.""" - source_dir, target_dir = temp_dirs - username = current_user - - # Create only source file (no corresponding target) - source_file = os.path.join(source_dir, "orphan.txt") - with open(source_file, "w", encoding="utf-8") as f: - f.write("orphan content") - - # Run the function - with caplog.at_level(logging.INFO): - relink.find_and_replace_owned_files(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" - assert os.path.isfile(source_file), "Original file should still exist" - - # Check warning message - assert "Warning: Corresponding file not found" in caplog.text - - def test_invalid_username(self, temp_dirs, caplog): - """Test behavior with invalid username.""" - source_dir, target_dir = temp_dirs - - # Use a username that doesn't exist - invalid_username = "nonexistent_user_12345" - try: - pwd.getpwnam(invalid_username).pw_uid - except KeyError: - pass - else: - raise RuntimeError(f"{invalid_username=} DOES actually exist") - - # Run the function - with caplog.at_level(logging.INFO): - relink.find_and_replace_owned_files( - source_dir, target_dir, invalid_username - ) - - # Check error message - assert "Error: User" in caplog.text - assert "not found" in caplog.text - - def test_multiple_files(self, temp_dirs, current_user): - """Test with multiple files in the directory.""" - source_dir, target_dir = temp_dirs - username = current_user - - # Create multiple files - for i in range(5): - source_file = os.path.join(source_dir, f"file_{i}.txt") - target_file = os.path.join(target_dir, f"file_{i}.txt") - - with open(source_file, "w", encoding="utf-8") as f: - f.write(f"source {i}") - with open(target_file, "w", encoding="utf-8") as f: - f.write(f"target {i}") - - # Run the function - relink.find_and_replace_owned_files(source_dir, target_dir, username) - - # Verify all files are symlinks - for i in range(5): - source_file = os.path.join(source_dir, f"file_{i}.txt") - target_file = os.path.join(target_dir, f"file_{i}.txt") - assert os.path.islink(source_file) - assert os.readlink(source_file) == target_file - - def test_absolute_paths(self, temp_dirs, current_user): - """Test that function handles relative paths by converting to absolute.""" - source_dir, target_dir = temp_dirs - username = current_user - - # Create test files - source_file = os.path.join(source_dir, "test.txt") - target_file = os.path.join(target_dir, "test.txt") - - with open(source_file, "w", encoding="utf-8") as f: - f.write("test") - with open(target_file, "w", encoding="utf-8") as f: - f.write("test target") - - # Use relative paths (if possible) - cwd = os.getcwd() - try: - os.chdir(os.path.dirname(source_dir)) - rel_source = os.path.basename(source_dir) - rel_target = os.path.basename(target_dir) - - # Run with relative paths - relink.find_and_replace_owned_files(rel_source, rel_target, username) - - # Verify it still works - assert os.path.islink(source_file) - finally: - os.chdir(cwd) - - def test_print_searching_message(self, temp_dirs, current_user, caplog): - """Test that searching message is printed.""" - source_dir, target_dir = temp_dirs - username = current_user - - # Run the function - with caplog.at_level(logging.INFO): - relink.find_and_replace_owned_files(source_dir, target_dir, username) - - # Check that searching message was logged - assert f"Searching for files owned by '{username}'" in caplog.text - assert f"in '{os.path.abspath(source_dir)}'" in caplog.text - - def test_print_found_owned_file(self, temp_dirs, current_user, caplog): - """Test that 'Found owned file' message is printed.""" - source_dir, target_dir = temp_dirs - username = current_user - - # Create a file owned by current user - source_file = os.path.join(source_dir, "owned_file.txt") - target_file = os.path.join(target_dir, "owned_file.txt") - - with open(source_file, "w", encoding="utf-8") as f: - f.write("content") - with open(target_file, "w", encoding="utf-8") as f: - f.write("target content") - - # Run the function - with caplog.at_level(logging.INFO): - relink.find_and_replace_owned_files(source_dir, target_dir, username) - - # Check that "Found owned file" message was logged - assert "Found owned file:" in caplog.text - assert source_file in caplog.text - - def test_print_deleted_and_created_messages(self, temp_dirs, current_user, caplog): - """Test that deleted and created symlink messages are printed.""" - source_dir, target_dir = temp_dirs - username = current_user - - # Create files - source_file = os.path.join(source_dir, "test_file.txt") - target_file = os.path.join(target_dir, "test_file.txt") - - with open(source_file, "w", encoding="utf-8") as f: - f.write("source") - with open(target_file, "w", encoding="utf-8") as f: - f.write("target") - - # Run the function - with caplog.at_level(logging.INFO): - relink.find_and_replace_owned_files(source_dir, target_dir, username) - - # Check messages - assert "Deleted original file:" in caplog.text - assert "Created symbolic link:" in caplog.text - assert f"{source_file} -> {target_file}" in caplog.text - - def test_handles_file_deleted_during_traversal( - self, 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(self, temp_dirs, caplog): - """Test error message when symlink creation fails.""" - source_dir, target_dir = temp_dirs - username = os.environ["USER"] - - # Create source file - source_file = os.path.join(source_dir, "test.txt") - target_file = os.path.join(target_dir, "test.txt") - - with open(source_file, "w", encoding="utf-8") as f: - f.write("source") - with open(target_file, "w", encoding="utf-8") as f: - f.write("target") - - # Mock os.symlink to raise an error - def mock_symlink(src, dst): - raise OSError("Simulated symlink error") - - 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) - - # Check error message - assert "Error creating symlink" in caplog.text - assert source_file in caplog.text - - -class TestParseArguments: - """Test suite for parse_arguments function.""" - - def test_default_arguments(self, mock_default_dirs): - """Test that default arguments are used when none provided.""" - source_dir, target_dir = mock_default_dirs - with patch("sys.argv", ["relink.py"]): - args = relink.parse_arguments() - assert args.source_root == source_dir - assert args.target_root == target_dir - - def test_custom_source_root(self, mock_default_dirs, tmp_path): - """Test custom source root argument.""" - _, target_dir = mock_default_dirs - custom_source = tmp_path / "custom_source" - custom_source.mkdir() - with patch("sys.argv", ["relink.py", "--source-root", str(custom_source)]): - args = relink.parse_arguments() - assert args.source_root == str(custom_source.resolve()) - assert args.target_root == target_dir - - def test_custom_target_root(self, mock_default_dirs, tmp_path): - """Test custom target root argument.""" - source_dir, _ = mock_default_dirs - custom_target = tmp_path / "custom_target" - custom_target.mkdir() - with patch("sys.argv", ["relink.py", "--target-root", str(custom_target)]): - args = relink.parse_arguments() - assert args.source_root == source_dir - assert args.target_root == str(custom_target.resolve()) - - def test_both_custom_paths(self, tmp_path): - """Test both custom source and target roots.""" - source_path = tmp_path / "custom_source" - target_path = tmp_path / "custom_target" - source_path.mkdir() - target_path.mkdir() - with patch( - "sys.argv", - [ - "relink.py", - "--source-root", - str(source_path), - "--target-root", - str(target_path), - ], - ): - args = relink.parse_arguments() - assert args.source_root == str(source_path.resolve()) - assert args.target_root == str(target_path.resolve()) - - def test_verbose_flag(self, mock_default_dirs): # pylint: disable=unused-argument - """Test that --verbose flag is parsed correctly.""" - with patch("sys.argv", ["relink.py", "--verbose"]): - args = relink.parse_arguments() - assert args.verbose is True - assert args.quiet is False - - def test_quiet_flag(self, mock_default_dirs): # pylint: disable=unused-argument - """Test that --quiet flag is parsed correctly.""" - with patch("sys.argv", ["relink.py", "--quiet"]): - args = relink.parse_arguments() - assert args.quiet is True - assert args.verbose is False - - def test_verbose_short_flag( - self, mock_default_dirs - ): # pylint: disable=unused-argument - """Test that -v flag is parsed correctly.""" - with patch("sys.argv", ["relink.py", "-v"]): - args = relink.parse_arguments() - assert args.verbose is True - - def test_quiet_short_flag( - self, mock_default_dirs - ): # pylint: disable=unused-argument - """Test that -q flag is parsed correctly.""" - with patch("sys.argv", ["relink.py", "-q"]): - args = relink.parse_arguments() - assert args.quiet is True - - def test_default_verbosity( - self, mock_default_dirs - ): # pylint: disable=unused-argument - """Test that default verbosity has both flags as False.""" - with patch("sys.argv", ["relink.py"]): - args = relink.parse_arguments() - assert args.verbose is False - assert args.quiet is False - - def test_verbose_and_quiet_mutually_exclusive(self, mock_default_dirs): - """Test that --verbose and --quiet cannot be used together.""" - # pylint: disable=unused-argument - with patch("sys.argv", ["relink.py", "--verbose", "--quiet"]): - with pytest.raises(SystemExit) as exc_info: - relink.parse_arguments() - # Mutually exclusive arguments cause SystemExit with code 2 - assert exc_info.value.code == 2 - - def test_verbose_and_quiet_short_flags_mutually_exclusive(self, mock_default_dirs): - """Test that -v and -q cannot be used together.""" - # pylint: disable=unused-argument - with patch("sys.argv", ["relink.py", "-v", "-q"]): - with pytest.raises(SystemExit) as exc_info: - relink.parse_arguments() - # Mutually exclusive arguments cause SystemExit with code 2 - assert exc_info.value.code == 2 - - def test_dry_run_flag(self, mock_default_dirs): - """Test that --dry-run flag is parsed correctly.""" - # pylint: disable=unused-argument - with patch("sys.argv", ["relink.py", "--dry-run"]): - args = relink.parse_arguments() - assert args.dry_run is True - - def test_dry_run_default(self, mock_default_dirs): - """Test that dry_run defaults to False.""" - # pylint: disable=unused-argument - with patch("sys.argv", ["relink.py"]): - args = relink.parse_arguments() - assert args.dry_run is False - - def test_timing_flag(self, mock_default_dirs): - """Test that --timing flag is parsed correctly.""" - # pylint: disable=unused-argument - with patch("sys.argv", ["relink.py", "--timing"]): - args = relink.parse_arguments() - assert args.timing is True - - def test_timing_default(self, mock_default_dirs): - """Test that timing defaults to False.""" - # pylint: disable=unused-argument - with patch("sys.argv", ["relink.py"]): - args = relink.parse_arguments() - assert args.timing is False - - -class TestVerbosityLevels: - """Test suite for verbosity level behavior.""" - - def test_quiet_mode_suppresses_info_messages(self, temp_dirs, caplog): - """Test that quiet mode suppresses INFO level messages.""" - source_dir, target_dir = temp_dirs - username = os.environ["USER"] - - # Create files - source_file = os.path.join(source_dir, "test_file.txt") - target_file = os.path.join(target_dir, "test_file.txt") - - with open(source_file, "w", encoding="utf-8") as f: - f.write("source") - with open(target_file, "w", encoding="utf-8") as f: - f.write("target") - - # Create a symlink to test "Skipping symlink" message - source_link = os.path.join(source_dir, "existing_link.txt") - dummy_target = os.path.join(tempfile.gettempdir(), "somewhere") - os.symlink(dummy_target, source_link) - - # 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) - - # Verify INFO messages are NOT in the log - assert "Searching for files owned by" not in caplog.text - assert "Skipping symlink:" not in caplog.text - assert "Found owned file:" not in caplog.text - assert "Deleted original file:" not in caplog.text - assert "Created symbolic link:" not in caplog.text - - def test_quiet_mode_shows_warnings(self, temp_dirs, caplog): - """Test that quiet mode still shows WARNING level messages.""" - source_dir, target_dir = temp_dirs - username = os.environ["USER"] - - # Create only source file (no corresponding target) to trigger warning - source_file = os.path.join(source_dir, "orphan.txt") - with open(source_file, "w", encoding="utf-8") as f: - f.write("orphan content") - - # 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) - - # Verify WARNING message IS in the log - assert "Warning: Corresponding file not found" in caplog.text - - def test_quiet_mode_shows_errors(self, temp_dirs, caplog): - """Test that quiet mode still shows ERROR level messages.""" - source_dir, target_dir = temp_dirs - username = os.environ["USER"] - - # 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 - ) - assert "Error: User" in caplog.text - assert "not found" in caplog.text - - # Clear the log for next test - caplog.clear() - - # Test 2: Error deleting file - source_file = os.path.join(source_dir, "test.txt") - target_file = os.path.join(target_dir, "test.txt") - - with open(source_file, "w", encoding="utf-8") as f: - f.write("source") - with open(target_file, "w", encoding="utf-8") as f: - f.write("target") - - def mock_rename(src, dst): - raise OSError("Simulated rename error") - - 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) - assert "Error deleting file" in caplog.text - - # Clear the log for next test - caplog.clear() - - # Test 3: Error creating symlink - source_file2 = os.path.join(source_dir, "test2.txt") - target_file2 = os.path.join(target_dir, "test2.txt") - - with open(source_file2, "w", encoding="utf-8") as f: - f.write("source2") - with open(target_file2, "w", encoding="utf-8") as f: - f.write("target2") - - def mock_symlink(src, dst): - raise OSError("Simulated symlink error") - - 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) - assert "Error creating symlink" in caplog.text - - -class TestEdgeCases: - """Test edge cases and error handling.""" - - def test_empty_directories(self, temp_dirs): - """Test with empty directories.""" - source_dir, target_dir = temp_dirs - username = os.environ["USER"] - - # Run with empty directories (should not crash) - relink.find_and_replace_owned_files(source_dir, target_dir, username) - - # Should complete without errors - assert True - - def test_file_with_spaces_in_name(self, temp_dirs): - """Test files with spaces in their names.""" - source_dir, target_dir = temp_dirs - username = os.environ["USER"] - - # Create files with spaces - source_file = os.path.join(source_dir, "file with spaces.txt") - target_file = os.path.join(target_dir, "file with spaces.txt") - - with open(source_file, "w", encoding="utf-8") as f: - f.write("content") - with open(target_file, "w", encoding="utf-8") as f: - f.write("target content") - - # Run the function - relink.find_and_replace_owned_files(source_dir, target_dir, username) - - # Verify - assert os.path.islink(source_file) - assert os.readlink(source_file) == target_file - - def test_file_with_special_characters(self, temp_dirs): - """Test files with special characters in names.""" - source_dir, target_dir = temp_dirs - username = os.environ["USER"] - - # Create files with special chars (that are valid in filenames) - filename = "file-with_special.chars@123.txt" - source_file = os.path.join(source_dir, filename) - target_file = os.path.join(target_dir, filename) - - with open(source_file, "w", encoding="utf-8") as f: - f.write("content") - with open(target_file, "w", encoding="utf-8") as f: - f.write("target content") - - # Run the function - relink.find_and_replace_owned_files(source_dir, target_dir, username) - - # Verify - assert os.path.islink(source_file) - assert os.readlink(source_file) == target_file - - def test_error_deleting_file(self, temp_dirs, caplog): - """Test error message when file deletion fails.""" - source_dir, target_dir = temp_dirs - username = os.environ["USER"] - - # Create files - source_file = os.path.join(source_dir, "test.txt") - target_file = os.path.join(target_dir, "test.txt") - - with open(source_file, "w", encoding="utf-8") as f: - f.write("source") - with open(target_file, "w", encoding="utf-8") as f: - f.write("target") - - # Mock os.rename to raise an error - def mock_rename(src, dst): - raise OSError("Simulated rename error") - - 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) - - # Check error message - assert "Error deleting file" in caplog.text - assert source_file in caplog.text - - def test_error_creating_symlink(self, temp_dirs, caplog): - """Test error message when symlink creation fails.""" - source_dir, target_dir = temp_dirs - username = os.environ["USER"] - - # Create source file - source_file = os.path.join(source_dir, "test.txt") - target_file = os.path.join(target_dir, "test.txt") - - with open(source_file, "w", encoding="utf-8") as f: - f.write("source") - with open(target_file, "w", encoding="utf-8") as f: - f.write("target") - - # Mock os.symlink to raise an error - def mock_symlink(src, dst): - raise OSError("Simulated symlink error") - - 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) - - # Check error message - assert "Error creating symlink" in caplog.text - assert source_file in caplog.text - - -class TestTiming: - """Test suite for timing functionality.""" - - @pytest.mark.parametrize( - "use_timing, should_log_timing", [(True, True), (False, False)] - ) - def test_timing_logging(self, tmp_path, caplog, use_timing, should_log_timing): - """Test that timing message is logged only when --timing 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 or without --timing flag - test_argv = [ - "relink.py", - "--source-root", - str(source_dir), - "--target-root", - str(target_dir), - ] - if use_timing: - test_argv.append("--timing") - - with patch("sys.argv", test_argv): - with caplog.at_level(logging.INFO): - # Call main() which includes the timing logic - relink.main() - - # Verify timing message presence based on flag - if should_log_timing: - assert "Execution time:" in caplog.text - assert "seconds" in caplog.text - else: - assert "Execution time:" not in caplog.text - - -class TestValidateDirectory: - """Test suite for validate_directory function.""" - - def test_valid_directory(self, tmp_path): - """Test that valid directory is accepted and returns absolute path.""" - test_dir = tmp_path / "valid_dir" - test_dir.mkdir() - - result = relink.validate_directory(str(test_dir)) - assert result == str(test_dir.resolve()) - - def test_nonexistent_directory(self): - """Test that nonexistent directory raises ArgumentTypeError.""" - nonexistent = os.path.join(os.sep, "nonexistent", "directory", "12345") - - with pytest.raises(argparse.ArgumentTypeError) as exc_info: - relink.validate_directory(nonexistent) - - assert "does not exist" in str(exc_info.value) - assert nonexistent in str(exc_info.value) - - def test_file_instead_of_directory(self, tmp_path): - """Test that a file path raises ArgumentTypeError.""" - test_file = tmp_path / "test_file.txt" - test_file.write_text("content") - - with pytest.raises(argparse.ArgumentTypeError) as exc_info: - relink.validate_directory(str(test_file)) - - assert "not a directory" in str(exc_info.value) - - def test_relative_path_converted_to_absolute(self, tmp_path): - """Test that relative paths are converted to absolute.""" - test_dir = tmp_path / "relative_test" - test_dir.mkdir() - - # Change to parent directory and use relative path - cwd = os.getcwd() - try: - os.chdir(str(tmp_path)) - result = relink.validate_directory("relative_test") - assert os.path.isabs(result) - assert result == str(test_dir.resolve()) - finally: - os.chdir(cwd) - - def test_symlink_to_directory(self, tmp_path): - """Test that symlink to a directory is accepted.""" - real_dir = tmp_path / "real_dir" - real_dir.mkdir() - - link_dir = tmp_path / "link_dir" - link_dir.symlink_to(real_dir) - - result = relink.validate_directory(str(link_dir)) - # validate_directory returns absolute path of the symlink itself - assert result == str(link_dir.absolute()) - # Verify it's still a symlink - assert os.path.islink(result) - - -class TestDryRun: - """Test suite for dry-run functionality.""" - - @pytest.fixture - def dry_run_setup(self, temp_dirs): - """Set up directories and files for dry-run tests.""" - source_dir, target_dir = temp_dirs - username = os.environ["USER"] - - # Create files - source_file = os.path.join(source_dir, "test_file.txt") - target_file = os.path.join(target_dir, "test_file.txt") - - with open(source_file, "w", encoding="utf-8") as f: - f.write("source content") - with open(target_file, "w", encoding="utf-8") as f: - f.write("target content") - - return source_dir, target_dir, source_file, target_file, username - - def test_dry_run_no_changes(self, dry_run_setup, caplog): - """Test that dry-run mode makes no actual changes.""" - source_dir, target_dir, source_file, _, username = dry_run_setup - - # Get original file info - with open(source_file, "r", encoding="utf-8") as f: - original_content = f.read() - original_is_link = os.path.islink(source_file) - - # Run in dry-run mode - with caplog.at_level(logging.INFO): - relink.find_and_replace_owned_files( - source_dir, target_dir, username, dry_run=True - ) - - # Verify no changes were made - assert os.path.isfile(source_file), "Original file should still exist" - assert not os.path.islink(source_file), "File should not be a symlink" - with open(source_file, "r", encoding="utf-8") as f: - assert f.read() == original_content - assert os.path.islink(source_file) == original_is_link - - def test_dry_run_shows_message(self, dry_run_setup, caplog): - """Test that dry-run mode shows what would be done.""" - source_dir, target_dir, source_file, target_file, username = dry_run_setup - - # Run in dry-run mode - with caplog.at_level(logging.INFO): - relink.find_and_replace_owned_files( - source_dir, target_dir, username, dry_run=True - ) - - # Check that dry-run messages were logged - assert "DRY RUN MODE" in caplog.text - assert "[DRY RUN] Would create symbolic link:" in caplog.text - assert f"{source_file} -> {target_file}" in caplog.text - - def test_dry_run_no_delete_or_create_messages(self, dry_run_setup, caplog): - """Test that dry-run doesn't show delete/create messages.""" - source_dir, target_dir, _, _, username = dry_run_setup - - # Run in dry-run mode - with caplog.at_level(logging.INFO): - relink.find_and_replace_owned_files( - source_dir, target_dir, username, dry_run=True - ) - - # Verify actual operation messages are NOT logged - assert "Deleted original file:" not in caplog.text - assert "Created symbolic link:" not in caplog.text - # But the dry-run message should be there - assert "[DRY RUN] Would create symbolic link: " in caplog.text - - -class TestCommandLineExecution: - """Test suite for command-line execution of relink.py.""" - - @pytest.fixture - def mock_dirs(self, tmp_path): - """Create temporary directories and files for command-line testing.""" - source_dir = tmp_path / "source" - target_dir = tmp_path / "target" - source_dir.mkdir() - target_dir.mkdir() - - # Create a test file - source_file = source_dir / "test_file.txt" - target_file = target_dir / "test_file.txt" - source_file.write_text("source content") - target_file.write_text("target content") - - return source_dir, target_dir, source_file, target_file - - def test_command_line_execution_dry_run(self, mock_dirs): - """Test executing relink.py from command line with --dry-run flag.""" - source_dir, target_dir, source_file, _ = mock_dirs - - # Get the path to relink.py - relink_script = os.path.join( - os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "relink.py" - ) - - # Build the command - command = [ - sys.executable, - relink_script, - "--source-root", - str(source_dir), - "--target-root", - str(target_dir), - "--dry-run", - ] - - # Execute the command - result = subprocess.run(command, capture_output=True, text=True, check=False) - - # Verify the command executed successfully - assert result.returncode == 0, f"Command failed with stderr: {result.stderr}" - - # Verify dry-run messages in output - assert "DRY RUN MODE" in result.stdout - assert "[DRY RUN] Would create symbolic link:" in result.stdout - - # Verify no actual changes were made - assert source_file.is_file() - assert not source_file.is_symlink() - - def test_command_line_execution_actual_run(self, mock_dirs): - """Test executing relink.py from command line without dry-run.""" - source_dir, target_dir, source_file, target_file = mock_dirs - - # Get the path to relink.py - relink_script = os.path.join( - os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "relink.py" - ) - - # Build the command - command = [ - sys.executable, - relink_script, - "--source-root", - str(source_dir), - "--target-root", - str(target_dir), - ] - - # Execute the command - result = subprocess.run(command, capture_output=True, text=True, check=False) - - # Verify the command executed successfully - assert result.returncode == 0, f"Command failed with stderr: {result.stderr}" - - # Verify the file was converted to a symlink - assert source_file.is_symlink() - assert os.readlink(str(source_file)) == str(target_file) - - # Verify success messages in output - assert "Created symbolic link:" in result.stdout - - -class TestProcessArgs: - """Test suite for process_args function.""" - - # pylint: disable=no-member - - def test_process_args_quiet_sets_warning_level(self): - """Test that quiet flag sets log level to WARNING.""" - args = argparse.Namespace(quiet=True, verbose=False) - relink.process_args(args) - assert args.log_level == logging.WARNING - - def test_process_args_verbose_sets_debug_level(self): - """Test that verbose flag sets log level to DEBUG.""" - args = argparse.Namespace(quiet=False, verbose=True) - relink.process_args(args) - assert args.log_level == logging.DEBUG - - def test_process_args_default_sets_info_level(self): - """Test that default (no flags) sets log level to INFO.""" - args = argparse.Namespace(quiet=False, verbose=False) - relink.process_args(args) - assert args.log_level == logging.INFO - - def test_process_args_modifies_args_in_place(self): - """Test that process_args modifies the args object in place.""" - args = argparse.Namespace(quiet=False, verbose=False) - original_args = args - relink.process_args(args) - # Should be the same object, modified in place - assert args is original_args - assert hasattr(args, "log_level") From 05e25b55831a37b2493dd5a6295ae144f92d355a Mon Sep 17 00:00:00 2001 From: Sam Rabin Date: Thu, 15 Jan 2026 13:34:28 -0700 Subject: [PATCH 2/5] Delete a duplicate test. --- .../test_find_and_replace_owned_files.py | 28 ------------------- 1 file changed, 28 deletions(-) diff --git a/tests/relink/test_find_and_replace_owned_files.py b/tests/relink/test_find_and_replace_owned_files.py index cea7d72..9b06c28 100644 --- a/tests/relink/test_find_and_replace_owned_files.py +++ b/tests/relink/test_find_and_replace_owned_files.py @@ -462,31 +462,3 @@ def mock_rename(src, dst): # Check error message assert "Error deleting file" in caplog.text assert source_file in caplog.text - - -def test_error_creating_symlink(temp_dirs, caplog): - """Test error message when symlink creation fails.""" - source_dir, target_dir = temp_dirs - username = os.environ["USER"] - - # Create source file - source_file = os.path.join(source_dir, "test.txt") - target_file = os.path.join(target_dir, "test.txt") - - with open(source_file, "w", encoding="utf-8") as f: - f.write("source") - with open(target_file, "w", encoding="utf-8") as f: - f.write("target") - - # Mock os.symlink to raise an error - def mock_symlink(src, dst): - raise OSError("Simulated symlink error") - - 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) - - # Check error message - assert "Error creating symlink" in caplog.text - assert source_file in caplog.text From 633669f212a06d9d1e360f2339716ee53c9a5113 Mon Sep 17 00:00:00 2001 From: Sam Rabin Date: Thu, 15 Jan 2026 13:40:38 -0700 Subject: [PATCH 3/5] relink tests: Delete unused fixtures. --- tests/relink/test_args.py | 28 --- tests/relink/test_cmdline.py | 229 +++++++----------- tests/relink/test_dryrun.py | 31 --- .../test_find_and_replace_owned_files.py | 30 --- tests/relink/test_timing.py | 45 ---- tests/relink/test_verbosity.py | 37 --- 6 files changed, 85 insertions(+), 315 deletions(-) diff --git a/tests/relink/test_args.py b/tests/relink/test_args.py index f6e4bde..56f5e6b 100644 --- a/tests/relink/test_args.py +++ b/tests/relink/test_args.py @@ -20,21 +20,6 @@ import relink # noqa: E402 -@pytest.fixture(scope="function", autouse=True) -def configure_logging(): - """Configure logging to output to stdout for all tests.""" - # Configure logging before each test - logging.basicConfig( - level=logging.INFO, - format="%(message)s", - stream=sys.stdout, - force=True, # Force reconfiguration - ) - yield - # Clean up logging handlers after each test - logging.getLogger().handlers.clear() - - @pytest.fixture(scope="function", name="mock_default_dirs") def fixture_mock_default_dirs(): """Mock the default directories to use temporary directories.""" @@ -50,19 +35,6 @@ def fixture_mock_default_dirs(): shutil.rmtree(target_dir, ignore_errors=True) -@pytest.fixture(scope="function", name="temp_dirs") -def fixture_temp_dirs(): - """Create temporary source and target directories for testing.""" - source_dir = tempfile.mkdtemp(prefix="test_source_") - target_dir = tempfile.mkdtemp(prefix="test_target_") - - yield source_dir, target_dir - - # Cleanup - shutil.rmtree(source_dir, ignore_errors=True) - shutil.rmtree(target_dir, ignore_errors=True) - - class TestParseArguments: """Test suite for parse_arguments function.""" diff --git a/tests/relink/test_cmdline.py b/tests/relink/test_cmdline.py index 214a931..2274515 100644 --- a/tests/relink/test_cmdline.py +++ b/tests/relink/test_cmdline.py @@ -4,152 +4,93 @@ import os import sys -import tempfile -import shutil -import logging import subprocess -from unittest.mock import patch import pytest -# 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 - - -@pytest.fixture(scope="function", autouse=True) -def configure_logging(): - """Configure logging to output to stdout for all tests.""" - # Configure logging before each test - logging.basicConfig( - level=logging.INFO, - format="%(message)s", - stream=sys.stdout, - force=True, # Force reconfiguration + +@pytest.fixture(name="mock_dirs") +def fixture_mock_dirs(tmp_path): + """Create temporary directories and files for command-line testing.""" + source_dir = tmp_path / "source" + target_dir = tmp_path / "target" + source_dir.mkdir() + target_dir.mkdir() + + # Create a test file + source_file = source_dir / "test_file.txt" + target_file = target_dir / "test_file.txt" + source_file.write_text("source content") + target_file.write_text("target content") + + return source_dir, target_dir, source_file, target_file + + +def test_command_line_execution_dry_run(mock_dirs): + """Test executing relink.py from command line with --dry-run flag.""" + source_dir, target_dir, source_file, _ = mock_dirs + + # Get the path to relink.py + relink_script = os.path.join( + os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), + "relink.py", + ) + + # Build the command + command = [ + sys.executable, + relink_script, + "--source-root", + str(source_dir), + "--target-root", + str(target_dir), + "--dry-run", + ] + + # Execute the command + result = subprocess.run(command, capture_output=True, text=True, check=False) + + # Verify the command executed successfully + assert result.returncode == 0, f"Command failed with stderr: {result.stderr}" + + # Verify dry-run messages in output + assert "DRY RUN MODE" in result.stdout + assert "[DRY RUN] Would create symbolic link:" in result.stdout + + # Verify no actual changes were made + assert source_file.is_file() + assert not source_file.is_symlink() + + +def test_command_line_execution_actual_run(mock_dirs): + """Test executing relink.py from command line without dry-run.""" + source_dir, target_dir, source_file, target_file = mock_dirs + + # Get the path to relink.py + relink_script = os.path.join( + os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), + "relink.py", ) - yield - # Clean up logging handlers after each test - logging.getLogger().handlers.clear() - - -@pytest.fixture(scope="function", name="mock_default_dirs") -def fixture_mock_default_dirs(): - """Mock the default directories to use temporary directories.""" - source_dir = tempfile.mkdtemp(prefix="test_default_source_") - target_dir = tempfile.mkdtemp(prefix="test_default_target_") - - with patch.object(relink, "DEFAULT_SOURCE_ROOT", source_dir): - with patch.object(relink, "DEFAULT_TARGET_ROOT", target_dir): - yield source_dir, target_dir - - # Cleanup - shutil.rmtree(source_dir, ignore_errors=True) - shutil.rmtree(target_dir, ignore_errors=True) - - -@pytest.fixture(scope="function", name="temp_dirs") -def fixture_temp_dirs(): - """Create temporary source and target directories for testing.""" - source_dir = tempfile.mkdtemp(prefix="test_source_") - target_dir = tempfile.mkdtemp(prefix="test_target_") - - yield source_dir, target_dir - - # Cleanup - shutil.rmtree(source_dir, ignore_errors=True) - shutil.rmtree(target_dir, ignore_errors=True) - - -class TestCommandLineExecution: - """Test suite for command-line execution of relink.py.""" - - @pytest.fixture - def mock_dirs(self, tmp_path): - """Create temporary directories and files for command-line testing.""" - source_dir = tmp_path / "source" - target_dir = tmp_path / "target" - source_dir.mkdir() - target_dir.mkdir() - - # Create a test file - source_file = source_dir / "test_file.txt" - target_file = target_dir / "test_file.txt" - source_file.write_text("source content") - target_file.write_text("target content") - - return source_dir, target_dir, source_file, target_file - - def test_command_line_execution_dry_run(self, mock_dirs): - """Test executing relink.py from command line with --dry-run flag.""" - source_dir, target_dir, source_file, _ = mock_dirs - - # Get the path to relink.py - relink_script = os.path.join( - os.path.dirname( - os.path.dirname(os.path.dirname(os.path.abspath(__file__))) - ), - "relink.py", - ) - - # Build the command - command = [ - sys.executable, - relink_script, - "--source-root", - str(source_dir), - "--target-root", - str(target_dir), - "--dry-run", - ] - - # Execute the command - result = subprocess.run(command, capture_output=True, text=True, check=False) - - # Verify the command executed successfully - assert result.returncode == 0, f"Command failed with stderr: {result.stderr}" - - # Verify dry-run messages in output - assert "DRY RUN MODE" in result.stdout - assert "[DRY RUN] Would create symbolic link:" in result.stdout - - # Verify no actual changes were made - assert source_file.is_file() - assert not source_file.is_symlink() - - def test_command_line_execution_actual_run(self, mock_dirs): - """Test executing relink.py from command line without dry-run.""" - source_dir, target_dir, source_file, target_file = mock_dirs - - # Get the path to relink.py - relink_script = os.path.join( - os.path.dirname( - os.path.dirname(os.path.dirname(os.path.abspath(__file__))) - ), - "relink.py", - ) - - # Build the command - command = [ - sys.executable, - relink_script, - "--source-root", - str(source_dir), - "--target-root", - str(target_dir), - ] - - # Execute the command - result = subprocess.run(command, capture_output=True, text=True, check=False) - - # Verify the command executed successfully - assert result.returncode == 0, f"Command failed with stderr: {result.stderr}" - - # Verify the file was converted to a symlink - assert source_file.is_symlink() - assert os.readlink(str(source_file)) == str(target_file) - - # Verify success messages in output - assert "Created symbolic link:" in result.stdout + + # Build the command + command = [ + sys.executable, + relink_script, + "--source-root", + str(source_dir), + "--target-root", + str(target_dir), + ] + + # Execute the command + result = subprocess.run(command, capture_output=True, text=True, check=False) + + # Verify the command executed successfully + assert result.returncode == 0, f"Command failed with stderr: {result.stderr}" + + # Verify the file was converted to a symlink + assert source_file.is_symlink() + assert os.readlink(str(source_file)) == str(target_file) + + # Verify success messages in output + assert "Created symbolic link:" in result.stdout diff --git a/tests/relink/test_dryrun.py b/tests/relink/test_dryrun.py index d43088d..ac729eb 100644 --- a/tests/relink/test_dryrun.py +++ b/tests/relink/test_dryrun.py @@ -7,7 +7,6 @@ import tempfile import shutil import logging -from unittest.mock import patch import pytest @@ -19,36 +18,6 @@ import relink # noqa: E402 -@pytest.fixture(scope="function", autouse=True) -def configure_logging(): - """Configure logging to output to stdout for all tests.""" - # Configure logging before each test - logging.basicConfig( - level=logging.INFO, - format="%(message)s", - stream=sys.stdout, - force=True, # Force reconfiguration - ) - yield - # Clean up logging handlers after each test - logging.getLogger().handlers.clear() - - -@pytest.fixture(scope="function", name="mock_default_dirs") -def fixture_mock_default_dirs(): - """Mock the default directories to use temporary directories.""" - source_dir = tempfile.mkdtemp(prefix="test_default_source_") - target_dir = tempfile.mkdtemp(prefix="test_default_target_") - - with patch.object(relink, "DEFAULT_SOURCE_ROOT", source_dir): - with patch.object(relink, "DEFAULT_TARGET_ROOT", target_dir): - yield source_dir, target_dir - - # Cleanup - shutil.rmtree(source_dir, ignore_errors=True) - shutil.rmtree(target_dir, ignore_errors=True) - - @pytest.fixture(scope="function", name="temp_dirs") def fixture_temp_dirs(): """Create temporary source and target directories for testing.""" diff --git a/tests/relink/test_find_and_replace_owned_files.py b/tests/relink/test_find_and_replace_owned_files.py index 9b06c28..f95efae 100644 --- a/tests/relink/test_find_and_replace_owned_files.py +++ b/tests/relink/test_find_and_replace_owned_files.py @@ -20,36 +20,6 @@ import relink # noqa: E402 -@pytest.fixture(scope="function", autouse=True) -def configure_logging(): - """Configure logging to output to stdout for all tests.""" - # Configure logging before each test - logging.basicConfig( - level=logging.INFO, - format="%(message)s", - stream=sys.stdout, - force=True, # Force reconfiguration - ) - yield - # Clean up logging handlers after each test - logging.getLogger().handlers.clear() - - -@pytest.fixture(scope="function", name="mock_default_dirs") -def fixture_mock_default_dirs(): - """Mock the default directories to use temporary directories.""" - source_dir = tempfile.mkdtemp(prefix="test_default_source_") - target_dir = tempfile.mkdtemp(prefix="test_default_target_") - - with patch.object(relink, "DEFAULT_SOURCE_ROOT", source_dir): - with patch.object(relink, "DEFAULT_TARGET_ROOT", target_dir): - yield source_dir, target_dir - - # Cleanup - shutil.rmtree(source_dir, ignore_errors=True) - shutil.rmtree(target_dir, ignore_errors=True) - - @pytest.fixture(scope="function", name="temp_dirs") def fixture_temp_dirs(): """Create temporary source and target directories for testing.""" diff --git a/tests/relink/test_timing.py b/tests/relink/test_timing.py index 1ff8ae5..79b9fe0 100644 --- a/tests/relink/test_timing.py +++ b/tests/relink/test_timing.py @@ -4,8 +4,6 @@ import os import sys -import tempfile -import shutil import logging from unittest.mock import patch @@ -19,49 +17,6 @@ import relink # noqa: E402 -@pytest.fixture(scope="function", autouse=True) -def configure_logging(): - """Configure logging to output to stdout for all tests.""" - # Configure logging before each test - logging.basicConfig( - level=logging.INFO, - format="%(message)s", - stream=sys.stdout, - force=True, # Force reconfiguration - ) - yield - # Clean up logging handlers after each test - logging.getLogger().handlers.clear() - - -@pytest.fixture(scope="function", name="mock_default_dirs") -def fixture_mock_default_dirs(): - """Mock the default directories to use temporary directories.""" - source_dir = tempfile.mkdtemp(prefix="test_default_source_") - target_dir = tempfile.mkdtemp(prefix="test_default_target_") - - with patch.object(relink, "DEFAULT_SOURCE_ROOT", source_dir): - with patch.object(relink, "DEFAULT_TARGET_ROOT", target_dir): - yield source_dir, target_dir - - # Cleanup - shutil.rmtree(source_dir, ignore_errors=True) - shutil.rmtree(target_dir, ignore_errors=True) - - -@pytest.fixture(scope="function", name="temp_dirs") -def fixture_temp_dirs(): - """Create temporary source and target directories for testing.""" - source_dir = tempfile.mkdtemp(prefix="test_source_") - target_dir = tempfile.mkdtemp(prefix="test_target_") - - yield source_dir, target_dir - - # Cleanup - shutil.rmtree(source_dir, ignore_errors=True) - shutil.rmtree(target_dir, ignore_errors=True) - - @pytest.mark.parametrize( "use_timing, should_log_timing", [(True, True), (False, False)] ) diff --git a/tests/relink/test_verbosity.py b/tests/relink/test_verbosity.py index efe1777..e73671e 100644 --- a/tests/relink/test_verbosity.py +++ b/tests/relink/test_verbosity.py @@ -19,36 +19,6 @@ import relink # noqa: E402 -@pytest.fixture(scope="function", autouse=True) -def configure_logging(): - """Configure logging to output to stdout for all tests.""" - # Configure logging before each test - logging.basicConfig( - level=logging.INFO, - format="%(message)s", - stream=sys.stdout, - force=True, # Force reconfiguration - ) - yield - # Clean up logging handlers after each test - logging.getLogger().handlers.clear() - - -@pytest.fixture(scope="function", name="mock_default_dirs") -def fixture_mock_default_dirs(): - """Mock the default directories to use temporary directories.""" - source_dir = tempfile.mkdtemp(prefix="test_default_source_") - target_dir = tempfile.mkdtemp(prefix="test_default_target_") - - with patch.object(relink, "DEFAULT_SOURCE_ROOT", source_dir): - with patch.object(relink, "DEFAULT_TARGET_ROOT", target_dir): - yield source_dir, target_dir - - # Cleanup - shutil.rmtree(source_dir, ignore_errors=True) - shutil.rmtree(target_dir, ignore_errors=True) - - @pytest.fixture(scope="function", name="temp_dirs") def fixture_temp_dirs(): """Create temporary source and target directories for testing.""" @@ -62,13 +32,6 @@ def fixture_temp_dirs(): shutil.rmtree(target_dir, ignore_errors=True) -@pytest.fixture(name="current_user") -def fixture_current_user(): - """Get the current user's username.""" - username = os.environ["USER"] - return username - - def test_quiet_mode_suppresses_info_messages(temp_dirs, caplog): """Test that quiet mode suppresses INFO level messages.""" source_dir, target_dir = temp_dirs From 3a7b50c4b99f2d1868c9e990b8bb628efb76fd0b Mon Sep 17 00:00:00 2001 From: Sam Rabin Date: Thu, 15 Jan 2026 13:46:07 -0700 Subject: [PATCH 4/5] relink tests: Move some fixtures to conftest.py. --- tests/relink/conftest.py | 29 +++++++++++++++++++ tests/relink/test_dryrun.py | 15 ---------- .../test_find_and_replace_owned_files.py | 23 --------------- tests/relink/test_verbosity.py | 16 ---------- 4 files changed, 29 insertions(+), 54 deletions(-) create mode 100644 tests/relink/conftest.py diff --git a/tests/relink/conftest.py b/tests/relink/conftest.py new file mode 100644 index 0000000..5a7eaa2 --- /dev/null +++ b/tests/relink/conftest.py @@ -0,0 +1,29 @@ +""" +Shared fixtures for relink tests. +""" + +import os +import tempfile +import shutil + +import pytest + + +@pytest.fixture(scope="function", name="temp_dirs") +def fixture_temp_dirs(): + """Create temporary source and target directories for testing.""" + source_dir = tempfile.mkdtemp(prefix="test_source_") + target_dir = tempfile.mkdtemp(prefix="test_target_") + + yield source_dir, target_dir + + # Cleanup + shutil.rmtree(source_dir, ignore_errors=True) + shutil.rmtree(target_dir, ignore_errors=True) + + +@pytest.fixture(name="current_user") +def fixture_current_user(): + """Get the current user's username.""" + username = os.environ["USER"] + return username diff --git a/tests/relink/test_dryrun.py b/tests/relink/test_dryrun.py index ac729eb..42a583d 100644 --- a/tests/relink/test_dryrun.py +++ b/tests/relink/test_dryrun.py @@ -4,8 +4,6 @@ import os import sys -import tempfile -import shutil import logging import pytest @@ -18,19 +16,6 @@ import relink # noqa: E402 -@pytest.fixture(scope="function", name="temp_dirs") -def fixture_temp_dirs(): - """Create temporary source and target directories for testing.""" - source_dir = tempfile.mkdtemp(prefix="test_source_") - target_dir = tempfile.mkdtemp(prefix="test_target_") - - yield source_dir, target_dir - - # Cleanup - shutil.rmtree(source_dir, ignore_errors=True) - shutil.rmtree(target_dir, ignore_errors=True) - - @pytest.fixture(name="dry_run_setup") def fixture_dry_run_setup(temp_dirs): """Set up directories and files for dry-run tests.""" diff --git a/tests/relink/test_find_and_replace_owned_files.py b/tests/relink/test_find_and_replace_owned_files.py index f95efae..c1e360b 100644 --- a/tests/relink/test_find_and_replace_owned_files.py +++ b/tests/relink/test_find_and_replace_owned_files.py @@ -5,13 +5,10 @@ import os import sys import tempfile -import shutil import pwd import logging from unittest.mock import patch -import pytest - # 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__)))) @@ -20,26 +17,6 @@ import relink # noqa: E402 -@pytest.fixture(scope="function", name="temp_dirs") -def fixture_temp_dirs(): - """Create temporary source and target directories for testing.""" - source_dir = tempfile.mkdtemp(prefix="test_source_") - target_dir = tempfile.mkdtemp(prefix="test_target_") - - yield source_dir, target_dir - - # Cleanup - shutil.rmtree(source_dir, ignore_errors=True) - shutil.rmtree(target_dir, ignore_errors=True) - - -@pytest.fixture(name="current_user") -def fixture_current_user(): - """Get the current user's username.""" - username = os.environ["USER"] - return username - - def test_basic_file_replacement(temp_dirs, current_user): """Test basic functionality: replace owned file with symlink.""" source_dir, target_dir = temp_dirs diff --git a/tests/relink/test_verbosity.py b/tests/relink/test_verbosity.py index e73671e..3251f5e 100644 --- a/tests/relink/test_verbosity.py +++ b/tests/relink/test_verbosity.py @@ -5,12 +5,9 @@ import os import sys import tempfile -import shutil import logging from unittest.mock import patch -import pytest - # 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__)))) @@ -19,19 +16,6 @@ import relink # noqa: E402 -@pytest.fixture(scope="function", name="temp_dirs") -def fixture_temp_dirs(): - """Create temporary source and target directories for testing.""" - source_dir = tempfile.mkdtemp(prefix="test_source_") - target_dir = tempfile.mkdtemp(prefix="test_target_") - - yield source_dir, target_dir - - # Cleanup - shutil.rmtree(source_dir, ignore_errors=True) - shutil.rmtree(target_dir, ignore_errors=True) - - def test_quiet_mode_suppresses_info_messages(temp_dirs, caplog): """Test that quiet mode suppresses INFO level messages.""" source_dir, target_dir = temp_dirs From 3a432b8ed82a8b7461de16953781866e94509e78 Mon Sep 17 00:00:00 2001 From: Sam Rabin Date: Thu, 15 Jan 2026 13:51:00 -0700 Subject: [PATCH 5/5] relink tests: Update module docstrings. --- tests/relink/test_args.py | 2 +- tests/relink/test_dryrun.py | 2 +- tests/relink/test_find_and_replace_owned_files.py | 2 +- tests/relink/test_timing.py | 2 +- tests/relink/test_verbosity.py | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/relink/test_args.py b/tests/relink/test_args.py index 56f5e6b..d9b3136 100644 --- a/tests/relink/test_args.py +++ b/tests/relink/test_args.py @@ -1,5 +1,5 @@ """ -Tests for relink.py script. +Tests related to argument parsing and processing in relink.py script. """ import os diff --git a/tests/relink/test_dryrun.py b/tests/relink/test_dryrun.py index 42a583d..c844d56 100644 --- a/tests/relink/test_dryrun.py +++ b/tests/relink/test_dryrun.py @@ -1,5 +1,5 @@ """ -Tests for relink.py script. +Tests for relink.py --dry-run option. """ import os diff --git a/tests/relink/test_find_and_replace_owned_files.py b/tests/relink/test_find_and_replace_owned_files.py index c1e360b..781a0f7 100644 --- a/tests/relink/test_find_and_replace_owned_files.py +++ b/tests/relink/test_find_and_replace_owned_files.py @@ -1,5 +1,5 @@ """ -Tests for relink.py script. +Tests of find_and_replace_owned_files() in relink.py """ import os diff --git a/tests/relink/test_timing.py b/tests/relink/test_timing.py index 79b9fe0..a60e949 100644 --- a/tests/relink/test_timing.py +++ b/tests/relink/test_timing.py @@ -1,5 +1,5 @@ """ -Tests for relink.py script. +Tests of relink.py --timing option """ import os diff --git a/tests/relink/test_verbosity.py b/tests/relink/test_verbosity.py index 3251f5e..d96dbd6 100644 --- a/tests/relink/test_verbosity.py +++ b/tests/relink/test_verbosity.py @@ -1,5 +1,5 @@ """ -Tests for relink.py script. +Tests of verbosity levels in relink.py script. """ import os