Files
bin/finance/tests/test_grafana_dashboard.py
T
rdmsandClaude Opus 5.5 d2b21c8f8b fix: Balkendiagramme ohne Segment-Beschriftung, Tooltip mit allen Kategorien, Monat ohne Uhrzeit
In gestapelten Balken mit vielen Kategorien überlagerten sich die
Segment-Beschriftungen, sichtbar blieb ein zufälliger kleiner Wert.
Betrifft Monat nach Kategorie und Planung nach Kategorie.

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

91 lines
3.4 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))
BARCHARTS = ("Monat nach Kategorie", "Planung nach Kategorie")
def test_barcharts_ohne_segment_beschriftung_mit_voller_aufschluesselung():
"""Gestapelte Balken mit vielen Kategorien: Segment-Beschriftungen
überlagern sich (sichtbar bleibt ein zufälliger kleiner Wert). Daher keine
Werte im Balken, stattdessen alle Kategorien im Tooltip, größte Ausgabe
zuerst, und den Monat ohne Uhrzeit (Monatserster UTC wirkte in MEZ wie
01:00 Uhr)."""
panels = {p["title"]: p for p in _load()["panels"]}
for title in BARCHARTS:
p = panels[title]
assert p["options"]["showValue"] == "never", title
assert p["options"]["tooltip"] == {"mode": "multi", "sort": "asc"}, title
overrides = p["fieldConfig"]["overrides"]
assert {"matcher": {"id": "byType", "options": "time"},
"properties": [{"id": "unit", "value": "time:MM.YYYY"}]} in overrides, title