Skip to content

Reset reference to failed test frame before each test executes #3384

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 12, 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
3 changes: 2 additions & 1 deletion _pytest/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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


Expand Down
3 changes: 3 additions & 0 deletions changelog/2798.bugfix.rst
Original file line number Diff line number Diff line change
@@ -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.
30 changes: 30 additions & 0 deletions testing/acceptance_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -988,3 +988,33 @@ def test_value():
''')
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*'])
19 changes: 14 additions & 5 deletions testing/test_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -719,25 +719,34 @@ def test_fix(foo):
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
assert sys.last_type is IndexError
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 = []
Expand Down