Skip to content

Commit 21b950e

Browse files
committed
CI: only run tests that might use threads for TSAN run
1 parent 9e4e413 commit 21b950e

File tree

8 files changed

+133
-128
lines changed

8 files changed

+133
-128
lines changed

.github/workflows/compiler_sanitizers.yml

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -114,9 +114,8 @@ jobs:
114114
python -m spin build -j2 -- -Db_sanitize=thread
115115
- name: Test
116116
run: |
117-
# pass -s to pytest to see TSAN errors and warnings, otherwise pytest captures them
117+
# These tests are slow, so only run tests in files that do "import threading" to make them count
118118
TSAN_OPTIONS=allocator_may_return_null=1:halt_on_error=1 \
119-
python -m spin test -- -v -s --timeout=600 --durations=10
120-
- name: Setup tmate session
121-
if: ${{ failure() }}
122-
uses: mxschmitt/action-tmate@v3
119+
python -m spin test \
120+
`find numpy -name "test*.py" | xargs grep -l "import threading" | tr '\n' ' '` \
121+
-- -v -s --timeout=600 --durations=10

numpy/_core/tests/test_indexing.py

Lines changed: 0 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -590,32 +590,6 @@ def test_too_many_advanced_indices(self, index, num, original_ndim):
590590
with pytest.raises(IndexError):
591591
arr[(index,) * num] = 1.
592592

593-
@pytest.mark.skipif(IS_WASM, reason="no threading")
594-
def test_structured_advanced_indexing(self):
595-
# Test that copyswap(n) used by integer array indexing is threadsafe
596-
# for structured datatypes, see gh-15387. This test can behave randomly.
597-
from concurrent.futures import ThreadPoolExecutor
598-
599-
# Create a deeply nested dtype to make a failure more likely:
600-
dt = np.dtype([("", "f8")])
601-
dt = np.dtype([("", dt)] * 2)
602-
dt = np.dtype([("", dt)] * 2)
603-
# The array should be large enough to likely run into threading issues
604-
arr = np.random.uniform(size=(6000, 8)).view(dt)[:, 0]
605-
606-
rng = np.random.default_rng()
607-
608-
def func(arr):
609-
indx = rng.integers(0, len(arr), size=6000, dtype=np.intp)
610-
arr[indx]
611-
612-
tpe = ThreadPoolExecutor(max_workers=8)
613-
futures = [tpe.submit(func, arr) for _ in range(10)]
614-
for f in futures:
615-
f.result()
616-
617-
assert arr.dtype is dt
618-
619593
def test_nontuple_ndindex(self):
620594
a = np.arange(25).reshape((5, 5))
621595
assert_equal(a[[0, 1]], np.array([a[0], a[1]]))

numpy/_core/tests/test_multithreading.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
import concurrent.futures
12
import threading
3+
import string
24

35
import numpy as np
46
import pytest
@@ -165,3 +167,90 @@ def closure(b):
165167
x = np.repeat(x0, 2, axis=0)[::2]
166168

167169
run_threaded(closure, max_workers=10, pass_barrier=True)
170+
171+
172+
def test_structured_advanced_indexing():
173+
# Test that copyswap(n) used by integer array indexing is threadsafe
174+
# for structured datatypes, see gh-15387. This test can behave randomly.
175+
176+
# Create a deeply nested dtype to make a failure more likely:
177+
dt = np.dtype([("", "f8")])
178+
dt = np.dtype([("", dt)] * 2)
179+
dt = np.dtype([("", dt)] * 2)
180+
# The array should be large enough to likely run into threading issues
181+
arr = np.random.uniform(size=(6000, 8)).view(dt)[:, 0]
182+
183+
rng = np.random.default_rng()
184+
185+
def func(arr):
186+
indx = rng.integers(0, len(arr), size=6000, dtype=np.intp)
187+
arr[indx]
188+
189+
tpe = concurrent.futures.ThreadPoolExecutor(max_workers=8)
190+
futures = [tpe.submit(func, arr) for _ in range(10)]
191+
for f in futures:
192+
f.result()
193+
194+
assert arr.dtype is dt
195+
196+
197+
def test_structured_threadsafety2():
198+
# Nonzero (and some other functions) should be threadsafe for
199+
# structured datatypes, see gh-15387. This test can behave randomly.
200+
from concurrent.futures import ThreadPoolExecutor
201+
202+
# Create a deeply nested dtype to make a failure more likely:
203+
dt = np.dtype([("", "f8")])
204+
dt = np.dtype([("", dt)])
205+
dt = np.dtype([("", dt)] * 2)
206+
# The array should be large enough to likely run into threading issues
207+
arr = np.random.uniform(size=(5000, 4)).view(dt)[:, 0]
208+
209+
def func(arr):
210+
arr.nonzero()
211+
212+
tpe = ThreadPoolExecutor(max_workers=8)
213+
futures = [tpe.submit(func, arr) for _ in range(10)]
214+
for f in futures:
215+
f.result()
216+
217+
assert arr.dtype is dt
218+
219+
220+
def test_stringdtype_multithreaded_access_and_mutation(
221+
dtype, random_string_list):
222+
# this test uses an RNG and may crash or cause deadlocks if there is a
223+
# threading bug
224+
rng = np.random.default_rng(0x4D3D3D3)
225+
226+
chars = list(string.ascii_letters + string.digits)
227+
chars = np.array(chars, dtype="U1")
228+
ret = rng.choice(chars, size=100 * 10, replace=True)
229+
random_string_list = ret.view("U100")
230+
231+
def func(arr):
232+
rnd = rng.random()
233+
# either write to random locations in the array, compute a ufunc, or
234+
# re-initialize the array
235+
if rnd < 0.25:
236+
num = np.random.randint(0, arr.size)
237+
arr[num] = arr[num] + "hello"
238+
elif rnd < 0.5:
239+
if rnd < 0.375:
240+
np.add(arr, arr)
241+
else:
242+
np.add(arr, arr, out=arr)
243+
elif rnd < 0.75:
244+
if rnd < 0.875:
245+
np.multiply(arr, np.int64(2))
246+
else:
247+
np.multiply(arr, np.int64(2), out=arr)
248+
else:
249+
arr[:] = random_string_list
250+
251+
with concurrent.futures.ThreadPoolExecutor(max_workers=8) as tpe:
252+
arr = np.array(random_string_list, dtype=dtype)
253+
futures = [tpe.submit(func, arr) for _ in range(500)]
254+
255+
for f in futures:
256+
f.result()

