Skip to content

Commit a5b0be0

Browse files
committed
rimport: Add tests of main().
1 parent 891c990 commit a5b0be0

1 file changed

Lines changed: 355 additions & 0 deletions

File tree

tests/rimport/test_main.py

Lines changed: 355 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,355 @@
1+
"""
2+
Tests for main() function in rimport script.
3+
4+
These tests focus on the logic and control flow in main(), mocking out
5+
the helper functions to isolate main()'s behavior.
6+
"""
7+
8+
import os
9+
import importlib.util
10+
from importlib.machinery import SourceFileLoader
11+
from unittest.mock import patch, call
12+
13+
# pylint: disable=too-many-arguments,too-many-positional-arguments
14+
15+
# Import rimport module from file without .py extension
16+
rimport_path = os.path.join(
17+
os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))),
18+
"rimport",
19+
)
20+
loader = SourceFileLoader("rimport", rimport_path)
21+
spec = importlib.util.spec_from_loader("rimport", loader)
22+
if spec is None:
23+
raise ImportError(f"Could not create spec for rimport from {rimport_path}")
24+
rimport = importlib.util.module_from_spec(spec)
25+
# Don't add to sys.modules to avoid conflict with other test files (patches here not being applied)
26+
loader.exec_module(rimport)
27+
28+
29+
class TestMain:
30+
"""Test suite for main() function."""
31+
32+
@patch.object(rimport, "stage_data")
33+
@patch.object(rimport, "get_staging_root")
34+
@patch.object(rimport, "normalize_paths")
35+
@patch.object(rimport, "ensure_running_as")
36+
def test_single_file_success(
37+
self,
38+
_mock_ensure_running_as,
39+
mock_normalize_paths,
40+
mock_get_staging_root,
41+
mock_stage_data,
42+
tmp_path,
43+
):
44+
"""Test main() logic flow when a single file stages successfully."""
45+
# Setup
46+
inputdata_root = tmp_path / "inputdata"
47+
inputdata_root.mkdir()
48+
staging_root = tmp_path / "staging"
49+
staging_root.mkdir()
50+
51+
mock_get_staging_root.return_value = staging_root
52+
test_file = inputdata_root / "test.nc"
53+
mock_normalize_paths.return_value = [test_file]
54+
55+
# Run
56+
result = rimport.main(["-file", "test.nc", "-inputdata", str(inputdata_root)])
57+
58+
# Verify
59+
assert result == 0
60+
mock_normalize_paths.assert_called_once_with(inputdata_root, ["test.nc"])
61+
mock_stage_data.assert_called_once_with(
62+
test_file, inputdata_root, staging_root, False
63+
)
64+
65+
@patch.object(rimport, "stage_data")
66+
@patch.object(rimport, "get_staging_root")
67+
@patch.object(rimport, "normalize_paths")
68+
@patch.object(rimport, "read_filelist")
69+
@patch.object(rimport, "ensure_running_as")
70+
def test_file_list_success(
71+
self,
72+
_mock_ensure_running_as,
73+
mock_read_filelist,
74+
mock_normalize_paths,
75+
mock_get_staging_root,
76+
mock_stage_data,
77+
tmp_path,
78+
):
79+
"""Test main() logic flow when a file list stages successfully."""
80+
# Setup
81+
inputdata_root = tmp_path / "inputdata"
82+
inputdata_root.mkdir()
83+
staging_root = tmp_path / "staging"
84+
staging_root.mkdir()
85+
filelist = tmp_path / "files.txt"
86+
filelist.write_text("file1.nc\nfile2.nc\n")
87+
88+
mock_get_staging_root.return_value = staging_root
89+
mock_read_filelist.return_value = ["file1.nc", "file2.nc"]
90+
file1 = inputdata_root / "file1.nc"
91+
file2 = inputdata_root / "file2.nc"
92+
mock_normalize_paths.return_value = [file1, file2]
93+
94+
# Run
95+
result = rimport.main(
96+
["-list", str(filelist), "-inputdata", str(inputdata_root)]
97+
)
98+
99+
# Verify
100+
assert result == 0
101+
mock_read_filelist.assert_called_once_with(filelist)
102+
mock_normalize_paths.assert_called_once_with(
103+
inputdata_root, ["file1.nc", "file2.nc"]
104+
)
105+
assert mock_stage_data.call_count == 2
106+
mock_stage_data.assert_has_calls(
107+
[
108+
call(file1, inputdata_root, staging_root, False),
109+
call(file2, inputdata_root, staging_root, False),
110+
]
111+
)
112+
113+
@patch.object(rimport, "stage_data")
114+
@patch.object(rimport, "get_staging_root")
115+
@patch.object(rimport, "normalize_paths")
116+
@patch.object(rimport, "ensure_running_as")
117+
def test_stage_data_exception_handling(
118+
self,
119+
_mock_ensure_running_as,
120+
mock_normalize_paths,
121+
_mock_get_staging_root,
122+
mock_stage_data,
123+
tmp_path,
124+
capsys,
125+
):
126+
"""Test that main() handles exceptions from stage_data and continues processing."""
127+
# Setup
128+
inputdata_root = tmp_path / "inputdata"
129+
inputdata_root.mkdir()
130+
131+
file1 = inputdata_root / "file1.nc"
132+
file2 = inputdata_root / "file2.nc"
133+
file3 = inputdata_root / "file3.nc"
134+
mock_normalize_paths.return_value = [file1, file2, file3]
135+
136+
# Make stage_data fail for file2 but succeed for others
137+
def stage_data_side_effect(src, *_args, **_kwargs):
138+
if src == file2:
139+
raise RuntimeError("Test error for file2")
140+
141+
mock_stage_data.side_effect = stage_data_side_effect
142+
143+
# Run
144+
result = rimport.main(["-file", "test.nc", "-inputdata", str(inputdata_root)])
145+
146+
# Verify
147+
assert result == 1 # Should return 1 because of error
148+
assert mock_stage_data.call_count == 3 # All files should be attempted
149+
150+
# Check that error was printed to stderr
151+
captured = capsys.readouterr()
152+
assert "error processing" in captured.err
153+
assert "Test error for file2" in captured.err
154+
155+
@patch.object(rimport, "ensure_running_as")
156+
def test_nonexistent_inputdata_directory(
157+
self, _mock_ensure_running_as, tmp_path, capsys
158+
):
159+
"""Test that main() returns error code 2 for nonexistent inputdata directory."""
160+
nonexistent = tmp_path / "nonexistent"
161+
162+
result = rimport.main(["-file", "test.nc", "-inputdata", str(nonexistent)])
163+
164+
assert result == 2
165+
captured = capsys.readouterr()
166+
assert "inputdata directory does not exist" in captured.err
167+
168+
@patch.object(rimport, "ensure_running_as")
169+
def test_nonexistent_filelist(self, _mock_ensure_running_as, tmp_path, capsys):
170+
"""Test that main() returns error code 2 for nonexistent file list."""
171+
inputdata_root = tmp_path / "inputdata"
172+
inputdata_root.mkdir()
173+
nonexistent_list = tmp_path / "nonexistent.txt"
174+
175+
result = rimport.main(
176+
["-list", str(nonexistent_list), "-inputdata", str(inputdata_root)]
177+
)
178+
179+
assert result == 2
180+
captured = capsys.readouterr()
181+
assert "list file not found" in captured.err
182+
183+
@patch.object(rimport, "read_filelist")
184+
@patch.object(rimport, "ensure_running_as")
185+
def test_empty_filelist(
186+
self, _mock_ensure_running_as, mock_read_filelist, tmp_path, capsys
187+
):
188+
"""Test that main() returns error code 2 for empty file list."""
189+
inputdata_root = tmp_path / "inputdata"
190+
inputdata_root.mkdir()
191+
filelist = tmp_path / "empty.txt"
192+
filelist.write_text("")
193+
194+
mock_read_filelist.return_value = []
195+
196+
result = rimport.main(
197+
["-list", str(filelist), "-inputdata", str(inputdata_root)]
198+
)
199+
200+
assert result == 2
201+
captured = capsys.readouterr()
202+
assert "no filenames found in list" in captured.err
203+
204+
@patch.object(rimport, "stage_data")
205+
@patch.object(rimport, "get_staging_root")
206+
@patch.object(rimport, "normalize_paths")
207+
@patch.object(rimport, "ensure_running_as")
208+
def test_check_mode_calls(
209+
self,
210+
mock_ensure_running_as,
211+
mock_normalize_paths,
212+
mock_get_staging_root,
213+
mock_stage_data,
214+
tmp_path,
215+
):
216+
"""Test that --check mode skips the user check but does call stage_data."""
217+
inputdata_root = tmp_path / "inputdata"
218+
inputdata_root.mkdir()
219+
staging_root = tmp_path / "staging"
220+
staging_root.mkdir()
221+
222+
mock_get_staging_root.return_value = staging_root
223+
test_file = inputdata_root / "test.nc"
224+
mock_normalize_paths.return_value = [test_file]
225+
226+
result = rimport.main(
227+
["-file", "test.nc", "-inputdata", str(inputdata_root), "--check"]
228+
)
229+
230+
assert result == 0
231+
# ensure_running_as should NOT be called in check mode
232+
mock_ensure_running_as.assert_not_called()
233+
# stage_data should be called with check=True
234+
mock_stage_data.assert_called_once_with(
235+
test_file, inputdata_root, staging_root, True
236+
)
237+
238+
@patch.object(rimport, "stage_data")
239+
@patch.object(rimport, "get_staging_root")
240+
@patch.object(rimport, "normalize_paths")
241+
@patch.object(rimport, "ensure_running_as")
242+
def test_skip_user_check_env_var(
243+
self,
244+
mock_ensure_running_as,
245+
mock_normalize_paths,
246+
_mock_get_staging_root,
247+
_mock_stage,
248+
tmp_path,
249+
monkeypatch,
250+
):
251+
"""Test that RIMPORT_SKIP_USER_CHECK=1 skips the user check."""
252+
monkeypatch.setenv("RIMPORT_SKIP_USER_CHECK", "1")
253+
254+
inputdata_root = tmp_path / "inputdata"
255+
inputdata_root.mkdir()
256+
257+
test_file = inputdata_root / "test.nc"
258+
mock_normalize_paths.return_value = [test_file]
259+
260+
result = rimport.main(["-file", "test.nc", "-inputdata", str(inputdata_root)])
261+
262+
assert result == 0
263+
# ensure_running_as should NOT be called when env var is set
264+
mock_ensure_running_as.assert_not_called()
265+
266+
@patch.object(rimport, "stage_data")
267+
@patch.object(rimport, "get_staging_root")
268+
@patch.object(rimport, "normalize_paths")
269+
@patch.object(rimport, "ensure_running_as")
270+
def test_prints_file_path_before_processing(
271+
self,
272+
_mock_ensure_running_as,
273+
mock_normalize_paths,
274+
_mock_get_staging_root,
275+
_mock_stage,
276+
tmp_path,
277+
capsys,
278+
):
279+
"""Test that main() prints each file path before processing."""
280+
inputdata_root = tmp_path / "inputdata"
281+
inputdata_root.mkdir()
282+
file1 = inputdata_root / "file1.nc"
283+
file2 = inputdata_root / "file2.nc"
284+
mock_normalize_paths.return_value = [file1, file2]
285+
286+
result = rimport.main(["-file", "test.nc", "-inputdata", str(inputdata_root)])
287+
288+
assert result == 0
289+
captured = capsys.readouterr()
290+
# Check that file paths are printed with quotes
291+
assert f"'{file1}':" in captured.out
292+
assert f"'{file2}':" in captured.out
293+
294+
@patch.object(rimport, "stage_data")
295+
@patch.object(rimport, "get_staging_root")
296+
@patch.object(rimport, "normalize_paths")
297+
@patch.object(rimport, "ensure_running_as")
298+
def test_multiple_errors_returns_1(
299+
self,
300+
_mock_ensure_running_as,
301+
mock_normalize_paths,
302+
_mock_get_staging_root,
303+
mock_stage_data,
304+
tmp_path,
305+
):
306+
"""Test that main() returns 1 when multiple files fail."""
307+
inputdata_root = tmp_path / "inputdata"
308+
inputdata_root.mkdir()
309+
310+
file1 = inputdata_root / "file1.nc"
311+
file2 = inputdata_root / "file2.nc"
312+
file3 = inputdata_root / "file3.nc"
313+
mock_normalize_paths.return_value = [file1, file2, file3]
314+
315+
# Make all files fail
316+
mock_stage_data.side_effect = RuntimeError("Test error")
317+
318+
result = rimport.main(["-file", "test.nc", "-inputdata", str(inputdata_root)])
319+
320+
assert result == 1
321+
assert mock_stage_data.call_count == 3
322+
323+
@patch.object(rimport, "stage_data")
324+
@patch.object(rimport, "get_staging_root")
325+
@patch.object(rimport, "normalize_paths")
326+
@patch.object(rimport, "ensure_running_as")
327+
def test_error_counter_increments_correctly(
328+
self,
329+
_mock_ensure_running_as,
330+
mock_normalize_paths,
331+
_mock_get_staging_root,
332+
mock_stage_data,
333+
tmp_path,
334+
capsys,
335+
):
336+
"""Test that the error counter increments for each failed file."""
337+
inputdata_root = tmp_path / "inputdata"
338+
inputdata_root.mkdir()
339+
340+
files = [inputdata_root / f"file{i}.nc" for i in range(5)]
341+
mock_normalize_paths.return_value = files
342+
343+
# Make files 1 and 3 fail
344+
def stage_data_side_effect(src, *_args, **_kwargs):
345+
if src in [files[1], files[3]]:
346+
raise RuntimeError(f"Test error for {src.name}")
347+
348+
mock_stage_data.side_effect = stage_data_side_effect
349+
350+
result = rimport.main(["-file", "test.nc", "-inputdata", str(inputdata_root)])
351+
352+
assert result == 1
353+
captured = capsys.readouterr()
354+
# Should have 2 error messages
355+
assert captured.err.count("error processing") == 2

0 commit comments

Comments
 (0)