Files
mission-control-v2/backend/tests/test_ausreisser.py
T
HitonabiandClaude Opus 5.5 2972cdfeca Später · 22 Ausreißer ohne feste Schwellen: Normalwert je Wochenstunde
- kern/ausreisser.py: 168 Fächer je Reihe (CPU, GPU, Speicher, Temperaturen, Netz), erst genau, ab acht Werten
  gleitend; auffällig ab 4 Streuungen und Mindestabstand, erst mit drei Wochen Erfahrung; Ausreißer gekappt gelernt
- services/ausreisser.py: Wächter beider Rollen, gelber Hinweis nach zwei auffälligen Stunden mit Normalwert und
  Wochenstunde im Satz; erster Lauf lernt aus den aufbewahrten acht Tagen
- Monitoring: Zeile mit Lernwoche bzw. was gerade auffällt; GET /api/ausreisser, /api/homelab/ausreisser

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 10:04:30 +02:00

90 lines
4.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Ausreißer ohne feste Schwellen (Ausbauplan „Später“, Punkt 22, 26.09.2026): Normalwert je Wochenstunde, Meldung erst
nach zwei auffälligen Stunden und erst mit drei Wochen Erfahrung; ein Ausreißer zieht den Normalwert nicht mit."""
import json
import math
from datetime import datetime
import pytest
from kern import ausreisser as ka
from kern import einstellungen as einstellungen_mod
from kern import messreihen
from kern.zeit import LOCAL_TZ
from services import ausreisser
DIENSTAG_18 = datetime(2026, 9, 29, 18, 0, tzinfo=LOCAL_TZ).timestamp()
@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 test_faecher():
assert ka.fach(datetime(2026, 9, 28, 0, 30, tzinfo=LOCAL_TZ).timestamp()) == 0 # Montag 0 Uhr
assert ka.fach(DIENSTAG_18) == 42
assert ka.fach_text(42) == "dienstags gegen 18 Uhr" and ka.fach_text(167) == "sonntags gegen 23 Uhr"
def test_lernen_genau_dann_gleitend():
stat = None
for x in (2.0, 4.0, 6.0):
stat = ka.lernen(stat, x)
assert stat[0] == 3 and stat[1] == pytest.approx(4.0) and stat[2] == pytest.approx(8 / 3)
for _ in range(20):
stat = ka.lernen(stat, 10.0)
assert stat[0] == 23 and 9.0 < stat[1] < 10.0 # folgt der dauerhaft höheren Nutzung
def test_auffaellig_braucht_erfahrung_und_abstand():
stat = [3, 3.0, 1.0]
assert ka.auffaellig(stat, 35.0, "cpu")
assert not ka.auffaellig(stat, 15.0, "cpu") # unter 15 Prozentpunkten Abstand
assert not ka.auffaellig([2, 3.0, 1.0], 35.0, "cpu") # erst zwei Wochen Erfahrung
assert not ka.auffaellig([5, 30.0, 100.0], 55.0, "cpu") # 25 Punkte, aber die Stunde schwankt stark (4 × 10)
assert not ka.auffaellig(stat, 0.0, "cpu") # weniger Last ist kein Problem
def _minuten(quelle: str, von: float, stunden: int, **werte: float) -> None:
for m in range(stunden * 60):
messreihen.schreiben(quelle, werte, t=von + m * 60)
def test_zwei_auffaellige_stunden_ergeben_einen_hinweis(daten, monkeypatch):
# Drei Wochen Erfahrung für dienstags 18 und 19 Uhr: Gitea braucht dann etwa 3 % CPU.
(daten / "mc2-normalwerte.json").write_text(json.dumps({"stunde": DIENSTAG_18 - 3600, "reihen": {
"ct-104/cpu": {"faecher": {"42": [3, 3.0, 1.0], "43": [3, 3.0, 1.0]}, "folge": 0}}}), encoding="utf-8")
monkeypatch.setattr(ausreisser, "geraetename", lambda q: "Gitea")
monkeypatch.setattr(ausreisser, "quellen", lambda jetzt: ["ct-104"])
_minuten("ct-104", DIENSTAG_18, 1, cpu=35.0, ram=40.0)
assert ausreisser.pruefe_ausreisser(DIENSTAG_18 + 3600 + 30) == [] # eine Stunde reicht nicht
_minuten("ct-104", DIENSTAG_18 + 3600, 1, cpu=36.0, ram=40.0)
(b,) = ausreisser.pruefe_ausreisser(DIENSTAG_18 + 7200 + 30)
assert (b.id, b.stufe, b.quelle) == ("ausreisser:ct-104:cpu", "gelb", "ct-104")
assert b.titel == "Gitea braucht seit 2 Stunden ungewöhnlich viel CPU"
assert b.text.startswith("36 % statt sonst etwa 3 % dienstags gegen 19 Uhr.")
# Der Ausreißer wird gekappt gelernt: Das Fach bleibt nahe 3 %.
fach = ka.lesen()["reihen"]["ct-104/cpu"]["faecher"]["43"]
assert fach[0] == 4 and fach[1] < 3 + 3 * math.sqrt(1.0) + 0.01
# Normale Stunde danach: vorbei.
_minuten("ct-104", DIENSTAG_18 + 7200, 1, cpu=3.0, ram=40.0)
assert ausreisser.pruefe_ausreisser(DIENSTAG_18 + 3 * 3600 + 30) == []
def test_erster_lauf_lernt_aus_den_aufbewahrten_tagen(daten, monkeypatch):
monkeypatch.setattr(ausreisser, "quellen", lambda jetzt: ["box"])
_minuten("box", DIENSTAG_18 - 2 * 3600, 2, cpu=5.0, temp_cpu=40.0)
assert ausreisser.pruefe_ausreisser(DIENSTAG_18 + 30) == []
stand = ausreisser.lage()
assert stand["reihen"] == 2 and stand["wochen"] == 1 and stand["lernt"] and stand["min_wochen"] == 3
assert ka.lesen()["stunde"] == DIENSTAG_18 - 3600
# Ohne neue Stunde rechnet er nicht noch einmal.
assert ausreisser.pruefe_ausreisser(DIENSTAG_18 + 600) == []
def test_zahlen_in_worten():
assert ka.zahl_text(2_450_000, "B/s") == "2,5 MB/s" and ka.zahl_text(71.6, "°C") == "72 °C"