Files
omarchy/plugins/io.github.x3me.nexthop/test/test_instruments.py
T
asepharyana 1cdb82a76f Sync config from arch
- hypr/apps.lua
- hypr/autostart.lua
- hypr/envs.lua
- hypr/hyprland.lua
- hypr/hyprsunset.conf
- hypr/input.lua
- hypr/looknfeel.lua
- hypr/omasettings.lua
- hypr/xdph.conf
- omarchy/branding/about.txt
- omarchy/branding/screensaver.txt
- omarchy/extensions/omarchy-menu.jsonc
- omarchy/hooks/battery-low.d/play-warning-sound.sample
- omarchy/hooks/font-set.d/show-font-notification.sample
- omarchy/hooks/post-boot.d/weather.sample
- omarchy/hooks/post-update.d/install-voxtype.hook
- omarchy/hooks/post-update.d/setup-agent.hook
- omarchy/hooks/post-update.d/setup-fingerprint.hook
- omarchy/hooks/post-update.d/show-update-notification.sample
- omarchy/hooks/pre-refresh-pacman.d/add-custom-repo.sample
- omarchy/hooks/theme-set.d/show-theme-notification.sample
- omarchy/shell.json
- omarchy/shell.toml
- omarchy/theme.name
- omarchy/themes/azure-glow/README.md
- omarchy/themes/azure-glow/alacritty.toml
- omarchy/themes/azure-glow/btop.theme
- omarchy/themes/azure-glow/hyprland.conf
- omarchy/themes/azure-glow/hyprlock.conf
- omarchy/themes/azure-glow/icons.theme
- … 269 more
2026-09-23 15:19:12 +07:00

253 lines
11 KiB
Python

