Files
mission-control-v2/backend/tests/test_prognose.py
T
HitonabiandClaude Opus 5.5 f7a9b8ad50 Welle 1 · Vorhersage „voll in etwa N Tagen“: Pool, NAS, Systemplatte, Container, VMs und die Platte der KI-Box
Aus dem Verlauf der Woche (Tagesmittel, Schutz gegen einmalige Sprünge, schneller werdendes Volllaufen zählt sofort):
gelb unter 14, rot unter 3 Tagen. Der Wächter meldet es, auch wenn der Füllstand selbst noch unauffällig ist;
Speicherkarte, Geräte-Tabelle, Instrumente der Übersicht und die KI-Box-Seite zeigen es an. Jeder Bericht des
Ausführers schreibt dafür die Belegungen in die Messreihe „speicher“.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 16:21:38 +02:00

163 lines
9.7 KiB
Python

"""Füllstands-Vorhersage „voll in etwa N Tagen“ (kern/prognose.py, Ausbauplan Welle 1, 25.09.2026): die Rechnung auf
künstlichen Verläufen (stetig, ein Sprung, plötzlich schneller, zu wenig Verlauf) und wie Speicherkarte und Wächter sie
nutzen."""
from types import SimpleNamespace
import pytest
from kern import einstellungen as einstellungen_mod
from kern import messreihen, prognose
JETZT = 1_790_340_000.0 # 25.09.2026 ~14:40
TAG = 86400
def _reihe(wert_bei, tage: float = 7, alter_ab: float = 0) -> list[list]:
"""30-min-Raster wie messreihen.lesen("7d"): [[t, wert], …], ältester zuerst; wert_bei(alter in Tagen)."""
schritte = int(tage * TAG // 1800)
return [[JETZT - alter_ab - i * 1800, wert_bei((alter_ab + i * 1800) / TAG)] for i in reversed(range(schritte))]
def test_stetiges_wachstum():
v = prognose.vorhersage(_reihe(lambda alter: 57 - alter), JETZT) # 1 % am Tag, jetzt 57 %
assert v == {"tage": 43.0, "rate": 1.0, "anteil": 57.0, "grund": None}
assert prognose.stufe(v) == "ok"
assert prognose.vorhersage(_reihe(lambda alter: 57 - alter), JETZT, voll_bei=95)["tage"] == 38.0
def test_ein_einmaliger_sprung_haelt_keinen_alarm():
"""Ein großer Download vor zweieinhalb Tagen, seitdem nichts: Die Woche steigt, der letzte Tag nicht."""
v = prognose.vorhersage(_reihe(lambda alter: 50 if alter < 2.5 else 40), JETZT)
assert (v["grund"], v["tage"], v["rate"]) == ("gleich", None, 0.0)
def test_ploetzlich_schneller_wartet_nicht_eine_woche():
"""Fünf Tage gleich, dann 5 % am Tag: Die Woche allein sähe rund 1 % am Tag (voll in 30 Tagen)."""
v = prognose.vorhersage(_reihe(lambda alter: 60 + 5 * max(0.0, 2 - alter)), JETZT)
assert v["rate"] == pytest.approx(3.75, abs=0.05) and v["anteil"] == 70.0 and v["tage"] == pytest.approx(8, abs=0.2)
assert prognose.stufe(v) == "gelb"
def test_freigeraeumt_in_der_woche_zaehlt_die_woche():
"""Sicherungsziel: jeden Tag +1 %, vor dreieinhalb Tagen hat das Aufräumen 6 % freigegeben — netto kaum Zuwachs.
Die letzten beiden Tage allein (je +1 %) sollen nicht „schneller“ heißen."""
v = prognose.vorhersage(_reihe(lambda alter: 80 - alter - (0 if alter < 3.5 else -6)), JETZT)
assert v["rate"] < 1.0
def test_ohne_genug_verlauf_keine_vorhersage():
assert prognose.vorhersage([], JETZT)["grund"] == "verlauf"
kurz = prognose.vorhersage(_reihe(lambda alter: 50 - alter, tage=1.5), JETZT)
assert (kurz["grund"], kurz["anteil"]) == ("verlauf", 50.0)
alt = prognose.vorhersage(_reihe(lambda alter: 50 - alter, alter_ab=3 * 3600), JETZT) # seit 3 h nichts
assert (alt["grund"], alt["anteil"]) == ("verlauf", None)
luecken = [[t, w if i % 5 == 0 else None] for i, (t, w) in enumerate(_reihe(lambda alter: 50 - alter))]
assert prognose.vorhersage(luecken, JETZT)["grund"] == "verlauf" # je Tag keine sechs Stunden mit Messungen
def test_texte_und_stufen():
assert [prognose.in_tagen(t) for t in (0.4, 1.2, 9.4, 35, 200, 400)] == [
"in weniger als einem Tag", "in etwa einem Tag", "in etwa 9 Tagen", "in etwa 5 Wochen", "in etwa 7 Monaten",
"frühestens in einem Jahr"]
v = {"tage": 2.5, "rate": 1.25, "anteil": 91.9, "grund": None}
assert prognose.tempo(v) == "Die Belegung wächst um etwa 1,2 % am Tag (jetzt 92 %)."
assert prognose.schlimmer("gelb", v) == ("rot", True) and prognose.schlimmer("rot", v) == ("rot", False)
assert prognose.schlimmer(None, {"tage": None}) == ("ok", False)
assert prognose.dazu({"tage": 30}) == " Bei diesem Tempo voll in etwa 4 Wochen." and prognose.dazu(None) == ""
assert prognose.dazu({"tage": 80}) == ""
# --- Mit echten Dateien: Verlauf aus den Berichten -------------------------------------------------------------
@pytest.fixture
def daten(tmp_path, monkeypatch):
monkeypatch.setenv("MC_DATEN_DIR", str(tmp_path))
einstellungen_mod.einstellungen.cache_clear()
yield tmp_path
einstellungen_mod.einstellungen.cache_clear()
def _bericht(pool: float, vm: float) -> dict:
return {"host": {"pools": [{"speicher": "local-lvm", "belegt": int(pool * 1000), "gesamt": 1000, "meta_anteil": 0.01}],
"netzlaufwerke": [{"ziel": "/mnt/qnap-backup", "platz": {"belegt": 400, "gesamt": 1000}},
{"ziel": "/mnt/weg", "zustand": "haengt", "platz": None}],
"sicherungsziele": {"pbs-qnap": {"belegt": 400, "gesamt": 1000}}},
"gaeste": [{"vmid": 106, "art": "qemu", "platte": {"belegt": int(vm * 1000), "gesamt": 1000}},
{"vmid": 102, "art": "lxc", "platte": {"belegt": 1, "gesamt": 4}}]}
def test_berichte_schreiben_den_verlauf_und_die_vorhersage_liest_ihn(daten):
from services.homelab import speicher
for stunde in range(3 * 24, -1, -1): # drei Tage lang stündlich ein Bericht
tage = stunde / 24
speicher.verlauf_merken(_bericht(pool=0.70 - 0.02 * tage, vm=0.5), JETZT - stunde * 3600)
letzte = messreihen.letzter_punkt(speicher.VERLAUF, JETZT)
assert {k: v for k, v in letzte.items() if k != "t"} == {
"pool:local-lvm": 70.0, "pool-meta:local-lvm": 1.0, "nas:/mnt/qnap-backup": 40.0, "ziel:pbs-qnap": 40.0,
"vm-106": 50.0}
v, meta = speicher.pool_vorhersage("local-lvm", JETZT)
assert meta is False and v["rate"] == pytest.approx(2.0, abs=0.05) and v["tage"] == pytest.approx(15, abs=0.5)
assert speicher.platten_vorhersage("vm-106", JETZT)["grund"] == "gleich"
assert speicher.platten_vorhersage("ct-102", JETZT)["grund"] == "verlauf" # Minutenwerte fehlen hier
assert speicher.platten_reihe("pve") == ("pve", "platte", 95.0) and speicher.platten_reihe("nas") is None
speicher.verlauf_merken({"host": "kaputt"}, JETZT) # kostet nur diesen Punkt
def test_speicherkarte_und_waechter_mit_vorhersage(monkeypatch):
from services.homelab import speicher
from test_homelab_speicher import _bericht as echter_bericht
monkeypatch.setattr(speicher, "antwortet", lambda s, p: True)
monkeypatch.setattr(speicher, "_bericht", lambda: echter_bericht())
bald = {"tage": 5.2, "rate": 2.0, "anteil": 60.0, "grund": None}
spaeter = {"tage": 30.0, "rate": 0.3, "anteil": 91.0, "grund": None}
kurven = {"pool:local-lvm": bald, "nas:/mnt/qnap-backup": spaeter, "ziel:pbs-qnap": spaeter}
monkeypatch.setattr(speicher, "_vorhersage", lambda name, jetzt: kurven.get(name, {"tage": None, "grund": "verlauf"}))
monkeypatch.setattr(speicher, "platten_vorhersage", lambda gid, jetzt=None: {"tage": 2.0, "rate": 5.0, "anteil": 85.0})
lage = speicher.lage()
assert lage["pools"][0]["vorhersage"] == bald and lage["nas"][0]["vorhersage"] == spaeter
assert lage["systemplatte"] is None and lage["sicherungen"]["ziele"][0]["vorhersage"] == spaeter
befunde = {b.id: b for b in speicher.pruefe_speicher(1_790_330_000.0)}
pool = befunde["pool:local-lvm"]
assert (pool.stufe, pool.titel) == ("gelb", "Speicherpool local-lvm in etwa 5 Tagen voll")
assert pool.text.startswith("Die Belegung wächst um etwa 2,0 % am Tag (jetzt 60 %). Noch ")
assert "nas:/mnt/qnap-backup" not in befunde # 39 % und 30 Tage: nichts zu melden
# Der Füllstand löst aus, die Vorhersage steht dabei; die Systemplatte läuft in zwei Tagen voll (rot).
b = echter_bericht(nas_anteil=0.91)
b["host"]["systemplatte"] = {"belegt": 40, "gesamt": 100}
monkeypatch.setattr(speicher, "_bericht", lambda: b)
befunde = {x.id: x for x in speicher.pruefe_speicher(1_790_330_000.0)}
nas = befunde["nas:/mnt/qnap-backup"]
assert (nas.stufe, nas.titel) == ("rot", "NAS (192.168.178.62) zu 91 % voll")
assert "frei. Bei diesem Tempo voll in etwa 4 Wochen. Die Sicherungen" in nas.text
host = befunde["host-platte"]
assert (host.stufe, host.titel) == ("rot", "Systemplatte des Proxmox-Hosts in etwa 2 Tagen voll")
def test_waechter_gastplatten_und_box_platte(monkeypatch):
from services import waechter
from services.homelab import kanal, speicher
bericht = {"gaeste": [{"vmid": 101, "art": "lxc", "name": "npmplus", "platte": {"belegt": 1, "gesamt": 4}},
{"vmid": 106, "art": "qemu", "name": "arcane", "platte": {"belegt": 90, "gesamt": 100}}]}
monkeypatch.setattr(kanal, "bericht", lambda: {"bericht": bericht})
kurven = {"ct-101": {"tage": 1.2, "rate": 60.0, "anteil": 25.0}, "vm-106": {"tage": 40.0, "rate": 0.1, "anteil": 90.0}}
monkeypatch.setattr(speicher, "platten_vorhersage", lambda gid, jetzt=None: kurven[gid])
befunde = {b.id: b for b in waechter.pruefe_gast_platten()}
ct = befunde["gast-platte:101"]
assert (ct.stufe, ct.quelle) == ("rot", "ct-101") and ct.titel.endswith(" in etwa einem Tag voll")
assert ct.text.startswith("Die Belegung wächst um etwa 60,0 % am Tag (jetzt 25 %). 0,0 von 0,0 GB belegt")
vm = befunde["gast-platte:106"]
assert vm.stufe == "rot" and vm.titel.endswith(" zu 90 % voll") and "(VM 106). Bei diesem Tempo voll in etwa 6 Wochen. Platz" in vm.text
# KI-Box: Füllstand unauffällig, aber in 10 Tagen voll → eigener gelber Hinweis.
monkeypatch.setattr(waechter, "ROLLE", "box")
monkeypatch.setattr(waechter.psutil, "disk_usage", lambda pfad: SimpleNamespace(percent=62.0))
monkeypatch.setattr(waechter.prognose, "aus_messreihe",
lambda quelle, name, voll_bei=100.0, jetzt=None: {"tage": 10.4, "rate": 3.6, "anteil": 62.0})
(tempo,) = waechter.pruefe_platte()
assert (tempo.id, tempo.stufe, tempo.titel) == ("platte:tempo", "gelb", "Die Platte ist in etwa 10 Tagen voll")
monkeypatch.setattr(waechter.psutil, "disk_usage", lambda pfad: SimpleNamespace(percent=85.0))
assert [b.id for b in waechter.pruefe_platte()] == ["platte"] # gelb ist gelb: kein zweiter Hinweis