"""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"