Files
glances/tests/test_plugin_cpu_render_curses_v5.py
nicolargo b6a48993ce feat(v5): stable column widths, prominent-on-any-level reverse, 3-cycle alert warmup
Three TUI/alerts corrections raised by the v4 visual review:

1. Column widths now floored to a per-unit minimum so cells don't jiggle
   cycle-to-cycle:
     percent     6 chars  ('100.0%')
     bytes       6        ('999.9G')
     bytespers   8        ('999.9G/s')
     seconds     6        ('1d23h ')
     number      5
     bool        3
   An explicit `column_width` in fields_description still overrides.
   cpu's bespoke 3-col grid uses the same approach with fixed value
   columns of 6 chars.

2. _attr_for now applies A_REVERSE (background highlight) when a cell is
   prominent AND carries any non-DEFAULT alert level (OK/CAREFUL/WARNING/
   CRITICAL) — previously only WARNING and CRITICAL. Matches v4 behaviour
   for *_LOG decorations on watched-prominent fields.

3. GlancesAlerts now skips ingestion for the first N refresh cycles per
   plugin (default N=3, configurable via [alerts] warmup_cycles). Rates
   and threshold-derived _levels can fire spuriously at startup before
   the system has settled — this matches v4's behaviour of ignoring logs
   during the same warmup. The TUI colouring is unaffected (it reads
   _levels directly from the payload).

Test fixtures now default to warmup_cycles=0 so existing assertions
remain valid; three new tests cover the warmup behaviour explicitly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 15:08:49 +02:00

194 lines
6.3 KiB
Python

