Files
meshcore-stats/tests/reports/test_aggregation_helpers.py
Jorijn Schrijvershof ca13e31aae test: stabilize suite and broaden integration coverage (#32)
* tests: cache integration/report fixtures to speed up tests

* fix: speed up yearly aggregation and refresh timings report

* chore: remove the report

* fix: unrecognized named-value: 'runner'. Located at position 1 within expression: runner.temp

* fix: ruff linting error

* test: strengthen assertions and stabilize tests

* test(integration): expand rendered chart metrics
2026-01-08 21:20:34 +01:00

425 lines
16 KiB
Python

"""Tests for report aggregation helper functions."""
from datetime import date, datetime
import pytest
from meshmon.reports import (
DailyAggregate,
MetricStats,
MonthlyAggregate,
_aggregate_daily_counter_to_summary,
_aggregate_daily_gauge_to_summary,
_aggregate_monthly_counter_to_summary,
_aggregate_monthly_gauge_to_summary,
_compute_counter_stats,
_compute_gauge_stats,
)
class TestComputeGaugeStats:
"""Tests for _compute_gauge_stats function."""
def test_returns_metric_stats(self):
"""Returns a MetricStats dataclass."""
values = [
(datetime(2024, 1, 1, 0, 0), 3.8),
(datetime(2024, 1, 1, 1, 0), 3.9),
(datetime(2024, 1, 1, 2, 0), 4.0),
]
result = _compute_gauge_stats(values)
assert isinstance(result, MetricStats)
def test_computes_min_max_mean(self):
"""Computes correct min, max, and mean."""
values = [
(datetime(2024, 1, 1, 0, 0), 3.8),
(datetime(2024, 1, 1, 1, 0), 3.9),
(datetime(2024, 1, 1, 2, 0), 4.0),
]
result = _compute_gauge_stats(values)
assert result.min_value == 3.8
assert result.max_value == 4.0
assert result.mean == pytest.approx(3.9)
assert result.count == 3
def test_handles_single_value(self):
"""Handles single value correctly."""
values = [(datetime(2024, 1, 1, 0, 0), 3.85)]
result = _compute_gauge_stats(values)
assert result.min_value == 3.85
assert result.max_value == 3.85
assert result.mean == 3.85
assert result.count == 1
assert result.min_time == datetime(2024, 1, 1, 0, 0)
assert result.max_time == datetime(2024, 1, 1, 0, 0)
def test_handles_empty_list(self):
"""Handles empty list gracefully."""
result = _compute_gauge_stats([])
assert result.min_value is None
assert result.max_value is None
assert result.mean is None
assert result.count == 0
def test_tracks_count(self):
"""Tracks the number of values."""
values = [
(datetime(2024, 1, 1, i, 0), 3.8 + i * 0.01)
for i in range(10)
]
result = _compute_gauge_stats(values)
assert result.count == 10
def test_tracks_min_time(self):
"""Tracks timestamp of minimum value."""
values = [
(datetime(2024, 1, 1, 0, 0), 3.9),
(datetime(2024, 1, 1, 1, 0), 3.7), # Min
(datetime(2024, 1, 1, 2, 0), 3.8),
]
result = _compute_gauge_stats(values)
assert result.min_time == datetime(2024, 1, 1, 1, 0)
def test_tracks_max_time(self):
"""Tracks timestamp of maximum value."""
values = [
(datetime(2024, 1, 1, 0, 0), 3.9),
(datetime(2024, 1, 1, 1, 0), 4.1), # Max
(datetime(2024, 1, 1, 2, 0), 3.8),
]
result = _compute_gauge_stats(values)
assert result.max_time == datetime(2024, 1, 1, 1, 0)
class TestComputeCounterStats:
"""Tests for _compute_counter_stats function."""
def test_returns_metric_stats(self):
"""Returns a MetricStats dataclass."""
values = [
(datetime(2024, 1, 1, 0, 0), 100),
(datetime(2024, 1, 1, 1, 0), 150),
(datetime(2024, 1, 1, 2, 0), 200),
]
result = _compute_counter_stats(values)
assert isinstance(result, MetricStats)
def test_computes_total_delta(self):
"""Computes total delta from counter values."""
values = [
(datetime(2024, 1, 1, 0, 0), 100),
(datetime(2024, 1, 1, 1, 0), 150), # +50
(datetime(2024, 1, 1, 2, 0), 200), # +50
]
result = _compute_counter_stats(values)
# Total should be 100 (50 + 50)
assert result.total == 100
assert result.count == 3
assert result.reboot_count == 0
def test_handles_counter_reboot(self):
"""Handles counter reboot (value decrease)."""
values = [
(datetime(2024, 1, 1, 0, 0), 100),
(datetime(2024, 1, 1, 1, 0), 150), # +50
(datetime(2024, 1, 1, 2, 0), 20), # Reboot - counts from 0
(datetime(2024, 1, 1, 3, 0), 50), # +30
]
result = _compute_counter_stats(values)
# Total: 50 + 20 + 30 = 100
assert result.total == 100
assert result.reboot_count == 1
assert result.count == 4
def test_tracks_reboot_count(self):
"""Tracks number of reboots."""
values = [
(datetime(2024, 1, 1, 0, 0), 100),
(datetime(2024, 1, 1, 1, 0), 150),
(datetime(2024, 1, 1, 2, 0), 20), # Reboot 1
(datetime(2024, 1, 1, 3, 0), 50),
(datetime(2024, 1, 1, 4, 0), 10), # Reboot 2
]
result = _compute_counter_stats(values)
assert result.reboot_count == 2
assert result.total == 110
assert result.count == 5
def test_handles_empty_list(self):
"""Handles empty list gracefully."""
result = _compute_counter_stats([])
assert result.total is None
assert result.count == 0
assert result.reboot_count == 0
def test_handles_single_value(self):
"""Handles single value (no delta possible)."""
values = [(datetime(2024, 1, 1, 0, 0), 100)]
result = _compute_counter_stats(values)
# Single value means no delta can be computed
assert result.total is None
assert result.count == 1
assert result.reboot_count == 0
class TestAggregateDailyGaugeToSummary:
"""Tests for _aggregate_daily_gauge_to_summary function."""
@pytest.fixture
def daily_gauge_data(self):
"""Sample daily gauge aggregates."""
return [
DailyAggregate(
date=date(2024, 1, 1),
metrics={
"battery": MetricStats(
min_value=3.7, min_time=datetime(2024, 1, 1, 3, 0),
max_value=3.9, max_time=datetime(2024, 1, 1, 15, 0),
mean=3.8, count=96
)
}
),
DailyAggregate(
date=date(2024, 1, 2),
metrics={
"battery": MetricStats(
min_value=3.6, min_time=datetime(2024, 1, 2, 4, 0),
max_value=4.0, max_time=datetime(2024, 1, 2, 12, 0),
mean=3.85, count=96
)
}
),
DailyAggregate(
date=date(2024, 1, 3),
metrics={
"battery": MetricStats(
min_value=3.8, min_time=datetime(2024, 1, 3, 2, 0),
max_value=4.1, max_time=datetime(2024, 1, 3, 18, 0),
mean=3.95, count=96
)
}
),
]
def test_returns_metric_stats(self, daily_gauge_data):
"""Returns a MetricStats object."""
result = _aggregate_daily_gauge_to_summary(daily_gauge_data, "battery")
assert isinstance(result, MetricStats)
def test_finds_overall_min(self, daily_gauge_data):
"""Finds minimum across all days."""
result = _aggregate_daily_gauge_to_summary(daily_gauge_data, "battery")
assert result.min_value == 3.6
assert result.min_time == datetime(2024, 1, 2, 4, 0)
def test_finds_overall_max(self, daily_gauge_data):
"""Finds maximum across all days."""
result = _aggregate_daily_gauge_to_summary(daily_gauge_data, "battery")
assert result.max_value == 4.1
assert result.max_time == datetime(2024, 1, 3, 18, 0)
def test_computes_weighted_mean(self, daily_gauge_data):
"""Computes weighted mean based on count."""
result = _aggregate_daily_gauge_to_summary(daily_gauge_data, "battery")
# All have same count, so simple average: (3.8 + 3.85 + 3.95) / 3 = 3.8667
assert result.mean == pytest.approx(3.8667, rel=0.01)
assert result.count == 288
def test_handles_empty_list(self):
"""Handles empty daily list."""
result = _aggregate_daily_gauge_to_summary([], "battery")
assert result.min_value is None
assert result.max_value is None
assert result.mean is None
assert result.count == 0
def test_handles_missing_metric(self, daily_gauge_data):
"""Handles when metric doesn't exist in daily data."""
result = _aggregate_daily_gauge_to_summary(daily_gauge_data, "nonexistent")
assert result.min_value is None
assert result.max_value is None
assert result.mean is None
assert result.count == 0
class TestAggregateDailyCounterToSummary:
"""Tests for _aggregate_daily_counter_to_summary function."""
@pytest.fixture
def daily_counter_data(self):
"""Sample daily counter aggregates."""
return [
DailyAggregate(
date=date(2024, 1, 1),
metrics={
"packets_rx": MetricStats(total=1000, reboot_count=0, count=96)
}
),
DailyAggregate(
date=date(2024, 1, 2),
metrics={
"packets_rx": MetricStats(total=1500, reboot_count=1, count=96)
}
),
DailyAggregate(
date=date(2024, 1, 3),
metrics={
"packets_rx": MetricStats(total=800, reboot_count=0, count=96)
}
),
]
def test_returns_metric_stats(self, daily_counter_data):
"""Returns a MetricStats object."""
result = _aggregate_daily_counter_to_summary(daily_counter_data, "packets_rx")
assert isinstance(result, MetricStats)
def test_sums_totals(self, daily_counter_data):
"""Sums totals across all days."""
result = _aggregate_daily_counter_to_summary(daily_counter_data, "packets_rx")
assert result.total == 3300 # 1000 + 1500 + 800
assert result.count == 288
def test_sums_reboots(self, daily_counter_data):
"""Sums reboot counts across all days."""
result = _aggregate_daily_counter_to_summary(daily_counter_data, "packets_rx")
assert result.reboot_count == 1
def test_handles_empty_list(self):
"""Handles empty daily list."""
result = _aggregate_daily_counter_to_summary([], "packets_rx")
assert result.total is None
assert result.count == 0
assert result.reboot_count == 0
def test_handles_missing_metric(self, daily_counter_data):
"""Handles when metric doesn't exist in daily data."""
result = _aggregate_daily_counter_to_summary(daily_counter_data, "nonexistent")
assert result.total is None
assert result.count == 0
assert result.reboot_count == 0
class TestAggregateMonthlyGaugeToSummary:
"""Tests for _aggregate_monthly_gauge_to_summary function."""
@pytest.fixture
def monthly_gauge_data(self):
"""Sample monthly gauge aggregates."""
return [
MonthlyAggregate(
year=2024,
month=1,
role="companion",
summary={
"battery": MetricStats(
min_value=3.6, min_time=datetime(2024, 1, 15, 4, 0),
max_value=4.0, max_time=datetime(2024, 1, 20, 14, 0),
mean=3.8, count=2976
)
}
),
MonthlyAggregate(
year=2024,
month=2,
role="companion",
summary={
"battery": MetricStats(
min_value=3.5, min_time=datetime(2024, 2, 10, 5, 0),
max_value=4.1, max_time=datetime(2024, 2, 25, 16, 0),
mean=3.9, count=2784
)
}
),
]
def test_returns_metric_stats(self, monthly_gauge_data):
"""Returns a MetricStats object."""
result = _aggregate_monthly_gauge_to_summary(monthly_gauge_data, "battery")
assert isinstance(result, MetricStats)
def test_finds_overall_min(self, monthly_gauge_data):
"""Finds minimum across all months."""
result = _aggregate_monthly_gauge_to_summary(monthly_gauge_data, "battery")
assert result.min_value == 3.5
assert result.min_time == datetime(2024, 2, 10, 5, 0)
def test_finds_overall_max(self, monthly_gauge_data):
"""Finds maximum across all months."""
result = _aggregate_monthly_gauge_to_summary(monthly_gauge_data, "battery")
assert result.max_value == 4.1
assert result.max_time == datetime(2024, 2, 25, 16, 0)
def test_computes_weighted_mean(self, monthly_gauge_data):
"""Computes weighted mean based on count."""
result = _aggregate_monthly_gauge_to_summary(monthly_gauge_data, "battery")
# Weighted: (3.8 * 2976 + 3.9 * 2784) / (2976 + 2784)
expected = (3.8 * 2976 + 3.9 * 2784) / (2976 + 2784)
assert result.mean == pytest.approx(expected, rel=0.01)
assert result.count == 5760
def test_handles_empty_list(self):
"""Handles empty monthly list."""
result = _aggregate_monthly_gauge_to_summary([], "battery")
assert result.min_value is None
assert result.max_value is None
assert result.mean is None
assert result.count == 0
class TestAggregateMonthlyCounterToSummary:
"""Tests for _aggregate_monthly_counter_to_summary function."""
@pytest.fixture
def monthly_counter_data(self):
"""Sample monthly counter aggregates."""
return [
MonthlyAggregate(
year=2024,
month=1,
role="companion",
summary={
"packets_rx": MetricStats(total=50000, reboot_count=2, count=2976)
}
),
MonthlyAggregate(
year=2024,
month=2,
role="companion",
summary={
"packets_rx": MetricStats(total=45000, reboot_count=1, count=2784)
}
),
]
def test_returns_metric_stats(self, monthly_counter_data):
"""Returns a MetricStats object."""
result = _aggregate_monthly_counter_to_summary(monthly_counter_data, "packets_rx")
assert isinstance(result, MetricStats)
def test_sums_totals(self, monthly_counter_data):
"""Sums totals across all months."""
result = _aggregate_monthly_counter_to_summary(monthly_counter_data, "packets_rx")
assert result.total == 95000
assert result.count == 5760
def test_sums_reboots(self, monthly_counter_data):
"""Sums reboot counts across all months."""
result = _aggregate_monthly_counter_to_summary(monthly_counter_data, "packets_rx")
assert result.reboot_count == 3
def test_handles_empty_list(self):
"""Handles empty monthly list."""
result = _aggregate_monthly_counter_to_summary([], "packets_rx")
assert result.total is None
assert result.count == 0
assert result.reboot_count == 0
def test_handles_missing_metric(self, monthly_counter_data):
"""Handles when metric doesn't exist in monthly data."""
result = _aggregate_monthly_counter_to_summary(monthly_counter_data, "nonexistent")
assert result.total is None
assert result.count == 0
assert result.reboot_count == 0