Skip to content

Commit 0c6a548

Browse files
dairikimvanderlee
authored andcommitted
Test for memory leaks as described in lovasoa#198
1 parent d6396c1 commit 0c6a548

File tree

1 file changed

+111
-0
lines changed

1 file changed

+111
-0
lines changed

tests/test_memory_leak.py

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import gc
2+
import inspect
3+
import sys
4+
import unittest
5+
import weakref
6+
from dataclasses import dataclass
7+
8+
import marshmallow
9+
import marshmallow_dataclass as md
10+
11+
12+
class Referenceable:
13+
pass
14+
15+
16+
class TestMemoryLeak(unittest.TestCase):
17+
"""Test for memory leaks as decribed in `#198`_.
18+
19+
.. _#198: https://github.com/lovasoa/marshmallow_dataclass/issues/198
20+
"""
21+
22+
def setUp(self):
23+
gc.collect()
24+
gc.disable()
25+
self.frame_collected = False
26+
27+
def tearDown(self):
28+
gc.enable()
29+
30+
def trackFrame(self):
31+
"""Create a tracked local variable in the callers frame.
32+
33+
We track these locals in the WeakSet self.livingLocals.
34+
35+
When the callers frame is freed, the locals will be GCed as well.
36+
In this way we can check that the callers frame has been collected.
37+
"""
38+
local = Referenceable()
39+
weakref.finalize(local, self._set_frame_collected)
40+
try:
41+
frame = inspect.currentframe()
42+
frame.f_back.f_locals["local_variable"] = local
43+
finally:
44+
del frame
45+
46+
def _set_frame_collected(self):
47+
self.frame_collected = True
48+
49+
def assertFrameCollected(self):
50+
"""Check that all locals created by makeLocal have been GCed"""
51+
if not hasattr(sys, "getrefcount"):
52+
# pypy does not do reference counting
53+
gc.collect(0)
54+
self.assertTrue(self.frame_collected)
55+
56+
def test_sanity(self):
57+
"""Test that our scheme for detecting leaked frames works."""
58+
frames = []
59+
60+
def f():
61+
frames.append(inspect.currentframe())
62+
self.trackFrame()
63+
64+
f()
65+
66+
gc.collect(0)
67+
self.assertFalse(
68+
self.frame_collected
69+
) # with frame leaked, f's locals are still alive
70+
frames.clear()
71+
self.assertFrameCollected()
72+
73+
def test_class_schema(self):
74+
def f():
75+
@dataclass
76+
class Foo:
77+
value: int
78+
79+
md.class_schema(Foo)
80+
81+
self.trackFrame()
82+
83+
f()
84+
self.assertFrameCollected()
85+
86+
def test_md_dataclass_lazy_schema(self):
87+
def f():
88+
@md.dataclass
89+
class Foo:
90+
value: int
91+
92+
self.trackFrame()
93+
94+
f()
95+
# NB: The "lazy" Foo.Schema attribute descriptor holds a reference to f's frame,
96+
# which, in turn, holds a reference to class Foo, thereby creating ref cycle.
97+
# So, a gc pass is required to clean that up.
98+
gc.collect(0)
99+
self.assertFrameCollected()
100+
101+
def test_md_dataclass(self):
102+
def f():
103+
@md.dataclass
104+
class Foo:
105+
value: int
106+
107+
self.assertIsInstance(Foo.Schema(), marshmallow.Schema)
108+
self.trackFrame()
109+
110+
f()
111+
self.assertFrameCollected()

0 commit comments

Comments
 (0)