Skip to content

Commit ea8b132

Browse files
committed
More sophisiticated _run_in_process() implementation, clearly reporting DEADLOCK, additionally exercised via added intentional_deadlock()
1 parent 10b0334 commit ea8b132

File tree

2 files changed

+38
-38
lines changed

2 files changed

+38
-38
lines changed

tests/test_gil_scoped.cpp

+3
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ TEST_SUBMODULE(gil_scoped, m) {
4242
false;
4343
#endif
4444

45+
m.def("intentional_deadlock",
46+
[]() { std::thread([]() { py::gil_scoped_acquire gil_acquired; }).join(); });
47+
4548
py::class_<VirtClass, PyVirtClass>(m, "VirtClass")
4649
.def(py::init<>())
4750
.def("virtual_func", &VirtClass::virtual_func)

tests/test_gil_scoped.py

+35-38
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
import pytest
66

7-
import env
87
from pybind11_tests import gil_scoped as m
98

109

@@ -139,36 +138,50 @@ def test_all_basic_tests_completeness():
139138
assert len(ALL_BASIC_TESTS) == num_found
140139

141140

142-
# Issue #2754:
143-
ThreadSanitizer_exitcode_66_message = (
144-
"ThreadSanitizer: starting new threads after multi-threaded fork is not supported."
145-
)
141+
ALL_BASIC_TESTS_PLUS_INTENTIONAL_DEADLOCK = ALL_BASIC_TESTS + (m.intentional_deadlock,)
146142

147143

148144
def _run_in_process(target, *args, **kwargs):
149-
"""Runs target in process and returns its exitcode after 10s (None if still alive)."""
145+
if len(args) == 0:
146+
test_fn = target
147+
else:
148+
test_fn = args[0]
149+
# Do not need to wait much, 10s should be more than enough.
150+
timeout = 0.1 if test_fn is m.intentional_deadlock else 10
150151
process = multiprocessing.Process(target=target, args=args, kwargs=kwargs)
151152
process.daemon = True
152153
try:
153154
t_start = time.time()
154155
process.start()
155-
# Do not need to wait much, 10s should be more than enough.
156-
process.join(timeout=10)
156+
if timeout >= 100: # For debugging.
157+
print("\nprocess.pid STARTED", process.pid, flush=True)
158+
process.join(timeout=timeout)
159+
if timeout >= 100:
160+
print("\nprocess.pid JOINED", process.pid, flush=True)
157161
t_delta = time.time() - t_start
158-
if process.exitcode is None:
159-
assert t_delta > 9.9
160-
if process.exitcode == 66:
161-
pass # NICE-TO-HAVE: Check output for ThreadSanitizer_exitcode_66_message
162+
if process.exitcode == 66 and m.defined_THREAD_SANITIZER: # Issue #2754
163+
# WOULD-BE-NICE-TO-HAVE: Check that the message below is actually in the output.
164+
pytest.skip(
165+
"ThreadSanitizer: starting new threads after multi-threaded fork is not supported."
166+
)
167+
elif test_fn is m.intentional_deadlock:
168+
assert process.exitcode is None
169+
return 0
170+
elif process.exitcode is None:
171+
assert t_delta > 0.9 * timeout
172+
raise RuntimeError(
173+
"DEADLOCK, most likely, exactly what this test is meant to detect."
174+
)
162175
return process.exitcode
163176
finally:
164177
if process.is_alive():
165178
process.terminate()
166179

167180

168-
def _run_in_threads(target, num_threads, parallel):
181+
def _run_in_threads(test_fn, num_threads, parallel):
169182
threads = []
170183
for _ in range(num_threads):
171-
thread = threading.Thread(target=target)
184+
thread = threading.Thread(target=test_fn)
172185
thread.daemon = True
173186
thread.start()
174187
if parallel:
@@ -180,56 +193,40 @@ def _run_in_threads(target, num_threads, parallel):
180193

181194

182195
# TODO: FIXME, sometimes returns -11 (segfault) instead of 0 on macOS Python 3.9
183-
@pytest.mark.parametrize("test_fn", ALL_BASIC_TESTS)
196+
@pytest.mark.parametrize("test_fn", ALL_BASIC_TESTS_PLUS_INTENTIONAL_DEADLOCK)
184197
def test_run_in_process_one_thread(test_fn):
185198
"""Makes sure there is no GIL deadlock when running in a thread.
186199
187200
It runs in a separate process to be able to stop and assert if it deadlocks.
188201
"""
189-
exitcode = _run_in_process(_run_in_threads, test_fn, num_threads=1, parallel=False)
190-
if exitcode == 66 and m.defined_THREAD_SANITIZER:
191-
pytest.skip(ThreadSanitizer_exitcode_66_message)
192-
assert exitcode == 0
202+
assert _run_in_process(_run_in_threads, test_fn, num_threads=1, parallel=False) == 0
193203

194204

195205
# TODO: FIXME on macOS Python 3.9
196-
@pytest.mark.parametrize("test_fn", ALL_BASIC_TESTS)
206+
@pytest.mark.parametrize("test_fn", ALL_BASIC_TESTS_PLUS_INTENTIONAL_DEADLOCK)
197207
def test_run_in_process_multiple_threads_parallel(test_fn):
198208
"""Makes sure there is no GIL deadlock when running in a thread multiple times in parallel.
199209
200210
It runs in a separate process to be able to stop and assert if it deadlocks.
201211
"""
202-
exitcode = _run_in_process(_run_in_threads, test_fn, num_threads=8, parallel=True)
203-
if exitcode is None and env.PYPY and env.WIN: # Seems to be flaky.
204-
pytest.skip("Ignoring unexpected exitcode None (PYPY WIN)")
205-
if exitcode == 66 and m.defined_THREAD_SANITIZER:
206-
pytest.skip(ThreadSanitizer_exitcode_66_message)
207-
assert exitcode == 0
212+
assert _run_in_process(_run_in_threads, test_fn, num_threads=8, parallel=True) == 0
208213

209214

210215
# TODO: FIXME on macOS Python 3.9
211-
@pytest.mark.parametrize("test_fn", ALL_BASIC_TESTS)
216+
@pytest.mark.parametrize("test_fn", ALL_BASIC_TESTS_PLUS_INTENTIONAL_DEADLOCK)
212217
def test_run_in_process_multiple_threads_sequential(test_fn):
213218
"""Makes sure there is no GIL deadlock when running in a thread multiple times sequentially.
214219
215220
It runs in a separate process to be able to stop and assert if it deadlocks.
216221
"""
217-
exitcode = _run_in_process(_run_in_threads, test_fn, num_threads=8, parallel=False)
218-
if exitcode == 66 and m.defined_THREAD_SANITIZER:
219-
pytest.skip(ThreadSanitizer_exitcode_66_message)
220-
assert exitcode == 0
222+
assert _run_in_process(_run_in_threads, test_fn, num_threads=8, parallel=False) == 0
221223

222224

223225
# TODO: FIXME on macOS Python 3.9
224-
@pytest.mark.parametrize("test_fn", ALL_BASIC_TESTS)
226+
@pytest.mark.parametrize("test_fn", ALL_BASIC_TESTS_PLUS_INTENTIONAL_DEADLOCK)
225227
def test_run_in_process_direct(test_fn):
226228
"""Makes sure there is no GIL deadlock when using processes.
227229
228230
This test is for completion, but it was never an issue.
229231
"""
230-
exitcode = _run_in_process(test_fn)
231-
if exitcode is None and env.PYPY and env.WIN: # Seems to be flaky.
232-
pytest.skip("Ignoring unexpected exitcode None (PYPY WIN)")
233-
if exitcode == 66 and m.defined_THREAD_SANITIZER:
234-
pytest.skip(ThreadSanitizer_exitcode_66_message)
235-
assert exitcode == 0
232+
assert _run_in_process(test_fn) == 0

0 commit comments

Comments
 (0)