From 1130b9f742d053a429149e56b8a0c8aec72a9968 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 8 Nov 2016 21:34:45 -0200 Subject: [PATCH] Fix the stubborn test about cyclic references left by pytest.raises In Python 2, a context manager's __exit__() leaves sys.exc_info with the exception values even when it was supposed to suppress the exception, so we explicitly call sys.exc_clear() which removes the traceback and allow the object to be released. Also updated the test to not depend on the immediate destruction of the object but instead to ensure it is not being tracked as a cyclic reference. Fix #1965 --- CHANGELOG.rst | 5 ++++- _pytest/python.py | 6 +++++- testing/python/raises.py | 22 +++++++++++++++++----- 3 files changed, 26 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 35a54de4b..e88eb5b09 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -12,7 +12,10 @@ * When loading plugins, import errors which contain non-ascii messages are now properly handled in Python 2 (`#1998`_). Thanks `@nicoddemus`_ for the PR. -* Fixed memory leak in the RaisesContext (pytest.raises) (`#1965`_). +* Fixed cyclic reference when ``pytest.raises`` is used in context-manager form (`#1965`_). Also as a + result of this fix, ``sys.exc_info()`` is left empty in both context-manager and function call usages. + Previously, ``sys.exc_info`` would contain the exception caught by the context manager, + even when the expected exception occurred. Thanks `@MSeifert04`_ for the report and the PR. * Fixed false-positives warnings from assertion rewrite hook for modules that were rewritten but diff --git a/_pytest/python.py b/_pytest/python.py index a42e7185e..18432c1e7 100644 --- a/_pytest/python.py +++ b/_pytest/python.py @@ -1237,7 +1237,11 @@ class RaisesContext(object): exc_type, value, traceback = tp tp = exc_type, exc_type(value), traceback self.excinfo.__init__(tp) - return issubclass(self.excinfo.type, self.expected_exception) + suppress_exception = issubclass(self.excinfo.type, self.expected_exception) + if sys.version_info[0] == 2 and suppress_exception: + sys.exc_clear() + return suppress_exception + # builtin pytest.approx helper diff --git a/testing/python/raises.py b/testing/python/raises.py index 80c3df2ab..8f141cfa1 100644 --- a/testing/python/raises.py +++ b/testing/python/raises.py @@ -1,4 +1,6 @@ import pytest +import sys + class TestRaises: def test_raises(self): @@ -98,10 +100,13 @@ class TestRaises: assert False, "Expected pytest.raises.Exception" @pytest.mark.parametrize('method', ['function', 'with']) - def test_raises_memoryleak(self, method): - import weakref + def test_raises_cyclic_reference(self, method): + """ + Ensure pytest.raises does not leave a reference cycle (#1965). + """ + import gc - class T: + class T(object): def __call__(self): raise ValueError @@ -112,5 +117,12 @@ class TestRaises: with pytest.raises(ValueError): t() - t = weakref.ref(t) - assert t() is None + # ensure both forms of pytest.raises don't leave exceptions in sys.exc_info() + assert sys.exc_info() == (None, None, None) + + del t + + # ensure the t instance is not stuck in a cyclic reference + for o in gc.get_objects(): + assert type(o) is not T +