66import sys
77from unittest import mock
88
9+ import pytest
10+
911from CodeEntropy .core .dask_clusters import HPCDaskManager
1012
1113
@@ -22,6 +24,8 @@ def args_helper(args_list):
2224 parser .add_argument ("--hpc-nodes" , type = int , default = 4 )
2325 parser .add_argument ("--hpc-processes" , type = int , default = 20 )
2426 parser .add_argument ("--hpc-walltime" , type = str , default = "24:00:00" )
27+ parser .add_argument ("--hpc-interface" , type = str , default = None )
28+ parser .add_argument ("--hpc-modules" , nargs = "+" , default = None )
2529
2630 parser .add_argument ("--conda-env" , type = str , default = "codeentropy" )
2731 parser .add_argument ("--conda-exec" , type = str , default = "conda" )
@@ -138,6 +142,33 @@ def test_slurm_prologues_mamba():
138142 ]
139143
140144
145+ def test_slurm_prologues_includes_hpc_modules ():
146+ args = args_helper (
147+ [
148+ "--hpc-modules" ,
149+ "apps/binapps/conda/miniforge3/25.9.1" ,
150+ "gcc/12.2.0" ,
151+ "--conda-env" ,
152+ "codeentropy" ,
153+ "--conda-exec" ,
154+ "conda" ,
155+ "--conda-path" ,
156+ "/path/to/conda" ,
157+ ]
158+ )
159+ manager = HPCDaskManager (args )
160+
161+ prologue = manager .slurm_prologues ()
162+
163+ assert prologue == [
164+ "module load apps/binapps/conda/miniforge3/25.9.1" ,
165+ "module load gcc/12.2.0" ,
166+ 'eval "$(/path/to/conda shell.bash hook)"' ,
167+ "conda activate codeentropy" ,
168+ "export SLURM_CPU_FREQ_REQ=2250000" ,
169+ ]
170+
171+
141172@mock .patch ("psutil.net_if_addrs" )
142173def test_system_network_interface_prefers_ib0 (net_if_addrs ):
143174 net_if_addrs .return_value = {"ib0" : [], "eth0" : []}
@@ -149,13 +180,34 @@ def test_system_network_interface_prefers_ib0(net_if_addrs):
149180
150181
151182@mock .patch ("psutil.net_if_addrs" )
152- def test_system_network_interface_fallback (net_if_addrs ):
153- net_if_addrs .return_value = {"lo" : [], "docker0" : []}
183+ def test_system_network_interface_uses_configured_interface (net_if_addrs ):
184+ net_if_addrs .return_value = {"lo" : [], "ib0" : []}
185+
186+ args = args_helper (["--hpc-interface" , "custom0" ])
187+ manager = HPCDaskManager (args )
188+
189+ assert manager .system_network_interface () == "custom0"
190+
191+
192+ @mock .patch ("psutil.net_if_addrs" )
193+ def test_system_network_interface_falls_back_to_non_loopback_interface (net_if_addrs ):
194+ net_if_addrs .return_value = {"lo" : [], "ens5" : [], "docker0" : []}
154195
155196 args = args_helper ([])
156197 manager = HPCDaskManager (args )
157198
158- assert manager .system_network_interface () == "lo"
199+ assert manager .system_network_interface () == "ens5"
200+
201+
202+ @mock .patch ("psutil.net_if_addrs" )
203+ def test_system_network_interface_raises_without_non_loopback_interface (net_if_addrs ):
204+ net_if_addrs .return_value = {"lo" : [], "docker0" : [], "veth123" : []}
205+
206+ args = args_helper ([])
207+ manager = HPCDaskManager (args )
208+
209+ with pytest .raises (RuntimeError , match = "Could not find a non-loopback" ):
210+ manager .system_network_interface ()
159211
160212
161213@mock .patch ("subprocess.check_output" )
@@ -219,7 +271,6 @@ def test_submit_master_writes_expected_script_conda(check_output):
219271 assert "conda activate codeentropy" in script
220272 assert "srun CodeEntropy" in script
221273 assert "--submit" not in script
222- assert "srun CodeEntropy" in script
223274 assert " --submit " not in script
224275 assert not script .rstrip ().endswith (" true" )
225276
@@ -274,6 +325,53 @@ def test_submit_master_writes_expected_script_mamba(check_output):
274325 os .remove ("CodeEntropy-master-submit.sh" )
275326
276327
328+ @mock .patch ("subprocess.check_output" )
329+ def test_submit_master_writes_hpc_modules (check_output ):
330+ check_output .return_value = b"Submitted batch job 12345\n "
331+
332+ args = args_helper (
333+ [
334+ "--hpc-modules" ,
335+ "apps/binapps/conda/miniforge3/25.9.1" ,
336+ "--conda-env" ,
337+ "codeentropy" ,
338+ "--conda-exec" ,
339+ "conda" ,
340+ "--conda-path" ,
341+ "/path/to/conda" ,
342+ "--hpc-queue" ,
343+ "standard" ,
344+ ]
345+ )
346+ manager = HPCDaskManager (args )
347+
348+ cli = [
349+ "CodeEntropy" ,
350+ "--top_traj_file" ,
351+ "topology.tpr" ,
352+ "trajectory.trr" ,
353+ "--hpc" ,
354+ "true" ,
355+ "--submit" ,
356+ "true" ,
357+ ]
358+
359+ try :
360+ with mock .patch .object (sys , "argv" , cli ):
361+ manager .submit_master ()
362+
363+ with open ("CodeEntropy-master-submit.sh" , encoding = "utf-8" ) as file :
364+ script = file .read ()
365+
366+ assert "module load apps/binapps/conda/miniforge3/25.9.1" in script
367+ assert 'eval "$(/path/to/conda shell.bash hook)"' in script
368+ assert "conda activate codeentropy" in script
369+
370+ finally :
371+ if os .path .exists ("CodeEntropy-master-submit.sh" ):
372+ os .remove ("CodeEntropy-master-submit.sh" )
373+
374+
277375@mock .patch ("CodeEntropy.core.dask_clusters.Client" )
278376@mock .patch ("CodeEntropy.core.dask_clusters.SLURMCluster" )
279377@mock .patch .object (HPCDaskManager , "system_network_interface" )
@@ -324,6 +422,10 @@ def test_configure_cluster_writes_job_script(
324422 assert returned_client is client_instance
325423
326424 slurm_cluster .assert_called_once ()
425+ _ , kwargs = slurm_cluster .call_args
426+ assert kwargs ["interface" ] == "ib0"
427+ assert kwargs ["scheduler_options" ] == {"interface" : "ib0" }
428+
327429 cluster_instance .scale .assert_called_once_with (jobs = 4 )
328430 client .assert_called_once_with (cluster_instance )
329431
0 commit comments