Skip to content

Commit b3dd56f

Browse files
authored
Merge pull request #11 from samsrabin/refactor-testing
Refactor testing
2 parents 31f741a + 3a432b8 commit b3dd56f

9 files changed

Lines changed: 1068 additions & 1026 deletions

tests/relink/__init__.py

Whitespace-only changes.

tests/relink/conftest.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
"""
2+
Shared fixtures for relink tests.
3+
"""
4+
5+
import os
6+
import tempfile
7+
import shutil
8+
9+
import pytest
10+
11+
12+
@pytest.fixture(scope="function", name="temp_dirs")
13+
def fixture_temp_dirs():
14+
"""Create temporary source and target directories for testing."""
15+
source_dir = tempfile.mkdtemp(prefix="test_source_")
16+
target_dir = tempfile.mkdtemp(prefix="test_target_")
17+
18+
yield source_dir, target_dir
19+
20+
# Cleanup
21+
shutil.rmtree(source_dir, ignore_errors=True)
22+
shutil.rmtree(target_dir, ignore_errors=True)
23+
24+
25+
@pytest.fixture(name="current_user")
26+
def fixture_current_user():
27+
"""Get the current user's username."""
28+
username = os.environ["USER"]
29+
return username

tests/relink/test_args.py

Lines changed: 266 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,266 @@
1+
"""
2+
Tests related to argument parsing and processing in relink.py script.
3+
"""
4+
5+
import os
6+
import sys
7+
import tempfile
8+
import shutil
9+
import logging
10+
import argparse
11+
from unittest.mock import patch
12+
13+
import pytest
14+
15+
# Add parent directory to path to import relink module
16+
sys.path.insert(
17+
0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
18+
)
19+
# pylint: disable=wrong-import-position
20+
import relink # noqa: E402
21+
22+
23+
@pytest.fixture(scope="function", name="mock_default_dirs")
24+
def fixture_mock_default_dirs():
25+
"""Mock the default directories to use temporary directories."""
26+
source_dir = tempfile.mkdtemp(prefix="test_default_source_")
27+
target_dir = tempfile.mkdtemp(prefix="test_default_target_")
28+
29+
with patch.object(relink, "DEFAULT_SOURCE_ROOT", source_dir):
30+
with patch.object(relink, "DEFAULT_TARGET_ROOT", target_dir):
31+
yield source_dir, target_dir
32+
33+
# Cleanup
34+
shutil.rmtree(source_dir, ignore_errors=True)
35+
shutil.rmtree(target_dir, ignore_errors=True)
36+
37+
38+
class TestParseArguments:
39+
"""Test suite for parse_arguments function."""
40+
41+
def test_default_arguments(self, mock_default_dirs):
42+
"""Test that default arguments are used when none provided."""
43+
source_dir, target_dir = mock_default_dirs
44+
with patch("sys.argv", ["relink.py"]):
45+
args = relink.parse_arguments()
46+
assert args.source_root == source_dir
47+
assert args.target_root == target_dir
48+
49+
def test_custom_source_root(self, mock_default_dirs, tmp_path):
50+
"""Test custom source root argument."""
51+
_, target_dir = mock_default_dirs
52+
custom_source = tmp_path / "custom_source"
53+
custom_source.mkdir()
54+
with patch("sys.argv", ["relink.py", "--source-root", str(custom_source)]):
55+
args = relink.parse_arguments()
56+
assert args.source_root == str(custom_source.resolve())
57+
assert args.target_root == target_dir
58+
59+
def test_custom_target_root(self, mock_default_dirs, tmp_path):
60+
"""Test custom target root argument."""
61+
source_dir, _ = mock_default_dirs
62+
custom_target = tmp_path / "custom_target"
63+
custom_target.mkdir()
64+
with patch("sys.argv", ["relink.py", "--target-root", str(custom_target)]):
65+
args = relink.parse_arguments()
66+
assert args.source_root == source_dir
67+
assert args.target_root == str(custom_target.resolve())
68+
69+
def test_both_custom_paths(self, tmp_path):
70+
"""Test both custom source and target roots."""
71+
source_path = tmp_path / "custom_source"
72+
target_path = tmp_path / "custom_target"
73+
source_path.mkdir()
74+
target_path.mkdir()
75+
with patch(
76+
"sys.argv",
77+
[
78+
"relink.py",
79+
"--source-root",
80+
str(source_path),
81+
"--target-root",
82+
str(target_path),
83+
],
84+
):
85+
args = relink.parse_arguments()
86+
assert args.source_root == str(source_path.resolve())
87+
assert args.target_root == str(target_path.resolve())
88+
89+
def test_verbose_flag(self, mock_default_dirs): # pylint: disable=unused-argument
90+
"""Test that --verbose flag is parsed correctly."""
91+
with patch("sys.argv", ["relink.py", "--verbose"]):
92+
args = relink.parse_arguments()
93+
assert args.verbose is True
94+
assert args.quiet is False
95+
96+
def test_quiet_flag(self, mock_default_dirs): # pylint: disable=unused-argument
97+
"""Test that --quiet flag is parsed correctly."""
98+
with patch("sys.argv", ["relink.py", "--quiet"]):
99+
args = relink.parse_arguments()
100+
assert args.quiet is True
101+
assert args.verbose is False
102+
103+
def test_verbose_short_flag(
104+
self, mock_default_dirs
105+
): # pylint: disable=unused-argument
106+
"""Test that -v flag is parsed correctly."""
107+
with patch("sys.argv", ["relink.py", "-v"]):
108+
args = relink.parse_arguments()
109+
assert args.verbose is True
110+
111+
def test_quiet_short_flag(
112+
self, mock_default_dirs
113+
): # pylint: disable=unused-argument
114+
"""Test that -q flag is parsed correctly."""
115+
with patch("sys.argv", ["relink.py", "-q"]):
116+
args = relink.parse_arguments()
117+
assert args.quiet is True
118+
119+
def test_default_verbosity(
120+
self, mock_default_dirs
121+
): # pylint: disable=unused-argument
122+
"""Test that default verbosity has both flags as False."""
123+
with patch("sys.argv", ["relink.py"]):
124+
args = relink.parse_arguments()
125+
assert args.verbose is False
126+
assert args.quiet is False
127+
128+
def test_verbose_and_quiet_mutually_exclusive(self, mock_default_dirs):
129+
"""Test that --verbose and --quiet cannot be used together."""
130+
# pylint: disable=unused-argument
131+
with patch("sys.argv", ["relink.py", "--verbose", "--quiet"]):
132+
with pytest.raises(SystemExit) as exc_info:
133+
relink.parse_arguments()
134+
# Mutually exclusive arguments cause SystemExit with code 2
135+
assert exc_info.value.code == 2
136+
137+
def test_verbose_and_quiet_short_flags_mutually_exclusive(self, mock_default_dirs):
138+
"""Test that -v and -q cannot be used together."""
139+
# pylint: disable=unused-argument
140+
with patch("sys.argv", ["relink.py", "-v", "-q"]):
141+
with pytest.raises(SystemExit) as exc_info:
142+
relink.parse_arguments()
143+
# Mutually exclusive arguments cause SystemExit with code 2
144+
assert exc_info.value.code == 2
145+
146+
def test_dry_run_flag(self, mock_default_dirs):
147+
"""Test that --dry-run flag is parsed correctly."""
148+
# pylint: disable=unused-argument
149+
with patch("sys.argv", ["relink.py", "--dry-run"]):
150+
args = relink.parse_arguments()
151+
assert args.dry_run is True
152+
153+
def test_dry_run_default(self, mock_default_dirs):
154+
"""Test that dry_run defaults to False."""
155+
# pylint: disable=unused-argument
156+
with patch("sys.argv", ["relink.py"]):
157+
args = relink.parse_arguments()
158+
assert args.dry_run is False
159+
160+
def test_timing_flag(self, mock_default_dirs):
161+
"""Test that --timing flag is parsed correctly."""
162+
# pylint: disable=unused-argument
163+
with patch("sys.argv", ["relink.py", "--timing"]):
164+
args = relink.parse_arguments()
165+
assert args.timing is True
166+
167+
def test_timing_default(self, mock_default_dirs):
168+
"""Test that timing defaults to False."""
169+
# pylint: disable=unused-argument
170+
with patch("sys.argv", ["relink.py"]):
171+
args = relink.parse_arguments()
172+
assert args.timing is False
173+
174+
175+
class TestValidateDirectory:
176+
"""Test suite for validate_directory function."""
177+
178+
def test_valid_directory(self, tmp_path):
179+
"""Test that valid directory is accepted and returns absolute path."""
180+
test_dir = tmp_path / "valid_dir"
181+
test_dir.mkdir()
182+
183+
result = relink.validate_directory(str(test_dir))
184+
assert result == str(test_dir.resolve())
185+
186+
def test_nonexistent_directory(self):
187+
"""Test that nonexistent directory raises ArgumentTypeError."""
188+
nonexistent = os.path.join(os.sep, "nonexistent", "directory", "12345")
189+
190+
with pytest.raises(argparse.ArgumentTypeError) as exc_info:
191+
relink.validate_directory(nonexistent)
192+
193+
assert "does not exist" in str(exc_info.value)
194+
assert nonexistent in str(exc_info.value)
195+
196+
def test_file_instead_of_directory(self, tmp_path):
197+
"""Test that a file path raises ArgumentTypeError."""
198+
test_file = tmp_path / "test_file.txt"
199+
test_file.write_text("content")
200+
201+
with pytest.raises(argparse.ArgumentTypeError) as exc_info:
202+
relink.validate_directory(str(test_file))
203+
204+
assert "not a directory" in str(exc_info.value)
205+
206+
def test_relative_path_converted_to_absolute(self, tmp_path):
207+
"""Test that relative paths are converted to absolute."""
208+
test_dir = tmp_path / "relative_test"
209+
test_dir.mkdir()
210+
211+
# Change to parent directory and use relative path
212+
cwd = os.getcwd()
213+
try:
214+
os.chdir(str(tmp_path))
215+
result = relink.validate_directory("relative_test")
216+
assert os.path.isabs(result)
217+
assert result == str(test_dir.resolve())
218+
finally:
219+
os.chdir(cwd)
220+
221+
def test_symlink_to_directory(self, tmp_path):
222+
"""Test that symlink to a directory is accepted."""
223+
real_dir = tmp_path / "real_dir"
224+
real_dir.mkdir()
225+
226+
link_dir = tmp_path / "link_dir"
227+
link_dir.symlink_to(real_dir)
228+
229+
result = relink.validate_directory(str(link_dir))
230+
# validate_directory returns absolute path of the symlink itself
231+
assert result == str(link_dir.absolute())
232+
# Verify it's still a symlink
233+
assert os.path.islink(result)
234+
235+
236+
class TestProcessArgs:
237+
"""Test suite for process_args function."""
238+
239+
# pylint: disable=no-member
240+
241+
def test_process_args_quiet_sets_warning_level(self):
242+
"""Test that quiet flag sets log level to WARNING."""
243+
args = argparse.Namespace(quiet=True, verbose=False)
244+
relink.process_args(args)
245+
assert args.log_level == logging.WARNING
246+
247+
def test_process_args_verbose_sets_debug_level(self):
248+
"""Test that verbose flag sets log level to DEBUG."""
249+
args = argparse.Namespace(quiet=False, verbose=True)
250+
relink.process_args(args)
251+
assert args.log_level == logging.DEBUG
252+
253+
def test_process_args_default_sets_info_level(self):
254+
"""Test that default (no flags) sets log level to INFO."""
255+
args = argparse.Namespace(quiet=False, verbose=False)
256+
relink.process_args(args)
257+
assert args.log_level == logging.INFO
258+
259+
def test_process_args_modifies_args_in_place(self):
260+
"""Test that process_args modifies the args object in place."""
261+
args = argparse.Namespace(quiet=False, verbose=False)
262+
original_args = args
263+
relink.process_args(args)
264+
# Should be the same object, modified in place
265+
assert args is original_args
266+
assert hasattr(args, "log_level")

