Skip to content

Commit 8a0b914

Browse files
committed
relink.py: Require that source_dir(s) be in inputdata_root and target_dir not be.
1 parent 1ada123 commit 8a0b914

4 files changed

Lines changed: 99 additions & 75 deletions

File tree

relink.py

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -180,12 +180,12 @@ def replace_files_with_symlinks(
180180

181181
# Use efficient scandir-based search
182182
for file_path in find_owned_files_scandir(source_dir, user_uid, inputdata_root):
183-
replace_one_file_with_symlink(source_dir, target_dir, file_path, dry_run=dry_run)
183+
replace_one_file_with_symlink(
184+
source_dir, target_dir, file_path, dry_run=dry_run
185+
)
184186

185187

186-
def replace_one_file_with_symlink(
187-
source_dir, target_dir, file_path, dry_run=False
188-
):
188+
def replace_one_file_with_symlink(source_dir, target_dir, file_path, dry_run=False):
189189
"""
190190
Given a file, replaces it with a symbolic link to the same relative path in a target directory
191191
tree.
@@ -356,6 +356,26 @@ def process_args(args):
356356
else:
357357
args.log_level = logging.INFO
358358

359+
# Ensure that source_root is a list
360+
if hasattr(args, "source_root") and not isinstance(args.source_root, list):
361+
args.source_root = [args.source_root]
362+
363+
# Check that every item in source_root is a child of inputdata_root
364+
if hasattr(args, "source_root"): # Sometimes doesn't if we're testing
365+
for item in args.source_root:
366+
if not Path(item).is_relative_to(args.inputdata_root):
367+
raise argparse.ArgumentTypeError(
368+
f"Item '{item}' not under inputdata root '{args.inputdata_root}'"
369+
)
370+
371+
# Check that target_root is NOT a child of inputdata_root
372+
if hasattr(args, "target_root"): # Sometimes doesn't if we're testing
373+
if Path(args.target_root).is_relative_to(args.inputdata_root):
374+
raise argparse.ArgumentTypeError(
375+
f"Target root ('{args.target_root}') must not be under inputdata root "
376+
f"'{args.inputdata_root}'"
377+
)
378+
359379

360380
def main():
361381

tests/relink/conftest.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ def fixture_temp_dirs():
1717
target_dir = tempfile.mkdtemp(prefix="test_target_")
1818

1919
with patch("relink.DEFAULT_SOURCE_ROOT", source_dir):
20-
yield source_dir, target_dir
20+
with patch("relink.DEFAULT_TARGET_ROOT", target_dir):
21+
yield source_dir, target_dir
2122

2223
# Cleanup
2324
shutil.rmtree(source_dir, ignore_errors=True)

tests/relink/test_args.py

Lines changed: 66 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,7 @@
44

55
import os
66
import sys
7-
import tempfile
8-
import shutil
7+
from pathlib import Path
98
import logging
109
import argparse
1110
from unittest.mock import patch
@@ -20,112 +19,91 @@
2019
import relink # noqa: E402
2120

2221

23-
@pytest.fixture(scope="function", name="mock_default_dirs")
24-
def fixture_mock_default_dirs(temp_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-
source_dir, target_dir = temp_dirs
29-
30-
with patch.object(relink, "DEFAULT_SOURCE_ROOT", source_dir):
31-
with patch.object(relink, "DEFAULT_TARGET_ROOT", target_dir):
32-
yield source_dir, target_dir
33-
34-
# Cleanup
35-
shutil.rmtree(source_dir, ignore_errors=True)
36-
shutil.rmtree(target_dir, ignore_errors=True)
37-
38-
3922
class TestParseArguments:
4023
"""Test suite for parse_arguments function."""
4124

42-
def test_default_arguments(self, mock_default_dirs):
25+
def test_default_arguments(self, temp_dirs):
4326
"""Test that default arguments are used when none provided."""
44-
source_dir, target_dir = mock_default_dirs
45-
with patch("sys.argv", ["relink.py"]):
27+
source_dir, target_dir = temp_dirs
28+
with patch("sys.argv", ["relink.py", source_dir]):
4629
args = relink.parse_arguments()
47-
assert args.source_root == source_dir
30+
assert args.source_root == [source_dir]
4831
assert args.target_root == target_dir
4932

50-
def test_custom_source_root(self, mock_default_dirs, tmp_path):
33+
def test_custom_source_root(self, temp_dirs):
5134
"""Test custom source root argument."""
52-
_, target_dir = mock_default_dirs
53-
custom_source = tmp_path / "custom_source"
35+
source_dir, target_dir = temp_dirs
36+
custom_source = Path(os.path.join(source_dir, "custom_source"))
5437
custom_source.mkdir()
5538
with patch("sys.argv", ["relink.py", str(custom_source)]):
5639
args = relink.parse_arguments()
5740
assert args.source_root == [str(custom_source.resolve())]
5841
assert args.target_root == target_dir
5942

60-
def test_custom_target_root(self, mock_default_dirs, tmp_path):
43+
def test_custom_target_root(self, temp_dirs):
6144
"""Test custom target root argument."""
62-
source_dir, _ = mock_default_dirs
63-
custom_target = tmp_path / "custom_target"
45+
source_dir, target_dir = temp_dirs
46+
custom_target = Path(os.path.join(target_dir, "custom_target"))
6447
custom_target.mkdir()
6548
with patch("sys.argv", ["relink.py", "--target-root", str(custom_target)]):
6649
args = relink.parse_arguments()
67-
assert args.source_root == source_dir
50+
assert args.source_root == [source_dir]
6851
assert args.target_root == str(custom_target.resolve())
6952

70-
def test_both_custom_paths(self, tmp_path):
53+
def test_both_custom_paths(self, temp_dirs):
7154
"""Test both custom source and target roots."""
72-
source_path = tmp_path / "custom_source"
73-
target_path = tmp_path / "custom_target"
74-
source_path.mkdir()
75-
target_path.mkdir()
55+
source_root, target_root = temp_dirs
56+
source_dir = Path(os.path.join(source_root, "custom_source"))
57+
target_dir = Path(os.path.join(target_root, "custom_target"))
58+
source_dir.mkdir()
59+
target_dir.mkdir()
7660
with patch(
7761
"sys.argv",
7862
[
7963
"relink.py",
80-
str(source_path),
64+
str(source_dir),
8165
"--target-root",
82-
str(target_path),
66+
str(target_dir),
8367
],
8468
):
8569
args = relink.parse_arguments()
86-
assert args.source_root == [str(source_path.resolve())]
87-
assert args.target_root == str(target_path.resolve())
70+
assert args.source_root == [str(source_dir.resolve())]
71+
assert args.target_root == str(target_dir.resolve())
8872

89-
def test_verbose_flag(self, mock_default_dirs): # pylint: disable=unused-argument
73+
def test_verbose_flag(self, temp_dirs): # pylint: disable=unused-argument
9074
"""Test that --verbose flag is parsed correctly."""
9175
with patch("sys.argv", ["relink.py", "--verbose"]):
9276
args = relink.parse_arguments()
9377
assert args.verbose is True
9478
assert args.quiet is False
9579

96-
def test_quiet_flag(self, mock_default_dirs): # pylint: disable=unused-argument
80+
def test_quiet_flag(self, temp_dirs): # pylint: disable=unused-argument
9781
"""Test that --quiet flag is parsed correctly."""
9882
with patch("sys.argv", ["relink.py", "--quiet"]):
9983
args = relink.parse_arguments()
10084
assert args.quiet is True
10185
assert args.verbose is False
10286

103-
def test_verbose_short_flag(
104-
self, mock_default_dirs
105-
): # pylint: disable=unused-argument
87+
def test_verbose_short_flag(self, temp_dirs): # pylint: disable=unused-argument
10688
"""Test that -v flag is parsed correctly."""
10789
with patch("sys.argv", ["relink.py", "-v"]):
10890
args = relink.parse_arguments()
10991
assert args.verbose is True
11092

111-
def test_quiet_short_flag(
112-
self, mock_default_dirs
113-
): # pylint: disable=unused-argument
93+
def test_quiet_short_flag(self, temp_dirs): # pylint: disable=unused-argument
11494
"""Test that -q flag is parsed correctly."""
11595
with patch("sys.argv", ["relink.py", "-q"]):
11696
args = relink.parse_arguments()
11797
assert args.quiet is True
11898

119-
def test_default_verbosity(
120-
self, mock_default_dirs
121-
): # pylint: disable=unused-argument
99+
def test_default_verbosity(self, temp_dirs): # pylint: disable=unused-argument
122100
"""Test that default verbosity has both flags as False."""
123101
with patch("sys.argv", ["relink.py"]):
124102
args = relink.parse_arguments()
125103
assert args.verbose is False
126104
assert args.quiet is False
127105

128-
def test_verbose_and_quiet_mutually_exclusive(self, mock_default_dirs):
106+
def test_verbose_and_quiet_mutually_exclusive(self, temp_dirs):
129107
"""Test that --verbose and --quiet cannot be used together."""
130108
# pylint: disable=unused-argument
131109
with patch("sys.argv", ["relink.py", "--verbose", "--quiet"]):
@@ -134,7 +112,7 @@ def test_verbose_and_quiet_mutually_exclusive(self, mock_default_dirs):
134112
# Mutually exclusive arguments cause SystemExit with code 2
135113
assert exc_info.value.code == 2
136114

137-
def test_verbose_and_quiet_short_flags_mutually_exclusive(self, mock_default_dirs):
115+
def test_verbose_and_quiet_short_flags_mutually_exclusive(self, temp_dirs):
138116
"""Test that -v and -q cannot be used together."""
139117
# pylint: disable=unused-argument
140118
with patch("sys.argv", ["relink.py", "-v", "-q"]):
@@ -143,40 +121,40 @@ def test_verbose_and_quiet_short_flags_mutually_exclusive(self, mock_default_dir
143121
# Mutually exclusive arguments cause SystemExit with code 2
144122
assert exc_info.value.code == 2
145123

146-
def test_dry_run_flag(self, mock_default_dirs):
124+
def test_dry_run_flag(self, temp_dirs):
147125
"""Test that --dry-run flag is parsed correctly."""
148126
# pylint: disable=unused-argument
149127
with patch("sys.argv", ["relink.py", "--dry-run"]):
150128
args = relink.parse_arguments()
151129
assert args.dry_run is True
152130

153-
def test_dry_run_default(self, mock_default_dirs):
131+
def test_dry_run_default(self, temp_dirs):
154132
"""Test that dry_run defaults to False."""
155133
# pylint: disable=unused-argument
156134
with patch("sys.argv", ["relink.py"]):
157135
args = relink.parse_arguments()
158136
assert args.dry_run is False
159137

160-
def test_timing_flag(self, mock_default_dirs):
138+
def test_timing_flag(self, temp_dirs):
161139
"""Test that --timing flag is parsed correctly."""
162140
# pylint: disable=unused-argument
163141
with patch("sys.argv", ["relink.py", "--timing"]):
164142
args = relink.parse_arguments()
165143
assert args.timing is True
166144

167-
def test_timing_default(self, mock_default_dirs):
145+
def test_timing_default(self, temp_dirs):
168146
"""Test that timing defaults to False."""
169147
# pylint: disable=unused-argument
170148
with patch("sys.argv", ["relink.py"]):
171149
args = relink.parse_arguments()
172150
assert args.timing is False
173151

174-
def test_multiple_source_roots(self, mock_default_dirs, tmp_path):
152+
def test_multiple_source_roots(self, temp_dirs):
175153
"""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"
154+
inputdata_root, target_dir = temp_dirs
155+
source1 = Path(os.path.join(inputdata_root, "source1"))
156+
source2 = Path(os.path.join(inputdata_root, "source2"))
157+
source3 = Path(os.path.join(inputdata_root, "source3"))
180158
source1.mkdir()
181159
source2.mkdir()
182160
source3.mkdir()
@@ -189,11 +167,12 @@ def test_multiple_source_roots(self, mock_default_dirs, tmp_path):
189167
assert str(source3.resolve()) in args.source_root
190168
assert args.target_root == target_dir
191169

192-
def test_multiple_source_roots_with_target(self, tmp_path):
170+
def test_multiple_source_roots_with_target(self, temp_dirs):
193171
"""Test multiple source roots with custom target root."""
194-
source1 = tmp_path / "source1"
195-
source2 = tmp_path / "source2"
196-
target = tmp_path / "target"
172+
inputdata_root, target_dir = temp_dirs
173+
source1 = Path(os.path.join(inputdata_root, "source1"))
174+
source2 = Path(os.path.join(inputdata_root, "source2"))
175+
target = Path(os.path.join(target_dir, "target"))
197176
source1.mkdir()
198177
source2.mkdir()
199178
target.mkdir()
@@ -330,3 +309,26 @@ def test_process_args_modifies_args_in_place(self):
330309
# Should be the same object, modified in place
331310
assert args is original_args
332311
assert hasattr(args, "log_level")
312+
313+
def test_error_if_source_not_in_inputdata(self):
314+
"""Test that process_args errors if source isn't in inputdata_root."""
315+
args = argparse.Namespace(
316+
quiet=False, verbose=False, source_root="abc123", inputdata_root="def456"
317+
)
318+
with pytest.raises(argparse.ArgumentTypeError) as exc_info:
319+
relink.process_args(args)
320+
assert "not under inputdata root" in str(exc_info.value)
321+
322+
def test_error_if_target_in_inputdata(self):
323+
"""Test that process_args errors if target is in inputdata_root."""
324+
inputdata_root = "inputdata"
325+
target_root = os.path.join(inputdata_root, "abc123")
326+
args = argparse.Namespace(
327+
quiet=False,
328+
verbose=False,
329+
target_root=target_root,
330+
inputdata_root=inputdata_root,
331+
)
332+
with pytest.raises(argparse.ArgumentTypeError) as exc_info:
333+
relink.process_args(args)
334+
assert "must not be under inputdata root" in str(exc_info.value)

tests/relink/test_cmdline.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import os
66
import sys
77
import subprocess
8+
from pathlib import Path
89

910
import pytest
1011

@@ -98,15 +99,15 @@ def test_command_line_execution_actual_run(mock_dirs):
9899
assert "Created symbolic link:" in result.stdout
99100

100101

101-
def test_command_line_multiple_source_dirs(tmp_path):
102+
def test_command_line_multiple_source_dirs(temp_dirs):
102103
"""Test executing relink.py with multiple source directories."""
104+
inputdata_dir, target_dir = temp_dirs
103105
# Create multiple source directories
104-
source1 = tmp_path / "source1"
105-
source2 = tmp_path / "source2"
106-
target_dir = tmp_path / "target"
106+
source1 = Path(os.path.join(inputdata_dir, "source1"))
107+
source2 = Path(os.path.join(inputdata_dir, "source2"))
107108
source1.mkdir()
108109
source2.mkdir()
109-
target_dir.mkdir()
110+
target_dir = Path(target_dir)
110111

111112
# Create files in each source directory
112113
source1_file = source1 / "file1.txt"
@@ -134,7 +135,7 @@ def test_command_line_multiple_source_dirs(tmp_path):
134135
"--target-root",
135136
str(target_dir),
136137
"--inputdata-root",
137-
str(tmp_path),
138+
str(inputdata_dir),
138139
]
139140

140141
# Execute the command

0 commit comments

Comments
 (0)