Files
glances/tests/test_plugin_smart_v5.py
2026-07-13 19:22:21 +02:00

163 lines
6.3 KiB
Python
Executable File

#!/usr/bin/env python
#
# Glances - An eye on your system
#
# SPDX-FileCopyrightText: 2026 Nicolas Hennion <nicolas@nicolargo.com>
#
# SPDX-License-Identifier: LGPL-3.0-only
#
"""Tests for the v5 smart plugin model."""
from __future__ import annotations
import pytest
import glances.plugins.smart as smart_module
from glances.config_v5 import GlancesConfigV5
from glances.plugins.smart.model_v5 import PluginModel
from glances.stats_store_v5 import StatsStoreV5
@pytest.fixture
def store() -> StatsStoreV5:
return StatsStoreV5()
@pytest.fixture
def config(tmp_path, monkeypatch) -> GlancesConfigV5:
monkeypatch.setattr(GlancesConfigV5, "SYSTEM_CONFIG_PATH", tmp_path / "etc" / "glances.conf")
return GlancesConfigV5()
def _cfg_with(tmp_path, monkeypatch, body: str) -> GlancesConfigV5:
monkeypatch.setattr(GlancesConfigV5, "SYSTEM_CONFIG_PATH", tmp_path / "etc" / "glances.conf")
xdg = tmp_path / "xdg"
cfg_dir = xdg / "glances"
cfg_dir.mkdir(parents=True)
(cfg_dir / "glances.conf").write_text(body)
monkeypatch.setenv("XDG_CONFIG_HOME", str(xdg))
return GlancesConfigV5()
def test_plugin_identity(store, config):
p = PluginModel(store, config)
assert p.plugin_name == "smart"
assert p.IS_COLLECTION is True
assert p._primary_key == "name"
def test_fields_description_flags():
fd = PluginModel.fields_description
assert fd["name"]["primary_key"] is True
# attributes is an internal, non-watched nested list (survives the flat filter).
assert fd["attributes"].get("internal") is True
assert fd["attributes"].get("watched", False) is False
def test_emits_alerts_false():
# v4 smart has no colouring/alerts — the port must not raise alerts.
assert PluginModel.EMITS_ALERTS is False
def _fake_device(name="/dev/sda Samsung SSD 850"):
# v4 shape: DeviceName + numeric attribute keys (out of order on purpose).
return {
"DeviceName": name,
5: {"name": "Reallocated_Sector_Ct", "key": "Reallocated_Sector_Ct", "raw": 0, "value": 100},
1: {"name": "Raw_Read_Error_Rate", "key": "Raw_Read_Error_Rate", "raw": 200, "value": 100},
9: {"name": "Power_On_Hours", "key": "Power_On_Hours", "raw": 12345, "value": 99},
}
@pytest.mark.asyncio
async def test_grab_reshapes_numeric_keys_to_attributes_list(store, config, monkeypatch):
p = PluginModel(store, config)
monkeypatch.setattr("glances.plugins.smart.model_v5.is_admin", lambda: True)
monkeypatch.setattr(smart_module, "import_error_tag", False)
monkeypatch.setattr(smart_module, "get_smart_data", lambda hide: [_fake_device()])
rows = await p._grab_stats()
assert len(rows) == 1
dev = rows[0]
assert dev["name"] == "/dev/sda Samsung SSD 850"
# Numeric keys flattened into a list, sorted by the v4 numeric order (1,5,9).
assert [a["name"] for a in dev["attributes"]] == [
"Raw_Read_Error_Rate",
"Reallocated_Sector_Ct",
"Power_On_Hours",
]
# DeviceName is not smuggled into the attributes list.
assert all("DeviceName" not in a for a in dev["attributes"])
@pytest.mark.asyncio
async def test_grab_passes_hide_attributes_to_helper(tmp_path, monkeypatch, store):
config = _cfg_with(tmp_path, monkeypatch, "[smart]\nhide_attributes=Self-tests,Errors\n")
p = PluginModel(store, config)
assert p._hide_attributes == ["Self-tests", "Errors"]
captured = {}
def _grab(hide):
captured["hide"] = hide
return []
monkeypatch.setattr("glances.plugins.smart.model_v5.is_admin", lambda: True)
monkeypatch.setattr(smart_module, "import_error_tag", False)
monkeypatch.setattr(smart_module, "get_smart_data", _grab)
await p._grab_stats()
assert captured["hide"] == ["Self-tests", "Errors"]
@pytest.mark.asyncio
async def test_grab_empty_when_not_root(store, config, monkeypatch):
p = PluginModel(store, config)
monkeypatch.setattr("glances.plugins.smart.model_v5.is_admin", lambda: False)
# get_smart_data must NOT be called when not root.
monkeypatch.setattr(smart_module, "get_smart_data", lambda hide: (_ for _ in ()).throw(AssertionError("called")))
assert await p._grab_stats() == []
@pytest.mark.asyncio
async def test_grab_empty_when_import_error(store, config, monkeypatch):
p = PluginModel(store, config)
monkeypatch.setattr("glances.plugins.smart.model_v5.is_admin", lambda: True)
monkeypatch.setattr(smart_module, "import_error_tag", True)
monkeypatch.setattr(smart_module, "get_smart_data", lambda hide: (_ for _ in ()).throw(AssertionError("called")))
assert await p._grab_stats() == []
@pytest.mark.asyncio
async def test_grab_non_numeric_keys_keep_insertion_order(store, config, monkeypatch):
"""#2904: some attribute keys are not numeric — reshape must not crash and
must preserve insertion order for that device."""
p = PluginModel(store, config)
dev = {"DeviceName": "/dev/nvme0 NVMe", "bytesWritten": {"name": "Bytes written", "key": "bytesWritten", "raw": 1}}
monkeypatch.setattr("glances.plugins.smart.model_v5.is_admin", lambda: True)
monkeypatch.setattr(smart_module, "import_error_tag", False)
monkeypatch.setattr(smart_module, "get_smart_data", lambda hide: [dev])
rows = await p._grab_stats()
assert [a["key"] for a in rows[0]["attributes"]] == ["bytesWritten"]
def test_reshape_mixed_numeric_and_non_numeric_keeps_full_insertion_order():
"""#2904: when a device mixes numeric and non-numeric attribute keys the
`sorted(key=int)` raises mid-sort, so the WHOLE key list must fall back to
dict insertion order (not a partial sort). Guards against a regression where
the numeric keys get sorted before the exception aborts."""
dev = {
"DeviceName": "/dev/nvme0 NVMe",
5: {"name": "Reallocated_Sector_Ct", "key": "Reallocated_Sector_Ct", "raw": 0},
1: {"name": "Raw_Read_Error_Rate", "key": "Raw_Read_Error_Rate", "raw": 200},
"bytesWritten": {"name": "Bytes written", "key": "bytesWritten", "raw": 1},
}
reshaped = PluginModel._reshape(dev)
assert reshaped["name"] == "/dev/nvme0 NVMe"
# Insertion order preserved verbatim (5, 1, bytesWritten) — NOT sorted to 1, 5.
assert [a["key"] for a in reshaped["attributes"]] == [
"Reallocated_Sector_Ct",
"Raw_Read_Error_Rate",
"bytesWritten",
]