Native crashes print a Python traceback instead of nothing, and kill -USR1 dumps every thread's stack from a hung process without attaching anything or restarting it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
78 lines
2.5 KiB
Python
78 lines
2.5 KiB
Python
"""Turning a silent death into a traceback.
|
|
|
|
Two failures this project has actually hit leave nothing in the log: a native crash (no
|
|
Python traceback at all) and a hang (nothing to print by definition). faulthandler covers
|
|
both — the crash handlers dump the stack before dying, and SIGUSR1 dumps it on demand from
|
|
another terminal without restarting anything.
|
|
"""
|
|
import sys
|
|
import types
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
|
|
|
import diagnostics as dg # noqa: E402
|
|
|
|
|
|
class _FH:
|
|
def __init__(self, fail=False):
|
|
self.enabled = None
|
|
self.registered = []
|
|
self._fail = fail
|
|
|
|
def enable(self, **kw):
|
|
if self._fail:
|
|
raise RuntimeError("no can do")
|
|
self.enabled = kw
|
|
|
|
def register(self, signum, **kw):
|
|
self.registered.append((signum, kw))
|
|
|
|
|
|
@pytest.mark.parametrize("text", ["1", "true", "yes", "on", "ENABLE", " Enabled "])
|
|
def test_the_on_words(text):
|
|
assert dg.wanted(text)
|
|
|
|
|
|
@pytest.mark.parametrize("text", ["", None, "0", "off", "no", "maybe"])
|
|
def test_everything_else_is_off(text):
|
|
assert not dg.wanted(text)
|
|
|
|
|
|
def test_it_does_nothing_unless_asked():
|
|
"""Installing signal handlers in someone else's process should be a choice."""
|
|
fh = _FH()
|
|
assert dg.enable("", fh=fh) is None
|
|
assert fh.enabled is None and fh.registered == []
|
|
|
|
|
|
def test_it_installs_the_crash_handlers_for_every_thread():
|
|
fh = _FH()
|
|
note = dg.enable("1", fh=fh, sig=types.SimpleNamespace())
|
|
assert fh.enabled["all_threads"] is True
|
|
assert "native crash" in note
|
|
|
|
|
|
def test_sigusr1_dumps_a_hung_process_on_demand():
|
|
"""The half that matters for a hang: no py-spy, no gdb, no restart."""
|
|
fh = _FH()
|
|
note = dg.enable("1", fh=fh, sig=types.SimpleNamespace(SIGUSR1=10))
|
|
assert fh.registered and fh.registered[0][0] == 10
|
|
assert fh.registered[0][1]["all_threads"] is True
|
|
assert "kill -USR1" in note
|
|
|
|
|
|
def test_no_sigusr1_is_not_an_error():
|
|
"""Windows has no SIGUSR1; the crash handler is still the part that matters there."""
|
|
fh = _FH()
|
|
note = dg.enable("1", fh=fh, sig=types.SimpleNamespace())
|
|
assert fh.registered == [] and "kill -USR1" not in note and note
|
|
|
|
|
|
def test_a_broken_faulthandler_never_breaks_the_import():
|
|
"""This runs at import. A diagnostic that stops FunPack loading is worse than none."""
|
|
note = dg.enable("1", fh=_FH(fail=True), sig=types.SimpleNamespace())
|
|
assert "could not be enabled" in note
|