fix(omny-shared): filters_out_of_beam tolerates a missing/None dev
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:
x01dcandClaude Sonnet 5 committed 2026-09-17 14:22:29 +02:00
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"]