Skip to content

Commit 61d7822

Browse files
authored
Merge pull request #16 from samsrabin/add-rimport-tests
Add rimport tests
2 parents db095fd + 02ece9e commit 61d7822

9 files changed

Lines changed: 1113 additions & 3 deletions

rimport

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
#!/glade/u/apps/derecho/24.12/opt/view/bin/python
2+
# TODO: Move all the Python into new file rimport.py for simpler testing. Keep rimport as a
3+
# convenience wrapper.
24
"""
35
A drop-in CLI replacement for the legacy `rimport` csh tool.
46
@@ -77,7 +79,7 @@ def build_parser() -> argparse.ArgumentParser:
7779
help=argparse.SUPPRESS,
7880
)
7981

80-
# Provide -help to mirror legacy behavior (no -h)
82+
# Provide -help to mirror legacy behavior
8183
parser.add_argument("-help", action="help", help=argparse.SUPPRESS)
8284

8385
return parser
@@ -122,6 +124,8 @@ def stage_data(src: Path, inputdata_root: Path, staging_root: Path) -> None:
122124
try:
123125
rel = src.resolve().relative_to(inputdata_root.resolve())
124126
except ValueError:
127+
# TODO: Do not hard-code string here
128+
# TODO: Check whether it's IN THE DIRECTORY, not whether the path contains a string
125129
if "d651077" in str(src):
126130
raise RuntimeError(f"Source file {src.name} is already published.")
127131
else:
@@ -137,17 +141,21 @@ def ensure_running_as(target_user: str, argv: list[str]) -> None:
137141
try:
138142
target_uid = pwd.getpwnam(target_user).pw_uid
139143
except KeyError:
144+
# TODO: Raise Python error instead of SystemExit
140145
print(f"rimport: target user '{target_user}' not found on this system", file=sys.stderr)
141146
raise SystemExit(2)
142147

143148
if os.geteuid() != target_uid:
144149
if not sys.stdin.isatty():
150+
# TODO: Do not hard-code "cesmdata" here
145151
print("rimport: need interactive TTY to authenticate as 'cesmdata' (2FA).\n"
146152
" Try: sudo -u cesmdata rimport …", file=sys.stderr)
153+
# TODO: Raise Python error instead of SystemExit
147154
raise SystemExit(2)
148155
# Re-exec under target user; this invokes sudo’s normal password/2FA flow.
149156
os.execvp("sudo", ["sudo", "-u", target_user, "--"] + argv)
150157

158+
# TODO: Unused; delete.
151159
def safe_mvandlink(src: Path, dst: Path) -> None:
152160
dst.parent.mkdir(parents=True, exist_ok=True)
153161
# Move (handles cross-filesystem with copy2+remove under the hood)
@@ -163,6 +171,7 @@ def get_staging_root() -> Path:
163171
env = os.getenv("RIMPORT_STAGING")
164172
if env:
165173
return Path(env).expanduser().resolve()
174+
# TODO: This should be a module-level variable.
166175
return Path("/glade/campaign/collections/gdex/data/d651077/cesmdata/inputdata")
167176

168177

@@ -171,8 +180,10 @@ def main(argv: List[str] | None = None) -> int:
171180
args = parser.parse_args(argv)
172181

173182
# Ensure we are running as the cesmdata account before touching the tree
174-
# Comment out the next line if you prefer to run `sudox -u cesmdata rimport …` explicitly.
175-
ensure_running_as("cesmdata", sys.argv)
183+
# Set env var RIMPORT_SKIP_USER_CHECK=1 if you prefer to run `sudox -u cesmdata rimport …`
184+
# explicitly (or for testing).
185+
if os.getenv("RIMPORT_SKIP_USER_CHECK") != "1":
186+
ensure_running_as("cesmdata", sys.argv)
176187

177188
root = Path(args.inputdata).expanduser().resolve()
178189
if not root.exists():

tests/rimport/__init__.py

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

tests/rimport/test_build_parser.py

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
"""
2+
Tests for build_parser() function in rimport script.
3+
"""
4+
5+
import os
6+
import sys
7+
import argparse
8+
import importlib.util
9+
from importlib.machinery import SourceFileLoader
10+
11+
import pytest
12+
13+
# Import rimport module from file without .py extension
14+
rimport_path = os.path.join(
15+
os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))),
16+
"rimport",
17+
)
18+
loader = SourceFileLoader("rimport", rimport_path)
19+
spec = importlib.util.spec_from_loader("rimport", loader)
20+
if spec is None:
21+
raise ImportError(f"Could not create spec for rimport from {rimport_path}")
22+
rimport = importlib.util.module_from_spec(spec)
23+
sys.modules["rimport"] = rimport
24+
loader.exec_module(rimport)
25+
26+
27+
class TestBuildParser:
28+
"""Test suite for build_parser() function."""
29+
30+
def test_parser_creation(self):
31+
"""Test that build_parser creates an ArgumentParser."""
32+
parser = rimport.build_parser()
33+
assert isinstance(parser, argparse.ArgumentParser)
34+
35+
def test_parser_prog_name(self):
36+
"""Test that parser has correct program name."""
37+
parser = rimport.build_parser()
38+
assert parser.prog == "rimport"
39+
40+
def test_file_argument_accepted(self):
41+
"""Test that -file argument is accepted."""
42+
parser = rimport.build_parser()
43+
args = parser.parse_args(["-file", "test.txt"])
44+
assert args.file == "test.txt"
45+
assert args.filelist is None
46+
47+
def test_list_argument_accepted(self):
48+
"""Test that -list argument is accepted."""
49+
parser = rimport.build_parser()
50+
args = parser.parse_args(["-list", "files.txt"])
51+
assert args.filelist == "files.txt"
52+
assert args.file is None
53+
54+
def test_file_and_list_mutually_exclusive(self, capsys):
55+
"""Test that -file and -list cannot be used together."""
56+
parser = rimport.build_parser()
57+
with pytest.raises(SystemExit):
58+
parser.parse_args(["-file", "test.txt", "-list", "files.txt"])
59+
60+
# Check that the error message explains the problem
61+
captured = capsys.readouterr()
62+
stderr_lines = captured.err.strip().split("\n")
63+
assert "not allowed with argument" in stderr_lines[-1]
64+
65+
def test_file_or_list_required(self, capsys):
66+
"""Test that either -file or -list is required."""
67+
parser = rimport.build_parser()
68+
with pytest.raises(SystemExit):
69+
parser.parse_args([])
70+
71+
# Check that the error message explains the problem
72+
captured = capsys.readouterr()
73+
stderr_lines = captured.err.strip().split("\n")
74+
assert "rimport: error: one of the arguments" in stderr_lines[-1]
75+
76+
def test_inputdata_default(self):
77+
"""Test that -inputdata has correct default value."""
78+
parser = rimport.build_parser()
79+
args = parser.parse_args(["-file", "test.txt"])
80+
expected_default = os.path.join(
81+
"/glade", "campaign", "cesm", "cesmdata", "cseg", "inputdata"
82+
)
83+
assert args.inputdata == expected_default
84+
85+
def test_inputdata_custom(self):
86+
"""Test that -inputdata can be customized."""
87+
parser = rimport.build_parser()
88+
custom_path = "/custom/path"
89+
args = parser.parse_args(["-file", "test.txt", "-inputdata", custom_path])
90+
assert args.inputdata == custom_path
91+
92+
@pytest.mark.parametrize("help_flag", ["-help", "-h"])
93+
def test_help_flags_show_help(self, help_flag):
94+
"""Test that -help and -h flags trigger help."""
95+
parser = rimport.build_parser()
96+
with pytest.raises(SystemExit) as exc_info:
97+
parser.parse_args([help_flag])
98+
# Help should exit with code 0
99+
assert exc_info.value.code == 0
100+
101+
def test_file_with_inputdata(self):
102+
"""Test combining -file with -inputdata."""
103+
parser = rimport.build_parser()
104+
args = parser.parse_args(["-file", "data.nc", "-inputdata", "/my/data"])
105+
assert args.file == "data.nc"
106+
assert args.inputdata == "/my/data"
107+
108+
def test_list_with_inputdata(self):
109+
"""Test combining -list with -inputdata."""
110+
parser = rimport.build_parser()
111+
args = parser.parse_args(["-list", "files.txt", "-inputdata", "/my/data"])
112+
assert args.filelist == "files.txt"
113+
assert args.inputdata == "/my/data"

0 commit comments

Comments
 (0)