Merge pull request #3384 from nicoddemus/leak-frame

Reset reference to failed test frame before each test executes
This commit is contained in:
Bruno Oliveira 2018-04-12 07:53:34 -03:00 committed by GitHub
commit 015626ce69
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 49 additions and 6 deletions

View File

@ -105,6 +105,7 @@ def pytest_runtest_setup(item):
def pytest_runtest_call(item): def pytest_runtest_call(item):
_update_current_test_var(item, 'call') _update_current_test_var(item, 'call')
sys.last_type, sys.last_value, sys.last_traceback = (None, None, None)
try: try:
item.runtest() item.runtest()
except Exception: except Exception:
@ -114,7 +115,7 @@ def pytest_runtest_call(item):
sys.last_type = type sys.last_type = type
sys.last_value = value sys.last_value = value
sys.last_traceback = tb 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 raise

View File

@ -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.

View File

@ -988,3 +988,33 @@ def test_fixture_order_respects_scope(testdir):
''') ''')
result = testdir.runpytest() result = testdir.runpytest()
assert result.ret == 0 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*'])

View File

@ -719,18 +719,20 @@ def test_makereport_getsource_dynamic_code(testdir, monkeypatch):
result.stdout.fnmatch_lines(["*test_fix*", "*fixture*'missing'*not found*"]) 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 """ Test that upon test failure, the exception info is stored on
sys.last_traceback and friends. sys.last_traceback and friends.
""" """
# Simulate item that raises a specific exception # Simulate item that might raise a specific exception, depending on `raise_error` class var
class ItemThatRaises(object): class ItemMightRaise(object):
nodeid = 'item_that_raises' nodeid = 'item_that_raises'
raise_error = True
def runtest(self): def runtest(self):
raise IndexError('TEST') if self.raise_error:
raise IndexError('TEST')
try: try:
runner.pytest_runtest_call(ItemThatRaises()) runner.pytest_runtest_call(ItemMightRaise())
except IndexError: except IndexError:
pass pass
# Check that exception info is stored on sys # 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_value.args[0] == 'TEST'
assert sys.last_traceback 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): def test_current_test_env_var(testdir, monkeypatch):
pytest_current_test_vars = [] pytest_current_test_vars = []