Files
bin/finance/tests/test_grafana_dashboard.py
T
rdmsandClaude Opus 5.5 42b410446d feat: Grafana-Panel Planung nach Kategorie inkl. Kreditraten (A11-T3)
Neue Tabelle planning_monthly (Monatssummen je Kategorie aus wiederkehrenden
Posten, Einmalposten und allen Kreditraten), Service planning_chart mit
refresh_planning_chart (Advisory-Lock, komplette Neuschreibung), GUI-Hooks
in index/planung_page ueber _safe_refresh, neues Barchart-Panel im
Grafana-Dashboard (Panel 5).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 19:15:26 +02:00

72 lines
2.5 KiB
Python

"""Regressionstests fuer das provisionierte Grafana-Dashboard finanzen.json.
Hintergrund: Kein Panel nutzt $__timeFilter, jedes liefert alle Zeilen und
Grafana beschneidet auf das Dashboard-Zeitfenster. Das Szenario-Vergleich-Panel
zeichnet die Projektion (Zukunft, horizon_days ab heute). Steht 'to' auf 'now',
wird die gesamte Kurve ab morgen abgeschnitten (nur die ~4 Tage <= heute
sichtbar, Y-Achse auf deren schmalen Bereich fixiert). Dieser Test haelt fest,
dass 'to' weit genug in die Zukunft reicht.
"""
import json
import re
from pathlib import Path
from app.config import get_settings
DASHBOARD = Path(__file__).resolve().parent.parent / "grafana" / "dashboards" / "finanzen.json"
_UNIT_DAYS = {"y": 365, "M": 30, "w": 7, "d": 1, "h": 1 / 24}
def _relative_to_days(expr: str) -> float:
"""Grafana-Relativausdruck ('now', 'now+19M', 'now-1y') -> vorzeichen-
behafteter Tages-Offset relativ zu 'now' (Zukunft positiv)."""
expr = expr.strip()
if expr == "now":
return 0.0
m = re.fullmatch(r"now([+-])(\d+)([yMwdh])", expr)
if not m:
raise ValueError(f"Unerwarteter Grafana-Zeitausdruck: {expr!r}")
sign = 1 if m.group(1) == "+" else -1
return sign * int(m.group(2)) * _UNIT_DAYS[m.group(3)]
def _load() -> dict:
return json.loads(DASHBOARD.read_text())
def test_dashboard_has_szenario_panel():
titles = [p.get("title") for p in _load().get("panels", [])]
assert "Szenario-Vergleich" in titles
def test_time_window_covers_projection_horizon():
d = _load()
horizon = get_settings().horizon_days
to_days = _relative_to_days(d["time"]["to"])
assert to_days >= horizon, (
f"time.to reicht nur {to_days} Tage in die Zukunft, deckt aber den "
f"Projektionshorizont von {horizon} Tagen nicht ab -> Projektionskurve "
f"wird abgeschnitten"
)
def test_panel_planung_nach_kategorie():
panels = {p["title"]: p for p in _load()["panels"]}
p = panels["Planung nach Kategorie"]
assert p["type"] == "barchart"
assert "planning_monthly" in p["targets"][0]["rawSql"]
assert p["options"]["stacking"] == "normal"
def test_panels_ueberlappen_nicht_und_ids_eindeutig():
panels = _load()["panels"]
assert len({p["id"] for p in panels}) == len(panels)
cells = set()
for p in panels:
g = p["gridPos"]
for x in range(g["x"], g["x"] + g["w"]):
for y in range(g["y"], g["y"] + g["h"]):
assert (x, y) not in cells, p["title"]
cells.add((x, y))