"""Glances v5 — tests for the cpu plugin's curses renderer."""
from __future__ import annotations
import pytest
from glances.outputs.curses_renderer_v5 import ColorRole
from glances.plugins.cpu.render_curses_v5 import render
@pytest.fixture
def cpu_fields():
"""A minimal subset of the cpu fields_description schema."""
return {
"total": {"unit": "percent", "watched": True, "prominent": True, "label": "CPU"},
"user": {"unit": "percent"},
"system": {"unit": "percent"},
"idle": {"unit": "percent"},
"nice": {"unit": "percent"},
"iowait": {"unit": "percent"},
"irq": {"unit": "percent"},
"steal": {"unit": "percent"},
"guest": {"unit": "percent"},
"ctx_switches": {"unit": "number", "rate": True},
"interrupts": {"unit": "number", "rate": True},
"soft_interrupts": {"unit": "number", "rate": True},
}
@pytest.fixture
def cpu_payload_linux():
"""Realistic cpu payload with all Linux fields, post-_transform_gauge."""
return {
"total": 4.5,
"user": 3.8,
"system": 0.7,
"idle": 95.5,
"nice": 0.0,
"iowait": 0.5,
"irq": 0.0,
"steal": 0.0,
"guest": 0.1,
"ctx_switches": 6727.5,
"interrupts": 3000.4,
"soft_interrupts": 1782.5,
"_levels": {
"total": {"level": "ok", "prominent": True},
"steal": {"level": "ok", "prominent": True},
},
}
# ---------------------------------------------------------------- structure
def test_render_produces_four_rows(cpu_payload_linux, cpu_fields):
rows = render(cpu_payload_linux, cpu_fields)
assert len(rows) == 4
def test_render_first_row_has_cpu_title(cpu_payload_linux, cpu_fields):
rows = render(cpu_payload_linux, cpu_fields)
first_row_text = " ".join(c.text for c in rows[0].cells)
assert "CPU" in first_row_text
def test_render_first_row_carries_total_value(cpu_payload_linux, cpu_fields):
rows = render(cpu_payload_linux, cpu_fields)
flat = " ".join(c.text for row in rows for c in row.cells)
assert "4.5%" in flat
def test_render_first_row_includes_idle_and_ctx_sw(cpu_payload_linux, cpu_fields):
rows = render(cpu_payload_linux, cpu_fields)
first = " ".join(c.text for c in rows[0].cells)
assert "idle" in first
assert "95.5%" in first
assert "ctx_sw" in first
def test_render_ctx_switches_uses_k_scaling(cpu_payload_linux, cpu_fields):
rows = render(cpu_payload_linux, cpu_fields)
flat = " ".join(c.text for row in rows for c in row.cells)
# 6727.5 → "6.6K" with our auto-unit (6727.5/1024 ≈ 6.57)
assert "K" in flat
def test_render_lines_2_4_form_three_column_grid(cpu_payload_linux, cpu_fields):
"""Lines 2-4 must each carry 6 cells (3 label+value pairs)."""
rows = render(cpu_payload_linux, cpu_fields)
for i in (1, 2, 3):
assert len(rows[i].cells) == 6, f"row {i} has {len(rows[i].cells)} cells"
def test_render_columns_align_across_rows(cpu_payload_linux, cpu_fields):
"""Each column has uniform width across all rows."""
rows = render(cpu_payload_linux, cpu_fields)
ncols = max(len(r.cells) for r in rows)
for col in range(ncols):
widths = {len(r.cells[col].text) for r in rows if col < len(r.cells)}
assert len(widths) == 1, f"col {col} widths differ: {widths}"
def test_render_includes_user_system_iowait_on_linux(cpu_payload_linux, cpu_fields):
rows = render(cpu_payload_linux, cpu_fields)
flat = " ".join(c.text for row in rows for c in row.cells)
assert "user" in flat
assert "system" in flat
assert "iowait" in flat
def test_render_includes_irq_nice_steal(cpu_payload_linux, cpu_fields):
rows = render(cpu_payload_linux, cpu_fields)
flat = " ".join(c.text for row in rows for c in row.cells)
assert "irq" in flat
assert "nice" in flat
assert "steal" in flat
def test_render_handles_empty_payload(cpu_fields):
"""Cycle-0: just the title, no data rows."""
rows = render({}, cpu_fields)
assert len(rows) == 1
flat = " ".join(c.text for c in rows[0].cells)
assert "CPU" in flat
def test_render_windows_path_uses_idle_core_dpc(cpu_fields):
"""Without `user`, the renderer falls back to idle/core/dpc (v4 Windows)."""
payload = {
"total": 5.0,
"idle": 95.0, # idle is present but no user → idle_tag triggers
"irq": 0.0,
"nice": 0.0,
"steal": 0.0,
"interrupts": 2000.0,
"_levels": {},
}
rows = render(payload, cpu_fields)
flat = " ".join(c.text for row in rows for c in row.cells)
# On the Windows path, idle/core/dpc populate column 1.
assert "idle" in flat
def test_render_value_columns_have_stable_width_across_cycles(cpu_fields):
"""Value columns are floored at a fixed width — same width whether
every stat is 5.0% or 100.0%."""
payload_low = {
"total": 5.0,
"user": 1.0,
"system": 0.5,
"idle": 95.0,
"iowait": 0.0,
"irq": 0.0,
"nice": 0.0,
"steal": 0.0,
"guest": 0.0,
"ctx_switches": 100.0,
"interrupts": 50.0,
"soft_interrupts": 20.0,
"_levels": {},
}
payload_high = {
"total": 100.0,
"user": 99.9,
"system": 99.9,
"idle": 0.0,
"iowait": 99.9,
"irq": 99.9,
"nice": 99.9,
"steal": 99.9,
"guest": 99.9,
"ctx_switches": 9999.0,
"interrupts": 9999.0,
"soft_interrupts": 9999.0,
"_levels": {},
}
rows_low = render(payload_low, cpu_fields)
rows_high = render(payload_high, cpu_fields)
# Compare value-column widths (cols 1, 3, 5) between the two cycles.
for col in (1, 3, 5):
w_low = {len(r.cells[col].text) for r in rows_low if col < len(r.cells)}
w_high = {len(r.cells[col].text) for r in rows_high if col < len(r.cells)}
assert w_low == w_high, f"col {col} widths differ across cycles: {w_low} vs {w_high}"
def test_render_uses_header_role_for_cpu_title(cpu_payload_linux, cpu_fields):
rows = render(cpu_payload_linux, cpu_fields)
# First cell of first row is the CPU title (after potential padding).
first_cell = rows[0].cells[0]
assert first_cell.color == ColorRole.HEADER
assert "CPU" in first_cell.text