"""Rate estimation over a labelled window. Every throughput number in this dashboard is Δcounter/Δt. If the estimator returns a plausible-looking number for a counter that restarted, or a silent 0 for a scrape that arrived late, the UI cannot tell the difference — so the None cases matter more here than the happy path. """ from __future__ import annotations import math import pytest from dashboards.rates import RateEstimator def test_first_sample_has_no_rate() -> None: r = RateEstimator(span=10) assert r.rate("cpu", 100.0, now=1_000.0) == (None, None) def test_second_sample_gives_rate_and_window() -> None: r = RateEstimator(span=10) r.rate("cpu", 100.0, now=1_000.0) rate, window = r.rate("cpu", 300.0, now=1_010.0) assert rate == 20.0 assert window == 10.0 def test_window_is_labelled_with_the_span_actually_used() -> None: r = RateEstimator(span=10) r.rate("x", 0.0, now=0.0) r.rate("x", 10.0, now=2.0) # inside the span rate, window = r.rate("x", 30.0, now=11.0) # base is the sample 11s ago (outside the span) -> falls back to the previous sample assert window == 9.0 assert rate == pytest.approx(20.0 / 9.0) def test_none_value_does_not_poison_the_window() -> None: r = RateEstimator(span=10) r.rate("x", 10.0, now=0.0) assert r.rate("x", None, now=1.0) == (None, None) rate, _window = r.rate("x", 30.0, now=2.0) assert rate == 10.0, "the None scrape must not become the baseline" def test_nan_and_inf_are_treated_as_missing() -> None: r = RateEstimator(span=10) r.rate("x", 1.0, now=0.0) for bad in (float("nan"), float("inf")): assert r.rate("x", bad, now=1.0) == (None, None) def test_counter_reset_yields_none_once_then_recovers() -> None: """A restart must not report a huge negative — or, worse, a huge positive — rate.""" r = RateEstimator(span=60) r.rate("tokens", 5_000.0, now=0.0) rate, _ = r.rate("tokens", 10.0, now=10.0) # process restarted assert rate is None rate, _ = r.rate("tokens", 60.0, now=20.0) assert rate == 5.0, "the window re-seeds from the post-restart counter" def test_flat_counter_reports_zero_not_none() -> None: r = RateEstimator(span=10) r.rate("idle", 5.0, now=0.0) rate, window = r.rate("idle", 5.0, now=5.0) assert rate == 0.0 and window == 5.0 def test_keys_are_independent() -> None: r = RateEstimator(span=10) r.rate("a", 0.0, now=0.0) r.rate("b", 100.0, now=0.0) assert r.rate("a", 10.0, now=1.0)[0] == 10.0 assert r.rate("b", 100.0, now=1.0)[0] == 0.0 def test_forget_drops_a_removed_target() -> None: r = RateEstimator(span=10) for key in ("driveripper:cpu.total", "driveripper:disk.read", "deskwork:cpu.total"): r.rate(key, 1.0, now=0.0) assert r.forget("driveripper:") == 2 assert r.keys() == ["deskwork:cpu.total"] # and the surviving target is untouched assert r.rate("deskwork:cpu.total", 11.0, now=1.0)[0] == 10.0 def test_history_is_trimmed_to_max_samples() -> None: r = RateEstimator(span=1e6, max_samples=8) for i in range(50): r.rate("x", float(i), now=float(i)) assert len(r._samples["x"]) <= 8 # bounded memory is the contract assert all(math.isfinite(v) for _, v in r._samples["x"])