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>
72 lines
2.5 KiB
Python
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))
|