Files
glances/tests/test_thresholds_v5.py
nicolargo a720335f12 fix(v5): categorical default + drop cmd path from default view
processlist UX refinements after observing the v5 layout live:

- ``compute_level_categorical`` now returns ``None`` (was ``"ok"``) when
  the value matches no configured bucket. ``base_v5`` skips emitting
  a ``_levels`` entry in that case → the renderer keeps the DEFAULT
  colour (white/gray) instead of painting "S" / nice=0 in the OK
  green. Mirrors v4 ``get_alert`` returning ``'DEFAULT'`` for
  unmatched categorical values. The alert pipeline still sees no
  event (semantically equivalent to "ok" for alerts).

- Command rendering drops the ``/usr/bin/`` path prefix from the
  default view. Now: bold cmd + plain args only. The full-path mode
  (toggled by ``/`` in v4) is deferred to G5+ along with the rest of
  the hotkey plumbing.

Tests updated accordingly. Suite v5: 1370 green, lint clean.

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

346 lines
13 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
#
"""Glances v5 — unit tests for the threshold engine (pure functions).
Test stack: pytest-native (architecture decisions §9). No I/O.
"""
from __future__ import annotations
from typing import Any
import pytest
from glances.plugins.plugin.thresholds_v5 import (
compute_level,
compute_level_categorical,
read_thresholds,
read_thresholds_categorical,
)
# ---------------------------------------------------------- compute_level
THRESHOLDS_HIGH = {"careful": 50.0, "warning": 70.0, "critical": 90.0}
THRESHOLDS_LOW = {"careful": 20.0, "warning": 10.0, "critical": 5.0}
@pytest.mark.parametrize(
"value, expected",
[
(10.0, "ok"),
(49.99, "ok"),
(50.0, "careful"),
(60.0, "careful"),
(70.0, "warning"),
(89.0, "warning"),
(90.0, "critical"),
(100.0, "critical"),
],
)
def test_compute_level_high_direction(value, expected):
assert compute_level(value, THRESHOLDS_HIGH, "high") == expected
@pytest.mark.parametrize(
"value, expected",
[
(50.0, "ok"),
(21.0, "ok"),
(20.0, "careful"),
(15.0, "careful"),
(10.0, "warning"),
(6.0, "warning"),
(5.0, "critical"),
(0.0, "critical"),
],
)
def test_compute_level_low_direction(value, expected):
assert compute_level(value, THRESHOLDS_LOW, "low") == expected
def test_compute_level_none_value_returns_ok():
assert compute_level(None, THRESHOLDS_HIGH, "high") == "ok"
def test_compute_level_empty_thresholds_returns_ok():
assert compute_level(99.0, {}, "high") == "ok"
def test_compute_level_partial_thresholds_walks_severity_descending():
# Only `warning` is configured. value=80 is at warning, value=50 is ok.
partial = {"warning": 70.0}
assert compute_level(80.0, partial, "high") == "warning"
assert compute_level(60.0, partial, "high") == "ok"
def test_compute_level_int_value_accepted():
# Int inputs flow through (no float coercion required).
assert compute_level(75, THRESHOLDS_HIGH, "high") == "warning"
# ---------------------------------------------------------- read_thresholds
class FakeConfig:
"""Minimal stand-in for GlancesConfigV5.get used by `read_thresholds`."""
def __init__(self, mapping: dict[tuple[str, str], Any]):
self._mapping = mapping
def get(self, section: str, option: str, default: Any) -> Any:
return self._mapping.get((section, option), default)
def test_read_thresholds_no_config_no_defaults_returns_empty():
config = FakeConfig({})
assert read_thresholds(config, "mem", "percent") == {}
def test_read_thresholds_uses_defaults_when_config_silent():
config = FakeConfig({})
defaults = {"careful": 50.0, "warning": 70.0, "critical": 90.0}
assert read_thresholds(config, "mem", "percent", defaults=defaults) == defaults
def test_read_thresholds_bare_keys_override_defaults():
config = FakeConfig({("mem", "critical"): "85"})
defaults = {"careful": 50.0, "warning": 70.0, "critical": 90.0}
out = read_thresholds(config, "mem", "percent", defaults=defaults)
assert out == {"careful": 50.0, "warning": 70.0, "critical": 85.0}
def test_read_thresholds_field_prefixed_keys_take_priority_over_bare():
config = FakeConfig({("mem", "warning"): "60", ("mem", "percent_warning"): "65"})
out = read_thresholds(config, "mem", "percent")
assert out == {"warning": 65.0}
def test_read_thresholds_falls_back_to_bare_when_field_prefixed_missing():
config = FakeConfig({("load", "warning"): "1.5"})
out = read_thresholds(config, "load", "min1")
assert out == {"warning": 1.5}
def test_read_thresholds_strict_ignores_bare_level_keys():
"""With strict=True, only field-prefixed (or pk-prefixed) keys are
considered. Bare ``<level>`` keys in the plugin section are skipped
entirely — used by opt-in alert fields (e.g. memswap.sin/sout) that
must not inherit unrelated bare keys."""
config = FakeConfig({("memswap", "warning"): "70"})
out = read_thresholds(config, "memswap", field="sin", strict=True)
assert out == {}
def test_read_thresholds_strict_still_honours_field_prefixed_keys():
"""Strict mode does NOT block explicit ``<field>_<level>`` keys."""
config = FakeConfig({("memswap", "warning"): "70", ("memswap", "sin_warning"): "100"})
out = read_thresholds(config, "memswap", field="sin", strict=True)
assert out == {"warning": 100.0}
def test_read_thresholds_strict_collection_honours_pk_prefixed_keys():
"""Strict mode keeps the per-item key working for collection plugins."""
config = FakeConfig({("network", "warning"): "1000", ("network", "eth0_bytes_recv_warning"): "9000"})
out = read_thresholds(config, "network", field="bytes_recv", pk_value="eth0", strict=True)
assert out == {"warning": 9000.0}
def test_read_thresholds_strict_collection_ignores_bare_warning():
"""Strict + collection + only bare-level present → empty."""
config = FakeConfig({("network", "warning"): "1000"})
out = read_thresholds(config, "network", field="bytes_recv", pk_value="eth0", strict=True)
assert out == {}
def test_read_thresholds_no_field_argument_only_bare_keys():
config = FakeConfig({("mem", "warning"): "60", ("mem", "percent_warning"): "65"})
# field=None → field-prefixed keys are ignored.
out = read_thresholds(config, "mem", field=None)
assert out == {"warning": 60.0}
def test_read_thresholds_negative_value_treated_as_disabled():
# Per the docstring contract, a negative threshold disables that level —
# the helper sees the sentinel and falls back to the default if any.
config = FakeConfig({("mem", "critical"): "-1"})
defaults = {"warning": 70.0, "critical": 90.0}
out = read_thresholds(config, "mem", "percent", defaults=defaults)
# critical is disabled in the config, so the default applies. Bare-key
# match found nothing meaningful, the default carries.
# Implementation note: a negative value is *ignored* (not retained).
assert out["critical"] == 90.0
assert out["warning"] == 70.0
def test_read_thresholds_non_numeric_value_is_skipped():
config = FakeConfig({("mem", "critical"): "not-a-number"})
defaults = {"critical": 90.0}
out = read_thresholds(config, "mem", "percent", defaults=defaults)
assert out == {"critical": 90.0}
def test_read_thresholds_per_level_layering():
# User overrides only `critical`; the others come from defaults.
config = FakeConfig({("mem", "critical"): "95"})
defaults = {"careful": 50.0, "warning": 70.0, "critical": 90.0}
out = read_thresholds(config, "mem", "percent", defaults=defaults)
assert out == {"careful": 50.0, "warning": 70.0, "critical": 95.0}
# ---------------------------------------------------------- 3-level precedence
def test_read_thresholds_pk_specific_takes_priority_over_field_and_bare():
"""`<pk>_<field>_<level>` beats `<field>_<level>` beats `<level>`."""
config = FakeConfig(
{
("network", "warning"): "0.80",
("network", "bytes_recv_warning"): "0.75",
("network", "wlan0_bytes_recv_warning"): "0.70",
}
)
out = read_thresholds(config, "network", field="bytes_recv", pk_value="wlan0")
assert out == {"warning": 0.70}
def test_read_thresholds_falls_back_to_field_when_pk_specific_missing():
"""Without a `<pk>_<field>_<level>` key, the `<field>_<level>` key applies."""
config = FakeConfig(
{
("network", "warning"): "0.80",
("network", "bytes_recv_warning"): "0.75",
}
)
# wlan0 has no specific key — falls back to the field-wide one.
out = read_thresholds(config, "network", field="bytes_recv", pk_value="wlan0")
assert out == {"warning": 0.75}
def test_read_thresholds_pk_specific_only_applies_to_matching_pk():
"""A wlan0-specific key must not bleed into other interfaces."""
config = FakeConfig(
{
("network", "bytes_recv_warning"): "0.80",
("network", "wlan0_bytes_recv_warning"): "0.50",
}
)
# eth0 sees the field-wide value, not the wlan0-specific override.
out_eth = read_thresholds(config, "network", field="bytes_recv", pk_value="eth0")
assert out_eth == {"warning": 0.80}
# wlan0 sees its own override.
out_wlan = read_thresholds(config, "network", field="bytes_recv", pk_value="wlan0")
assert out_wlan == {"warning": 0.50}
def test_read_thresholds_pk_layering_per_level():
"""Different levels may use different scopes — each level resolves independently."""
config = FakeConfig(
{
("network", "critical"): "0.95", # bare: applies to all
("network", "bytes_recv_warning"): "0.80", # field-wide warning
("network", "wlan0_bytes_recv_careful"): "0.50", # pk-specific careful
}
)
out = read_thresholds(config, "network", field="bytes_recv", pk_value="wlan0")
# careful from pk-specific ; warning from field-wide ; critical from bare.
assert out == {"careful": 0.50, "warning": 0.80, "critical": 0.95}
def test_read_thresholds_pk_value_none_falls_back_to_two_levels():
"""When pk_value is None (scalar plugin), only field-wide and bare keys are tried."""
config = FakeConfig(
{
("cpu", "warning"): "70",
("cpu", "total_warning"): "80",
("cpu", "host1_total_warning"): "60", # would NOT match — pk_value=None
}
)
out = read_thresholds(config, "cpu", field="total", pk_value=None)
assert out == {"warning": 80.0}
# ---------------------------------------------------------- categorical
def test_compute_level_categorical_membership_returns_level():
mapping = {
"ok": {"R", "W", "P", "I"},
"critical": {"Z", "D"},
}
assert compute_level_categorical("R", mapping) == "ok"
assert compute_level_categorical("Z", mapping) == "critical"
# Unconfigured value returns None → no level entry (v4 get_alert
# returns 'DEFAULT', not 'OK', for unmatched categorical values).
assert compute_level_categorical("S", mapping) is None
def test_compute_level_categorical_walks_severity_descending():
"""A value in two buckets escalates to the higher severity."""
mapping = {
"ok": {"R"},
"critical": {"R"}, # misconfiguration: R in both
}
# 'critical' wins over 'ok' because the walk starts with most-severe.
assert compute_level_categorical("R", mapping) == "critical"
def test_compute_level_categorical_none_value_returns_none():
assert compute_level_categorical(None, {"critical": {"Z"}}) is None
def test_compute_level_categorical_handles_integer_value():
"""nice is an int; the helper compares string tokens."""
mapping = {"warning": {"1", "2", "3"}, "critical": {"15", "16"}}
assert compute_level_categorical(2, mapping) == "warning"
assert compute_level_categorical(15, mapping) == "critical"
# 0 is not in any bucket → None (the renderer leaves the cell default).
assert compute_level_categorical(0, mapping) is None
def test_read_thresholds_categorical_field_prefixed_csv():
config = FakeConfig(
{
("processlist", "status_ok"): "R,W,P,I",
("processlist", "status_critical"): "Z,D",
}
)
out = read_thresholds_categorical(config, "processlist", field="status")
assert out == {"ok": {"R", "W", "P", "I"}, "critical": {"Z", "D"}}
def test_read_thresholds_categorical_handles_whitespace_in_csv():
config = FakeConfig({("processlist", "nice_warning"): " -2, -1 , 1 ,2 "})
out = read_thresholds_categorical(config, "processlist", field="nice")
assert out == {"warning": {"-2", "-1", "1", "2"}}
def test_read_thresholds_categorical_empty_keys_dropped():
"""A level configured with an empty string is dropped from the mapping."""
config = FakeConfig({("processlist", "status_warning"): ""})
out = read_thresholds_categorical(config, "processlist", field="status")
assert out == {}
def test_read_thresholds_categorical_no_keys_returns_empty():
config = FakeConfig({})
assert read_thresholds_categorical(config, "processlist", field="status") == {}
def test_read_thresholds_categorical_pk_prefixed_wins():
"""Per-item override takes priority for collection plugins."""
config = FakeConfig(
{
("network", "status_critical"): "Z",
("network", "wlan0_status_critical"): "Z,D",
}
)
out = read_thresholds_categorical(config, "network", field="status", pk_value="wlan0")
assert out["critical"] == {"Z", "D"}