"""Tests for instruments.py — ranking, the bench, the merged view and the equal-weight fold.
Run: python3 -m unittest discover -s test
"""
import sys
import time
import unittest
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from nexthopd import score # noqa: E402
from nexthopd.instruments import Bench, MergedSeries, penalty # noqa: E402
from nexthopd.probes import Series # noqa: E402
from support import _st # noqa: E402
class InstrumentRanking(unittest.TestCase):
def test_loss_dominates_then_spread_then_median(self):
clean = penalty(_st(loss=0.0, p50=20, p95=30))
lossy = penalty(_st(loss=0.05, p50=10, p95=12))
wobbly = penalty(_st(loss=0.0, p50=20, p95=200))
slower = penalty(_st(loss=0.0, p50=60, p95=70))
self.assertLess(clean, lossy) # 5% loss loses to 10 ms spread
self.assertLess(clean, wobbly) # tail spread beats nothing
self.assertLess(clean, slower) # median only as tiebreak
self.assertLess(slower, wobbly) # 40 ms slower < 170 ms wobblier
def test_too_few_samples_judge_nothing(self):
self.assertIsNone(penalty(_st(count=Bench.MIN_SAMPLES - 1)))
self.assertIsNone(penalty(None))
self.assertIsNone(penalty({}))
def test_full_loss_is_dead_not_slow(self):
dead = penalty({"count": 60, "loss": 1.0, "p50": None, "p95": None})
self.assertGreaterEqual(dead, Bench.DEAD_AT)
class BenchSeats(unittest.TestCase):
POOL = [("icmp-a", "icmp", "1.1.1.1"), ("tcp-a", "tcp", "1.1.1.1:443"),
("tcp-b", "tcp", "cf:443"), ("tcp-c", "tcp", "google:443")]
def bench(self):
return Bench(self.POOL)
def keys(self, b):
return sorted(i.key for i in b.actives())
def test_the_opening_pair_spans_two_hosts(self):
"""Not `pool[:2]`, which was one host wearing two protocols.
`icmp-a` and `tcp-a` both point at 1.1.1.1, so seating both put every
scored instrument on one address before any evidence existed. On a
network that blocks it, that is a false outage at 4 s and a desktop
notification at 5 s on every daemon start, with no way back until the
bench's next pass a minute later (#5).
"""
self.assertEqual(self.keys(self.bench()), ["icmp-a", "tcp-b"])
def test_a_blocked_anchor_still_leaves_a_working_seat(self):
"""The property the pairing exists for.
One live instrument is enough: the leg answers if either does, so no
outage is declared while the bench re-ranks.
"""
b = self.bench()
hosts = {i.target.rpartition(":")[0] or i.target for i in b.actives()}
self.assertEqual(len(hosts), 2)
anchor_seats = [i for i in b.actives() if i.target.startswith("1.1.1.1")]
self.assertEqual(len(anchor_seats), 1)
def test_a_pool_with_one_host_still_fills_both_seats(self):
# Fewer scored instruments than the bench expects is the worse
# failure, so a pool that cannot offer two hosts falls back to order.
b = Bench([("icmp-a", "icmp", "1.1.1.1"), ("tcp-a", "tcp", "1.1.1.1:443")])
self.assertEqual(self.keys(b), ["icmp-a", "tcp-a"])
def test_dead_seat_is_replaced_immediately(self):
b = self.bench()
stats = {"icmp-a": _st(), "tcp-b": _st(loss=1.0, p50=None, p95=None),
"tcp-a": _st(p50=25, p95=35), "tcp-c": _st(p50=40, p95=60)}
changes = b.evaluate(1000.0, stats)
self.assertEqual(sorted(changes), [("tcp-a", True), ("tcp-b", False)])
self.assertEqual(self.keys(b), ["icmp-a", "tcp-a"])
def test_no_churn_during_a_full_outage(self):
b = self.bench()
dead = _st(loss=1.0, p50=None, p95=None)
stats = {k: dict(dead) for k in ("icmp-a", "tcp-a", "tcp-b", "tcp-c")}
self.assertEqual(b.evaluate(1000.0, stats), [])
self.assertEqual(self.keys(b), ["icmp-a", "tcp-b"])
def test_challenger_needs_two_consecutive_clear_wins(self):
b = self.bench()
# tcp-a is 20%+ better than the worst seat; one win is not enough.
stats = {"icmp-a": _st(p50=10, p95=14), "tcp-b": _st(p50=100, p95=160),
"tcp-a": _st(p50=20, p95=24), "tcp-c": _st(p50=90, p95=150)}
self.assertEqual(b.evaluate(1000.0, stats), [])
changes = b.evaluate(1000.0 + Bench.RESELECT_EVERY_S, stats)
self.assertEqual(sorted(changes), [("tcp-a", True), ("tcp-b", False)])
self.assertEqual(self.keys(b), ["icmp-a", "tcp-a"])
def test_a_win_streak_broken_starts_over(self):
b = self.bench()
better = {"icmp-a": _st(p50=10, p95=14), "tcp-b": _st(p50=100, p95=160),
"tcp-a": _st(p50=20, p95=24), "tcp-c": _st(p50=90, p95=150)}
level = {"icmp-a": _st(p50=10, p95=14), "tcp-b": _st(p50=19, p95=24),
"tcp-a": _st(p50=20, p95=24), "tcp-c": _st(p50=90, p95=150)}
t = 1000.0
self.assertEqual(b.evaluate(t, better), [])
self.assertEqual(b.evaluate(t + 300, level), []) # streak broken
self.assertEqual(b.evaluate(t + 600, better), []) # back to one win
self.assertEqual(self.keys(b), ["icmp-a", "tcp-b"])
def test_flapping_instrument_is_quarantined(self):
b = self.bench()
inst = b.instruments["tcp-a"]
t = 1000.0
# Three seat changes inside an hour is a flap.
for _, active in (("tcp-a", True), ("tcp-a", False), ("tcp-a", True)):
b._seat(inst, t, active)
t += 60
self.assertGreater(inst.quarantined_until, t)
# While quarantined it cannot be promoted, even over a corpse.
b._seat(inst, t, False)
stats = {"icmp-a": _st(), "tcp-b": _st(loss=1.0, p50=None, p95=None),
"tcp-a": _st(p50=5, p95=6), "tcp-c": _st(p50=40, p95=60)}
b.evaluate(t + 60, stats)
self.assertEqual(self.keys(b), ["icmp-a", "tcp-c"])
def test_snapshot_names_the_seats(self):
b = self.bench()
snap = b.snapshot(1000.0, {"icmp-a": _st(p50=7)})
by_key = {row["key"]: row for row in snap}
self.assertTrue(by_key["icmp-a"]["active"])
self.assertEqual(by_key["icmp-a"]["p50"], 7)
self.assertFalse(by_key["tcp-c"]["active"])
self.assertEqual(len(snap), 4)
class MergedView(unittest.TestCase):
def test_merges_and_orders_the_seated_series(self):
from nexthopd.probes import Series
a, c = Series(), Series()
now = time.time()
a.add(now - 3, 10.0)
c.add(now - 2, None)
a.add(now - 1, 12.0)
m = MergedSeries(lambda: [a, c])
self.assertEqual([s[1] for s in m.since(10)], [10.0, None, 12.0])
self.assertEqual(len(m.all()), 3)
m2 = MergedSeries(lambda: [a])
self.assertEqual(len(m2.since(10)), 2)
class EqualWeightMerge(unittest.TestCase):
"""The scored internet leg is two instruments. Each must count once
whatever its cadence, and jitter must never be measured across them."""
@staticmethod
def stream(value_fn, interval, span=30.0, offset=0.0, now=None):
now = now or time.time()
s = Series()
t, i = now - span + offset, 0
while t <= now:
s.add(t, value_fn(i), False)
t += interval
i += 1
return s
def test_cadence_cannot_move_the_statistics(self):
from nexthopd.instruments import merged_stats
now = time.time()
merged, pooled = set(), set()
for icmp_iv in (0.2, 0.5, 1.0):
a = self.stream(lambda i: 5.0, icmp_iv, now=now)
b = self.stream(lambda i: 15.0, 1.0, offset=0.25, now=now)
st = merged_stats([a.since(30), b.since(30)])
merged.add((st["p50"], st["p75"], st["p95"], st["jitter"], st["loss"]))
# The pooled stream, which is what used to be scored.
old = Series.stats(MergedSeries(lambda: [a, b]).since(30))
pooled.add((old["p75"], old["jitter"]))
self.assertEqual(len(merged), 1, merged)
self.assertEqual(merged.pop(), (5.0, 15.0, 15.0, 0.0, 0.0))
# ...whereas the setting alone used to move p75 and jitter.
self.assertGreater(len(pooled), 1, pooled)
def test_jitter_never_crosses_instruments(self):
from nexthopd.instruments import merged_stats
now = time.time()
steady_a = self.stream(lambda i: 5.0, 0.5, now=now)
steady_b = self.stream(lambda i: 15.0, 1.0, offset=0.25, now=now)
self.assertEqual(merged_stats([steady_a.since(30),
steady_b.since(30)])["jitter"], 0.0)
# Two streams with no jitter read as jittery when pooled: the defect.
self.assertGreater(Series.stats(
MergedSeries(lambda: [steady_a, steady_b]).since(30))["jitter"], 5.0)
# An instrument that really alternates contributes its own IPDV,
# averaged with the steady one's zero.
swinging = self.stream(lambda i: 10.0 if i % 2 else 40.0, 1.0, now=now)
st = merged_stats([swinging.since(30), steady_a.since(30)])
self.assertEqual(st["jitter"], 15.0)
def test_loss_is_the_mean_of_the_instruments_loss_rates(self):
from nexthopd.instruments import merged_stats
now = time.time()
lossy = self.stream(lambda i: None if i % 10 == 0 else 5.0, 0.5, now=now)
clean = self.stream(lambda i: 15.0, 1.0, offset=0.25, now=now)
a, b = lossy.since(30), clean.since(30)
a_loss = sum(1 for s in a if s[1] is None) / len(a)
st = merged_stats([a, b])
self.assertAlmostEqual(st["loss"], a_loss / 2, places=9)
# Not the count-weighted figure the pool gave, which the faster
# instrument dominated.
self.assertNotAlmostEqual(st["loss"], a_loss * len(a) / (len(a) + len(b)),
places=3)
def test_one_instrument_is_series_stats_exactly(self):
from nexthopd.instruments import merged_stats
a = self.stream(lambda i: 5.0 + (i % 3), 0.5)
self.assertEqual(merged_stats([a.since(30)]), Series.stats(a.since(30)))
# A seated instrument with nothing yet does not dilute the other.
self.assertEqual(merged_stats([a.since(30), []]), Series.stats(a.since(30)))
self.assertEqual(merged_stats([])["count"], 0)
def test_this_line_as_built(self):
"""ICMP 3.41 ms at 500 ms and TCP 4.82 ms at 1 s — live.json's own
figures on 2026-09-08. Two stable instruments: no jitter, and Lag
is the slower instrument's round trip."""
from nexthopd.instruments import merged_stats
now = time.time()
a = self.stream(lambda i: 3.41, 0.5, now=now)
b = self.stream(lambda i: 4.82, 1.0, offset=0.25, now=now)
# lag_ms rounds to a tenth: the slower instrument's 4.82 and nothing added.
self.assertEqual(score.lag_ms(merged_stats([a.since(30), b.since(30)])), 4.8)
# The pool manufactured 0.93 ms of jitter and 1.4 ms of Lag.
old = score.lag_ms(Series.stats(MergedSeries(lambda: [a, b]).since(30)))
self.assertAlmostEqual(old, 6.2, delta=0.1)
def test_merged_series_stats_is_the_equal_weight_fold(self):
from nexthopd.instruments import merged_stats
a = self.stream(lambda i: 5.0, 0.5)
b = self.stream(lambda i: 15.0, 1.0, offset=0.25)
m = MergedSeries(lambda: [a, b])
self.assertEqual(m.stats(30), merged_stats([a.since(30), b.since(30)]))
self.assertEqual(len(m.each(30)), 2)
self.assertEqual(len(m.each()), 2)
if __name__ == "__main__":
unittest.main()