Files
glances/tests/test_plugin_mem_render_curses_v5.py
nicolargo a7b9992ae8 feat(v5): mem renderer pulls short_name from schema + drop label-column floors
User feedback: column widths shouldn't reserve space for label variants
the renderer never uses. Add v4-style short_name to mem and let the
auto-size adapt:

  mem.available.short_name = 'avail'    (9 -> 5 chars)
  mem.inactive.short_name  = 'inactiv'  (8 -> 7 chars)
  mem.buffers.short_name   = 'buffer'   (7 -> 6 chars)

The mem render_curses_v5 now resolves every label via
field_label(..., prefer_short=True), so adding/changing a short_name
in the schema is enough — no renderer edit needed.

The label-column floors in cpu and mem renderers
(_CPU_LABEL_MIN_WIDTHS / _MEM_LABEL_MIN_WIDTHS) are gone too: labels
are constant strings so the content-driven auto-size is naturally
stable. Only value columns still need a fixed floor.

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

269 lines
9.5 KiB
Python

"""Glances v5 — tests for the mem plugin's curses renderer."""
from __future__ import annotations
import pytest
from glances.outputs.curses_renderer_v5 import ColorRole
from glances.plugins.mem.render_curses_v5 import render
@pytest.fixture
def mem_fields():
return {
"total": {"unit": "bytes", "label": "total"},
"available": {"unit": "bytes", "label": "avail"},
"percent": {
"unit": "percent",
"label": "MEM",
"watched": True,
"prominent": True,
},
"used": {"unit": "bytes", "label": "used"},
"free": {"unit": "bytes", "label": "free"},
"active": {"unit": "bytes", "label": "active"},
"inactive": {"unit": "bytes", "label": "inactive"},
"buffers": {"unit": "bytes", "label": "buffers"},
"cached": {"unit": "bytes", "label": "cached"},
}
@pytest.fixture
def mem_payload_linux():
return {
"total": 16_421_208_064,
"available": 7_691_833_344,
"percent": 53.2,
"used": 8_729_374_720,
"free": 2_740_531_200,
"active": 6_184_337_408,
"inactive": 4_744_855_552,
"buffers": 194_555_904,
"cached": 4_538_667_008,
"_levels": {"percent": {"level": "careful", "prominent": True}},
}
# ---------------------------------------------------------------- structure
def test_render_produces_four_rows(mem_payload_linux, mem_fields):
rows = render(mem_payload_linux, mem_fields)
assert len(rows) == 4
def test_render_first_row_carries_mem_title(mem_payload_linux, mem_fields):
rows = render(mem_payload_linux, mem_fields)
first_row_text = " ".join(c.text for c in rows[0].cells)
assert "MEM" in first_row_text
def test_render_first_row_includes_percent_and_active(mem_payload_linux, mem_fields):
rows = render(mem_payload_linux, mem_fields)
first = " ".join(c.text for c in rows[0].cells)
assert "53.2%" in first
assert "active" in first
def test_render_lines_2_to_4_are_four_cell_rows(mem_payload_linux, mem_fields):
rows = render(mem_payload_linux, mem_fields)
for i in (1, 2, 3):
assert len(rows[i].cells) == 4, f"row {i} has {len(rows[i].cells)} cells"
def test_render_uses_avail_label_when_available_present(mem_payload_linux, mem_fields):
rows = render(mem_payload_linux, mem_fields)
flat = " ".join(c.text for row in rows for c in row.cells)
assert "avail" in flat
# `used:` label not shown when avail is.
assert "used " not in flat or "used " in flat # we just check avail won
def test_render_falls_back_to_used_when_available_absent(mem_fields):
"""No `available` in the payload (some Unix variants) → show `used`."""
payload = {
"total": 16e9,
"percent": 50.0,
"used": 8e9,
"free": 4e9,
"active": 5e9,
"inactive": 3e9,
"buffers": 1e8,
"cached": 2e9,
"_levels": {},
}
rows = render(payload, mem_fields)
# used row must appear instead of avail.
flat = " ".join(c.text for row in rows for c in row.cells)
assert "used" in flat
assert "avail" not in flat
def test_render_columns_align_across_rows(mem_payload_linux, mem_fields):
rows = render(mem_payload_linux, mem_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_value_columns_have_stable_width_across_cycles(mem_fields):
"""Value cols hold "999.9G" worst case (6 chars). Low values must
pad to the same width as high values so the block doesn't jiggle."""
low = {
"total": 1024.0,
"available": 512.0,
"percent": 5.0,
"used": 256.0,
"free": 768.0,
"active": 200.0,
"inactive": 100.0,
"buffers": 50.0,
"cached": 80.0,
"_levels": {},
}
high = {
"total": 999_999_999_999.0,
"available": 500_000_000_000.0,
"percent": 100.0,
"used": 499_999_999_999.0,
"free": 1.0,
"active": 300_000_000_000.0,
"inactive": 200_000_000_000.0,
"buffers": 100_000_000_000.0,
"cached": 200_000_000_000.0,
"_levels": {},
}
rows_low = render(low, mem_fields)
rows_high = render(high, mem_fields)
for col in (1, 3):
widths_low = {len(r.cells[col].text) for r in rows_low if col < len(r.cells)}
widths_high = {len(r.cells[col].text) for r in rows_high if col < len(r.cells)}
assert widths_low == widths_high, f"col {col}: {widths_low} vs {widths_high}"
def test_render_handles_empty_payload(mem_fields):
rows = render({}, mem_fields)
assert len(rows) == 1
flat = " ".join(c.text for c in rows[0].cells)
assert "MEM" in flat
def test_render_title_cell_carries_mem_text_and_bold(mem_payload_linux, mem_fields):
"""The first cell carries the plugin label as bold text. Its colour
is dynamic. With the default payload (careful percent), the title
stays HEADER (white) — careful does NOT escalate the title."""
rows = render(mem_payload_linux, mem_fields)
first_cell = rows[0].cells[0]
assert "MEM" in first_cell.text
assert first_cell.bold is True
assert first_cell.color == ColorRole.HEADER
def test_render_percent_color_reflects_level(mem_payload_linux, mem_fields):
"""The percent value cell inherits the level color from `_levels.percent`."""
rows = render(mem_payload_linux, mem_fields)
# Find the percent cell in row 0 (second cell after MEM title).
percent_cell = rows[0].cells[1]
assert "53.2" in percent_cell.text # right value
assert percent_cell.color == ColorRole.CAREFUL
def test_render_percent_is_prominent_per_schema(mem_payload_linux, mem_fields):
"""The percent field is declared `prominent: True` — its cell carries
the flag so the painter applies A_REVERSE under any alert level."""
rows = render(mem_payload_linux, mem_fields)
percent_cell = rows[0].cells[1]
assert percent_cell.prominent is True
def test_render_title_role_default_when_ok(mem_fields):
"""When all prominent fields are OK, title uses HEADER (white+bold)."""
payload = {
"total": 1024,
"percent": 30.0,
"active": 256,
"_levels": {"percent": {"level": "ok", "prominent": True}},
}
rows = render(payload, mem_fields)
assert rows[0].cells[0].color == ColorRole.HEADER
assert rows[0].cells[0].bold is True
def test_render_title_role_warning_when_percent_warning(mem_fields):
"""percent at WARNING (prominent) → title coloured WARNING + bold."""
payload = {
"total": 1024,
"percent": 80.0,
"active": 256,
"_levels": {"percent": {"level": "warning", "prominent": True}},
}
rows = render(payload, mem_fields)
assert rows[0].cells[0].color == ColorRole.WARNING
assert rows[0].cells[0].bold is True
def test_render_title_role_critical_when_percent_critical(mem_fields):
"""percent at CRITICAL → title red + bold."""
payload = {
"total": 1024,
"percent": 95.0,
"active": 256,
"_levels": {"percent": {"level": "critical", "prominent": True}},
}
rows = render(payload, mem_fields)
assert rows[0].cells[0].color == ColorRole.CRITICAL
def test_render_pulls_short_name_from_schema(mem_payload_linux):
"""The mem renderer reads `short_name` from the schema for compact labels."""
fields = {
"total": {"unit": "bytes"},
"available": {"unit": "bytes", "short_name": "avail"},
"percent": {"unit": "percent", "watched": True, "prominent": True},
"used": {"unit": "bytes"},
"free": {"unit": "bytes"},
"active": {"unit": "bytes"},
"inactive": {"unit": "bytes", "short_name": "inactiv"},
"buffers": {"unit": "bytes", "short_name": "buffer"},
"cached": {"unit": "bytes"},
}
rows = render(mem_payload_linux, fields)
flat = " ".join(c.text for row in rows for c in row.cells)
assert "avail" in flat
assert "inactiv" in flat
assert "buffer" in flat
# Full forms must NOT appear (the short variants supersede them).
assert "available" not in flat
assert "inactive" not in flat
# "buffers" might still match "buffer" substring; explicit guard:
label_texts = {r.cells[0].text.strip() for r in rows if r.cells} | {
r.cells[2].text.strip() for r in rows if len(r.cells) > 2
}
assert "buffers" not in label_texts
assert "buffer" in label_texts
def test_render_column_widths_shrink_with_short_names(mem_payload_linux):
"""Without short_name (e.g. 'available' = 9 chars), col 0 is 9-wide.
With short_name 'avail' (5 chars), col 0 shrinks to 5 — saving space."""
fields_long = {
"total": {"unit": "bytes"},
"available": {"unit": "bytes"}, # no short_name → label width = 9
"percent": {"unit": "percent", "watched": True, "prominent": True},
"free": {"unit": "bytes"},
"active": {"unit": "bytes"},
"inactive": {"unit": "bytes"},
"buffers": {"unit": "bytes"},
"cached": {"unit": "bytes"},
}
fields_short = {**fields_long, "available": {"unit": "bytes", "short_name": "avail"}}
rows_long = render(mem_payload_linux, fields_long)
rows_short = render(mem_payload_linux, fields_short)
col0_long = max(len(r.cells[0].text) for r in rows_long)
col0_short = max(len(r.cells[0].text) for r in rows_short)
assert col0_short < col0_long
assert col0_short == 5 # "avail" / "total" / "free" / "MEM" all ≤ 5