Skip to content

Commit

Permalink
merge from master
Browse files Browse the repository at this point in the history
  • Loading branch information
giampaolo committed May 7, 2020
2 parents 67dfb10 + e1ea2bc commit e903726
Show file tree
Hide file tree
Showing 4 changed files with 135 additions and 46 deletions.
108 changes: 72 additions & 36 deletions psutil/tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@
'ThreadTask'
# test utils
'unittest', 'skip_on_access_denied', 'skip_on_not_implemented',
'retry_on_failure', 'TestMemoryLeak', 'PsutilTestCase',
'retry_on_failure', 'TestMemoryLeak', 'TestFdsLeak', 'PsutilTestCase',
'process_namespace', 'system_namespace',
# install utils
'install_pip', 'install_test_deps',
Expand Down Expand Up @@ -895,28 +895,47 @@ def assertProcessGone(self, proc):

@unittest.skipIf(PYPY, "unreliable on PYPY")
class TestMemoryLeak(PsutilTestCase):
"""Test framework class for detecting function memory leaks (typically
functions implemented in C).
It does so by calling a function many times, and checks whether the
process memory usage increased before and after having called the
function repeadetly.
"""Test framework class for detecting function memory leaks,
typically functions implemented in C which forgot to free() memory
from the heap. It does so by checking whether the process memory
usage increased before and after calling the function many times.
The logic:
call_fun_n_times()
if mem_diff > tolerance:
call_fun_for_3_secs()
if mem_diff > 0:
return 1 # failure
return 0 # success
Note that this is hard (probably impossible) to do reliably, due
to how the OS handles memory, the GC and so on (memory can even
decrease!). In order to avoid false positives you should adjust the
tolerance of each individual test case, but most of the times you
won't have to.
If available (Linux, OSX, Windows) USS memory is used for comparison,
since it's supposed to be more precise, see:
http://grodola.blogspot.com/2016/02/psutil-4-real-process-memory-and-environ.html
If not, RSS memory is used. mallinfo() on Linux and _heapwalk() on
Windows may give even more precision, but at the moment are not
implemented.
In addition also call the function onces and make sure num_fds()
(POSIX) or num_handles() (Windows) does not increase. This is done
in order to discover forgotten close(2) and CloseHandle syscalls.
PyPy appears to be completely unstable for this framework, probably
because of its JIT, so tests on PYPY are skipped.
Note that sometimes this may produce false positives.
Usage:
PyPy appears to be completely unstable for this framework, probably
because of how its JIT handles memory, so tests on PYPY are
automatically skipped.
class TestLeaks(psutil.tests.TestMemoryLeak):
def test_fun(self):
self.execute(some_function)
"""
# Configurable class attrs.
times = 1200
times = 1000
warmup_times = 10
tolerance = 4096 # memory
retry_for = 3.0 # seconds
check_fds = True # whether to check if num_fds() increased
verbose = True

def setUp(self):
Expand Down Expand Up @@ -972,7 +991,7 @@ def _log(self, msg):
print_color(msg, color="yellow", file=sys.stderr)

def execute(self, fun, times=times, warmup_times=warmup_times,
tolerance=tolerance, retry_for=retry_for, check_fds=check_fds):
tolerance=tolerance, retry_for=retry_for):
"""Test a callable."""
if times <= 0:
raise ValueError("times must be > 0")
Expand All @@ -983,15 +1002,6 @@ def execute(self, fun, times=times, warmup_times=warmup_times,
if retry_for is not None and retry_for < 0:
raise ValueError("retry_for must be >= 0")

if check_fds:
before = self._get_fds_or_handles()
self._call(fun)
after = self._get_fds_or_handles()
diff = abs(after - before)
if diff > 0:
msg = "%s unclosed fd(s) or handle(s)" % (diff)
raise self.fail(msg)

# warm up
self._call_ntimes(fun, warmup_times)
mem1 = self._call_ntimes(fun, times)
Expand Down Expand Up @@ -1029,6 +1039,43 @@ def call():
self.execute(call, **kwargs)


class TestFdsLeak(PsutilTestCase):
"""Test framework class which makes sure num_fds() (POSIX) or
num_handles() (Windows) does not increase after calling a function.
This can be used to discover forgotten close(2) and CloseHandle
syscalls.
"""

tolerance = 0
_thisproc = psutil.Process()

def _get_fds_or_handles(self):
if POSIX:
return self._thisproc.num_fds()
else:
return self._thisproc.num_handles()

def _call(self, fun):
return fun()

def execute(self, fun, tolerance=tolerance):
# This is supposed to close() any unclosed file object.
gc.collect()
before = self._get_fds_or_handles()
self._call(fun)
after = self._get_fds_or_handles()
diff = after - before
if diff < 0:
raise self.fail("negative diff %r (gc probably collected a "
"resource from a previous test)" % diff)
if diff > 0:
type_ = "fd" if POSIX else "handle"
if diff > 1:
type_ += "s"
msg = "%s unclosed %s after calling %r" % (diff, type_, fun)
raise self.fail(msg)


def _get_eligible_cpu():
p = psutil.Process()
if hasattr(p, "cpu_num"):
Expand Down Expand Up @@ -1238,17 +1285,6 @@ def iter(ls):
fun = functools.partial(fun, *args, **kwds)
yield (fun, fun_name)

@classmethod
def test(cls):
this = set([x[0] for x in cls.all])
ignored = set([x[0] for x in cls.ignored])
# there's a separate test for __all__
mod = set([x for x in dir(psutil) if x.islower() and x[0] != '_' and
x in psutil.__all__ and callable(getattr(psutil, x))])
leftout = (this | ignored) ^ mod
if leftout:
raise ValueError("uncovered psutil mod name(s): %r" % leftout)

test_class_coverage = process_namespace.test_class_coverage


Expand Down
43 changes: 43 additions & 0 deletions psutil/tests/test_memory_leaks.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,12 @@
from psutil.tests import spawn_testproc
from psutil.tests import system_namespace
from psutil.tests import terminate
from psutil.tests import TestFdsLeak
from psutil.tests import TestMemoryLeak
from psutil.tests import TRAVIS
from psutil.tests import unittest


SKIP_PYTHON_IMPL = True
cext = psutil._psplatform.cext
thisproc = psutil.Process()
Expand Down Expand Up @@ -487,6 +489,47 @@ def test_win_service_get_description(self):
self.execute(lambda: cext.winservice_query_descr(name))


# =====================================================================
# --- File descriptors and handlers
# =====================================================================


class TestUnclosedFdsOrHandles(TestFdsLeak):

def test_process_apis(self):
p = psutil.Process()
ns = process_namespace(p)
for fun, name in ns.iter(ns.getters + ns.setters):
if WINDOWS:
fun()
self.execute(fun)

def test_process_apis_nsp(self):
def wrapper(fun):
try:
fun()
except psutil.NoSuchProcess:
pass

p = psutil.Process(self.spawn_testproc().pid)
p.terminate()
p.wait()
ns = process_namespace(p)
for fun, name in ns.iter(ns.getters + ns.setters + ns.killers):
if WINDOWS:
wrapper(fun)
self.execute(lambda: wrapper(fun))

def test_system_apis(self):
ns = system_namespace
for fun, name in ns.iter(ns.all):
if WINDOWS:
fun()
if MACOS and name == 'connections':
continue # raise AD
self.execute(fun)


if __name__ == '__main__':
from psutil.tests.runner import run_from_name
run_from_name(__file__)
22 changes: 12 additions & 10 deletions psutil/tests/test_testutils.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
from psutil.tests import system_namespace
from psutil.tests import tcp_socketpair
from psutil.tests import terminate
from psutil.tests import TestFdsLeak
from psutil.tests import TestMemoryLeak
from psutil.tests import unittest
from psutil.tests import unix_socketpair
Expand Down Expand Up @@ -358,17 +359,17 @@ def fun():
cnt['cnt'] += 1
cnt = {'cnt': 0}
self.execute(fun, times=1, warmup_times=10)
self.assertEqual(cnt['cnt'], 12)
self.assertEqual(cnt['cnt'], 11)
self.execute(fun, times=10, warmup_times=10)
self.assertEqual(cnt['cnt'], 33)
self.assertEqual(cnt['cnt'], 31)

@retry_on_failure()
def test_warmup_times(self):
def fun():
cnt['cnt'] += 1
cnt = {'cnt': 0}
self.execute(fun, times=1, warmup_times=10)
self.assertEqual(cnt['cnt'], 12)
self.assertEqual(cnt['cnt'], 11)

def test_param_err(self):
self.assertRaises(ValueError, self.execute, lambda: 0, times=0)
Expand All @@ -385,7 +386,7 @@ def fun():
times = 100
self.assertRaises(AssertionError, self.execute, fun, times=times,
warmup_times=10, retry_for=None)
self.assertEqual(len(ls), times + 11)
self.assertEqual(len(ls), times + 10)

@retry_on_failure(retries=20) # 2 secs
def test_leak_with_retry(self, ls=[]):
Expand All @@ -406,7 +407,7 @@ def fun():
ls = []
times = 100
self.execute(fun, times=times, warmup_times=0,
tolerance=200 * 1024 * 1024, check_fds=False)
tolerance=200 * 1024 * 1024)
self.assertEqual(len(ls), times)

@retry_on_failure()
Expand All @@ -424,16 +425,18 @@ def fun():
with self.assertRaises(AssertionError):
self.execute_w_exc(ZeroDivisionError, fun)

def test_unclosed_fds(self):

class TestFdsLeakClass(TestFdsLeak):

def test_unclosed_files(self):
def fun():
f = open(__file__)
self.addCleanup(f.close)
box.append(f)

box = []
self.assertRaisesRegex(
AssertionError, r"unclosed fd\(s\) or handle\(s\)",
self.execute, fun, times=5, warmup_times=5)
self.assertRaisesRegex(AssertionError, "unclosed", self.execute, fun)
self.assertEqual(len(box), 1)


class TestTestingUtils(PsutilTestCase):
Expand All @@ -447,7 +450,6 @@ def test_process_namespace(self):

def test_system_namespace(self):
ns = system_namespace()
ns.test()
fun = [x for x in ns.iter(ns.getters) if x[1] == 'net_if_addrs'][0][0]
self.assertEqual(fun(), psutil.net_if_addrs())

Expand Down
8 changes: 8 additions & 0 deletions scripts/internal/winmake.py
Original file line number Diff line number Diff line change
Expand Up @@ -464,6 +464,13 @@ def test_contracts():
sh("%s psutil\\tests\\test_contracts.py" % PYTHON)


def test_testutils():
"""Run test utilities tests"""
build()
test_setup()
sh("%s psutil\\tests\\test_testutils.py" % PYTHON)


def test_by_name(name):
"""Run test by name"""
build()
Expand Down Expand Up @@ -569,6 +576,7 @@ def main():
sp.add_parser('test-process', help="run process tests")
sp.add_parser('test-system', help="run system tests")
sp.add_parser('test-unicode', help="run unicode tests")
sp.add_parser('test-testutils', help="run test utils tests")
sp.add_parser('uninstall', help="uninstall psutil")
sp.add_parser('upload-wheels', help="upload wheel files on PyPI")
sp.add_parser('wheel', help="create wheel file")
Expand Down

0 comments on commit e903726

Please sign in to comment.