33import shutil
44import signal
55import subprocess
6+ import sys
67from glob import glob
78from types import SimpleNamespace
89from typing import TYPE_CHECKING
@@ -227,7 +228,7 @@ def test_gamma_checks(self) -> None:
227228
228229
229230class TestVaspJobTerminate :
230- """Tests for VaspJob.terminate() process group termination ."""
231+ """Tests for VaspJob.terminate() - cross-platform with POSIX process group support ."""
231232
232233 @pytest .fixture
233234 def mocks (self ) -> "Generator[SimpleNamespace, None, None]" :
@@ -238,8 +239,8 @@ def mocks(self) -> "Generator[SimpleNamespace, None, None]":
238239
239240 with (
240241 patch ("custodian.vasp.jobs.logger" ) as logger ,
241- patch ("os.killpg" ) as killpg ,
242- patch ("os.getpgid" , return_value = 67890 ),
242+ patch ("os.killpg" , create = True ) as killpg ,
243+ patch ("os.getpgid" , return_value = 67890 , create = True ),
243244 ):
244245 yield SimpleNamespace (job = job , process = process , logger = logger , killpg = killpg )
245246
@@ -259,7 +260,7 @@ def test_graceful_sigterm(self, mocks: SimpleNamespace) -> None:
259260 mocks .logger .info .assert_any_call ("Sending SIGTERM to process group 67890" )
260261 mocks .logger .info .assert_any_call ("Process 12345 terminated gracefully" )
261262 mocks .killpg .assert_called_once_with (67890 , signal .SIGTERM )
262- mocks .process .wait .assert_called_once_with (timeout = 10.0 ) # default timeout
263+ mocks .process .wait .assert_called_once_with (timeout = 10.0 )
263264 mocks .process .kill .assert_not_called ()
264265
265266 def test_force_kill_after_timeout (self , mocks : SimpleNamespace ) -> None :
@@ -268,7 +269,7 @@ def test_force_kill_after_timeout(self, mocks: SimpleNamespace) -> None:
268269 mocks .process .wait .side_effect = [subprocess .TimeoutExpired ("vasp" , 10 ), None ]
269270 mocks .job .terminate ()
270271
271- mocks .logger .warning .assert_called_with ("SIGTERM timeout (10.0s), sending SIGKILL to process group 67890 " )
272+ mocks .logger .warning .assert_called_with ("Timeout (10.0s), force killing 12345 " )
272273 mocks .logger .info .assert_any_call ("Process 12345 force-killed" )
273274 assert mocks .killpg .call_count == 2
274275 mocks .process .kill .assert_called_once ()
@@ -281,12 +282,12 @@ def test_custom_timeout(self, mocks: SimpleNamespace) -> None:
281282 mocks .job .terminate ()
282283
283284 mocks .process .wait .assert_any_call (timeout = 60.0 )
284- mocks .logger .warning .assert_called_with ("SIGTERM timeout (60.0s), sending SIGKILL to process group 67890 " )
285+ mocks .logger .warning .assert_called_with ("Timeout (60.0s), force killing 12345 " )
285286
286287 def test_process_not_found_on_getpgid (self , mocks : SimpleNamespace ) -> None :
287288 """ProcessLookupError when getting PGID."""
288289 mocks .process .poll .return_value = None
289- with patch ("os.getpgid" , side_effect = ProcessLookupError ):
290+ with patch ("os.getpgid" , side_effect = ProcessLookupError , create = True ):
290291 mocks .job .terminate ()
291292
292293 mocks .logger .warning .assert_called_with ("Process 12345 not found (already dead)" )
@@ -300,10 +301,11 @@ def test_process_group_not_found_on_sigterm(self, mocks: SimpleNamespace) -> Non
300301
301302 mocks .logger .warning .assert_called_with ("Process group 67890 not found (already dead)" )
302303
304+ @pytest .mark .skipif (sys .platform == "win32" , reason = "sleep command and PGID not available" )
303305 def test_integration_with_real_process (self ) -> None :
304- """Integration test with real subprocess."""
306+ """Integration test with real subprocess (POSIX only) ."""
305307 vasp_job = VaspJob .__new__ (VaspJob )
306- vasp_job .terminate_timeout = 10.0 # Set since __new__ bypasses __init__
308+ vasp_job .terminate_timeout = 10.0
307309 real_process = subprocess .Popen (["sleep" , "30" ], start_new_session = True )
308310 vasp_job ._vasp_process = real_process
309311 original_pgid = os .getpgid (real_process .pid )
0 commit comments