diff --git a/psutil/tests/__init__.py b/psutil/tests/__init__.py index 28586fd3d..41a791c1b 100644 --- a/psutil/tests/__init__.py +++ b/psutil/tests/__init__.py @@ -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', @@ -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): @@ -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") @@ -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) @@ -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"): @@ -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 diff --git a/psutil/tests/test_memory_leaks.py b/psutil/tests/test_memory_leaks.py index 9131e4422..02e61aa00 100755 --- a/psutil/tests/test_memory_leaks.py +++ b/psutil/tests/test_memory_leaks.py @@ -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() @@ -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__) diff --git a/psutil/tests/test_testutils.py b/psutil/tests/test_testutils.py index 737820798..64bd8d3b2 100755 --- a/psutil/tests/test_testutils.py +++ b/psutil/tests/test_testutils.py @@ -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 @@ -358,9 +359,9 @@ 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): @@ -368,7 +369,7 @@ 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) @@ -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=[]): @@ -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() @@ -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): @@ -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()) diff --git a/scripts/internal/winmake.py b/scripts/internal/winmake.py index 4f1e65ed0..de99c3276 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")