Files
glances/tests/test_plugin_processcount_v5.py
nicolargo 645527de06 feat(v5): G4-processlist — port processcount + processlist plugins to v5
Last MCP gap closure. Both plugins reuse the v4 glances_processes
singleton (no engine rewrite — strategy two-phase): processcount calls
engine.update() + get_count() each cycle, processlist consumes the
pre-sorted list via get_list(). KNOWN_V5_MISSING_PLUGINS shrinks to ().

- processcount: scalar with total / running / sleeping / thread /
  pid_max; TUI mirrors v4's "TASKS N (M thr), R run, S slp, O oth"
  header.
- processlist: collection PK=pid; minimal column set CPU% / MEM% / PID /
  USER / THR / NI / S / Command, top-20 rows. cpu_percent and
  memory_percent are watched (50/70/90, prominent=False — parity fs).
- Engine-internal fields (memory_info, cpu_times, io_counters, gids,
  time_since_update, key) flagged internal=True so MCP/export keep
  them but the generic TUI skips them.
- Out of scope (deferred to G5 with args/config plumbing): extended
  view, programs aggregation, filter UI, interactive sort.

41 new tests (14 model + 27 renderer), v4 catalogue updated, MCP gap
log + adapter docstring updated.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 14:01:59 +02:00

121 lines
4.1 KiB
Python

#
# Glances - An eye on your system
#
# SPDX-FileCopyrightText: 2026 Nicolas Hennion <nicolas@nicolargo.com>
#
# SPDX-License-Identifier: LGPL-3.0-only
#
"""Glances v5 — unit tests for the ``processcount`` plugin (scalar)."""
from __future__ import annotations
from unittest.mock import patch
import pytest
from glances.config_v5 import GlancesConfigV5
from glances.plugins.processcount.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")
monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path / "xdg"))
return GlancesConfigV5()
# ---------------------------------------------------------- contract
def test_plugin_identity(store, config):
plugin = PluginModel(store, config)
assert plugin.plugin_name == "processcount"
assert plugin.IS_COLLECTION is False
def test_schema_fields(store, config):
fields = PluginModel(store, config)._fields
for name in ("total", "running", "sleeping", "thread", "pid_max"):
assert name in fields, name
# None of them are watched — pure aggregate counts.
assert fields[name].get("watched", False) is False, name
# ---------------------------------------------------------- update pipeline
async def test_update_calls_engine_and_surfaces_count(store, config):
"""``_grab_stats`` triggers ``engine.update()`` and returns the aggregate."""
plugin = PluginModel(store, config)
fake_count = {"total": 42, "running": 5, "sleeping": 30, "thread": 100, "pid_max": 32768}
with (
patch("glances.plugins.processcount.model_v5.glances_processes.update") as upd,
patch(
"glances.plugins.processcount.model_v5.glances_processes.get_count",
return_value=fake_count,
),
):
await plugin.update()
assert upd.called
payload = store.get("processcount")
assert payload["total"] == 42
assert payload["running"] == 5
assert payload["sleeping"] == 30
assert payload["thread"] == 100
assert payload["pid_max"] == 32768
async def test_update_returns_copy_of_engine_dict(store, config):
"""Mutating the stored payload must not affect the engine's internal state."""
plugin = PluginModel(store, config)
engine_dict = {"total": 1, "running": 0, "sleeping": 1, "thread": 1, "pid_max": 32768}
with (
patch("glances.plugins.processcount.model_v5.glances_processes.update"),
patch(
"glances.plugins.processcount.model_v5.glances_processes.get_count",
return_value=engine_dict,
),
):
await plugin.update()
stored = store.get("processcount")
stored["total"] = 999
# The engine's source dict must be untouched.
assert engine_dict["total"] == 1
async def test_update_handles_engine_failure(store, config):
"""If the engine raises, the plugin yields an empty payload (no crash)."""
plugin = PluginModel(store, config)
with patch(
"glances.plugins.processcount.model_v5.glances_processes.update",
side_effect=RuntimeError("psutil fault"),
):
await plugin.update()
payload = store.get("processcount")
# Empty dict from grab_stats → no declared fields surface (filter strips them).
for name in ("total", "running", "sleeping", "thread", "pid_max"):
assert name not in payload, name
async def test_update_handles_get_count_non_dict(store, config):
"""Defensive: if engine.get_count() returns junk, fall back to empty."""
plugin = PluginModel(store, config)
with (
patch("glances.plugins.processcount.model_v5.glances_processes.update"),
patch(
"glances.plugins.processcount.model_v5.glances_processes.get_count",
return_value=None,
),
):
await plugin.update()
payload = store.get("processcount")
for name in ("total", "running", "sleeping", "thread", "pid_max"):
assert name not in payload, name