pybind11/tests/test_gil_scoped.py
Michael Šimáček c4a05f9344
Add support for GraalPy (#5380)
* Initial support for GraalPy

* Mark tests that currently fail on GraalPy with xfail

* Add graalpy to CI

* Limit test deps on graalpy to available binary wheels

* Skip cmake test installed_function on GraalPy

CMake won't find libpython on GraalPy, it either fails or silently picks
CPython's libpython.

* Factor out setting function docstrings into a macro

* Try to narrow down skipped tests
2024-10-07 14:12:04 -07:00

266 lines
9.4 KiB
Python

from __future__ import annotations
import multiprocessing
import sys
import threading
import time
import pytest
import env
from pybind11_tests import gil_scoped as m
class ExtendedVirtClass(m.VirtClass):
def virtual_func(self):
pass
def pure_virtual_func(self):
pass
def test_callback_py_obj():
m.test_callback_py_obj(lambda: None)
def test_callback_std_func():
m.test_callback_std_func(lambda: None)
def test_callback_virtual_func():
extended = ExtendedVirtClass()
m.test_callback_virtual_func(extended)
def test_callback_pure_virtual_func():
extended = ExtendedVirtClass()
m.test_callback_pure_virtual_func(extended)
def test_cross_module_gil_released():
"""Makes sure that the GIL can be acquired by another module from a GIL-released state."""
m.test_cross_module_gil_released() # Should not raise a SIGSEGV
def test_cross_module_gil_acquired():
"""Makes sure that the GIL can be acquired by another module from a GIL-acquired state."""
m.test_cross_module_gil_acquired() # Should not raise a SIGSEGV
def test_cross_module_gil_inner_custom_released():
"""Makes sure that the GIL can be acquired/released by another module
from a GIL-released state using custom locking logic."""
m.test_cross_module_gil_inner_custom_released()
def test_cross_module_gil_inner_custom_acquired():
"""Makes sure that the GIL can be acquired/acquired by another module
from a GIL-acquired state using custom locking logic."""
m.test_cross_module_gil_inner_custom_acquired()
def test_cross_module_gil_inner_pybind11_released():
"""Makes sure that the GIL can be acquired/released by another module
from a GIL-released state using pybind11 locking logic."""
m.test_cross_module_gil_inner_pybind11_released()
def test_cross_module_gil_inner_pybind11_acquired():
"""Makes sure that the GIL can be acquired/acquired by another module
from a GIL-acquired state using pybind11 locking logic."""
m.test_cross_module_gil_inner_pybind11_acquired()
@pytest.mark.skipif(sys.platform.startswith("emscripten"), reason="Requires threads")
def test_cross_module_gil_nested_custom_released():
"""Makes sure that the GIL can be nested acquired/released by another module
from a GIL-released state using custom locking logic."""
m.test_cross_module_gil_nested_custom_released()
@pytest.mark.skipif(sys.platform.startswith("emscripten"), reason="Requires threads")
def test_cross_module_gil_nested_custom_acquired():
"""Makes sure that the GIL can be nested acquired/acquired by another module
from a GIL-acquired state using custom locking logic."""
m.test_cross_module_gil_nested_custom_acquired()
@pytest.mark.skipif(sys.platform.startswith("emscripten"), reason="Requires threads")
def test_cross_module_gil_nested_pybind11_released():
"""Makes sure that the GIL can be nested acquired/released by another module
from a GIL-released state using pybind11 locking logic."""
m.test_cross_module_gil_nested_pybind11_released()
@pytest.mark.skipif(sys.platform.startswith("emscripten"), reason="Requires threads")
def test_cross_module_gil_nested_pybind11_acquired():
"""Makes sure that the GIL can be nested acquired/acquired by another module
from a GIL-acquired state using pybind11 locking logic."""
m.test_cross_module_gil_nested_pybind11_acquired()
def test_release_acquire():
assert m.test_release_acquire(0xAB) == "171"
def test_nested_acquire():
assert m.test_nested_acquire(0xAB) == "171"
@pytest.mark.skipif(sys.platform.startswith("emscripten"), reason="Requires threads")
def test_multi_acquire_release_cross_module():
for bits in range(16 * 8):
internals_ids = m.test_multi_acquire_release_cross_module(bits)
assert len(internals_ids) == 2 if bits % 8 else 1
# Intentionally putting human review in the loop here, to guard against accidents.
VARS_BEFORE_ALL_BASIC_TESTS = dict(vars()) # Make a copy of the dict (critical).
ALL_BASIC_TESTS = (
test_callback_py_obj,
test_callback_std_func,
test_callback_virtual_func,
test_callback_pure_virtual_func,
test_cross_module_gil_released,
test_cross_module_gil_acquired,
test_cross_module_gil_inner_custom_released,
test_cross_module_gil_inner_custom_acquired,
test_cross_module_gil_inner_pybind11_released,
test_cross_module_gil_inner_pybind11_acquired,
test_cross_module_gil_nested_custom_released,
test_cross_module_gil_nested_custom_acquired,
test_cross_module_gil_nested_pybind11_released,
test_cross_module_gil_nested_pybind11_acquired,
test_release_acquire,
test_nested_acquire,
test_multi_acquire_release_cross_module,
)
def test_all_basic_tests_completeness():
num_found = 0
for key, value in VARS_BEFORE_ALL_BASIC_TESTS.items():
if not key.startswith("test_"):
continue
assert value in ALL_BASIC_TESTS
num_found += 1
assert len(ALL_BASIC_TESTS) == num_found
def _intentional_deadlock():
m.intentional_deadlock()
ALL_BASIC_TESTS_PLUS_INTENTIONAL_DEADLOCK = ALL_BASIC_TESTS + (_intentional_deadlock,)
def _run_in_process(target, *args, **kwargs):
test_fn = target if len(args) == 0 else args[0]
# Do not need to wait much, 10s should be more than enough.
timeout = 0.1 if test_fn is _intentional_deadlock else 10
process = multiprocessing.Process(target=target, args=args, kwargs=kwargs)
process.daemon = True
try:
t_start = time.time()
process.start()
if timeout >= 100: # For debugging.
print(
"\nprocess.pid STARTED", process.pid, (sys.argv, target, args, kwargs)
)
print(f"COPY-PASTE-THIS: gdb {sys.argv[0]} -p {process.pid}", flush=True)
process.join(timeout=timeout)
if timeout >= 100:
print("\nprocess.pid JOINED", process.pid, flush=True)
t_delta = time.time() - t_start
if process.exitcode == 66 and m.defined_THREAD_SANITIZER: # Issue #2754
# WOULD-BE-NICE-TO-HAVE: Check that the message below is actually in the output.
# Maybe this could work:
# https://gist.github.com/alexeygrigorev/01ce847f2e721b513b42ea4a6c96905e
pytest.skip(
"ThreadSanitizer: starting new threads after multi-threaded fork is not supported."
)
elif test_fn is _intentional_deadlock:
assert process.exitcode is None
return 0
if process.exitcode is None:
assert t_delta > 0.9 * timeout
msg = "DEADLOCK, most likely, exactly what this test is meant to detect."
if env.PYPY and env.WIN:
pytest.skip(msg)
raise RuntimeError(msg)
return process.exitcode
finally:
if process.is_alive():
process.terminate()
def _run_in_threads(test_fn, num_threads, parallel):
threads = []
for _ in range(num_threads):
thread = threading.Thread(target=test_fn)
thread.daemon = True
thread.start()
if parallel:
threads.append(thread)
else:
thread.join()
for thread in threads:
thread.join()
@pytest.mark.skipif(sys.platform.startswith("emscripten"), reason="Requires threads")
@pytest.mark.parametrize("test_fn", ALL_BASIC_TESTS_PLUS_INTENTIONAL_DEADLOCK)
@pytest.mark.skipif(
"env.GRAALPY",
reason="GraalPy transiently complains about unfinished threads at process exit",
)
def test_run_in_process_one_thread(test_fn):
"""Makes sure there is no GIL deadlock when running in a thread.
It runs in a separate process to be able to stop and assert if it deadlocks.
"""
assert _run_in_process(_run_in_threads, test_fn, num_threads=1, parallel=False) == 0
@pytest.mark.skipif(sys.platform.startswith("emscripten"), reason="Requires threads")
@pytest.mark.parametrize("test_fn", ALL_BASIC_TESTS_PLUS_INTENTIONAL_DEADLOCK)
@pytest.mark.skipif(
"env.GRAALPY",
reason="GraalPy transiently complains about unfinished threads at process exit",
)
def test_run_in_process_multiple_threads_parallel(test_fn):
"""Makes sure there is no GIL deadlock when running in a thread multiple times in parallel.
It runs in a separate process to be able to stop and assert if it deadlocks.
"""
assert _run_in_process(_run_in_threads, test_fn, num_threads=8, parallel=True) == 0
@pytest.mark.skipif(sys.platform.startswith("emscripten"), reason="Requires threads")
@pytest.mark.parametrize("test_fn", ALL_BASIC_TESTS_PLUS_INTENTIONAL_DEADLOCK)
@pytest.mark.skipif(
"env.GRAALPY",
reason="GraalPy transiently complains about unfinished threads at process exit",
)
def test_run_in_process_multiple_threads_sequential(test_fn):
"""Makes sure there is no GIL deadlock when running in a thread multiple times sequentially.
It runs in a separate process to be able to stop and assert if it deadlocks.
"""
assert _run_in_process(_run_in_threads, test_fn, num_threads=8, parallel=False) == 0
@pytest.mark.skipif(sys.platform.startswith("emscripten"), reason="Requires threads")
@pytest.mark.parametrize("test_fn", ALL_BASIC_TESTS_PLUS_INTENTIONAL_DEADLOCK)
@pytest.mark.skipif(
"env.GRAALPY",
reason="GraalPy transiently complains about unfinished threads at process exit",
)
def test_run_in_process_direct(test_fn):
"""Makes sure there is no GIL deadlock when using processes.
This test is for completion, but it was never an issue.
"""
assert _run_in_process(test_fn) == 0