summaryrefslogtreecommitdiff
path: root/numpy/core
diff options
context:
space:
mode:
authorSebastian Berg <sebastian@sipsolutions.net>2020-08-11 18:22:19 -0500
committerSebastian Berg <sebastian@sipsolutions.net>2020-08-11 18:54:53 -0500
commit101192cbb5eedd8da97237a73ef2754fb9c65ddf (patch)
tree4d6afc9e61e8791987e50848fddf94033987ca40 /numpy/core
parent37dc69cd01934b78bbf9bcccab5fbc4a7ff5eacb (diff)
downloadnumpy-101192cbb5eedd8da97237a73ef2754fb9c65ddf.tar.gz
TST: Add tests for correct nditer buffer clearing on cast error
Diffstat (limited to 'numpy/core')
-rw-r--r--numpy/core/tests/test_nditer.py65
-rw-r--r--numpy/core/tests/test_ufunc.py60
2 files changed, 124 insertions, 1 deletions
diff --git a/numpy/core/tests/test_nditer.py b/numpy/core/tests/test_nditer.py
index 7b3c3a40d..e10c7ad92 100644
--- a/numpy/core/tests/test_nditer.py
+++ b/numpy/core/tests/test_nditer.py
@@ -2880,3 +2880,68 @@ def test_warn_noclose():
casting='equiv', op_dtypes=[np.dtype('f4')])
del it
assert len(sup.log) == 1
+
+
+@pytest.mark.skipif(not HAS_REFCOUNT, reason="Python lacks refcounts")
+@pytest.mark.parametrize(["in_dtype", "buf_dtype"],
+ [("i", "O"), ("O", "i"), # most simple cases
+ ("i,O", "O,O"), # structured partially only copying O
+ ("O,i", "i,O"), # structured casting to and from O
+ ])
+@pytest.mark.parametrize("steps", [1, 2, 3])
+def test_partial_iteration_cleanup(in_dtype, buf_dtype, steps):
+ value = 123 # relies on python cache (leak-check will still find it)
+ arr = np.full(int(np.BUFSIZE * 2.5), value).astype(in_dtype)
+ count = sys.getrefcount(value)
+
+ it = np.nditer(arr, op_dtypes=[np.dtype(buf_dtype)],
+ flags=["buffered", "external_loop", "refs_ok"], casting="unsafe")
+ for step in range(steps):
+ # The iteration finishes in 3 steps, the first two are partial
+ next(it)
+
+ # Note that resetting does not free references
+ del it
+ assert count == sys.getrefcount(value)
+
+ # Repeat the test with `iternext`
+ it = np.nditer(arr, op_dtypes=[np.dtype(buf_dtype)],
+ flags=["buffered", "external_loop", "refs_ok"], casting="unsafe")
+ for step in range(steps):
+ it.iternext()
+
+ del it # should ensure cleanup
+ assert count == sys.getrefcount(value)
+
+
+@pytest.mark.skipif(not HAS_REFCOUNT, reason="Python lacks refcounts")
+@pytest.mark.parametrize(["in_dtype", "buf_dtype"],
+ [("O", "i"), # most simple cases
+ ("O,i", "i,O"), # structured casting to and from O
+ ])
+def test_partial_iteration_error(in_dtype, buf_dtype):
+ value = 123 # relies on python cache (leak-check will still find it)
+ arr = np.full(int(np.BUFSIZE * 2.5), value).astype(in_dtype)
+ if in_dtype == "O":
+ arr[int(np.BUFSIZE * 1.5)] = None
+ else:
+ arr[int(np.BUFSIZE * 1.5)]["f0"] = None
+
+ count = sys.getrefcount(value)
+
+ it = np.nditer(arr, op_dtypes=[np.dtype(buf_dtype)],
+ flags=["buffered", "external_loop", "refs_ok"], casting="unsafe")
+ with pytest.raises(TypeError):
+ # pytest.raises seems to have issues with the error originating
+ # in the for loop, so manually unravel:
+ next(it)
+ next(it) # raises TypeError
+
+ # Repeat the test with `iternext` after resetting, the buffers should
+ # already be cleared from any references, so resetting is sufficient.
+ it.reset()
+ with pytest.raises(TypeError):
+ it.iternext()
+ it.iternext()
+
+ assert count == sys.getrefcount(value)
diff --git a/numpy/core/tests/test_ufunc.py b/numpy/core/tests/test_ufunc.py
index 1305f4877..9eaa1a977 100644
--- a/numpy/core/tests/test_ufunc.py
+++ b/numpy/core/tests/test_ufunc.py
@@ -1,5 +1,6 @@
import warnings
import itertools
+import sys
import pytest
@@ -11,7 +12,7 @@ import numpy.core._rational_tests as _rational_tests
from numpy.testing import (
assert_, assert_equal, assert_raises, assert_array_equal,
assert_almost_equal, assert_array_almost_equal, assert_no_warnings,
- assert_allclose,
+ assert_allclose, HAS_REFCOUNT,
)
from numpy.compat import pickle
@@ -2074,3 +2075,60 @@ def test_ufunc_warn_with_nan(ufunc):
else:
raise ValueError('ufunc with more than 2 inputs')
+
+@pytest.mark.skipif(not HAS_REFCOUNT, reason="Python lacks refcounts")
+def test_ufunc_casterrors():
+ # Tests that casting errors are correctly reported and buffers are
+ # cleared.
+ # The following array can be added to itself as an object array, but
+ # the result cannot be cast to an integer output:
+ value = 123 # relies on python cache (leak-check will still find it)
+ arr = np.array([value] * int(np.BUFSIZE * 1.5) +
+ ["string"] +
+ [value] * int(1.5 * np.BUFSIZE), dtype=object)
+ out = np.ones(len(arr), dtype=np.intp)
+
+ count = sys.getrefcount(value)
+ with pytest.raises(ValueError):
+ # Output casting failure:
+ np.add(arr, arr, out=out, casting="unsafe")
+
+ assert count == sys.getrefcount(value)
+ # output is unchanged after the error, this shows that the iteration
+ # was aborted (this is not necessarily defined behaviour)
+ assert out[-1] == 1
+
+ with pytest.raises(ValueError):
+ # Input casting failure:
+ np.add(arr, arr, out=out, dtype=np.intp, casting="unsafe")
+
+ assert count == sys.getrefcount(value)
+ # output is unchanged after the error, this shows that the iteration
+ # was aborted (this is not necessarily defined behaviour)
+ assert out[-1] == 1
+
+
+@pytest.mark.skipif(not HAS_REFCOUNT, reason="Python lacks refcounts")
+@pytest.mark.parametrize("offset",
+ [0, np.BUFSIZE//2, int(1.5*np.BUFSIZE)])
+def test_reduce_casterrors(offset):
+ # Test reporting of casting errors in reductions, we test various
+ # offsets to where the casting error will occur, since these may occur
+ # at different places during the reduction procedure. For example
+ # the first item may be special.
+ value = 123 # relies on python cache (leak-check will still find it)
+ arr = np.array([value] * offset +
+ ["string"] +
+ [value] * int(1.5 * np.BUFSIZE), dtype=object)
+ out = np.array(-1, dtype=np.intp)
+
+ count = sys.getrefcount(value)
+ with pytest.raises(ValueError):
+ # This is an unsafe cast, but we currently always allow that:
+ np.add.reduce(arr, dtype=np.intp, out=out)
+ assert count == sys.getrefcount(value)
+ # If an error occurred during casting, the operation is done at most until
+ # the error occurs (the result of which would be `value * offset`) and -1
+ # if the error happened immediately.
+ # This does not define behaviour, the output is invalid and thus undefined
+ assert out[()] < value * offset