willemjan/glucose-dashboard/pipeline/head This commit looks good
pytest-cov + pyproject.toml [tool.coverage.*] measure reporting.py, nightscout_client.py, data_processing.py, pdf_export.py, profile_data.py, ui_common.py, and auth.py; app.py and pages/*.py are omitted (thin Streamlit UI scripts, not meaningfully unit-testable without a running Streamlit session — the handful of similarly UI-coupled functions left in ui_common.py/auth.py are marked "# pragma: no cover" with the same rationale, since the logic they call into is already fully tested). The Gitea Actions test job now runs with --cov and fails the build below 80%; the coverage XML is uploaded as a build artifact and fed into the SonarQube scan (closing the gap flagged in homelab-cicd-werkinstructie.md). Backfilled a few missing branches (get_colors() theme fallback, secondary-y-axis overlay traces) to reach 98% on the measured modules — comfortable headroom above the 80% gate. Builds on #29/#30/#31.
187 lines
6.7 KiB
Python
187 lines
6.7 KiB
Python
from types import SimpleNamespace
|
|
|
|
import pandas as pd
|
|
import plotly.graph_objects as go
|
|
import streamlit as st
|
|
|
|
from ui_common import (
|
|
PALETTE,
|
|
THRESHOLD_LINES,
|
|
add_basal_area_trace,
|
|
add_bolus_marker_traces,
|
|
add_carb_bars_trace,
|
|
add_target_line_trace,
|
|
add_threshold_lines,
|
|
build_tir_stacked_bar_figure,
|
|
get_colors,
|
|
hex_to_rgba,
|
|
)
|
|
|
|
|
|
def test_get_colors_returns_dark_palette_when_theme_is_dark(monkeypatch):
|
|
monkeypatch.setattr(st, "context", SimpleNamespace(theme=SimpleNamespace(type="dark")))
|
|
assert get_colors() == PALETTE["dark"]
|
|
|
|
|
|
def test_get_colors_returns_light_palette_when_theme_is_light(monkeypatch):
|
|
monkeypatch.setattr(st, "context", SimpleNamespace(theme=SimpleNamespace(type="light")))
|
|
assert get_colors() == PALETTE["light"]
|
|
|
|
|
|
def test_get_colors_falls_back_to_light_when_theme_context_unavailable(monkeypatch):
|
|
monkeypatch.setattr(st, "context", SimpleNamespace()) # geen .theme-attribuut
|
|
assert get_colors() == PALETTE["light"]
|
|
|
|
|
|
def test_hex_to_rgba_converts_correctly():
|
|
assert hex_to_rgba("#2a78d6", 0.3) == "rgba(42,120,214,0.3)"
|
|
|
|
|
|
def test_hex_to_rgba_handles_hash_optional():
|
|
assert hex_to_rgba("2a78d6", 0.3) == "rgba(42,120,214,0.3)"
|
|
|
|
|
|
def test_add_threshold_lines_adds_one_line_per_clinical_threshold():
|
|
fig = go.Figure()
|
|
add_threshold_lines(fig, PALETTE["light"])
|
|
assert len(fig.layout.shapes) == len(THRESHOLD_LINES)
|
|
|
|
|
|
def test_add_threshold_lines_targets_subplot_row_when_given():
|
|
fig = go.Figure()
|
|
fig.add_trace(go.Scatter(x=[0], y=[0]))
|
|
add_threshold_lines(fig, PALETTE["light"], row=1)
|
|
assert len(fig.layout.shapes) == len(THRESHOLD_LINES)
|
|
|
|
|
|
def test_build_tir_stacked_bar_figure_has_one_bar_trace_per_band():
|
|
tir = {"very_low": 1.0, "low": 3.0, "in_range": 80.0, "high": 12.0, "very_high": 4.0}
|
|
fig = build_tir_stacked_bar_figure(tir, PALETTE["light"])
|
|
assert len(fig.data) == 5
|
|
assert [trace.y[0] for trace in fig.data] == [1.0, 3.0, 80.0, 12.0, 4.0]
|
|
assert fig.layout.barmode == "stack"
|
|
|
|
|
|
def test_add_basal_area_trace_adds_one_filled_step_trace():
|
|
basal = pd.DataFrame(
|
|
{"timestamp": pd.to_datetime(["2026-01-01 00:00", "2026-01-01 06:00"]), "rate": [0.8, 1.2]}
|
|
)
|
|
fig = go.Figure()
|
|
add_basal_area_trace(fig, basal, PALETTE["light"])
|
|
assert len(fig.data) == 1
|
|
assert fig.data[0].name == "Basaal"
|
|
assert fig.data[0].fill == "tozeroy"
|
|
assert fig.data[0].line.shape == "hv"
|
|
|
|
|
|
def test_add_basal_area_trace_supports_secondary_y_on_a_subplot_figure():
|
|
from plotly.subplots import make_subplots
|
|
|
|
basal = pd.DataFrame(
|
|
{"timestamp": pd.to_datetime(["2026-01-01 00:00", "2026-01-01 06:00"]), "rate": [0.8, 1.2]}
|
|
)
|
|
fig = make_subplots(specs=[[{"secondary_y": True}]])
|
|
add_basal_area_trace(fig, basal, PALETTE["light"], row=1, secondary_y=True)
|
|
assert len(fig.data) == 1
|
|
assert fig.data[0].yaxis == "y2"
|
|
|
|
|
|
def test_add_basal_area_trace_skips_empty_input():
|
|
fig = go.Figure()
|
|
add_basal_area_trace(fig, pd.DataFrame(columns=["timestamp", "rate"]), PALETTE["light"])
|
|
assert len(fig.data) == 0
|
|
|
|
|
|
def test_add_carb_bars_trace_supports_secondary_y_on_a_subplot_figure():
|
|
from plotly.subplots import make_subplots
|
|
|
|
carbs = pd.DataFrame({"timestamp": pd.to_datetime(["2026-01-01 08:00"]), "carbs": [40.0]})
|
|
fig = make_subplots(specs=[[{"secondary_y": True}]])
|
|
add_carb_bars_trace(fig, carbs, PALETTE["light"], row=1, secondary_y=True)
|
|
assert len(fig.data) == 1
|
|
assert fig.data[0].yaxis == "y2"
|
|
|
|
|
|
def test_add_carb_bars_trace_adds_one_bar_trace():
|
|
carbs = pd.DataFrame({"timestamp": pd.to_datetime(["2026-01-01 08:00"]), "carbs": [40.0]})
|
|
fig = go.Figure()
|
|
add_carb_bars_trace(fig, carbs, PALETTE["light"])
|
|
assert len(fig.data) == 1
|
|
assert fig.data[0].name == "Koolhydraten"
|
|
assert isinstance(fig.data[0], go.Bar)
|
|
|
|
|
|
def test_add_carb_bars_trace_skips_empty_input():
|
|
fig = go.Figure()
|
|
add_carb_bars_trace(fig, pd.DataFrame(columns=["timestamp", "carbs"]), PALETTE["light"])
|
|
assert len(fig.data) == 0
|
|
|
|
|
|
def test_add_bolus_marker_traces_adds_one_trace_per_category_present():
|
|
bolus = pd.DataFrame(
|
|
{
|
|
"timestamp": pd.to_datetime(["2026-01-01 08:00", "2026-01-01 12:00"]),
|
|
"insulin": [4.0, 0.3],
|
|
"category": ["food", "smb"],
|
|
}
|
|
)
|
|
fig = go.Figure()
|
|
add_bolus_marker_traces(fig, bolus, PALETTE["light"], lane_y=2.0)
|
|
names = {trace.name for trace in fig.data}
|
|
assert names == {"Bolus (voeding)", "SMB"}
|
|
|
|
|
|
def test_add_bolus_marker_traces_labels_food_and_correction_but_not_smb():
|
|
bolus = pd.DataFrame(
|
|
{
|
|
"timestamp": pd.to_datetime(["2026-01-01 08:00", "2026-01-01 12:00"]),
|
|
"insulin": [4.0, 0.3],
|
|
"category": ["food", "smb"],
|
|
}
|
|
)
|
|
fig = go.Figure()
|
|
add_bolus_marker_traces(fig, bolus, PALETTE["light"], lane_y=2.0)
|
|
by_name = {trace.name: trace for trace in fig.data}
|
|
assert by_name["Bolus (voeding)"].mode == "markers+text"
|
|
assert by_name["SMB"].mode == "markers"
|
|
|
|
|
|
def test_add_bolus_marker_traces_skips_empty_input():
|
|
fig = go.Figure()
|
|
add_bolus_marker_traces(fig, pd.DataFrame(columns=["timestamp", "insulin", "category"]), PALETTE["light"], 2.0)
|
|
assert len(fig.data) == 0
|
|
|
|
|
|
def test_add_target_line_trace_adds_one_line_trace():
|
|
target = pd.DataFrame({"timestamp": pd.to_datetime(["2026-01-01 00:00"]), "target": [6.5]})
|
|
fig = go.Figure()
|
|
add_target_line_trace(fig, target, PALETTE["light"])
|
|
assert len(fig.data) == 1
|
|
assert fig.data[0].name == "Streefwaarde (pomp)"
|
|
|
|
|
|
def test_add_target_line_trace_skips_empty_input():
|
|
fig = go.Figure()
|
|
add_target_line_trace(fig, pd.DataFrame(columns=["timestamp", "target"]), PALETTE["light"])
|
|
assert len(fig.data) == 0
|
|
|
|
|
|
def test_overlay_traces_support_an_hour_based_x_column_for_dagoverzicht():
|
|
"""Dagoverzicht plot elke dag op een gedeelde 0-24u-as ('hour') i.p.v. echte
|
|
datums; de hover mag daar dan ook geen datumformattering tonen."""
|
|
basal = pd.DataFrame({"hour": [0.0, 6.0], "rate": [0.8, 1.2]})
|
|
carbs = pd.DataFrame({"hour": [8.0], "carbs": [40.0]})
|
|
bolus = pd.DataFrame({"hour": [8.0], "insulin": [4.0], "category": ["food"]})
|
|
target = pd.DataFrame({"hour": [0.0], "target": [6.5]})
|
|
|
|
fig = go.Figure()
|
|
add_basal_area_trace(fig, basal, PALETTE["light"], x_col="hour")
|
|
add_carb_bars_trace(fig, carbs, PALETTE["light"], x_col="hour")
|
|
add_bolus_marker_traces(fig, bolus, PALETTE["light"], lane_y=2.0, x_col="hour")
|
|
add_target_line_trace(fig, target, PALETTE["light"], x_col="hour")
|
|
|
|
assert len(fig.data) == 4
|
|
bolus_trace = next(t for t in fig.data if t.name == "Bolus (voeding)")
|
|
assert list(bolus_trace.x) == [8.0]
|
|
assert "%d-%m" not in bolus_trace.hovertemplate
|