From ce3c31ccb4e1da021c1743faacf98969bda0fd8d Mon Sep 17 00:00:00 2001 From: menzel Date: Fri, 4 Sep 2026 18:50:15 +0200 Subject: [PATCH] fix(macros): look devices up in dev, not dev.devices The tensor-tomography branch of run_cont_grid_scan_for_table_row has never worked. `dev` is already the device container -- bec_lib binds dev = device_manager.devices in the client namespace -- so dev.devices[name] asks DeviceContainer for a device literally called "devices" and its __getattr__ raises DeviceConfigError before any motor moves. Observed at the beamline on a tensor table using sgchi/sgphi as the rotation axes: --> 72 roty_motor = dev.devices[row["roty_axis"]] DeviceConfigError: Device devices does not exist. The failure is deterministic, so @scan_repeat(max_repeats=3, default=True) retried it three times, and the surrounding except reported only "Error while moving motors to starting position for sample ..." -- the traceback goes to SciLog and nowhere else, which is why this survived undetected. Adds tests/tests_macros, which had no equivalent: macros run in the client namespace and are not executed by any test, so mistakes in them reach the beamline unfiltered. The check parses each macro and flags dev.devices attribute access rather than matching text, so comments and docstrings that mention the pattern do not trip it. Verified to fail against the unfixed macro. The rotation axes themselves are not hard-coded: rotx_axis/roty_axis are row fields holding a device name chosen from the SAXS widget's positioner combo boxes. Only the four row keys are fixed. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_0148tn6uK6oiTH25mzLfJcyc --- .../run_cont_grid_scan_for_table_row.py | 7 ++- .../tests_macros/test_macro_device_access.py | 44 +++++++++++++++++++ 2 files changed, 49 insertions(+), 2 deletions(-) create mode 100644 tests/tests_macros/test_macro_device_access.py diff --git a/csaxs_bec/macros/run_cont_grid_scan_for_table_row.py b/csaxs_bec/macros/run_cont_grid_scan_for_table_row.py index cdfbd530..ce19b5a3 100644 --- a/csaxs_bec/macros/run_cont_grid_scan_for_table_row.py +++ b/csaxs_bec/macros/run_cont_grid_scan_for_table_row.py @@ -69,11 +69,14 @@ def run_cont_grid_scan_for_table_row( try: # Move motors to the starting position of the scan move_command = [] - roty_motor = dev.devices[row["roty_axis"]] + # NOTE `dev` IS the device container (bec_lib client binds + # dev = device_manager.devices), so `dev.devices[...]` asks the container for a + # device named "devices" and raises DeviceConfigError before any motor moves. + roty_motor = dev[row["roty_axis"]] setpoint_roty = roty_motor.setpoint.get(cached=True) if not np.isclose(setpoint_roty, row["roty"]): move_command.extend([row["roty_axis"], row["roty"]]) - setpoint_rotx = dev.devices[row["rotx_axis"]].setpoint.get(cached=True) + setpoint_rotx = dev[row["rotx_axis"]].setpoint.get(cached=True) if not np.isclose(setpoint_rotx, row["rotx"]): move_command.extend([row["rotx_axis"], row["rotx"]]) # Wait for all motor movements to complete diff --git a/tests/tests_macros/test_macro_device_access.py b/tests/tests_macros/test_macro_device_access.py new file mode 100644 index 00000000..f46dcc60 --- /dev/null +++ b/tests/tests_macros/test_macro_device_access.py @@ -0,0 +1,44 @@ +"""Static checks on the user macros. + +Macros run in the BEC client namespace and are not covered by unit tests, so mistakes in +them surface only at the beamline -- and, because several macros wrap their body in a +try/except that reports to SciLog, often only as a generic message with the traceback +discarded. These checks catch the failure modes that are decidable statically. +""" + +import ast +import pathlib + +import pytest + +MACRO_DIR = pathlib.Path(__file__).resolve().parents[2] / "csaxs_bec" / "macros" +MACROS = sorted(p for p in MACRO_DIR.glob("*.py") if p.name != "__init__.py") + + +def test_macro_directory_is_found(): + """Guard against the glob silently matching nothing if the layout changes.""" + assert MACROS, f"no macros found under {MACRO_DIR}" + + +@pytest.mark.parametrize("macro", MACROS, ids=lambda p: p.name) +def test_macros_do_not_go_through_dev_devices(macro: pathlib.Path): + """`dev` is already the device container, so `dev.devices` cannot resolve. + + bec_lib binds `dev = device_manager.devices` in the client namespace, and + DeviceContainer.__getattr__ raises DeviceConfigError for an unknown attribute. So + `dev.devices[name]` asks for a device literally called "devices" and fails before + doing anything useful. Use `dev[name]`. + """ + tree = ast.parse(macro.read_text(), filename=str(macro)) + offenders = [ + node.lineno + for node in ast.walk(tree) + if isinstance(node, ast.Attribute) + and node.attr == "devices" + and isinstance(node.value, ast.Name) + and node.value.id == "dev" + ] + assert not offenders, ( + f"{macro.name}: line(s) {offenders} use dev.devices -- `dev` is the device " + "container itself, so this raises DeviceConfigError at runtime. Use dev[]." + )