Skip to content

Commit

Permalink
TST: Add test [circle full]
Browse files Browse the repository at this point in the history
  • Loading branch information
larsoner committed Oct 21, 2020
1 parent 510da25 commit 13ce561
Show file tree
Hide file tree
Showing 6 changed files with 89 additions and 31 deletions.
9 changes: 3 additions & 6 deletions doc/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
import mne
from mne.viz import Brain
from mne.utils import (linkcode_resolve, # noqa, analysis:ignore
_get_referrers, sizeof_fmt)
_assert_no_instances, sizeof_fmt)

if LooseVersion(sphinx_gallery.__version__) < LooseVersion('0.2'):
raise ImportError('Must have at least version 0.2 of sphinx-gallery, got '
Expand Down Expand Up @@ -460,12 +460,9 @@ def __call__(self, gallery_conf, fname):
# turn it off here (otherwise the build can be very slow)
plt.ioff()
plt.rcParams['animation.embed_limit'] = 30.
new = '\n'
n, ref = _get_referrers(Brain) # calls gc.collect()
assert n == 0, f'Brain after:\n{new.join(ref)}'
_assert_no_instances(Brain, 'running') # calls gc.collect()
if Plotter is not None:
n, ref = _get_referrers(Plotter) # calls gc.collect()
assert n == 0, f'Plotter after:\n{new.join(ref)}'
_assert_no_instances(Plotter, 'running')
# This will overwrite some Sphinx printing but it's useful
# for memory timestamps
if os.getenv('SG_STAMP_STARTS', '').lower() == 'true':
Expand Down
35 changes: 28 additions & 7 deletions mne/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
import numpy as np
import mne
from mne.datasets import testing
from mne.utils import _pl, _get_referrers
from mne.utils import _pl, _assert_no_instances

test_path = testing.data_path(download=False)
s_path = op.join(test_path, 'MEG', 'sample')
Expand Down Expand Up @@ -472,15 +472,36 @@ def download_is_error(monkeypatch):


@pytest.fixture()
def brain_gc():
def brain_gc(request):
"""Ensure that brain can be properly garbage collected."""
keys = ('renderer_interactive', 'renderer')
assert set(request.fixturenames) & set(keys) != set()
for key in keys:
if key in request.fixturenames:
is_pv = request.getfixturevalue(key)._get_3d_backend() == 'pyvista'
break
if not is_pv:
yield
return
from mne.viz import Brain
new = '\n'
n, ref = _get_referrers(Brain)
assert n == 0, f'{n} before:\n{new.join(ref)}'
_assert_no_instances(Brain, 'before')
ignore = set(id(o) for o in gc.get_objects())
yield
n, ref = _get_referrers(Brain)
assert n == 0, f'{n} after:\n{new.join(ref)}'
_assert_no_instances(Brain, 'after')
# We only check VTK for PyVista -- Mayavi/PySurfer is not as strict
objs = gc.get_objects()
bad = list()
for o in objs:
try:
name = o.__class__.__name__
except Exception: # old Python, probably
pass
else:
if name.startswith('vtk') and id(o) not in ignore:
bad.append(name)
del o
del objs, ignore, Brain
assert len(bad) == 0, 'VTK objects linger:\n' + '\n'.join(bad)


def pytest_sessionfinish(session, exitstatus):
Expand Down
3 changes: 2 additions & 1 deletion mne/utils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@
ClosingStringIO)
from .misc import (run_subprocess, _pl, _clean_names, pformat, _file_like,
_explain_exception, _get_argvalues, sizeof_fmt,
running_subprocess, _DefaultEventParser, _get_referrers)
running_subprocess, _DefaultEventParser,
_assert_no_instances)
from .progressbar import ProgressBar
from ._testing import (run_tests_if_main, run_command_if_main,
requires_sklearn,
Expand Down
27 changes: 19 additions & 8 deletions mne/utils/misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -322,15 +322,26 @@ def _file_like(obj):
return all(callable(getattr(obj, name, None)) for name in ('read', 'seek'))


def _get_referrers(cls):
def _assert_no_instances(cls, when=''):
__tracebackhide__ = True
n = 0
ref = list()
new = '\n'
gc.collect()
for obj in gc.get_objects():
objs = gc.get_objects()
for obj in objs:
if isinstance(obj, cls):
n += 1
ref.extend([
f'{r.__class__.__name__}: {repr(r)[:100].replace(new, " ")}'
for r in gc.get_referrers(obj)])
return n, ref
rr = gc.get_referrers(obj)
count = 0
for r in rr:
if r is not objs and \
r is not globals() and \
r is not locals() and \
not inspect.isframe(r):
ref.append(
f'{r.__class__.__name__}: ' +
repr(r)[:100].replace('\n', ' '))
count += 1
del r
del rr
n += count > 0
assert n == 0, f'{n} {when}:\n' + '\n'.join(ref)
39 changes: 30 additions & 9 deletions mne/viz/_brain/_brain.py
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,12 @@ def __init__(self, subject_id, hemi, surf, title=None,
bgcolor=background,
shape=shape,
fig=figure)

if _get_3d_backend() == "pyvista":
self.plotter = self._renderer.plotter
self.window = self.plotter.app_window
self.window.signal_close.connect(self._clean)

for h in self._hemis:
# Initialize a Surface object as the geometry
geo = Surface(subject_id, h, surf, subjects_dir, offset,
Expand Down Expand Up @@ -303,6 +309,7 @@ def __init__(self, subject_id, hemi, surf, title=None,
actor, mesh = mesh_data.actor, mesh_data
self._hemi_meshes[h] = mesh
self._hemi_actors[h] = actor
del mesh_data, actor, mesh
else:
self._renderer.polydata(
self._hemi_meshes[h],
Expand Down Expand Up @@ -375,14 +382,11 @@ def setup_time_viewer(self, time_viewer=True, show_traces=True):
self.slider_tube_color = (0.69803922, 0.70196078, 0.70980392)

# Direct access parameters:
self.plotter = self._renderer.plotter
self._iren = self._renderer.plotter.iren
self.main_menu = self.plotter.main_menu
self.window = self.plotter.app_window
self.tool_bar = self.window.addToolBar("toolbar")
self.status_bar = self.window.statusBar()
self.interactor = self.plotter.interactor
self.window.signal_close.connect(self._clean)

# Derived parameters:
self.playback_speed = self.default_playback_speed_value
Expand Down Expand Up @@ -426,16 +430,29 @@ def setup_time_viewer(self, time_viewer=True, show_traces=True):
def _clean(self):
# resolve the reference cycle
self.clear_points()
for h in self._hemis:
actor = self._hemi_actors[h]
if actor is not None:
mapper = actor.GetMapper()
mapper.SetLookupTable(None)
actor.SetMapper(None)
self._clear_callbacks()
self.actions.clear()
self.sliders.clear()
if self.mpl_canvas is not None:
if getattr(self, 'mpl_canvas', None) is not None:
self.mpl_canvas.clear()
for key in list(self.act_data_smooth.keys()):
self.act_data_smooth[key] = None
if getattr(self, 'act_data_smooth', None) is not None:
for key in list(self.act_data_smooth.keys()):
self.act_data_smooth[key] = None
# XXX this should be done in PyVista
for renderer in self.plotter.renderers:
renderer.RemoveAllLights()
for key in ('lighting', 'interactor', 'app_window', '_RenderWindow',
'_Iren'):
setattr(self.plotter, key, None)
# XXX end PyVista
for key in ('reps', 'plotter', 'main_menu', 'window', 'tool_bar',
'status_bar', 'interactor', 'mpl_canvas', 'time_actor',
'picked_renderer', 'act_data_smooth', '_iren'):
'picked_renderer', 'act_data_smooth', '_iren',
'actions', 'sliders', 'geo', '_hemi_actors', '_data'):
setattr(self, key, None)

@contextlib.contextmanager
Expand Down Expand Up @@ -1206,6 +1223,8 @@ def remove_point(self, mesh):

def clear_points(self):
"""Clear the picked points."""
if not hasattr(self, '_spheres'):
return
for sphere in list(self._spheres): # will remove itself, so copy
self.remove_point(sphere)
assert sum(len(v) for v in self.picked_points.values()) == 0
Expand Down Expand Up @@ -1307,6 +1326,8 @@ def help(self):

def _clear_callbacks(self):
from ..backends._pyvista import _remove_picking_callback
if not hasattr(self, 'callbacks'):
return
for callback in self.callbacks.values():
if callback is not None:
if hasattr(callback, "plotter"):
Expand Down
7 changes: 7 additions & 0 deletions mne/viz/_brain/tests/test_brain.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,13 @@ def GetPosition(self):
return np.array(self.GetPickPosition()) - (0, 0, 100)


@testing.requires_testing_data
def test_brain_gc(renderer, brain_gc):
"""Test that a minimal version of Brain gets GC'ed."""
brain = Brain('fsaverage', 'both', 'inflated', subjects_dir=subjects_dir)
brain.close()


@testing.requires_testing_data
def test_brain_init(renderer, tmpdir, pixel_ratio, brain_gc):
"""Test initialization of the Brain instance."""
Expand Down

0 comments on commit 13ce561

Please sign in to comment.