Repository: scipy/scipy
File: scipy/_lib/tests/test_import_cycles.py (Python)

Code before:
import multiprocessing
import subprocess
import sys

import pytest

from .test_public_api import PUBLIC_MODULES

# Regression tests for gh-6793.
# Check that all modules are importable in a new Python process.
# This is not necessarily true if there are import cycles present.


def _check_single_module(module):
    pid = subprocess.Popen([sys.executable, '-c', f'import {module}'])
    assert pid.wait() == 0, f'Failed to import {module}'


@pytest.mark.fail_slow(40)
@pytest.mark.slow
def test_public_modules_importable_2():
    # Ensure we use max 6 processes, to limit peak resource usage (memory, file handles)
    # on resource-constrained systems (e.g., RISC-V - see gh-24163).
    with multiprocessing.Pool(processes=6) as pool:
        pool.map(_check_single_module, PUBLIC_MODULES)


Review comment (bug):
Sorry to bug you about this again, but I was thinking about this on a hike, and I realized that one of the reasons we get so little information about the Windows ARM failure is that the failure happens inside a process where faulthandler is not enabled. The test is not really intended to find segfaults, but if it's going to happen anyway it would be nice to get more debug info.

Could you enable faulthandler with the following?

```suggestion
    pid = subprocess.Popen([sys.executable, '-X', 'faulthandler', '-c', f'import {module}'])
    assert pid.wait() == 0, f'Failed to import {module}'
```

pytest would normally give us a C / Python stack trace, but not for subprocesses we've started ourselves. I experimented with this by intentionally creating a segfault with this code:

<details>

Note: the following code is AI generated.

```diff
diff --git a/scipy/_lib/_ccallback_c.pyx b/scipy/_lib/_ccallback_c.pyx
index 6c493dac7c..037d03538f 100644
--- a/scipy/_lib/_ccallback_c.pyx
+++ b/scipy/_lib/_ccallback_c.pyx
@@ -217,3 +217,16 @@ plus1bc_ctypes = ctypes.cast(<size_t>&plus1bc_cython, plus1bc_t)
 
 sine_t = ctypes.CFUNCTYPE(ctypes.c_double, ctypes.c_double, ctypes.c_void_p)
 sine_ctypes = ctypes.cast(<size_t>&sine, sine_t)
+
+import os
+
+def maybe_segfault():
+    cdef int* ptr
+    if os.environ.get('SEGFAULT'):
+        print("Segfault time\n" * 10)
+        ptr = NULL
+        ptr[0] = 42
+    else:
+        print("SEGFAULT env var not set, running safely")
+
+maybe_segfault()
```

</details>

I tested this change with the segfault program, and it is able to give us more specific debugging information about the segfault if faulthandler is enabled.

Code after:
import multiprocessing
import subprocess
import sys

import pytest

from .test_public_api import PUBLIC_MODULES

# Regression tests for gh-6793.
# Check that all modules are importable in a new Python process.
# This is not necessarily true if there are import cycles present.


def _check_single_module(module):
    pid = subprocess.Popen([sys.executable, '-X', 'faulthandler', '-c',
                            f'import {module}'])
    assert pid.wait() == 0, f'Failed to import {module}'


@pytest.mark.fail_slow(40)
@pytest.mark.slow
def test_public_modules_importable_2():
    # Ensure we use max 6 processes, to limit peak resource usage (memory, file handles)
    # on resource-constrained systems (e.g., RISC-V - see gh-24163).
    with multiprocessing.Pool(processes=6) as pool:
        pool.map(_check_single_module, PUBLIC_MODULES)