Skip to content

Commit 13d455d

Browse files
committed
relink.py: Now supports multiple source_root dirs.
1 parent 5b076f9 commit 13d455d

3 files changed

Lines changed: 140 additions & 16 deletions

File tree

relink.py

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -228,17 +228,23 @@ def replace_files_with_symlinks(
228228

229229
def validate_directory(path):
230230
"""
231-
Validate that the path exists and is a directory.
231+
Validate that one or more paths exist and are directories.
232232
233233
Args:
234-
path (str): The path to validate.
234+
path (str or list): The path to validate, or a list of such paths.
235235
236236
Returns:
237-
str: The absolute path if valid.
237+
str or list: The absolute path(s) if valid.
238238
239239
Raises:
240-
argparse.ArgumentTypeError: If path doesn't exist or is not a directory.
240+
argparse.ArgumentTypeError: If a path doesn't exist or is not a directory.
241241
"""
242+
if isinstance(path, list):
243+
result = []
244+
for item in path:
245+
result.append(validate_directory(item))
246+
return result
247+
242248
if not os.path.exists(path):
243249
raise argparse.ArgumentTypeError(f"Directory '{path}' does not exist")
244250
if not os.path.isdir(path):
@@ -261,11 +267,10 @@ def parse_arguments():
261267
)
262268
parser.add_argument(
263269
"source_root",
264-
nargs="?",
265-
type=validate_directory,
270+
nargs="*",
266271
default=DEFAULT_SOURCE_ROOT,
267272
help=(
268-
f"The root of the directory tree to search for files (default: {DEFAULT_SOURCE_ROOT})"
273+
f"One or more directories to search for files (default: {DEFAULT_SOURCE_ROOT})"
269274
),
270275
)
271276
parser.add_argument(
@@ -347,13 +352,14 @@ def main():
347352
start_time = time.time()
348353

349354
# --- Execution ---
350-
replace_files_with_symlinks(
351-
args.source_root,
352-
args.target_root,
353-
my_username,
354-
inputdata_root=args.inputdata_root,
355-
dry_run=args.dry_run,
356-
)
355+
for item in args.source_root:
356+
replace_files_with_symlinks(
357+
item,
358+
args.target_root,
359+
my_username,
360+
inputdata_root=args.inputdata_root,
361+
dry_run=args.dry_run,
362+
)
357363

358364
if args.timing:
359365
elapsed_time = time.time() - start_time

tests/relink/test_args.py

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ def test_custom_source_root(self, mock_default_dirs, tmp_path):
5454
custom_source.mkdir()
5555
with patch("sys.argv", ["relink.py", str(custom_source)]):
5656
args = relink.parse_arguments()
57-
assert args.source_root == str(custom_source.resolve())
57+
assert args.source_root == [str(custom_source.resolve())]
5858
assert args.target_root == target_dir
5959

6060
def test_custom_target_root(self, mock_default_dirs, tmp_path):
@@ -83,7 +83,7 @@ def test_both_custom_paths(self, tmp_path):
8383
],
8484
):
8585
args = relink.parse_arguments()
86-
assert args.source_root == str(source_path.resolve())
86+
assert args.source_root == [str(source_path.resolve())]
8787
assert args.target_root == str(target_path.resolve())
8888

8989
def test_verbose_flag(self, mock_default_dirs): # pylint: disable=unused-argument
@@ -171,6 +171,49 @@ def test_timing_default(self, mock_default_dirs):
171171
args = relink.parse_arguments()
172172
assert args.timing is False
173173

