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