Source code for id3c.tests.test_ad_plugins
"""Unit tests for :mod:`id3c.plans.ad_plugins`.
These tests use an ophyd ``Device`` with in-memory ``Signal``
components as a stand-in for an area-detector file plugin. No
EPICS PVs are required, so the tests run on the off-network
development host.
"""
import io
from contextlib import redirect_stdout
import pytest
from ophyd import Component as Cpt
from ophyd import Device
from ophyd import Signal
from id3c.plans.ad_plugins import _select_ad_plugin_keys
from id3c.plans.ad_plugins import read_ad_plugin_components
from id3c.plans.ad_plugins import set_ad_plugin_components
class _ReadOnlySignal(Signal):
"""A Signal that advertises itself as not writable."""
@property
def write_access(self):
"""Always False; mimics ``EpicsSignalRO``."""
return False
class _FakePlugin(Device):
"""Minimal stand-in for an area-detector file plugin.
Mix of kinds and access modes so the helpers' selection rules
can be exercised.
"""
cfg_a = Cpt(Signal, value=10, kind="config")
cfg_b = Cpt(Signal, value=20, kind="config")
normal_c = Cpt(Signal, value=30, kind="normal")
hinted_d = Cpt(Signal, value=40, kind="hinted")
omitted_e = Cpt(Signal, value=50, kind="omitted")
ro_f = Cpt(_ReadOnlySignal, value=60, kind="config")
@pytest.fixture
[docs]
def plugin():
"""Return a freshly constructed fake plugin."""
return _FakePlugin(name="plugin")
# --- _select_ad_plugin_keys ------------------------------------------------
[docs]
def test_select_skips_omitted(plugin):
"""``omitted`` kind components are not yielded."""
keys = list(_select_ad_plugin_keys(plugin))
assert "omitted_e" not in keys
[docs]
def test_select_includes_config_normal_hinted_readonly(plugin):
"""All other reportable kinds (including read-only) are yielded."""
keys = list(_select_ad_plugin_keys(plugin))
assert set(keys) == {"cfg_a", "cfg_b", "normal_c", "hinted_d", "ro_f"}
[docs]
def test_select_is_sorted(plugin):
"""Selection order is alphabetical for predictability."""
keys = list(_select_ad_plugin_keys(plugin))
assert keys == sorted(keys)
# --- read_ad_plugin_components --------------------------------------------
[docs]
def test_read_prints_table_with_expected_rows(plugin):
"""The diagnostic prints one row per reportable component."""
buf = io.StringIO()
with redirect_stdout(buf):
read_ad_plugin_components(plugin)
out = buf.getvalue()
# One row per reportable signal; omitted should be absent.
for name in ("cfg_a", "cfg_b", "normal_c", "hinted_d", "ro_f"):
assert name in out
assert "omitted_e" not in out
[docs]
def test_read_marks_readonly_access(plugin):
"""Read-only component shows ``R-``; read-write shows ``RW``."""
buf = io.StringIO()
with redirect_stdout(buf):
read_ad_plugin_components(plugin)
out = buf.getvalue()
# The ro_f row contains 'R-'; the cfg_a row contains 'RW'.
ro_line = next(line for line in out.splitlines() if "ro_f" in line)
rw_line = next(line for line in out.splitlines() if "cfg_a" in line)
assert "R-" in ro_line
assert "RW" in rw_line
# --- set_ad_plugin_components ---------------------------------------------
def _msgs(plan_stub):
"""Materialize a plan stub's yielded Msg sequence."""
return list(plan_stub)
[docs]
def test_set_emits_one_mv_per_call(plugin):
"""Multiple kwargs collapse to a single ``bps.mv`` invocation."""
msgs = _msgs(set_ad_plugin_components(plugin, cfg_a=1, cfg_b=2))
set_msgs = [m for m in msgs if m.command == "set"]
# bps.mv issues one set per signal plus a wait; both signals appear.
set_targets = {m.obj.attr_name for m in set_msgs}
assert set_targets == {"cfg_a", "cfg_b"}
[docs]
def test_set_values_propagate_through_messages(plugin):
"""Each setpoint appears in the corresponding ``Msg.args``."""
msgs = _msgs(set_ad_plugin_components(plugin, cfg_a=99))
set_msgs = [m for m in msgs if m.command == "set"]
assert len(set_msgs) == 1
assert set_msgs[0].args == (99,)
[docs]
def test_set_no_kwargs_yields_null_message(plugin):
"""An empty call still yields one message (so @plan does not warn)."""
msgs = _msgs(set_ad_plugin_components(plugin))
assert len(msgs) == 1
assert msgs[0].command == "null"
[docs]
def test_set_rejects_unknown_kwarg(plugin):
"""Unknown kwargs raise ``KeyError`` before any message is yielded."""
with pytest.raises(KeyError, match="bogus"):
_msgs(set_ad_plugin_components(plugin, bogus=1))
[docs]
def test_set_rejects_omitted_kwarg(plugin):
"""A kwarg naming an ``omitted``-kind component is unknown."""
with pytest.raises(KeyError, match="omitted_e"):
_msgs(set_ad_plugin_components(plugin, omitted_e=1))
[docs]
def test_set_rejects_readonly_kwarg(plugin):
"""A kwarg naming a read-only component raises ``TypeError``."""
with pytest.raises(TypeError, match="ro_f"):
_msgs(set_ad_plugin_components(plugin, ro_f=1))
[docs]
def test_set_reports_readonly_before_unknown(plugin):
"""Both errors at once: ``TypeError`` (read-only) wins."""
with pytest.raises(TypeError, match="ro_f"):
_msgs(set_ad_plugin_components(plugin, ro_f=1, bogus=2))