mirror of
https://github.com/nicolargo/glances.git
synced 2026-09-23 18:45:44 -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
204 lines
7.5 KiB
Python
204 lines
7.5 KiB
Python
"""Glances v5 — tests for the load plugin's curses renderer."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
from glances.outputs.curses_renderer_v5 import ColorRole
|
|
from glances.plugins.load.model_v5 import PluginModel
|
|
from glances.plugins.load.render_curses_v5 import render
|
|
|
|
|
|
@pytest.fixture
|
|
def load_fields():
|
|
"""The REAL schema, not a hand-written stub.
|
|
|
|
Production always passes `PluginModel.fields_description`
|
|
(`glances/outputs/curses_renderer_v5.py`), whose labels are `short_name`s;
|
|
the stub this replaced carried `label` instead, so every label assertion
|
|
exercised `field_label()`'s *fallback* branch rather than the branch
|
|
production takes. Deleting a `short_name` from the model would then have
|
|
changed both the TUI and the WebUI (which resolves the same strings from
|
|
`/api/5/load/info`) with no test failing.
|
|
|
|
Only `field_label()` reads this mapping in the load renderer -- the
|
|
prominence and level decorations come from the payload's `_levels` -- so
|
|
swapping the stub for the schema changes nothing but the labels.
|
|
"""
|
|
return PluginModel.fields_description
|
|
|
|
|
|
@pytest.fixture
|
|
def load_payload():
|
|
return {
|
|
"min1": 0.857,
|
|
"min5": 0.716,
|
|
"min15": 0.801,
|
|
"cpucore": 16,
|
|
"_levels": {
|
|
"min5": {"level": "ok", "prominent": False},
|
|
"min15": {"level": "ok", "prominent": True},
|
|
},
|
|
}
|
|
|
|
|
|
# --------------------------------------------------------------- structure
|
|
|
|
|
|
def test_render_produces_four_rows(load_payload, load_fields):
|
|
rows = render(load_payload, load_fields)
|
|
assert len(rows) == 4
|
|
|
|
|
|
def test_render_first_row_has_load_title(load_payload, load_fields):
|
|
rows = render(load_payload, load_fields)
|
|
first = " ".join(c.text for c in rows[0].cells)
|
|
assert "LOAD" in first
|
|
|
|
|
|
def test_render_first_row_carries_corecount(load_payload, load_fields):
|
|
rows = render(load_payload, load_fields)
|
|
first = " ".join(c.text for c in rows[0].cells)
|
|
assert "core" in first
|
|
assert "16" in first
|
|
|
|
|
|
def test_render_first_cell_uses_header_role(load_payload, load_fields):
|
|
rows = render(load_payload, load_fields)
|
|
assert rows[0].cells[0].color == ColorRole.HEADER
|
|
|
|
|
|
def test_render_load_average_rows_have_n_min_labels(load_payload, load_fields):
|
|
rows = render(load_payload, load_fields)
|
|
labels = [r.cells[0].text.strip() for r in rows[1:]]
|
|
assert labels == ["1 min", "5 min", "15 min"]
|
|
|
|
|
|
def test_render_load_values_use_two_decimals(load_payload, load_fields):
|
|
"""v4 fidelity: `{:>6.2f}` produces e.g. ' 0.86'."""
|
|
rows = render(load_payload, load_fields)
|
|
# min1 = 0.857 → " 0.86"
|
|
assert "0.86" in rows[1].cells[1].text
|
|
# min5 = 0.716 → " 0.72"
|
|
assert "0.72" in rows[2].cells[1].text
|
|
# min15 = 0.801 → " 0.80"
|
|
assert "0.80" in rows[3].cells[1].text
|
|
|
|
|
|
def test_render_cpucore_is_internal_not_rendered_as_row(load_payload, load_fields):
|
|
"""cpucore is `internal: True` — never appears as its own row, only
|
|
on line 1 as the 'Ncore' suffix."""
|
|
rows = render(load_payload, load_fields)
|
|
label_texts = [r.cells[0].text.strip() for r in rows]
|
|
assert "cpucore" not in label_texts
|
|
assert "cores" not in label_texts
|
|
|
|
|
|
def test_render_min5_inherits_level_color(load_payload, load_fields):
|
|
"""The min5 row's value cell carries the OK color from `_levels.min5`."""
|
|
rows = render(load_payload, load_fields)
|
|
min5_cell = rows[2].cells[1]
|
|
assert min5_cell.color == ColorRole.OK
|
|
|
|
|
|
def test_render_min15_prominent_flag(load_payload, load_fields):
|
|
"""min15 is declared prominent: True — the level entry tags it as such."""
|
|
rows = render(load_payload, load_fields)
|
|
min15_cell = rows[3].cells[1]
|
|
assert min15_cell.prominent is True
|
|
|
|
|
|
def test_render_min15_warning_level(load_fields):
|
|
payload = {
|
|
"min1": 5.0,
|
|
"min5": 3.0,
|
|
"min15": 2.0,
|
|
"cpucore": 2,
|
|
"_levels": {"min15": {"level": "warning", "prominent": True}},
|
|
}
|
|
rows = render(payload, load_fields)
|
|
assert rows[3].cells[1].color == ColorRole.WARNING
|
|
|
|
|
|
def test_render_handles_empty_payload(load_fields):
|
|
rows = render({}, load_fields)
|
|
assert len(rows) == 1
|
|
flat = " ".join(c.text for c in rows[0].cells)
|
|
assert "LOAD" in flat
|
|
|
|
|
|
def test_render_handles_missing_cpucore(load_fields):
|
|
"""When cpucore is unknown (Windows, locked-down env) → no '4core' suffix."""
|
|
payload = {"min1": 1.0, "min5": 1.0, "min15": 1.0, "_levels": {}}
|
|
rows = render(payload, load_fields)
|
|
first = " ".join(c.text for c in rows[0].cells)
|
|
assert "core" not in first
|
|
|
|
|
|
def test_render_columns_align(load_payload, load_fields):
|
|
"""Label column has a uniform width across rows."""
|
|
rows = render(load_payload, load_fields)
|
|
label_widths = {len(r.cells[0].text) for r in rows if r.cells}
|
|
assert len(label_widths) == 1
|
|
|
|
|
|
def test_render_value_cells_share_width_across_header_and_body(load_payload, load_fields):
|
|
"""The corecount cell (header) and the load-average cells (body) must
|
|
have the same width so right edges align. Earlier bug: `{:3}core`
|
|
produced 7-char header value vs 6-char body values → 1-char overhang."""
|
|
rows = render(load_payload, load_fields)
|
|
widths = {len(r.cells[1].text) for r in rows if len(r.cells) >= 2}
|
|
assert len(widths) == 1, f"value cells not uniform: {widths}"
|
|
|
|
|
|
def test_render_total_line_width_matches_across_rows(load_payload, load_fields):
|
|
"""Each line's total rendered width (cells joined with 1 space) is identical."""
|
|
rows = render(load_payload, load_fields)
|
|
totals = {sum(len(c.text) for c in r.cells) + max(0, len(r.cells) - 1) for r in rows}
|
|
assert len(totals) == 1, f"line widths differ: {totals}"
|
|
|
|
|
|
def test_irix_mode_shows_percentages_of_the_core_count():
|
|
"""`0` (v4 `args.disable_irix`, issue #1554): each average divided by
|
|
`cpucore`, as a percentage."""
|
|
from glances.plugins.load.render_curses_v5 import render
|
|
|
|
payload = {"min1": 2.0, "min5": 1.0, "min15": 0.5, "cpucore": 4, "_levels": {}}
|
|
fields = {"min1": {"short_name": "1 min"}, "min5": {"short_name": "5 min"}, "min15": {"short_name": "15 min"}}
|
|
|
|
plain = [r.cells[1].text.strip() for r in render(payload, fields)[1:]]
|
|
irix = [r.cells[1].text.strip() for r in render(payload, fields, view={"load_irix": True})[1:]]
|
|
|
|
assert plain == ["2.00", "1.00", "0.50"]
|
|
assert irix == ["50.0%", "25.0%", "12.5%"]
|
|
|
|
|
|
def test_irix_mode_falls_back_when_the_core_count_is_unusable():
|
|
"""v4 guards on `log_core() != 0`; an absent or zero count must not divide
|
|
by zero, and must not print a nonsense percentage either."""
|
|
from glances.plugins.load.render_curses_v5 import render
|
|
|
|
for cores in (0, None):
|
|
payload = {"min1": 2.0, "min5": 1.0, "min15": 0.5, "cpucore": cores, "_levels": {}}
|
|
values = [r.cells[1].text.strip() for r in render(payload, {}, view={"load_irix": True})[1:]]
|
|
assert values == ["2.00", "1.00", "0.50"], cores
|
|
|
|
|
|
def test_irix_mode_keeps_the_threshold_colour():
|
|
"""The key changes what the cell DISPLAYS, never which level it is
|
|
coloured with — per-core normalisation is already implicit in the
|
|
threshold computation (`normalize_by: cpucore`)."""
|
|
from glances.outputs.curses_renderer_v5 import ColorRole
|
|
from glances.plugins.load.render_curses_v5 import render
|
|
|
|
payload = {
|
|
"min1": 2.0,
|
|
"min5": 1.0,
|
|
"min15": 8.0,
|
|
"cpucore": 4,
|
|
"_levels": {"min15": {"level": "critical", "prominent": True}},
|
|
}
|
|
row = render(payload, {}, view={"load_irix": True})[3]
|
|
assert row.cells[1].color == ColorRole.CRITICAL
|
|
assert row.cells[1].prominent is True
|