Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions tests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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))"` (`<Handlers.SIG_IGN: 1>` 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.
Expand Down
24 changes: 23 additions & 1 deletion tests/test_python_function_interrupt.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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))
Expand Down
Loading