|
1 | 1 | """Tests for s_tui.helper_functions module.""" |
2 | 2 |
|
3 | 3 | import os |
| 4 | +import signal |
4 | 5 | import sys |
5 | 6 | import json |
6 | 7 | import tempfile |
7 | 8 |
|
| 9 | +import psutil |
8 | 10 | import pytest |
| 11 | +from unittest.mock import MagicMock, patch |
9 | 12 |
|
10 | 13 | from s_tui.helper_functions import ( |
11 | 14 | __version__, |
@@ -201,9 +204,59 @@ def test_make_user_config_dir(self, tmp_path, monkeypatch): |
201 | 204 |
|
202 | 205 | class TestKillChildProcesses: |
203 | 206 | def test_none_parent(self): |
204 | | - """Should handle None gracefully (AttributeError caught).""" |
| 207 | + """Should handle None gracefully.""" |
205 | 208 | kill_child_processes(None) # should not raise |
206 | 209 |
|
| 210 | + def test_sigterm_via_process_group(self): |
| 211 | + """Should send SIGTERM to the process group first.""" |
| 212 | + parent = MagicMock() |
| 213 | + parent.pid = 12345 |
| 214 | + parent.children.return_value = [] |
| 215 | + |
| 216 | + with patch("os.getpgid", return_value=12345) as mock_getpgid, patch( |
| 217 | + "os.killpg" |
| 218 | + ) as mock_killpg, patch("psutil.wait_procs", return_value=([], [])): |
| 219 | + kill_child_processes(parent, timeout=1) |
| 220 | + mock_getpgid.assert_called_once_with(12345) |
| 221 | + mock_killpg.assert_called_once_with(12345, signal.SIGTERM) |
| 222 | + |
| 223 | + def test_fallback_to_per_process_terminate(self): |
| 224 | + """When process group kill fails, falls back to per-process terminate.""" |
| 225 | + child = MagicMock() |
| 226 | + parent = MagicMock() |
| 227 | + parent.pid = 100 |
| 228 | + parent.children.return_value = [child] |
| 229 | + |
| 230 | + with patch("os.getpgid", side_effect=OSError("no pgid")), patch( |
| 231 | + "psutil.wait_procs", return_value=([], []) |
| 232 | + ): |
| 233 | + kill_child_processes(parent, timeout=1) |
| 234 | + child.terminate.assert_called_once() |
| 235 | + parent.terminate.assert_called_once() |
| 236 | + |
| 237 | + def test_sigkill_stragglers_after_timeout(self): |
| 238 | + """Processes still alive after timeout get SIGKILL.""" |
| 239 | + straggler = MagicMock() |
| 240 | + parent = MagicMock() |
| 241 | + parent.pid = 200 |
| 242 | + parent.children.return_value = [] |
| 243 | + |
| 244 | + with patch("os.getpgid", return_value=200), patch("os.killpg"), patch( |
| 245 | + "psutil.wait_procs", return_value=([], [straggler]) |
| 246 | + ): |
| 247 | + kill_child_processes(parent, timeout=1) |
| 248 | + straggler.kill.assert_called_once() |
| 249 | + |
| 250 | + def test_already_dead_process(self): |
| 251 | + """Should not raise when process is already dead.""" |
| 252 | + parent = MagicMock() |
| 253 | + parent.pid = 300 |
| 254 | + |
| 255 | + with patch("os.getpgid", side_effect=ProcessLookupError()), patch.object( |
| 256 | + parent, "children", side_effect=psutil.NoSuchProcess(300) |
| 257 | + ): |
| 258 | + kill_child_processes(parent, timeout=1) # should not raise |
| 259 | + |
207 | 260 |
|
208 | 261 | # --------------------------------------------------------------------------- |
209 | 262 | # output_to_terminal / output_to_json |
|
0 commit comments