Skip to content

Commit da4254a

Browse files
committed
Split relink tests across multiple files.
1 parent dd3603d commit da4254a

8 files changed

Lines changed: 1351 additions & 1026 deletions

File tree

tests/relink/__init__.py

Whitespace-only changes.

tests/relink/test_args.py

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

0 commit comments

Comments
 (0)