"""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 from bec_lib.macro_update_handler import has_executable_code 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[]." ) @pytest.mark.parametrize("macro", MACROS, ids=lambda p: p.name) def test_macros_are_loadable_by_the_macro_loader(macro: pathlib.Path): """A module-level statement makes the loader refuse the whole file, silently. bec_lib.macro_update_handler.has_executable_code rejects any module-level node that is not an import, a def, a class, an annotated assignment or a docstring -- a plain `logger = bec_logger.logger` is enough. The macro is then simply not loaded, and the only trace is a WARNING in the log stream, so a broken macro looks like a macro that was never there. The real checker is used here rather than a re-implementation. """ blocked, lineno = has_executable_code(macro.read_text()) assert not blocked, ( f"{macro.name}: module-level executable code at line {lineno}; the macro loader " "will refuse the file. Move it inside a function, or use an annotated assignment." )