From ba407b5eb601da54a893df769fd61e65a718bb66 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 4 Apr 2018 20:36:07 -0300 Subject: Clear sys.last_* attributes before running an item Otherwise we will keep the last failed exception around forever Related to #2798 --- _pytest/runner.py | 3 ++- testing/test_runner.py | 19 ++++++++++++++----- 2 files changed, 16 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/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 = [] -- cgit v1.2.3 From 78c900448e2683ba4ae03d6f21f500c1cc43ddd5 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 9 Apr 2018 20:43:14 -0300 Subject: Add acceptance test for #2798 --- testing/acceptance_test.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) 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*']) -- cgit v1.2.3 From 6e8e3c967ada43ba47d0b85842607a5f975c5c81 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 10 Apr 2018 21:07:34 -0300 Subject: Add changelog entry for #2798 --- changelog/2798.bugfix.rst | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 changelog/2798.bugfix.rst 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. -- cgit v1.2.3