"""Tests for hytop.core.history.SlidingHistory.""" from __future__ import annotations from dataclasses import dataclass import pytest from hytop.core.history import SlidingHistory @dataclass class _Sample: """Minimal MetricSample for testing.""" ts: float value: float = 0.0 class TestSlidingHistoryBasics: def test_empty_latest_is_none(self): h = SlidingHistory(max_window_s=10) assert h.latest() is None def test_add_and_latest(self): h = SlidingHistory(max_window_s=10) s = _Sample(ts=1.0, value=42.0) h.add(s) assert h.latest() is s def test_latest_returns_most_recent(self): h = SlidingHistory(max_window_s=10) h.add(_Sample(ts=1.0, value=1.0)) s2 = _Sample(ts=2.0, value=2.0) h.add(s2) assert h.latest() is s2 def test_add_prunes_outside_window(self): h = SlidingHistory(max_window_s=5) h.add(_Sample(ts=0.0)) h.add(_Sample(ts=6.0)) # now=6.0, cutoff=1.0 → ts=0.0 pruned assert len(h.samples) == 1 def test_add_keeps_samples_within_window(self): h = SlidingHistory(max_window_s=10) h.add(_Sample(ts=0.0)) h.add(_Sample(ts=8.0)) # cutoff=-2.0, both kept assert len(h.samples) == 2 class TestSlidingHistoryAvg: def test_empty_returns_zero(self): h = SlidingHistory(max_window_s=10) assert h.avg("value", window_s=5, now=10.0) == 0.0 def test_single_sample_in_window(self): h = SlidingHistory(max_window_s=10) h.add(_Sample(ts=9.0, value=4.0)) assert h.avg("value", window_s=5, now=10.0) == pytest.approx(4.0) def test_average_of_multiple(self): h = SlidingHistory(max_window_s=10) h.add(_Sample(ts=8.0, value=2.0)) h.add(_Sample(ts=9.0, value=4.0)) assert h.avg("value", window_s=5, now=10.0) == pytest.approx(3.0) def test_sample_outside_window_excluded(self): h = SlidingHistory(max_window_s=20) h.add(_Sample(ts=0.0, value=100.0)) # inside max_window but outside avg window h.add(_Sample(ts=9.0, value=2.0)) # avg window=5, now=10 → cutoff=5.0 → ts=0.0 excluded assert h.avg("value", window_s=5, now=10.0) == pytest.approx(2.0) def test_no_samples_in_window_returns_zero(self): h = SlidingHistory(max_window_s=100) h.add(_Sample(ts=0.0, value=5.0)) # window=2, now=10 → cutoff=8, ts=0 excluded assert h.avg("value", window_s=2, now=10.0) == 0.0 def test_none_value_excluded_from_avg(self): """avg should skip samples where the attribute is None.""" h = SlidingHistory(max_window_s=10) h.add(_Sample(ts=9.0, value=6.0)) # Add a sample without a 'value' attr at all — use missing attribute branch # (MetricSample Protocol only requires ts; extra attrs may be None) @dataclass class _NoneValue: ts: float value: None = None h.add(_NoneValue(ts=9.5)) # Only the float sample should count assert h.avg("value", window_s=5, now=10.0) == pytest.approx(6.0)