fix(omny-shared): filters_out_of_beam tolerates a missing/None dev
CI for csaxs_bec / test (push) Successful in 2m31s
CI for csaxs_bec / test (push) Successful in 2m31s
Surfaced live on a simulated flomni session: tomo_scan_projection()
crashed with "DeviceConfigError: Device filter_array_1_x does not
exist" -- the real beamline's four filter_array_*_x devices
(bl_endstation.yaml) aren't part of any endstation simulation config,
by design (out of scope for an isolated endstation sim, same as
frontend/optics-hutch/detector devices).
filters_out_of_beam() already tried to skip a missing device via
getattr(dev, axis_name, None), but BEC's device container raises
DeviceConfigError -- a plain Exception, not AttributeError -- for a
missing device, so getattr's default never applied. Wrap the lookup
in try/except instead, which also preserves the pre-existing (and
apparently relied-upon, per several LamNI tests) behavior of `dev`
itself being None -- the usual state of this codebase's
`dev = builtins.__dict__.get("dev")` idiom before a real session sets
it.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lx3KffiFyyDMKT8vvPENUW
This commit is contained in:
1 parent
0690fbb391
commit
d237357012
2 files changed
+94
-1
No files matched your search
@@ -9,12 +9,24 @@ def filters_out_of_beam(dev, tol: float = 0.1) -> tuple[bool, list[str]]:
|
||||
Reuses cSAXSFilterTransmission's own axis list/position table (index 0 of
|
||||
each row is the "out" position) so there is one source of truth for what
|
||||
"out" means, matching csaxs.fil_trans()/_fil_trans_report()'s own check.
|
||||
|
||||
Tolerates a deployment missing one or more filter axes (e.g. a simulated
|
||||
session without the real beamline's filter_array_* devices), and `dev`
|
||||
itself being None (the usual state of the module-level `dev =
|
||||
builtins.__dict__.get("dev")` idiom used throughout this codebase before
|
||||
a real BEC session has set it). BEC's device container raises
|
||||
DeviceConfigError -- not AttributeError -- for a missing device, so a
|
||||
plain `getattr(dev, axis_name, None)` does NOT safely skip it; wrapping
|
||||
the lookup itself in try/except does, for both cases.
|
||||
"""
|
||||
offending = []
|
||||
for axis_name, positions in zip(
|
||||
cSAXSFilterTransmission._AXES, cSAXSFilterTransmission._POSITIONS_USER
|
||||
):
|
||||
axis_obj = getattr(dev, axis_name, None)
|
||||
try:
|
||||
axis_obj = getattr(dev, axis_name, None)
|
||||
except Exception:
|
||||
axis_obj = None
|
||||
out_position = positions[0]
|
||||
if axis_obj is None or out_position is None:
|
||||
continue
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
"""Tests for filters_out_of_beam() (csaxs_bec/bec_ipython_client/plugins/OMNY_shared/filter_check.py).
|
||||
|
||||
Regression coverage for a bug surfaced on a simulated flomni session:
|
||||
tomo_scan_projection() -> filters_out_of_beam() crashed with
|
||||
DeviceConfigError: Device filter_array_1_x does not exist, because the sim
|
||||
config (unlike the real beamline's bl_endstation.yaml) doesn't define the
|
||||
filter_array_*_x devices at all. The lookup used `getattr(dev, axis_name,
|
||||
None)`, but BEC's device container raises DeviceConfigError (a plain
|
||||
Exception, not AttributeError) for a missing device, so getattr's default
|
||||
never applied.
|
||||
"""
|
||||
|
||||
from unittest import mock
|
||||
|
||||
from bec_lib.bec_errors import DeviceConfigError
|
||||
|
||||
from csaxs_bec.bec_ipython_client.plugins.OMNY_shared.filter_check import filters_out_of_beam
|
||||
|
||||
|
||||
class _FakeDeviceContainer(dict):
|
||||
"""Stands in for BEC's real device-manager container: attribute access
|
||||
for a present device returns it, for a missing one raises
|
||||
DeviceConfigError (not AttributeError) -- see devicemanager.py's
|
||||
__getattr__."""
|
||||
|
||||
def __getattr__(self, attr):
|
||||
dev = self.get(attr)
|
||||
if not dev:
|
||||
raise DeviceConfigError(f"Device {attr} does not exist.")
|
||||
return dev
|
||||
|
||||
|
||||
def _axis_at(position: float):
|
||||
axis = mock.MagicMock()
|
||||
axis.readback.get.return_value = position
|
||||
return axis
|
||||
|
||||
|
||||
def test_filters_out_of_beam_dev_is_none():
|
||||
# The usual state of the module-level `dev` idiom before a real BEC
|
||||
# session has set it (`dev = builtins.__dict__.get("dev")`) -- several
|
||||
# LamNI/Flomni call sites pass this straight through. Must not raise.
|
||||
all_out, offending = filters_out_of_beam(None)
|
||||
|
||||
assert all_out is True
|
||||
assert offending == []
|
||||
|
||||
|
||||
def test_filters_out_of_beam_all_devices_missing_from_container():
|
||||
# Mirrors a simulated deployment: none of the filter_array_*_x devices
|
||||
# exist at all. Must not raise, and must report "all out" (nothing to
|
||||
# check -> vacuously true), matching the existing axis_obj-is-None skip.
|
||||
dev = _FakeDeviceContainer()
|
||||
|
||||
all_out, offending = filters_out_of_beam(dev)
|
||||
|
||||
assert all_out is True
|
||||
assert offending == []
|
||||
|
||||
|
||||
def test_filters_out_of_beam_some_devices_missing_from_container():
|
||||
# A mixed deployment: filter_array_1_x exists and is out of the beam,
|
||||
# the other three axes don't exist at all.
|
||||
dev = _FakeDeviceContainer(filter_array_1_x=_axis_at(25.0))
|
||||
|
||||
all_out, offending = filters_out_of_beam(dev)
|
||||
|
||||
assert all_out is True
|
||||
assert offending == []
|
||||
|
||||
|
||||
def test_filters_out_of_beam_flags_axis_in_beam():
|
||||
dev = _FakeDeviceContainer(
|
||||
filter_array_1_x=_axis_at(25.0), # out
|
||||
filter_array_2_x=_axis_at(17.6), # NOT out (out position is 25.5)
|
||||
)
|
||||
|
||||
all_out, offending = filters_out_of_beam(dev)
|
||||
|
||||
assert all_out is False
|
||||
assert offending == ["filter_array_2_x"]
|
||||
Reference in new issue
Block a user