numpy/_core/tests/test_nep50_promotions.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@
55
"""
66

77
import operator
8-
import threading
9-
import warnings
108

119
import numpy as np
1210

numpy/_core/tests/test_numeric.py

Lines changed: 0 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1956,29 +1956,6 @@ def __bool__(self):
19561956
a = np.array([[ThrowsAfter(15)]] * 10)
19571957
assert_raises(ValueError, np.nonzero, a)
19581958

1959-
@pytest.mark.skipif(IS_WASM, reason="wasm doesn't have threads")
1960-
def test_structured_threadsafety(self):
1961-
# Nonzero (and some other functions) should be threadsafe for
1962-
# structured datatypes, see gh-15387. This test can behave randomly.
1963-
from concurrent.futures import ThreadPoolExecutor
1964-
1965-
# Create a deeply nested dtype to make a failure more likely:
1966-
dt = np.dtype([("", "f8")])
1967-
dt = np.dtype([("", dt)])
1968-
dt = np.dtype([("", dt)] * 2)
1969-
# The array should be large enough to likely run into threading issues
1970-
arr = np.random.uniform(size=(5000, 4)).view(dt)[:, 0]
1971-
1972-
def func(arr):
1973-
arr.nonzero()
1974-
1975-
tpe = ThreadPoolExecutor(max_workers=8)
1976-
futures = [tpe.submit(func, arr) for _ in range(10)]
1977-
for f in futures:
1978-
f.result()
1979-
1980-
assert arr.dtype is dt
1981-
19821959

19831960
class TestIndex:
19841961
def test_boolean(self):

numpy/_core/tests/test_stringdtype.py

Lines changed: 2 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import concurrent.futures
21
import itertools
32
import os
43
import pickle
@@ -11,48 +10,15 @@
1110

1211
from numpy.dtypes import StringDType
1312
from numpy._core.tests._natype import pd_NA
14-
from numpy.testing import assert_array_equal, IS_WASM, IS_PYPY
13+
from numpy.testing import assert_array_equal, IS_PYPY
14+
from numpy.testing._private.utils import get_stringdtype_dtype as get_dtype
1515

1616

1717
@pytest.fixture
1818
def string_list():
1919
return ["abc", "def", "ghi" * 10, "A¢☃€ 😊" * 100, "Abc" * 1000, "DEF"]
2020

2121

22-
@pytest.fixture
23-
def random_string_list():
24-
chars = list(string.ascii_letters + string.digits)
25-
chars = np.array(chars, dtype="U1")
26-
ret = np.random.choice(chars, size=100 * 10, replace=True)
27-
return ret.view("U100")
28-
29-
30-
@pytest.fixture(params=[True, False])
31-
def coerce(request):
32-
return request.param
33-
34-
35-
@pytest.fixture(
36-
params=["unset", None, pd_NA, np.nan, float("nan"), "__nan__"],
37-
ids=["unset", "None", "pandas.NA", "np.nan", "float('nan')", "string nan"],
38-
)
39-
def na_object(request):
40-
return request.param
41-
42-
43-
def get_dtype(na_object, coerce=True):
44-
# explicit is check for pd_NA because != with pd_NA returns pd_NA
45-
if na_object is pd_NA or na_object != "unset":
46-
return StringDType(na_object=na_object, coerce=coerce)
47-
else:
48-
return StringDType(coerce=coerce)
49-
50-
51-
@pytest.fixture()
52-
def dtype(na_object, coerce):
53-
return get_dtype(na_object, coerce)
54-
55-
5622
# second copy for cast tests to do a cartesian product over dtypes
5723
@pytest.fixture(params=[True, False])
5824
def coerce2(request):
@@ -1208,40 +1174,6 @@ def test_growing_strings(dtype):
12081174
assert_array_equal(arr, uarr)
12091175

12101176

1211-
@pytest.mark.skipif(IS_WASM, reason="no threading support in wasm")
1212-
def test_threaded_access_and_mutation(dtype, random_string_list):
1213-
# this test uses an RNG and may crash or cause deadlocks if there is a
1214-
# threading bug
1215-
rng = np.random.default_rng(0x4D3D3D3)
1216-
1217-
def func(arr):
1218-
rnd = rng.random()
1219-
# either write to random locations in the array, compute a ufunc, or
1220-
# re-initialize the array
1221-
if rnd < 0.25:
1222-
num = np.random.randint(0, arr.size)
1223-
arr[num] = arr[num] + "hello"
1224-
elif rnd < 0.5:
1225-
if rnd < 0.375:
1226-
np.add(arr, arr)
1227-
else:
1228-
np.add(arr, arr, out=arr)
1229-
elif rnd < 0.75:
1230-
if rnd < 0.875:
1231-
np.multiply(arr, np.int64(2))
1232-
else:
1233-
np.multiply(arr, np.int64(2), out=arr)
1234-
else:
1235-
arr[:] = random_string_list
1236-
1237-
with concurrent.futures.ThreadPoolExecutor(max_workers=8) as tpe:
1238-
arr = np.array(random_string_list, dtype=dtype)
1239-
futures = [tpe.submit(func, arr) for _ in range(500)]
1240-
1241-
for f in futures:
1242-
f.result()
1243-
1244-
12451177
UFUNC_TEST_DATA = [
12461178
"hello" * 10,
12471179
"Ae¢☃€ 😊" * 20,

numpy/conftest.py

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
Pytest configuration and fixtures for the Numpy test suite.
33
"""
44
import os
5+
import string
56
import sys
67
import tempfile
78
from contextlib import contextmanager
@@ -10,9 +11,11 @@
1011
import hypothesis
1112
import pytest
1213
import numpy
14+
import numpy as np
1315

