Files
glances/tests/test_plugin_diskio_render_curses_v5.py
nicolargo 779dee82ab feat(v5): G4-diskio — port diskio plugin to v5 (collection)
Last entry of ``KNOWN_V5_MISSING_PLUGINS`` after this commit:
``("processlist",)``.

Model (``glances/plugins/diskio/model_v5.py``):
- Fields: disk_name (PK, string), read_count / write_count
  (rate, number, internal — exportable for IOPS consumers but
  not rendered), read_bytes / write_bytes (rate, bytespers, watched,
  ``prominent=False``, ``strict_thresholds=True``, NO default
  thresholds).
- Sustained disk traffic is host-specific (a DB server may stream
  MB/s by design) — alerts only fire when operators set
  ``read_bytes_warning=...`` per-disk or per-field in
  ``[diskio]``. ``strict_thresholds=True`` blocks the bare-``<level>``
  fallback (same pattern as memswap.sin/sout) so a legacy
  ``[diskio] careful=50`` cannot trigger spurious alerts.
- ``read_time``/``write_time`` and the derived ``read_latency`` /
  ``write_latency`` of v4 are not ported — deferred with the
  ``--diskio-latency`` mode.
- ``psutil.disk_io_counters()`` may raise or return ``None`` on
  platforms without disk I/O support — model returns ``[]`` rather
  than crashing.

Renderer (``glances/plugins/diskio/render_curses_v5.py``):

    DISK I/O              R/s     W/s
    nvme0n1               0B      0B
    sda                   1.4M   732K

- 3-cell rows, 18 + 1 + 7 + 1 + 7 = 34 chars (fits sidebar cap).
- Sorted by disk_name. Cycle-1 disks (no rate baseline) are skipped
  entirely — no ``-`` placeholder wall on startup.
- Rate cells display ``auto_unit(bytes_per_sec)`` WITHOUT a trailing
  ``/s`` — header carries the per-second semantic (v4 parity).
- Long disk names tail-truncated with leading underscore.

Adjacent:
- ``KNOWN_V5_MISSING_PLUGINS`` shrinks to ``("processlist",)``.
- ``test_attach_mcp_logs_known_v5_gaps`` updated.
- v4 catalogue grows a ``## diskio`` section +  footer.

28 new tests (13 model + 15 renderer). Full v5 suite: 762 passed.
2026-05-15 17:47:07 +02:00

190 lines
6.2 KiB
Python

