Skip to content

Commit 556158f

Browse files
committed
Refact memleak.py
1 parent 6eda024 commit 556158f

File tree

2 files changed

+89
-68
lines changed

2 files changed

+89
-68
lines changed

psutil/test/memleak.py

Lines changed: 86 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -93,22 +93,67 @@ def qualname(obj):
9393
return getattr(obj, "__qualname__", getattr(obj, "__name__", str(obj)))
9494

9595

96-
class MemoryLeakError(AssertionError):
97-
"""Raised when a memory leak is detected."""
96+
# --- exceptions
97+
98+
99+
class UnclosedResourceError(AssertionError):
100+
"""Base class for errors raised when some resource created during a
101+
function call is left unclosed or unfreed afterward.
102+
"""
103+
104+
resource_name = "resource" # override in subclasses
105+
106+
def __init__(self, count, fun_name):
107+
self.count = count
108+
self.fun_name = fun_name
109+
name = self.resource_name
110+
name += "s" if count > 1 else "" # pluralize
111+
msg = (
112+
f"detected {count} unclosed {name} after calling {fun_name!r} 1"
113+
" time"
114+
)
115+
super().__init__(msg)
116+
117+
118+
class UnclosedFdError(UnclosedResourceError):
119+
"""Raised when an unclosed file descriptor is detected after
120+
calling function once. Used to detect forgotten close(). UNIX only.
121+
"""
122+
123+
resource_name = "file descriptor"
98124

99125

100-
class UnclosedFdError(AssertionError):
101-
"""Raised when an unclosed file descriptor (UNIX) or handle
102-
(Windows) is detected.
126+
class UnclosedHandleError(UnclosedResourceError):
127+
"""Raised when an unclosed handle is detected after calling
128+
function once. Used to detect forgotten CloseHandle().
129+
Windows only.
103130
"""
104131

132+
resource_name = "handle"
133+
134+
135+
class UnclosedHeapCreateError(UnclosedResourceError):
136+
"""Raised when test detects HeapCreate() without a corresponding
137+
HeapDestroy() after calling function once. Windows only.
138+
"""
139+
140+
resource_name = "HeapCreate() call"
141+
142+
143+
class MemoryLeakError(AssertionError):
144+
"""Raised when a memory leak is detected after calling function
145+
many times. Aims to detect:
105146
106-
class UnclosedHeapCreateError(AssertionError):
107-
"""Raised on Windows when test detects HeapCreate() without a
108-
corresponding HeapDestroy().
147+
- `malloc()` without a corresponding `free()`
148+
- `mmap()` without `munmap()`
149+
- `HeapAlloc()` without `HeapFree()` (Windows)
150+
- `VirtualAlloc()` without `VirtualFree()` (Windows)
109151
"""
110152

111153

154+
# ---
155+
156+
112157
class MemoryLeakTestCase(unittest.TestCase):
113158
# Number of times to call the tested function in each iteration.
114159
times = 200
@@ -167,6 +212,13 @@ def _warmup(self, fun, warmup_times):
167212

168213
# --- getters
169214

215+
def _get_oneshot(self):
216+
return {
217+
"num_fds": thisproc.num_fds() if POSIX else 0,
218+
"num_handles": thisproc.num_handles() if WINDOWS else 0,
219+
"heap_count": psutil.heap_info().heap_count if WINDOWS else 0,
220+
}
221+
170222
def _get_mem(self):
171223
mem = thisproc.memory_full_info()
172224
heap_used = mmap_used = 0
@@ -182,64 +234,34 @@ def _get_mem(self):
182234
"vms": mem.vms,
183235
}
184236

185-
def _get_num_fds(self):
186-
if POSIX:
187-
return thisproc.num_fds()
188-
else:
189-
return thisproc.num_handles()
190-
191237
# --- checkers
192238

