px-explorer-v4 / tests /test_crash_handler.py
BuildBot
push_hf: sparse-branch fΓΌr HF-Push (nur Code, 0 LFS)
9644d0b
Raw
History Blame Contribute Delete
5.6 kB
"""Tests for crash_handler.py β€” verify the install() hooks actually kill
the process on unhandled exceptions, and the PX_HARD_CRASH=0 kill-switch
disables them.
Run: /run/media/julian/ML4/open-mythos_p2/venv_openmythos/bin/python tests/test_crash_handler.py
Each test launches a fresh subprocess (since the hooks affect the current
process and we can't easily undo them in-process). We assert on the
subprocess return code + stderr content.
"""
import os
import subprocess
import sys
import textwrap
_HERE = os.path.dirname(os.path.abspath(__file__))
_ROOT = os.path.dirname(_HERE)
_PYTHON = sys.executable
def _run_subprocess(script, env_extra=None, timeout=10):
"""Run ``script`` (Python code) in a fresh subprocess and return
(returncode, stderr). Inject sys.path so ``import crash_handler``
resolves to the project copy."""
full_env = os.environ.copy()
if env_extra:
full_env.update(env_extra)
full_env["PYTHONPATH"] = _ROOT + os.pathsep + full_env.get("PYTHONPATH", "")
r = subprocess.run(
[_PYTHON, "-c", script],
capture_output=True, text=True, timeout=timeout, env=full_env,
)
return r.returncode, r.stderr
# ── Tests ────────────────────────────────────────────────────────────────
def test_sys_hook_kills_process():
"""sys.excepthook: raise RuntimeError β†’ exit code 1 + CRASH_HANDLER banner."""
script = textwrap.dedent("""
import crash_handler
crash_handler.install()
raise RuntimeError("deliberate sys-hook test crash")
""")
rc, stderr = _run_subprocess(script)
assert rc == 1, f"expected exit 1, got {rc}; stderr={stderr!r}"
assert "CRASH_HANDLER" in stderr, f"missing banner in stderr={stderr!r}"
assert "deliberate sys-hook test crash" in stderr, stderr
assert "Traceback" in stderr, stderr
assert "sys.excepthook" in stderr, stderr
def test_threading_hook_kills_process():
"""threading.excepthook: Thread raises β†’ process dies with banner."""
script = textwrap.dedent("""
import crash_handler
crash_handler.install()
import threading, time
def boom():
raise RuntimeError("deliberate threading-hook test crash")
t = threading.Thread(target=boom, name="test-boomer")
t.start()
# Main thread sleeps; the worker crash should fire threading.excepthook
# which calls os._exit(1) and tears down the whole process.
time.sleep(10)
""")
rc, stderr = _run_subprocess(script, timeout=15)
assert rc == 1, f"expected exit 1, got {rc}; stderr={stderr!r}"
assert "CRASH_HANDLER" in stderr, f"missing banner in stderr={stderr!r}"
assert "deliberate threading-hook test crash" in stderr, stderr
assert "threading.excepthook" in stderr, stderr
assert "test-boomer" in stderr, stderr
def test_env_flag_disables_hook():
"""PX_HARD_CRASH=0 β†’ install() is no-op; unhandled exception propagates
normally (Python prints traceback, exit code != 0 but NOT 1 from us)."""
script = textwrap.dedent("""
import crash_handler
crash_handler.install()
raise RuntimeError("kill-switch test crash")
""")
rc, stderr = _run_subprocess(script, env_extra={"PX_HARD_CRASH": "0"})
# When hook is disabled, Python's default behavior prints the traceback
# and exits with code 1 (uncaught exception). The CRASH_HANDLER banner
# must NOT appear (that's the kill-switch assertion).
assert "CRASH_HANDLER" not in stderr, (
f"banner should be suppressed when PX_HARD_CRASH=0, got stderr={stderr!r}"
)
# The exception itself still propagates β€” that's the point of disabling
# the hook is NOT to hide bugs from the user, but to opt out of the
# forced process termination.
assert "kill-switch test crash" in stderr, stderr
# Default uncaught-exception exit code is 1 too, so rc check is the same.
assert rc == 1, f"expected exit 1 (Python default), got {rc}"
def test_keyboard_interrupt_not_caught():
"""KeyboardInterrupt must NOT trigger hard-crash β€” graceful Ctrl-C."""
script = textwrap.dedent("""
import crash_handler
crash_handler.install()
raise KeyboardInterrupt()
""")
rc, stderr = _run_subprocess(script)
# KeyboardInterrupt exits with 130 by default (128 + SIGINT=2).
# Our hook must NOT fire the CRASH_HANDLER banner.
assert "CRASH_HANDLER" not in stderr, (
f"banner should NOT appear for KeyboardInterrupt, got stderr={stderr!r}"
)
assert rc != 1, f"KeyboardInterrupt should not exit with 1, got {rc}"
# ── Runner ────────────────────────────────────────────────────────────────
def _run_all():
tests = [v for k, v in sorted(globals().items())
if k.startswith("test_") and callable(v)]
passed = 0
failed = 0
for t in tests:
try:
t()
print(f"PASS {t.__name__}")
passed += 1
except AssertionError as e:
print(f"FAIL {t.__name__}: {e}")
failed += 1
except Exception as e: # noqa
print(f"ERROR {t.__name__}: {type(e).__name__}: {e}")
failed += 1
print(f"\n{passed} passed, {failed} failed")
return failed == 0
if __name__ == "__main__":
sys.exit(0 if _run_all() else 1)