mirror of
https://github.com/jorijn/meshcore-stats.git
synced 2026-03-28 17:42:55 +01:00
228 lines
7.7 KiB
Python
228 lines
7.7 KiB
Python
"""Tests for TimeSeries data class and loading."""
|
|
|
|
from datetime import datetime, timedelta
|
|
|
|
import pytest
|
|
|
|
from meshmon.charts import (
|
|
DataPoint,
|
|
TimeSeries,
|
|
load_timeseries_from_db,
|
|
)
|
|
from meshmon.db import insert_metrics
|
|
|
|
BASE_TIME = datetime(2024, 1, 1, 0, 0, 0)
|
|
|
|
|
|
class TestDataPoint:
|
|
"""Tests for DataPoint dataclass."""
|
|
|
|
def test_stores_timestamp_and_value(self):
|
|
"""Stores timestamp and value."""
|
|
ts = BASE_TIME
|
|
dp = DataPoint(timestamp=ts, value=3.85)
|
|
|
|
assert dp.timestamp == ts
|
|
assert dp.value == 3.85
|
|
|
|
def test_value_types(self):
|
|
"""Accepts float and int values."""
|
|
ts = BASE_TIME
|
|
|
|
dp_float = DataPoint(timestamp=ts, value=3.85)
|
|
assert dp_float.value == 3.85
|
|
|
|
dp_int = DataPoint(timestamp=ts, value=100)
|
|
assert dp_int.value == 100
|
|
|
|
|
|
class TestTimeSeries:
|
|
"""Tests for TimeSeries dataclass."""
|
|
|
|
def test_stores_metadata(self):
|
|
"""Stores metric, role, period metadata."""
|
|
ts = TimeSeries(
|
|
metric="bat",
|
|
role="repeater",
|
|
period="day",
|
|
)
|
|
|
|
assert ts.metric == "bat"
|
|
assert ts.role == "repeater"
|
|
assert ts.period == "day"
|
|
|
|
def test_empty_by_default(self):
|
|
"""Points list is empty by default."""
|
|
ts = TimeSeries(metric="bat", role="repeater", period="day")
|
|
|
|
assert ts.points == []
|
|
assert ts.is_empty is True
|
|
|
|
def test_timestamps_property(self, sample_timeseries):
|
|
"""timestamps property returns list of timestamps."""
|
|
timestamps = sample_timeseries.timestamps
|
|
|
|
assert len(timestamps) == len(sample_timeseries.points)
|
|
assert all(isinstance(t, datetime) for t in timestamps)
|
|
|
|
def test_values_property(self, sample_timeseries):
|
|
"""values property returns list of values."""
|
|
values = sample_timeseries.values
|
|
|
|
assert len(values) == len(sample_timeseries.points)
|
|
assert all(isinstance(v, float) for v in values)
|
|
|
|
def test_is_empty_false_with_data(self, sample_timeseries):
|
|
"""is_empty is False when points exist."""
|
|
assert sample_timeseries.is_empty is False
|
|
|
|
def test_is_empty_true_without_data(self, empty_timeseries):
|
|
"""is_empty is True when no points."""
|
|
assert empty_timeseries.is_empty is True
|
|
|
|
|
|
class TestLoadTimeseriesFromDb:
|
|
"""Tests for load_timeseries_from_db function."""
|
|
|
|
def test_loads_metric_data(self, initialized_db, configured_env):
|
|
"""Loads metric data from database."""
|
|
base_ts = 1704067200
|
|
insert_metrics(base_ts, "repeater", {"bat": 3850.0}, initialized_db)
|
|
insert_metrics(base_ts + 900, "repeater", {"bat": 3860.0}, initialized_db)
|
|
|
|
ts = load_timeseries_from_db(
|
|
role="repeater",
|
|
metric="bat",
|
|
end_time=datetime.fromtimestamp(base_ts + 1000),
|
|
lookback=timedelta(hours=1),
|
|
period="day",
|
|
)
|
|
|
|
assert len(ts.points) == 2
|
|
|
|
def test_filters_by_time_range(self, initialized_db, configured_env):
|
|
"""Only loads data within time range."""
|
|
base_ts = 1704067200
|
|
|
|
# Insert data outside and inside range
|
|
insert_metrics(base_ts - 7200, "repeater", {"bat": 3800.0}, initialized_db) # Outside
|
|
insert_metrics(base_ts, "repeater", {"bat": 3850.0}, initialized_db) # Inside
|
|
insert_metrics(base_ts + 7200, "repeater", {"bat": 3900.0}, initialized_db) # Outside
|
|
|
|
ts = load_timeseries_from_db(
|
|
role="repeater",
|
|
metric="bat",
|
|
end_time=datetime.fromtimestamp(base_ts + 1800),
|
|
lookback=timedelta(hours=1),
|
|
period="day",
|
|
)
|
|
|
|
assert len(ts.points) == 1
|
|
assert ts.points[0].value == pytest.approx(3.85) # Transformed to volts
|
|
|
|
def test_returns_correct_metadata(self, initialized_db, configured_env):
|
|
"""Returned TimeSeries has correct metadata."""
|
|
ts = load_timeseries_from_db(
|
|
role="repeater",
|
|
metric="bat",
|
|
end_time=BASE_TIME,
|
|
lookback=timedelta(hours=1),
|
|
period="week",
|
|
)
|
|
|
|
assert ts.metric == "bat"
|
|
assert ts.role == "repeater"
|
|
assert ts.period == "week"
|
|
|
|
def test_uses_prefetched_metrics(self, initialized_db, configured_env):
|
|
"""Can use pre-fetched metrics dict."""
|
|
base_ts = 1704067200
|
|
insert_metrics(base_ts, "repeater", {"bat": 3850.0}, initialized_db)
|
|
|
|
# Pre-fetch metrics
|
|
from meshmon.db import get_metrics_for_period
|
|
all_metrics = get_metrics_for_period("repeater", base_ts - 3600, base_ts + 3600)
|
|
|
|
ts = load_timeseries_from_db(
|
|
role="repeater",
|
|
metric="bat",
|
|
end_time=datetime.fromtimestamp(base_ts + 3600),
|
|
lookback=timedelta(hours=2),
|
|
period="day",
|
|
all_metrics=all_metrics,
|
|
)
|
|
|
|
assert len(ts.points) == 1
|
|
|
|
def test_handles_missing_metric(self, initialized_db, configured_env):
|
|
"""Returns empty TimeSeries for missing metric."""
|
|
ts = load_timeseries_from_db(
|
|
role="repeater",
|
|
metric="nonexistent_metric",
|
|
end_time=BASE_TIME,
|
|
lookback=timedelta(hours=1),
|
|
period="day",
|
|
)
|
|
|
|
assert ts.is_empty
|
|
|
|
def test_sorts_by_timestamp(self, initialized_db, configured_env):
|
|
"""Points are sorted by timestamp."""
|
|
base_ts = 1704067200
|
|
|
|
# Insert out of order
|
|
insert_metrics(base_ts + 300, "repeater", {"bat": 3860.0}, initialized_db)
|
|
insert_metrics(base_ts, "repeater", {"bat": 3850.0}, initialized_db)
|
|
insert_metrics(base_ts + 150, "repeater", {"bat": 3855.0}, initialized_db)
|
|
|
|
ts = load_timeseries_from_db(
|
|
role="repeater",
|
|
metric="bat",
|
|
end_time=datetime.fromtimestamp(base_ts + 600),
|
|
lookback=timedelta(hours=1),
|
|
period="day",
|
|
)
|
|
|
|
timestamps = [p.timestamp for p in ts.points]
|
|
assert timestamps == sorted(timestamps)
|
|
|
|
def test_telemetry_temperature_converts_to_imperial(self, initialized_db, configured_env, monkeypatch):
|
|
"""Telemetry temperature converts from C to F when DISPLAY_UNIT_SYSTEM=imperial."""
|
|
monkeypatch.setenv("DISPLAY_UNIT_SYSTEM", "imperial")
|
|
import meshmon.env
|
|
meshmon.env._config = None
|
|
|
|
base_ts = 1704067200
|
|
insert_metrics(base_ts, "repeater", {"telemetry.temperature.1": 0.0}, initialized_db)
|
|
insert_metrics(base_ts + 900, "repeater", {"telemetry.temperature.1": 10.0}, initialized_db)
|
|
|
|
ts = load_timeseries_from_db(
|
|
role="repeater",
|
|
metric="telemetry.temperature.1",
|
|
end_time=datetime.fromtimestamp(base_ts + 1000),
|
|
lookback=timedelta(hours=1),
|
|
period="day",
|
|
)
|
|
|
|
assert [p.value for p in ts.points] == pytest.approx([32.0, 50.0])
|
|
|
|
def test_telemetry_temperature_stays_metric(self, initialized_db, configured_env, monkeypatch):
|
|
"""Telemetry temperature remains Celsius when DISPLAY_UNIT_SYSTEM=metric."""
|
|
monkeypatch.setenv("DISPLAY_UNIT_SYSTEM", "metric")
|
|
import meshmon.env
|
|
meshmon.env._config = None
|
|
|
|
base_ts = 1704067200
|
|
insert_metrics(base_ts, "repeater", {"telemetry.temperature.1": 0.0}, initialized_db)
|
|
insert_metrics(base_ts + 900, "repeater", {"telemetry.temperature.1": 10.0}, initialized_db)
|
|
|
|
ts = load_timeseries_from_db(
|
|
role="repeater",
|
|
metric="telemetry.temperature.1",
|
|
end_time=datetime.fromtimestamp(base_ts + 1000),
|
|
lookback=timedelta(hours=1),
|
|
period="day",
|
|
)
|
|
|
|
assert [p.value for p in ts.points] == pytest.approx([0.0, 10.0])
|