193-
def _check_fds(self, fun):
194-
"""Makes sure `num_fds()` (POSIX) or `num_handles()` (Windows)
195-
do not increase after calling function 1 time. Used to
196-
discover forgotten `close(2)` and `CloseHandle()`.
197-
"""
198-
199-
before = self._get_num_fds()
200-
self.call(fun)
201-
after = self._get_num_fds()
202-
diff = after - before
203-
204-
if diff < 0:
205-
msg = (
206-
f"negative diff {diff!r} (gc probably collected a"
207-
" resource from a previous test)"
208-
)
209-
raise UnclosedFdError(msg)
210-
211-
if diff > 0:
212-
type_ = "fd" if POSIX else "handle"
213-
if diff > 1:
214-
type_ += "s"
215-
msg = (
216-
f"detected {diff} unclosed {type_} after calling"
217-
f" {qualname(fun)!r} 1 time"
218-
)
219-
raise UnclosedFdError(msg)
220-
221-
def _check_heap_count(self, fun):
222-
"""Windows only. Calls function once, and detects HeapCreate()
223-
without a corresponding HeapDestroy().
224-
"""
225-
if not WINDOWS:
226-
return
227-
228-
before = psutil.heap_info().heap_count
239+
def _check_oneshot(self, fun):
240+
before = self._get_oneshot()
229241
self.call(fun)
230-
after = psutil.heap_info().heap_count
231-
diff = after - before
232-
233-
if diff < 0:
234-
msg = f"negative diff {diff!r}"
235-
raise UnclosedHeapCreateError(msg)
236-
237-
if diff > 0:
238-
msg = (
239-
f"detected {diff} HeapCreate() without a corresponding "
240-
f" HeapDestroy() after calling {qualname(fun)!r} 1 time"
241-
)
242-
raise UnclosedHeapCreateError(msg)
242+
after = self._get_oneshot()
243+
244+
for what, value_before in before.items():
245+
value_after = after[what]
246+
diff = value_after - value_before
247+
248+
if diff < 0:
249+
msg = (
250+
f"WARNING: {what!r} decreased by {abs(diff)} after calling"
251+
f" {qualname(fun)!r} 1 time"
252+
)
253+
self._log(msg, 0)
254+
255+
elif diff > 0:
256+
mapping = {
257+
"num_fds": UnclosedFdError,
258+
"num_handles": UnclosedHandleError,
259+
"heap_count": UnclosedHeapCreateError,
260+
}
261+
exc = mapping.get(what)
262+
if exc is None:
263+
raise ValueError(what)
264+
raise exc(diff, qualname(fun))
243265

244266
def _call_ntimes(self, fun, times):
245267
"""Get memory samples before and after calling fun repeatedly,
@@ -331,8 +353,6 @@ def execute(
331353
if args:
332354
fun = functools.partial(fun, *args)
333355

356+
self._check_oneshot(fun)
334357
self._warmup(fun, warmup_times)
335-
self._check_fds(fun)
336-
if WINDOWS:
337-
self._check_heap_count(fun)
338358
self._check_mem(fun, times=times, retries=retries, tolerance=tolerance)

tests/test_testutils.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
from psutil.test import MemoryLeakTestCase
2929
from psutil.test.memleak import MemoryLeakError
3030
from psutil.test.memleak import UnclosedFdError
31+
from psutil.test.memleak import UnclosedHandleError
3132

3233
from . import CI_TESTING
3334
from . import COVERAGE
@@ -402,7 +403,7 @@ def fun(ls=ls):
402403
finally:
403404
del ls
404405

405-
def test_unclosed_files(self):
406+
def test_unclosed_fds(self):
406407
def fun():
407408
f = open(__file__) # noqa: SIM115
408409
self.addCleanup(f.close)
@@ -423,7 +424,7 @@ def fun():
423424
)
424425
self.addCleanup(win32api.CloseHandle, handle)
425426

426-
with pytest.raises(UnclosedFdError):
427+
with pytest.raises(UnclosedHandleError):
427428
self.execute(fun)
428429

429430
def test_tolerance(self):

0 commit comments

Comments
 (0)