testsAndMisc/linux_configuration/tests/test_usage_report_host.py
Krzysztof kuhy Rudnicki ad646541ad
Some checks are pending
Pre-commit checks / pre-commit (push) Waiting to run
Python tests / test (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
usage_report: split _usage_report_render.py 310 -> 181 + 123 + 24
Verified by the 100%-coverage tests from the previous commit, written
before this seam existed.

_cpu_table/_dedupe_ram/_ram_table/_gpu_table move to
_usage_report_tables.py with the two constants they own. _md_escape and
_fmt_h move to _usage_report_format.py: both the tables module and the
render module need them, so a third module is what avoids a cycle.
Import sites name the owning module; no re-export barrel.

usage_report.py needed no edit - it imports _fmt_h and _render_report
from the render module, and both names still resolve there.

Evidence the move was verbatim: an AST comparison of all 10 functions
against HEAD reports 10 identical, 0 mismatches. That matters because I
retyped rather than cut-and-pasted, and did briefly introduce a literal
× where the original had × - ruff's RUF001 caught it.

Coverage stays 100% line+branch on all three (15/55/54 stmts, 4/10/14
branches, 0 missed). Test count 43 before and after, summed across the
five files it now lives in - the check that matters for a test split,
since a file outside the runner's glob is silently never collected.
Directory total 143 passed, unchanged.

Ran the real program: a full report from today's atop + pmon logs, every
section rendering through the new boundaries including the RAM sibling
dedupe and its (+20 more) truncation.

Over-cap 41 -> 39. No suppressions; a shared test-helper module was tried
and abandoned because name-tests-test would have needed an exclusion.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
2026-08-17 11:34:08 +02:00

151 lines
4.7 KiB
Python

"""Tests for usage_report's host fingerprint: the /proc and nvidia-smi probes
behind _host_profile, and the Markdown bullets _fingerprint_section builds.
"""
from __future__ import annotations
import io
from typing import TYPE_CHECKING
import _usage_report_render as render
if TYPE_CHECKING:
from pathlib import Path
import pytest
def _fake_proc_files(
monkeypatch: pytest.MonkeyPatch,
contents: dict[str, str | OSError],
) -> None:
"""Route Path.open for /proc/cpuinfo and /proc/meminfo to canned text.
_host_profile only ever opens those two paths, so every caller supplies
both and there is no real-filesystem fallback to keep here.
"""
def fake_open(
self: Path,
*_args: object,
**_kwargs: object,
) -> io.StringIO:
value = contents[str(self)]
if isinstance(value, OSError):
raise value
return io.StringIO(value)
monkeypatch.setattr(render.Path, "open", fake_open)
def _stub_platform(monkeypatch: pytest.MonkeyPatch, *, cpus: int | None) -> None:
"""Pin hostname, kernel and CPU count so assertions are machine-agnostic."""
monkeypatch.setattr(render.platform, "node", lambda: "box")
monkeypatch.setattr(render.platform, "release", lambda: "6.1.0")
monkeypatch.setattr(render.os, "cpu_count", lambda: cpus)
# --------------------------------------------------------------------------- #
# _host_profile
# --------------------------------------------------------------------------- #
def test_host_profile_reads_cpu_model_and_memory(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""CPU model and total memory are lifted out of the /proc files."""
_stub_platform(monkeypatch, cpus=8)
monkeypatch.setattr(render, "_run", lambda _cmd: "")
_fake_proc_files(
monkeypatch,
{
"/proc/cpuinfo": "flags\t: x\nmodel name\t: Ryzen 9\nmodel name\t: other\n",
"/proc/meminfo": "SwapTotal: 1 kB\nMemTotal: 32768000 kB\n",
},
)
info = render._host_profile()
assert info["hostname"] == "box"
assert info["kernel"] == "6.1.0"
assert info["cpus_online"] == "8"
assert info["cpu_model"] == "Ryzen 9"
assert info["memory_total_gib"] == "31.2"
assert "gpu" not in info
def test_host_profile_includes_gpu_when_nvidia_smi_answers(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Multi-line nvidia-smi output is flattened onto one line."""
_stub_platform(monkeypatch, cpus=1)
monkeypatch.setattr(render, "_run", lambda _cmd: "GPU0, 8 GiB\nGPU1, 8 GiB\n")
_fake_proc_files(monkeypatch, {"/proc/cpuinfo": "", "/proc/meminfo": ""})
assert render._host_profile()["gpu"] == "GPU0, 8 GiB; GPU1, 8 GiB"
def test_host_profile_survives_unreadable_proc_files(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""An OSError on either /proc file leaves that key absent, not raising."""
_stub_platform(monkeypatch, cpus=1)
monkeypatch.setattr(render, "_run", lambda _cmd: "")
_fake_proc_files(
monkeypatch,
{
"/proc/cpuinfo": OSError("nope"),
"/proc/meminfo": OSError("nope"),
},
)
info = render._host_profile()
assert "cpu_model" not in info
assert "memory_total_gib" not in info
def test_host_profile_survives_meminfo_without_digits(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""A MemTotal line with no parseable number is skipped, not fatal."""
_stub_platform(monkeypatch, cpus=1)
monkeypatch.setattr(render, "_run", lambda _cmd: "")
_fake_proc_files(
monkeypatch,
{"/proc/cpuinfo": "", "/proc/meminfo": "MemTotal: kB\n"},
)
assert "memory_total_gib" not in render._host_profile()
def test_host_profile_defaults_cpu_count_to_zero(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""os.cpu_count() returning None renders as '0' rather than 'None'."""
_stub_platform(monkeypatch, cpus=None)
monkeypatch.setattr(render, "_run", lambda _cmd: "")
_fake_proc_files(monkeypatch, {"/proc/cpuinfo": "", "/proc/meminfo": ""})
assert render._host_profile()["cpus_online"] == "0"
# --------------------------------------------------------------------------- #
# _fingerprint_section
# --------------------------------------------------------------------------- #
def test_fingerprint_section_renders_each_fact_as_a_bullet(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Every host-profile key becomes one bold Markdown bullet."""
monkeypatch.setattr(
render,
"_host_profile",
lambda: {"hostname": "box", "kernel": "6"},
)
assert render._fingerprint_section() == [
"## Host",
"",
"- **hostname**: box",
"- **kernel**: 6",
"",
]