tests/relink/test_cmdline.py

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
"""
2+
Tests for relink.py script as called from command line
3+
"""
4+
5+
import os
6+
import sys
7+
import subprocess
8+
9+
import pytest
10+
11+
12+
@pytest.fixture(name="mock_dirs")
13+
def fixture_mock_dirs(tmp_path):
14+
"""Create temporary directories and files for command-line testing."""
15+
source_dir = tmp_path / "source"
16+
target_dir = tmp_path / "target"
17+
source_dir.mkdir()
18+
target_dir.mkdir()
19+
20+
# Create a test file
21+
source_file = source_dir / "test_file.txt"
22+
target_file = target_dir / "test_file.txt"
23+
source_file.write_text("source content")
24+
target_file.write_text("target content")
25+
26+
return source_dir, target_dir, source_file, target_file
27+
28+
29+
def test_command_line_execution_dry_run(mock_dirs):
30+
"""Test executing relink.py from command line with --dry-run flag."""
31+
source_dir, target_dir, source_file, _ = mock_dirs
32+
33+
# Get the path to relink.py
34+
relink_script = os.path.join(
35+
os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))),
36+
"relink.py",
37+
)
38+
39+
# Build the command
40+
command = [
41+
sys.executable,
42+
relink_script,
43+
"--source-root",
44+
str(source_dir),
45+
"--target-root",
46+
str(target_dir),
47+
"--dry-run",
48+
]
49+
50+
# Execute the command
51+
result = subprocess.run(command, capture_output=True, text=True, check=False)
52+
53+
# Verify the command executed successfully
54+
assert result.returncode == 0, f"Command failed with stderr: {result.stderr}"
55+
56+
# Verify dry-run messages in output
57+
assert "DRY RUN MODE" in result.stdout
58+
assert "[DRY RUN] Would create symbolic link:" in result.stdout
59+
60+
# Verify no actual changes were made
61+
assert source_file.is_file()
62+
assert not source_file.is_symlink()
63+
64+
65+
def test_command_line_execution_actual_run(mock_dirs):
66+
"""Test executing relink.py from command line without dry-run."""
67+
source_dir, target_dir, source_file, target_file = mock_dirs
68+
69+
# Get the path to relink.py
70+
relink_script = os.path.join(
71+
os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))),
72+
"relink.py",
73+
)
74+
75+
# Build the command
76+
command = [
77+
sys.executable,
78+
relink_script,
79+
"--source-root",
80+
str(source_dir),
81+
"--target-root",
82+
str(target_dir),
83+
]
84+
85+
# Execute the command
86+
result = subprocess.run(command, capture_output=True, text=True, check=False)
87+
88+
# Verify the command executed successfully
89+
assert result.returncode == 0, f"Command failed with stderr: {result.stderr}"
90+
91+
# Verify the file was converted to a symlink
92+
assert source_file.is_symlink()
93+
assert os.readlink(str(source_file)) == str(target_file)
94+
95+
# Verify success messages in output
96+
assert "Created symbolic link:" in result.stdout

0 commit comments

Comments
 (0)