114 lines
3.9 KiB
Python
114 lines
3.9 KiB
Python
"""MasterEventSystem._get_slot_codes: an unconfigured slot is not a failure.
|
|
|
|
Measured on SIN-TIMAST-TMA (2026-09-06): exactly 74 of 256 event slots
|
|
exist. The other 182 have no record on the IOC - their PVs do not connect
|
|
even given 5 s on an idle network, and the count is identical inside and
|
|
outside `init_all()`. The old code could not tell that apart from a
|
|
timed-out read, so it retried all 182 three times (~15 s of every namespace
|
|
init) and then warned alarmingly about a completely normal configuration.
|
|
"""
|
|
|
|
import logging
|
|
import types
|
|
|
|
import pytest
|
|
|
|
evt = pytest.importorskip("eco.timing.event_timing_new_new")
|
|
|
|
|
|
class ProbePV:
|
|
"""A PV that connects only if its slot is in `configured`."""
|
|
|
|
configured = set()
|
|
|
|
def __init__(self, pvname, connection_timeout=None, auto_monitor=None):
|
|
self.pvname = pvname
|
|
slot = int(pvname.split("Evt-")[1].split("-")[0])
|
|
self.connected = slot in ProbePV.configured
|
|
|
|
|
|
@pytest.fixture
|
|
def master(monkeypatch):
|
|
m = evt.MasterEventSystem.__new__(evt.MasterEventSystem)
|
|
m.pvname = "TEST-TMA"
|
|
monkeypatch.setattr(evt, "PV", ProbePV)
|
|
monkeypatch.setattr(evt.time, "sleep", lambda s: None)
|
|
return m
|
|
|
|
|
|
def _install_caget(monkeypatch, table, flaky=()):
|
|
"""table: slot -> code (absent slot => None). `flaky` slots return None
|
|
on the first read and their code afterwards."""
|
|
seen = {"calls": 0}
|
|
state = {s: 0 for s in flaky}
|
|
|
|
def caget_many(pvs, timeout=None):
|
|
seen["calls"] += 1
|
|
out = []
|
|
for pv in pvs:
|
|
slot = int(pv.split("Evt-")[1].split("-")[0])
|
|
if slot in state:
|
|
state[slot] += 1
|
|
out.append(table.get(slot) if state[slot] > 1 else None)
|
|
else:
|
|
out.append(table.get(slot))
|
|
return out
|
|
|
|
monkeypatch.setattr(evt, "caget_many", caget_many)
|
|
return seen
|
|
|
|
|
|
def test_unconfigured_slots_are_skipped_without_retry_or_warning(
|
|
master, monkeypatch, caplog
|
|
):
|
|
table = {1: 10, 2: 20, 3: 30} # only 3 of 6 exist
|
|
ProbePV.configured = set(table)
|
|
seen = _install_caget(monkeypatch, table)
|
|
|
|
with caplog.at_level(logging.DEBUG, logger=evt.__name__):
|
|
slots, codes = master._get_slot_codes(slots=range(1, 7))
|
|
|
|
assert sorted(codes) == [10, 20, 30]
|
|
assert seen["calls"] == 1, "non-existent slots must not be retried"
|
|
text = caplog.text
|
|
assert "not configured" in text and "3 in use" in text
|
|
assert "could not be read" not in text, "that would be the false alarm"
|
|
|
|
|
|
def test_a_connected_but_unread_slot_is_retried_and_warned_about(
|
|
master, monkeypatch, caplog
|
|
):
|
|
"""The case the retry and the warning actually exist for."""
|
|
table = {1: 10, 2: 20}
|
|
ProbePV.configured = {1, 2, 3} # slot 3 exists but never reads
|
|
seen = _install_caget(monkeypatch, table)
|
|
|
|
with caplog.at_level(logging.INFO):
|
|
slots, codes = master._get_slot_codes(slots=range(1, 4), attempts=3)
|
|
|
|
assert sorted(codes) == [10, 20]
|
|
assert seen["calls"] == 3, "a connected slot should be retried"
|
|
assert "connected but" in caplog.text and "could not be read" in caplog.text
|
|
|
|
|
|
def test_a_transient_read_recovers_on_retry(master, monkeypatch, caplog):
|
|
table = {1: 10, 2: 20, 3: 30}
|
|
ProbePV.configured = set(table)
|
|
seen = _install_caget(monkeypatch, table, flaky=[3])
|
|
|
|
with caplog.at_level(logging.INFO):
|
|
slots, codes = master._get_slot_codes(slots=range(1, 4), attempts=3)
|
|
|
|
assert sorted(codes) == [10, 20, 30], "the retry should have recovered slot 3"
|
|
assert seen["calls"] == 2
|
|
assert "could not be read" not in caplog.text
|
|
|
|
|
|
def test_everything_present_needs_no_probing_at_all(master, monkeypatch):
|
|
table = {1: 10, 2: 20, 3: 30}
|
|
ProbePV.configured = set(table)
|
|
seen = _install_caget(monkeypatch, table)
|
|
slots, codes = master._get_slot_codes(slots=range(1, 4))
|
|
assert seen["calls"] == 1
|
|
assert sorted(codes) == [10, 20, 30]
|