Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
b8a9f18
rimport: Anchor relative CLI-arg paths to cwd inside the inputdata tree
samsrabin Aug 25, 2026
f9b5fae
rimport: Anchor --list relative entries to the list file's own directory
samsrabin Aug 25, 2026
27885c4
relink: Pin cwd-relative resolution of positionals from an inputdata …
samsrabin Aug 25, 2026
34b02a7
docs: Describe cwd/list-relative path resolution in rimport help and …
samsrabin Aug 25, 2026
75c79cd
rimport: Guard stage_data against directory sources
samsrabin Aug 25, 2026
964a719
rimport: Fix stage_data docstring overclaim on symlink-to-directory
samsrabin Aug 25, 2026
8768208
tests: Pin --list entries anchoring to list-file dir, not cwd
samsrabin Aug 25, 2026
17cf275
rimport: Don't raise when cwd has been deleted
samsrabin Aug 25, 2026
69b3e47
rimport: Make --help and the list-file error self-sufficient
samsrabin Aug 25, 2026
1b770e4
tests: Add decoy to relink single-file pinning test
samsrabin Aug 25, 2026
1a2991d
tests(rimport): cover list-error remedy text, pin log level, add Setu…
samsrabin Aug 25, 2026
85fb2d7
tests(rimport): revert item-3 Setup headers per review finding
samsrabin Aug 25, 2026
6422292
tests(rimport): use absolute paths in eight e2e tests, drop hidden fa…
samsrabin Aug 25, 2026
4f7a9a2
rimport: Delete the root-fallback for relative CLI/list resolution
samsrabin Aug 25, 2026
92739b8
rimport: Add pre-flight validation so a bad path aborts before publis…
samsrabin Aug 26, 2026
bdb4335
docs(rimport): describe the one-rule resolution and pre-flight valida…
samsrabin Aug 26, 2026
03866de
tests: bring nested_mock_dirs fixture in shape with fixture_temp_dirs
samsrabin Aug 31, 2026
142111b
tests: derive decoy basenames from real files, assert decoy content d…
samsrabin Aug 31, 2026
66ca8b3
tests(rimport): give test_list_inside_tree_relative_entries a decoy
samsrabin Aug 31, 2026
8f756a2
tests: de-duplicate two near-identical test pairs added in this PR
samsrabin Aug 31, 2026
8f15cf1
tests: make comments and docstrings stand on their own
samsrabin Aug 31, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ Tools used for publishing CESM input data.
Notes:
- Use `rimport --check` if you'd like to see the current status of a file, including whether it's available for download.
- The `relink.py` script was previously used for step 3 above, but that functionality is now built into `rimport`. It's still there if you want to use it by itself.
- A relative filename passed to `rimport` directly (via `--file` or as a positional argument) is always resolved against your current directory — never against the inputdata root, and it doesn't matter whether you're running from inside or outside the inputdata tree. Pass an absolute path if you want to name a file without regard to your current directory.
- A relative entry in a `--list` file is always resolved against that list file's own directory — again never against the inputdata root, wherever the list file itself lives. Pass absolute entries in the list if you want them independent of the list file's location.
- Before staging anything, `rimport` validates every file it's about to process (all `--file`/`--list`/positional entries together). If any of them fail — missing, a directory, a broken symlink, outside the inputdata root, etc. — none of them are touched, and every failing path is reported at once so you can fix them all in one pass. This is a promise about rejected input, not about success: pre-flight passing doesn't guarantee the whole batch will finish, since a file can still fail later for a reason pre-flight can't see (e.g. a runtime/relink failure partway through).
- `--check` is gated by the same pre-flight validation as a real run: if any file in the batch fails validation, `rimport` reports the failures and exits without checking (or reporting on) any of the other files. This is deliberate, not a bug — fix the bad entries and re-run to see the rest.
- Exit codes: `0` means everything succeeded (or, under `--check`, everything checked cleanly); `2` means the run was rejected before touching anything (bad arguments, a missing/empty list file, or a pre-flight validation failure); `1` means pre-flight passed but something failed for real while actually being staged or relinked.

## Filenames and metadata:

Expand Down
239 changes: 201 additions & 38 deletions rimport

Large diffs are not rendered by default.

25 changes: 25 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,28 @@ def fixture_temp_dirs():
# Cleanup
shutil.rmtree(source_dir, ignore_errors=True)
shutil.rmtree(target_dir, ignore_errors=True)


@pytest.fixture(scope="function", name="nested_mock_dirs")
def fixture_nested_mock_dirs(tmp_path):
"""Create a nested source/target directory layout for testing relative-path
resolution."""
source_dir = tmp_path / "source"
target_dir = tmp_path / "target"
source_sub_dir = source_dir / "sub"
target_sub_dir = target_dir / "sub"
source_sub_dir.mkdir(parents=True)
target_sub_dir.mkdir(parents=True)

# Create a test file
source_file = source_sub_dir / "test_file.txt"
target_file = target_sub_dir / "test_file.txt"
source_file.write_text("source content")
target_file.write_text("target content")

yield source_dir, target_dir, source_sub_dir, source_file, target_file

