summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGiampaolo Rodola <g.rodola@gmail.com>2020-05-07 23:30:58 +0200
committerGiampaolo Rodola <g.rodola@gmail.com>2020-05-07 23:30:58 +0200
commite903726beba758a7e16543cfc61debafa4b52ab3 (patch)
treeeee85900de7239d5425ddd7e0ee4ff6d4b006fb1
parent67dfb10696dce46d62fb6c9723e78aa3ef8f99a4 (diff)
parente1ea2bccf8aea404dca0f79398f36f37217c45f6 (diff)
downloadpsutil-e903726beba758a7e16543cfc61debafa4b52ab3.tar.gz
merge from master
-rw-r--r--psutil/tests/__init__.py108
-rwxr-xr-xpsutil/tests/test_memory_leaks.py43
-rwxr-xr-xpsutil/tests/test_testutils.py22
-rwxr-xr-xscripts/internal/winmake.py8
4 files changed, 135 insertions, 46 deletions
diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py
index 28586fd3..41a791c1 100644
--- a/psutil/tests/__init__.py
+++ b/psutil/tests/__init__.py
@@ -89,7 +89,7 @@ __all__ = [
'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',
@@ -895,28 +895,47 @@ class PsutilTestCase(TestCase):
@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):
@@ -972,7 +991,7 @@ class TestMemoryLeak(PsutilTestCase):
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")
@@ -983,15 +1002,6 @@ class TestMemoryLeak(PsutilTestCase):
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)
@@ -1029,6 +1039,43 @@ class TestMemoryLeak(PsutilTestCase):
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"):
@@ -1238,17 +1285,6 @@ class system_namespace:
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
diff --git a/psutil/tests/test_memory_leaks.py b/psutil/tests/test_memory_leaks.py
index 9131e442..02e61aa0 100755
--- a/psutil/tests/test_memory_leaks.py
+++ b/psutil/tests/test_memory_leaks.py
@@ -50,10 +50,12 @@ from psutil.tests import skip_on_access_denied
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()
@@ -487,6 +489,47 @@ class TestModuleFunctionsLeaks(TestMemoryLeak):
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__)
diff --git a/psutil/tests/test_testutils.py b/psutil/tests/test_testutils.py
index 73782079..64bd8d3b 100755
--- a/psutil/tests/test_testutils.py
+++ b/psutil/tests/test_testutils.py
@@ -47,6 +47,7 @@ from psutil.tests import serialrun
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
@@ -358,9 +359,9 @@ class TestMemLeakClass(TestMemoryLeak):
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):
@@ -368,7 +369,7 @@ class TestMemLeakClass(TestMemoryLeak):
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)
@@ -385,7 +386,7 @@ class TestMemLeakClass(TestMemoryLeak):
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=[]):
@@ -406,7 +407,7 @@ class TestMemLeakClass(TestMemoryLeak):
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()
@@ -424,16 +425,18 @@ class TestMemLeakClass(TestMemoryLeak):
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):
@@ -447,7 +450,6 @@ class TestTestingUtils(PsutilTestCase):
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())
diff --git a/scripts/internal/winmake.py b/scripts/internal/winmake.py
index 4f1e65ed..de99c327 100755
--- a/scripts/internal/winmake.py
+++ b/scripts/internal/winmake.py
@@ -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()
@@ -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")