1416
from numpy._core._multiarray_tests import get_fpu_mode
15-
from numpy.testing._private.utils import NOGIL_BUILD
17+
from numpy._core.tests._natype import pd_NA
18+
from numpy.testing._private.utils import NOGIL_BUILD, get_stringdtype_dtype
1619

1720
try:
1821
from scipy_doctest.conftest import dt_config
@@ -231,3 +234,28 @@ def warnings_errors_and_rng(test=None):
231234
'numpy/f2py/_backends/_distutils.py',
232235
]
233236

237+
238+
@pytest.fixture
239+
def random_string_list():
240+
chars = list(string.ascii_letters + string.digits)
241+
chars = np.array(chars, dtype="U1")
242+
ret = np.random.choice(chars, size=100 * 10, replace=True)
243+
return ret.view("U100")
244+
245+
246+
@pytest.fixture(params=[True, False])
247+
def coerce(request):
248+
return request.param
249+
250+
251+
@pytest.fixture(
252+
params=["unset", None, pd_NA, np.nan, float("nan"), "__nan__"],
253+
ids=["unset", "None", "pandas.NA", "np.nan", "float('nan')", "string nan"],
254+
)
255+
def na_object(request):
256+
return request.param
257+
258+
259+
@pytest.fixture()
260+
def dtype(na_object, coerce):
261+
return get_stringdtype_dtype(na_object, coerce)

numpy/testing/_private/utils.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
intp, float32, empty, arange, array_repr, ndarray, isnat, array)
2626
from numpy import isfinite, isnan, isinf
2727
import numpy.linalg._umath_linalg
28-
from numpy._utils import _rename_parameter
28+
from numpy._core.tests._natype import pd_NA
2929

3030
from io import StringIO
3131

@@ -2706,3 +2706,11 @@ def run_threaded(func, max_workers=8, pass_count=False,
27062706
futures = [tpe.submit(func, *args) for _ in range(max_workers)]
27072707
for f in futures:
27082708
f.result()
2709+
2710+
2711+
def get_stringdtype_dtype(na_object, coerce=True):
2712+
# explicit is check for pd_NA because != with pd_NA returns pd_NA
2713+
if na_object is pd_NA or na_object != "unset":
2714+
return np.dtypes.StringDType(na_object=na_object, coerce=coerce)
2715+
else:
2716+
return np.dtypes.StringDType(coerce=coerce)

0 commit comments

Comments
 (0)