From 4febe165eb05a5486fe7a7f2791c9325618aed52 Mon Sep 17 00:00:00 2001 From: Taewoon Kim Date: Mon, 26 Oct 2026 16:58:46 +0900 Subject: [PATCH] Install the default SIGINT handler in the self-SIGINT interrupt test test_sigint_during_a_query_that_calls_python_is_a_keyboard_interrupt sends SIGINT to its own process and needs Python's default_int_handler. Python installs that handler at startup only when SIGINT is not ignored, and a pytest started as a background job (a shell `cmd &` without job control, many launchers and agent shells) inherits SIGINT as ignored, so the signal was dropped, no query was interrupted and the test failed every time (20 of 20 on the laptop, on both the 26.10.2 and the 26.11.1-SNAPSHOT engine) while it passed in CI. The test now installs signal.default_int_handler for its duration and restores the old handler. tests/README.md says how to check a runner. Co-Authored-By: Claude Sonnet 5.5 --- tests/README.md | 4 ++++ tests/test_python_function_interrupt.py | 24 +++++++++++++++++++++++- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/tests/README.md b/tests/README.md index ef1c32d..6effc6c 100644 --- a/tests/README.md +++ b/tests/README.md @@ -28,6 +28,10 @@ uv run pytest -m server -v uv run pytest -m "not server" ``` +## Tests that send SIGINT to their own process + +A pytest started as a background job (a shell `cmd &` without job control, many launchers and agent shells) inherits SIGINT as ignored, and Python then installs no `KeyboardInterrupt` handler. A test that sends SIGINT to its own process (`test_python_function_interrupt.py`) installs `signal.default_int_handler` for its duration and restores the old handler; without it the signal is dropped, no query is interrupted and the test fails on such a runner while it passes in CI. Check a runner with `grep SigIgn /proc/self/status` (bit `0x2` is SIGINT) or `python -c "import signal; print(signal.getsignal(signal.SIGINT))"` (`` means ignored). A test that starts a child process for SIGINT resets the signal in the child (`test_sigint.py`, `preexec_fn`). + ## No skips The server tests do not skip when the server stack is absent: the wheel always ships it, and a test that skips on a missing feature cannot notice the feature going missing (in 26.7.2 the server JARs were dropped from the wheel and the guarded tests skipped, so the suite stayed green while the feature was gone). `test_server_packaging.py` states the packaging requirement and fails without the JARs. CI also fails any skip that `scripts/check_test_skips.py` does not list, and that list is empty. diff --git a/tests/test_python_function_interrupt.py b/tests/test_python_function_interrupt.py index 0ca4ddd..37fa818 100644 --- a/tests/test_python_function_interrupt.py +++ b/tests/test_python_function_interrupt.py @@ -7,6 +7,13 @@ (jpype-project/jpype#1496), which was an ArcadeDBError in about 5% of the runs; it is a KeyboardInterrupt now. Not run on Windows, where a signal cannot be sent to the own process like this (see test_sigint.py). + +The test needs Python's own SIGINT handler (``signal.default_int_handler``), and Python installs it +at startup only when SIGINT is not ignored. A pytest started as a background job inherits SIGINT as +ignored (a shell runs ``cmd &`` without job control that way, so do many launchers and agent +shells): ``os.kill(os.getpid(), SIGINT)`` is then dropped, no query is interrupted, and the test +failed every time on such a runner while it passed in CI. The ``default_sigint_handler`` fixture +installs the handler for the test and puts back what it found. """ import os @@ -31,7 +38,22 @@ def db(temp_db_path): yield database -def test_sigint_during_a_query_that_calls_python_is_a_keyboard_interrupt(db): +@pytest.fixture +def default_sigint_handler(): + """KeyboardInterrupt on SIGINT for the test, whatever the runner left (see the module docstring).""" + previous = signal.getsignal(signal.SIGINT) + signal.signal(signal.SIGINT, signal.default_int_handler) + try: + yield + finally: + # getsignal is None for a handler installed by C code: nothing to give back from Python + if previous is not None: + signal.signal(signal.SIGINT, previous) + + +def test_sigint_during_a_query_that_calls_python_is_a_keyboard_interrupt( + db, default_sigint_handler +): interrupted = 0 for delay in (0.05, 0.09, 0.13, 0.17, 0.21, 0.25, 0.29, 0.33): timer = threading.Timer(delay, os.kill, (os.getpid(), signal.SIGINT))