mirror of
https://github.com/nicolargo/glances.git
synced 2026-07-31 07:16:17 -04:00
218 lines
6.4 KiB
Python
Executable File
218 lines
6.4 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 quicklook plugin model."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from unittest.mock import mock_open
|
|
|
|
import pytest
|
|
|
|
from glances.config_v5 import GlancesConfigV5
|
|
from glances.plugins.quicklook.model_v5 import PluginModel, _collect_sync, _cpu_name
|
|
from glances.stats_store_v5 import StatsStoreV5
|
|
|
|
|
|
@pytest.fixture
|
|
def store() -> StatsStoreV5:
|
|
return StatsStoreV5()
|
|
|
|
|
|
@pytest.fixture
|
|
def config(tmp_path, monkeypatch) -> GlancesConfigV5:
|
|
# v5 idiom (mirrors tests/test_plugin_load_v5.py): real config object
|
|
# with the system config path redirected to an empty tmp file.
|
|
monkeypatch.setattr(GlancesConfigV5, "SYSTEM_CONFIG_PATH", tmp_path / "etc" / "glances.conf")
|
|
return GlancesConfigV5()
|
|
|
|
|
|
def test_plugin_identity(store, config):
|
|
p = PluginModel(store, config)
|
|
assert p.plugin_name == "quicklook"
|
|
assert p.IS_COLLECTION is False
|
|
|
|
|
|
def test_quicklook_opts_out_of_alerts(store, config):
|
|
"""quicklook colours its bars from cpu/mem/swap/load `_levels` but must
|
|
NOT emit alerts events / actions: those aggregate signals are already
|
|
watched by the cpu/mem/memswap/load plugins, so ingesting quicklook too
|
|
would duplicate every alert. See base ``EMITS_ALERTS`` doc."""
|
|
assert PluginModel.EMITS_ALERTS is False
|
|
|
|
|
|
def test_fields_description_keys():
|
|
fd = PluginModel.fields_description
|
|
for key in ("cpu", "mem", "swap", "load"):
|
|
assert fd[key]["unit"] == "percent"
|
|
assert fd[key].get("watched") is True
|
|
# Render-support fields are internal + not watched (never level-computed).
|
|
for key in ("percpu", "cpu_name", "cpu_hz", "cpu_hz_current", "cpu_log_core", "cpu_phys_core"):
|
|
assert fd[key].get("internal") is True
|
|
assert fd[key].get("watched", False) is False
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_grab_stats_shape(store, config, monkeypatch):
|
|
"""_grab_stats returns the documented scalar shape with a percpu list."""
|
|
p = PluginModel(store, config)
|
|
|
|
class _Sample:
|
|
idle = 80.0
|
|
|
|
class _FakeSampler:
|
|
cpu_count = 4
|
|
|
|
async def get_aggregate(self):
|
|
return _Sample()
|
|
|
|
async def get_per_core(self):
|
|
return [_Sample(), _Sample()]
|
|
|
|
import glances.plugins.quicklook.model_v5 as mod
|
|
|
|
monkeypatch.setattr(mod, "sampler", _FakeSampler())
|
|
monkeypatch.setattr(
|
|
mod,
|
|
"_collect_sync",
|
|
lambda: {
|
|
"mem": 42.0,
|
|
"swap": 10.0,
|
|
"load": 25.0,
|
|
"cpu_log_core": 4,
|
|
"cpu_phys_core": 2,
|
|
"cpu_name": "Test CPU",
|
|
"cpu_hz_current": 2_000_000_000,
|
|
"cpu_hz": 3_000_000_000,
|
|
},
|
|
)
|
|
|
|
stats = await p._grab_stats()
|
|
assert stats["cpu"] == 20.0 # 100 - idle(80)
|
|
assert stats["mem"] == 42.0
|
|
assert stats["swap"] == 10.0
|
|
assert stats["load"] == 25.0
|
|
assert isinstance(stats["percpu"], list) and len(stats["percpu"]) == 2
|
|
assert stats["percpu"][0] == {"cpu_number": 0, "total": 20.0}
|
|
assert stats["cpu_name"] == "Test CPU"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_grab_stats_survives_sampler_failure(store, config, monkeypatch):
|
|
"""A sampler raising OSError yields a partial dict, not an exception."""
|
|
p = PluginModel(store, config)
|
|
|
|
class _BoomSampler:
|
|
cpu_count = 1
|
|
|
|
async def get_aggregate(self):
|
|
raise OSError("boom")
|
|
|
|
async def get_per_core(self):
|
|
raise OSError("boom")
|
|
|
|
import glances.plugins.quicklook.model_v5 as mod
|
|
|
|
monkeypatch.setattr(mod, "sampler", _BoomSampler())
|
|
monkeypatch.setattr(mod, "_collect_sync", lambda: {})
|
|
|
|
stats = await p._grab_stats()
|
|
assert "cpu" not in stats
|
|
assert "percpu" not in stats
|
|
|
|
|
|
def test_cpu_name_parses_proc_cpuinfo(monkeypatch):
|
|
data = "processor\t: 0\nmodel name\t: Test Chip 9000\nflags\t: fpu\n"
|
|
monkeypatch.setattr("builtins.open", mock_open(read_data=data))
|
|
assert _cpu_name() == "Test Chip 9000"
|
|
|
|
|
|
def test_cpu_name_falls_back_on_oserror(monkeypatch):
|
|
def _boom(*args, **kwargs):
|
|
raise OSError("snap confinement")
|
|
|
|
monkeypatch.setattr("builtins.open", _boom)
|
|
monkeypatch.setattr("platform.processor", lambda: "FallbackCPU")
|
|
assert _cpu_name() == "FallbackCPU"
|
|
|
|
|
|
def test_collect_sync_smoke():
|
|
out = _collect_sync()
|
|
assert isinstance(out, dict)
|
|
assert "cpu_name" in out # always set, even when other metrics fail
|
|
|
|
|
|
class _NoGpuSampler:
|
|
cpu_count = 1
|
|
|
|
async def get_aggregate(self):
|
|
class _A:
|
|
idle = 100.0
|
|
|
|
return _A()
|
|
|
|
async def get_per_core(self):
|
|
return []
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_gpu_means_from_store(store, config, monkeypatch):
|
|
"""quicklook computes gpu_mem/gpu_proc as the mean of the gpu plugin's cards."""
|
|
await store.set(
|
|
"gpu",
|
|
[
|
|
{"gpu_id": "n0", "mem": 40, "proc": 20},
|
|
{"gpu_id": "n1", "mem": 60, "proc": 40},
|
|
],
|
|
)
|
|
p = PluginModel(store, config)
|
|
|
|
import glances.plugins.quicklook.model_v5 as mod
|
|
|
|
monkeypatch.setattr(mod, "_collect_sync", lambda: {})
|
|
monkeypatch.setattr(mod, "sampler", _NoGpuSampler())
|
|
|
|
stats = await p._grab_stats()
|
|
assert stats["gpu_mem"] == 50.0
|
|
assert stats["gpu_proc"] == 30.0
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_no_gpu_keys_when_store_empty(store, config, monkeypatch):
|
|
p = PluginModel(store, config)
|
|
import glances.plugins.quicklook.model_v5 as mod
|
|
|
|
monkeypatch.setattr(mod, "_collect_sync", lambda: {})
|
|
monkeypatch.setattr(mod, "sampler", _NoGpuSampler())
|
|
|
|
stats = await p._grab_stats()
|
|
assert "gpu_mem" not in stats
|
|
assert "gpu_proc" not in stats
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_no_gpu_keys_when_all_none(store, config, monkeypatch):
|
|
await store.set("gpu", [{"gpu_id": "n0", "mem": None, "proc": None}])
|
|
p = PluginModel(store, config)
|
|
import glances.plugins.quicklook.model_v5 as mod
|
|
|
|
monkeypatch.setattr(mod, "_collect_sync", lambda: {})
|
|
monkeypatch.setattr(mod, "sampler", _NoGpuSampler())
|
|
|
|
stats = await p._grab_stats()
|
|
assert "gpu_mem" not in stats
|
|
assert "gpu_proc" not in stats
|
|
|
|
|
|
def test_gpu_fields_declared_watched():
|
|
fd = PluginModel.fields_description
|
|
for key in ("gpu_mem", "gpu_proc"):
|
|
assert fd[key]["watched"] is True
|
|
assert fd[key]["unit"] == "percent"
|