from unittest import mock from csaxs_bec.bec_ipython_client.plugins.cSAXS.slits import cSAXSSlits def _make_slits(devices: dict): client = mock.MagicMock() client.device_manager.devices = devices return cSAXSSlits(client) def test_sl_get_position_uses_cached_read(): dev = mock.MagicMock() dev.read.return_value = {"sl4xc": {"value": 1.25}} slits = _make_slits({"sl4xc": dev}) assert slits._sl_get_position("sl4xc") == 1.25 dev.read.assert_called_once_with(cached=True) def test_sl_get_position_returns_none_without_cached_value(): dev = mock.MagicMock() dev.read.return_value = None # nothing published to redis yet slits = _make_slits({"sl4xc": dev}) assert slits._sl_get_position("sl4xc") is None assert slits._sl_get_position("unknown_device") is None def test_slits_show_all_reads_every_slit_device_cached(capsys): devices = {} for prefix in cSAXSSlits._SLIT_LABELS: for suffix in ("xc", "xs", "yc", "ys"): name = f"{prefix}{suffix}" dev = mock.MagicMock() dev.read.return_value = {name: {"value": 0.5}} devices[name] = dev slits = _make_slits(devices) slits.slits_show_all() assert len(devices) == 24 for dev in devices.values(): dev.read.assert_called_once_with(cached=True) assert "cSAXS Slits Overview" in capsys.readouterr().out def _slit_devices(displayed: dict, epics_offset=0.1, smaract_offset=0.2): devices = {} for prefix in cSAXSSlits._SLIT_LABELS: smaract = prefix in ("sl4", "sl5", "sl6") for suffix in ("xc", "xs", "yc", "ys"): name = f"{prefix}{suffix}" dev = mock.MagicMock() dev.read.return_value = {name: {"value": displayed[suffix]}} if smaract: cls = "VirtualSlitWidth" if suffix.endswith("s") else "VirtualSlitCenter" dev._config = { "deviceClass": f"ophyd_devices.devices.virtual_slit.{cls}", "deviceConfig": {"offset": smaract_offset}, } else: dev._config = {"deviceClass": "ophyd_devices.EpicsMotorEC", "deviceConfig": {}} dev.user_offset.get.return_value = epics_offset devices[name] = dev return devices def test_slits_calibration_report_offsets(capsys): displayed = {"xc": 0.03, "xs": 1.05, "yc": -0.02, "ys": 0.9} slits = _make_slits(_slit_devices(displayed)) result = slits.slits_calibration_report(full=False) assert len(result) == 24 # EPICS: new user_offset = old + (target - displayed) assert abs(result["sl1xc"] - (0.1 - 0.03)) < 1e-12 assert abs(result["sl2xs"] - (0.1 + 1.0 - 1.05)) < 1e-12 assert abs(result["sl3yc"] - (0.1 + 0.02)) < 1e-12 # SmarAct: new config offset = old + (target - displayed) assert abs(result["sl4ys"] - (0.2 + 1.0 - 0.9)) < 1e-12 assert abs(result["sl6xc"] - (0.2 - 0.03)) < 1e-12 out = capsys.readouterr().out assert "dev.sl1xc.user_offset.put(0.070000)" in out assert "sl4ys: offset: 0.300000" in out assert "dev.sl4" not in out assert "Beamline state" not in out def test_slits_calibration_report_uses_fresh_reads_and_target_width(): displayed = {"xc": 0.0, "xs": 2.0, "yc": 0.0, "ys": 2.0} devices = _slit_devices(displayed, epics_offset=0.0, smaract_offset=0.0) slits = _make_slits(devices) result = slits.slits_calibration_report(full=False, target_width=2.0) assert all(abs(v) < 1e-12 for v in result.values()) devices["sl1xc"].read.assert_called_with(cached=False) def test_slits_calibration_report_missing_device_and_save(tmp_path): displayed = {"xc": 0.0, "xs": 1.0, "yc": 0.0, "ys": 1.0} devices = _slit_devices(displayed) del devices["sl5xc"] slits = _make_slits(devices) fname = tmp_path / "sub" / "state.txt" result = slits.slits_calibration_report(full=False, save=True, filename=str(fname)) assert result["sl5xc"] is None text = fname.read_text() assert "could not compute" in text and "sl5xc" in text assert "Slit Calibration Report" in text def test_slits_calibration_report_full_dump(capsys): displayed = {"xc": 0.0, "xs": 1.0, "yc": 0.0, "ys": 1.0} slit_devs = _slit_devices(displayed) devices = mock.MagicMock() devices.__getitem__.side_effect = slit_devs.__getitem__ kb = mock.MagicMock() kb.name = "kbvpitch" kb.enabled = True kb.read.return_value = {"kbvpitch": {"value": 3.25}} kb.read_configuration.return_value = {"kbvpitch_user_offset": {"value": -0.5}} disabled = mock.MagicMock() disabled.name = "disabled_dev" disabled.enabled = False devices.get_devices_with_tags.side_effect = lambda tag: ( [kb, disabled] if tag == "bl_optics_hutch" else [] ) client = mock.MagicMock() client.device_manager.devices = devices slits = cSAXSSlits(client) slits.slits_calibration_report(full=True) out = capsys.readouterr().out assert "---------- bl_optics_hutch ----------" in out assert "kbvpitch_user_offset" in out and "-0.5" in out assert "3.25" in out assert "disabled_dev" not in out kb.read.assert_called_once_with(cached=True) tags = [c.args[0] for c in devices.get_devices_with_tags.call_args_list] assert tags == list(cSAXSSlits._BEAMLINE_TAGS) def _epics_motor(user_parameter: dict | None, live_offset: float, enabled=True): dev = mock.MagicMock() dev.enabled = enabled dev._config = {"deviceClass": "ophyd_devices.EpicsMotorEC", "deviceConfig": {}} if user_parameter is not None: dev._config["userParameter"] = user_parameter dev.user_offset.get.return_value = live_offset return dev def test_slits_apply_epics_offsets_only_where_configured_and_different(): devices = { "sl1xc": _epics_motor({"epics_offset": 0.12}, live_offset=0.0), "sl1xs": _epics_motor({"epics_offset": 0.5}, live_offset=0.5), # already right "sl2xc": _epics_motor({"other": 1}, live_offset=0.3), # no epics_offset "sl2xs": _epics_motor(None, live_offset=0.3), # no userParameter "sl3xc": _epics_motor({"epics_offset": -1.0}, live_offset=0.0, enabled=False), } slits = _make_slits(devices) written = slits.slits_apply_epics_offsets() assert written == {"sl1xc": 0.12} devices["sl1xc"].user_offset.put.assert_called_once_with(0.12) for name in ("sl1xs", "sl2xc", "sl2xs", "sl3xc"): devices[name].user_offset.put.assert_not_called() def test_slits_apply_epics_offsets_survives_device_errors(capsys): bad = _epics_motor({"epics_offset": 0.1}, live_offset=0.0) bad.user_offset.get.side_effect = RuntimeError("no connection") good = _epics_motor({"epics_offset": 0.2}, live_offset=0.0) slits = _make_slits({"sl1xc": bad, "sl1yc": good}) written = slits.slits_apply_epics_offsets() assert written == {"sl1yc": 0.2} assert "sl1xc: could not apply epics_offset" in capsys.readouterr().out def test_slits_calibration_report_prints_epics_offset_param_and_mismatch(capsys): displayed = {"xc": 0.03, "xs": 1.0, "yc": 0.0, "ys": 1.0} devices = _slit_devices(displayed, epics_offset=0.1) devices["sl1xc"]._config["userParameter"] = {"epics_offset": 0.25} slits = _make_slits(devices) slits.slits_calibration_report(full=False) out = capsys.readouterr().out assert "sl1xc: epics_offset: 0.070000" in out assert "NOTE: sl1xc EPICS offset 0.100000 differs from configured epics_offset 0.250000" in out