| #!/usr/bin/env python3 |
| |
| # Copyright (c) 2009, Giampaolo Rodola'. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| """Linux specific tests.""" |
| |
| import collections |
| import contextlib |
| import errno |
| import glob |
| import io |
| import os |
| import platform |
| import re |
| import shutil |
| import socket |
| import struct |
| import textwrap |
| import time |
| import warnings |
| from unittest import mock |
| |
| import psutil |
| from psutil import LINUX |
| |
| from . import AARCH64 |
| from . import GITHUB_ACTIONS |
| from . import GLOBAL_TIMEOUT |
| from . import HAS_BATTERY |
| from . import HAS_CPU_FREQ |
| from . import HAS_PROC_RLIMIT |
| from . import RISCV64 |
| from . import TOLERANCE_DISK_USAGE |
| from . import TOLERANCE_SYS_MEM |
| from . import PsutilTestCase |
| from . import ThreadTask |
| from . import call_until |
| from . import pytest |
| from . import reload_module |
| from . import retry_on_failure |
| from . import safe_rmpath |
| from . import sh |
| from . import skip_on_not_implemented |
| |
| if LINUX: |
| from psutil._pslinux import CLOCK_TICKS |
| from psutil._pslinux import RootFsDeviceFinder |
| from psutil._pslinux import calculate_avail_vmem |
| from psutil._pslinux import open_binary |
| |
| |
| SIOCGIFADDR = 0x8915 |
| SIOCGIFHWADDR = 0x8927 |
| SIOCGIFNETMASK = 0x891B |
| SIOCGIFBRDADDR = 0x8919 |
| if LINUX: |
| SECTOR_SIZE = 512 |
| |
| |
| @pytest.mark.skipif(not LINUX, reason="LINUX only") |
| class LinuxTestCase(PsutilTestCase): |
| pass |
| |
| |
| # ===================================================================== |
| # --- utils |
| # ===================================================================== |
| |
| |
| def get_ipv4_address(ifname): |
| import fcntl |
| |
| ifname = bytes(ifname[:15], "ascii") |
| with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s: |
| return socket.inet_ntoa( |
| fcntl.ioctl(s.fileno(), SIOCGIFADDR, struct.pack('256s', ifname))[ |
| 20:24 |
| ] |
| ) |
| |
| |
| def get_ipv4_netmask(ifname): |
| import fcntl |
| |
| ifname = bytes(ifname[:15], "ascii") |
| with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s: |
| return socket.inet_ntoa( |
| fcntl.ioctl( |
| s.fileno(), SIOCGIFNETMASK, struct.pack('256s', ifname) |
| )[20:24] |
| ) |
| |
| |
| def get_ipv4_broadcast(ifname): |
| import fcntl |
| |
| ifname = bytes(ifname[:15], "ascii") |
| with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s: |
| return socket.inet_ntoa( |
| fcntl.ioctl( |
| s.fileno(), SIOCGIFBRDADDR, struct.pack('256s', ifname) |
| )[20:24] |
| ) |
| |
| |
| def get_ipv6_addresses(ifname): |
| with open("/proc/net/if_inet6") as f: |
| all_fields = [] |
| for line in f: |
| fields = line.split() |
| if fields[-1] == ifname: |
| all_fields.append(fields) |
| |
| if len(all_fields) == 0: |
| raise ValueError(f"could not find interface {ifname!r}") |
| |
| for i in range(len(all_fields)): |
| unformatted = all_fields[i][0] |
| groups = [ |
| unformatted[j : j + 4] for j in range(0, len(unformatted), 4) |
| ] |
| formatted = ":".join(groups) |
| packed = socket.inet_pton(socket.AF_INET6, formatted) |
| all_fields[i] = socket.inet_ntop(socket.AF_INET6, packed) |
| return all_fields |
| |
| |
| def get_mac_address(ifname): |
| import fcntl |
| |
| ifname = bytes(ifname[:15], "ascii") |
| with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s: |
| info = fcntl.ioctl( |
| s.fileno(), SIOCGIFHWADDR, struct.pack('256s', ifname) |
| ) |
| return "".join([f"{char:02x}:" for char in info[18:24]])[:-1] |
| |
| |
| def free_swap(): |
| """Parse 'free' cmd and return swap memory's s total, used and free |
| values. |
| """ |
| out = sh(["free", "-b"], env={"LANG": "C.UTF-8"}) |
| lines = out.split('\n') |
| for line in lines: |
| if line.startswith('Swap'): |
| _, total, used, free = line.split() |
| nt = collections.namedtuple('free', 'total used free') |
| return nt(int(total), int(used), int(free)) |
| raise ValueError(f"can't find 'Swap' in 'free' output:\n{out}") |
| |
| |
| def free_physmem(): |
| """Parse 'free' cmd and return physical memory's total, used |
| and free values. |
| """ |
| # Note: free can have 2 different formats, invalidating 'shared' |
| # and 'cached' memory which may have different positions so we |
| # do not return them. |
| # https://github.com/giampaolo/psutil/issues/538#issuecomment-57059946 |
| out = sh(["free", "-b"], env={"LANG": "C.UTF-8"}) |
| lines = out.split('\n') |
| for line in lines: |
| if line.startswith('Mem'): |
| total, used, free, shared = (int(x) for x in line.split()[1:5]) |
| nt = collections.namedtuple( |
| 'free', 'total used free shared output' |
| ) |
| return nt(total, used, free, shared, out) |
| raise ValueError(f"can't find 'Mem' in 'free' output:\n{out}") |
| |
| |
| def vmstat(stat): |
| out = sh(["vmstat", "-s"], env={"LANG": "C.UTF-8"}) |
| for line in out.split("\n"): |
| line = line.strip() |
| if stat in line: |
| return int(line.split(' ')[0]) |
| raise ValueError(f"can't find {stat!r} in 'vmstat' output") |
| |
| |
| def get_free_version_info(): |
| out = sh(["free", "-V"]).strip() |
| if 'UNKNOWN' in out: |
| return pytest.skip("can't determine free version") |
| return tuple(map(int, re.findall(r'\d+', out.split()[-1]))) |
| |
| |
| @contextlib.contextmanager |
| def mock_open_content(pairs): |
| """Mock open() builtin and forces it to return a certain content |
| for a given path. `pairs` is a {"path": "content", ...} dict. |
| """ |
| |
| def open_mock(name, *args, **kwargs): |
| if name in pairs: |
| content = pairs[name] |
| if isinstance(content, str): |
| return io.StringIO(content) |
| else: |
| return io.BytesIO(content) |
| else: |
| return orig_open(name, *args, **kwargs) |
| |
| orig_open = open |
| with mock.patch("builtins.open", create=True, side_effect=open_mock) as m: |
| yield m |
| |
| |
| @contextlib.contextmanager |
| def mock_open_exception(for_path, exc): |
| """Mock open() builtin and raises `exc` if the path being opened |
| matches `for_path`. |
| """ |
| |
| def open_mock(name, *args, **kwargs): |
| if name == for_path: |
| raise exc |
| return orig_open(name, *args, **kwargs) |
| |
| orig_open = open |
| with mock.patch("builtins.open", create=True, side_effect=open_mock) as m: |
| yield m |
| |
| |
| # ===================================================================== |
| # --- system virtual memory |
| # ===================================================================== |
| |
| |
| class TestVirtualMemoryAgainstFree(LinuxTestCase): |
| def test_total(self): |
| cli_value = free_physmem().total |
| psutil_value = psutil.virtual_memory().total |
| assert cli_value == psutil_value |
| |
| @retry_on_failure() |
| def test_used(self): |
| # Older versions of procps used slab memory to calculate used memory. |
| # This got changed in: |
| # https://gitlab.com/procps-ng/procps/-/commit/05d751c4f07 |
| # Newer versions of procps (>=4.0.1) are using yet another way to |
| # compute used memory. |
| # https://gitlab.com/procps-ng/procps/-/commit/2184e90d2e7 |
| if get_free_version_info() < (4, 0, 1): |
| return pytest.skip("free version too old") |
| cli_value = free_physmem().used |
| psutil_value = psutil.virtual_memory().used |
| assert abs(cli_value - psutil_value) < TOLERANCE_SYS_MEM |
| |
| @retry_on_failure() |
| def test_free(self): |
| cli_value = free_physmem().free |
| psutil_value = psutil.virtual_memory().free |
| assert abs(cli_value - psutil_value) < TOLERANCE_SYS_MEM |
| |
| @retry_on_failure() |
| def test_shared(self): |
| free = free_physmem() |
| free_value = free.shared |
| if free_value == 0: |
| return pytest.skip("free does not support 'shared' column") |
| psutil_value = psutil.virtual_memory().shared |
| assert ( |
| abs(free_value - psutil_value) < TOLERANCE_SYS_MEM |
| ), f"{free_value} {psutil_value} \n{free.output}" |
| |
| @retry_on_failure() |
| def test_available(self): |
| # "free" output format has changed at some point: |
| # https://github.com/giampaolo/psutil/issues/538#issuecomment-147192098 |
| out = sh(["free", "-b"]) |
| lines = out.split('\n') |
| if 'available' not in lines[0]: |
| return pytest.skip("free does not support 'available' column") |
| free_value = int(lines[1].split()[-1]) |
| psutil_value = psutil.virtual_memory().available |
| assert abs(free_value - psutil_value) < TOLERANCE_SYS_MEM |
| |
| |
| class TestVirtualMemoryAgainstVmstat(LinuxTestCase): |
| def test_total(self): |
| vmstat_value = vmstat('total memory') * 1024 |
| psutil_value = psutil.virtual_memory().total |
| assert abs(vmstat_value - psutil_value) < TOLERANCE_SYS_MEM |
| |
| @retry_on_failure() |
| def test_used(self): |
| # Older versions of procps used slab memory to calculate used memory. |
| # This got changed in: |
| # https://gitlab.com/procps-ng/procps/-/commit/05d751c4f07 |
| # Newer versions of procps (>=4.0.1) are using yet another way to |
| # compute used memory. |
| # https://gitlab.com/procps-ng/procps/-/commit/2184e90d2e7 |
| if get_free_version_info() < (4, 0, 1): |
| return pytest.skip("free version too old") |
| vmstat_value = vmstat('used memory') * 1024 |
| psutil_value = psutil.virtual_memory().used |
| assert abs(vmstat_value - psutil_value) < TOLERANCE_SYS_MEM |
| |
| @retry_on_failure() |
| def test_free(self): |
| vmstat_value = vmstat('free memory') * 1024 |
| psutil_value = psutil.virtual_memory().free |
| assert abs(vmstat_value - psutil_value) < TOLERANCE_SYS_MEM |
| |
| @retry_on_failure() |
| def test_buffers(self): |
| vmstat_value = vmstat('buffer memory') * 1024 |
| psutil_value = psutil.virtual_memory().buffers |
| assert abs(vmstat_value - psutil_value) < TOLERANCE_SYS_MEM |
| |
| @retry_on_failure() |
| def test_active(self): |
| vmstat_value = vmstat('active memory') * 1024 |
| psutil_value = psutil.virtual_memory().active |
| assert abs(vmstat_value - psutil_value) < TOLERANCE_SYS_MEM |
| |
| @retry_on_failure() |
| def test_inactive(self): |
| vmstat_value = vmstat('inactive memory') * 1024 |
| psutil_value = psutil.virtual_memory().inactive |
| assert abs(vmstat_value - psutil_value) < TOLERANCE_SYS_MEM |
| |
| |
| class TestVirtualMemoryAgainstMeminfo(LinuxTestCase): |
| @staticmethod |
| def read_meminfo(): |
| mems = {} |
| with open("/proc/meminfo") as f: |
| for line in f: |
| fields = line.split() |
| if len(fields) >= 2: |
| mems[fields[0]] = int(fields[1]) * 1024 |
| return mems |
| |
| @retry_on_failure() |
| def test_buffers(self): |
| proc_value = self.read_meminfo()["Buffers:"] |
| psutil_value = psutil.virtual_memory().buffers |
| assert abs(psutil_value - proc_value) < TOLERANCE_SYS_MEM |
| |
| @retry_on_failure() |
| def test_cached(self): |
| # psutil cached = Cached + SReclaimable |
| mems = self.read_meminfo() |
| proc_value = mems["Cached:"] + mems.get("SReclaimable:", 0) |
| psutil_value = psutil.virtual_memory().cached |
| assert abs(psutil_value - proc_value) < TOLERANCE_SYS_MEM |
| |
| |
| class TestVirtualMemoryMocks(LinuxTestCase): |
| def test_warnings_on_misses(self): |
| # Emulate a case where /proc/meminfo provides few info. |
| # psutil is supposed to set the missing fields to 0 and |
| # raise a warning. |
| content = textwrap.dedent("""\ |
| Active(anon): 6145416 kB |
| Active(file): 2950064 kB |
| Inactive(anon): 574764 kB |
| Inactive(file): 1567648 kB |
| MemAvailable: -1 kB |
| MemFree: 2057400 kB |
| MemTotal: 16325648 kB |
| SReclaimable: 346648 kB |
| """).encode() |
| with mock_open_content({'/proc/meminfo': content}) as m: |
| with warnings.catch_warnings(record=True) as ws: |
| warnings.simplefilter("always") |
| ret = psutil.virtual_memory() |
| assert m.called |
| assert len(ws) == 1 |
| w = ws[0] |
| assert "memory stats couldn't be determined" in str(w.message) |
| assert "cached" in str(w.message) |
| assert "shared" in str(w.message) |
| assert "active" in str(w.message) |
| assert "inactive" in str(w.message) |
| assert "buffers" in str(w.message) |
| assert "available" in str(w.message) |
| assert ret.cached == 0 |
| assert ret.active == 0 |
| assert ret.inactive == 0 |
| assert ret.shared == 0 |
| assert ret.buffers == 0 |
| assert ret.available == 0 |
| assert ret.slab == 0 |
| |
| @retry_on_failure() |
| def test_avail_old_percent(self): |
| # Make sure that our calculation of avail mem for old kernels |
| # is off by max 15%. |
| mems = {} |
| with open_binary('/proc/meminfo') as f: |
| for line in f: |
| fields = line.split() |
| mems[fields[0]] = int(fields[1]) * 1024 |
| |
| a = calculate_avail_vmem(mems) |
| if b'MemAvailable:' in mems: |
| b = mems[b'MemAvailable:'] |
| diff_percent = abs(a - b) / a * 100 |
| assert diff_percent < 15 |
| |
| def test_avail_old_comes_from_kernel(self): |
| # Make sure "MemAvailable:" coluimn is used instead of relying |
| # on our internal algorithm to calculate avail mem. |
| content = textwrap.dedent("""\ |
| Active: 9444728 kB |
| Active(anon): 6145416 kB |
| Active(file): 2950064 kB |
| Buffers: 287952 kB |
| Cached: 4818144 kB |
| Inactive(file): 1578132 kB |
| Inactive(anon): 574764 kB |
| Inactive(file): 1567648 kB |
| MemAvailable: 6574984 kB |
| MemFree: 2057400 kB |
| MemTotal: 16325648 kB |
| Shmem: 577588 kB |
| SReclaimable: 346648 kB |
| """).encode() |
| with mock_open_content({'/proc/meminfo': content}) as m: |
| with warnings.catch_warnings(record=True) as ws: |
| ret = psutil.virtual_memory() |
| assert m.called |
| assert ret.available == 6574984 * 1024 |
| w = ws[0] |
| assert "inactive memory stats couldn't be determined" in str( |
| w.message |
| ) |
| |
| def test_avail_old_missing_fields(self): |
| # Remove Active(file), Inactive(file) and SReclaimable |
| # from /proc/meminfo and make sure the fallback is used |
| # (free + cached), |
| content = textwrap.dedent("""\ |
| Active: 9444728 kB |
| Active(anon): 6145416 kB |
| Buffers: 287952 kB |
| Cached: 4818144 kB |
| Inactive(file): 1578132 kB |
| Inactive(anon): 574764 kB |
| MemFree: 2057400 kB |
| MemTotal: 16325648 kB |
| Shmem: 577588 kB |
| """).encode() |
| with mock_open_content({"/proc/meminfo": content}) as m: |
| with warnings.catch_warnings(record=True) as ws: |
| ret = psutil.virtual_memory() |
| assert m.called |
| assert ret.available == 2057400 * 1024 + 4818144 * 1024 |
| w = ws[0] |
| assert "inactive memory stats couldn't be determined" in str( |
| w.message |
| ) |
| |
| def test_avail_old_missing_zoneinfo(self): |
| # Remove /proc/zoneinfo file. Make sure fallback is used |
| # (free + cached). |
| content = textwrap.dedent("""\ |
| Active: 9444728 kB |
| Active(anon): 6145416 kB |
| Active(file): 2950064 kB |
| Buffers: 287952 kB |
| Cached: 4818144 kB |
| Inactive(file): 1578132 kB |
| Inactive(anon): 574764 kB |
| Inactive(file): 1567648 kB |
| MemFree: 2057400 kB |
| MemTotal: 16325648 kB |
| Shmem: 577588 kB |
| SReclaimable: 346648 kB |
| """).encode() |
| with mock_open_content({"/proc/meminfo": content}): |
| with mock_open_exception("/proc/zoneinfo", FileNotFoundError): |
| with warnings.catch_warnings(record=True) as ws: |
| ret = psutil.virtual_memory() |
| assert ret.available == 2057400 * 1024 + 4818144 * 1024 |
| w = ws[0] |
| assert ( |
| "inactive memory stats couldn't be determined" |
| in str(w.message) |
| ) |
| |
| def test_virtual_memory_mocked(self): |
| # Emulate /proc/meminfo because neither vmstat nor free return slab. |
| content = textwrap.dedent("""\ |
| MemTotal: 100 kB |
| MemFree: 2 kB |
| MemAvailable: 3 kB |
| Buffers: 4 kB |
| Cached: 5 kB |
| SwapCached: 6 kB |
| Active: 7 kB |
| Inactive: 8 kB |
| Active(anon): 9 kB |
| Inactive(anon): 10 kB |
| Active(file): 11 kB |
| Inactive(file): 12 kB |
| Unevictable: 13 kB |
| Mlocked: 14 kB |
| SwapTotal: 15 kB |
| SwapFree: 16 kB |
| Dirty: 17 kB |
| Writeback: 18 kB |
| AnonPages: 19 kB |
| Mapped: 20 kB |
| Shmem: 21 kB |
| Slab: 22 kB |
| SReclaimable: 23 kB |
| SUnreclaim: 24 kB |
| KernelStack: 25 kB |
| PageTables: 26 kB |
| NFS_Unstable: 27 kB |
| Bounce: 28 kB |
| WritebackTmp: 29 kB |
| CommitLimit: 30 kB |
| Committed_AS: 31 kB |
| VmallocTotal: 32 kB |
| VmallocUsed: 33 kB |
| VmallocChunk: 34 kB |
| HardwareCorrupted: 35 kB |
| AnonHugePages: 36 kB |
| ShmemHugePages: 37 kB |
| ShmemPmdMapped: 38 kB |
| CmaTotal: 39 kB |
| CmaFree: 40 kB |
| HugePages_Total: 41 kB |
| HugePages_Free: 42 kB |
| HugePages_Rsvd: 43 kB |
| HugePages_Surp: 44 kB |
| Hugepagesize: 45 kB |
| DirectMap46k: 46 kB |
| DirectMap47M: 47 kB |
| DirectMap48G: 48 kB |
| """).encode() |
| with mock_open_content({"/proc/meminfo": content}) as m: |
| mem = psutil.virtual_memory() |
| assert m.called |
| assert mem.total == 100 * 1024 |
| assert mem.free == 2 * 1024 |
| assert mem.buffers == 4 * 1024 |
| # cached mem also includes reclaimable memory |
| assert mem.cached == (5 + 23) * 1024 |
| assert mem.shared == 21 * 1024 |
| assert mem.active == 7 * 1024 |
| assert mem.inactive == 8 * 1024 |
| assert mem.slab == 22 * 1024 |
| assert mem.available == 3 * 1024 |
| |
| def test_virtual_memory_no_space_after_colon(self): |
| """Some Linux meminfo fields may not have a space after the colon: |
| https://github.com/torvalds/linux/blob/8356a5a3b078ca89c526dd6d71e9a76fec571c37/fs/proc/meminfo.c#L113-L116 |
| """ |
| content = textwrap.dedent("""\ |
| MemTotal: 100 kB |
| MemFree: 2 kB |
| MemAvailable: 3 kB |
| Buffers: 4 kB |
| Cached: 5 kB |
| Active: 6 kB |
| Inactive: 7 kB |
| Shmem: 8 kB |
| Slab: 9 kB |
| ShadowCallStack:10373888 kB |
| """).encode() |
| with mock_open_content({"/proc/meminfo": content}) as m: |
| mem = psutil.virtual_memory() |
| assert m.called |
| assert mem.total == 100 * 1024 |
| |
| |
| # ===================================================================== |
| # --- system swap memory |
| # ===================================================================== |
| |
| |
| class TestSwapMemory(LinuxTestCase): |
| @staticmethod |
| def meminfo_has_swap_info(): |
| """Return True if /proc/meminfo provides swap metrics.""" |
| with open("/proc/meminfo") as f: |
| data = f.read() |
| return 'SwapTotal:' in data and 'SwapFree:' in data |
| |
| def test_total(self): |
| free_value = free_swap().total |
| psutil_value = psutil.swap_memory().total |
| assert abs(free_value - psutil_value) < TOLERANCE_SYS_MEM |
| |
| @retry_on_failure() |
| def test_used(self): |
| free_value = free_swap().used |
| psutil_value = psutil.swap_memory().used |
| assert abs(free_value - psutil_value) < TOLERANCE_SYS_MEM |
| |
| @retry_on_failure() |
| def test_free(self): |
| free_value = free_swap().free |
| psutil_value = psutil.swap_memory().free |
| assert abs(free_value - psutil_value) < TOLERANCE_SYS_MEM |
| |
| @retry_on_failure() |
| def test_sin_sout(self): |
| # Cross-check sin/sout against /proc/vmstat pswpin/pswpout fields. |
| # psutil converts pages to bytes using a 4096-byte page size. |
| PAGE_SIZE = 4 * 1024 |
| with open("/proc/vmstat") as f: |
| vmstat = dict(line.split() for line in f if line.split()) |
| sin = int(vmstat["pswpin"]) * PAGE_SIZE |
| sout = int(vmstat["pswpout"]) * PAGE_SIZE |
| swap = psutil.swap_memory() |
| assert swap.sin == sin |
| assert swap.sout == sout |
| |
| def test_missing_sin_sout(self): |
| with mock.patch('psutil._common.open', create=True) as m: |
| with warnings.catch_warnings(record=True) as ws: |
| warnings.simplefilter("always") |
| ret = psutil.swap_memory() |
| assert m.called |
| assert len(ws) == 1 |
| w = ws[0] |
| assert ( |
| "'sin' and 'sout' swap memory stats couldn't be determined" |
| in str(w.message) |
| ) |
| assert ret.sin == 0 |
| assert ret.sout == 0 |
| |
| def test_no_vmstat_mocked(self): |
| # see https://github.com/giampaolo/psutil/issues/722 |
| with mock_open_exception("/proc/vmstat", FileNotFoundError) as m: |
| with warnings.catch_warnings(record=True) as ws: |
| warnings.simplefilter("always") |
| ret = psutil.swap_memory() |
| assert m.called |
| assert len(ws) == 1 |
| w = ws[0] |
| assert ( |
| "'sin' and 'sout' swap memory stats couldn't " |
| "be determined and were set to 0" |
| in str(w.message) |
| ) |
| assert ret.sin == 0 |
| assert ret.sout == 0 |
| |
| def test_meminfo_against_sysinfo(self): |
| # Make sure the content of /proc/meminfo about swap memory |
| # matches sysinfo() syscall, see: |
| # https://github.com/giampaolo/psutil/issues/1015 |
| if not self.meminfo_has_swap_info(): |
| return pytest.skip("/proc/meminfo has no swap metrics") |
| with mock.patch('psutil._pslinux.cext.linux_sysinfo') as m: |
| swap = psutil.swap_memory() |
| assert not m.called |
| import psutil._psutil_linux as cext |
| |
| _, _, _, _, total, free, unit_multiplier = cext.linux_sysinfo() |
| total *= unit_multiplier |
| free *= unit_multiplier |
| assert swap.total == total |
| assert abs(swap.free - free) < TOLERANCE_SYS_MEM |
| |
| def test_emulate_meminfo_has_no_metrics(self): |
| # Emulate a case where /proc/meminfo provides no swap metrics |
| # in which case sysinfo() syscall is supposed to be used |
| # as a fallback. |
| with mock_open_content({"/proc/meminfo": b""}) as m: |
| psutil.swap_memory() |
| assert m.called |
| |
| def test_no_space_after_colon(self): |
| # Some Linux meminfo fields may not have a space after the |
| # colon, see: |
| # https://github.com/giampaolo/psutil/issues/2809 |
| content = textwrap.dedent("""\ |
| MemTotal: 100 kB |
| MemFree: 2 kB |
| SwapTotal: 15 kB |
| SwapFree: 14 kB |
| ShadowCallStack:10373888 kB |
| """).encode() |
| with mock_open_content({"/proc/meminfo": content}) as m: |
| swap = psutil.swap_memory() |
| assert m.called |
| assert swap.total == 15 * 1024 |
| assert swap.free == 14 * 1024 |
| |
| |
| # ===================================================================== |
| # --- system CPU |
| # ===================================================================== |
| |
| |
| class TestCpuCountLogical(LinuxTestCase): |
| @pytest.mark.skipif( |
| not os.path.exists("/sys/devices/system/cpu/online"), |
| reason="/sys/devices/system/cpu/online does not exist", |
| ) |
| def test_against_sysdev_cpu_online(self): |
| with open("/sys/devices/system/cpu/online") as f: |
| value = f.read().strip() |
| if "-" in str(value): |
| value = int(value.split('-')[1]) + 1 |
| assert psutil.cpu_count() == value |
| |
| @pytest.mark.skipif( |
| not os.path.exists("/sys/devices/system/cpu"), |
| reason="/sys/devices/system/cpu does not exist", |
| ) |
| def test_against_sysdev_cpu_num(self): |
| ls = os.listdir("/sys/devices/system/cpu") |
| count = len([x for x in ls if re.search(r"cpu\d+$", x) is not None]) |
| assert psutil.cpu_count() == count |
| |
| @pytest.mark.skipif( |
| not shutil.which("nproc"), reason="nproc utility not available" |
| ) |
| def test_against_nproc(self): |
| num = int(sh("nproc --all")) |
| assert psutil.cpu_count(logical=True) == num |
| |
| @pytest.mark.skipif( |
| not shutil.which("lscpu"), reason="lscpu utility not available" |
| ) |
| def test_against_lscpu(self): |
| out = sh("lscpu -p") |
| num = len([x for x in out.split('\n') if not x.startswith('#')]) |
| assert psutil.cpu_count(logical=True) == num |
| |
| def test_emulate_fallbacks(self): |
| import psutil._pslinux |
| |
| original = psutil._pslinux.cpu_count_logical() |
| # Here we want to mock os.sysconf("SC_NPROCESSORS_ONLN") in |
| # order to cause the parsing of /proc/cpuinfo and /proc/stat. |
| with mock.patch( |
| 'psutil._pslinux.os.sysconf', side_effect=ValueError |
| ) as m: |
| assert psutil._pslinux.cpu_count_logical() == original |
| assert m.called |
| |
| # Let's have open() return empty data and make sure None is |
| # returned ('cause we mimic os.cpu_count()). |
| with mock.patch('psutil._common.open', create=True) as m: |
| assert psutil._pslinux.cpu_count_logical() is None |
| assert m.call_count == 2 |
| # /proc/stat should be the last one |
| assert m.call_args[0][0] == '/proc/stat' |
| |
| # Let's push this a bit further and make sure /proc/cpuinfo |
| # parsing works as expected. |
| with open('/proc/cpuinfo', 'rb') as f: |
| cpuinfo_data = f.read() |
| fake_file = io.BytesIO(cpuinfo_data) |
| with mock.patch( |
| 'psutil._common.open', return_value=fake_file, create=True |
| ) as m: |
| assert psutil._pslinux.cpu_count_logical() == original |
| |
| # Finally, let's make /proc/cpuinfo return meaningless data; |
| # this way we'll fall back on relying on /proc/stat |
| with mock_open_content({"/proc/cpuinfo": b""}) as m: |
| assert psutil._pslinux.cpu_count_logical() == original |
| assert m.called |
| |
| |
| class TestCpuCountCores(LinuxTestCase): |
| @pytest.mark.skipif( |
| not shutil.which("lscpu"), reason="lscpu utility not available" |
| ) |
| def test_against_lscpu(self): |
| out = sh("lscpu -p") |
| core_ids = set() |
| for line in out.split('\n'): |
| if not line.startswith('#'): |
| fields = line.split(',') |
| core_ids.add(fields[1]) |
| assert psutil.cpu_count(logical=False) == len(core_ids) |
| |
| @pytest.mark.skipif( |
| platform.machine() not in {"x86_64", "i686"}, reason="x86_64/i686 only" |
| ) |
| def test_method_2(self): |
| meth_1 = psutil._pslinux.cpu_count_cores() |
| with mock.patch('glob.glob', return_value=[]) as m: |
| meth_2 = psutil._pslinux.cpu_count_cores() |
| assert m.called |
| if meth_1 is not None: |
| assert meth_1 == meth_2 |
| |
| def test_emulate_none(self): |
| with mock.patch('glob.glob', return_value=[]) as m1: |
| with mock.patch('psutil._common.open', create=True) as m2: |
| assert psutil._pslinux.cpu_count_cores() is None |
| assert m1.called |
| assert m2.called |
| |
| |
| class TestCpuFreq(LinuxTestCase): |
| @pytest.mark.skipif(not HAS_CPU_FREQ, reason="not supported") |
| @pytest.mark.skipif( |
| AARCH64, reason="aarch64 does not always expose frequency" |
| ) |
| def test_emulate_use_second_file(self): |
| # https://github.com/giampaolo/psutil/issues/981 |
| def path_exists_mock(path): |
| if path.startswith("/sys/devices/system/cpu/cpufreq/policy"): |
| return False |
| else: |
| return orig_exists(path) |
| |
| orig_exists = os.path.exists |
| with mock.patch( |
| "os.path.exists", side_effect=path_exists_mock, create=True |
| ): |
| assert psutil.cpu_freq() |
| |
| @pytest.mark.skipif(not HAS_CPU_FREQ, reason="not supported") |
| @pytest.mark.skipif( |
| AARCH64 or RISCV64, |
| reason=f"{platform.machine()} does not report mhz in /proc/cpuinfo", |
| ) |
| def test_emulate_use_cpuinfo(self): |
| # Emulate a case where /sys/devices/system/cpu/cpufreq* does not |
| # exist and /proc/cpuinfo is used instead. |
| def path_exists_mock(path): |
| if path.startswith('/sys/devices/system/cpu/'): |
| return False |
| else: |
| return os_path_exists(path) |
| |
| os_path_exists = os.path.exists |
| try: |
| with mock.patch("os.path.exists", side_effect=path_exists_mock): |
| reload_module(psutil._pslinux) |
| ret = psutil.cpu_freq() |
| assert ret, ret |
| assert ret.max == 0.0 |
| assert ret.min == 0.0 |
| for freq in psutil.cpu_freq(percpu=True): |
| assert freq.max == 0.0 |
| assert freq.min == 0.0 |
| finally: |
| reload_module(psutil._pslinux) |
| reload_module(psutil) |
| |
| @pytest.mark.skipif(not HAS_CPU_FREQ, reason="not supported") |
| def test_emulate_data(self): |
| def open_mock(name, *args, **kwargs): |
| if name.endswith('/scaling_cur_freq') and name.startswith( |
| "/sys/devices/system/cpu/cpufreq/policy" |
| ): |
| return io.BytesIO(b"500000") |
| elif name.endswith('/scaling_min_freq') and name.startswith( |
| "/sys/devices/system/cpu/cpufreq/policy" |
| ): |
| return io.BytesIO(b"600000") |
| elif name.endswith('/scaling_max_freq') and name.startswith( |
| "/sys/devices/system/cpu/cpufreq/policy" |
| ): |
| return io.BytesIO(b"700000") |
| elif name == '/proc/cpuinfo': |
| return io.BytesIO(b"cpu MHz : 500") |
| else: |
| return orig_open(name, *args, **kwargs) |
| |
| orig_open = open |
| with mock.patch("builtins.open", side_effect=open_mock): |
| with mock.patch('os.path.exists', return_value=True): |
| freq = psutil.cpu_freq() |
| assert freq.current == 500.0 |
| # when /proc/cpuinfo is used min and max frequencies are not |
| # available and are set to 0. |
| if freq.min != 0.0: |
| assert freq.min == 600.0 |
| if freq.max != 0.0: |
| assert freq.max == 700.0 |
| |
| @pytest.mark.skipif(not HAS_CPU_FREQ, reason="not supported") |
| def test_emulate_multi_cpu(self): |
| def open_mock(name, *args, **kwargs): |
| n = name |
| if n.endswith('/scaling_cur_freq') and n.startswith( |
| "/sys/devices/system/cpu/cpufreq/policy0" |
| ): |
| return io.BytesIO(b"100000") |
| elif n.endswith('/scaling_min_freq') and n.startswith( |
| "/sys/devices/system/cpu/cpufreq/policy0" |
| ): |
| return io.BytesIO(b"200000") |
| elif n.endswith('/scaling_max_freq') and n.startswith( |
| "/sys/devices/system/cpu/cpufreq/policy0" |
| ): |
| return io.BytesIO(b"300000") |
| elif n.endswith('/scaling_cur_freq') and n.startswith( |
| "/sys/devices/system/cpu/cpufreq/policy1" |
| ): |
| return io.BytesIO(b"400000") |
| elif n.endswith('/scaling_min_freq') and n.startswith( |
| "/sys/devices/system/cpu/cpufreq/policy1" |
| ): |
| return io.BytesIO(b"500000") |
| elif n.endswith('/scaling_max_freq') and n.startswith( |
| "/sys/devices/system/cpu/cpufreq/policy1" |
| ): |
| return io.BytesIO(b"600000") |
| elif name == '/proc/cpuinfo': |
| return io.BytesIO(b"cpu MHz : 100\ncpu MHz : 400") |
| else: |
| return orig_open(name, *args, **kwargs) |
| |
| orig_open = open |
| with mock.patch("builtins.open", side_effect=open_mock): |
| with mock.patch('os.path.exists', return_value=True): |
| with mock.patch( |
| 'psutil._pslinux.cpu_count_logical', return_value=2 |
| ): |
| freq = psutil.cpu_freq(percpu=True) |
| assert freq[0].current == 100.0 |
| if freq[0].min != 0.0: |
| assert freq[0].min == 200.0 |
| if freq[0].max != 0.0: |
| assert freq[0].max == 300.0 |
| assert freq[1].current == 400.0 |
| if freq[1].min != 0.0: |
| assert freq[1].min == 500.0 |
| if freq[1].max != 0.0: |
| assert freq[1].max == 600.0 |
| |
| @pytest.mark.skipif(not HAS_CPU_FREQ, reason="not supported") |
| def test_emulate_no_scaling_cur_freq_file(self): |
| # See: https://github.com/giampaolo/psutil/issues/1071 |
| def open_mock(name, *args, **kwargs): |
| if name.endswith('/scaling_cur_freq'): |
| raise FileNotFoundError |
| if name.endswith('/cpuinfo_cur_freq'): |
| return io.BytesIO(b"200000") |
| elif name == '/proc/cpuinfo': |
| return io.BytesIO(b"cpu MHz : 200") |
| else: |
| return orig_open(name, *args, **kwargs) |
| |
| orig_open = open |
| with mock.patch("builtins.open", side_effect=open_mock): |
| with mock.patch('os.path.exists', return_value=True): |
| with mock.patch( |
| 'psutil._pslinux.cpu_count_logical', return_value=1 |
| ): |
| freq = psutil.cpu_freq() |
| assert freq.current == 200 |
| |
| @pytest.mark.skipif(not HAS_CPU_FREQ, reason="not supported") |
| def test_emulate_offline_cpus(self): |
| # Offline CPU cores must not be taken into account, else they |
| # drag down the average frequency. See: |
| # https://github.com/giampaolo/psutil/issues/2628 |
| policies = [ |
| f"/sys/devices/system/cpu/cpufreq/policy{n}" for n in range(4) |
| ] |
| |
| def exists_mock(path): |
| # Make sure the sysfs-based implementation is used. |
| if path.startswith("/sys/devices/system/cpu/"): |
| return True |
| return orig_exists(path) |
| |
| def glob_mock(pattern): |
| if pattern == "/sys/devices/system/cpu/cpufreq/policy[0-9]*": |
| return list(policies) |
| return orig_glob(pattern) |
| |
| def open_mock(name, *args, **kwargs): |
| # Only CPUs 0 and 1 are online; 2 and 3 are offline. |
| if name == '/proc/cpuinfo': |
| return io.BytesIO(b"cpu MHz\t: 200\ncpu MHz\t: 400") |
| elif "/policy0/" in name or "/policy1/" in name: |
| if name.endswith('/scaling_cur_freq'): |
| cur = b"200000" if "/policy0/" in name else b"400000" |
| return io.BytesIO(cur) |
| elif name.endswith('/scaling_min_freq'): |
| return io.BytesIO(b"100000") |
| elif name.endswith('/scaling_max_freq'): |
| return io.BytesIO(b"300000") |
| elif "/policy2/" in name or "/policy3/" in name: |
| # Offline cores have no frequency files. |
| if name.endswith(('/scaling_cur_freq', '/cpuinfo_cur_freq')): |
| raise FileNotFoundError |
| elif name.endswith('/online'): |
| # CPUs 2 and 3 are offline. |
| return io.StringIO("0\n") |
| return orig_open(name, *args, **kwargs) |
| |
| orig_exists = os.path.exists |
| orig_glob = glob.glob |
| orig_open = open |
| try: |
| with mock.patch("os.path.exists", side_effect=exists_mock): |
| reload_module(psutil._pslinux) |
| with mock.patch("glob.glob", side_effect=glob_mock): |
| with mock.patch("builtins.open", side_effect=open_mock): |
| percpu = psutil.cpu_freq(percpu=True) |
| assert len(percpu) == 2 |
| assert [f.current for f in percpu] == [200.0, 400.0] |
| |
| freq = psutil.cpu_freq() |
| assert freq.current == 300.0 |
| assert freq.min == 100.0 |
| assert freq.max == 300.0 |
| finally: |
| reload_module(psutil._pslinux) |
| reload_module(psutil) |
| |
| |
| class TestCpuTimes(LinuxTestCase): |
| |
| @retry_on_failure() |
| def test_against_proc_stat(self): |
| with open("/proc/stat") as f: |
| line = f.readline() |
| ticks = [float(x) for x in line.split()[1:]] |
| fields = [t / CLOCK_TICKS for t in ticks] |
| ct = psutil.cpu_times() |
| TOLERANCE = 1 # 1 second |
| assert abs(ct.user - fields[0]) < TOLERANCE |
| assert abs(ct.nice - fields[1]) < TOLERANCE |
| assert abs(ct.system - fields[2]) < TOLERANCE |
| assert abs(ct.idle - fields[3]) < TOLERANCE |
| assert abs(ct.iowait - fields[4]) < TOLERANCE |
| assert abs(ct.irq - fields[5]) < TOLERANCE |
| assert abs(ct.softirq - fields[6]) < TOLERANCE |
| assert abs(ct.steal - fields[7]) < TOLERANCE |
| |
| |
| class TestCpuStats(LinuxTestCase): |
| |
| # XXX: fails too often. |
| # def test_ctx_switches(self): |
| # vmstat_value = vmstat("context switches") |
| # psutil_value = psutil.cpu_stats().ctx_switches |
| # assert abs(vmstat_value - psutil_value) < 500 |
| |
| def test_interrupts(self): |
| vmstat_value = vmstat("interrupts") |
| psutil_value = psutil.cpu_stats().interrupts |
| assert abs(vmstat_value - psutil_value) < 500 |
| |
| |
| class TestLoadAvg(LinuxTestCase): |
| def test_getloadavg(self): |
| psutil_value = psutil.getloadavg() |
| with open("/proc/loadavg") as f: |
| proc_value = f.read().split() |
| |
| assert abs(float(proc_value[0]) - psutil_value[0]) < 1 |
| assert abs(float(proc_value[1]) - psutil_value[1]) < 1 |
| assert abs(float(proc_value[2]) - psutil_value[2]) < 1 |
| |
| |
| # ===================================================================== |
| # --- system network |
| # ===================================================================== |
| |
| |
| class TestNetIfAddrs(LinuxTestCase): |
| def test_ips(self): |
| for name, addrs in psutil.net_if_addrs().items(): |
| for addr in addrs: |
| if addr.family == psutil.AF_LINK: |
| assert addr.address == get_mac_address(name) |
| elif addr.family == socket.AF_INET: |
| assert addr.address == get_ipv4_address(name) |
| assert addr.netmask == get_ipv4_netmask(name) |
| if addr.broadcast is not None: |
| assert addr.broadcast == get_ipv4_broadcast(name) |
| else: |
| assert get_ipv4_broadcast(name) == '0.0.0.0' |
| elif addr.family == socket.AF_INET6: |
| # IPv6 addresses can have a percent symbol at the end. |
| # E.g. these 2 are equivalent: |
| # "fe80::1ff:fe23:4567:890a" |
| # "fe80::1ff:fe23:4567:890a%eth0" |
| # That is the "zone id" portion, which usually is the name |
| # of the network interface. |
| address = addr.address.split('%')[0] |
| assert address in get_ipv6_addresses(name) |
| |
| @pytest.mark.skipif( |
| not shutil.which("ip"), reason="'ip' command not available" |
| ) |
| @retry_on_failure() |
| def test_against_ip_addr_v4(self): |
| # Parse IPv4 addresses per interface from `ip addr` output and |
| # compare against psutil. Use the label at the end of each inet |
| # line as the interface name, since it reflects aliases like |
| # "vboxnet0:avahi" that psutil also uses as keys. |
| out = sh("ip addr") |
| ip_addrs = {} # {ifname: [addr, ...]} |
| for line in out.splitlines(): |
| # " inet 1.2.3.4/24 brd ... scope global eth0" |
| m = re.match(r'^\s+inet\s+(\S+).*\s+(\S+)$', line) |
| if m: |
| addr = m.group(1).split('/')[0] |
| ifname = m.group(2) |
| ip_addrs.setdefault(ifname, []).append(addr) |
| psutil_addrs = psutil.net_if_addrs() |
| for ifname, addrs in ip_addrs.items(): |
| if ifname not in psutil_addrs: |
| continue |
| psutil_ipv4 = { |
| a.address |
| for a in psutil_addrs[ifname] |
| if a.family == socket.AF_INET |
| } |
| for addr in addrs: |
| assert addr in psutil_ipv4 |
| |
| @pytest.mark.skipif( |
| not shutil.which("ip"), reason="'ip' command not available" |
| ) |
| @retry_on_failure() |
| def test_against_ip_addr_v6(self): |
| # Parse IPv6 addresses per interface from `ip addr` output and |
| # compare against psutil. Unlike inet, inet6 lines have no label, |
| # so the interface name comes from the header line. |
| out = sh("ip addr") |
| ip_addrs = {} # {ifname: [addr, ...]} |
| current_if = None |
| for line in out.splitlines(): |
| m = re.match(r'^\d+:\s+(\S+):', line) |
| if m: |
| current_if = m.group(1).rstrip(':') |
| m = re.match(r'^\s+inet6\s+(\S+)', line) |
| if m and current_if: |
| addr = m.group(1).split('/')[0] |
| ip_addrs.setdefault(current_if, []).append(addr) |
| psutil_addrs = psutil.net_if_addrs() |
| for ifname, addrs in ip_addrs.items(): |
| if ifname not in psutil_addrs: |
| continue |
| # psutil may append %ifname zone ID to link-local addresses. |
| psutil_ipv6 = { |
| a.address.split('%')[0] |
| for a in psutil_addrs[ifname] |
| if a.family == socket.AF_INET6 |
| } |
| for addr in addrs: |
| assert addr in psutil_ipv6 |
| |
| # XXX - not reliable when having virtual NICs installed by Docker. |
| # @pytest.mark.skipif(not shutil.which("ip"), |
| # reason="'ip' utility not available") |
| # def test_net_if_names(self): |
| # out = sh("ip addr").strip() |
| # nics = [x for x in psutil.net_if_addrs().keys() if ':' not in x] |
| # found = 0 |
| # for line in out.split('\n'): |
| # line = line.strip() |
| # if re.search(r"^\d+:", line): |
| # found += 1 |
| # name = line.split(':')[1].strip() |
| # assert name in nics |
| # assert len(nics) == found |
| |
| |
| class TestNetIfStats(LinuxTestCase): |
| @pytest.mark.skipif( |
| not shutil.which("ifconfig"), reason="ifconfig utility not available" |
| ) |
| def test_against_ifconfig(self): |
| for name, stats in psutil.net_if_stats().items(): |
| try: |
| out = sh(f"ifconfig {name}") |
| except RuntimeError: |
| pass |
| else: |
| assert stats.isup == ('RUNNING' in out), out |
| assert stats.mtu == int( |
| re.findall(r'(?i)MTU[: ](\d+)', out)[0] |
| ) |
| |
| def test_mtu(self): |
| for name, stats in psutil.net_if_stats().items(): |
| with open(f"/sys/class/net/{name}/mtu") as f: |
| assert stats.mtu == int(f.read().strip()) |
| |
| @pytest.mark.skipif( |
| not shutil.which("ifconfig"), reason="ifconfig utility not available" |
| ) |
| def test_flags(self): |
| # first line looks like this: |
| # "eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500" |
| matches_found = 0 |
| for name, stats in psutil.net_if_stats().items(): |
| try: |
| out = sh(f"ifconfig {name}") |
| except RuntimeError: |
| pass |
| else: |
| match = re.search(r"flags=(\d+)?<(.*?)>", out) |
| if match and len(match.groups()) >= 2: |
| matches_found += 1 |
| ifconfig_flags = set(match.group(2).lower().split(",")) |
| psutil_flags = set(stats.flags.split(",")) |
| assert ifconfig_flags == psutil_flags |
| else: |
| # ifconfig has a different output on CentOS 6 |
| # let's try that |
| match = re.search(r"(.*) MTU:(\d+) Metric:(\d+)", out) |
| if match and len(match.groups()) >= 3: |
| matches_found += 1 |
| ifconfig_flags = set(match.group(1).lower().split()) |
| psutil_flags = set(stats.flags.split(",")) |
| assert ifconfig_flags == psutil_flags |
| |
| if not matches_found: |
| return pytest.fail("no matches were found") |
| |
| |
| class TestNetIoCounters(LinuxTestCase): |
| @pytest.mark.skipif( |
| not shutil.which("ifconfig"), reason="ifconfig utility not available" |
| ) |
| @retry_on_failure() |
| def test_against_ifconfig(self): |
| def ifconfig(nic): |
| ret = {} |
| out = sh(f"ifconfig {nic}") |
| ret['packets_recv'] = int( |
| re.findall(r'RX packets[: ](\d+)', out)[0] |
| ) |
| ret['packets_sent'] = int( |
| re.findall(r'TX packets[: ](\d+)', out)[0] |
| ) |
| ret['errin'] = int(re.findall(r'errors[: ](\d+)', out)[0]) |
| ret['errout'] = int(re.findall(r'errors[: ](\d+)', out)[1]) |
| ret['dropin'] = int(re.findall(r'dropped[: ](\d+)', out)[0]) |
| ret['dropout'] = int(re.findall(r'dropped[: ](\d+)', out)[1]) |
| ret['bytes_recv'] = int( |
| re.findall(r'RX (?:packets \d+ +)?bytes[: ](\d+)', out)[0] |
| ) |
| ret['bytes_sent'] = int( |
| re.findall(r'TX (?:packets \d+ +)?bytes[: ](\d+)', out)[0] |
| ) |
| return ret |
| |
| nio = psutil.net_io_counters(pernic=True, nowrap=False) |
| for name, stats in nio.items(): |
| try: |
| ifconfig_ret = ifconfig(name) |
| except RuntimeError: |
| continue |
| assert ( |
| abs(stats.bytes_recv - ifconfig_ret['bytes_recv']) < 1024 * 10 |
| ) |
| assert ( |
| abs(stats.bytes_sent - ifconfig_ret['bytes_sent']) < 1024 * 10 |
| ) |
| assert ( |
| abs(stats.packets_recv - ifconfig_ret['packets_recv']) < 1024 |
| ) |
| assert ( |
| abs(stats.packets_sent - ifconfig_ret['packets_sent']) < 1024 |
| ) |
| assert abs(stats.errin - ifconfig_ret['errin']) < 10 |
| assert abs(stats.errout - ifconfig_ret['errout']) < 10 |
| assert abs(stats.dropin - ifconfig_ret['dropin']) < 10 |
| assert abs(stats.dropout - ifconfig_ret['dropout']) < 10 |
| |
| |
| class TestNetConnections(LinuxTestCase): |
| @mock.patch('psutil._pslinux.socket.inet_ntop', side_effect=ValueError) |
| @mock.patch('psutil._pslinux.supports_ipv6', return_value=False) |
| def test_emulate_ipv6_unsupported(self, supports_ipv6, inet_ntop): |
| # see: https://github.com/giampaolo/psutil/issues/623 |
| with socket.socket(socket.AF_INET6, socket.SOCK_STREAM) as s: |
| try: |
| s.bind(("::1", 0)) |
| except OSError: |
| pass |
| psutil.net_connections(kind='inet6') |
| |
| def test_emulate_unix(self): |
| content = textwrap.dedent("""\ |
| 0: 00000003 000 000 0001 03 462170 @/tmp/dbus-Qw2hMPIU3n |
| 0: 00000003 000 000 0001 03 35010 @/tmp/dbus-tB2X8h69BQ |
| 0: 00000003 000 000 0001 03 34424 @/tmp/dbus-cHy80Y8O |
| 000000000000000000000000000000000000000000000000000000 |
| """) |
| with mock_open_content({"/proc/net/unix": content}) as m: |
| psutil.net_connections(kind='unix') |
| assert m.called |
| |
| @pytest.mark.skipif( |
| not shutil.which("ss"), reason="'ss' command not available" |
| ) |
| def test_against_ss(self): |
| # Listening ports are stable, so an exact set comparison is |
| # reliable. |
| out = sh(["ss", "-tuanp"]) |
| ss_ports = set() |
| for line in out.splitlines(): |
| fields = line.split() |
| if ( |
| len(fields) >= 5 |
| and fields[0] == "tcp" |
| and fields[1] == "LISTEN" |
| ): |
| port = int(fields[4].rsplit(":", 1)[-1]) |
| ss_ports.add(port) |
| psutil_ports = { |
| c.laddr.port |
| for c in psutil.net_connections(kind="tcp") |
| if c.status == psutil.CONN_LISTEN |
| } |
| assert ss_ports == psutil_ports |
| |
| |
| # ===================================================================== |
| # --- system disks |
| # ===================================================================== |
| |
| |
| class TestDiskPartitions(LinuxTestCase): |
| @pytest.mark.skipif( |
| not hasattr(os, 'statvfs'), reason="os.statvfs() not available" |
| ) |
| @skip_on_not_implemented() |
| def test_against_df(self): |
| # test psutil.disk_usage() and psutil.disk_partitions() |
| # against "df -a" |
| def df(path): |
| out = sh(f'df -P -B 1 "{path}"').strip() |
| lines = out.split('\n') |
| lines.pop(0) |
| line = lines.pop(0) |
| dev, total, used, free = line.split()[:4] |
| if dev == 'none': |
| dev = '' |
| total, used, free = int(total), int(used), int(free) |
| return dev, total, used, free |
| |
| for part in psutil.disk_partitions(all=False): |
| usage = psutil.disk_usage(part.mountpoint) |
| _, total, used, free = df(part.mountpoint) |
| assert usage.total == total |
| assert abs(usage.free - free) < TOLERANCE_DISK_USAGE |
| assert abs(usage.used - used) < TOLERANCE_DISK_USAGE |
| |
| def test_zfs_fs(self): |
| # Test that ZFS partitions are returned. |
| with open("/proc/filesystems") as f: |
| data = f.read() |
| if 'zfs' in data: |
| for part in psutil.disk_partitions(): |
| if part.fstype == 'zfs': |
| return |
| |
| # No ZFS partitions on this system. Let's fake one. |
| fake_file = io.StringIO("nodev\tzfs\n") |
| with mock.patch( |
| 'psutil._common.open', return_value=fake_file, create=True |
| ) as m1: |
| with mock.patch( |
| 'psutil._pslinux.cext.disk_partitions', |
| return_value=[('/dev/sdb3', '/', 'zfs', 'rw')], |
| ) as m2: |
| ret = psutil.disk_partitions() |
| assert m1.called |
| assert m2.called |
| assert ret |
| assert ret[0].fstype == 'zfs' |
| |
| def test_emulate_realpath_fail(self): |
| # See: https://github.com/giampaolo/psutil/issues/1307 |
| try: |
| with mock.patch( |
| 'os.path.realpath', return_value='/non/existent' |
| ) as m: |
| with pytest.raises(FileNotFoundError): |
| psutil.disk_partitions() |
| assert m.called |
| finally: |
| psutil.PROCFS_PATH = "/proc" |
| |
| |
| class TestDiskIoCounters(LinuxTestCase): |
| def test_emulate_kernel_2_4(self): |
| # Tests /proc/diskstats parsing format for 2.4 kernels, see: |
| # https://github.com/giampaolo/psutil/issues/767 |
| content = " 3 0 1 hda 2 3 4 5 6 7 8 9 10 11 12" |
| with mock_open_content({'/proc/diskstats': content}): |
| with mock.patch( |
| 'psutil._pslinux.is_storage_device', return_value=True |
| ): |
| ret = psutil.disk_io_counters(nowrap=False) |
| assert ret.read_count == 1 |
| assert ret.read_merged_count == 2 |
| assert ret.read_bytes == 3 * SECTOR_SIZE |
| assert ret.read_time == 4 |
| assert ret.write_count == 5 |
| assert ret.write_merged_count == 6 |
| assert ret.write_bytes == 7 * SECTOR_SIZE |
| assert ret.write_time == 8 |
| assert ret.busy_time == 10 |
| |
| def test_emulate_kernel_2_6_full(self): |
| # Tests /proc/diskstats parsing format for 2.6 kernels, |
| # lines reporting all metrics: |
| # https://github.com/giampaolo/psutil/issues/767 |
| content = " 3 0 hda 1 2 3 4 5 6 7 8 9 10 11" |
| with mock_open_content({"/proc/diskstats": content}): |
| with mock.patch( |
| 'psutil._pslinux.is_storage_device', return_value=True |
| ): |
| ret = psutil.disk_io_counters(nowrap=False) |
| assert ret.read_count == 1 |
| assert ret.read_merged_count == 2 |
| assert ret.read_bytes == 3 * SECTOR_SIZE |
| assert ret.read_time == 4 |
| assert ret.write_count == 5 |
| assert ret.write_merged_count == 6 |
| assert ret.write_bytes == 7 * SECTOR_SIZE |
| assert ret.write_time == 8 |
| assert ret.busy_time == 10 |
| |
| def test_emulate_kernel_2_6_limited(self): |
| # Tests /proc/diskstats parsing format for 2.6 kernels, |
| # where one line of /proc/partitions return a limited |
| # amount of metrics when it bumps into a partition |
| # (instead of a disk). See: |
| # https://github.com/giampaolo/psutil/issues/767 |
| with mock_open_content({"/proc/diskstats": " 3 1 hda 1 2 3 4"}): |
| with mock.patch( |
| 'psutil._pslinux.is_storage_device', return_value=True |
| ): |
| ret = psutil.disk_io_counters(nowrap=False) |
| assert ret.read_count == 1 |
| assert ret.read_bytes == 2 * SECTOR_SIZE |
| assert ret.write_count == 3 |
| assert ret.write_bytes == 4 * SECTOR_SIZE |
| |
| assert ret.read_merged_count == 0 |
| assert ret.read_time == 0 |
| assert ret.write_merged_count == 0 |
| assert ret.write_time == 0 |
| assert ret.busy_time == 0 |
| |
| def test_emulate_include_partitions(self): |
| # Make sure that when perdisk=True disk partitions are returned, |
| # see: |
| # https://github.com/giampaolo/psutil/pull/1313#issuecomment-408626842 |
| content = textwrap.dedent("""\ |
| 3 0 nvme0n1 1 2 3 4 5 6 7 8 9 10 11 |
| 3 0 nvme0n1p1 1 2 3 4 5 6 7 8 9 10 11 |
| """) |
| with mock_open_content({"/proc/diskstats": content}): |
| with mock.patch( |
| 'psutil._pslinux.is_storage_device', return_value=False |
| ): |
| ret = psutil.disk_io_counters(perdisk=True, nowrap=False) |
| assert len(ret) == 2 |
| assert ret['nvme0n1'].read_count == 1 |
| assert ret['nvme0n1p1'].read_count == 1 |
| assert ret['nvme0n1'].write_count == 5 |
| assert ret['nvme0n1p1'].write_count == 5 |
| |
| def test_emulate_exclude_partitions(self): |
| # Make sure that when perdisk=False partitions (e.g. 'sda1', |
| # 'nvme0n1p1') are skipped and not included in the total count. |
| # https://github.com/giampaolo/psutil/pull/1313#issuecomment-408626842 |
| content = textwrap.dedent("""\ |
| 3 0 nvme0n1 1 2 3 4 5 6 7 8 9 10 11 |
| 3 0 nvme0n1p1 1 2 3 4 5 6 7 8 9 10 11 |
| """) |
| with mock_open_content({"/proc/diskstats": content}): |
| with mock.patch( |
| 'psutil._pslinux.is_storage_device', return_value=False |
| ): |
| ret = psutil.disk_io_counters(perdisk=False, nowrap=False) |
| assert ret is None |
| |
| def is_storage_device(name): |
| return name == 'nvme0n1' |
| |
| content = textwrap.dedent("""\ |
| 3 0 nvme0n1 1 2 3 4 5 6 7 8 9 10 11 |
| 3 0 nvme0n1p1 1 2 3 4 5 6 7 8 9 10 11 |
| """) |
| with mock_open_content({"/proc/diskstats": content}): |
| with mock.patch( |
| 'psutil._pslinux.is_storage_device', |
| create=True, |
| side_effect=is_storage_device, |
| ): |
| ret = psutil.disk_io_counters(perdisk=False, nowrap=False) |
| assert ret.read_count == 1 |
| assert ret.write_count == 5 |
| |
| def test_emulate_use_sysfs(self): |
| def exists(path): |
| return path == '/proc/diskstats' |
| |
| wprocfs = psutil.disk_io_counters(perdisk=True) |
| with mock.patch( |
| 'psutil._pslinux.os.path.exists', create=True, side_effect=exists |
| ): |
| wsysfs = psutil.disk_io_counters(perdisk=True) |
| assert len(wprocfs) == len(wsysfs) |
| |
| def test_emulate_not_impl(self): |
| def exists(path): |
| return False |
| |
| with mock.patch( |
| 'psutil._pslinux.os.path.exists', create=True, side_effect=exists |
| ): |
| with pytest.raises(NotImplementedError): |
| psutil.disk_io_counters() |
| |
| @pytest.mark.skipif( |
| not shutil.which("iostat"), reason="'iostat' command not available" |
| ) |
| @retry_on_failure() |
| def test_against_iostat(self): |
| # Cross-check read_bytes/write_bytes against 'iostat -d -k' |
| # cumulative totals (kB_read, kB_wrtn columns). |
| out = sh(["iostat", "-d", "-k"]) |
| iostat_disks = {} |
| for line in out.splitlines(): |
| fields = line.split() |
| if len(fields) < 7 or fields[0] in {"Linux", "Device"}: |
| continue |
| name = fields[0] |
| try: |
| kb_read = int(fields[5]) |
| kb_wrtn = int(fields[6]) |
| except ValueError: |
| continue |
| iostat_disks[name] = (kb_read * 1024, kb_wrtn * 1024) |
| |
| psutil_disks = psutil.disk_io_counters(perdisk=True, nowrap=False) |
| for name, (bytes_read, bytes_wrtn) in iostat_disks.items(): |
| if name not in psutil_disks: |
| continue |
| stats = psutil_disks[name] |
| assert abs(stats.read_bytes - bytes_read) < 1024 * 1024 |
| assert abs(stats.write_bytes - bytes_wrtn) < 1024 * 1024 |
| |
| |
| class TestRootFsDeviceFinder(LinuxTestCase): |
| def setUp(self): |
| dev = os.stat("/").st_dev |
| self.major = os.major(dev) |
| self.minor = os.minor(dev) |
| |
| def test_call_methods(self): |
| finder = RootFsDeviceFinder() |
| if os.path.exists("/proc/partitions"): |
| finder.ask_proc_partitions() |
| else: |
| with pytest.raises(FileNotFoundError): |
| finder.ask_proc_partitions() |
| if os.path.exists(f"/sys/dev/block/{self.major}:{self.minor}/uevent"): |
| finder.ask_sys_dev_block() |
| else: |
| with pytest.raises(FileNotFoundError): |
| finder.ask_sys_dev_block() |
| finder.ask_sys_class_block() |
| |
| @pytest.mark.skipif(GITHUB_ACTIONS, reason="unsupported on GITHUB_ACTIONS") |
| def test_comparisons(self): |
| finder = RootFsDeviceFinder() |
| assert finder.find() is not None |
| |
| a = b = c = None |
| if os.path.exists("/proc/partitions"): |
| a = finder.ask_proc_partitions() |
| if os.path.exists(f"/sys/dev/block/{self.major}:{self.minor}/uevent"): |
| b = finder.ask_sys_class_block() |
| c = finder.ask_sys_dev_block() |
| |
| base = a or b or c |
| if base and a: |
| assert base == a |
| if base and b: |
| assert base == b |
| if base and c: |
| assert base == c |
| |
| @pytest.mark.skipif( |
| not shutil.which("findmnt"), reason="findmnt utility not available" |
| ) |
| @pytest.mark.skipif(GITHUB_ACTIONS, reason="unsupported on GITHUB_ACTIONS") |
| def test_against_findmnt(self): |
| psutil_value = RootFsDeviceFinder().find() |
| findmnt_value = sh("findmnt -o SOURCE -rn /") |
| assert psutil_value == findmnt_value |
| |
| def test_disk_partitions_mocked(self): |
| with mock.patch( |
| 'psutil._pslinux.cext.disk_partitions', |
| return_value=[('/dev/root', '/', 'ext4', 'rw')], |
| ) as m: |
| part = psutil.disk_partitions()[0] |
| assert m.called |
| if not GITHUB_ACTIONS: |
| assert part.device != "/dev/root" |
| assert part.device == RootFsDeviceFinder().find() |
| else: |
| assert part.device == "/dev/root" |
| |
| |
| # ===================================================================== |
| # --- misc |
| # ===================================================================== |
| |
| |
| class TestMisc(LinuxTestCase): |
| def test_boot_time(self): |
| vmstat_value = vmstat('boot time') |
| psutil_value = psutil.boot_time() |
| assert int(vmstat_value) == int(psutil_value) |
| |
| def test_no_procfs_on_import(self): |
| my_procfs = self.get_testfn() |
| os.mkdir(my_procfs) |
| |
| with open(os.path.join(my_procfs, 'stat'), 'w') as f: |
| f.write('cpu 0 0 0 0 0 0 0 0 0 0\n') |
| f.write('cpu0 0 0 0 0 0 0 0 0 0 0\n') |
| f.write('cpu1 0 0 0 0 0 0 0 0 0 0\n') |
| |
| try: |
| orig_open = open |
| |
| def open_mock(name, *args, **kwargs): |
| if name.startswith('/proc'): |
| raise FileNotFoundError |
| return orig_open(name, *args, **kwargs) |
| |
| with mock.patch("builtins.open", side_effect=open_mock): |
| reload_module(psutil) |
| |
| with pytest.raises(OSError): |
| psutil.cpu_times() |
| with pytest.raises(OSError): |
| psutil.cpu_times(percpu=True) |
| with pytest.raises(OSError): |
| psutil.cpu_percent() |
| with pytest.raises(OSError): |
| psutil.cpu_percent(percpu=True) |
| with pytest.raises(OSError): |
| psutil.cpu_times_percent() |
| with pytest.raises(OSError): |
| psutil.cpu_times_percent(percpu=True) |
| |
| psutil.PROCFS_PATH = my_procfs |
| |
| assert psutil.cpu_percent() == 0 |
| assert sum(psutil.cpu_times_percent()) == 0 |
| |
| # since we don't know the number of CPUs at import time, |
| # we awkwardly say there are none until the second call |
| per_cpu_percent = psutil.cpu_percent(percpu=True) |
| assert sum(per_cpu_percent) == 0 |
| |
| # ditto awkward length |
| per_cpu_times_percent = psutil.cpu_times_percent(percpu=True) |
| assert sum(map(sum, per_cpu_times_percent)) == 0 |
| |
| # much user, very busy |
| with open(os.path.join(my_procfs, 'stat'), 'w') as f: |
| f.write('cpu 1 0 0 0 0 0 0 0 0 0\n') |
| f.write('cpu0 1 0 0 0 0 0 0 0 0 0\n') |
| f.write('cpu1 1 0 0 0 0 0 0 0 0 0\n') |
| |
| assert psutil.cpu_percent() != 0 |
| assert sum(psutil.cpu_percent(percpu=True)) != 0 |
| assert sum(psutil.cpu_times_percent()) != 0 |
| assert ( |
| sum(map(sum, psutil.cpu_times_percent(percpu=True))) != 0 |
| ) |
| finally: |
| shutil.rmtree(my_procfs) |
| reload_module(psutil) |
| |
| assert psutil.PROCFS_PATH == '/proc' |
| |
| def test_cpu_steal_decrease(self): |
| # Test cumulative cpu stats decrease. We should ignore this. |
| # See issue #1210. |
| content = textwrap.dedent("""\ |
| cpu 0 0 0 0 0 0 0 1 0 0 |
| cpu0 0 0 0 0 0 0 0 1 0 0 |
| cpu1 0 0 0 0 0 0 0 1 0 0 |
| """).encode() |
| with mock_open_content({"/proc/stat": content}) as m: |
| # first call to "percent" functions should read the new stat file |
| # and compare to the "real" file read at import time - so the |
| # values are meaningless |
| psutil.cpu_percent() |
| assert m.called |
| psutil.cpu_percent(percpu=True) |
| psutil.cpu_times_percent() |
| psutil.cpu_times_percent(percpu=True) |
| |
| content = textwrap.dedent("""\ |
| cpu 1 0 0 0 0 0 0 0 0 0 |
| cpu0 1 0 0 0 0 0 0 0 0 0 |
| cpu1 1 0 0 0 0 0 0 0 0 0 |
| """).encode() |
| with mock_open_content({"/proc/stat": content}): |
| # Increase "user" while steal goes "backwards" to zero. |
| cpu_percent = psutil.cpu_percent() |
| assert m.called |
| cpu_percent_percpu = psutil.cpu_percent(percpu=True) |
| cpu_times_percent = psutil.cpu_times_percent() |
| cpu_times_percent_percpu = psutil.cpu_times_percent(percpu=True) |
| assert cpu_percent != 0 |
| assert sum(cpu_percent_percpu) != 0 |
| assert sum(cpu_times_percent) != 0 |
| assert sum(cpu_times_percent) != 100.0 |
| assert sum(map(sum, cpu_times_percent_percpu)) != 0 |
| assert sum(map(sum, cpu_times_percent_percpu)) != 100.0 |
| assert cpu_times_percent.steal == 0 |
| assert cpu_times_percent.user != 0 |
| |
| def test_boot_time_mocked(self): |
| with mock.patch('psutil._common.open', create=True) as m: |
| with pytest.raises(RuntimeError): |
| psutil._pslinux.boot_time() |
| assert m.called |
| |
| def test_users(self): |
| # Make sure the C extension converts ':0' and ':0.0' to |
| # 'localhost'. |
| for user in psutil.users(): |
| assert user.host not in {":0", ":0.0"} |
| |
| def test_procfs_path(self): |
| tdir = self.get_testfn() |
| os.mkdir(tdir) |
| try: |
| psutil.PROCFS_PATH = tdir |
| with pytest.raises(OSError): |
| psutil.virtual_memory() |
| with pytest.raises(OSError): |
| psutil.cpu_times() |
| with pytest.raises(OSError): |
| psutil.cpu_times(percpu=True) |
| with pytest.raises(OSError): |
| psutil.boot_time() |
| with pytest.raises(OSError): |
| psutil.net_connections() |
| with pytest.raises(OSError): |
| psutil.net_io_counters() |
| with pytest.raises(OSError): |
| psutil.net_if_stats() |
| with pytest.raises(OSError): |
| psutil.disk_partitions() |
| with pytest.raises(psutil.NoSuchProcess): |
| psutil.Process() |
| finally: |
| psutil.PROCFS_PATH = "/proc" |
| |
| @retry_on_failure() |
| @pytest.mark.xdist_group(name="serial") |
| def test_issue_687(self): |
| # In case of thread ID: |
| # - pid_exists() is supposed to return False |
| # - Process(tid) is supposed to work |
| # - pids() should not return the TID |
| # See: https://github.com/giampaolo/psutil/issues/687 |
| |
| p = psutil.Process() |
| nthreads = len(p.threads()) |
| with ThreadTask(): |
| threads = p.threads() |
| assert len(threads) == nthreads + 1 |
| tid = sorted(threads, key=lambda x: x.id)[1].id |
| assert p.pid != tid |
| pt = psutil.Process(tid) |
| pt.as_dict() |
| assert tid not in psutil.pids() |
| |
| def test_pid_exists_no_proc_status(self): |
| # Internally pid_exists relies on /proc/{pid}/status. |
| # Emulate a case where this file is empty in which case |
| # psutil is supposed to fall back on using pids(). |
| with mock_open_content({"/proc/%s/status": ""}) as m: |
| assert psutil.pid_exists(os.getpid()) |
| assert m.called |
| |
| |
| # ===================================================================== |
| # --- sensors |
| # ===================================================================== |
| |
| |
| @pytest.mark.skipif(not HAS_BATTERY, reason="no battery") |
| class TestSensorsBattery(LinuxTestCase): |
| @pytest.mark.skipif( |
| not shutil.which("acpi"), reason="acpi utility not available" |
| ) |
| def test_percent(self): |
| out = sh("acpi -b") |
| acpi_value = int(out.split(",")[1].strip().replace('%', '')) |
| psutil_value = psutil.sensors_battery().percent |
| assert abs(acpi_value - psutil_value) < 1 |
| |
| def test_emulate_power_plugged(self): |
| # Pretend the AC power cable is connected. |
| def open_mock(name, *args, **kwargs): |
| if name.endswith(('AC0/online', 'AC/online')): |
| return io.BytesIO(b"1") |
| else: |
| return orig_open(name, *args, **kwargs) |
| |
| orig_open = open |
| with mock.patch("builtins.open", side_effect=open_mock) as m: |
| assert psutil.sensors_battery().power_plugged is True |
| assert ( |
| psutil.sensors_battery().secsleft |
| == psutil.POWER_TIME_UNLIMITED |
| ) |
| assert m.called |
| |
| def test_emulate_power_plugged_2(self): |
| # Same as above but pretend /AC0/online does not exist in which |
| # case code relies on /status file. |
| def open_mock(name, *args, **kwargs): |
| if name.endswith(('AC0/online', 'AC/online')): |
| raise FileNotFoundError |
| if name.endswith("/status"): |
| return io.StringIO("charging") |
| else: |
| return orig_open(name, *args, **kwargs) |
| |
| orig_open = open |
| with mock.patch("builtins.open", side_effect=open_mock) as m: |
| assert psutil.sensors_battery().power_plugged is True |
| assert m.called |
| |
| def test_emulate_power_not_plugged(self): |
| # Pretend the AC power cable is not connected. |
| def open_mock(name, *args, **kwargs): |
| if name.endswith(('AC0/online', 'AC/online')): |
| return io.BytesIO(b"0") |
| else: |
| return orig_open(name, *args, **kwargs) |
| |
| orig_open = open |
| with mock.patch("builtins.open", side_effect=open_mock) as m: |
| assert psutil.sensors_battery().power_plugged is False |
| assert m.called |
| |
| def test_emulate_power_not_plugged_2(self): |
| # Same as above but pretend /AC0/online does not exist in which |
| # case code relies on /status file. |
| def open_mock(name, *args, **kwargs): |
| if name.endswith(('AC0/online', 'AC/online')): |
| raise FileNotFoundError |
| if name.endswith("/status"): |
| return io.StringIO("discharging") |
| else: |
| return orig_open(name, *args, **kwargs) |
| |
| orig_open = open |
| with mock.patch("builtins.open", side_effect=open_mock) as m: |
| assert psutil.sensors_battery().power_plugged is False |
| assert m.called |
| |
| def test_emulate_power_undetermined(self): |
| # Pretend we can't know whether the AC power cable not |
| # connected (assert fallback to False). |
| def open_mock(name, *args, **kwargs): |
| if name.startswith(( |
| '/sys/class/power_supply/AC0/online', |
| '/sys/class/power_supply/AC/online', |
| )): |
| raise FileNotFoundError |
| if name.startswith("/sys/class/power_supply/BAT0/status"): |
| return io.BytesIO(b"???") |
| else: |
| return orig_open(name, *args, **kwargs) |
| |
| orig_open = open |
| with mock.patch("builtins.open", side_effect=open_mock) as m: |
| assert psutil.sensors_battery().power_plugged is None |
| assert m.called |
| |
| def test_emulate_energy_full_0(self): |
| # Emulate a case where energy_full files returns 0. |
| with mock_open_content( |
| {"/sys/class/power_supply/BAT0/energy_full": b"0"} |
| ) as m: |
| assert psutil.sensors_battery().percent == 0 |
| assert m.called |
| |
| def test_emulate_energy_full_not_avail(self): |
| # Emulate a case where energy_full file does not exist. |
| # Expected fallback on /capacity. |
| with mock_open_exception( |
| "/sys/class/power_supply/BAT0/energy_full", |
| FileNotFoundError, |
| ): |
| with mock_open_exception( |
| "/sys/class/power_supply/BAT0/charge_full", |
| FileNotFoundError, |
| ): |
| with mock_open_content( |
| {"/sys/class/power_supply/BAT0/capacity": b"88"} |
| ): |
| assert psutil.sensors_battery().percent == 88 |
| |
| @pytest.mark.skipif( |
| not os.path.isfile("/sys/class/power_supply/BAT0/capacity"), |
| reason="BAT /capacity file don't exist", |
| ) |
| def test_percent_against_capacity(self): |
| # Internally, if we have /energy_full, the percentage will be |
| # calculated by NOT reading the /capacity file, to get more |
| # accuracy. Check against /capacity to make sure our percentage |
| # is calculated correctly. |
| with open("/sys/class/power_supply/BAT0/capacity") as f: |
| capacity = float(f.read()) |
| assert psutil.sensors_battery().percent == pytest.approx( |
| capacity, abs=1 |
| ) |
| |
| def test_emulate_no_power(self): |
| # Emulate a case where /AC0/online file nor /BAT0/status exist. |
| with mock_open_exception( |
| "/sys/class/power_supply/AC/online", FileNotFoundError |
| ): |
| with mock_open_exception( |
| "/sys/class/power_supply/AC0/online", FileNotFoundError |
| ): |
| with mock_open_exception( |
| "/sys/class/power_supply/BAT0/status", |
| FileNotFoundError, |
| ): |
| assert psutil.sensors_battery().power_plugged is None |
| |
| def test_fully_emulated(self): |
| def open_mock(name, *args, **kwargs): |
| if name.endswith("/energy_now"): |
| return io.StringIO("60000000") |
| elif name.endswith("/power_now"): |
| return io.StringIO("0") |
| elif name.endswith("/energy_full"): |
| return io.StringIO("60000001") |
| else: |
| return orig_open(name, *args, **kwargs) |
| |
| orig_open = open |
| with mock.patch('os.listdir', return_value=["BAT0"]) as mlistdir: |
| with mock.patch("builtins.open", side_effect=open_mock) as mopen: |
| assert psutil.sensors_battery() is not None |
| assert mlistdir.called |
| assert mopen.called |
| |
| |
| class TestSensorsTemperatures(LinuxTestCase): |
| def test_emulate_class_hwmon(self): |
| def open_mock(name, *args, **kwargs): |
| if name.endswith('/name'): |
| return io.StringIO("name") |
| elif name.endswith('/temp1_label'): |
| return io.StringIO("label") |
| elif name.endswith('/temp1_input'): |
| return io.BytesIO(b"30000") |
| elif name.endswith('/temp1_max'): |
| return io.BytesIO(b"40000") |
| elif name.endswith('/temp1_crit'): |
| return io.BytesIO(b"50000") |
| else: |
| return orig_open(name, *args, **kwargs) |
| |
| orig_open = open |
| with mock.patch("builtins.open", side_effect=open_mock): |
| # Test case with /sys/class/hwmon |
| with mock.patch( |
| 'glob.glob', return_value=['/sys/class/hwmon/hwmon0/temp1'] |
| ): |
| temp = psutil.sensors_temperatures()['name'][0] |
| assert temp.label == 'label' |
| assert temp.current == 30.0 |
| assert temp.high == 40.0 |
| assert temp.critical == 50.0 |
| |
| def test_emulate_class_thermal(self): |
| def open_mock(name, *args, **kwargs): |
| if name.endswith('0_temp'): |
| return io.BytesIO(b"50000") |
| elif name.endswith('temp'): |
| return io.BytesIO(b"30000") |
| elif name.endswith('0_type'): |
| return io.StringIO("critical") |
| elif name.endswith('type'): |
| return io.StringIO("name") |
| else: |
| return orig_open(name, *args, **kwargs) |
| |
| def glob_mock(path): |
| if path in { |
| '/sys/class/hwmon/hwmon*/temp*_*', |
| '/sys/class/hwmon/hwmon*/device/temp*_*', |
| }: |
| return [] |
| elif path == '/sys/class/thermal/thermal_zone*': |
| return ['/sys/class/thermal/thermal_zone0'] |
| elif path == '/sys/class/thermal/thermal_zone0/trip_point*': |
| return [ |
| '/sys/class/thermal/thermal_zone1/trip_point_0_type', |
| '/sys/class/thermal/thermal_zone1/trip_point_0_temp', |
| ] |
| return [] |
| |
| orig_open = open |
| with mock.patch("builtins.open", side_effect=open_mock): |
| with mock.patch('glob.glob', create=True, side_effect=glob_mock): |
| temp = psutil.sensors_temperatures()['name'][0] |
| assert temp.label == '' |
| assert temp.current == 30.0 |
| assert temp.high == 50.0 |
| assert temp.critical == 50.0 |
| |
| |
| class TestSensorsFans(LinuxTestCase): |
| def test_emulate_data(self): |
| def open_mock(name, *args, **kwargs): |
| if name.endswith('/name'): |
| return io.StringIO("name") |
| elif name.endswith('/fan1_label'): |
| return io.StringIO("label") |
| elif name.endswith('/fan1_input'): |
| return io.StringIO("2000") |
| else: |
| return orig_open(name, *args, **kwargs) |
| |
| orig_open = open |
| with mock.patch("builtins.open", side_effect=open_mock): |
| with mock.patch( |
| 'glob.glob', return_value=['/sys/class/hwmon/hwmon2/fan1'] |
| ): |
| fan = psutil.sensors_fans()['name'][0] |
| assert fan.label == 'label' |
| assert fan.current == 2000 |
| |
| |
| # ===================================================================== |
| # --- test process |
| # ===================================================================== |
| |
| |
| class TestProcess(LinuxTestCase): |
| @retry_on_failure() |
| def test_parse_smaps_vs_memory_maps(self): |
| sproc = self.spawn_subproc() |
| uss, pss, swap = psutil._pslinux.Process(sproc.pid)._parse_smaps() |
| maps = psutil.Process(sproc.pid).memory_maps(grouped=False) |
| assert ( |
| abs(uss - sum(x.private_dirty + x.private_clean for x in maps)) |
| < 4096 |
| ) |
| assert abs(pss - sum(x.pss for x in maps)) < 4096 |
| assert abs(swap - sum(x.swap for x in maps)) < 4096 |
| |
| def test_parse_smaps_mocked(self): |
| # See: https://github.com/giampaolo/psutil/issues/1222 |
| content = textwrap.dedent("""\ |
| fffff0 r-xp 00000000 00:00 0 [vsyscall] |
| Size: 1 kB |
| Rss: 2 kB |
| Pss: 3 kB |
| Shared_Clean: 4 kB |
| Shared_Dirty: 5 kB |
| Private_Clean: 6 kB |
| Private_Dirty: 7 kB |
| Referenced: 8 kB |
| Anonymous: 9 kB |
| LazyFree: 10 kB |
| AnonHugePages: 11 kB |
| ShmemPmdMapped: 12 kB |
| Shared_Hugetlb: 13 kB |
| Private_Hugetlb: 14 kB |
| Swap: 15 kB |
| SwapPss: 16 kB |
| KernelPageSize: 17 kB |
| MMUPageSize: 18 kB |
| Locked: 19 kB |
| VmFlags: rd ex |
| """).encode() |
| with mock_open_content({f"/proc/{os.getpid()}/smaps": content}) as m: |
| p = psutil._pslinux.Process(os.getpid()) |
| uss, pss, swap = p._parse_smaps() |
| assert m.called |
| assert uss == (6 + 7 + 14) * 1024 |
| assert pss == 3 * 1024 |
| assert swap == 15 * 1024 |
| |
| def test_open_files_mode(self): |
| def get_test_file(fname): |
| p = psutil.Process() |
| giveup_at = time.time() + GLOBAL_TIMEOUT |
| while True: |
| for file in p.open_files(): |
| if file.path == os.path.abspath(fname): |
| return file |
| elif time.time() > giveup_at: |
| break |
| raise RuntimeError("timeout looking for test file") |
| |
| testfn = self.get_testfn() |
| with open(testfn, "w"): |
| assert get_test_file(testfn).mode == "w" |
| with open(testfn): |
| assert get_test_file(testfn).mode == "r" |
| with open(testfn, "a"): |
| assert get_test_file(testfn).mode == "a" |
| with open(testfn, "r+"): |
| assert get_test_file(testfn).mode == "r+" |
| with open(testfn, "w+"): |
| assert get_test_file(testfn).mode == "r+" |
| with open(testfn, "a+"): |
| assert get_test_file(testfn).mode == "a+" |
| |
| safe_rmpath(testfn) |
| with open(testfn, "x"): |
| assert get_test_file(testfn).mode == "w" |
| safe_rmpath(testfn) |
| with open(testfn, "x+"): |
| assert get_test_file(testfn).mode == "r+" |
| |
| def test_open_files_file_gone(self): |
| # simulates a file which gets deleted during open_files() |
| # execution |
| p = psutil.Process() |
| files = p.open_files() |
| with open(self.get_testfn(), 'w'): |
| # give the kernel some time to see the new file |
| call_until(lambda: len(p.open_files()) != len(files)) |
| with mock.patch( |
| 'psutil._pslinux.os.readlink', |
| side_effect=FileNotFoundError, |
| ) as m: |
| assert p.open_files() == [] |
| assert m.called |
| # also simulate the case where os.readlink() returns EINVAL |
| # in which case psutil is supposed to 'continue' |
| with mock.patch( |
| 'psutil._pslinux.os.readlink', |
| side_effect=OSError(errno.EINVAL, ""), |
| ) as m: |
| assert p.open_files() == [] |
| assert m.called |
| |
| def test_open_files_fd_gone(self): |
| # Simulate a case where /proc/{pid}/fdinfo/{fd} disappears |
| # while iterating through fds. |
| # https://travis-ci.org/giampaolo/psutil/jobs/225694530 |
| p = psutil.Process() |
| files = p.open_files() |
| with open(self.get_testfn(), 'w'): |
| # give the kernel some time to see the new file |
| call_until(lambda: len(p.open_files()) != len(files)) |
| with mock.patch( |
| "builtins.open", side_effect=FileNotFoundError |
| ) as m: |
| assert p.open_files() == [] |
| assert m.called |
| |
| def test_open_files_enametoolong(self): |
| # Simulate a case where /proc/{pid}/fd/{fd} symlink |
| # points to a file with full path longer than PATH_MAX, see: |
| # https://github.com/giampaolo/psutil/issues/1940 |
| p = psutil.Process() |
| files = p.open_files() |
| with open(self.get_testfn(), 'w'): |
| # give the kernel some time to see the new file |
| call_until(lambda: len(p.open_files()) != len(files)) |
| patch_point = 'psutil._pslinux.os.readlink' |
| with mock.patch( |
| patch_point, side_effect=OSError(errno.ENAMETOOLONG, "") |
| ) as m: |
| with mock.patch("psutil._pslinux.debug"): |
| assert p.open_files() == [] |
| assert m.called |
| |
| # --- mocked tests |
| |
| def test_terminal_mocked(self): |
| with mock.patch( |
| 'psutil._pslinux._psposix.get_terminal_map', return_value={} |
| ) as m: |
| assert psutil._pslinux.Process(os.getpid()).terminal() is None |
| assert m.called |
| |
| def test_cmdline_mocked(self): |
| # see: https://github.com/giampaolo/psutil/issues/639 |
| p = psutil.Process() |
| fake_file = io.StringIO('foo\x00bar\x00') |
| with mock.patch( |
| 'psutil._common.open', return_value=fake_file, create=True |
| ) as m: |
| assert p.cmdline() == ['foo', 'bar'] |
| assert m.called |
| fake_file = io.StringIO('foo\x00bar\x00\x00') |
| with mock.patch( |
| 'psutil._common.open', return_value=fake_file, create=True |
| ) as m: |
| assert p.cmdline() == ['foo', 'bar', ''] |
| assert m.called |
| |
| def test_cmdline_spaces_mocked(self): |
| # see: https://github.com/giampaolo/psutil/issues/1179 |
| p = psutil.Process() |
| fake_file = io.StringIO('foo bar ') |
| with mock.patch( |
| 'psutil._common.open', return_value=fake_file, create=True |
| ) as m: |
| assert p.cmdline() == ['foo', 'bar'] |
| assert m.called |
| fake_file = io.StringIO('foo bar ') |
| with mock.patch( |
| 'psutil._common.open', return_value=fake_file, create=True |
| ) as m: |
| assert p.cmdline() == ['foo', 'bar', ''] |
| assert m.called |
| |
| def test_cmdline_mixed_separators(self): |
| # https://github.com/giampaolo/psutil/issues/1179#issuecomment-552984549 |
| p = psutil.Process() |
| fake_file = io.StringIO('foo\x20bar\x00') |
| with mock.patch( |
| 'psutil._common.open', return_value=fake_file, create=True |
| ) as m: |
| assert p.cmdline() == ['foo', 'bar'] |
| assert m.called |
| |
| def test_readlink_path_deleted_mocked(self): |
| with mock.patch( |
| 'psutil._pslinux.os.readlink', return_value='/home/foo (deleted)' |
| ): |
| assert psutil.Process().exe() == "/home/foo" |
| assert psutil.Process().cwd() == "/home/foo" |
| |
| def test_threads_mocked(self): |
| # Test the case where os.listdir() returns a file (thread) |
| # which no longer exists by the time we open() it (race |
| # condition). threads() is supposed to ignore that instead |
| # of raising NSP. |
| def open_mock_1(name, *args, **kwargs): |
| if name.startswith(f"/proc/{os.getpid()}/task"): |
| raise FileNotFoundError |
| return orig_open(name, *args, **kwargs) |
| |
| orig_open = open |
| with mock.patch("builtins.open", side_effect=open_mock_1) as m: |
| ret = psutil.Process().threads() |
| assert m.called |
| assert ret == [] |
| |
| # ...but if it bumps into something != ENOENT we want an |
| # exception. |
| def open_mock_2(name, *args, **kwargs): |
| if name.startswith(f"/proc/{os.getpid()}/task"): |
| raise PermissionError |
| return orig_open(name, *args, **kwargs) |
| |
| with mock.patch("builtins.open", side_effect=open_mock_2): |
| with pytest.raises(psutil.AccessDenied): |
| psutil.Process().threads() |
| |
| def test_exe_mocked(self): |
| with mock.patch( |
| 'psutil._pslinux.readlink', side_effect=FileNotFoundError |
| ) as m: |
| # de-activate guessing from cmdline() |
| with mock.patch( |
| 'psutil._pslinux.Process.cmdline', return_value=[] |
| ): |
| ret = psutil.Process().exe() |
| assert m.called |
| assert ret == "" |
| |
| def test_cwd_mocked(self): |
| # https://github.com/giampaolo/psutil/issues/2514 |
| with mock.patch( |
| 'psutil._pslinux.readlink', side_effect=FileNotFoundError |
| ) as m: |
| ret = psutil.Process().cwd() |
| assert m.called |
| assert ret == "" |
| |
| def test_issue_1014(self): |
| # Emulates a case where smaps file does not exist. In this case |
| # wrap_exception decorator should not raise NoSuchProcess. |
| with mock_open_exception( |
| f"/proc/{os.getpid()}/smaps", FileNotFoundError |
| ) as m: |
| p = psutil.Process() |
| with pytest.raises(FileNotFoundError): |
| p.memory_maps() |
| assert m.called |
| |
| def test_issue_2418(self): |
| p = psutil.Process() |
| with mock_open_exception( |
| f"/proc/{os.getpid()}/statm", FileNotFoundError |
| ): |
| with mock.patch("os.path.exists", return_value=False): |
| with pytest.raises(psutil.NoSuchProcess): |
| p.memory_info() |
| |
| @pytest.mark.skipif(not HAS_PROC_RLIMIT, reason="not supported") |
| def test_rlimit_zombie(self): |
| # Emulate a case where rlimit() raises ENOSYS, which may |
| # happen in case of zombie process: |
| # https://travis-ci.org/giampaolo/psutil/jobs/51368273 |
| with mock.patch( |
| "resource.prlimit", side_effect=OSError(errno.ENOSYS, "") |
| ) as m1: |
| with mock.patch( |
| "psutil._pslinux.Process._is_zombie", return_value=True |
| ) as m2: |
| p = psutil.Process() |
| p.name() |
| with pytest.raises(psutil.ZombieProcess) as cm: |
| p.rlimit(psutil.RLIMIT_NOFILE) |
| assert m1.called |
| assert m2.called |
| assert cm.value.pid == p.pid |
| assert cm.value.name == p.name() |
| |
| def test_stat_file_parsing(self): |
| args = [ |
| "0", # pid |
| "(cat)", # name |
| "Z", # status |
| "1", # ppid |
| "0", # pgrp |
| "0", # session |
| "0", # tty |
| "0", # tpgid |
| "0", # flags |
| "0", # minflt |
| "0", # cminflt |
| "0", # majflt |
| "0", # cmajflt |
| "2", # utime |
| "3", # stime |
| "4", # cutime |
| "5", # cstime |
| "0", # priority |
| "0", # nice |
| "0", # num_threads |
| "0", # itrealvalue |
| "6", # starttime |
| "0", # vsize |
| "0", # rss |
| "0", # rsslim |
| "0", # startcode |
| "0", # endcode |
| "0", # startstack |
| "0", # kstkesp |
| "0", # kstkeip |
| "0", # signal |
| "0", # blocked |
| "0", # sigignore |
| "0", # sigcatch |
| "0", # wchan |
| "0", # nswap |
| "0", # cnswap |
| "0", # exit_signal |
| "6", # processor |
| "0", # rt priority |
| "0", # policy |
| "7", # delayacct_blkio_ticks |
| ] |
| content = " ".join(args).encode() |
| with mock_open_content({f"/proc/{os.getpid()}/stat": content}): |
| p = psutil.Process() |
| assert p.name() == 'cat' |
| assert p.status() == psutil.STATUS_ZOMBIE |
| assert p.ppid() == 1 |
| assert p.create_time() == 6 / CLOCK_TICKS + psutil.boot_time() |
| cpu = p.cpu_times() |
| assert cpu.user == 2 / CLOCK_TICKS |
| assert cpu.system == 3 / CLOCK_TICKS |
| assert cpu.children_user == 4 / CLOCK_TICKS |
| assert cpu.children_system == 5 / CLOCK_TICKS |
| assert cpu.iowait == 7 / CLOCK_TICKS |
| assert p.cpu_num() == 6 |
| |
| def test_status_file_parsing(self): |
| content = textwrap.dedent("""\ |
| Uid:\t1000\t1001\t1002\t1003 |
| Gid:\t1004\t1005\t1006\t1007 |
| Threads:\t66 |
| Cpus_allowed:\tf |
| Cpus_allowed_list:\t0-7 |
| voluntary_ctxt_switches:\t12 |
| nonvoluntary_ctxt_switches:\t13""").encode() |
| with mock_open_content({f"/proc/{os.getpid()}/status": content}): |
| p = psutil.Process() |
| assert p.num_ctx_switches().voluntary == 12 |
| assert p.num_ctx_switches().involuntary == 13 |
| assert p.num_threads() == 66 |
| uids = p.uids() |
| assert uids.real == 1000 |
| assert uids.effective == 1001 |
| assert uids.saved == 1002 |
| gids = p.gids() |
| assert gids.real == 1004 |
| assert gids.effective == 1005 |
| assert gids.saved == 1006 |
| assert p._proc._get_eligible_cpus() == list(range(8)) |
| |
| def test_net_connections_enametoolong(self): |
| # Simulate a case where /proc/{pid}/fd/{fd} symlink points to |
| # a file with full path longer than PATH_MAX, see: |
| # https://github.com/giampaolo/psutil/issues/1940 |
| with mock.patch( |
| 'psutil._pslinux.os.readlink', |
| side_effect=OSError(errno.ENAMETOOLONG, ""), |
| ) as m: |
| p = psutil.Process() |
| with mock.patch("psutil._pslinux.debug"): |
| assert p.net_connections() == [] |
| assert m.called |
| |
| def test_create_time_monotonic(self): |
| p = psutil.Process() |
| assert p._proc.create_time() != p._proc.create_time(monotonic=True) |
| assert p._get_ident()[1] == p._proc.create_time(monotonic=True) |
| |
| def test_memory_info_ex(self): |
| mem = psutil.Process().memory_info_ex() |
| assert mem.rss == mem.rss_anon + mem.rss_file + mem.rss_shmem |
| |
| |
| class TestProcessAgainstStatus(LinuxTestCase): |
| """/proc/pid/stat and /proc/pid/status have many values in common. |
| Whenever possible, psutil uses /proc/pid/stat (it's faster). |
| For all those cases we check that the value found in |
| /proc/pid/stat (by psutil) matches the one found in |
| /proc/pid/status. |
| """ |
| |
| @classmethod |
| def setUpClass(cls): |
| cls.proc = psutil.Process() |
| |
| def read_status_file(self, linestart): |
| with psutil._psplatform.open_text( |
| f"/proc/{self.proc.pid}/status" |
| ) as f: |
| for line in f: |
| line = line.strip() |
| if line.startswith(linestart): |
| value = line.partition('\t')[2] |
| try: |
| return int(value) |
| except ValueError: |
| return value |
| raise ValueError(f"can't find {linestart!r}") |
| |
| def test_name(self): |
| value = self.read_status_file("Name:") |
| assert self.proc.name() == value |
| |
| def test_status(self): |
| value = self.read_status_file("State:") |
| value = value[value.find('(') + 1 : value.rfind(')')] |
| value = value.replace(' ', '-') |
| assert self.proc.status() == value |
| |
| def test_ppid(self): |
| value = self.read_status_file("PPid:") |
| assert self.proc.ppid() == value |
| |
| def test_num_threads(self): |
| value = self.read_status_file("Threads:") |
| assert self.proc.num_threads() == value |
| |
| def test_uids(self): |
| value = self.read_status_file("Uid:") |
| value = tuple(map(int, value.split()[1:4])) |
| assert self.proc.uids() == value |
| |
| def test_gids(self): |
| value = self.read_status_file("Gid:") |
| value = tuple(map(int, value.split()[1:4])) |
| assert self.proc.gids() == value |
| |
| @retry_on_failure() |
| def test_num_ctx_switches(self): |
| value = self.read_status_file("voluntary_ctxt_switches:") |
| assert self.proc.num_ctx_switches().voluntary == value |
| value = self.read_status_file("nonvoluntary_ctxt_switches:") |
| assert self.proc.num_ctx_switches().involuntary == value |
| |
| def test_cpu_affinity(self): |
| value = self.read_status_file("Cpus_allowed_list:") |
| if '-' in str(value): |
| min_, max_ = map(int, value.split('-')) |
| assert self.proc.cpu_affinity() == list(range(min_, max_ + 1)) |
| |
| def test_cpu_affinity_eligible_cpus(self): |
| value = self.read_status_file("Cpus_allowed_list:") |
| with mock.patch("psutil._pslinux.per_cpu_times") as m: |
| self.proc._proc._get_eligible_cpus() |
| if '-' in str(value): |
| assert not m.called |
| else: |
| assert m.called |
| |
| |
| # ===================================================================== |
| # --- test utils |
| # ===================================================================== |
| |
| |
| class TestUtils(LinuxTestCase): |
| def test_readlink(self): |
| with mock.patch("os.readlink", return_value="foo (deleted)") as m: |
| assert psutil._psplatform.readlink("bar") == "foo" |
| assert m.called |