Mercurial > repos > guerler > springsuite
comparison planemo/lib/python3.7/site-packages/psutil/tests/__init__.py @ 1:56ad4e20f292 draft
"planemo upload commit 6eee67778febed82ddd413c3ca40b3183a3898f1"
author | guerler |
---|---|
date | Fri, 31 Jul 2020 00:32:28 -0400 |
parents | |
children |
comparison
equal
deleted
inserted
replaced
0:d30785e31577 | 1:56ad4e20f292 |
---|---|
1 # -*- coding: utf-8 -*- | |
2 | |
3 # Copyright (c) 2009, Giampaolo Rodola'. All rights reserved. | |
4 # Use of this source code is governed by a BSD-style license that can be | |
5 # found in the LICENSE file. | |
6 | |
7 """ | |
8 Test utilities. | |
9 """ | |
10 | |
11 from __future__ import print_function | |
12 import atexit | |
13 import contextlib | |
14 import ctypes | |
15 import errno | |
16 import functools | |
17 import gc | |
18 import inspect | |
19 import os | |
20 import random | |
21 import re | |
22 import select | |
23 import shutil | |
24 import signal | |
25 import socket | |
26 import stat | |
27 import subprocess | |
28 import sys | |
29 import tempfile | |
30 import textwrap | |
31 import threading | |
32 import time | |
33 import warnings | |
34 from socket import AF_INET | |
35 from socket import AF_INET6 | |
36 from socket import SOCK_STREAM | |
37 | |
38 import psutil | |
39 from psutil import AIX | |
40 from psutil import LINUX | |
41 from psutil import MACOS | |
42 from psutil import POSIX | |
43 from psutil import SUNOS | |
44 from psutil import WINDOWS | |
45 from psutil._common import bytes2human | |
46 from psutil._common import print_color | |
47 from psutil._common import supports_ipv6 | |
48 from psutil._compat import FileExistsError | |
49 from psutil._compat import FileNotFoundError | |
50 from psutil._compat import PY3 | |
51 from psutil._compat import range | |
52 from psutil._compat import super | |
53 from psutil._compat import u | |
54 from psutil._compat import unicode | |
55 from psutil._compat import which | |
56 | |
57 if PY3: | |
58 import unittest | |
59 else: | |
60 import unittest2 as unittest # requires "pip install unittest2" | |
61 | |
62 try: | |
63 from unittest import mock # py3 | |
64 except ImportError: | |
65 with warnings.catch_warnings(): | |
66 warnings.simplefilter("ignore") | |
67 import mock # NOQA - requires "pip install mock" | |
68 | |
69 if sys.version_info >= (3, 4): | |
70 import enum | |
71 else: | |
72 enum = None | |
73 | |
74 | |
75 __all__ = [ | |
76 # constants | |
77 'APPVEYOR', 'DEVNULL', 'GLOBAL_TIMEOUT', 'TOLERANCE_SYS_MEM', 'NO_RETRIES', | |
78 'PYPY', 'PYTHON_EXE', 'ROOT_DIR', 'SCRIPTS_DIR', 'TESTFN_PREFIX', | |
79 'UNICODE_SUFFIX', 'INVALID_UNICODE_SUFFIX', 'TRAVIS', 'CIRRUS', | |
80 'CI_TESTING', 'VALID_PROC_STATUSES', 'TOLERANCE_DISK_USAGE', 'IS_64BIT', | |
81 "HAS_CPU_AFFINITY", "HAS_CPU_FREQ", "HAS_ENVIRON", "HAS_PROC_IO_COUNTERS", | |
82 "HAS_IONICE", "HAS_MEMORY_MAPS", "HAS_PROC_CPU_NUM", "HAS_RLIMIT", | |
83 "HAS_SENSORS_BATTERY", "HAS_BATTERY", "HAS_SENSORS_FANS", | |
84 "HAS_SENSORS_TEMPERATURES", "HAS_MEMORY_FULL_INFO", | |
85 # subprocesses | |
86 'pyrun', 'terminate', 'reap_children', 'spawn_testproc', 'spawn_zombie', | |
87 'spawn_children_pair', | |
88 # threads | |
89 'ThreadTask' | |
90 # test utils | |
91 'unittest', 'skip_on_access_denied', 'skip_on_not_implemented', | |
92 'retry_on_failure', 'TestMemoryLeak', 'PsutilTestCase', | |
93 'process_namespace', 'system_namespace', 'print_sysinfo', | |
94 # install utils | |
95 'install_pip', 'install_test_deps', | |
96 # fs utils | |
97 'chdir', 'safe_rmpath', 'create_exe', 'decode_path', 'encode_path', | |
98 'get_testfn', | |
99 # os | |
100 'get_winver', 'get_kernel_version', | |
101 # sync primitives | |
102 'call_until', 'wait_for_pid', 'wait_for_file', | |
103 # network | |
104 'check_net_address', | |
105 'get_free_port', 'bind_socket', 'bind_unix_socket', 'tcp_socketpair', | |
106 'unix_socketpair', 'create_sockets', | |
107 # compat | |
108 'reload_module', 'import_module_by_path', | |
109 # others | |
110 'warn', 'copyload_shared_lib', 'is_namedtuple', | |
111 ] | |
112 | |
113 | |
114 # =================================================================== | |
115 # --- constants | |
116 # =================================================================== | |
117 | |
118 # --- platforms | |
119 | |
120 PYPY = '__pypy__' in sys.builtin_module_names | |
121 # whether we're running this test suite on a Continuous Integration service | |
122 TRAVIS = 'TRAVIS' in os.environ | |
123 APPVEYOR = 'APPVEYOR' in os.environ | |
124 CIRRUS = 'CIRRUS' in os.environ | |
125 GITHUB_WHEELS = 'CIBUILDWHEEL' in os.environ | |
126 CI_TESTING = TRAVIS or APPVEYOR or CIRRUS or GITHUB_WHEELS | |
127 # are we a 64 bit process? | |
128 IS_64BIT = sys.maxsize > 2 ** 32 | |
129 | |
130 | |
131 # --- configurable defaults | |
132 | |
133 # how many times retry_on_failure() decorator will retry | |
134 NO_RETRIES = 10 | |
135 # bytes tolerance for system-wide related tests | |
136 TOLERANCE_SYS_MEM = 5 * 1024 * 1024 # 5MB | |
137 TOLERANCE_DISK_USAGE = 10 * 1024 * 1024 # 10MB | |
138 # the timeout used in functions which have to wait | |
139 GLOBAL_TIMEOUT = 5 | |
140 # be more tolerant if we're on travis / appveyor in order to avoid | |
141 # false positives | |
142 if CI_TESTING: | |
143 NO_RETRIES *= 3 | |
144 GLOBAL_TIMEOUT *= 3 | |
145 TOLERANCE_SYS_MEM *= 3 | |
146 TOLERANCE_DISK_USAGE *= 3 | |
147 | |
148 # --- file names | |
149 | |
150 # Disambiguate TESTFN for parallel testing. | |
151 if os.name == 'java': | |
152 # Jython disallows @ in module names | |
153 TESTFN_PREFIX = '$psutil-%s-' % os.getpid() | |
154 else: | |
155 TESTFN_PREFIX = '@psutil-%s-' % os.getpid() | |
156 UNICODE_SUFFIX = u("-ƒőő") | |
157 # An invalid unicode string. | |
158 if PY3: | |
159 INVALID_UNICODE_SUFFIX = b"f\xc0\x80".decode('utf8', 'surrogateescape') | |
160 else: | |
161 INVALID_UNICODE_SUFFIX = "f\xc0\x80" | |
162 ASCII_FS = sys.getfilesystemencoding().lower() in ('ascii', 'us-ascii') | |
163 | |
164 # --- paths | |
165 | |
166 ROOT_DIR = os.path.realpath( | |
167 os.path.join(os.path.dirname(__file__), '..', '..')) | |
168 SCRIPTS_DIR = os.path.join(ROOT_DIR, 'scripts') | |
169 HERE = os.path.realpath(os.path.dirname(__file__)) | |
170 | |
171 # --- support | |
172 | |
173 HAS_CONNECTIONS_UNIX = POSIX and not SUNOS | |
174 HAS_CPU_AFFINITY = hasattr(psutil.Process, "cpu_affinity") | |
175 HAS_CPU_FREQ = hasattr(psutil, "cpu_freq") | |
176 HAS_GETLOADAVG = hasattr(psutil, "getloadavg") | |
177 HAS_ENVIRON = hasattr(psutil.Process, "environ") | |
178 HAS_IONICE = hasattr(psutil.Process, "ionice") | |
179 HAS_MEMORY_MAPS = hasattr(psutil.Process, "memory_maps") | |
180 HAS_NET_IO_COUNTERS = hasattr(psutil, "net_io_counters") | |
181 HAS_PROC_CPU_NUM = hasattr(psutil.Process, "cpu_num") | |
182 HAS_PROC_IO_COUNTERS = hasattr(psutil.Process, "io_counters") | |
183 HAS_RLIMIT = hasattr(psutil.Process, "rlimit") | |
184 HAS_SENSORS_BATTERY = hasattr(psutil, "sensors_battery") | |
185 try: | |
186 HAS_BATTERY = HAS_SENSORS_BATTERY and bool(psutil.sensors_battery()) | |
187 except Exception: | |
188 HAS_BATTERY = True | |
189 HAS_SENSORS_FANS = hasattr(psutil, "sensors_fans") | |
190 HAS_SENSORS_TEMPERATURES = hasattr(psutil, "sensors_temperatures") | |
191 HAS_THREADS = hasattr(psutil.Process, "threads") | |
192 SKIP_SYSCONS = (MACOS or AIX) and os.getuid() != 0 | |
193 | |
194 # --- misc | |
195 | |
196 | |
197 def _get_py_exe(): | |
198 def attempt(exe): | |
199 try: | |
200 subprocess.check_call( | |
201 [exe, "-V"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) | |
202 except Exception: | |
203 return None | |
204 else: | |
205 return exe | |
206 | |
207 if GITHUB_WHEELS: | |
208 if PYPY: | |
209 return which("pypy3") if PY3 else which("pypy") | |
210 else: | |
211 return which('python') | |
212 elif MACOS: | |
213 exe = \ | |
214 attempt(sys.executable) or \ | |
215 attempt(os.path.realpath(sys.executable)) or \ | |
216 attempt(which("python%s.%s" % sys.version_info[:2])) or \ | |
217 attempt(psutil.Process().exe()) | |
218 if not exe: | |
219 raise ValueError("can't find python exe real abspath") | |
220 return exe | |
221 else: | |
222 exe = os.path.realpath(sys.executable) | |
223 assert os.path.exists(exe), exe | |
224 return exe | |
225 | |
226 | |
227 PYTHON_EXE = _get_py_exe() | |
228 DEVNULL = open(os.devnull, 'r+') | |
229 atexit.register(DEVNULL.close) | |
230 | |
231 VALID_PROC_STATUSES = [getattr(psutil, x) for x in dir(psutil) | |
232 if x.startswith('STATUS_')] | |
233 AF_UNIX = getattr(socket, "AF_UNIX", object()) | |
234 | |
235 _subprocesses_started = set() | |
236 _pids_started = set() | |
237 | |
238 | |
239 # =================================================================== | |
240 # --- threads | |
241 # =================================================================== | |
242 | |
243 | |
244 class ThreadTask(threading.Thread): | |
245 """A thread task which does nothing expect staying alive.""" | |
246 | |
247 def __init__(self): | |
248 super().__init__() | |
249 self._running = False | |
250 self._interval = 0.001 | |
251 self._flag = threading.Event() | |
252 | |
253 def __repr__(self): | |
254 name = self.__class__.__name__ | |
255 return '<%s running=%s at %#x>' % (name, self._running, id(self)) | |
256 | |
257 def __enter__(self): | |
258 self.start() | |
259 return self | |
260 | |
261 def __exit__(self, *args, **kwargs): | |
262 self.stop() | |
263 | |
264 def start(self): | |
265 """Start thread and keep it running until an explicit | |
266 stop() request. Polls for shutdown every 'timeout' seconds. | |
267 """ | |
268 if self._running: | |
269 raise ValueError("already started") | |
270 threading.Thread.start(self) | |
271 self._flag.wait() | |
272 | |
273 def run(self): | |
274 self._running = True | |
275 self._flag.set() | |
276 while self._running: | |
277 time.sleep(self._interval) | |
278 | |
279 def stop(self): | |
280 """Stop thread execution and and waits until it is stopped.""" | |
281 if not self._running: | |
282 raise ValueError("already stopped") | |
283 self._running = False | |
284 self.join() | |
285 | |
286 | |
287 # =================================================================== | |
288 # --- subprocesses | |
289 # =================================================================== | |
290 | |
291 | |
292 def _reap_children_on_err(fun): | |
293 @functools.wraps(fun) | |
294 def wrapper(*args, **kwargs): | |
295 try: | |
296 return fun(*args, **kwargs) | |
297 except Exception: | |
298 reap_children() | |
299 raise | |
300 return wrapper | |
301 | |
302 | |
303 @_reap_children_on_err | |
304 def spawn_testproc(cmd=None, **kwds): | |
305 """Creates a python subprocess which does nothing for 60 secs and | |
306 return it as a subprocess.Popen instance. | |
307 If "cmd" is specified that is used instead of python. | |
308 By default stdin and stdout are redirected to /dev/null. | |
309 It also attemps to make sure the process is in a reasonably | |
310 initialized state. | |
311 The process is registered for cleanup on reap_children(). | |
312 """ | |
313 kwds.setdefault("stdin", DEVNULL) | |
314 kwds.setdefault("stdout", DEVNULL) | |
315 kwds.setdefault("cwd", os.getcwd()) | |
316 kwds.setdefault("env", os.environ) | |
317 if WINDOWS: | |
318 # Prevents the subprocess to open error dialogs. This will also | |
319 # cause stderr to be suppressed, which is suboptimal in order | |
320 # to debug broken tests. | |
321 CREATE_NO_WINDOW = 0x8000000 | |
322 kwds.setdefault("creationflags", CREATE_NO_WINDOW) | |
323 if cmd is None: | |
324 testfn = get_testfn() | |
325 try: | |
326 safe_rmpath(testfn) | |
327 pyline = "from time import sleep;" \ | |
328 "open(r'%s', 'w').close();" \ | |
329 "sleep(60);" % testfn | |
330 cmd = [PYTHON_EXE, "-c", pyline] | |
331 sproc = subprocess.Popen(cmd, **kwds) | |
332 _subprocesses_started.add(sproc) | |
333 wait_for_file(testfn, delete=True, empty=True) | |
334 finally: | |
335 safe_rmpath(testfn) | |
336 else: | |
337 sproc = subprocess.Popen(cmd, **kwds) | |
338 _subprocesses_started.add(sproc) | |
339 wait_for_pid(sproc.pid) | |
340 return sproc | |
341 | |
342 | |
343 @_reap_children_on_err | |
344 def spawn_children_pair(): | |
345 """Create a subprocess which creates another one as in: | |
346 A (us) -> B (child) -> C (grandchild). | |
347 Return a (child, grandchild) tuple. | |
348 The 2 processes are fully initialized and will live for 60 secs | |
349 and are registered for cleanup on reap_children(). | |
350 """ | |
351 tfile = None | |
352 testfn = get_testfn(dir=os.getcwd()) | |
353 try: | |
354 s = textwrap.dedent("""\ | |
355 import subprocess, os, sys, time | |
356 s = "import os, time;" | |
357 s += "f = open('%s', 'w');" | |
358 s += "f.write(str(os.getpid()));" | |
359 s += "f.close();" | |
360 s += "time.sleep(60);" | |
361 p = subprocess.Popen([r'%s', '-c', s]) | |
362 p.wait() | |
363 """ % (os.path.basename(testfn), PYTHON_EXE)) | |
364 # On Windows if we create a subprocess with CREATE_NO_WINDOW flag | |
365 # set (which is the default) a "conhost.exe" extra process will be | |
366 # spawned as a child. We don't want that. | |
367 if WINDOWS: | |
368 subp, tfile = pyrun(s, creationflags=0) | |
369 else: | |
370 subp, tfile = pyrun(s) | |
371 child = psutil.Process(subp.pid) | |
372 grandchild_pid = int(wait_for_file(testfn, delete=True, empty=False)) | |
373 _pids_started.add(grandchild_pid) | |
374 grandchild = psutil.Process(grandchild_pid) | |
375 return (child, grandchild) | |
376 finally: | |
377 safe_rmpath(testfn) | |
378 if tfile is not None: | |
379 safe_rmpath(tfile) | |
380 | |
381 | |
382 def spawn_zombie(): | |
383 """Create a zombie process and return a (parent, zombie) process tuple. | |
384 In order to kill the zombie parent must be terminate()d first, then | |
385 zombie must be wait()ed on. | |
386 """ | |
387 assert psutil.POSIX | |
388 unix_file = get_testfn() | |
389 src = textwrap.dedent("""\ | |
390 import os, sys, time, socket, contextlib | |
391 child_pid = os.fork() | |
392 if child_pid > 0: | |
393 time.sleep(3000) | |
394 else: | |
395 # this is the zombie process | |
396 s = socket.socket(socket.AF_UNIX) | |
397 with contextlib.closing(s): | |
398 s.connect('%s') | |
399 if sys.version_info < (3, ): | |
400 pid = str(os.getpid()) | |
401 else: | |
402 pid = bytes(str(os.getpid()), 'ascii') | |
403 s.sendall(pid) | |
404 """ % unix_file) | |
405 tfile = None | |
406 sock = bind_unix_socket(unix_file) | |
407 try: | |
408 sock.settimeout(GLOBAL_TIMEOUT) | |
409 parent, tfile = pyrun(src) | |
410 conn, _ = sock.accept() | |
411 try: | |
412 select.select([conn.fileno()], [], [], GLOBAL_TIMEOUT) | |
413 zpid = int(conn.recv(1024)) | |
414 _pids_started.add(zpid) | |
415 zombie = psutil.Process(zpid) | |
416 call_until(lambda: zombie.status(), "ret == psutil.STATUS_ZOMBIE") | |
417 return (parent, zombie) | |
418 finally: | |
419 conn.close() | |
420 finally: | |
421 sock.close() | |
422 safe_rmpath(unix_file) | |
423 if tfile is not None: | |
424 safe_rmpath(tfile) | |
425 | |
426 | |
427 @_reap_children_on_err | |
428 def pyrun(src, **kwds): | |
429 """Run python 'src' code string in a separate interpreter. | |
430 Returns a subprocess.Popen instance and the test file where the source | |
431 code was written. | |
432 """ | |
433 kwds.setdefault("stdout", None) | |
434 kwds.setdefault("stderr", None) | |
435 srcfile = get_testfn() | |
436 try: | |
437 with open(srcfile, 'wt') as f: | |
438 f.write(src) | |
439 subp = spawn_testproc([PYTHON_EXE, f.name], **kwds) | |
440 wait_for_pid(subp.pid) | |
441 return (subp, srcfile) | |
442 except Exception: | |
443 safe_rmpath(srcfile) | |
444 raise | |
445 | |
446 | |
447 @_reap_children_on_err | |
448 def sh(cmd, **kwds): | |
449 """run cmd in a subprocess and return its output. | |
450 raises RuntimeError on error. | |
451 """ | |
452 shell = True if isinstance(cmd, (str, unicode)) else False | |
453 # Prevents subprocess to open error dialogs in case of error. | |
454 flags = 0x8000000 if WINDOWS and shell else 0 | |
455 kwds.setdefault("shell", shell) | |
456 kwds.setdefault("stdout", subprocess.PIPE) | |
457 kwds.setdefault("stderr", subprocess.PIPE) | |
458 kwds.setdefault("universal_newlines", True) | |
459 kwds.setdefault("creationflags", flags) | |
460 p = subprocess.Popen(cmd, **kwds) | |
461 _subprocesses_started.add(p) | |
462 if PY3: | |
463 stdout, stderr = p.communicate(timeout=GLOBAL_TIMEOUT) | |
464 else: | |
465 stdout, stderr = p.communicate() | |
466 if p.returncode != 0: | |
467 raise RuntimeError(stderr) | |
468 if stderr: | |
469 warn(stderr) | |
470 if stdout.endswith('\n'): | |
471 stdout = stdout[:-1] | |
472 return stdout | |
473 | |
474 | |
475 def terminate(proc_or_pid, sig=signal.SIGTERM, wait_timeout=GLOBAL_TIMEOUT): | |
476 """Terminate a process and wait() for it. | |
477 Process can be a PID or an instance of psutil.Process(), | |
478 subprocess.Popen() or psutil.Popen(). | |
479 If it's a subprocess.Popen() or psutil.Popen() instance also closes | |
480 its stdin / stdout / stderr fds. | |
481 PID is wait()ed even if the process is already gone (kills zombies). | |
482 Does nothing if the process does not exist. | |
483 Return process exit status. | |
484 """ | |
485 if POSIX: | |
486 from psutil._psposix import wait_pid | |
487 | |
488 def wait(proc, timeout): | |
489 if isinstance(proc, subprocess.Popen) and not PY3: | |
490 proc.wait() | |
491 else: | |
492 proc.wait(timeout) | |
493 if WINDOWS and isinstance(proc, subprocess.Popen): | |
494 # Otherwise PID may still hang around. | |
495 try: | |
496 return psutil.Process(proc.pid).wait(timeout) | |
497 except psutil.NoSuchProcess: | |
498 pass | |
499 | |
500 def sendsig(proc, sig): | |
501 # XXX: otherwise the build hangs for some reason. | |
502 if MACOS and GITHUB_WHEELS: | |
503 sig = signal.SIGKILL | |
504 # If the process received SIGSTOP, SIGCONT is necessary first, | |
505 # otherwise SIGTERM won't work. | |
506 if POSIX and sig != signal.SIGKILL: | |
507 proc.send_signal(signal.SIGCONT) | |
508 proc.send_signal(sig) | |
509 | |
510 def term_subproc(proc, timeout): | |
511 try: | |
512 sendsig(proc, sig) | |
513 except OSError as err: | |
514 if WINDOWS and err.winerror == 6: # "invalid handle" | |
515 pass | |
516 elif err.errno != errno.ESRCH: | |
517 raise | |
518 return wait(proc, timeout) | |
519 | |
520 def term_psproc(proc, timeout): | |
521 try: | |
522 sendsig(proc, sig) | |
523 except psutil.NoSuchProcess: | |
524 pass | |
525 return wait(proc, timeout) | |
526 | |
527 def term_pid(pid, timeout): | |
528 try: | |
529 proc = psutil.Process(pid) | |
530 except psutil.NoSuchProcess: | |
531 # Needed to kill zombies. | |
532 if POSIX: | |
533 return wait_pid(pid, timeout) | |
534 else: | |
535 return term_psproc(proc, timeout) | |
536 | |
537 def flush_popen(proc): | |
538 if proc.stdout: | |
539 proc.stdout.close() | |
540 if proc.stderr: | |
541 proc.stderr.close() | |
542 # Flushing a BufferedWriter may raise an error. | |
543 if proc.stdin: | |
544 proc.stdin.close() | |
545 | |
546 p = proc_or_pid | |
547 try: | |
548 if isinstance(p, int): | |
549 return term_pid(p, wait_timeout) | |
550 elif isinstance(p, (psutil.Process, psutil.Popen)): | |
551 return term_psproc(p, wait_timeout) | |
552 elif isinstance(p, subprocess.Popen): | |
553 return term_subproc(p, wait_timeout) | |
554 else: | |
555 raise TypeError("wrong type %r" % p) | |
556 finally: | |
557 if isinstance(p, (subprocess.Popen, psutil.Popen)): | |
558 flush_popen(p) | |
559 pid = p if isinstance(p, int) else p.pid | |
560 assert not psutil.pid_exists(pid), pid | |
561 | |
562 | |
563 def reap_children(recursive=False): | |
564 """Terminate and wait() any subprocess started by this test suite | |
565 and any children currently running, ensuring that no processes stick | |
566 around to hog resources. | |
567 If resursive is True it also tries to terminate and wait() | |
568 all grandchildren started by this process. | |
569 """ | |
570 # Get the children here before terminating them, as in case of | |
571 # recursive=True we don't want to lose the intermediate reference | |
572 # pointing to the grandchildren. | |
573 children = psutil.Process().children(recursive=recursive) | |
574 | |
575 # Terminate subprocess.Popen. | |
576 while _subprocesses_started: | |
577 subp = _subprocesses_started.pop() | |
578 terminate(subp) | |
579 | |
580 # Collect started pids. | |
581 while _pids_started: | |
582 pid = _pids_started.pop() | |
583 terminate(pid) | |
584 | |
585 # Terminate children. | |
586 if children: | |
587 for p in children: | |
588 terminate(p, wait_timeout=None) | |
589 gone, alive = psutil.wait_procs(children, timeout=GLOBAL_TIMEOUT) | |
590 for p in alive: | |
591 warn("couldn't terminate process %r; attempting kill()" % p) | |
592 terminate(p, sig=signal.SIGKILL) | |
593 | |
594 | |
595 # =================================================================== | |
596 # --- OS | |
597 # =================================================================== | |
598 | |
599 | |
600 def get_kernel_version(): | |
601 """Return a tuple such as (2, 6, 36).""" | |
602 if not POSIX: | |
603 raise NotImplementedError("not POSIX") | |
604 s = "" | |
605 uname = os.uname()[2] | |
606 for c in uname: | |
607 if c.isdigit() or c == '.': | |
608 s += c | |
609 else: | |
610 break | |
611 if not s: | |
612 raise ValueError("can't parse %r" % uname) | |
613 minor = 0 | |
614 micro = 0 | |
615 nums = s.split('.') | |
616 major = int(nums[0]) | |
617 if len(nums) >= 2: | |
618 minor = int(nums[1]) | |
619 if len(nums) >= 3: | |
620 micro = int(nums[2]) | |
621 return (major, minor, micro) | |
622 | |
623 | |
624 def get_winver(): | |
625 if not WINDOWS: | |
626 raise NotImplementedError("not WINDOWS") | |
627 wv = sys.getwindowsversion() | |
628 if hasattr(wv, 'service_pack_major'): # python >= 2.7 | |
629 sp = wv.service_pack_major or 0 | |
630 else: | |
631 r = re.search(r"\s\d$", wv[4]) | |
632 if r: | |
633 sp = int(r.group(0)) | |
634 else: | |
635 sp = 0 | |
636 return (wv[0], wv[1], sp) | |
637 | |
638 | |
639 # =================================================================== | |
640 # --- sync primitives | |
641 # =================================================================== | |
642 | |
643 | |
644 class retry(object): | |
645 """A retry decorator.""" | |
646 | |
647 def __init__(self, | |
648 exception=Exception, | |
649 timeout=None, | |
650 retries=None, | |
651 interval=0.001, | |
652 logfun=None, | |
653 ): | |
654 if timeout and retries: | |
655 raise ValueError("timeout and retries args are mutually exclusive") | |
656 self.exception = exception | |
657 self.timeout = timeout | |
658 self.retries = retries | |
659 self.interval = interval | |
660 self.logfun = logfun | |
661 | |
662 def __iter__(self): | |
663 if self.timeout: | |
664 stop_at = time.time() + self.timeout | |
665 while time.time() < stop_at: | |
666 yield | |
667 elif self.retries: | |
668 for _ in range(self.retries): | |
669 yield | |
670 else: | |
671 while True: | |
672 yield | |
673 | |
674 def sleep(self): | |
675 if self.interval is not None: | |
676 time.sleep(self.interval) | |
677 | |
678 def __call__(self, fun): | |
679 @functools.wraps(fun) | |
680 def wrapper(*args, **kwargs): | |
681 exc = None | |
682 for _ in self: | |
683 try: | |
684 return fun(*args, **kwargs) | |
685 except self.exception as _: # NOQA | |
686 exc = _ | |
687 if self.logfun is not None: | |
688 self.logfun(exc) | |
689 self.sleep() | |
690 continue | |
691 if PY3: | |
692 raise exc | |
693 else: | |
694 raise | |
695 | |
696 # This way the user of the decorated function can change config | |
697 # parameters. | |
698 wrapper.decorator = self | |
699 return wrapper | |
700 | |
701 | |
702 @retry(exception=psutil.NoSuchProcess, logfun=None, timeout=GLOBAL_TIMEOUT, | |
703 interval=0.001) | |
704 def wait_for_pid(pid): | |
705 """Wait for pid to show up in the process list then return. | |
706 Used in the test suite to give time the sub process to initialize. | |
707 """ | |
708 psutil.Process(pid) | |
709 if WINDOWS: | |
710 # give it some more time to allow better initialization | |
711 time.sleep(0.01) | |
712 | |
713 | |
714 @retry(exception=(FileNotFoundError, AssertionError), logfun=None, | |
715 timeout=GLOBAL_TIMEOUT, interval=0.001) | |
716 def wait_for_file(fname, delete=True, empty=False): | |
717 """Wait for a file to be written on disk with some content.""" | |
718 with open(fname, "rb") as f: | |
719 data = f.read() | |
720 if not empty: | |
721 assert data | |
722 if delete: | |
723 safe_rmpath(fname) | |
724 return data | |
725 | |
726 | |
727 @retry(exception=AssertionError, logfun=None, timeout=GLOBAL_TIMEOUT, | |
728 interval=0.001) | |
729 def call_until(fun, expr): | |
730 """Keep calling function for timeout secs and exit if eval() | |
731 expression is True. | |
732 """ | |
733 ret = fun() | |
734 assert eval(expr) | |
735 return ret | |
736 | |
737 | |
738 # =================================================================== | |
739 # --- fs | |
740 # =================================================================== | |
741 | |
742 | |
743 def safe_rmpath(path): | |
744 "Convenience function for removing temporary test files or dirs" | |
745 def retry_fun(fun): | |
746 # On Windows it could happen that the file or directory has | |
747 # open handles or references preventing the delete operation | |
748 # to succeed immediately, so we retry for a while. See: | |
749 # https://bugs.python.org/issue33240 | |
750 stop_at = time.time() + GLOBAL_TIMEOUT | |
751 while time.time() < stop_at: | |
752 try: | |
753 return fun() | |
754 except FileNotFoundError: | |
755 pass | |
756 except WindowsError as _: | |
757 err = _ | |
758 warn("ignoring %s" % (str(err))) | |
759 time.sleep(0.01) | |
760 raise err | |
761 | |
762 try: | |
763 st = os.stat(path) | |
764 if stat.S_ISDIR(st.st_mode): | |
765 fun = functools.partial(shutil.rmtree, path) | |
766 else: | |
767 fun = functools.partial(os.remove, path) | |
768 if POSIX: | |
769 fun() | |
770 else: | |
771 retry_fun(fun) | |
772 except FileNotFoundError: | |
773 pass | |
774 | |
775 | |
776 def safe_mkdir(dir): | |
777 "Convenience function for creating a directory" | |
778 try: | |
779 os.mkdir(dir) | |
780 except FileExistsError: | |
781 pass | |
782 | |
783 | |
784 @contextlib.contextmanager | |
785 def chdir(dirname): | |
786 "Context manager which temporarily changes the current directory." | |
787 curdir = os.getcwd() | |
788 try: | |
789 os.chdir(dirname) | |
790 yield | |
791 finally: | |
792 os.chdir(curdir) | |
793 | |
794 | |
795 def create_exe(outpath, c_code=None): | |
796 """Creates an executable file in the given location.""" | |
797 assert not os.path.exists(outpath), outpath | |
798 if c_code: | |
799 if not which("gcc"): | |
800 raise ValueError("gcc is not installed") | |
801 if isinstance(c_code, bool): # c_code is True | |
802 c_code = textwrap.dedent( | |
803 """ | |
804 #include <unistd.h> | |
805 int main() { | |
806 pause(); | |
807 return 1; | |
808 } | |
809 """) | |
810 assert isinstance(c_code, str), c_code | |
811 with open(get_testfn(suffix='.c'), 'wt') as f: | |
812 f.write(c_code) | |
813 try: | |
814 subprocess.check_call(["gcc", f.name, "-o", outpath]) | |
815 finally: | |
816 safe_rmpath(f.name) | |
817 else: | |
818 # copy python executable | |
819 shutil.copyfile(PYTHON_EXE, outpath) | |
820 if POSIX: | |
821 st = os.stat(outpath) | |
822 os.chmod(outpath, st.st_mode | stat.S_IEXEC) | |
823 | |
824 | |
825 def get_testfn(suffix="", dir=None): | |
826 """Return an absolute pathname of a file or dir that did not | |
827 exist at the time this call is made. Also schedule it for safe | |
828 deletion at interpreter exit. It's technically racy but probably | |
829 not really due to the time variant. | |
830 """ | |
831 while True: | |
832 name = tempfile.mktemp(prefix=TESTFN_PREFIX, suffix=suffix, dir=dir) | |
833 if not os.path.exists(name): # also include dirs | |
834 return os.path.realpath(name) # needed for OSX | |
835 | |
836 | |
837 # =================================================================== | |
838 # --- testing | |
839 # =================================================================== | |
840 | |
841 | |
842 class TestCase(unittest.TestCase): | |
843 | |
844 # Print a full path representation of the single unit tests | |
845 # being run. | |
846 def __str__(self): | |
847 fqmod = self.__class__.__module__ | |
848 if not fqmod.startswith('psutil.'): | |
849 fqmod = 'psutil.tests.' + fqmod | |
850 return "%s.%s.%s" % ( | |
851 fqmod, self.__class__.__name__, self._testMethodName) | |
852 | |
853 # assertRaisesRegexp renamed to assertRaisesRegex in 3.3; | |
854 # add support for the new name. | |
855 if not hasattr(unittest.TestCase, 'assertRaisesRegex'): | |
856 assertRaisesRegex = unittest.TestCase.assertRaisesRegexp | |
857 | |
858 # ...otherwise multiprocessing.Pool complains | |
859 if not PY3: | |
860 def runTest(self): | |
861 pass | |
862 | |
863 | |
864 # monkey patch default unittest.TestCase | |
865 unittest.TestCase = TestCase | |
866 | |
867 | |
868 class PsutilTestCase(TestCase): | |
869 """Test class providing auto-cleanup wrappers on top of process | |
870 test utilities. | |
871 """ | |
872 | |
873 def get_testfn(self, suffix="", dir=None): | |
874 fname = get_testfn(suffix=suffix, dir=dir) | |
875 self.addCleanup(safe_rmpath, fname) | |
876 return fname | |
877 | |
878 def spawn_testproc(self, *args, **kwds): | |
879 sproc = spawn_testproc(*args, **kwds) | |
880 self.addCleanup(terminate, sproc) | |
881 return sproc | |
882 | |
883 def spawn_children_pair(self): | |
884 child1, child2 = spawn_children_pair() | |
885 self.addCleanup(terminate, child2) | |
886 self.addCleanup(terminate, child1) # executed first | |
887 return (child1, child2) | |
888 | |
889 def spawn_zombie(self): | |
890 parent, zombie = spawn_zombie() | |
891 self.addCleanup(terminate, zombie) | |
892 self.addCleanup(terminate, parent) # executed first | |
893 return (parent, zombie) | |
894 | |
895 def pyrun(self, *args, **kwds): | |
896 sproc, srcfile = pyrun(*args, **kwds) | |
897 self.addCleanup(safe_rmpath, srcfile) | |
898 self.addCleanup(terminate, sproc) # executed first | |
899 return sproc | |
900 | |
901 def assertProcessGone(self, proc): | |
902 self.assertRaises(psutil.NoSuchProcess, psutil.Process, proc.pid) | |
903 if isinstance(proc, (psutil.Process, psutil.Popen)): | |
904 assert not proc.is_running() | |
905 self.assertRaises(psutil.NoSuchProcess, proc.status) | |
906 proc.wait(timeout=0) # assert not raise TimeoutExpired | |
907 assert not psutil.pid_exists(proc.pid), proc.pid | |
908 self.assertNotIn(proc.pid, psutil.pids()) | |
909 | |
910 | |
911 @unittest.skipIf(PYPY, "unreliable on PYPY") | |
912 class TestMemoryLeak(PsutilTestCase): | |
913 """Test framework class for detecting function memory leaks, | |
914 typically functions implemented in C which forgot to free() memory | |
915 from the heap. It does so by checking whether the process memory | |
916 usage increased before and after calling the function many times. | |
917 | |
918 Note that this is hard (probably impossible) to do reliably, due | |
919 to how the OS handles memory, the GC and so on (memory can even | |
920 decrease!). In order to avoid false positives, in case of failure | |
921 (mem > 0) we retry the test for up to 5 times, increasing call | |
922 repetitions each time. If the memory keeps increasing then it's a | |
923 failure. | |
924 | |
925 If available (Linux, OSX, Windows), USS memory is used for comparison, | |
926 since it's supposed to be more precise, see: | |
927 https://gmpy.dev/blog/2016/real-process-memory-and-environ-in-python | |
928 If not, RSS memory is used. mallinfo() on Linux and _heapwalk() on | |
929 Windows may give even more precision, but at the moment are not | |
930 implemented. | |
931 | |
932 PyPy appears to be completely unstable for this framework, probably | |
933 because of its JIT, so tests on PYPY are skipped. | |
934 | |
935 Usage: | |
936 | |
937 class TestLeaks(psutil.tests.TestMemoryLeak): | |
938 | |
939 def test_fun(self): | |
940 self.execute(some_function) | |
941 """ | |
942 # Configurable class attrs. | |
943 times = 200 | |
944 warmup_times = 10 | |
945 tolerance = 0 # memory | |
946 retries = 10 if CI_TESTING else 5 | |
947 verbose = True | |
948 _thisproc = psutil.Process() | |
949 | |
950 def _get_mem(self): | |
951 # USS is the closest thing we have to "real" memory usage and it | |
952 # should be less likely to produce false positives. | |
953 mem = self._thisproc.memory_full_info() | |
954 return getattr(mem, "uss", mem.rss) | |
955 | |
956 def _get_num_fds(self): | |
957 if POSIX: | |
958 return self._thisproc.num_fds() | |
959 else: | |
960 return self._thisproc.num_handles() | |
961 | |
962 def _log(self, msg): | |
963 if self.verbose: | |
964 print_color(msg, color="yellow", file=sys.stderr) | |
965 | |
966 def _check_fds(self, fun): | |
967 """Makes sure num_fds() (POSIX) or num_handles() (Windows) does | |
968 not increase after calling a function. Used to discover forgotten | |
969 close(2) and CloseHandle syscalls. | |
970 """ | |
971 before = self._get_num_fds() | |
972 self.call(fun) | |
973 after = self._get_num_fds() | |
974 diff = after - before | |
975 if diff < 0: | |
976 raise self.fail("negative diff %r (gc probably collected a " | |
977 "resource from a previous test)" % diff) | |
978 if diff > 0: | |
979 type_ = "fd" if POSIX else "handle" | |
980 if diff > 1: | |
981 type_ += "s" | |
982 msg = "%s unclosed %s after calling %r" % (diff, type_, fun) | |
983 raise self.fail(msg) | |
984 | |
985 def _call_ntimes(self, fun, times): | |
986 """Get 2 distinct memory samples, before and after having | |
987 called fun repeadetly, and return the memory difference. | |
988 """ | |
989 gc.collect(generation=1) | |
990 mem1 = self._get_mem() | |
991 for x in range(times): | |
992 ret = self.call(fun) | |
993 del x, ret | |
994 gc.collect(generation=1) | |
995 mem2 = self._get_mem() | |
996 self.assertEqual(gc.garbage, []) | |
997 diff = mem2 - mem1 # can also be negative | |
998 return diff | |
999 | |
1000 def _check_mem(self, fun, times, warmup_times, retries, tolerance): | |
1001 messages = [] | |
1002 prev_mem = 0 | |
1003 increase = times | |
1004 for idx in range(1, retries + 1): | |
1005 mem = self._call_ntimes(fun, times) | |
1006 msg = "Run #%s: extra-mem=%s, per-call=%s, calls=%s" % ( | |
1007 idx, bytes2human(mem), bytes2human(mem / times), times) | |
1008 messages.append(msg) | |
1009 success = mem <= tolerance or mem <= prev_mem | |
1010 if success: | |
1011 if idx > 1: | |
1012 self._log(msg) | |
1013 return | |
1014 else: | |
1015 if idx == 1: | |
1016 print() # NOQA | |
1017 self._log(msg) | |
1018 times += increase | |
1019 prev_mem = mem | |
1020 raise self.fail(". ".join(messages)) | |
1021 | |
1022 # --- | |
1023 | |
1024 def call(self, fun): | |
1025 return fun() | |
1026 | |
1027 def execute(self, fun, times=None, warmup_times=None, retries=None, | |
1028 tolerance=None): | |
1029 """Test a callable.""" | |
1030 times = times if times is not None else self.times | |
1031 warmup_times = warmup_times if warmup_times is not None \ | |
1032 else self.warmup_times | |
1033 retries = retries if retries is not None else self.retries | |
1034 tolerance = tolerance if tolerance is not None else self.tolerance | |
1035 try: | |
1036 assert times >= 1, "times must be >= 1" | |
1037 assert warmup_times >= 0, "warmup_times must be >= 0" | |
1038 assert retries >= 0, "retries must be >= 0" | |
1039 assert tolerance >= 0, "tolerance must be >= 0" | |
1040 except AssertionError as err: | |
1041 raise ValueError(str(err)) | |
1042 | |
1043 self._call_ntimes(fun, warmup_times) # warm up | |
1044 self._check_fds(fun) | |
1045 self._check_mem(fun, times=times, warmup_times=warmup_times, | |
1046 retries=retries, tolerance=tolerance) | |
1047 | |
1048 def execute_w_exc(self, exc, fun, **kwargs): | |
1049 """Convenience method to test a callable while making sure it | |
1050 raises an exception on every call. | |
1051 """ | |
1052 def call(): | |
1053 self.assertRaises(exc, fun) | |
1054 | |
1055 self.execute(call, **kwargs) | |
1056 | |
1057 | |
1058 def print_sysinfo(): | |
1059 import collections | |
1060 import datetime | |
1061 import getpass | |
1062 import platform | |
1063 | |
1064 info = collections.OrderedDict() | |
1065 info['OS'] = platform.system() | |
1066 if psutil.OSX: | |
1067 info['version'] = str(platform.mac_ver()) | |
1068 elif psutil.WINDOWS: | |
1069 info['version'] = ' '.join(map(str, platform.win32_ver())) | |
1070 if hasattr(platform, 'win32_edition'): | |
1071 info['edition'] = platform.win32_edition() | |
1072 else: | |
1073 info['version'] = platform.version() | |
1074 if psutil.POSIX: | |
1075 info['kernel'] = '.'.join(map(str, get_kernel_version())) | |
1076 info['arch'] = ', '.join( | |
1077 list(platform.architecture()) + [platform.machine()]) | |
1078 info['hostname'] = platform.node() | |
1079 info['python'] = ', '.join([ | |
1080 platform.python_implementation(), | |
1081 platform.python_version(), | |
1082 platform.python_compiler()]) | |
1083 if psutil.POSIX: | |
1084 s = platform.libc_ver()[1] | |
1085 if s: | |
1086 info['glibc'] = s | |
1087 info['fs-encoding'] = sys.getfilesystemencoding() | |
1088 info['time'] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") | |
1089 info['user'] = getpass.getuser() | |
1090 info['pid'] = os.getpid() | |
1091 print("=" * 70) # NOQA | |
1092 for k, v in info.items(): | |
1093 print("%-14s %s" % (k + ':', v)) # NOQA | |
1094 print("=" * 70) # NOQA | |
1095 | |
1096 | |
1097 def _get_eligible_cpu(): | |
1098 p = psutil.Process() | |
1099 if hasattr(p, "cpu_num"): | |
1100 return p.cpu_num() | |
1101 elif hasattr(p, "cpu_affinity"): | |
1102 return p.cpu_affinity()[0] | |
1103 return 0 | |
1104 | |
1105 | |
1106 class process_namespace: | |
1107 """A container that lists all Process class method names + some | |
1108 reasonable parameters to be called with. Utility methods (parent(), | |
1109 children(), ...) are excluded. | |
1110 | |
1111 >>> ns = process_namespace(psutil.Process()) | |
1112 >>> for fun, name in ns.iter(ns.getters): | |
1113 ... fun() | |
1114 """ | |
1115 utils = [ | |
1116 ('cpu_percent', (), {}), | |
1117 ('memory_percent', (), {}), | |
1118 ] | |
1119 | |
1120 ignored = [ | |
1121 ('as_dict', (), {}), | |
1122 ('children', (), {'recursive': True}), | |
1123 ('is_running', (), {}), | |
1124 ('memory_info_ex', (), {}), | |
1125 ('oneshot', (), {}), | |
1126 ('parent', (), {}), | |
1127 ('parents', (), {}), | |
1128 ('pid', (), {}), | |
1129 ('wait', (0, ), {}), | |
1130 ] | |
1131 | |
1132 getters = [ | |
1133 ('cmdline', (), {}), | |
1134 ('connections', (), {'kind': 'all'}), | |
1135 ('cpu_times', (), {}), | |
1136 ('create_time', (), {}), | |
1137 ('cwd', (), {}), | |
1138 ('exe', (), {}), | |
1139 ('memory_full_info', (), {}), | |
1140 ('memory_info', (), {}), | |
1141 ('name', (), {}), | |
1142 ('nice', (), {}), | |
1143 ('num_ctx_switches', (), {}), | |
1144 ('num_threads', (), {}), | |
1145 ('open_files', (), {}), | |
1146 ('ppid', (), {}), | |
1147 ('status', (), {}), | |
1148 ('threads', (), {}), | |
1149 ('username', (), {}), | |
1150 ] | |
1151 if POSIX: | |
1152 getters += [('uids', (), {})] | |
1153 getters += [('gids', (), {})] | |
1154 getters += [('terminal', (), {})] | |
1155 getters += [('num_fds', (), {})] | |
1156 if HAS_PROC_IO_COUNTERS: | |
1157 getters += [('io_counters', (), {})] | |
1158 if HAS_IONICE: | |
1159 getters += [('ionice', (), {})] | |
1160 if HAS_RLIMIT: | |
1161 getters += [('rlimit', (psutil.RLIMIT_NOFILE, ), {})] | |
1162 if HAS_CPU_AFFINITY: | |
1163 getters += [('cpu_affinity', (), {})] | |
1164 if HAS_PROC_CPU_NUM: | |
1165 getters += [('cpu_num', (), {})] | |
1166 if HAS_ENVIRON: | |
1167 getters += [('environ', (), {})] | |
1168 if WINDOWS: | |
1169 getters += [('num_handles', (), {})] | |
1170 if HAS_MEMORY_MAPS: | |
1171 getters += [('memory_maps', (), {'grouped': False})] | |
1172 | |
1173 setters = [] | |
1174 if POSIX: | |
1175 setters += [('nice', (0, ), {})] | |
1176 else: | |
1177 setters += [('nice', (psutil.NORMAL_PRIORITY_CLASS, ), {})] | |
1178 if HAS_RLIMIT: | |
1179 setters += [('rlimit', (psutil.RLIMIT_NOFILE, (1024, 4096)), {})] | |
1180 if HAS_IONICE: | |
1181 if LINUX: | |
1182 setters += [('ionice', (psutil.IOPRIO_CLASS_NONE, 0), {})] | |
1183 else: | |
1184 setters += [('ionice', (psutil.IOPRIO_NORMAL, ), {})] | |
1185 if HAS_CPU_AFFINITY: | |
1186 setters += [('cpu_affinity', ([_get_eligible_cpu()], ), {})] | |
1187 | |
1188 killers = [ | |
1189 ('send_signal', (signal.SIGTERM, ), {}), | |
1190 ('suspend', (), {}), | |
1191 ('resume', (), {}), | |
1192 ('terminate', (), {}), | |
1193 ('kill', (), {}), | |
1194 ] | |
1195 if WINDOWS: | |
1196 killers += [('send_signal', (signal.CTRL_C_EVENT, ), {})] | |
1197 killers += [('send_signal', (signal.CTRL_BREAK_EVENT, ), {})] | |
1198 | |
1199 all = utils + getters + setters + killers | |
1200 | |
1201 def __init__(self, proc): | |
1202 self._proc = proc | |
1203 | |
1204 def iter(self, ls, clear_cache=True): | |
1205 """Given a list of tuples yields a set of (fun, fun_name) tuples | |
1206 in random order. | |
1207 """ | |
1208 ls = list(ls) | |
1209 random.shuffle(ls) | |
1210 for fun_name, args, kwds in ls: | |
1211 if clear_cache: | |
1212 self.clear_cache() | |
1213 fun = getattr(self._proc, fun_name) | |
1214 fun = functools.partial(fun, *args, **kwds) | |
1215 yield (fun, fun_name) | |
1216 | |
1217 def clear_cache(self): | |
1218 """Clear the cache of a Process instance.""" | |
1219 self._proc._init(self._proc.pid, _ignore_nsp=True) | |
1220 | |
1221 @classmethod | |
1222 def test_class_coverage(cls, test_class, ls): | |
1223 """Given a TestCase instance and a list of tuples checks that | |
1224 the class defines the required test method names. | |
1225 """ | |
1226 for fun_name, _, _ in ls: | |
1227 meth_name = 'test_' + fun_name | |
1228 if not hasattr(test_class, meth_name): | |
1229 msg = "%r class should define a '%s' method" % ( | |
1230 test_class.__class__.__name__, meth_name) | |
1231 raise AttributeError(msg) | |
1232 | |
1233 @classmethod | |
1234 def test(cls): | |
1235 this = set([x[0] for x in cls.all]) | |
1236 ignored = set([x[0] for x in cls.ignored]) | |
1237 klass = set([x for x in dir(psutil.Process) if x[0] != '_']) | |
1238 leftout = (this | ignored) ^ klass | |
1239 if leftout: | |
1240 raise ValueError("uncovered Process class names: %r" % leftout) | |
1241 | |
1242 | |
1243 class system_namespace: | |
1244 """A container that lists all the module-level, system-related APIs. | |
1245 Utilities such as cpu_percent() are excluded. Usage: | |
1246 | |
1247 >>> ns = system_namespace | |
1248 >>> for fun, name in ns.iter(ns.getters): | |
1249 ... fun() | |
1250 """ | |
1251 getters = [ | |
1252 ('boot_time', (), {}), | |
1253 ('cpu_count', (), {'logical': False}), | |
1254 ('cpu_count', (), {'logical': True}), | |
1255 ('cpu_stats', (), {}), | |
1256 ('cpu_times', (), {'percpu': False}), | |
1257 ('cpu_times', (), {'percpu': True}), | |
1258 ('disk_io_counters', (), {'perdisk': True}), | |
1259 ('disk_partitions', (), {'all': True}), | |
1260 ('disk_usage', (os.getcwd(), ), {}), | |
1261 ('net_connections', (), {'kind': 'all'}), | |
1262 ('net_if_addrs', (), {}), | |
1263 ('net_if_stats', (), {}), | |
1264 ('net_io_counters', (), {'pernic': True}), | |
1265 ('pid_exists', (os.getpid(), ), {}), | |
1266 ('pids', (), {}), | |
1267 ('swap_memory', (), {}), | |
1268 ('users', (), {}), | |
1269 ('virtual_memory', (), {}), | |
1270 ] | |
1271 if HAS_CPU_FREQ: | |
1272 getters += [('cpu_freq', (), {'percpu': True})] | |
1273 if HAS_GETLOADAVG: | |
1274 getters += [('getloadavg', (), {})] | |
1275 if HAS_SENSORS_TEMPERATURES: | |
1276 getters += [('sensors_temperatures', (), {})] | |
1277 if HAS_SENSORS_FANS: | |
1278 getters += [('sensors_fans', (), {})] | |
1279 if HAS_SENSORS_BATTERY: | |
1280 getters += [('sensors_battery', (), {})] | |
1281 if WINDOWS: | |
1282 getters += [('win_service_iter', (), {})] | |
1283 getters += [('win_service_get', ('alg', ), {})] | |
1284 | |
1285 ignored = [ | |
1286 ('process_iter', (), {}), | |
1287 ('wait_procs', ([psutil.Process()], ), {}), | |
1288 ('cpu_percent', (), {}), | |
1289 ('cpu_times_percent', (), {}), | |
1290 ] | |
1291 | |
1292 all = getters | |
1293 | |
1294 @staticmethod | |
1295 def iter(ls): | |
1296 """Given a list of tuples yields a set of (fun, fun_name) tuples | |
1297 in random order. | |
1298 """ | |
1299 ls = list(ls) | |
1300 random.shuffle(ls) | |
1301 for fun_name, args, kwds in ls: | |
1302 fun = getattr(psutil, fun_name) | |
1303 fun = functools.partial(fun, *args, **kwds) | |
1304 yield (fun, fun_name) | |
1305 | |
1306 test_class_coverage = process_namespace.test_class_coverage | |
1307 | |
1308 | |
1309 def serialrun(klass): | |
1310 """A decorator to mark a TestCase class. When running parallel tests, | |
1311 class' unit tests will be run serially (1 process). | |
1312 """ | |
1313 # assert issubclass(klass, unittest.TestCase), klass | |
1314 assert inspect.isclass(klass), klass | |
1315 klass._serialrun = True | |
1316 return klass | |
1317 | |
1318 | |
1319 def retry_on_failure(retries=NO_RETRIES): | |
1320 """Decorator which runs a test function and retries N times before | |
1321 actually failing. | |
1322 """ | |
1323 def logfun(exc): | |
1324 print("%r, retrying" % exc, file=sys.stderr) # NOQA | |
1325 | |
1326 return retry(exception=AssertionError, timeout=None, retries=retries, | |
1327 logfun=logfun) | |
1328 | |
1329 | |
1330 def skip_on_access_denied(only_if=None): | |
1331 """Decorator to Ignore AccessDenied exceptions.""" | |
1332 def decorator(fun): | |
1333 @functools.wraps(fun) | |
1334 def wrapper(*args, **kwargs): | |
1335 try: | |
1336 return fun(*args, **kwargs) | |
1337 except psutil.AccessDenied: | |
1338 if only_if is not None: | |
1339 if not only_if: | |
1340 raise | |
1341 raise unittest.SkipTest("raises AccessDenied") | |
1342 return wrapper | |
1343 return decorator | |
1344 | |
1345 | |
1346 def skip_on_not_implemented(only_if=None): | |
1347 """Decorator to Ignore NotImplementedError exceptions.""" | |
1348 def decorator(fun): | |
1349 @functools.wraps(fun) | |
1350 def wrapper(*args, **kwargs): | |
1351 try: | |
1352 return fun(*args, **kwargs) | |
1353 except NotImplementedError: | |
1354 if only_if is not None: | |
1355 if not only_if: | |
1356 raise | |
1357 msg = "%r was skipped because it raised NotImplementedError" \ | |
1358 % fun.__name__ | |
1359 raise unittest.SkipTest(msg) | |
1360 return wrapper | |
1361 return decorator | |
1362 | |
1363 | |
1364 # =================================================================== | |
1365 # --- network | |
1366 # =================================================================== | |
1367 | |
1368 | |
1369 def get_free_port(host='127.0.0.1'): | |
1370 """Return an unused TCP port.""" | |
1371 with contextlib.closing(socket.socket()) as sock: | |
1372 sock.bind((host, 0)) | |
1373 return sock.getsockname()[1] | |
1374 | |
1375 | |
1376 def bind_socket(family=AF_INET, type=SOCK_STREAM, addr=None): | |
1377 """Binds a generic socket.""" | |
1378 if addr is None and family in (AF_INET, AF_INET6): | |
1379 addr = ("", 0) | |
1380 sock = socket.socket(family, type) | |
1381 try: | |
1382 if os.name not in ('nt', 'cygwin'): | |
1383 sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) | |
1384 sock.bind(addr) | |
1385 if type == socket.SOCK_STREAM: | |
1386 sock.listen(5) | |
1387 return sock | |
1388 except Exception: | |
1389 sock.close() | |
1390 raise | |
1391 | |
1392 | |
1393 def bind_unix_socket(name, type=socket.SOCK_STREAM): | |
1394 """Bind a UNIX socket.""" | |
1395 assert psutil.POSIX | |
1396 assert not os.path.exists(name), name | |
1397 sock = socket.socket(socket.AF_UNIX, type) | |
1398 try: | |
1399 sock.bind(name) | |
1400 if type == socket.SOCK_STREAM: | |
1401 sock.listen(5) | |
1402 except Exception: | |
1403 sock.close() | |
1404 raise | |
1405 return sock | |
1406 | |
1407 | |
1408 def tcp_socketpair(family, addr=("", 0)): | |
1409 """Build a pair of TCP sockets connected to each other. | |
1410 Return a (server, client) tuple. | |
1411 """ | |
1412 with contextlib.closing(socket.socket(family, SOCK_STREAM)) as ll: | |
1413 ll.bind(addr) | |
1414 ll.listen(5) | |
1415 addr = ll.getsockname() | |
1416 c = socket.socket(family, SOCK_STREAM) | |
1417 try: | |
1418 c.connect(addr) | |
1419 caddr = c.getsockname() | |
1420 while True: | |
1421 a, addr = ll.accept() | |
1422 # check that we've got the correct client | |
1423 if addr == caddr: | |
1424 return (a, c) | |
1425 a.close() | |
1426 except OSError: | |
1427 c.close() | |
1428 raise | |
1429 | |
1430 | |
1431 def unix_socketpair(name): | |
1432 """Build a pair of UNIX sockets connected to each other through | |
1433 the same UNIX file name. | |
1434 Return a (server, client) tuple. | |
1435 """ | |
1436 assert psutil.POSIX | |
1437 server = client = None | |
1438 try: | |
1439 server = bind_unix_socket(name, type=socket.SOCK_STREAM) | |
1440 server.setblocking(0) | |
1441 client = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) | |
1442 client.setblocking(0) | |
1443 client.connect(name) | |
1444 # new = server.accept() | |
1445 except Exception: | |
1446 if server is not None: | |
1447 server.close() | |
1448 if client is not None: | |
1449 client.close() | |
1450 raise | |
1451 return (server, client) | |
1452 | |
1453 | |
1454 @contextlib.contextmanager | |
1455 def create_sockets(): | |
1456 """Open as many socket families / types as possible.""" | |
1457 socks = [] | |
1458 fname1 = fname2 = None | |
1459 try: | |
1460 socks.append(bind_socket(socket.AF_INET, socket.SOCK_STREAM)) | |
1461 socks.append(bind_socket(socket.AF_INET, socket.SOCK_DGRAM)) | |
1462 if supports_ipv6(): | |
1463 socks.append(bind_socket(socket.AF_INET6, socket.SOCK_STREAM)) | |
1464 socks.append(bind_socket(socket.AF_INET6, socket.SOCK_DGRAM)) | |
1465 if POSIX and HAS_CONNECTIONS_UNIX: | |
1466 fname1 = get_testfn() | |
1467 fname2 = get_testfn() | |
1468 s1, s2 = unix_socketpair(fname1) | |
1469 s3 = bind_unix_socket(fname2, type=socket.SOCK_DGRAM) | |
1470 for s in (s1, s2, s3): | |
1471 socks.append(s) | |
1472 yield socks | |
1473 finally: | |
1474 for s in socks: | |
1475 s.close() | |
1476 for fname in (fname1, fname2): | |
1477 if fname is not None: | |
1478 safe_rmpath(fname) | |
1479 | |
1480 | |
1481 def check_net_address(addr, family): | |
1482 """Check a net address validity. Supported families are IPv4, | |
1483 IPv6 and MAC addresses. | |
1484 """ | |
1485 import ipaddress # python >= 3.3 / requires "pip install ipaddress" | |
1486 if enum and PY3 and not PYPY: | |
1487 assert isinstance(family, enum.IntEnum), family | |
1488 if family == socket.AF_INET: | |
1489 octs = [int(x) for x in addr.split('.')] | |
1490 assert len(octs) == 4, addr | |
1491 for num in octs: | |
1492 assert 0 <= num <= 255, addr | |
1493 if not PY3: | |
1494 addr = unicode(addr) | |
1495 ipaddress.IPv4Address(addr) | |
1496 elif family == socket.AF_INET6: | |
1497 assert isinstance(addr, str), addr | |
1498 if not PY3: | |
1499 addr = unicode(addr) | |
1500 ipaddress.IPv6Address(addr) | |
1501 elif family == psutil.AF_LINK: | |
1502 assert re.match(r'([a-fA-F0-9]{2}[:|\-]?){6}', addr) is not None, addr | |
1503 else: | |
1504 raise ValueError("unknown family %r", family) | |
1505 | |
1506 | |
1507 # =================================================================== | |
1508 # --- compatibility | |
1509 # =================================================================== | |
1510 | |
1511 | |
1512 def reload_module(module): | |
1513 """Backport of importlib.reload of Python 3.3+.""" | |
1514 try: | |
1515 import importlib | |
1516 if not hasattr(importlib, 'reload'): # python <=3.3 | |
1517 raise ImportError | |
1518 except ImportError: | |
1519 import imp | |
1520 return imp.reload(module) | |
1521 else: | |
1522 return importlib.reload(module) | |
1523 | |
1524 | |
1525 def import_module_by_path(path): | |
1526 name = os.path.splitext(os.path.basename(path))[0] | |
1527 if sys.version_info[0] == 2: | |
1528 import imp | |
1529 return imp.load_source(name, path) | |
1530 elif sys.version_info[:2] <= (3, 4): | |
1531 from importlib.machinery import SourceFileLoader | |
1532 return SourceFileLoader(name, path).load_module() | |
1533 else: | |
1534 import importlib.util | |
1535 spec = importlib.util.spec_from_file_location(name, path) | |
1536 mod = importlib.util.module_from_spec(spec) | |
1537 spec.loader.exec_module(mod) | |
1538 return mod | |
1539 | |
1540 | |
1541 # =================================================================== | |
1542 # --- others | |
1543 # =================================================================== | |
1544 | |
1545 | |
1546 def warn(msg): | |
1547 """Raise a warning msg.""" | |
1548 warnings.warn(msg, UserWarning) | |
1549 | |
1550 | |
1551 def is_namedtuple(x): | |
1552 """Check if object is an instance of namedtuple.""" | |
1553 t = type(x) | |
1554 b = t.__bases__ | |
1555 if len(b) != 1 or b[0] != tuple: | |
1556 return False | |
1557 f = getattr(t, '_fields', None) | |
1558 if not isinstance(f, tuple): | |
1559 return False | |
1560 return all(type(n) == str for n in f) | |
1561 | |
1562 | |
1563 if POSIX: | |
1564 @contextlib.contextmanager | |
1565 def copyload_shared_lib(suffix=""): | |
1566 """Ctx manager which picks up a random shared CO lib used | |
1567 by this process, copies it in another location and loads it | |
1568 in memory via ctypes. Return the new absolutized path. | |
1569 """ | |
1570 exe = 'pypy' if PYPY else 'python' | |
1571 ext = ".so" | |
1572 dst = get_testfn(suffix=suffix + ext) | |
1573 libs = [x.path for x in psutil.Process().memory_maps() if | |
1574 os.path.splitext(x.path)[1] == ext and | |
1575 exe in x.path.lower()] | |
1576 src = random.choice(libs) | |
1577 shutil.copyfile(src, dst) | |
1578 try: | |
1579 ctypes.CDLL(dst) | |
1580 yield dst | |
1581 finally: | |
1582 safe_rmpath(dst) | |
1583 else: | |
1584 @contextlib.contextmanager | |
1585 def copyload_shared_lib(suffix=""): | |
1586 """Ctx manager which picks up a random shared DLL lib used | |
1587 by this process, copies it in another location and loads it | |
1588 in memory via ctypes. | |
1589 Return the new absolutized, normcased path. | |
1590 """ | |
1591 from ctypes import wintypes | |
1592 from ctypes import WinError | |
1593 ext = ".dll" | |
1594 dst = get_testfn(suffix=suffix + ext) | |
1595 libs = [x.path for x in psutil.Process().memory_maps() if | |
1596 x.path.lower().endswith(ext) and | |
1597 'python' in os.path.basename(x.path).lower() and | |
1598 'wow64' not in x.path.lower()] | |
1599 if PYPY and not libs: | |
1600 libs = [x.path for x in psutil.Process().memory_maps() if | |
1601 'pypy' in os.path.basename(x.path).lower()] | |
1602 src = random.choice(libs) | |
1603 shutil.copyfile(src, dst) | |
1604 cfile = None | |
1605 try: | |
1606 cfile = ctypes.WinDLL(dst) | |
1607 yield dst | |
1608 finally: | |
1609 # Work around OverflowError: | |
1610 # - https://ci.appveyor.com/project/giampaolo/psutil/build/1207/ | |
1611 # job/o53330pbnri9bcw7 | |
1612 # - http://bugs.python.org/issue30286 | |
1613 # - http://stackoverflow.com/questions/23522055 | |
1614 if cfile is not None: | |
1615 FreeLibrary = ctypes.windll.kernel32.FreeLibrary | |
1616 FreeLibrary.argtypes = [wintypes.HMODULE] | |
1617 ret = FreeLibrary(cfile._handle) | |
1618 if ret == 0: | |
1619 WinError() | |
1620 safe_rmpath(dst) | |
1621 | |
1622 | |
1623 # =================================================================== | |
1624 # --- Exit funs (first is executed last) | |
1625 # =================================================================== | |
1626 | |
1627 | |
1628 # this is executed first | |
1629 @atexit.register | |
1630 def cleanup_test_procs(): | |
1631 reap_children(recursive=True) | |
1632 | |
1633 | |
1634 # atexit module does not execute exit functions in case of SIGTERM, which | |
1635 # gets sent to test subprocesses, which is a problem if they import this | |
1636 # module. With this it will. See: | |
1637 # https://gmpy.dev/blog/2016/how-to-always-execute-exit-functions-in-python | |
1638 if POSIX: | |
1639 signal.signal(signal.SIGTERM, lambda sig, frame: sys.exit(sig)) |