Skip to content

Commit 3dfbca9

Browse files
committed
test(run[deadline]) Cover SIGKILL-didn't-reap warning path
why: ``_terminate_process`` emits a WARNING when a child resists both ``SIGTERM`` and ``SIGKILL`` (i.e. the second ``proc.wait`` raises ``TimeoutExpired`` and the final ``poll()`` still reports the child alive). Reproducing that state with a real subprocess requires a ``D``-state child or kernel fixtures, so the path was uncovered -- correct by inspection only. With downstream operators relying on the log record to triage zombie risk, "correct by inspection" isn't enough. what: - Add ``test_terminate_process_warns_when_sigkill_does_not_reap`` using ``unittest.mock.MagicMock(spec=subprocess.Popen)`` -- ``poll`` always reports alive, ``wait`` always raises ``TimeoutExpired``, so both ``_terminate_process`` branches fire and the WARNING is emitted with the canonical ``vcs_cmd`` extra. - Asserts the warning level, the message substring, the ``vcs_cmd`` extra value, and that ``terminate()`` and ``kill()`` were each called once -- enough to detect a regression that swallows the log or skips the second wait.
1 parent bc5af2c commit 3dfbca9

1 file changed

Lines changed: 45 additions & 0 deletions

File tree

tests/_internal/test_run.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import sys
99
import time
1010
import typing as t
11+
import unittest.mock
1112

1213
import pytest
1314

@@ -256,6 +257,50 @@ def test_run_timeout_logs_deadline_exceeded(
256257
assert "time.sleep" in cmd_extra
257258

258259

260+
def test_terminate_process_warns_when_sigkill_does_not_reap(
261+
caplog: pytest.LogCaptureFixture,
262+
) -> None:
263+
"""A child that resists ``SIGKILL`` surfaces a WARNING with ``vcs_cmd``.
264+
265+
Reproducing a true uninterruptible (``D`` state on POSIX) child requires
266+
kernel fixtures that aren't available in a pytest run, so we substitute
267+
a ``MagicMock`` whose ``poll()`` always reports the child alive and
268+
whose ``wait()`` always raises ``TimeoutExpired``. That drives both
269+
branches inside ``_terminate_process`` -- SIGTERM grace expires
270+
(DEBUG escalation log), SIGKILL is sent, the second wait also times
271+
out, and the final ``poll() is None`` triggers the WARNING this test
272+
asserts on.
273+
"""
274+
fake_proc = unittest.mock.MagicMock(spec=subprocess.Popen)
275+
fake_proc.args = ["sleep", "60"]
276+
fake_proc.returncode = None
277+
fake_proc.poll.return_value = None
278+
fake_proc.wait.side_effect = subprocess.TimeoutExpired(
279+
cmd=fake_proc.args,
280+
timeout=0.05,
281+
)
282+
283+
with caplog.at_level(logging.DEBUG, logger="libvcs._internal.run"):
284+
run_module._terminate_process(
285+
t.cast("subprocess.Popen[bytes]", fake_proc),
286+
"test-cmd",
287+
)
288+
289+
fake_proc.terminate.assert_called_once()
290+
fake_proc.kill.assert_called_once()
291+
292+
warnings = [
293+
record
294+
for record in caplog.records
295+
if record.levelname == "WARNING" and "did not reap" in record.getMessage()
296+
]
297+
assert len(warnings) == 1
298+
record = warnings[0]
299+
assert hasattr(record, "vcs_cmd")
300+
cmd_extra = t.cast("str", record.vcs_cmd)
301+
assert cmd_extra == "test-cmd"
302+
303+
259304
@pytest.mark.skipif(
260305
sys.platform == "win32",
261306
reason="os.close(sys.stderr.fileno()) has POSIX-specific semantics",

0 commit comments

Comments
 (0)