summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBruno Oliveira <nicoddemus@gmail.com>2018-04-12 07:53:34 -0300
committerGitHub <noreply@github.com>2018-04-12 07:53:34 -0300
commit015626ce69bfda6e12aad4e3c4454cf35ff5ac95 (patch)
treecedbf193b9bac8f62129fcf9c253a4c18537e150
parentf9a908abb80747e546a1fa87ce30d379257f38ce (diff)
parent6e8e3c967ada43ba47d0b85842607a5f975c5c81 (diff)
downloadpytest-015626ce69bfda6e12aad4e3c4454cf35ff5ac95.tar.gz
Merge pull request #3384 from nicoddemus/leak-frame
Reset reference to failed test frame before each test executes
-rw-r--r--_pytest/runner.py3
-rw-r--r--changelog/2798.bugfix.rst3
-rw-r--r--testing/acceptance_test.py30
-rw-r--r--testing/test_runner.py19
4 files changed, 49 insertions, 6 deletions
diff --git a/_pytest/runner.py b/_pytest/runner.py
index 6792387db..f62d34df2 100644
--- a/_pytest/runner.py
+++ b/_pytest/runner.py
@@ -105,6 +105,7 @@ def pytest_runtest_setup(item):
def pytest_runtest_call(item):
_update_current_test_var(item, 'call')
+ sys.last_type, sys.last_value, sys.last_traceback = (None, None, None)
try:
item.runtest()
except Exception:
@@ -114,7 +115,7 @@ def pytest_runtest_call(item):
sys.last_type = type
sys.last_value = value
sys.last_traceback = tb
- del tb # Get rid of it in this namespace
+ del type, value, tb # Get rid of these in this frame
raise
diff --git a/changelog/2798.bugfix.rst b/changelog/2798.bugfix.rst
new file mode 100644
index 000000000..dc24a724c
--- /dev/null
+++ b/changelog/2798.bugfix.rst
@@ -0,0 +1,3 @@
+Reset ``sys.last_type``, ``sys.last_value`` and ``sys.last_traceback`` before each test executes. Those attributes
+are added by pytest during the test run to aid debugging, but were never reset so they would create a leaking
+reference to the last failing test's frame which in turn could never be reclaimed by the garbage collector.
diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py
index 89a44911f..8ceb3bae1 100644
--- a/testing/acceptance_test.py
+++ b/testing/acceptance_test.py
@@ -988,3 +988,33 @@ def test_fixture_order_respects_scope(testdir):
''')
result = testdir.runpytest()
assert result.ret == 0
+
+
+def test_frame_leak_on_failing_test(testdir):
+ """pytest would leak garbage referencing the frames of tests that failed that could never be reclaimed (#2798)
+
+ Unfortunately it was not possible to remove the actual circles because most of them
+ are made of traceback objects which cannot be weakly referenced. Those objects at least
+ can be eventually claimed by the garbage collector.
+ """
+ testdir.makepyfile('''
+ import gc
+ import weakref
+
+ class Obj:
+ pass
+
+ ref = None
+
+ def test1():
+ obj = Obj()
+ global ref
+ ref = weakref.ref(obj)
+ assert 0
+
+ def test2():
+ gc.collect()
+ assert ref() is None
+ ''')
+ result = testdir.runpytest_subprocess()
+ result.stdout.fnmatch_lines(['*1 failed, 1 passed in*'])
diff --git a/testing/test_runner.py b/testing/test_runner.py
index a3bd8ecb4..7c179b1f2 100644
--- a/testing/test_runner.py
+++ b/testing/test_runner.py
@@ -719,18 +719,20 @@ def test_makereport_getsource_dynamic_code(testdir, monkeypatch):
result.stdout.fnmatch_lines(["*test_fix*", "*fixture*'missing'*not found*"])
-def test_store_except_info_on_eror():
+def test_store_except_info_on_error():
""" Test that upon test failure, the exception info is stored on
sys.last_traceback and friends.
"""
- # Simulate item that raises a specific exception
- class ItemThatRaises(object):
+ # Simulate item that might raise a specific exception, depending on `raise_error` class var
+ class ItemMightRaise(object):
nodeid = 'item_that_raises'
+ raise_error = True
def runtest(self):
- raise IndexError('TEST')
+ if self.raise_error:
+ raise IndexError('TEST')
try:
- runner.pytest_runtest_call(ItemThatRaises())
+ runner.pytest_runtest_call(ItemMightRaise())
except IndexError:
pass
# Check that exception info is stored on sys
@@ -738,6 +740,13 @@ def test_store_except_info_on_eror():
assert sys.last_value.args[0] == 'TEST'
assert sys.last_traceback
+ # The next run should clear the exception info stored by the previous run
+ ItemMightRaise.raise_error = False
+ runner.pytest_runtest_call(ItemMightRaise())
+ assert sys.last_type is None
+ assert sys.last_value is None
+ assert sys.last_traceback is None
+
def test_current_test_env_var(testdir, monkeypatch):
pytest_current_test_vars = []