250 lines
		
	
	
		
			8.9 KiB
		
	
	
	
		
			Python
		
	
	
		
		
			
		
	
	
			250 lines
		
	
	
		
			8.9 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) | ||
|  | 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) | ||
|  | 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) | ||
|  | 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) | ||
|  | 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 |