blob: d688f9ee91f28c0bd1a901aedb861472702defd1 [file]
from __future__ import annotations
import os
import subprocess
import sys
import textwrap
import threading
import pytest
import env
import pybind11_tests
from pybind11_tests import thread as m
class Thread(threading.Thread):
def __init__(self, fn):
super().__init__()
self.fn = fn
self.e = None
def run(self):
try:
for i in range(10):
self.fn(i, i)
except Exception as e:
self.e = e
def join(self):
super().join()
if self.e:
raise self.e
@pytest.mark.skipif(sys.platform.startswith("emscripten"), reason="Requires threads")
def test_implicit_conversion():
a = Thread(m.test)
b = Thread(m.test)
c = Thread(m.test)
for x in [a, b, c]:
x.start()
for x in [c, b, a]:
x.join()
@pytest.mark.skipif(sys.platform.startswith("emscripten"), reason="Requires threads")
def test_implicit_conversion_no_gil():
a = Thread(m.test_no_gil)
b = Thread(m.test_no_gil)
c = Thread(m.test_no_gil)
for x in [a, b, c]:
x.start()
for x in [c, b, a]:
x.join()
@pytest.mark.skipif(sys.platform.startswith("emscripten"), reason="Requires threads")
def test_bind_shared_instance():
nb_threads = 4
b = threading.Barrier(nb_threads)
def access_shared_instance():
b.wait()
for _ in range(1000):
m.EmptyStruct.SharedInstance # noqa: B018
threads = [
threading.Thread(target=access_shared_instance) for _ in range(nb_threads)
]
for thread in threads:
thread.start()
for thread in threads:
thread.join()
@pytest.mark.skipif(sys.platform.startswith("emscripten"), reason="Requires threads")
@pytest.mark.skipif(not m.defined_PYBIND11_HAS_STD_BARRIER, reason="no <barrier>")
@pytest.mark.skipif(env.sys_is_gil_enabled(), reason="Deadlock with the GIL")
def test_pythread_state_clear_destructor():
class Foo:
def __del__(self):
m.acquire_gil()
m.test_pythread_state_clear_destructor(Foo)
@pytest.mark.skipif(sys.platform.startswith("emscripten"), reason="Requires threads")
@pytest.mark.skipif(env.ANDROID or env.IOS, reason="Requires subprocess support")
@pytest.mark.skipif(
not env.PY_GIL_DISABLED, reason="The internals lock is a no-op with the GIL"
)
def test_dispatch_does_not_need_internals_lock():
"""A bound call must not block on the internals mutex while another thread holds it.
Regression test for #6159 (item 59): cpp_function::dispatcher() used to reach its
function_record through get_function_record_PyTypeObject(), which takes the internals lock
on every call. Runs in a subprocess because on a regression the call under test blocks
until the holder's watchdog fires.
"""
script = textwrap.dedent(
f"""
import sys
sys.path.insert(0, {os.path.dirname(env.__file__)!r})
sys.path.insert(0, {os.path.dirname(pybind11_tests.__file__)!r})
from pybind11_tests import thread as m
m.dispatch_noop() # Warm up any one-time initialization.
# Positive control: the holder holds the very mutex that internals users take, so a
# call that needs it can only return after the holder's watchdog fires.
m.start_internals_lock_holder(1.0)
m.take_internals_lock()
assert m.release_internals_lock_holder(), "take_internals_lock() did not block"
# The call under test: a plain bound call must not need the internals lock at all.
m.start_internals_lock_holder(5.0)
m.dispatch_noop()
released_by_watchdog = m.release_internals_lock_holder()
assert not released_by_watchdog, "cpp_function::dispatcher() needed the internals lock"
"""
)
try:
proc = subprocess.run(
[sys.executable, "-c", script],
capture_output=True,
text=True,
timeout=60,
check=False,
)
except subprocess.TimeoutExpired as ex:
pytest.fail(
f"Subprocess did not finish within {ex.timeout} s (deadlock?).\n"
f"Output:\n{ex.stdout}\n{ex.stderr}"
)
assert proc.returncode == 0, (
f"Subprocess failed with exit code {proc.returncode}.\n"
f"Output:\n{proc.stdout}\n{proc.stderr}"
)