summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCharles Harris <charlesr.harris@gmail.com>2018-04-21 21:05:58 -0600
committerGitHub <noreply@github.com>2018-04-21 21:05:58 -0600
commitb5c1bcf1e8ef6e9c11bb4138a15286e648fcbce0 (patch)
tree62cd661d640647bfd210682cf0ce8d553ef69d09
parente0b5e8740efe6d42c909c1374494e614592c65ab (diff)
parent3ff0c5c82b8abc4c94b1801a13f488778631f38a (diff)
downloadnumpy-b5c1bcf1e8ef6e9c11bb4138a15286e648fcbce0.tar.gz
Merge pull request #10891 from eric-wieser/assert-no-cycles
TST: Extract a helper function to test for reference cycles
-rw-r--r--numpy/lib/tests/test_io.py13
-rw-r--r--numpy/testing/_private/utils.py90
-rw-r--r--numpy/testing/tests/test_utils.py76
-rw-r--r--numpy/testing/utils.py2
4 files changed, 167 insertions, 14 deletions
diff --git a/numpy/lib/tests/test_io.py b/numpy/lib/tests/test_io.py
index 84aca9915..0ce44f28b 100644
--- a/numpy/lib/tests/test_io.py
+++ b/numpy/lib/tests/test_io.py
@@ -23,7 +23,7 @@ from numpy.ma.testutils import assert_equal
from numpy.testing import (
assert_warns, assert_, SkipTest, assert_raises_regex, assert_raises,
assert_allclose, assert_array_equal, temppath, tempdir, IS_PYPY,
- HAS_REFCOUNT, suppress_warnings,
+ HAS_REFCOUNT, suppress_warnings, assert_no_gc_cycles,
)
@@ -2416,14 +2416,5 @@ def test_load_refcount():
np.savez(f, [1, 2, 3])
f.seek(0)
- assert_(gc.isenabled())
- gc.disable()
- try:
- gc.collect()
+ with assert_no_gc_cycles():
np.load(f)
- # gc.collect returns the number of unreachable objects in cycles that
- # were found -- we are checking that no cycles were created by np.load
- n_objects_in_cycles = gc.collect()
- finally:
- gc.enable()
- assert_equal(n_objects_in_cycles, 0)
diff --git a/numpy/testing/_private/utils.py b/numpy/testing/_private/utils.py
index 507ecb1e2..b0c0b0c48 100644
--- a/numpy/testing/_private/utils.py
+++ b/numpy/testing/_private/utils.py
@@ -7,6 +7,7 @@ from __future__ import division, absolute_import, print_function
import os
import sys
import re
+import gc
import operator
import warnings
from functools import partial, wraps
@@ -14,6 +15,7 @@ import shutil
import contextlib
from tempfile import mkdtemp, mkstemp
from unittest.case import SkipTest
+import pprint
from numpy.core import(
float32, empty, arange, array_repr, ndarray, isnat, array)
@@ -35,7 +37,7 @@ __all__ = [
'assert_allclose', 'IgnoreException', 'clear_and_catch_warnings',
'SkipTest', 'KnownFailureException', 'temppath', 'tempdir', 'IS_PYPY',
'HAS_REFCOUNT', 'suppress_warnings', 'assert_array_compare',
- '_assert_valid_refcount', '_gen_alignment_data',
+ '_assert_valid_refcount', '_gen_alignment_data', 'assert_no_gc_cycles',
]
@@ -2272,3 +2274,89 @@ class suppress_warnings(object):
return func(*args, **kwargs)
return new_func
+
+
+@contextlib.contextmanager
+def _assert_no_gc_cycles_context(name=None):
+ __tracebackhide__ = True # Hide traceback for py.test
+
+ # not meaningful to test if there is no refcounting
+ if not HAS_REFCOUNT:
+ return
+
+ assert_(gc.isenabled())
+ gc.disable()
+ gc_debug = gc.get_debug()
+ try:
+ for i in range(100):
+ if gc.collect() == 0:
+ break
+ else:
+ raise RuntimeError(
+ "Unable to fully collect garbage - perhaps a __del__ method is "
+ "creating more reference cycles?")
+
+ gc.set_debug(gc.DEBUG_SAVEALL)
+ yield
+ # gc.collect returns the number of unreachable objects in cycles that
+ # were found -- we are checking that no cycles were created in the context
+ n_objects_in_cycles = gc.collect()
+ objects_in_cycles = gc.garbage[:]
+ finally:
+ del gc.garbage[:]
+ gc.set_debug(gc_debug)
+ gc.enable()
+
+ if n_objects_in_cycles:
+ name_str = " when calling %s" % name if name is not None else ""
+ raise AssertionError(
+ "Reference cycles were found{}: {} objects were collected, "
+ "of which {} are shown below:{}"
+ .format(
+ name_str,
+ n_objects_in_cycles,
+ len(objects_in_cycles),
+ ''.join(
+ "\n {} object with id={}:\n {}".format(
+ type(o).__name__,
+ id(o),
+ pprint.pformat(o).replace('\n', '\n ')
+ ) for o in objects_in_cycles
+ )
+ )
+ )
+
+
+def assert_no_gc_cycles(*args, **kwargs):
+ """
+ Fail if the given callable produces any reference cycles.
+
+ If called with all arguments omitted, may be used as a context manager:
+
+ with assert_no_gc_cycles():
+ do_something()
+
+ .. versionadded:: 1.15.0
+
+ Parameters
+ ----------
+ func : callable
+ The callable to test.
+ \\*args : Arguments
+ Arguments passed to `func`.
+ \\*\\*kwargs : Kwargs
+ Keyword arguments passed to `func`.
+
+ Returns
+ -------
+ Nothing. The result is deliberately discarded to ensure that all cycles
+ are found.
+
+ """
+ if not args:
+ return _assert_no_gc_cycles_context()
+
+ func = args[0]
+ args = args[1:]
+ with _assert_no_gc_cycles_context(name=func.__name__):
+ func(*args, **kwargs)
diff --git a/numpy/testing/tests/test_utils.py b/numpy/testing/tests/test_utils.py
index 35f81d8a7..0592e62f8 100644
--- a/numpy/testing/tests/test_utils.py
+++ b/numpy/testing/tests/test_utils.py
@@ -6,6 +6,7 @@ import os
import itertools
import textwrap
import pytest
+import weakref
import numpy as np
from numpy.testing import (
@@ -14,7 +15,7 @@ from numpy.testing import (
assert_raises, assert_warns, assert_no_warnings, assert_allclose,
assert_approx_equal, assert_array_almost_equal_nulp, assert_array_max_ulp,
clear_and_catch_warnings, suppress_warnings, assert_string_equal, assert_,
- tempdir, temppath,
+ tempdir, temppath, assert_no_gc_cycles, HAS_REFCOUNT
)
@@ -1360,3 +1361,76 @@ def test_clear_and_catch_warnings_inherit():
warnings.simplefilter('ignore')
warnings.warn('Some warning')
assert_equal(my_mod.__warningregistry__, {})
+
+
+@pytest.mark.skipif(not HAS_REFCOUNT, reason="Python lacks refcounts")
+class TestAssertNoGcCycles(object):
+ """ Test assert_no_gc_cycles """
+ def test_passes(self):
+ def no_cycle():
+ b = []
+ b.append([])
+ return b
+
+ with assert_no_gc_cycles():
+ no_cycle()
+
+ assert_no_gc_cycles(no_cycle)
+
+
+ def test_asserts(self):
+ def make_cycle():
+ a = []
+ a.append(a)
+ a.append(a)
+ return a
+
+ with assert_raises(AssertionError):
+ with assert_no_gc_cycles():
+ make_cycle()
+
+ with assert_raises(AssertionError):
+ assert_no_gc_cycles(make_cycle)
+
+
+ def test_fails(self):
+ """
+ Test that in cases where the garbage cannot be collected, we raise an
+ error, instead of hanging forever trying to clear it.
+ """
+
+ class ReferenceCycleInDel(object):
+ """
+ An object that not only contains a reference cycle, but creates new
+ cycles whenever it's garbage-collected and its __del__ runs
+ """
+ make_cycle = True
+
+ def __init__(self):
+ self.cycle = self
+
+ def __del__(self):
+ # break the current cycle so that `self` can be freed
+ self.cycle = None
+
+ if ReferenceCycleInDel.make_cycle:
+ # but create a new one so that the garbage collector has more
+ # work to do.
+ ReferenceCycleInDel()
+
+ try:
+ w = weakref.ref(ReferenceCycleInDel())
+ try:
+ with assert_raises(RuntimeError):
+ # this will be unable to get a baseline empty garbage
+ assert_no_gc_cycles(lambda: None)
+ except AssertionError:
+ # the above test is only necessary if the GC actually tried to free
+ # our object anyway, which python 2.7 does not.
+ if w() is not None:
+ pytest.skip("GC does not call __del__ on cyclic objects")
+ raise
+
+ finally:
+ # make sure that we stop creating reference cycles
+ ReferenceCycleInDel.make_cycle = False
diff --git a/numpy/testing/utils.py b/numpy/testing/utils.py
index 78cf405cf..184adcc74 100644
--- a/numpy/testing/utils.py
+++ b/numpy/testing/utils.py
@@ -25,5 +25,5 @@ __all__ = [
'assert_allclose', 'IgnoreException', 'clear_and_catch_warnings',
'SkipTest', 'KnownFailureException', 'temppath', 'tempdir', 'IS_PYPY',
'HAS_REFCOUNT', 'suppress_warnings', 'assert_array_compare',
- '_assert_valid_refcount', '_gen_alignment_data',
+ '_assert_valid_refcount', '_gen_alignment_data', 'assert_no_gc_cycles'
]