Files
glances/tests/test_plugin_fs_render_curses_v5.py
nicolargo a367dce523 feat(v5): fs thresholds 70/80/90 + non-prominent + alert "ongoing" marker
Three small UX adjustments raised on the live TUI:

1. ``fs.percent`` schema: explicit ``prominent: False``. Colours the
   Used cell at alert level but no longer reverse-videos it (the
   framework defaults missing ``prominent`` to True). Consistent with
   sin/sout in memswap.

2. ``fs.percent`` default thresholds raised to 70/80/90. Filesystems
   often sit at 60-70% on healthy hosts and the old 50/70/90 ladder
   produced noisy "careful" warnings. ``mem`` keeps the stricter
   50/70/90.

3. Alert block UI clarifies active-vs-resolved state:
   - Header was ``ALERTS (N)`` — now ``ALERT (X ongoing / Y total)``.
     ``ongoing`` = unique (plugin, key, field) tuples whose
     most-recent event has a non-ok level. ``total`` = full history
     length.
   - Each event row ending in a non-ok level for the **latest** event
     of its tuple gets a visible ``(ongoing)`` suffix on the level
     cell:
       ``careful (ongoing)``
       ``careful → warning (ongoing)``
     Older non-ok events superseded by a later resolution lose the
     marker — they are no longer ongoing. Resolutions
     (``warning → ok``) never carry the marker.

7 new tests:
- ``test_percent_is_watched_but_not_prominent`` (fs schema)
- ``test_percent_default_thresholds_are_70_80_90`` (fs schema)
- 1 fs runtime test updated to the new ladder + non-prominent
- 4 renderer tests covering header counts, ongoing marker, resolution
  exemption, latest-per-tuple semantics.

Full v5 suite: 734 passed, lint clean.
2026-05-15 17:39:22 +02:00

171 lines
5.7 KiB
Python

"""Glances v5 — tests for the fs plugin's curses renderer."""
from __future__ import annotations
import pytest
from glances.outputs.curses_renderer_v5 import ColorRole
from glances.plugins.fs.render_curses_v5 import render
@pytest.fixture
def fs_fields():
return {
"mnt_point": {"unit": "string", "primary_key": True},
"device_name": {"unit": "string"},
"fs_type": {"unit": "string", "internal": True},
"options": {"unit": "string", "internal": True},
"size": {"unit": "bytes"},
"used": {"unit": "bytes"},
"free": {"unit": "bytes"},
"percent": {"unit": "percent", "watched": True, "prominent": True},
}
@pytest.fixture
def fs_payload():
return {
"data": [
{
"mnt_point": "/",
"device_name": "/dev/sda1",
"fs_type": "ext4",
"options": "rw,relatime",
"size": 500 * 1024**3,
"used": 125 * 1024**3,
"free": 375 * 1024**3,
"percent": 25.0,
},
{
"mnt_point": "/home",
"device_name": "/dev/sda2",
"fs_type": "ext4",
"options": "rw,relatime",
"size": 1024**4,
"used": 512 * 1024**3,
"free": 512 * 1024**3,
"percent": 50.0,
},
],
"_levels": {
"/": {"percent": {"level": "ok", "prominent": True}},
"/home": {"percent": {"level": "careful", "prominent": True}},
},
}
# ---------------------------------------------------------------- structure
def test_render_first_row_is_filesys_header(fs_payload, fs_fields):
rows = render(fs_payload, fs_fields)
text = " ".join(c.text for c in rows[0].cells)
assert "FILE SYS" in text
assert "Used" in text
assert "Total" in text
def test_render_one_row_per_filesystem(fs_payload, fs_fields):
rows = render(fs_payload, fs_fields)
# 1 header + 2 filesystems
assert len(rows) == 3
def test_render_filesystems_sorted_by_mnt_point(fs_payload, fs_fields):
rows = render(fs_payload, fs_fields)
mnts = [r.cells[0].text.strip() for r in rows[1:]]
assert mnts == sorted(mnts)
def test_render_each_data_row_has_three_cells(fs_payload, fs_fields):
rows = render(fs_payload, fs_fields)
for r in rows[1:]:
assert len(r.cells) == 3
def test_render_used_and_total_in_bytes(fs_payload, fs_fields):
rows = render(fs_payload, fs_fields)
flat = " ".join(c.text for row in rows[1:] for c in row.cells)
# 125 GiB → "125.0G", 500 GiB → "500.0G"
assert "125.0G" in flat
assert "500.0G" in flat
def test_render_columns_align_across_rows(fs_payload, fs_fields):
rows = render(fs_payload, fs_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_block_width_fits_sidebar_cap(fs_payload, fs_fields):
"""Row width (cells + 1-char gaps) must stay ≤ 34 (left sidebar cap)."""
rows = render(fs_payload, fs_fields)
for r in rows:
natural_w = sum(len(c.text) for c in r.cells) + max(0, len(r.cells) - 1)
assert natural_w <= 34, f"row width {natural_w} exceeds sidebar cap 34"
def test_render_handles_empty_data(fs_fields):
rows = render({"data": [], "_levels": {}}, fs_fields)
assert len(rows) == 1
def test_render_handles_empty_payload(fs_fields):
rows = render({}, fs_fields)
assert len(rows) == 1
flat = " ".join(c.text for c in rows[0].cells)
assert "FILE SYS" in flat
# ---------------------------------------------------------------- truncation
def test_render_long_mnt_point_truncated_with_underscore(fs_fields):
"""Long mountpoints are tail-truncated with a leading ``_`` (v4 parity)."""
long_mnt = "/very/long/mountpoint/path/that/exceeds/limits"
payload = {
"data": [{"mnt_point": long_mnt, "size": 1024**3, "used": 0, "free": 1024**3, "percent": 0.0}],
"_levels": {long_mnt: {}},
}
rows = render(payload, fs_fields)
mnt_text = rows[1].cells[0].text
assert mnt_text.startswith("_")
# Width matches the value-row mnt column.
assert len(mnt_text) == len(rows[0].cells[0].text)
# ---------------------------------------------------------------- color
def test_render_used_cell_inherits_percent_level(fs_fields):
"""Per-fs ``_levels.<mnt>.percent`` drives the Used cell color (v4 parity:
v4 decorates the ``used`` cell with the percent threshold). The cell is
coloured but NOT prominent — see fs.percent schema (``prominent: False``)."""
payload = {
"data": [{"mnt_point": "/", "size": 100, "used": 95, "free": 5, "percent": 95.0}],
"_levels": {"/": {"percent": {"level": "critical", "prominent": False}}},
}
rows = render(payload, fs_fields)
used_cell = rows[1].cells[1]
assert used_cell.color == ColorRole.CRITICAL
assert used_cell.prominent is False
def test_render_title_role_header_when_no_prominent_alert(fs_payload, fs_fields):
rows = render(fs_payload, fs_fields)
title = rows[0].cells[0]
assert title.color == ColorRole.HEADER
assert title.bold is True
def test_render_title_role_escalates_on_critical(fs_fields):
payload = {
"data": [{"mnt_point": "/", "size": 100, "used": 95, "free": 5, "percent": 95.0}],
"_levels": {"/": {"percent": {"level": "critical", "prominent": True}}},
}
rows = render(payload, fs_fields)
assert rows[0].cells[0].color == ColorRole.CRITICAL
assert rows[0].cells[0].bold is True