"""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 100–300 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