mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-04 14:25:08 +00:00
The SSE status stream sent 'disk_used_percent': 0 as a literal, so every
consumer of the live view showed 0% disk no matter how full the card was.
/api/v3/system/status computed it correctly; the stream that the dashboard
actually watches did not. On a Pi with a modest SD card that is the warning a
user most needs, and it was guaranteed to never appear.
The stream also omitted memory_available_mb. /api/v3/system/status carries it
with a comment spelling out why it matters: MemAvailable accounts for
reclaimable page cache, so it is what separates a board reading 70% "used" that
is fine from one reading 70% that is about to fail fork(). A 1GB Pi 3B+ can sit
at either. The number that predicts the failure was missing from the live view.
An unreadable disk now reports None rather than 0. The UI already renders null
as '--'; a confident 0 reads as "plenty of room", which is worse than a blank.
Metric collection moves to web_interface/system_metrics.py, with no Flask or app
imports. That is not cosmetic: importing web_interface.app constructs the Flask
application and a CacheManager, and the latter claims the cache directory with a
cleanup thread. The first version of these tests imported the generator directly
and broke test_cache_cleanup_thread_ownership ("one thread per directory") plus
four starlark route tests through that side effect. Reading a CPU percentage
should not boot a web application, and testing it should not either.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
104 lines
4.4 KiB
Python
104 lines
4.4 KiB
Python
"""Tests for the metrics the live status stream reports.
|
|
|
|
Two gaps made a struggling 1GB Pi look healthy on the dashboard:
|
|
|
|
* ``disk_used_percent`` was the literal ``0`` in the SSE generator, so every
|
|
consumer of the live stream displayed 0% disk no matter how full the card was.
|
|
/api/v3/system/status computed it properly, but the live view -- the one people
|
|
actually watch -- did not.
|
|
|
|
* ``memory_available_mb`` was absent. /api/v3/system/status carries it with a
|
|
comment explaining that MemAvailable, not used%, is what separates a board
|
|
that is fine from one about to fail fork(). The live stream omitted the one
|
|
number that predicts the failure.
|
|
|
|
A wrong number is worse than a missing one, so an unreadable disk now reports
|
|
None (the UI renders '--') rather than a confident 0.
|
|
|
|
These import web_interface.system_metrics rather than web_interface.app on
|
|
purpose: importing the app constructs a Flask application and a CacheManager,
|
|
and the latter claims the cache directory with a cleanup thread -- which broke
|
|
test_cache_cleanup_thread_ownership and the starlark route tests when this file
|
|
first reached for the generator directly.
|
|
"""
|
|
|
|
import sys
|
|
from pathlib import Path
|
|
from types import SimpleNamespace
|
|
|
|
import pytest
|
|
|
|
PROJECT_ROOT = Path(__file__).parent.parent
|
|
sys.path.insert(0, str(PROJECT_ROOT))
|
|
|
|
from web_interface.system_metrics import collect_system_metrics # noqa: E402
|
|
|
|
|
|
@pytest.fixture
|
|
def metrics(monkeypatch):
|
|
"""collect_system_metrics() with psutil's readings under our control."""
|
|
def _collect(disk_percent=12.0, available_bytes=512 * 1024 * 1024,
|
|
used_percent=67.7, raise_disk=False):
|
|
import psutil
|
|
if raise_disk:
|
|
monkeypatch.setattr(psutil, "disk_usage",
|
|
lambda _p: (_ for _ in ()).throw(OSError("no such mount")))
|
|
else:
|
|
monkeypatch.setattr(psutil, "disk_usage",
|
|
lambda _p: SimpleNamespace(percent=disk_percent))
|
|
monkeypatch.setattr(psutil, "virtual_memory",
|
|
lambda: SimpleNamespace(percent=used_percent,
|
|
available=available_bytes))
|
|
return collect_system_metrics()
|
|
return _collect
|
|
|
|
|
|
class TestDiskUsageIsReal:
|
|
def test_reported_value_comes_from_the_filesystem(self, metrics):
|
|
# The regression: this field was the literal 0.
|
|
assert metrics(disk_percent=91.4)["disk_used_percent"] == 91.4
|
|
|
|
def test_a_full_disk_is_not_reported_as_zero(self, metrics):
|
|
assert metrics(disk_percent=99.9)["disk_used_percent"] != 0
|
|
|
|
def test_unreadable_disk_is_null_not_a_confident_zero(self, metrics):
|
|
# null renders as '--'; 0 would read as "plenty of room".
|
|
assert metrics(raise_disk=True)["disk_used_percent"] is None
|
|
|
|
|
|
class TestMemAvailableIsReported:
|
|
def test_mem_available_is_present_and_in_mb(self, metrics):
|
|
got = metrics(available_bytes=292 * 1024 * 1024)["memory_available_mb"]
|
|
assert got == pytest.approx(292.0, abs=0.1)
|
|
|
|
def test_the_1gb_danger_case_is_visible(self, metrics):
|
|
"""67% used can be fine or nearly out of memory; only MemAvailable says."""
|
|
m = metrics(used_percent=67.7, available_bytes=40 * 1024 * 1024)
|
|
assert m["memory_used_percent"] == 67.7
|
|
assert m["memory_available_mb"] == pytest.approx(40.0, abs=0.1)
|
|
|
|
|
|
class TestEveryPredictiveFieldIsPresent:
|
|
def test_nothing_the_dashboard_needs_is_missing(self, metrics):
|
|
m = metrics()
|
|
for field in ("cpu_percent", "cpu_temp", "memory_used_percent",
|
|
"memory_available_mb", "disk_used_percent"):
|
|
assert field in m, f"{field} missing from collected metrics"
|
|
|
|
def test_absent_psutil_still_yields_every_key(self, monkeypatch):
|
|
"""The no-psutil fallback must not drop fields the UI reads."""
|
|
import builtins
|
|
real_import = builtins.__import__
|
|
|
|
def _no_psutil(name, *args, **kwargs):
|
|
if name == "psutil":
|
|
raise ImportError("psutil missing")
|
|
return real_import(name, *args, **kwargs)
|
|
|
|
monkeypatch.setattr(builtins, "__import__", _no_psutil)
|
|
m = collect_system_metrics()
|
|
for field in ("cpu_percent", "cpu_temp", "memory_used_percent",
|
|
"memory_available_mb", "disk_used_percent"):
|
|
assert field in m
|
|
assert m["disk_used_percent"] is None
|