Merge branch 'worktree-agent-a872d2361a1cdaa60' into wartung/helfer-nutzung-dienste
This commit is contained in:
@@ -0,0 +1,295 @@
|
||||
"""Tests für die Dienste-Liste mit Kennzahlen (services/dienste.py) — mit echter `systemctl show`-Ausgabe der Box
|
||||
vom 25.09.2026 (/proc/uptime stand bei 80147,36 s)."""
|
||||
|
||||
import pytest
|
||||
from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
from services import dienste
|
||||
|
||||
JETZT = 80147.36
|
||||
|
||||
# systemctl --user show mc2-gateway mission-control-2 mc2-steward hermes-gateway hermes-builtin-ui voice-service
|
||||
# lucy-stimme -p Id,LoadState,ActiveState,UnitFileState,MemoryCurrent,CPUUsageNSec,ActiveEnterTimestampMonotonic,
|
||||
# InactiveEnterTimestampMonotonic,NRestarts
|
||||
USER_AUSGABE = """\
|
||||
Id=mc2-gateway.service
|
||||
LoadState=loaded
|
||||
ActiveState=active
|
||||
UnitFileState=enabled
|
||||
ActiveEnterTimestampMonotonic=79813970006
|
||||
InactiveEnterTimestampMonotonic=79813968344
|
||||
NRestarts=0
|
||||
MemoryCurrent=45731840
|
||||
CPUUsageNSec=1568208000
|
||||
|
||||
Id=mission-control-2.service
|
||||
LoadState=loaded
|
||||
ActiveState=active
|
||||
UnitFileState=enabled
|
||||
ActiveEnterTimestampMonotonic=79816961421
|
||||
InactiveEnterTimestampMonotonic=79816959875
|
||||
NRestarts=0
|
||||
MemoryCurrent=198352896
|
||||
CPUUsageNSec=5660312000
|
||||
|
||||
Id=mc2-steward.service
|
||||
LoadState=loaded
|
||||
ActiveState=active
|
||||
UnitFileState=enabled
|
||||
ActiveEnterTimestampMonotonic=79813795101
|
||||
InactiveEnterTimestampMonotonic=79813794078
|
||||
NRestarts=0
|
||||
MemoryCurrent=56500224
|
||||
CPUUsageNSec=1002999000
|
||||
|
||||
Id=hermes-gateway.service
|
||||
LoadState=loaded
|
||||
ActiveState=active
|
||||
UnitFileState=enabled
|
||||
ActiveEnterTimestampMonotonic=76113569939
|
||||
InactiveEnterTimestampMonotonic=76113568536
|
||||
NRestarts=0
|
||||
MemoryCurrent=421068800
|
||||
CPUUsageNSec=45597050000
|
||||
|
||||
Id=hermes-builtin-ui.service
|
||||
LoadState=loaded
|
||||
ActiveState=active
|
||||
UnitFileState=enabled
|
||||
ActiveEnterTimestampMonotonic=76110768028
|
||||
InactiveEnterTimestampMonotonic=76110766393
|
||||
NRestarts=0
|
||||
MemoryCurrent=162058240
|
||||
CPUUsageNSec=20446657000
|
||||
|
||||
Id=voice-service.service
|
||||
LoadState=loaded
|
||||
ActiveState=inactive
|
||||
UnitFileState=disabled
|
||||
ActiveEnterTimestampMonotonic=0
|
||||
InactiveEnterTimestampMonotonic=0
|
||||
NRestarts=0
|
||||
MemoryCurrent=[not set]
|
||||
CPUUsageNSec=[not set]
|
||||
|
||||
Id=lucy-stimme.service
|
||||
LoadState=loaded
|
||||
ActiveState=active
|
||||
UnitFileState=enabled
|
||||
ActiveEnterTimestampMonotonic=10066410
|
||||
InactiveEnterTimestampMonotonic=0
|
||||
NRestarts=0
|
||||
MemoryCurrent=7500505088
|
||||
CPUUsageNSec=502624087000
|
||||
"""
|
||||
|
||||
# systemctl show llama-swap -p … (System-Dienst)
|
||||
SYSTEM_AUSGABE = """\
|
||||
Id=llama-swap.service
|
||||
LoadState=loaded
|
||||
ActiveState=active
|
||||
UnitFileState=enabled
|
||||
ActiveEnterTimestampMonotonic=74032986745
|
||||
InactiveEnterTimestampMonotonic=74032938183
|
||||
NRestarts=0
|
||||
MemoryCurrent=3722850304
|
||||
CPUUsageNSec=31617381000
|
||||
"""
|
||||
|
||||
USER_UNITS = ["mc2-gateway", "mission-control-2", "mc2-steward", "hermes-gateway", "hermes-builtin-ui",
|
||||
"voice-service", "lucy-stimme"]
|
||||
|
||||
# Ein abgestürzter Dienst, wie systemd ihn nach Restart=always mit zu vielen Fehlversuchen stehen lässt.
|
||||
GESCHEITERT = {
|
||||
"Id": "hermes-gateway.service", "LoadState": "loaded", "ActiveState": "failed", "UnitFileState": "enabled",
|
||||
"ActiveEnterTimestampMonotonic": "76113569939", "InactiveEnterTimestampMonotonic": "80000000000",
|
||||
"NRestarts": "5", "MemoryCurrent": "[not set]", "CPUUsageNSec": "[not set]",
|
||||
}
|
||||
|
||||
|
||||
# --- systemctl show lesen ------------------------------------------------------------------------------------------
|
||||
|
||||
def test_liest_die_bloecke_der_echten_ausgabe_in_der_reihenfolge_der_units():
|
||||
stand = dienste.zuordnen(dienste.lies_show(USER_AUSGABE), USER_UNITS)
|
||||
assert list(stand) == USER_UNITS
|
||||
assert stand["lucy-stimme"]["MemoryCurrent"] == "7500505088"
|
||||
assert stand["voice-service"]["UnitFileState"] == "disabled"
|
||||
|
||||
|
||||
def test_ordnet_nach_id_zu_wenn_ein_block_fehlt():
|
||||
bloecke = dienste.lies_show(SYSTEM_AUSGABE)
|
||||
assert dienste.zuordnen(bloecke, ["llama-swap", "gibt-es-nicht"]) == {"llama-swap": bloecke[0]}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("wert", "erwartet"), [
|
||||
("198352896", 198352896), ("0", 0), ("[not set]", None), ("infinity", None), ("", None), (None, None),
|
||||
("18446744073709551615", None), # altes systemd: UINT64_MAX heißt „nicht gezählt“
|
||||
])
|
||||
def test_zahl(wert, erwartet):
|
||||
assert dienste.zahl(wert) == erwartet
|
||||
|
||||
|
||||
# --- Auswertung ----------------------------------------------------------------------------------------------------
|
||||
|
||||
def test_kennzahlen_eines_laufenden_dienstes():
|
||||
mc2 = dienste.zuordnen(dienste.lies_show(USER_AUSGABE), USER_UNITS)["mission-control-2"]
|
||||
assert dienste.kennzahlen(mc2, JETZT) == {
|
||||
"speicher_bytes": 198352896, "laeuft_seit_s": 330, "aus_seit_s": None, "neustarts": 0,
|
||||
}
|
||||
|
||||
|
||||
def test_kennzahlen_eines_schlafenden_und_eines_gescheiterten_dienstes():
|
||||
voice = dienste.zuordnen(dienste.lies_show(USER_AUSGABE), USER_UNITS)["voice-service"]
|
||||
assert dienste.kennzahlen(voice, JETZT) == {
|
||||
"speicher_bytes": None, "laeuft_seit_s": None, "aus_seit_s": None, "neustarts": 0,
|
||||
}
|
||||
k = dienste.kennzahlen(GESCHEITERT, JETZT)
|
||||
assert k["laeuft_seit_s"] is None and k["aus_seit_s"] == 147 and k["neustarts"] == 5
|
||||
|
||||
|
||||
def test_zustand_in_worten():
|
||||
laeuft = {"LoadState": "loaded", "ActiveState": "active"}
|
||||
assert dienste.zustand(laeuft, ok=True, schlaeft=False) == "laeuft"
|
||||
assert dienste.zustand(laeuft, ok=False, schlaeft=False) == "antwortet_nicht"
|
||||
assert dienste.zustand(GESCHEITERT, ok=False, schlaeft=False) == "gescheitert"
|
||||
assert dienste.zustand({**laeuft, "ActiveState": "activating"}, ok=False, schlaeft=False) == "startet"
|
||||
assert dienste.zustand({**laeuft, "ActiveState": "deactivating"}, ok=False, schlaeft=False) == "stoppt"
|
||||
assert dienste.zustand({**laeuft, "ActiveState": "inactive"}, ok=False, schlaeft=False) == "gestoppt"
|
||||
assert dienste.zustand({**laeuft, "ActiveState": "inactive"}, ok=False, schlaeft=True) == "schlaeft"
|
||||
|
||||
|
||||
def test_zustand_ohne_systemd_nur_nach_erreichbarkeit():
|
||||
"""Windows (Entwicklung) oder eine Unit, die systemd nicht kennt: wie vor dem 25.09. nur „antwortet (nicht)“."""
|
||||
assert dienste.zustand({}, ok=True, schlaeft=False) == "laeuft"
|
||||
assert dienste.zustand({}, ok=False, schlaeft=False) == "antwortet_nicht"
|
||||
assert dienste.zustand({"LoadState": "not-found", "ActiveState": "inactive"}, ok=True, schlaeft=False) == "laeuft"
|
||||
|
||||
|
||||
def test_cpu_anteil_bezogen_auf_alle_kerne():
|
||||
assert dienste.cpu_anteil((0.0, 0), (10.0, 3_200_000_000), kerne=32) == 1.0 # 0,32 Kerne von 32
|
||||
assert dienste.cpu_anteil((0.0, 0), (1.0, 64_000_000_000), kerne=32) == 100.0 # mehr geht nicht
|
||||
assert dienste.cpu_anteil((5.0, 900), (5.0, 1000), kerne=32) is None # keine Zeit vergangen
|
||||
assert dienste.cpu_anteil((0.0, 5_000), (10.0, 1_000), kerne=32) is None # Zähler neu (Neustart)
|
||||
|
||||
|
||||
def test_cpu_messung_haelt_den_letzten_wert_bei_zu_kurzem_abstand():
|
||||
m = dienste.CpuMessung(kerne=32)
|
||||
m.probe("hermes-gateway", 100.0, 0)
|
||||
assert m.wert("hermes-gateway") is None # erste Probe: noch nichts zu messen
|
||||
m.probe("hermes-gateway", 100.5, 160_000_000)
|
||||
assert m.wert("hermes-gateway") == 1.0 # 0,16 s CPU in 0,5 s auf 32 Kernen
|
||||
m.probe("hermes-gateway", 101.0, 1_000_000_000)
|
||||
assert m.wert("hermes-gateway") == 1.0 # nur 0,5 s später: Wert bleibt, Probe auch
|
||||
m.probe("hermes-gateway", 110.5, 160_000_000 + 32_000_000_000)
|
||||
assert m.wert("hermes-gateway") == 10.0 # 32 s CPU in 10 s = 3,2 Kerne
|
||||
m.probe("hermes-gateway", 120.5, 5_000)
|
||||
assert m.wert("hermes-gateway") is None # neu gestartet: Zähler fing von vorn an
|
||||
m.probe("hermes-gateway", 121.0, 25_000)
|
||||
assert m.wert("hermes-gateway") == 0.0
|
||||
m.probe("hermes-gateway", 121.0 + dienste.CPU_MAX_S + 1, 50_000)
|
||||
assert m.wert("hermes-gateway") is None # zu lange niemand geschaut: neu ansetzen
|
||||
m.probe("hermes-gateway", 400.0, None)
|
||||
assert m.wert("hermes-gateway") is None # gestoppt
|
||||
|
||||
|
||||
# --- Die ganze Liste -----------------------------------------------------------------------------------------------
|
||||
|
||||
@pytest.fixture
|
||||
def box(monkeypatch):
|
||||
"""Die Box vom 25.09.: alle Dienste laufen, die Spracherkennung schläft. Die erste Abfrage liest bei JETZT und —
|
||||
kurze zweite Probe — eine halbe Sekunde später, die nächste zehn Sekunden danach."""
|
||||
uhr = iter([JETZT, JETZT, JETZT + 0.5, JETZT + 0.5, JETZT + 10.5, JETZT + 10.5])
|
||||
monkeypatch.setattr(dienste, "_uhr", lambda: next(uhr))
|
||||
monkeypatch.setattr(dienste, "_cpu", dienste.CpuMessung(kerne=32))
|
||||
monkeypatch.setattr(dienste, "CPU_KURZ_S", 0)
|
||||
monkeypatch.setattr(dienste, "V1_UPSTREAM", "http://127.0.0.1:9010")
|
||||
monkeypatch.setattr(dienste, "agent_status", lambda: {
|
||||
"gateway_reachable": True, "hermes_ui_reachable": True, "hermes_ui_url": "/hermes-ui/"})
|
||||
monkeypatch.setattr(dienste, "engine_reachable", lambda: True)
|
||||
monkeypatch.setattr(dienste, "gateway_reachable", lambda: True)
|
||||
monkeypatch.setattr(dienste, "_voice_antwortet", lambda: False)
|
||||
monkeypatch.setattr(dienste.system, "_gpu_sysfs", lambda: {"busy_percent": 3, "gtt_used": 27611533312})
|
||||
monkeypatch.setattr(dienste, "_speicher_gesamt", lambda: 131738611712)
|
||||
lesungen = {"user": 0, "system": 0}
|
||||
|
||||
def show(units, system_dienst):
|
||||
art = "system" if system_dienst else "user"
|
||||
text = SYSTEM_AUSGABE if system_dienst else USER_AUSGABE
|
||||
stand = dienste.zuordnen(dienste.lies_show(text), units)
|
||||
# Die Engine rechnet 0,32 s CPU je Lesen, alle anderen nichts.
|
||||
if system_dienst:
|
||||
ns = int(stand["llama-swap"]["CPUUsageNSec"]) + lesungen[art] * 320_000_000
|
||||
stand["llama-swap"] = {**stand["llama-swap"], "CPUUsageNSec": str(ns)}
|
||||
lesungen[art] += 1
|
||||
return stand
|
||||
|
||||
monkeypatch.setattr(dienste, "_systemctl_show", show)
|
||||
return lesungen
|
||||
|
||||
|
||||
def test_liste_mit_zustand_und_kennzahlen(box):
|
||||
daten = dienste.liste()
|
||||
zeilen = {z["unit"]: z for z in daten["services"]}
|
||||
assert [z["unit"] for z in daten["services"]] == [
|
||||
"llama-swap", "mc2-gateway", "mission-control-2", "mc2-steward", "hermes-gateway", "hermes-builtin-ui",
|
||||
"voice-service", "lucy-stimme"]
|
||||
assert daten["speicher_gesamt"] == 131738611712 and daten["kerne"] == 32
|
||||
# Was die MCP-Werkzeuge lesen, ist geblieben.
|
||||
assert {"name", "unit", "url", "ok", "scope", "schlaeft"} <= set(zeilen["llama-swap"])
|
||||
assert daten["links"]["engine_ui"].endswith("/ui")
|
||||
|
||||
engine = zeilen["llama-swap"]
|
||||
assert engine["zustand"] == "laeuft" and engine["scope"] == "system"
|
||||
assert engine["speicher_bytes"] == 3722850304 and engine["grafik_bytes"] == 27611533312
|
||||
assert engine["laeuft_seit_s"] == int(JETZT + 0.5 - 74032.986745)
|
||||
assert engine["cpu_prozent"] == 2.0 # erste Abfrage: 0,32 s CPU in der halben Sekunde bis zur zweiten Probe
|
||||
assert zeilen["hermes-gateway"]["cpu_prozent"] == 0.0 and zeilen["hermes-gateway"]["grafik_bytes"] is None
|
||||
|
||||
# Ohne HTTP-Port entscheidet systemd, ob der Dienst läuft.
|
||||
assert zeilen["mc2-steward"]["ok"] is True and zeilen["lucy-stimme"]["speicher_bytes"] == 7500505088
|
||||
|
||||
voice = zeilen["voice-service"]
|
||||
assert voice["schlaeft"] is True and voice["zustand"] == "schlaeft" and voice["ok"] is False
|
||||
assert voice["speicher_bytes"] is None and voice["cpu_prozent"] is None
|
||||
|
||||
|
||||
def test_zweite_abfrage_misst_ueber_den_abstand_seit_der_ersten(box):
|
||||
dienste.liste()
|
||||
assert box == {"user": 2, "system": 2} # erste Abfrage: kurze zweite Probe
|
||||
engine = next(z for z in dienste.liste()["services"] if z["unit"] == "llama-swap")
|
||||
assert box == {"user": 3, "system": 3} # jetzt ohne zweite Probe
|
||||
assert engine["cpu_prozent"] == 0.1 # 0,32 s CPU in 10 s auf 32 Kernen
|
||||
|
||||
|
||||
def test_liste_ohne_systemd_wie_bisher(box, monkeypatch):
|
||||
"""Am PC gibt es kein systemd: die Liste steht, der Zustand kommt allein aus der Erreichbarkeit."""
|
||||
monkeypatch.setattr(dienste, "_systemctl_show", lambda units, system_dienst: {})
|
||||
zeilen = {z["unit"]: z for z in dienste.liste()["services"]}
|
||||
assert zeilen["llama-swap"]["zustand"] == "laeuft" and zeilen["llama-swap"]["grafik_bytes"] is None
|
||||
assert zeilen["voice-service"]["zustand"] == "antwortet_nicht" and zeilen["voice-service"]["schlaeft"] is False
|
||||
assert zeilen["mc2-steward"]["ok"] is False
|
||||
assert all(z["speicher_bytes"] is None and z["cpu_prozent"] is None for z in zeilen.values())
|
||||
|
||||
|
||||
def test_gescheiterter_dienst(box, monkeypatch):
|
||||
def show(units, system_dienst):
|
||||
stand = dienste.zuordnen(dienste.lies_show(SYSTEM_AUSGABE if system_dienst else USER_AUSGABE), units)
|
||||
if not system_dienst:
|
||||
stand["hermes-gateway"] = GESCHEITERT
|
||||
return stand
|
||||
|
||||
monkeypatch.setattr(dienste, "_systemctl_show", show)
|
||||
monkeypatch.setattr(dienste, "agent_status", lambda: {
|
||||
"gateway_reachable": False, "hermes_ui_reachable": True, "hermes_ui_url": "/hermes-ui/"})
|
||||
hermes = next(z for z in dienste.liste()["services"] if z["unit"] == "hermes-gateway")
|
||||
assert hermes["zustand"] == "gescheitert" and hermes["schlaeft"] is False
|
||||
assert hermes["neustarts"] == 5 and hermes["aus_seit_s"] is not None and hermes["cpu_prozent"] is None
|
||||
|
||||
|
||||
def test_schnittstelle_reicht_die_liste_durch(monkeypatch):
|
||||
from routers import system as system_router
|
||||
|
||||
monkeypatch.setattr(dienste, "liste", lambda: {"services": [{"unit": "llama-swap", "zustand": "laeuft"}]})
|
||||
app = FastAPI()
|
||||
app.include_router(system_router.router)
|
||||
assert TestClient(app).get("/api/system/services").json()["services"][0]["zustand"] == "laeuft"
|
||||
Reference in New Issue
Block a user