Files
FabledSteward/tests/plugins/host_agent/test_sensors.py
T
bvandeusenandClaude Opus 5.5 ec13f3797d
CI / lint (push) Successful in 3s
CI / integration (push) Failing after 1m23s
CI / unit (push) Successful in 1m31s
CI / publish (push) Skipped
perf(host_agent): the sensor judgment reads the week once, then only new hours (#5786)
On the live instance the hourly refresh took 3 min 8 s, averaging a week of
raw temp_c samples each time. The hourly averages are now kept in memory.
The first refresh after startup reads the whole window. Each later one reads
raw samples from the start of the hour the last read ended in, so the hour
that was still filling is read again whole. merge_hours folds the new rows
in and drops hours older than the window.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 21:42:11 -04:00

85 lines
4.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Which temperature sensors are thermometers, judged from how each moves
against the host's CPU (#5783). VDNT-Util01's SYSTIN ran at r = −0.81
against its CPU package over a week; CPUTIN beside it at +0.83 (#5782)."""
import math
from datetime import datetime, timedelta, timezone
from plugins.host_agent import sensors as s
T0 = datetime(2026, 10, 8, tzinfo=timezone.utc)
def _hours(n, f):
"""{hour: f(i)} for n hourly buckets."""
return {T0 + timedelta(hours=i): f(i) for i in range(n)}
def cpu(i): # a CPU that swings 50-80 °C through the day
return 65 + 15 * math.sin(i / 24 * 2 * math.pi)
def test_a_sensor_that_falls_as_the_cpu_warms_is_not_a_thermometer():
ref = _hours(96, cpu)
systin = _hours(96, lambda i: 114 - 0.1 * (cpu(i) - 65)) # Util01's shape
why = s.judge(ref, systin)
assert why.startswith("falls as the CPU warms (r -1.00 over 96 h")
def test_a_sensor_that_holds_still_is_not_a_thermometer():
why = s.judge(_hours(96, cpu), _hours(96, lambda i: 110.0 + (0.1 if i % 2 else 0.0)))
assert why == "holds at 110 °C while the CPU moved 50–80 °C"
def test_a_wired_sensor_is_left_alone_even_when_it_barely_follows_the_cpu():
ref = _hours(96, cpu)
assert s.judge(ref, _hours(96, lambda i: 40 + 0.3 * (cpu(i) - 65))) is None # a board sensor
assert s.judge(ref, _hours(96, lambda i: 45 + 3 * math.sin(i))) is None # a drive, its own rhythm
def test_nothing_is_judged_without_enough_history_or_a_cpu_that_moved():
falls = lambda i: 114 - 0.1 * (cpu(i) - 65) # noqa: E731
assert s.judge(_hours(48, cpu), _hours(48, falls)) is None # 48 h < MIN_HOURS
idle = _hours(96, lambda i: 50 + (i % 3)) # moved 2 °C
assert s.judge(idle, _hours(96, lambda i: 110 - (i % 3))) is None
def test_the_cpu_reference_is_found_by_its_driver_names():
assert s.reference_label(["SYSTIN", "Core 0", "Package id 1", "Package id 0"]) == "Package id 0"
assert s.reference_label(["Tccd1", "Tctl", "Composite"]) == "Tctl"
assert s.reference_label(["PECI Agent 0", "SYSTIN"]) == "PECI Agent 0"
assert s.reference_label(["Composite", "temp1"]) is None
def test_hosts_are_judged_from_hourly_rows_and_a_host_without_a_cpu_reading_is_skipped():
rows = []
for i in range(96):
h = T0 + timedelta(hours=i)
rows += [("util:temp:Package id 0", h, cpu(i)),
("util:temp:SYSTIN", h, 114 - 0.1 * (cpu(i) - 65)),
("util:temp:CPUTIN", h, 40 + 0.3 * (cpu(i) - 65)),
("nas:temp:temp1", h, 110 - 0.1 * (cpu(i) - 65)),
("util", h, 1.0)] # not a sensor row
got = s.judge_hosts(rows)
assert list(got) == ["util"] and list(got["util"]) == ["SYSTIN"]
def test_a_judged_sensor_is_left_out_of_the_host_temperature_and_listed_with_why(monkeypatch):
from plugins.host_agent.routes import is_thermometer, split_temps
monkeypatch.setattr(s, "_unwired", {"util": {"temp3": "falls as the CPU warms (r -0.81 over 168 h)"}})
assert not is_thermometer("util", "temp3", 62.0) # a plausible value, still not a thermometer
assert is_thermometer("other", "temp3", 62.0)
real, unwired = split_temps("util", {"temp3": 62.0, "Package id 0": 55.0})
assert list(real) == ["Package id 0"]
assert unwired == {"temp3": (62.0, "falls as the CPU warms (r -0.81 over 168 h)")}
def test_a_later_read_replaces_the_hour_that_was_filling_and_old_hours_drop():
hours = {"h:temp:a": {T0: 50.0, T0 + timedelta(hours=1): 51.0}}
s.merge_hours(hours, [("h:temp:a", T0 + timedelta(hours=1), 55.0), # the hour, read whole
("h:temp:a", T0 + timedelta(hours=2), 56.0),
("h:temp:b", T0 + timedelta(hours=2), None)], since=T0 + timedelta(hours=1))
assert hours == {"h:temp:a": {T0 + timedelta(hours=1): 55.0, T0 + timedelta(hours=2): 56.0}}
s.merge_hours(hours, [], since=T0 + timedelta(hours=9)) # a sensor gone quiet
assert hours == {}