"""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