mirror of
https://github.com/nicolargo/glances.git
synced 2026-09-29 21:45:08 -04:00
Three more of 2.X-c, on both surfaces. Unlike part 1's `b`/`6`/`F`, v5 rendered only ONE mode of each of these -- but the data for the second was already collected, so each is a renderer change, not a model change. `B` disk I/O byte/s vs IOPS. `read_count`/`write_count` were already collected and flagged `internal` (out of the generic renderer's default columns, never out of the payload). They now carry v4's own `IOR/s`/`IOW/s` short names, so swapping the field pair swaps the header with it on both surfaces -- the labels come from the schema. IOPS need their own formatter: counts scale by 1000 and carry no unit, where bytes scale by 1024 and carry a `B`. `_format_count_rate` and `formatIops` are that pair, deliberately not `formatCount` (1024-based). `0` load average vs Irix percentage (issue #1554). `cpucore` was already collected and `internal`. v4 reads the count from the `core` plugin's `log_core()`; v5 reads the field the load payload already carries -- same number, no cross-plugin reach. Both surfaces keep v4's guard: an absent or zero core count falls back to the plain float rather than dividing by zero. The key changes what the cell DISPLAYS, never which level colours it -- per-core normalisation is already implicit in the thresholds (`normalize_by: cpucore`), and a test pins that. `T` network Rx/Tx apart vs combined. A divergence in route, not in value: v4 renders a `bytes_all` field its model computes; v5's schema has no such field, so both renderers sum the two rates they already carry. The sum of two rates over one interval IS the combined rate. The combined cell takes no threshold colour -- the two fields own their levels and a sum belongs to neither, which is why v4 paints its own combined cell plain. The diskio render fixture gained `read_count`/`write_count` on every row and their short names. It predated the fields, so under `B` every row was dropped for a missing value and the header fell back to the raw field names. A fixture that does not carry what the server sends tests nothing. Verified against real plugins: `B` turns `R/s 0B` into `IOR/s 0`, `0` turns `1 min 0.46` into `1 min 11.6%`, and `T` turns `642b 642b` into `1.3Kb`. 2.X-c now stands at six of nine. The remaining three are blocked on data, not on keys: `U` needs the raw counter `_transform_gauge` replaces with its rate, `L` needs `read_time`/`write_time` collected at all, and `S` needs a v5 history store. Section 10 records each with its file reference. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PW4fwSR6ceSmETznbLExNK
275 lines
9.5 KiB
Python
275 lines
9.5 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.model_v5 import PluginModel
|
|
from glances.plugins.diskio.render_curses_v5 import render
|
|
|
|
|
|
@pytest.fixture
|
|
def diskio_fields():
|
|
"""The REAL schema, as production passes it (curses_renderer_v5.py:1459).
|
|
|
|
The column labels come from it (field_label), so a hand-written subset
|
|
without `short_name` would test a header no user ever sees.
|
|
"""
|
|
return PluginModel.fields_description
|
|
|
|
|
|
@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
|
|
|
|
|
|
def test_item_rows_are_marked_for_the_truncation_counter(diskio_payload, diskio_fields):
|
|
rows = render(diskio_payload, diskio_fields)
|
|
assert rows[0].item_start is False # header
|
|
assert sum(r.item_start for r in rows) == len(diskio_payload["data"])
|
|
|
|
|
|
# ---------------------------------------------------------------- hide_zero (design §5.1)
|
|
|
|
|
|
def test_render_skips_hidden_disks(diskio_fields):
|
|
payload = {
|
|
"data": [
|
|
{"disk_name": "sda", "read_bytes": 100.0, "write_bytes": 50.0, "hidden": False},
|
|
{"disk_name": "loop0", "read_bytes": 0.0, "write_bytes": 0.0, "hidden": True},
|
|
],
|
|
"_levels": {},
|
|
}
|
|
rows = render(payload, diskio_fields)
|
|
flat = " ".join(c.text for row in rows for c in row.cells)
|
|
assert "sda" in flat
|
|
assert "loop0" not in flat
|
|
|
|
|
|
def test_render_keeps_disk_when_hidden_key_absent(diskio_fields):
|
|
payload = {
|
|
"data": [{"disk_name": "sda", "read_bytes": 100.0, "write_bytes": 50.0}],
|
|
"_levels": {},
|
|
}
|
|
rows = render(payload, diskio_fields)
|
|
flat = " ".join(c.text for row in rows for c in row.cells)
|
|
assert "sda" in flat
|
|
|
|
|
|
# ---------------------------------------------------------------- alias (design §5.5)
|
|
|
|
|
|
def test_render_displays_alias_instead_of_disk_name(diskio_fields):
|
|
"""v4 parity (`diskio/__init__.py:262`): the alias REPLACES the raw
|
|
disk name in the rendered row."""
|
|
payload = {
|
|
"data": [{"disk_name": "sda", "alias": "SystemDisk", "read_bytes": 0.0, "write_bytes": 0.0}],
|
|
"_levels": {"sda": {}},
|
|
}
|
|
rows = render(payload, diskio_fields)
|
|
name_cell = rows[1].cells[0].text
|
|
assert "SystemDisk" in name_cell
|
|
assert name_cell.strip() != "sda"
|
|
|
|
|
|
def test_render_falls_back_to_disk_name_when_no_alias(diskio_fields):
|
|
payload = {
|
|
"data": [{"disk_name": "sda", "read_bytes": 0.0, "write_bytes": 0.0}],
|
|
"_levels": {"sda": {}},
|
|
}
|
|
rows = render(payload, diskio_fields)
|
|
assert "sda" in rows[1].cells[0].text
|
|
|
|
|
|
def test_iops_mode_swaps_both_the_columns_and_the_header():
|
|
"""`B` (v4 `_handle_diskio_iops`). The labels come from the schema, so
|
|
swapping the field pair swaps the header with it."""
|
|
from glances.plugins.diskio.render_curses_v5 import render
|
|
|
|
payload = {
|
|
"data": [
|
|
{"disk_name": "sda", "read_bytes": 2048.0, "write_bytes": 0.0, "read_count": 2500.0, "write_count": 7.4}
|
|
],
|
|
"_levels": {},
|
|
}
|
|
fields = {
|
|
"read_bytes": {"short_name": "R/s"},
|
|
"write_bytes": {"short_name": "W/s"},
|
|
"read_count": {"short_name": "IOR/s"},
|
|
"write_count": {"short_name": "IOW/s"},
|
|
}
|
|
|
|
default_rows = render(payload, fields)
|
|
iops_rows = render(payload, fields, view={"diskio_iops": True})
|
|
|
|
assert [c.text.strip() for c in default_rows[0].cells][1:] == ["R/s", "W/s"]
|
|
assert [c.text.strip() for c in iops_rows[0].cells][1:] == ["IOR/s", "IOW/s"]
|
|
# Byte rates take the 1024 scale and a `B`; counts take 1000 and no unit.
|
|
assert [c.text.strip() for c in default_rows[1].cells][1:] == ["2.0K", "0B"]
|
|
assert [c.text.strip() for c in iops_rows[1].cells][1:] == ["2.5K", "7"]
|
|
|
|
|
|
def test_iops_mode_skips_a_disk_with_no_count_yet():
|
|
"""Cycle 1 leaves every rate field None, counts included."""
|
|
from glances.plugins.diskio.render_curses_v5 import render
|
|
|
|
payload = {
|
|
"data": [{"disk_name": "sda", "read_bytes": 1.0, "write_bytes": 1.0, "read_count": None, "write_count": None}],
|
|
"_levels": {},
|
|
}
|
|
assert len(render(payload, {}, view={"diskio_iops": True})) == 1, "header only"
|