# No explicit cleanup: everything above was created under tmp_path, which
# pytest already removes automatically. Unlike fixture_temp_dirs (which
# uses tempfile.mkdtemp, a directory pytest does not manage), an explicit
# shutil.rmtree here would be redundant.
126 changes: 126 additions & 0 deletions tests/relink/test_cmdline.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,132 @@ def test_command_line_execution_given_file(mock_dirs):
assert f"{INDENT}Created symbolic link:" in result.stdout


def _run_relink_relative_positional(nested_mock_dirs, positional_arg):
"""Create a same-named decoy at the inputdata root (outside "sub"), then
run relink.py with `positional_arg` as the sole path argument and cwd
set to the inputdata subdirectory.

Shared setup and invocation for
test_command_line_relative_file_from_inputdata_subdir and
test_command_line_relative_dir_dot_from_inputdata_subdir. Those two
tests differ ONLY in `positional_arg` (a bare filename vs "."), and that
single difference changes what regression each one discriminates and
which of its own assertions does the discriminating -- see each test's
docstring. This helper deliberately makes no assertion about the
outcome of the run beyond confirming the decoy setup itself: the payoff
assertions that make each test meaningful differ between the two
callers and stay in the tests, not here.

Returns (result, decoy_file) for the caller to assert against.
"""
source_dir, target_dir, source_sub_dir, source_file, target_file = (
nested_mock_dirs
)

# Decoy file directly under the inputdata root (outside "sub"), same
# name as the intended file but different content, with a matching
# target copy (also different content).
decoy_file = source_dir / source_file.name
decoy_target = target_dir / target_file.name
decoy_file.write_text("decoy content")
decoy_target.write_text("decoy target content")
Comment thread
samsrabin marked this conversation as resolved.
assert decoy_file.read_text() != source_file.read_text()
assert decoy_target.read_text() != target_file.read_text()

# Get the path to relink.py
relink_script = os.path.join(
os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))),
"relink.py",
)

# Build the command
command = [
sys.executable,
relink_script,
positional_arg,
"--target-root",
str(target_dir),
"--inputdata-root",
str(source_dir),
]

# Execute the command with cwd set to the inputdata subdirectory
result = subprocess.run(
command, cwd=str(source_sub_dir), capture_output=True, text=True, check=False
)

return result, decoy_file


def test_command_line_relative_file_from_inputdata_subdir(nested_mock_dirs):
"""Test that a bare relative filename is resolved against the cwd (an
inputdata subdirectory), not against the inputdata root.

A same-named decoy file sits directly under the inputdata root, outside
"sub", with different content than the intended file, plus a matching
target copy (also with different content) so it *would* be relinked if a
root-relative regression resolved "test_file.txt" against inputdata_root
instead of cwd. Because this is a single-file argument rather than a
directory to recurse into, such a regression would process the decoy
INSTEAD OF the subdir file, not in addition to it. The return code and
decoy_file.is_file() pass either way (the latter because is_file()
follows a symlink to a real file); what actually discriminates is that
the decoy stays a plain file with its original content (not relinked),
and that the intended subdir file is the one converted to a symlink --
pointing at the subdir's target copy, not the root decoy's.
"""
*_, source_file, target_file = nested_mock_dirs

result, decoy_file = _run_relink_relative_positional(
nested_mock_dirs, source_file.name
)

# Verify the command executed successfully
assert result.returncode == 0, f"Command failed with stderr: {result.stderr}"

# Verify the intended subdir file was converted to a symlink pointing at
# the subdir's target copy (not the root decoy's)
assert source_file.is_symlink()
assert os.readlink(str(source_file)) == str(target_file)

# Verify the decoy at the inputdata root was NOT reached/relinked
assert decoy_file.is_file()
assert not decoy_file.is_symlink()
assert decoy_file.read_text() == "decoy content"


def test_command_line_relative_dir_dot_from_inputdata_subdir(nested_mock_dirs):
Comment thread
samsrabin marked this conversation as resolved.
"""Test that '.' is resolved against the cwd (an inputdata subdirectory),
not against the inputdata root.

A same-named decoy file sits directly under the inputdata root, outside
"sub", with different content than the intended file, plus a matching
target copy (also with different content) so it *would* be relinkable if
reached. This test's discriminator is LOCATION, not the name collision:
because relink's search is recursive, resolving '.' against cwd
(source_dir/sub) never reaches the decoy, while a root-relative
regression -- resolving '.' against inputdata_root (source_dir) instead
-- would recurse into the decoy too. Only the decoy assertion below
actually discriminates between those two resolutions; the symlink-target
subdirectory (source_file) is reached by recursion either way.
"""
*_, source_file, target_file = nested_mock_dirs

result, decoy_file = _run_relink_relative_positional(nested_mock_dirs, ".")

# Verify the command executed successfully
assert result.returncode == 0, f"Command failed with stderr: {result.stderr}"

# Verify the file was converted to a symlink pointing at the target copy
assert source_file.is_symlink()
assert os.readlink(str(source_file)) == str(target_file)

# Verify the decoy outside "sub" was NOT reached/relinked
assert decoy_file.is_file()
assert not decoy_file.is_symlink()
assert decoy_file.read_text() == "decoy content"


def test_command_line_multiple_source_dirs(temp_dirs):
"""Test executing relink.py with multiple source directories."""
inputdata_dir, target_dir = temp_dirs
Expand Down
Loading
Loading