"""Glances v5 — tests for the diskio plugin's curses renderer."""
from __future__ import annotations
import pytest
from glances.outputs.curses_renderer_v5 import ColorRole
from glances.plugins.diskio.render_curses_v5 import render
@pytest.fixture
def diskio_fields():
return {
"disk_name": {"unit": "string", "primary_key": True},
"read_count": {"unit": "number", "rate": True, "internal": True},
"write_count": {"unit": "number", "rate": True, "internal": True},
"read_bytes": {
"unit": "bytespers",
"rate": True,
"watched": True,
"prominent": False,
"strict_thresholds": True,
},
"write_bytes": {
"unit": "bytespers",
"rate": True,
"watched": True,
"prominent": False,
"strict_thresholds": True,
},
}
@pytest.fixture
def diskio_payload():
return {
"data": [
{
"disk_name": "sda",
"read_count": 100.0,
"write_count": 50.0,
"read_bytes": 1_500_000.0,
"write_bytes": 750_000.0,
},
{
"disk_name": "nvme0n1",
"read_count": 5.0,
"write_count": 2.0,
"read_bytes": 100.0,
"write_bytes": 50.0,
},
],
"_levels": {},
}
# ---------------------------------------------------------------- structure
def test_render_first_row_is_diskio_header(diskio_payload, diskio_fields):
rows = render(diskio_payload, diskio_fields)
text = " ".join(c.text for c in rows[0].cells)
assert "DISK I/O" in text
assert "R/s" in text
assert "W/s" in text
def test_render_one_row_per_disk(diskio_payload, diskio_fields):
rows = render(diskio_payload, diskio_fields)
# 1 header + 2 disks
assert len(rows) == 3
def test_render_disks_sorted_by_name(diskio_payload, diskio_fields):
rows = render(diskio_payload, diskio_fields)
names = [r.cells[0].text.strip() for r in rows[1:]]
assert names == sorted(names)
def test_render_each_data_row_has_three_cells(diskio_payload, diskio_fields):
rows = render(diskio_payload, diskio_fields)
for r in rows[1:]:
assert len(r.cells) == 3
def test_render_rate_cells_have_no_per_second_suffix(diskio_payload, diskio_fields):
"""The header carries the ``R/s`` / ``W/s`` labels so individual cells
show the byte rate without ``/s`` (v4 parity — saves column width)."""
rows = render(diskio_payload, diskio_fields)
for r in rows[1:]:
for c in r.cells[1:]:
assert "/s" not in c.text
def test_render_rate_values_use_auto_unit(diskio_payload, diskio_fields):
rows = render(diskio_payload, diskio_fields)
flat = " ".join(c.text for row in rows[1:] for c in row.cells)
# 1_500_000 B/s → "1.4M" ; 750_000 B/s → "732K" or "732.4K" depending
# on the auto-unit decimal policy. We just check the suffix appears.
assert "M" in flat or "K" in flat
def test_render_block_width_fits_sidebar_cap(diskio_payload, diskio_fields):
"""Row width ≤ 34 (left-sidebar cap)."""
rows = render(diskio_payload, diskio_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_columns_align_across_rows(diskio_payload, diskio_fields):
rows = render(diskio_payload, diskio_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_handles_empty_data(diskio_fields):
rows = render({"data": [], "_levels": {}}, diskio_fields)
assert len(rows) == 1
def test_render_handles_empty_payload(diskio_fields):
rows = render({}, diskio_fields)
assert len(rows) == 1
flat = " ".join(c.text for c in rows[0].cells)
assert "DISK I/O" in flat
def test_render_skips_disk_without_rate_yet(diskio_fields):
"""Cycle 1: read_bytes/write_bytes absent → skip the row entirely so
the user does not see a "-" placeholder for every disk on startup."""
payload = {
"data": [
{"disk_name": "sda", "read_count": 0.0, "write_count": 0.0},
# No read_bytes/write_bytes keys (base class strips on cycle 1).
],
"_levels": {},
}
rows = render(payload, diskio_fields)
# Header only.
assert len(rows) == 1
# ---------------------------------------------------------------- truncation
def test_render_long_disk_name_truncated_with_underscore(diskio_fields):
long_name = "very_long_disk_identifier_that_overflows"
payload = {
"data": [{"disk_name": long_name, "read_bytes": 0.0, "write_bytes": 0.0}],
"_levels": {long_name: {}},
}
rows = render(payload, diskio_fields)
name_text = rows[1].cells[0].text
assert name_text.startswith("_")
assert len(name_text) == len(rows[0].cells[0].text)
# ---------------------------------------------------------------- color
def test_render_rate_cells_default_color_when_no_thresholds(diskio_payload, diskio_fields):
"""No ``_levels`` entry → cells render in DEFAULT (no green)."""
rows = render(diskio_payload, diskio_fields)
for r in rows[1:]:
for c in r.cells[1:]:
assert c.color == ColorRole.DEFAULT
assert c.prominent is False
def test_render_rate_cell_inherits_level_when_threshold_fires(diskio_fields):
"""Per-disk ``_levels.<disk>.read_bytes`` drives the R/s cell color."""
payload = {
"data": [{"disk_name": "sda", "read_bytes": 15_000.0, "write_bytes": 100.0}],
"_levels": {"sda": {"read_bytes": {"level": "warning", "prominent": False}}},
}
rows = render(payload, diskio_fields)
rx_cell = rows[1].cells[1]
assert rx_cell.color == ColorRole.WARNING
assert rx_cell.prominent is False
def test_render_title_role_header_when_no_alert(diskio_payload, diskio_fields):
rows = render(diskio_payload, diskio_fields)
title = rows[0].cells[0]
assert title.color == ColorRole.HEADER
assert title.bold is True