Files
manage/backend/tests/test_prometheus_range.py
T
Developer df80c68f89 feat(grafana-metric-gateway): slice 1 — backend gateway transport
Route all prometheus widget queries through Grafana /api/ds/query instead of
direct Prom HTTP. PrometheusConfig: drop base_url, add grafana_url +
datasource_uid; secret grafana_api_key (required). PrometheusWidgetSource →
MetricSource with _gateway_query POST method. normalize_grafana_frames
recovered from 65bae95 + shared _dedup_label helper. Gateway-path status
check. Startup old-config validation. CHANGELOG migration note. All adapter
tests rewritten for POST /api/ds/query + Grafana frames mock. Backend: 331
pytest pass, ruff clean. Frontend: build green (unchanged in S1).
2026-07-09 21:26:05 +00:00

258 lines
8.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Unit tests for the shared Prometheus range-query helpers (SC-101..SC-104)."""
from __future__ import annotations
import pytest
from media_library_viewer_api.widgets.prometheus_range import (
WINDOW_PRESETS,
_dedup_label,
normalize_grafana_frames,
normalize_prometheus_matrix,
step_for_window,
)
class TestStepForWindow:
"""SC-104: every preset must yield 100300 points."""
@pytest.mark.parametrize("preset", sorted(WINDOW_PRESETS))
def test_presets_yield_in_band_point_counts(self, preset: str) -> None:
window = WINDOW_PRESETS[preset]
step = step_for_window(window)
# Clamped minimum.
assert step >= 15
point_count = window // step
assert 100 <= point_count <= 300, f"{preset}: {point_count} points (step={step})"
def test_floor_of_fifteen_seconds(self) -> None:
# A tiny window that would otherwise produce a sub-15s step is clamped.
assert step_for_window(60) == 15
def test_custom_target_points(self) -> None:
# Targeting 100 points for 1h yields step 36 (3600/100).
assert step_for_window(3_600, target_points=100) == 36
class TestNormalizePrometheusMatrix:
"""SC-102: label rule + null handling + dedup."""
def test_empty_matrix(self) -> None:
assert normalize_prometheus_matrix([]) == []
def test_drops_dunder_labels_and_joins(self) -> None:
result = [
{
"metric": {"__name__": "node_cpu_seconds_total", "instance": "host:9100", "mode": "idle"},
"values": [[1_700_000_000, "12.5"], [1_700_000_030, "13.0"]],
}
]
out = normalize_prometheus_matrix(result)
assert len(out) == 1
assert out[0]["label"] == "instance=host:9100 mode=idle"
assert out[0]["points"] == [
{"t": 1_700_000_000, "v": 12.5},
{"t": 1_700_000_030, "v": 13.0},
]
def test_falls_back_to_value_when_no_labels(self) -> None:
result = [{"metric": {}, "values": [[100, "1"]]}]
out = normalize_prometheus_matrix(result)
assert out[0]["label"] == "value"
def test_dedup_collisions_with_suffix(self) -> None:
# Two series with identical visible labels get a "(1)" suffix on the 2nd.
result = [
{"metric": {"job": "x"}, "values": [[1, "1"]]},
{"metric": {"job": "x"}, "values": [[1, "2"]]},
]
out = normalize_prometheus_matrix(result)
labels = [s["label"] for s in out]
assert labels == ["job=x", "job=x (1)"]
def test_non_numeric_sentinels_become_none(self) -> None:
result = [
{
"metric": {"job": "x"},
"values": [
[1, "NaN"],
[2, "+Inf"],
[3, "-Inf"],
[4, "3.5"],
],
}
]
out = normalize_prometheus_matrix(result)
assert out[0]["points"] == [
{"t": 1, "v": None},
{"t": 2, "v": None},
{"t": 3, "v": None},
{"t": 4, "v": 3.5},
]
def test_malformed_values_are_ignored_not_raised(self) -> None:
result = [
{
"metric": {"job": "x"},
"values": [
[1, "3.5"],
["not-a-ts", "9"], # unusable timestamp → dropped
[3, "junk-value"], # unparseable value → v: None
],
}
]
out = normalize_prometheus_matrix(result)
assert out[0]["points"] == [
{"t": 1, "v": 3.5},
{"t": 3, "v": None},
]
class TestDedupLabel:
"""The shared label-dedup helper used by both normalizers (GM-104)."""
def test_first_use_returns_label_unchanged(self) -> None:
seen: dict[str, int] = {}
assert _dedup_label("value", seen) == "value"
assert seen == {"value": 0}
def test_collision_appends_suffix(self) -> None:
seen: dict[str, int] = {}
assert _dedup_label("job=x", seen) == "job=x"
assert _dedup_label("job=x", seen) == "job=x (1)"
assert _dedup_label("job=x", seen) == "job=x (2)"
def test_different_labels_dont_collide(self) -> None:
seen: dict[str, int] = {}
assert _dedup_label("a", seen) == "a"
assert _dedup_label("b", seen) == "b"
class TestNormalizeGrafanaFrames:
"""GM-104: frames normalizer recovered from 65bae95 + shared dedup."""
def test_empty_response(self) -> None:
assert normalize_grafana_frames({"results": {}}) == []
assert normalize_grafana_frames({}) == []
def test_single_frame_with_values(self) -> None:
raw = {
"results": {
"A": {
"frames": [
{
"data": {"values": [[1000, 2000], [1.5, 2.5]]},
"schema": {"fields": [{"name": "Time"}, {"name": "Value"}]},
}
]
}
}
}
out = normalize_grafana_frames(raw)
assert len(out) == 1
assert out[0]["label"] == "Value"
assert out[0]["points"] == [
{"t": 1000, "v": 1.5},
{"t": 2000, "v": 2.5},
]
def test_display_name_takes_priority(self) -> None:
raw = {
"results": {
"A": {
"frames": [
{
"data": {"values": [[100, 200], [0.75, 0.80]]},
"schema": {
"fields": [
{"name": "Time"},
{
"name": "Value",
"labels": {"instance": "host:9100"},
"config": {"displayName": "CPU Usage"},
},
]
},
}
]
}
}
}
out = normalize_grafana_frames(raw)
assert out[0]["label"] == "CPU Usage"
def test_labels_fallback_when_no_display_name(self) -> None:
raw = {
"results": {
"A": {
"frames": [
{
"data": {"values": [[100], [1.0]]},
"schema": {
"fields": [
{"name": "Time"},
{
"name": "Value",
"labels": {"__name__": "up", "instance": "h:9100"},
},
]
},
}
]
}
}
}
out = normalize_grafana_frames(raw)
assert out[0]["label"] == "instance=h:9100"
def test_falls_back_to_value_when_no_metadata(self) -> None:
raw = {
"results": {
"A": {
"frames": [
{
"data": {"values": [[100], [1.0]]},
"schema": {"fields": [{"name": "Time"}, {}]},
}
]
}
}
}
out = normalize_grafana_frames(raw)
assert out[0]["label"] == "value"
def test_dedup_collisions(self) -> None:
raw = {
"results": {
"A": {
"frames": [
{
"data": {"values": [[100], [1.0]]},
"schema": {"fields": [{}, {"name": "Value"}]},
},
{
"data": {"values": [[100], [2.0]]},
"schema": {"fields": [{}, {"name": "Value"}]},
},
]
}
}
}
out = normalize_grafana_frames(raw)
labels = [s["label"] for s in out]
assert labels == ["Value", "Value (1)"]
def test_skips_frames_with_insufficient_values(self) -> None:
raw = {
"results": {
"A": {
"frames": [
{"data": {"values": [[100]]}, "schema": {"fields": []}},
{"data": {"values": [[100], [1.0]]}, "schema": {"fields": [{}, {}]}},
]
}
}
}
out = normalize_grafana_frames(raw)
assert len(out) == 1