174+
def test_multiple_source_roots(self, mock_default_dirs, tmp_path):
175+
"""Test that multiple source root arguments are parsed correctly."""
176+
_, target_dir = mock_default_dirs
177+
source1 = tmp_path / "source1"
178+
source2 = tmp_path / "source2"
179+
source3 = tmp_path / "source3"
180+
source1.mkdir()
181+
source2.mkdir()
182+
source3.mkdir()
183+
184+
with patch("sys.argv", ["relink.py", str(source1), str(source2), str(source3)]):
185+
args = relink.parse_arguments()
186+
assert len(args.source_root) == 3
187+
assert str(source1.resolve()) in args.source_root
188+
assert str(source2.resolve()) in args.source_root
189+
assert str(source3.resolve()) in args.source_root
190+
assert args.target_root == target_dir
191+
192+
def test_multiple_source_roots_with_target(self, tmp_path):
193+
"""Test multiple source roots with custom target root."""
194+
source1 = tmp_path / "source1"
195+
source2 = tmp_path / "source2"
196+
target = tmp_path / "target"
197+
source1.mkdir()
198+
source2.mkdir()
199+
target.mkdir()
200+
201+
with patch(
202+
"sys.argv",
203+
[
204+
"relink.py",
205+
str(source1),
206+
str(source2),
207+
"--target-root",
208+
str(target),
209+
],
210+
):
211+
args = relink.parse_arguments()
212+
assert len(args.source_root) == 2
213+
assert str(source1.resolve()) in args.source_root
214+
assert str(source2.resolve()) in args.source_root
215+
assert args.target_root == str(target.resolve())
216+
174217

175218
class TestValidateDirectory:
176219
"""Test suite for validate_directory function."""
@@ -232,6 +275,29 @@ def test_symlink_to_directory(self, tmp_path):
232275
# Verify it's still a symlink
233276
assert os.path.islink(result)
234277

278+
def test_list_with_invalid_directory(self, tmp_path):
279+
"""Test that a list with one invalid directory raises error."""
280+
dir1 = tmp_path / "dir1"
281+
dir1.mkdir()
282+
nonexistent = tmp_path / "nonexistent"
283+
284+
with pytest.raises(argparse.ArgumentTypeError) as exc_info:
285+
relink.validate_directory([str(dir1), str(nonexistent)])
286+
287+
assert "does not exist" in str(exc_info.value)
288+
289+
def test_list_with_file_instead_of_directory(self, tmp_path):
290+
"""Test that a list containing a file raises error."""
291+
dir1 = tmp_path / "dir1"
292+
dir1.mkdir()
293+
file1 = tmp_path / "file.txt"
294+
file1.write_text("content")
295+
296+
with pytest.raises(argparse.ArgumentTypeError) as exc_info:
297+
relink.validate_directory([str(dir1), str(file1)])
298+
299+
assert "not a directory" in str(exc_info.value)
300+
235301

236302
class TestProcessArgs:
237303
"""Test suite for process_args function."""

tests/relink/test_cmdline.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,3 +96,55 @@ def test_command_line_execution_actual_run(mock_dirs):
9696

9797
# Verify success messages in output
9898
assert "Created symbolic link:" in result.stdout
99+
100+
101+
def test_command_line_multiple_source_dirs(tmp_path):
102+
"""Test executing relink.py with multiple source directories."""
103+
# Create multiple source directories
104+
source1 = tmp_path / "source1"
105+
source2 = tmp_path / "source2"
106+
target_dir = tmp_path / "target"
107+
source1.mkdir()
108+
source2.mkdir()
109+
target_dir.mkdir()
110+
111+
# Create files in each source directory
112+
source1_file = source1 / "file1.txt"
113+
source2_file = source2 / "file2.txt"
114+
target1_file = target_dir / "file1.txt"
115+
target2_file = target_dir / "file2.txt"
116+
117+
source1_file.write_text("source1 content")
118+
source2_file.write_text("source2 content")
119+
target1_file.write_text("target1 content")
120+
target2_file.write_text("target2 content")
121+
122+
# Get the path to relink.py
123+
relink_script = os.path.join(
124+
os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))),
125+
"relink.py",
126+
)
127+
128+
# Build the command with multiple source directories
129+
command = [
130+
sys.executable,
131+
relink_script,
132+
str(source1),
133+
str(source2),
134+
"--target-root",
135+
str(target_dir),
136+
"--inputdata-root",
137+
str(tmp_path),
138+
]
139+
140+
# Execute the command
141+
result = subprocess.run(command, capture_output=True, text=True, check=False)
142+
143+
# Verify the command executed successfully
144+
assert result.returncode == 0, f"Command failed with stderr: {result.stderr}"
145+
146+
# Verify both files were converted to symlinks
147+
assert source1_file.is_symlink()
148+
assert source2_file.is_symlink()
149+
assert os.readlink(str(source1_file)) == str(target1_file)
150+
assert os.readlink(str(source2_file)) == str(target2_file)

0 commit comments

Comments
 (0)