Repository: scipy/scipy

Issue:
tests with `multiprocessing` hang on Python 3.8 on macOS
I'm running the whole test suite on macOS on Python 3.8 (via Github Actions on my local machine (Catalina)). I'm seeing several issues with multiprocessing, such as #11827.

They're related to https://bugs.python.org/issue38501. The tests that hang are:

- [`integrate.tests.test__quad_vec.test_quad_vec_pool`](https://github.com/scipy/scipy/blob/master/scipy/integrate/tests/test__quad_vec.py#L94)

I think the best thing to do at the moment is to use the skipif decorator (possibly also coupled to Darwin):

```
    @pytest.mark.skipif(multiprocessing.get_start_method() != 'fork',
                        reason=('multiprocessing with spawn method is not'
                                ' compatible with pytest.'))
```


Discussion:

Resolving patch:
diff --git a/runtests.py b/runtests.py
--- a/runtests.py
+++ b/runtests.py
@@ -49,6 +49,11 @@
 
 import sys
 import os
+# the following multiprocessing import is necessary to prevent tests that use
+# multiprocessing from hanging on >= Python3.8 (macOS) using pytest. Just the
+# import is enough...
+import multiprocessing
+
 
 # In case we are run from the source directory, we don't want to import the
 # project from there:


Test patch:
diff --git a/scipy/_lib/tests/test__util.py b/scipy/_lib/tests/test__util.py
--- a/scipy/_lib/tests/test__util.py
+++ b/scipy/_lib/tests/test__util.py
@@ -1,6 +1,7 @@
 from multiprocessing import Pool, get_start_method
 from multiprocessing.pool import Pool as PWL
 import os
+import math
 
 import numpy as np
 from numpy.testing import assert_equal, assert_
@@ -97,9 +98,11 @@ def test_mapwrapper_serial():
         p = MapWrapper(0)
 
 
-@pytest.mark.skipif(get_start_method() != 'fork',
-                    reason=('multiprocessing with spawn method is not'
-                            ' compatible with pytest.'))
+def test_pool():
+    with Pool(2) as p:
+        p.map(math.sin, [1,2,3, 4])
+
+
 def test_mapwrapper_parallel():
     in_arg = np.arange(10.)
     out_arg = np.sin(in_arg)
diff --git a/scipy/fft/tests/test_multithreading.py b/scipy/fft/tests/test_multithreading.py
--- a/scipy/fft/tests/test_multithreading.py
+++ b/scipy/fft/tests/test_multithreading.py
@@ -32,16 +32,9 @@ def _mt_fft(x):
 def test_mixed_threads_processes(x):
     # Test that the fft threadpool is safe to use before & after fork
 
-    # Must use fork to work around python 3.8 bug
-    # https://bugs.python.org/issue38501
-    try:
-        mp = multiprocessing.get_context('fork')
-    except ValueError:
-        pytest.skip('fork method not available')
-
     expect = fft.fft(x, workers=2)
 
-    with mp.Pool(2) as p:
+    with multiprocessing.Pool(2) as p:
         res = p.map(_mt_fft, [x for _ in range(4)])
 
     for r in res:
diff --git a/scipy/fft/tests/test_numpy.py b/scipy/fft/tests/test_numpy.py
--- a/scipy/fft/tests/test_numpy.py
+++ b/scipy/fft/tests/test_numpy.py
@@ -304,12 +304,8 @@ def test_ihfft(self):
 @pytest.mark.parametrize("func", [fft.fft, fft.ifft, fft.rfft, fft.irfft])
 def test_multiprocess(func):
     # Test that fft still works after fork (gh-10422)
-    try:
-        mp = multiprocessing.get_context('fork')
-    except ValueError:
-        pytest.skip('fork method not available')
 
-    with mp.Pool(2) as p:
+    with multiprocessing.Pool(2) as p:
         res = p.map(func, [np.ones(100) for _ in range(4)])
 
     expect = func(np.ones(100))
diff --git a/scipy/integrate/tests/test__quad_vec.py b/scipy/integrate/tests/test__quad_vec.py
--- a/scipy/integrate/tests/test__quad_vec.py
+++ b/scipy/integrate/tests/test__quad_vec.py
@@ -1,3 +1,4 @@
+import sys
 import pytest
 
 import numpy as np
diff --git a/scipy/optimize/tests/test__differential_evolution.py b/scipy/optimize/tests/test__differential_evolution.py
--- a/scipy/optimize/tests/test__differential_evolution.py
+++ b/scipy/optimize/tests/test__differential_evolution.py
@@ -1,7 +1,6 @@
 """
 Unit tests for the differential global minimization algorithm.
 """
-import gc
 import multiprocessing
 import sys
 import platform
@@ -23,11 +22,6 @@
 import pytest
 
 
-knownfail_on_py38 = pytest.mark.xfail(
-    sys.version_info >= (3, 8), run=False,
-    reason='Python 3.8 hangs when cleaning up MapWrapper')
-
-
 class TestDifferentialEvolutionSolver(object):
 
     def setup_method(self):
@@ -561,7 +555,6 @@ def test_deferred_updating(self):
         assert_(solver._mapwrapper._mapfunc is map)
         solver.solve()
 
-    @knownfail_on_py38
     def test_immediate_updating(self):
         # check setting of immediate updating, with default workers
         bounds = [(0., 2.), (0., 2.)]
@@ -571,12 +564,10 @@ def test_immediate_updating(self):
         # should raise a UserWarning because the updating='immediate'
         # is being overridden by the workers keyword
         with warns(UserWarning):
-            solver = DifferentialEvolutionSolver(rosen, bounds, workers=2)
+            with DifferentialEvolutionSolver(rosen, bounds, workers=2) as solver:
+                pass
         assert_(solver._updating == 'deferred')
-        del solver
-        gc.collect()  # ensure MapWrapper cleans up properly
 
-    @knownfail_on_py38
     def test_parallel(self):
         # smoke test for parallelization with deferred updating
         bounds = [(0., 2.), (0., 2.)]
@@ -591,8 +582,6 @@ def test_parallel(self):
             assert_(solver._mapwrapper.pool is not None)
             assert_(solver._updating == 'deferred')
             solver.solve()
-        del solver
-        gc.collect()  # ensure MapWrapper cleans up properly
 
     def test_converged(self):
         solver = DifferentialEvolutionSolver(rosen, [(0, 2), (0, 2)])
diff --git a/scipy/optimize/tests/test_optimize.py b/scipy/optimize/tests/test_optimize.py
--- a/scipy/optimize/tests/test_optimize.py
+++ b/scipy/optimize/tests/test_optimize.py
@@ -1565,9 +1565,6 @@ def f(x):
 
         optimize.brute(f, [(-1, 1)], Ns=3, finish=None)
 
-    @pytest.mark.skipif(multiprocessing.get_start_method() != 'fork',
-                        reason=('multiprocessing with spawn method is not'
-                                ' compatible with pytest.'))
     def test_workers(self):
         # check that parallel evaluation works
         resbrute = optimize.brute(brute_func, self.rranges, args=self.params,
diff --git a/scipy/stats/tests/test_stats.py b/scipy/stats/tests/test_stats.py
--- a/scipy/stats/tests/test_stats.py
+++ b/scipy/stats/tests/test_stats.py
@@ -5244,9 +5244,6 @@ def test_twosamp(self):
         assert_approx_equal(stat, 1.0, significant=1)
         assert_approx_equal(pvalue, 0.001, significant=1)
 
-    @pytest.mark.skipif(multiprocessing.get_start_method() != 'fork',
-                        reason=('multiprocessing with spawn method is not'
-                                ' compatible with pytest.'))
     def test_workers(self):
         np.random.seed(12345678)