Files
debye_bec/tests/tests_devices/test_acs_signal.py
T

109 lines
3.4 KiB
Python

"""Tests for the AcsSignal config-read cache and change-only value callbacks."""
import time
from unittest import mock
import numpy as np
import pytest
from ophyd import Component as Cpt
from ophyd import Device
from debye_bec.devices.mo1_bragg.acs import AcsSignal
# pylint: disable=protected-access
class _AcsDevice(Device):
"""Minimal host device providing the controller attribute AcsSignal expects."""
cached = Cpt(AcsSignal, tag=100, prec=3, kind="config", cache_ttl=30.0)
uncached = Cpt(AcsSignal, tag=200, prec=3, kind="config")
array = Cpt(AcsSignal, tag=300, prec=3, num_el=3, kind="config", cache_ttl=30.0)
def __init__(self, *args, controller=None, **kwargs):
self.controller = controller
super().__init__(*args, **kwargs)
@pytest.fixture
def acs_device():
controller = mock.MagicMock()
controller.get_var.return_value = 1.5
yield _AcsDevice(name="acs", controller=controller)
def test_cached_get_reads_hardware_once(acs_device):
assert acs_device.cached.get() == 1.5
assert acs_device.cached.get() == 1.5
acs_device.controller.get_var.assert_called_once_with(100, 3)
def test_uncached_get_reads_hardware_every_time(acs_device):
acs_device.uncached.get()
acs_device.uncached.get()
assert acs_device.controller.get_var.call_count == 2
def test_cache_expires_after_ttl(acs_device):
acs_device.cached.cache_ttl = 0.05
acs_device.cached.get()
time.sleep(0.06)
acs_device.cached.get()
assert acs_device.controller.get_var.call_count == 2
def test_put_invalidates_cache(acs_device):
acs_device.cached.get()
acs_device.cached.put(2.0)
acs_device.controller.set_var.assert_called_once_with(100, 2.0, 3)
acs_device.controller.get_var.return_value = 2.0
assert acs_device.cached.get() == 2.0
assert acs_device.controller.get_var.call_count == 2
def test_invalidate_cache_forces_fresh_read(acs_device):
acs_device.cached.get()
acs_device.cached.invalidate_cache()
acs_device.cached.get()
assert acs_device.controller.get_var.call_count == 2
def test_value_callbacks_fire_only_on_change(acs_device):
events = []
acs_device.cached.subscribe(lambda **kwargs: events.append(kwargs), run=False)
acs_device.cached.get() # None -> 1.5: fires
assert len(events) == 1
acs_device.cached.invalidate_cache()
acs_device.cached.get() # 1.5 -> 1.5: hardware read, but no callback
assert acs_device.controller.get_var.call_count == 2
assert len(events) == 1
acs_device.controller.get_var.return_value = 2.5
acs_device.cached.invalidate_cache()
acs_device.cached.get() # 1.5 -> 2.5: fires
assert len(events) == 2
assert events[-1]["value"] == 2.5
def test_array_signal_caches_and_compares_by_content(acs_device):
acs_device.controller.get_var.side_effect = lambda tag, prec, idx=None: float(idx)
events = []
acs_device.array.subscribe(lambda **kwargs: events.append(kwargs), run=False)
assert np.array_equal(acs_device.array.get(), np.array([0.0, 1.0, 2.0]))
assert acs_device.controller.get_var.call_count == 3 # one call per element
assert len(events) == 1
# cache hit: no additional hardware reads
acs_device.array.get()
assert acs_device.controller.get_var.call_count == 3
# fresh read with identical content: no callback
acs_device.array.invalidate_cache()
acs_device.array.get()
assert acs_device.controller.get_var.call_count == 6
assert len(events) == 1