Thanks to visit codestin.com
Credit goes to github.com

Skip to content

TST: Extract a helper function to test for reference cycles #10891

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Apr 22, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 2 additions & 11 deletions numpy/lib/tests/test_io.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
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,
)


Expand Down Expand Up @@ -2369,14 +2369,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)
90 changes: 89 additions & 1 deletion numpy/testing/_private/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@
import os
import sys
import re
import gc
import operator
import warnings
from functools import partial, wraps
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)
Expand All @@ -35,7 +37,7 @@
'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',
]


Expand Down Expand Up @@ -2272,3 +2274,89 @@ def new_func(*args, **kwargs):
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)
76 changes: 75 additions & 1 deletion numpy/testing/tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import itertools
import textwrap
import pytest
import weakref

import numpy as np
from numpy.testing import (
Expand All @@ -14,7 +15,7 @@
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
)


Expand Down Expand Up @@ -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
2 changes: 1 addition & 1 deletion numpy/testing/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,5 @@
'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'
]