Add tests for System Monitor (#107891)

* Add tests

* no coordinator

* Coverage

* processes

* test init

* util

* test icon

* Add tests

* Mod tests

* Add tests

* Test attributes

* snapshots

* icon

* test disk mounts

* fixes

* svmem

* cache_clear

* test icon

* reset icon test

* test_processor_temperature

* fix tests on macos

---------

Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
G Johansson 2024-01-15 18:26:49 +01:00 committed by GitHub
parent 749ef45727
commit 5b3e1306f8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 1147 additions and 16 deletions

View file

@ -1316,9 +1316,6 @@ omit =
homeassistant/components/system_bridge/notify.py
homeassistant/components/system_bridge/sensor.py
homeassistant/components/system_bridge/update.py
homeassistant/components/systemmonitor/__init__.py
homeassistant/components/systemmonitor/sensor.py
homeassistant/components/systemmonitor/util.py
homeassistant/components/tado/__init__.py
homeassistant/components/tado/binary_sensor.py
homeassistant/components/tado/climate.py

View file

@ -4,12 +4,12 @@ from __future__ import annotations
import asyncio
from dataclasses import dataclass
from datetime import datetime, timedelta
from functools import cache
from functools import cache, lru_cache
import logging
import os
import socket
import sys
from typing import Any
from typing import Any, Literal
import psutil
import voluptuous as vol
@ -56,10 +56,6 @@ _LOGGER = logging.getLogger(__name__)
CONF_ARG = "arg"
if sys.maxsize > 2**32:
CPU_ICON = "mdi:cpu-64-bit"
else:
CPU_ICON = "mdi:cpu-32-bit"
SENSOR_TYPE_NAME = 0
SENSOR_TYPE_UOM = 1
@ -70,6 +66,14 @@ SENSOR_TYPE_MANDATORY_ARG = 4
SIGNAL_SYSTEMMONITOR_UPDATE = "systemmonitor_update"
@lru_cache
def get_cpu_icon() -> Literal["mdi:cpu-64-bit", "mdi:cpu-32-bit"]:
"""Return cpu icon."""
if sys.maxsize > 2**32:
return "mdi:cpu-64-bit"
return "mdi:cpu-32-bit"
@dataclass(frozen=True)
class SysMonitorSensorEntityDescription(SensorEntityDescription):
"""Description for System Monitor sensor entities."""
@ -121,19 +125,19 @@ SENSOR_TYPES: dict[str, SysMonitorSensorEntityDescription] = {
"load_15m": SysMonitorSensorEntityDescription(
key="load_15m",
name="Load (15m)",
icon=CPU_ICON,
icon=get_cpu_icon(),
state_class=SensorStateClass.MEASUREMENT,
),
"load_1m": SysMonitorSensorEntityDescription(
key="load_1m",
name="Load (1m)",
icon=CPU_ICON,
icon=get_cpu_icon(),
state_class=SensorStateClass.MEASUREMENT,
),
"load_5m": SysMonitorSensorEntityDescription(
key="load_5m",
name="Load (5m)",
icon=CPU_ICON,
icon=get_cpu_icon(),
state_class=SensorStateClass.MEASUREMENT,
),
"memory_free": SysMonitorSensorEntityDescription(
@ -210,14 +214,14 @@ SENSOR_TYPES: dict[str, SysMonitorSensorEntityDescription] = {
"process": SysMonitorSensorEntityDescription(
key="process",
name="Process",
icon=CPU_ICON,
icon=get_cpu_icon(),
mandatory_arg=True,
),
"processor_use": SysMonitorSensorEntityDescription(
key="processor_use",
name="Processor use",
native_unit_of_measurement=PERCENTAGE,
icon=CPU_ICON,
icon=get_cpu_icon(),
state_class=SensorStateClass.MEASUREMENT,
),
"processor_temperature": SysMonitorSensorEntityDescription(
@ -751,7 +755,11 @@ def _getloadavg() -> tuple[float, float, float]:
def _read_cpu_temperature() -> float | None:
"""Attempt to read CPU / processor temperature."""
temps = psutil.sensors_temperatures()
try:
temps = psutil.sensors_temperatures()
except AttributeError:
# Linux, macOS
return None
for name, entries in temps.items():
for i, entry in enumerate(entries, start=1):

View file

@ -1,11 +1,61 @@
"""Fixtures for the System Monitor integration."""
from __future__ import annotations
from collections import namedtuple
from collections.abc import Generator
from unittest.mock import AsyncMock, patch
import socket
from unittest.mock import AsyncMock, Mock, patch
from psutil import NoSuchProcess, Process
from psutil._common import sdiskpart, sdiskusage, shwtemp, snetio, snicaddr, sswap
import pytest
from homeassistant.components.systemmonitor.const import DOMAIN
from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry
# Different depending on platform so making according to Linux
svmem = namedtuple(
"svmem",
[
"total",
"available",
"percent",
"used",
"free",
"active",
"inactive",
"buffers",
"cached",
"shared",
"slab",
],
)
@pytest.fixture(autouse=True)
def mock_sys_platform() -> Generator[None, None, None]:
"""Mock sys platform to Linux."""
with patch("sys.platform", "linux"):
yield
class MockProcess(Process):
"""Mock a Process class."""
def __init__(self, name: str, ex: bool = False) -> None:
"""Initialize the process."""
super().__init__(1)
self._name = name
self._ex = ex
def name(self):
"""Return a name."""
if self._ex:
raise NoSuchProcess(1, self._name)
return self._name
@pytest.fixture
def mock_setup_entry() -> Generator[AsyncMock, None, None]:
@ -15,3 +65,184 @@ def mock_setup_entry() -> Generator[AsyncMock, None, None]:
return_value=True,
) as mock_setup_entry:
yield mock_setup_entry
@pytest.fixture
def mock_config_entry() -> MockConfigEntry:
"""Mock ConfigEntry."""
return MockConfigEntry(
title="System Monitor",
domain=DOMAIN,
data={},
options={
"sensor": {"process": ["python3", "pip"]},
"resources": [
"disk_use_percent_/",
"disk_use_percent_/home/notexist/",
"memory_free_",
"network_out_eth0",
"process_python3",
],
},
)
@pytest.fixture
async def mock_added_config_entry(
hass: HomeAssistant,
mock_psutil: Mock,
mock_os: Mock,
mock_util: Mock,
mock_config_entry: MockConfigEntry,
) -> MockConfigEntry:
"""Mock ConfigEntry that's been added to HA."""
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
assert DOMAIN in hass.config_entries.async_domains()
return mock_config_entry
@pytest.fixture
def mock_process() -> list[MockProcess]:
"""Mock process."""
_process_python = MockProcess("python3")
_process_pip = MockProcess("pip")
return [_process_python, _process_pip]
@pytest.fixture
def mock_psutil(mock_process: list[MockProcess]) -> Mock:
"""Mock psutil."""
with patch(
"homeassistant.components.systemmonitor.sensor.psutil",
autospec=True,
) as mock_psutil:
mock_psutil.disk_usage.return_value = sdiskusage(
500 * 1024**2, 300 * 1024**2, 200 * 1024**2, 60.0
)
mock_psutil.swap_memory.return_value = sswap(
100 * 1024**2, 60 * 1024**2, 40 * 1024**2, 60.0, 1, 1
)
mock_psutil.virtual_memory.return_value = svmem(
100 * 1024**2,
40 * 1024**2,
40.0,
60 * 1024**2,
30 * 1024**2,
1,
1,
1,
1,
1,
1,
)
mock_psutil.net_io_counters.return_value = {
"eth0": snetio(100 * 1024**2, 100 * 1024**2, 50, 50, 0, 0, 0, 0),
"eth1": snetio(200 * 1024**2, 200 * 1024**2, 150, 150, 0, 0, 0, 0),
"vethxyzxyz": snetio(300 * 1024**2, 300 * 1024**2, 150, 150, 0, 0, 0, 0),
}
mock_psutil.net_if_addrs.return_value = {
"eth0": [
snicaddr(
socket.AF_INET,
"192.168.1.1",
"255.255.255.0",
"255.255.255.255",
None,
)
],
"eth1": [
snicaddr(
socket.AF_INET,
"192.168.10.1",
"255.255.255.0",
"255.255.255.255",
None,
)
],
"vethxyzxyz": [
snicaddr(
socket.AF_INET,
"172.16.10.1",
"255.255.255.0",
"255.255.255.255",
None,
)
],
}
mock_psutil.cpu_percent.return_value = 10.0
mock_psutil.boot_time.return_value = 1703973338.0
mock_psutil.process_iter.return_value = mock_process
# sensors_temperatures not available on MacOS so we
# need to override the spec
mock_psutil.sensors_temperatures = Mock()
mock_psutil.sensors_temperatures.return_value = {
"cpu0-thermal": [shwtemp("cpu0-thermal", 50.0, 60.0, 70.0)]
}
mock_psutil.NoSuchProcess = NoSuchProcess
yield mock_psutil
@pytest.fixture
def mock_util(mock_process) -> Mock:
"""Mock psutil."""
with patch(
"homeassistant.components.systemmonitor.util.psutil", autospec=True
) as mock_util:
mock_util.net_if_addrs.return_value = {
"eth0": [
snicaddr(
socket.AF_INET,
"192.168.1.1",
"255.255.255.0",
"255.255.255.255",
None,
)
],
"eth1": [
snicaddr(
socket.AF_INET,
"192.168.10.1",
"255.255.255.0",
"255.255.255.255",
None,
)
],
"vethxyzxyz": [
snicaddr(
socket.AF_INET,
"172.16.10.1",
"255.255.255.0",
"255.255.255.255",
None,
)
],
}
mock_process = [MockProcess("python3")]
mock_util.process_iter.return_value = mock_process
# sensors_temperatures not available on MacOS so we
# need to override the spec
mock_util.sensors_temperatures = Mock()
mock_util.sensors_temperatures.return_value = {
"cpu0-thermal": [shwtemp("cpu0-thermal", 50.0, 60.0, 70.0)]
}
mock_util.disk_partitions.return_value = [
sdiskpart("test", "/", "ext4", "", 1, 1),
sdiskpart("test2", "/media/share", "ext4", "", 1, 1),
sdiskpart("test3", "/incorrect", "", "", 1, 1),
sdiskpart("proc", "/proc/run", "proc", "", 1, 1),
]
mock_util.disk_usage.return_value = sdiskusage(10, 10, 0, 0)
yield mock_util
@pytest.fixture
def mock_os() -> Mock:
"""Mock os."""
with patch("homeassistant.components.systemmonitor.sensor.os") as mock_os, patch(
"homeassistant.components.systemmonitor.util.os"
) as mock_os_util:
mock_os_util.name = "nt"
mock_os.getloadavg.return_value = (1, 2, 3)
yield mock_os

View file

@ -0,0 +1,399 @@
# serializer version: 1
# name: test_sensor[System Monitor Disk free / - attributes]
ReadOnlyDict({
'device_class': 'data_size',
'friendly_name': 'System Monitor Disk free /',
'icon': 'mdi:harddisk',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfInformation.GIBIBYTES: 'GiB'>,
})
# ---
# name: test_sensor[System Monitor Disk free / - state]
'0.2'
# ---
# name: test_sensor[System Monitor Disk free /media/share - attributes]
ReadOnlyDict({
'device_class': 'data_size',
'friendly_name': 'System Monitor Disk free /media/share',
'icon': 'mdi:harddisk',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfInformation.GIBIBYTES: 'GiB'>,
})
# ---
# name: test_sensor[System Monitor Disk free /media/share - state]
'0.2'
# ---
# name: test_sensor[System Monitor Disk use (percent) / - attributes]
ReadOnlyDict({
'friendly_name': 'System Monitor Disk use (percent) /',
'icon': 'mdi:harddisk',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': '%',
})
# ---
# name: test_sensor[System Monitor Disk use (percent) / - state]
'60.0'
# ---
# name: test_sensor[System Monitor Disk use (percent) /home/notexist/ - attributes]
ReadOnlyDict({
'friendly_name': 'System Monitor Disk use (percent) /home/notexist/',
'icon': 'mdi:harddisk',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': '%',
})
# ---
# name: test_sensor[System Monitor Disk use (percent) /home/notexist/ - state]
'60.0'
# ---
# name: test_sensor[System Monitor Disk use (percent) /media/share - attributes]
ReadOnlyDict({
'friendly_name': 'System Monitor Disk use (percent) /media/share',
'icon': 'mdi:harddisk',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': '%',
})
# ---
# name: test_sensor[System Monitor Disk use (percent) /media/share - state]
'60.0'
# ---
# name: test_sensor[System Monitor Disk use / - attributes]
ReadOnlyDict({
'device_class': 'data_size',
'friendly_name': 'System Monitor Disk use /',
'icon': 'mdi:harddisk',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfInformation.GIBIBYTES: 'GiB'>,
})
# ---
# name: test_sensor[System Monitor Disk use / - state]
'0.3'
# ---
# name: test_sensor[System Monitor Disk use /media/share - attributes]
ReadOnlyDict({
'device_class': 'data_size',
'friendly_name': 'System Monitor Disk use /media/share',
'icon': 'mdi:harddisk',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfInformation.GIBIBYTES: 'GiB'>,
})
# ---
# name: test_sensor[System Monitor Disk use /media/share - state]
'0.3'
# ---
# name: test_sensor[System Monitor IPv4 address eth0 - attributes]
ReadOnlyDict({
'friendly_name': 'System Monitor IPv4 address eth0',
'icon': 'mdi:ip-network',
})
# ---
# name: test_sensor[System Monitor IPv4 address eth0 - state]
'192.168.1.1'
# ---
# name: test_sensor[System Monitor IPv4 address eth1 - attributes]
ReadOnlyDict({
'friendly_name': 'System Monitor IPv4 address eth1',
'icon': 'mdi:ip-network',
})
# ---
# name: test_sensor[System Monitor IPv4 address eth1 - state]
'192.168.10.1'
# ---
# name: test_sensor[System Monitor IPv6 address eth0 - attributes]
ReadOnlyDict({
'friendly_name': 'System Monitor IPv6 address eth0',
'icon': 'mdi:ip-network',
})
# ---
# name: test_sensor[System Monitor IPv6 address eth0 - state]
'unknown'
# ---
# name: test_sensor[System Monitor IPv6 address eth1 - attributes]
ReadOnlyDict({
'friendly_name': 'System Monitor IPv6 address eth1',
'icon': 'mdi:ip-network',
})
# ---
# name: test_sensor[System Monitor IPv6 address eth1 - state]
'unknown'
# ---
# name: test_sensor[System Monitor Last boot - attributes]
ReadOnlyDict({
'device_class': 'timestamp',
'friendly_name': 'System Monitor Last boot',
})
# ---
# name: test_sensor[System Monitor Last boot - state]
'2023-12-30T21:55:38+00:00'
# ---
# name: test_sensor[System Monitor Load (15m) - attributes]
ReadOnlyDict({
'friendly_name': 'System Monitor Load (15m)',
'icon': 'mdi:cpu-64-bit',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
})
# ---
# name: test_sensor[System Monitor Load (15m) - state]
'3'
# ---
# name: test_sensor[System Monitor Load (1m) - attributes]
ReadOnlyDict({
'friendly_name': 'System Monitor Load (1m)',
'icon': 'mdi:cpu-64-bit',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
})
# ---
# name: test_sensor[System Monitor Load (1m) - state]
'1'
# ---
# name: test_sensor[System Monitor Load (5m) - attributes]
ReadOnlyDict({
'friendly_name': 'System Monitor Load (5m)',
'icon': 'mdi:cpu-64-bit',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
})
# ---
# name: test_sensor[System Monitor Load (5m) - state]
'2'
# ---
# name: test_sensor[System Monitor Memory free - attributes]
ReadOnlyDict({
'device_class': 'data_size',
'friendly_name': 'System Monitor Memory free',
'icon': 'mdi:memory',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfInformation.MEBIBYTES: 'MiB'>,
})
# ---
# name: test_sensor[System Monitor Memory free - state]
'40.0'
# ---
# name: test_sensor[System Monitor Memory use (percent) - attributes]
ReadOnlyDict({
'friendly_name': 'System Monitor Memory use (percent)',
'icon': 'mdi:memory',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': '%',
})
# ---
# name: test_sensor[System Monitor Memory use (percent) - state]
'40.0'
# ---
# name: test_sensor[System Monitor Memory use - attributes]
ReadOnlyDict({
'device_class': 'data_size',
'friendly_name': 'System Monitor Memory use',
'icon': 'mdi:memory',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfInformation.MEBIBYTES: 'MiB'>,
})
# ---
# name: test_sensor[System Monitor Memory use - state]
'60.0'
# ---
# name: test_sensor[System Monitor Network in eth0 - attributes]
ReadOnlyDict({
'device_class': 'data_size',
'friendly_name': 'System Monitor Network in eth0',
'icon': 'mdi:server-network',
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
'unit_of_measurement': <UnitOfInformation.MEBIBYTES: 'MiB'>,
})
# ---
# name: test_sensor[System Monitor Network in eth0 - state]
'100.0'
# ---
# name: test_sensor[System Monitor Network in eth1 - attributes]
ReadOnlyDict({
'device_class': 'data_size',
'friendly_name': 'System Monitor Network in eth1',
'icon': 'mdi:server-network',
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
'unit_of_measurement': <UnitOfInformation.MEBIBYTES: 'MiB'>,
})
# ---
# name: test_sensor[System Monitor Network in eth1 - state]
'200.0'
# ---
# name: test_sensor[System Monitor Network out eth0 - attributes]
ReadOnlyDict({
'device_class': 'data_size',
'friendly_name': 'System Monitor Network out eth0',
'icon': 'mdi:server-network',
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
'unit_of_measurement': <UnitOfInformation.MEBIBYTES: 'MiB'>,
})
# ---
# name: test_sensor[System Monitor Network out eth0 - state]
'100.0'
# ---
# name: test_sensor[System Monitor Network out eth1 - attributes]
ReadOnlyDict({
'device_class': 'data_size',
'friendly_name': 'System Monitor Network out eth1',
'icon': 'mdi:server-network',
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
'unit_of_measurement': <UnitOfInformation.MEBIBYTES: 'MiB'>,
})
# ---
# name: test_sensor[System Monitor Network out eth1 - state]
'200.0'
# ---
# name: test_sensor[System Monitor Network throughput in eth0 - attributes]
ReadOnlyDict({
'device_class': 'data_rate',
'friendly_name': 'System Monitor Network throughput in eth0',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfDataRate.MEGABYTES_PER_SECOND: 'MB/s'>,
})
# ---
# name: test_sensor[System Monitor Network throughput in eth0 - state]
'unknown'
# ---
# name: test_sensor[System Monitor Network throughput in eth1 - attributes]
ReadOnlyDict({
'device_class': 'data_rate',
'friendly_name': 'System Monitor Network throughput in eth1',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfDataRate.MEGABYTES_PER_SECOND: 'MB/s'>,
})
# ---
# name: test_sensor[System Monitor Network throughput in eth1 - state]
'unknown'
# ---
# name: test_sensor[System Monitor Network throughput out eth0 - attributes]
ReadOnlyDict({
'device_class': 'data_rate',
'friendly_name': 'System Monitor Network throughput out eth0',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfDataRate.MEGABYTES_PER_SECOND: 'MB/s'>,
})
# ---
# name: test_sensor[System Monitor Network throughput out eth0 - state]
'unknown'
# ---
# name: test_sensor[System Monitor Network throughput out eth1 - attributes]
ReadOnlyDict({
'device_class': 'data_rate',
'friendly_name': 'System Monitor Network throughput out eth1',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfDataRate.MEGABYTES_PER_SECOND: 'MB/s'>,
})
# ---
# name: test_sensor[System Monitor Network throughput out eth1 - state]
'unknown'
# ---
# name: test_sensor[System Monitor Packets in eth0 - attributes]
ReadOnlyDict({
'friendly_name': 'System Monitor Packets in eth0',
'icon': 'mdi:server-network',
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
})
# ---
# name: test_sensor[System Monitor Packets in eth0 - state]
'50'
# ---
# name: test_sensor[System Monitor Packets in eth1 - attributes]
ReadOnlyDict({
'friendly_name': 'System Monitor Packets in eth1',
'icon': 'mdi:server-network',
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
})
# ---
# name: test_sensor[System Monitor Packets in eth1 - state]
'150'
# ---
# name: test_sensor[System Monitor Packets out eth0 - attributes]
ReadOnlyDict({
'friendly_name': 'System Monitor Packets out eth0',
'icon': 'mdi:server-network',
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
})
# ---
# name: test_sensor[System Monitor Packets out eth0 - state]
'50'
# ---
# name: test_sensor[System Monitor Packets out eth1 - attributes]
ReadOnlyDict({
'friendly_name': 'System Monitor Packets out eth1',
'icon': 'mdi:server-network',
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
})
# ---
# name: test_sensor[System Monitor Packets out eth1 - state]
'150'
# ---
# name: test_sensor[System Monitor Process pip - attributes]
ReadOnlyDict({
'friendly_name': 'System Monitor Process pip',
'icon': 'mdi:cpu-64-bit',
})
# ---
# name: test_sensor[System Monitor Process pip - state]
'on'
# ---
# name: test_sensor[System Monitor Process python3 - attributes]
ReadOnlyDict({
'friendly_name': 'System Monitor Process python3',
'icon': 'mdi:cpu-64-bit',
})
# ---
# name: test_sensor[System Monitor Process python3 - state]
'on'
# ---
# name: test_sensor[System Monitor Processor temperature - attributes]
ReadOnlyDict({
'device_class': 'temperature',
'friendly_name': 'System Monitor Processor temperature',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
})
# ---
# name: test_sensor[System Monitor Processor temperature - state]
'50.0'
# ---
# name: test_sensor[System Monitor Processor use - attributes]
ReadOnlyDict({
'friendly_name': 'System Monitor Processor use',
'icon': 'mdi:cpu-64-bit',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': '%',
})
# ---
# name: test_sensor[System Monitor Processor use - state]
'10'
# ---
# name: test_sensor[System Monitor Swap free - attributes]
ReadOnlyDict({
'device_class': 'data_size',
'friendly_name': 'System Monitor Swap free',
'icon': 'mdi:harddisk',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfInformation.MEBIBYTES: 'MiB'>,
})
# ---
# name: test_sensor[System Monitor Swap free - state]
'40.0'
# ---
# name: test_sensor[System Monitor Swap use (percent) - attributes]
ReadOnlyDict({
'friendly_name': 'System Monitor Swap use (percent)',
'icon': 'mdi:harddisk',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': '%',
})
# ---
# name: test_sensor[System Monitor Swap use (percent) - state]
'60.0'
# ---
# name: test_sensor[System Monitor Swap use - attributes]
ReadOnlyDict({
'device_class': 'data_size',
'friendly_name': 'System Monitor Swap use',
'icon': 'mdi:harddisk',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfInformation.MEBIBYTES: 'MiB'>,
})
# ---
# name: test_sensor[System Monitor Swap use - state]
'60.0'
# ---

View file

@ -0,0 +1,60 @@
"""Test for System Monitor init."""
from __future__ import annotations
from homeassistant.components.systemmonitor.const import CONF_PROCESS
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
from homeassistant.const import STATE_OFF
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType
async def test_load_unload_entry(
hass: HomeAssistant, mock_added_config_entry: ConfigEntry
) -> None:
"""Test load and unload an entry."""
assert mock_added_config_entry.state == ConfigEntryState.LOADED
assert await hass.config_entries.async_unload(mock_added_config_entry.entry_id)
await hass.async_block_till_done()
assert mock_added_config_entry.state is ConfigEntryState.NOT_LOADED
async def test_adding_processor_to_options(
hass: HomeAssistant, mock_added_config_entry: ConfigEntry
) -> None:
"""Test options listener."""
process_sensor = hass.states.get("sensor.system_monitor_process_systemd")
assert process_sensor is None
result = await hass.config_entries.options.async_init(
mock_added_config_entry.entry_id
)
assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "init"
result = await hass.config_entries.options.async_configure(
result["flow_id"],
user_input={
CONF_PROCESS: ["python3", "pip", "systemd"],
},
)
await hass.async_block_till_done()
assert result["type"] == FlowResultType.CREATE_ENTRY
assert result["data"] == {
"sensor": {
CONF_PROCESS: ["python3", "pip", "systemd"],
},
"resources": [
"disk_use_percent_/",
"disk_use_percent_/home/notexist/",
"memory_free_",
"network_out_eth0",
"process_python3",
],
}
process_sensor = hass.states.get("sensor.system_monitor_process_systemd")
assert process_sensor is not None
assert process_sensor.state == STATE_OFF

View file

@ -0,0 +1,346 @@
"""Test System Monitor sensor."""
from datetime import timedelta
import socket
from unittest.mock import Mock, patch
from freezegun.api import FrozenDateTimeFactory
from psutil._common import shwtemp, snetio, snicaddr
import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.systemmonitor.sensor import (
_read_cpu_temperature,
get_cpu_icon,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import STATE_ON, STATE_UNAVAILABLE, STATE_UNKNOWN
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from homeassistant.setup import async_setup_component
from .conftest import MockProcess, svmem
from tests.common import MockConfigEntry, async_fire_time_changed
async def test_sensor(
hass: HomeAssistant,
entity_registry_enabled_by_default: None,
mock_added_config_entry: ConfigEntry,
entity_registry: er.EntityRegistry,
snapshot: SnapshotAssertion,
) -> None:
"""Test the sensor."""
memory_sensor = hass.states.get("sensor.system_monitor_memory_free")
assert memory_sensor is not None
assert memory_sensor.state == "40.0"
assert memory_sensor.attributes == {
"state_class": "measurement",
"unit_of_measurement": "MiB",
"device_class": "data_size",
"icon": "mdi:memory",
"friendly_name": "System Monitor Memory free",
}
process_sensor = hass.states.get("sensor.system_monitor_process_python3")
assert process_sensor is not None
assert process_sensor.state == STATE_ON
for entity in er.async_entries_for_config_entry(
entity_registry, mock_added_config_entry.entry_id
):
state = hass.states.get(entity.entity_id)
assert state.state == snapshot(name=f"{state.name} - state")
assert state.attributes == snapshot(name=f"{state.name} - attributes")
async def test_sensor_not_loading_veth_networks(
hass: HomeAssistant,
entity_registry_enabled_by_default: None,
mock_added_config_entry: ConfigEntry,
) -> None:
"""Test the sensor."""
network_sensor_1 = hass.states.get("sensor.system_monitor_network_out_eth1")
network_sensor_2 = hass.states.get(
"sensor.sensor.system_monitor_network_out_vethxyzxyz"
)
assert network_sensor_1 is not None
assert network_sensor_1.state == "200.0"
assert network_sensor_2 is None
async def test_sensor_icon(
hass: HomeAssistant,
entity_registry_enabled_by_default: None,
mock_util: Mock,
mock_psutil: Mock,
mock_os: Mock,
mock_config_entry: MockConfigEntry,
caplog: pytest.LogCaptureFixture,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test the sensor icon for 32bit/64bit system."""
get_cpu_icon.cache_clear()
with patch("sys.maxsize", 2**32):
assert get_cpu_icon() == "mdi:cpu-32-bit"
get_cpu_icon.cache_clear()
with patch("sys.maxsize", 2**64):
assert get_cpu_icon() == "mdi:cpu-64-bit"
async def test_sensor_yaml(
hass: HomeAssistant,
entity_registry_enabled_by_default: None,
mock_psutil: Mock,
mock_os: Mock,
mock_util: Mock,
) -> None:
"""Test the sensor imported from YAML."""
config = {
"sensor": {
"platform": "systemmonitor",
"resources": [
{"type": "disk_use_percent"},
{"type": "disk_use_percent", "arg": "/media/share"},
{"type": "memory_free", "arg": "/"},
{"type": "network_out", "arg": "eth0"},
{"type": "process", "arg": "python3"},
],
}
}
assert await async_setup_component(hass, "sensor", config)
await hass.async_block_till_done()
memory_sensor = hass.states.get("sensor.system_monitor_memory_free")
assert memory_sensor is not None
assert memory_sensor.state == "40.0"
process_sensor = hass.states.get("sensor.system_monitor_process_python3")
assert process_sensor is not None
assert process_sensor.state == STATE_ON
async def test_sensor_yaml_fails_missing_argument(
caplog: pytest.LogCaptureFixture,
hass: HomeAssistant,
entity_registry_enabled_by_default: None,
mock_psutil: Mock,
mock_os: Mock,
mock_util: Mock,
) -> None:
"""Test the sensor imported from YAML fails on missing mandatory argument."""
config = {
"sensor": {
"platform": "systemmonitor",
"resources": [
{"type": "network_in"},
],
}
}
assert await async_setup_component(hass, "sensor", config)
await hass.async_block_till_done()
assert "Mandatory 'arg' is missing for sensor type 'network_in'" in caplog.text
async def test_sensor_updating(
hass: HomeAssistant,
mock_added_config_entry: ConfigEntry,
mock_psutil: Mock,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test the sensor."""
memory_sensor = hass.states.get("sensor.system_monitor_memory_free")
assert memory_sensor is not None
assert memory_sensor.state == "40.0"
process_sensor = hass.states.get("sensor.system_monitor_process_python3")
assert process_sensor is not None
assert process_sensor.state == STATE_ON
mock_psutil.virtual_memory.side_effect = Exception("Failed to update")
freezer.tick(timedelta(minutes=1))
async_fire_time_changed(hass)
await hass.async_block_till_done()
memory_sensor = hass.states.get("sensor.system_monitor_memory_free")
assert memory_sensor is not None
assert memory_sensor.state == STATE_UNAVAILABLE
mock_psutil.virtual_memory.side_effect = None
mock_psutil.virtual_memory.return_value = svmem(
100 * 1024**2,
25 * 1024**2,
25.0,
60 * 1024**2,
30 * 1024**2,
1,
1,
1,
1,
1,
1,
)
freezer.tick(timedelta(minutes=1))
async_fire_time_changed(hass)
await hass.async_block_till_done()
memory_sensor = hass.states.get("sensor.system_monitor_memory_free")
assert memory_sensor is not None
assert memory_sensor.state == "25.0"
async def test_sensor_process_fails(
hass: HomeAssistant,
mock_added_config_entry: ConfigEntry,
mock_psutil: Mock,
freezer: FrozenDateTimeFactory,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test process not exist failure."""
process_sensor = hass.states.get("sensor.system_monitor_process_python3")
assert process_sensor is not None
assert process_sensor.state == STATE_ON
_process = MockProcess("python3", True)
mock_psutil.process_iter.return_value = [_process]
freezer.tick(timedelta(minutes=1))
async_fire_time_changed(hass)
await hass.async_block_till_done()
process_sensor = hass.states.get("sensor.system_monitor_process_python3")
assert process_sensor is not None
# assert process_sensor.state == STATE_ON
assert "Failed to load process with ID: 1, old name: python3" in caplog.text
async def test_sensor_network_sensors(
hass: HomeAssistant,
entity_registry_enabled_by_default: None,
mock_added_config_entry: ConfigEntry,
mock_psutil: Mock,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test process not exist failure."""
network_out_sensor = hass.states.get("sensor.system_monitor_network_out_eth1")
packets_out_sensor = hass.states.get("sensor.system_monitor_packets_out_eth1")
throughput_network_out_sensor = hass.states.get(
"sensor.system_monitor_network_throughput_out_eth1"
)
assert network_out_sensor is not None
assert packets_out_sensor is not None
assert throughput_network_out_sensor is not None
assert network_out_sensor.state == "200.0"
assert packets_out_sensor.state == "150"
assert throughput_network_out_sensor.state == STATE_UNKNOWN
mock_psutil.net_io_counters.return_value = {
"eth0": snetio(200 * 1024**2, 200 * 1024**2, 100, 100, 0, 0, 0, 0),
"eth1": snetio(400 * 1024**2, 400 * 1024**2, 300, 300, 0, 0, 0, 0),
}
freezer.tick(timedelta(minutes=1))
async_fire_time_changed(hass)
await hass.async_block_till_done()
network_out_sensor = hass.states.get("sensor.system_monitor_network_out_eth1")
packets_out_sensor = hass.states.get("sensor.system_monitor_packets_out_eth1")
throughput_network_out_sensor = hass.states.get(
"sensor.system_monitor_network_throughput_out_eth1"
)
assert network_out_sensor is not None
assert packets_out_sensor is not None
assert throughput_network_out_sensor is not None
assert network_out_sensor.state == "400.0"
assert packets_out_sensor.state == "300"
assert float(throughput_network_out_sensor.state) == pytest.approx(3.493, rel=0.1)
mock_psutil.net_io_counters.return_value = {
"eth0": snetio(100 * 1024**2, 100 * 1024**2, 50, 50, 0, 0, 0, 0),
}
mock_psutil.net_if_addrs.return_value = {
"eth0": [
snicaddr(
socket.AF_INET,
"192.168.1.1",
"255.255.255.0",
"255.255.255.255",
None,
)
],
}
freezer.tick(timedelta(minutes=1))
async_fire_time_changed(hass)
await hass.async_block_till_done()
network_out_sensor = hass.states.get("sensor.system_monitor_network_out_eth1")
packets_out_sensor = hass.states.get("sensor.system_monitor_packets_out_eth1")
throughput_network_out_sensor = hass.states.get(
"sensor.system_monitor_network_throughput_out_eth1"
)
assert network_out_sensor is not None
assert packets_out_sensor is not None
assert throughput_network_out_sensor is not None
assert network_out_sensor.state == STATE_UNKNOWN
assert packets_out_sensor.state == STATE_UNKNOWN
assert throughput_network_out_sensor.state == STATE_UNKNOWN
async def test_missing_cpu_temperature(
hass: HomeAssistant,
entity_registry_enabled_by_default: None,
mock_util: Mock,
mock_psutil: Mock,
mock_os: Mock,
mock_config_entry: MockConfigEntry,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test the sensor when temperature missing."""
mock_psutil.sensors_temperatures.return_value = {
"not_exist": [shwtemp("not_exist", 50.0, 60.0, 70.0)]
}
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
assert "Cannot read CPU / processor temperature information" in caplog.text
temp_sensor = hass.states.get("sensor.system_monitor_processor_temperature")
assert temp_sensor is None
async def test_processor_temperature() -> None:
"""Test the disk failures."""
with patch("sys.platform", "linux"), patch(
"homeassistant.components.systemmonitor.sensor.psutil"
) as mock_psutil:
mock_psutil.sensors_temperatures.return_value = {
"cpu0-thermal": [shwtemp("cpu0-thermal", 50.0, 60.0, 70.0)]
}
temperature = _read_cpu_temperature()
assert temperature == 50.0
with patch("sys.platform", "nt"), patch(
"homeassistant.components.systemmonitor.sensor.psutil",
) as mock_psutil:
mock_psutil.sensors_temperatures.side_effect = AttributeError(
"sensors_temperatures not exist"
)
temperature = _read_cpu_temperature()
assert temperature is None
with patch("sys.platform", "darwin"), patch(
"homeassistant.components.systemmonitor.sensor.psutil"
) as mock_psutil:
mock_psutil.sensors_temperatures.return_value = {
"cpu0-thermal": [shwtemp("cpu0-thermal", 50.0, 60.0, 70.0)]
}
temperature = _read_cpu_temperature()
assert temperature == 50.0

View file

@ -0,0 +1,90 @@
"""Test System Monitor utils."""
from unittest.mock import Mock, patch
from psutil._common import sdiskpart
import pytest
from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry
@pytest.mark.parametrize(
("side_effect", "error_text"),
[
(PermissionError("No permission"), "No permission for running user to access"),
(OSError("OS error"), "was excluded because of: OS error"),
],
)
async def test_disk_setup_failure(
hass: HomeAssistant,
entity_registry_enabled_by_default: None,
mock_psutil: Mock,
mock_os: Mock,
mock_util: Mock,
mock_config_entry: MockConfigEntry,
side_effect: Exception,
error_text: str,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test the disk failures."""
with patch(
"homeassistant.components.systemmonitor.util.psutil.disk_usage",
side_effect=side_effect,
):
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
disk_sensor = hass.states.get("sensor.system_monitor_disk_free_media_share")
assert disk_sensor is None
assert error_text in caplog.text
async def test_disk_util(
hass: HomeAssistant,
entity_registry_enabled_by_default: None,
mock_psutil: Mock,
mock_os: Mock,
mock_util: Mock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test the disk failures."""
mock_util.disk_partitions.return_value = [
sdiskpart("test", "/", "ext4", "", 1, 1), # Should be ok
sdiskpart("test2", "/media/share", "ext4", "", 1, 1), # Should be ok
sdiskpart("test3", "/incorrect", "", "", 1, 1), # Should be skipped as no type
sdiskpart(
"proc", "/proc/run", "proc", "", 1, 1
), # Should be skipped as in skipped disk types
sdiskpart(
"test4",
"/tmpfs/", # noqa: S108
"tmpfs",
"",
1,
1,
), # Should be skipped as in skipped disk types
sdiskpart("test5", "E:", "cd", "cdrom", 1, 1), # Should be skipped as cdrom
]
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
disk_sensor1 = hass.states.get("sensor.system_monitor_disk_free")
disk_sensor2 = hass.states.get("sensor.system_monitor_disk_free_media_share")
disk_sensor3 = hass.states.get("sensor.system_monitor_disk_free_incorrect")
disk_sensor4 = hass.states.get("sensor.system_monitor_disk_free_proc_run")
disk_sensor5 = hass.states.get("sensor.system_monitor_disk_free_tmpfs")
disk_sensor6 = hass.states.get("sensor.system_monitor_disk_free_e")
assert disk_sensor1 is not None
assert disk_sensor2 is not None
assert disk_sensor3 is None
assert disk_sensor4 is None
assert disk_sensor5 is None
assert disk_sensor6 is None