Skip to content

Commit 17e567c

Browse files
committed
relink.py: Add basic pytest testing.
1 parent d4692fa commit 17e567c

5 files changed

Lines changed: 341 additions & 0 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
__pycache__

tests/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Test using `pytest` from this dir or repo top-level.

tests/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Tests package for inputdataTools."""

tests/conftest.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
"""
2+
Pytest configuration and shared fixtures for relink tests.
3+
"""
4+
5+
import os
6+
7+
import pytest
8+
9+
10+
@pytest.fixture(scope="session")
11+
def workspace_root():
12+
"""Return the root directory of the workspace."""
13+
return os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

tests/test_relink.py

Lines changed: 325 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,325 @@
1+
"""
2+
Tests for relink.py script.
3+
"""
4+
5+
import os
6+
import sys
7+
import tempfile
8+
import shutil
9+
import pwd
10+
from unittest.mock import patch
11+
import pytest
12+
13+
# Add parent directory to path to import relink module
14+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
15+
import relink
16+
17+
18+
class TestFindAndReplaceOwnedFiles:
19+
"""Test suite for find_and_replace_owned_files function."""
20+
21+
@pytest.fixture
22+
def temp_dirs(self):
23+
"""Create temporary source and target directories for testing."""
24+
source_dir = tempfile.mkdtemp(prefix="test_source_")
25+
target_dir = tempfile.mkdtemp(prefix="test_target_")
26+
27+
yield source_dir, target_dir
28+
29+
# Cleanup
30+
shutil.rmtree(source_dir, ignore_errors=True)
31+
shutil.rmtree(target_dir, ignore_errors=True)
32+
33+
@pytest.fixture
34+
def current_user(self):
35+
"""Get the current user's username."""
36+
username = os.environ["USER"]
37+
return username
38+
39+
def test_basic_file_replacement(self, temp_dirs, current_user):
40+
"""Test basic functionality: replace owned file with symlink."""
41+
source_dir, target_dir = temp_dirs
42+
username = current_user
43+
44+
# Create a file in source directory
45+
source_file = os.path.join(source_dir, "test_file.txt")
46+
with open(source_file, "w", encoding="utf-8") as f:
47+
f.write("source content")
48+
49+
# Create corresponding file in target directory
50+
target_file = os.path.join(target_dir, "test_file.txt")
51+
with open(target_file, "w", encoding="utf-8") as f:
52+
f.write("target content")
53+
54+
# Run the function
55+
relink.find_and_replace_owned_files(source_dir, target_dir, username)
56+
57+
# Verify the source file is now a symlink
58+
assert os.path.islink(source_file), "Source file should be a symlink"
59+
assert (
60+
os.readlink(source_file) == target_file
61+
), "Symlink should point to target file"
62+
63+
def test_nested_directory_structure(self, temp_dirs, current_user):
64+
"""Test with nested directory structures."""
65+
source_dir, target_dir = temp_dirs
66+
username = current_user
67+
68+
# Create nested directories
69+
nested_path = os.path.join("subdir1", "subdir2")
70+
os.makedirs(os.path.join(source_dir, nested_path))
71+
os.makedirs(os.path.join(target_dir, nested_path))
72+
73+
# Create files in nested directories
74+
source_file = os.path.join(source_dir, nested_path, "nested_file.txt")
75+
target_file = os.path.join(target_dir, nested_path, "nested_file.txt")
76+
77+
with open(source_file, "w", encoding="utf-8") as f:
78+
f.write("nested source")
79+
with open(target_file, "w", encoding="utf-8") as f:
80+
f.write("nested target")
81+
82+
# Run the function
83+
relink.find_and_replace_owned_files(source_dir, target_dir, username)
84+
85+
# Verify
86+
assert os.path.islink(source_file), "Nested file should be a symlink"
87+
assert os.readlink(source_file) == target_file
88+
89+
def test_skip_existing_symlinks(self, temp_dirs, current_user):
90+
"""Test that existing symlinks are skipped."""
91+
source_dir, target_dir = temp_dirs
92+
username = current_user
93+
94+
# Create a target file
95+
target_file = os.path.join(target_dir, "target.txt")
96+
with open(target_file, "w", encoding="utf-8") as f:
97+
f.write("target")
98+
99+
# Create a symlink in source (pointing somewhere else)
100+
source_link = os.path.join(source_dir, "existing_link.txt")
101+
dummy_target = os.path.join(tempfile.gettempdir(), "somewhere")
102+
os.symlink(dummy_target, source_link)
103+
104+
# Get the inode and mtime before running the function
105+
stat_before = os.lstat(source_link)
106+
inode_before = stat_before.st_ino
107+
mtime_before = stat_before.st_mtime
108+
109+
# Run the function
110+
relink.find_and_replace_owned_files(source_dir, target_dir, username)
111+
112+
# Verify the symlink is unchanged (same inode means it wasn't deleted/recreated)
113+
stat_after = os.lstat(source_link)
114+
assert (
115+
inode_before == stat_after.st_ino
116+
), "Symlink should not have been recreated"
117+
assert mtime_before == stat_after.st_mtime, "Symlink mtime should be unchanged"
118+
assert (
119+
os.readlink(source_link) == dummy_target
120+
), "Symlink target should be unchanged"
121+
122+
def test_missing_target_file(self, temp_dirs, current_user, capsys):
123+
"""Test behavior when target file doesn't exist."""
124+
source_dir, target_dir = temp_dirs
125+
username = current_user
126+
127+
# Create only source file (no corresponding target)
128+
source_file = os.path.join(source_dir, "orphan.txt")
129+
with open(source_file, "w", encoding="utf-8") as f:
130+
f.write("orphan content")
131+
132+
# Run the function
133+
relink.find_and_replace_owned_files(source_dir, target_dir, username)
134+
135+
# Verify the file is NOT converted to symlink
136+
assert not os.path.islink(source_file), "File should not be a symlink"
137+
assert os.path.isfile(source_file), "Original file should still exist"
138+
139+
# Check warning message
140+
captured = capsys.readouterr()
141+
assert "Warning: Corresponding file not found" in captured.out
142+
143+
def test_invalid_username(self, temp_dirs, capsys):
144+
"""Test behavior with invalid username."""
145+
source_dir, target_dir = temp_dirs
146+
147+
# Use a username that doesn't exist
148+
invalid_username = "nonexistent_user_12345"
149+
try:
150+
pwd.getpwnam(invalid_username).pw_uid
151+
except KeyError:
152+
pass
153+
else:
154+
raise RuntimeError(f"{invalid_username=} DOES actually exist")
155+
156+
# Run the function
157+
relink.find_and_replace_owned_files(source_dir, target_dir, invalid_username)
158+
159+
# Check error message
160+
captured = capsys.readouterr()
161+
assert "Error: User" in captured.out
162+
assert "not found" in captured.out
163+
164+
def test_multiple_files(self, temp_dirs, current_user):
165+
"""Test with multiple files in the directory."""
166+
source_dir, target_dir = temp_dirs
167+
username = current_user
168+
169+
# Create multiple files
170+
for i in range(5):
171+
source_file = os.path.join(source_dir, f"file_{i}.txt")
172+
target_file = os.path.join(target_dir, f"file_{i}.txt")
173+
174+
with open(source_file, "w", encoding="utf-8") as f:
175+
f.write(f"source {i}")
176+
with open(target_file, "w", encoding="utf-8") as f:
177+
f.write(f"target {i}")
178+
179+
# Run the function
180+
relink.find_and_replace_owned_files(source_dir, target_dir, username)
181+
182+
# Verify all files are symlinks
183+
for i in range(5):
184+
source_file = os.path.join(source_dir, f"file_{i}.txt")
185+
target_file = os.path.join(target_dir, f"file_{i}.txt")
186+
assert os.path.islink(source_file)
187+
assert os.readlink(source_file) == target_file
188+
189+
def test_absolute_paths(self, temp_dirs, current_user):
190+
"""Test that function handles relative paths by converting to absolute."""
191+
source_dir, target_dir = temp_dirs
192+
username = current_user
193+
194+
# Create test files
195+
source_file = os.path.join(source_dir, "test.txt")
196+
target_file = os.path.join(target_dir, "test.txt")
197+
198+
with open(source_file, "w", encoding="utf-8") as f:
199+
f.write("test")
200+
with open(target_file, "w", encoding="utf-8") as f:
201+
f.write("test target")
202+
203+
# Use relative paths (if possible)
204+
cwd = os.getcwd()
205+
try:
206+
os.chdir(os.path.dirname(source_dir))
207+
rel_source = os.path.basename(source_dir)
208+
rel_target = os.path.basename(target_dir)
209+
210+
# Run with relative paths
211+
relink.find_and_replace_owned_files(rel_source, rel_target, username)
212+
213+
# Verify it still works
214+
assert os.path.islink(source_file)
215+
finally:
216+
os.chdir(cwd)
217+
218+
219+
class TestParseArguments:
220+
"""Test suite for parse_arguments function."""
221+
222+
def test_default_arguments(self):
223+
"""Test that default arguments are used when none provided."""
224+
with patch("sys.argv", ["relink.py"]):
225+
args = relink.parse_arguments()
226+
assert args.source_root == relink.DEFAULT_SOURCE_ROOT
227+
assert args.target_root == relink.DEFAULT_TARGET_ROOT
228+
229+
def test_custom_source_root(self):
230+
"""Test custom source root argument."""
231+
test_path = os.path.join(os.sep, "custom", "source", "path")
232+
with patch("sys.argv", ["relink.py", "--source-root", test_path]):
233+
args = relink.parse_arguments()
234+
assert args.source_root == test_path
235+
assert args.target_root == relink.DEFAULT_TARGET_ROOT
236+
237+
def test_custom_target_root(self):
238+
"""Test custom target root argument."""
239+
test_path = os.path.join(os.sep, "custom", "target", "path")
240+
with patch("sys.argv", ["relink.py", "--target-root", test_path]):
241+
args = relink.parse_arguments()
242+
assert args.source_root == relink.DEFAULT_SOURCE_ROOT
243+
assert args.target_root == test_path
244+
245+
def test_both_custom_paths(self):
246+
"""Test both custom source and target roots."""
247+
source_path = os.path.join(os.sep, "custom", "source")
248+
target_path = os.path.join(os.sep, "custom", "target")
249+
with patch(
250+
"sys.argv",
251+
["relink.py", "--source-root", source_path, "--target-root", target_path],
252+
):
253+
args = relink.parse_arguments()
254+
assert args.source_root == source_path
255+
assert args.target_root == target_path
256+
257+
258+
class TestEdgeCases:
259+
"""Test edge cases and error handling."""
260+
261+
@pytest.fixture
262+
def temp_dirs(self):
263+
"""Create temporary source and target directories for testing."""
264+
source_dir = tempfile.mkdtemp(prefix="test_source_")
265+
target_dir = tempfile.mkdtemp(prefix="test_target_")
266+
267+
yield source_dir, target_dir
268+
269+
# Cleanup
270+
shutil.rmtree(source_dir, ignore_errors=True)
271+
shutil.rmtree(target_dir, ignore_errors=True)
272+
273+
def test_empty_directories(self, temp_dirs):
274+
"""Test with empty directories."""
275+
source_dir, target_dir = temp_dirs
276+
username = os.environ["USER"]
277+
278+
# Run with empty directories (should not crash)
279+
relink.find_and_replace_owned_files(source_dir, target_dir, username)
280+
281+
# Should complete without errors
282+
assert True
283+
284+
def test_file_with_spaces_in_name(self, temp_dirs):
285+
"""Test files with spaces in their names."""
286+
source_dir, target_dir = temp_dirs
287+
username = os.environ["USER"]
288+
289+
# Create files with spaces
290+
source_file = os.path.join(source_dir, "file with spaces.txt")
291+
target_file = os.path.join(target_dir, "file with spaces.txt")
292+
293+
with open(source_file, "w", encoding="utf-8") as f:
294+
f.write("content")
295+
with open(target_file, "w", encoding="utf-8") as f:
296+
f.write("target content")
297+
298+
# Run the function
299+
relink.find_and_replace_owned_files(source_dir, target_dir, username)
300+
301+
# Verify
302+
assert os.path.islink(source_file)
303+
assert os.readlink(source_file) == target_file
304+
305+
def test_file_with_special_characters(self, temp_dirs):
306+
"""Test files with special characters in names."""
307+
source_dir, target_dir = temp_dirs
308+
username = os.environ["USER"]
309+
310+
# Create files with special chars (that are valid in filenames)
311+
filename = "file-with_special.chars@123.txt"
312+
source_file = os.path.join(source_dir, filename)
313+
target_file = os.path.join(target_dir, filename)
314+
315+
with open(source_file, "w", encoding="utf-8") as f:
316+
f.write("content")
317+
with open(target_file, "w", encoding="utf-8") as f:
318+
f.write("target content")
319+
320+
# Run the function
321+
relink.find_and_replace_owned_files(source_dir, target_dir, username)
322+
323+
# Verify
324+
assert os.path.islink(source_file)
325+
assert os.readlink(source_file) == target_file

0 commit comments

Comments
 (0)