Skip to content

Commit 1f21ee2

Browse files
authored
Merge pull request #79 from CCPBioSim/78-test-cases-for-input-arguments
Additional Test Cases for Input Configuration
2 parents e04dad2 + 04379ac commit 1f21ee2

File tree

4 files changed

+419
-247
lines changed

4 files changed

+419
-247
lines changed

tests/data/__init__.py

Whitespace-only changes.
Lines changed: 395 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,395 @@
1+
import argparse
2+
import os
3+
import shutil
4+
import tempfile
5+
import unittest
6+
from unittest.mock import MagicMock, mock_open, patch
7+
8+
import tests.data as data
9+
from CodeEntropy.config.arg_config_manager import ConfigManager
10+
from CodeEntropy.main_mcc import main
11+
12+
13+
class test_arg_config_manager(unittest.TestCase):
14+
"""
15+
Unit tests for the ConfigManager.
16+
"""
17+
18+
def setUp(self):
19+
"""
20+
Setup test data and output directories.
21+
"""
22+
self.test_data_dir = os.path.dirname(data.__file__)
23+
self.test_dir = tempfile.mkdtemp(prefix="CodeEntropy_")
24+
self.config_file = os.path.join(self.test_dir, "config.yaml")
25+
26+
# Create a mock config file
27+
with patch("builtins.open", new_callable=mock_open) as mock_file:
28+
self.setup_file(mock_file)
29+
with open(self.config_file, "w") as f:
30+
f.write(mock_file.return_value.read())
31+
32+
# Change to test directory
33+
self._orig_dir = os.getcwd()
34+
os.chdir(self.test_dir)
35+
36+
def tearDown(self):
37+
"""
38+
Clean up after each test.
39+
"""
40+
os.chdir(self._orig_dir)
41+
if os.path.exists(self.test_dir):
42+
shutil.rmtree(self.test_dir)
43+
44+
def list_data_files(self):
45+
"""
46+
List all files in the test data directory.
47+
"""
48+
return os.listdir(self.test_data_dir)
49+
50+
def setup_file(self, mock_file):
51+
"""
52+
Mock the contents of a configuration file.
53+
"""
54+
mock_file.return_value = mock_open(
55+
read_data="--- \n \nrun1:\n "
56+
"top_traj_file: ['/path/to/tpr', '/path/to/trr']\n "
57+
"selection_string: 'all'\n "
58+
"start: 0\n "
59+
"end: -1\n "
60+
"step: 1\n "
61+
"bin_width: 30\n "
62+
"tempra: 298.0\n "
63+
"verbose: False\n "
64+
"thread: 1\n "
65+
"outfile: 'outfile.out'\n "
66+
"resfile: 'res_outfile.out'\n "
67+
"mout: null\n "
68+
"force_partitioning: 0.5\n "
69+
"waterEntropy: False"
70+
).return_value
71+
72+
@patch("builtins.open", new_callable=mock_open)
73+
@patch("os.path.exists", return_value=True)
74+
def test_load_config(self, mock_exists, mock_file):
75+
"""
76+
Test loading a valid configuration file.
77+
"""
78+
arg_config = ConfigManager()
79+
self.setup_file(mock_file)
80+
config = arg_config.load_config(self.config_file)
81+
self.assertIn("run1", config)
82+
self.assertEqual(
83+
config["run1"]["top_traj_file"], ["/path/to/tpr", "/path/to/trr"]
84+
)
85+
86+
@patch("builtins.open", side_effect=FileNotFoundError)
87+
def test_load_config_file_not_found(self, mock_file):
88+
"""
89+
Test loading a configuration file that does not exist.
90+
"""
91+
arg_config = ConfigManager()
92+
with self.assertRaises(FileNotFoundError):
93+
arg_config.load_config(self.config_file)
94+
95+
@patch.object(ConfigManager, "load_config", return_value=None)
96+
def test_no_cli_no_yaml(self, mock_load_config):
97+
"""Test behavior when no CLI arguments and no YAML file are provided."""
98+
with self.assertRaises(ValueError) as context:
99+
main()
100+
self.assertEqual(
101+
str(context.exception),
102+
"No configuration file found, and no CLI arguments were provided.",
103+
)
104+
105+
def test_invalid_run_config_type(self):
106+
"""
107+
Test that passing an invalid type for run_config raises a TypeError.
108+
"""
109+
arg_config = ConfigManager()
110+
args = MagicMock()
111+
invalid_configs = ["string", 123, 3.14, ["list"], {("tuple_key",): "value"}]
112+
113+
for invalid in invalid_configs:
114+
with self.assertRaises(TypeError):
115+
arg_config.merge_configs(args, invalid)
116+
117+
@patch(
118+
"argparse.ArgumentParser.parse_args",
119+
return_value=MagicMock(
120+
top_traj_file=["/path/to/tpr", "/path/to/trr"],
121+
selection_string="all",
122+
start=0,
123+
end=-1,
124+
step=1,
125+
bin_width=30,
126+
tempra=298.0,
127+
verbose=False,
128+
thread=1,
129+
outfile="outfile.out",
130+
resfile="res_outfile.out",
131+
mout=None,
132+
force_partitioning=0.5,
133+
waterEntropy=False,
134+
),
135+
)
136+
def test_setup_argparse(self, mock_args):
137+
"""
138+
Test parsing command-line arguments.
139+
"""
140+
arg_config = ConfigManager()
141+
parser = arg_config.setup_argparse()
142+
args = parser.parse_args()
143+
self.assertEqual(args.top_traj_file, ["/path/to/tpr", "/path/to/trr"])
144+
self.assertEqual(args.selection_string, "all")
145+
146+
def test_cli_overrides_defaults(self):
147+
"""
148+
Test if CLI parameters override default values.
149+
"""
150+
arg_config = ConfigManager()
151+
parser = arg_config.setup_argparse()
152+
args = parser.parse_args(
153+
["--top_traj_file", "/cli/path", "--selection_string", "cli_value"]
154+
)
155+
self.assertEqual(args.top_traj_file, ["/cli/path"])
156+
self.assertEqual(args.selection_string, "cli_value")
157+
158+
def test_cli_overrides_yaml(self):
159+
"""
160+
Test if CLI parameters override YAML parameters correctly.
161+
"""
162+
arg_config = ConfigManager()
163+
parser = arg_config.setup_argparse()
164+
args = parser.parse_args(
165+
["--top_traj_file", "/cli/path", "--selection_string", "cli_value"]
166+
)
167+
run_config = {"top_traj_file": ["/yaml/path"], "selection_string": "yaml_value"}
168+
merged_args = arg_config.merge_configs(args, run_config)
169+
self.assertEqual(merged_args.top_traj_file, ["/cli/path"])
170+
self.assertEqual(merged_args.selection_string, "cli_value")
171+
172+
def test_cli_overrides_yaml_with_multiple_values(self):
173+
"""
174+
Ensures that CLI arguments override YAML when multiple values are provided in
175+
YAML.
176+
"""
177+
arg_config = ConfigManager()
178+
yaml_config = {"top_traj_file": ["/yaml/path1", "/yaml/path2"]}
179+
args = argparse.Namespace(top_traj_file=["/cli/path"])
180+
181+
merged_args = arg_config.merge_configs(args, yaml_config)
182+
183+
self.assertEqual(merged_args.top_traj_file, ["/cli/path"])
184+
185+
def test_yaml_overrides_defaults(self):
186+
"""
187+
Test if YAML parameters override default values.
188+
"""
189+
run_config = {"top_traj_file": ["/yaml/path"], "selection_string": "yaml_value"}
190+
args = argparse.Namespace()
191+
arg_config = ConfigManager()
192+
merged_args = arg_config.merge_configs(args, run_config)
193+
self.assertEqual(merged_args.top_traj_file, ["/yaml/path"])
194+
self.assertEqual(merged_args.selection_string, "yaml_value")
195+
196+
def test_yaml_does_not_override_cli_if_set(self):
197+
"""
198+
Ensure YAML does not override CLI arguments that are set.
199+
"""
200+
arg_config = ConfigManager()
201+
202+
yaml_config = {"bin_width": 50}
203+
args = argparse.Namespace(bin_width=100)
204+
205+
merged_args = arg_config.merge_configs(args, yaml_config)
206+
207+
self.assertEqual(merged_args.bin_width, 100)
208+
209+
def test_yaml_overrides_defaults_when_no_cli(self):
210+
"""
211+
Test if YAML parameters override default values when no CLI input is given.
212+
"""
213+
arg_config = ConfigManager()
214+
215+
yaml_config = {
216+
"top_traj_file": ["/yaml/path"],
217+
"bin_width": 50,
218+
}
219+
220+
args = argparse.Namespace()
221+
222+
merged_args = arg_config.merge_configs(args, yaml_config)
223+
224+
self.assertEqual(merged_args.top_traj_file, ["/yaml/path"])
225+
self.assertEqual(merged_args.bin_width, 50)
226+
227+
def test_yaml_none_does_not_override_defaults(self):
228+
"""
229+
Ensures that YAML values set to `None` do not override existing CLI values.
230+
"""
231+
arg_config = ConfigManager()
232+
yaml_config = {"bin_width": None}
233+
args = argparse.Namespace(bin_width=100)
234+
235+
merged_args = arg_config.merge_configs(args, yaml_config)
236+
237+
self.assertEqual(merged_args.bin_width, 100)
238+
239+
def test_hierarchy_cli_yaml_defaults(self):
240+
"""
241+
Test if CLI arguments override YAML, and YAML overrides defaults.
242+
"""
243+
arg_config = ConfigManager()
244+
245+
yaml_config = {
246+
"top_traj_file": ["/yaml/path", "/yaml/path"],
247+
"bin_width": "50",
248+
}
249+
250+
args = argparse.Namespace(
251+
top_traj_file=["/cli/path", "/cli/path"], bin_width=100
252+
)
253+
254+
merged_args = arg_config.merge_configs(args, yaml_config)
255+
256+
self.assertEqual(merged_args.top_traj_file, ["/cli/path", "/cli/path"])
257+
self.assertEqual(merged_args.bin_width, 100)
258+
259+
def test_merge_configs(self):
260+
"""
261+
Test merging default arguments with a run configuration.
262+
"""
263+
arg_config = ConfigManager()
264+
args = MagicMock(
265+
top_traj_file=None,
266+
selection_string=None,
267+
start=None,
268+
end=None,
269+
step=None,
270+
bin_width=None,
271+
tempra=None,
272+
verbose=None,
273+
thread=None,
274+
outfile=None,
275+
resfile=None,
276+
mout=None,
277+
force_partitioning=None,
278+
waterEntropy=None,
279+
)
280+
run_config = {
281+
"top_traj_file": ["/path/to/tpr", "/path/to/trr"],
282+
"selection_string": "all",
283+
"start": 0,
284+
"end": -1,
285+
"step": 1,
286+
"bin_width": 30,
287+
"tempra": 298.0,
288+
"verbose": False,
289+
"thread": 1,
290+
"outfile": "outfile.out",
291+
"resfile": "res_outfile.out",
292+
"mout": None,
293+
"force_partitioning": 0.5,
294+
"waterEntropy": False,
295+
}
296+
merged_args = arg_config.merge_configs(args, run_config)
297+
self.assertEqual(merged_args.top_traj_file, ["/path/to/tpr", "/path/to/trr"])
298+
self.assertEqual(merged_args.selection_string, "all")
299+
300+
def test_merge_with_none_yaml(self):
301+
"""
302+
Ensure merging still works if no YAML config is provided.
303+
"""
304+
arg_config = ConfigManager()
305+
306+
args = argparse.Namespace(top_traj_file=["/cli/path"])
307+
yaml_config = None
308+
309+
merged_args = arg_config.merge_configs(args, yaml_config)
310+
311+
self.assertEqual(merged_args.top_traj_file, ["/cli/path"])
312+
313+
@patch("argparse.ArgumentParser.parse_args")
314+
def test_default_values(self, mock_parse_args):
315+
"""
316+
Test if argument parser assigns default values correctly.
317+
"""
318+
arg_config = ConfigManager()
319+
mock_parse_args.return_value = MagicMock(
320+
top_traj_file=["example.top", "example.traj"]
321+
)
322+
parser = arg_config.setup_argparse()
323+
args = parser.parse_args()
324+
self.assertEqual(args.top_traj_file, ["example.top", "example.traj"])
325+
326+
def test_fallback_to_defaults(self):
327+
"""
328+
Ensure arguments fall back to defaults if neither YAML nor CLI provides them.
329+
"""
330+
arg_config = ConfigManager()
331+
332+
yaml_config = {}
333+
args = argparse.Namespace()
334+
335+
merged_args = arg_config.merge_configs(args, yaml_config)
336+
337+
self.assertEqual(merged_args.step, 1)
338+
self.assertEqual(merged_args.end, -1)
339+
340+
@patch(
341+
"argparse.ArgumentParser.parse_args", return_value=MagicMock(top_traj_file=None)
342+
)
343+
def test_missing_required_arguments(self, mock_args):
344+
"""
345+
Test behavior when required arguments are missing.
346+
"""
347+
arg_config = ConfigManager()
348+
parser = arg_config.setup_argparse()
349+
args = parser.parse_args()
350+
with self.assertRaises(ValueError):
351+
if not args.top_traj_file:
352+
raise ValueError(
353+
"The 'top_traj_file' argument is required but not provided."
354+
)
355+
356+
def test_invalid_argument_type(self):
357+
"""
358+
Test handling of invalid argument types.
359+
"""
360+
arg_config = ConfigManager()
361+
parser = arg_config.setup_argparse()
362+
with self.assertRaises(SystemExit):
363+
parser.parse_args(["--start", "invalid"])
364+
365+
@patch(
366+
"argparse.ArgumentParser.parse_args", return_value=MagicMock(start=-1, end=-10)
367+
)
368+
def test_edge_case_argument_values(self, mock_args):
369+
"""
370+
Test parsing of edge case values.
371+
"""
372+
arg_config = ConfigManager()
373+
parser = arg_config.setup_argparse()
374+
args = parser.parse_args()
375+
self.assertEqual(args.start, -1)
376+
self.assertEqual(args.end, -10)
377+
378+
@patch("builtins.open", new_callable=mock_open, read_data="--- \n")
379+
@patch("os.path.exists", return_value=True)
380+
def test_empty_yaml_config(self, mock_exists, mock_file):
381+
"""
382+
Test behavior when an empty YAML file is provided.
383+
Should use defaults or raise an appropriate error.
384+
"""
385+
386+
arg_config = ConfigManager()
387+
388+
config = arg_config.load_config(self.config_file)
389+
390+
self.assertIsInstance(config, dict)
391+
self.assertEqual(config, {})
392+
393+
394+
if __name__ == "__main__":
395+
unittest.main()

0 commit comments

Comments
 (0)