class TestManagerController(unittest.TestCase): maxDiff = None def setUp(self): self.p = Process("process1") # create a child to client self.c_child = StatefulController("childBlock") self.c_part = MyPart("cp1") self.c_child.add_part(self.c_part) self.p.add_controller(self.c_child) # Create temporary config directory for ProcessController self.config_dir = tmp_dir("config_dir") self.main_block_name = "mainBlock" self.c = ManagerController("mainBlock", config_dir=self.config_dir.value) self.c.add_part(MyPart("part1")) self.c.add_part( ChildPart("part2", mri="childBlock", initial_visibility=True)) self.p.add_controller(self.c) self.b = self.p.block_view("mainBlock") # check that do_initial_reset works asynchronously assert self.c.state.value == "Disabled" self.p.start() assert self.c.state.value == "Ready" def tearDown(self): self.p.stop(timeout=1) shutil.rmtree(self.config_dir.value) def test_init(self): assert self.c.layout.value.name == ["part2"] assert self.c.layout.value.mri == ["childBlock"] assert self.c.layout.value.x == [0.0] assert self.c.layout.value.y == [0.0] assert self.c.layout.value.visible == [True] assert self.c.layout.meta.elements["name"].writeable is False assert self.c.layout.meta.elements["mri"].writeable is False assert self.c.layout.meta.elements["x"].writeable is True assert self.c.layout.meta.elements["y"].writeable is True assert self.c.layout.meta.elements["visible"].writeable is True assert self.c.design.value == "" assert self.c.exports.value.source == [] assert self.c.exports.meta.elements["source"].choices == [ "part2.health", "part2.state", "part2.disable", "part2.reset", "part2.attr", ] assert self.c.exports.value.export == [] assert self.c.modified.value is False assert self.c.modified.alarm.message == "" assert self.b.mri.value == "mainBlock" assert self.b.mri.meta.tags == ["sourcePort:block:mainBlock"] def _get_design_filename(self, block_name, design_name): return f"{self.config_dir.value}/{block_name}/{design_name}.json" def check_expected_save(self, design_name, x=0.0, y=0.0, visible="true", attr="defaultv"): expected = [ x.strip() for x in ("""{ "attributes": { "layout": { "part2": { "x": %s, "y": %s, "visible": %s } }, "exports": {}, "attr": "defaultv" }, "children": { "part2": { "attr": "%s" } } }""" % (x, y, visible, attr)).splitlines() ] with open(self._get_design_filename(self.main_block_name, design_name)) as f: actual = [x.strip() for x in f.readlines()] assert actual == expected def test_save(self): self.c._run_git_cmd = MagicMock() assert self.c.design.value == "" assert self.c.design.meta.choices == [""] c = Context(self.p) li = [] c.subscribe(["mainBlock", "design", "meta"], li.append) # Wait for long enough for the other process to get a look in c.sleep(0.1) assert len(li) == 1 assert li.pop()["choices"] == [""] b = c.block_view("mainBlock") design_name = "testSaveLayout" b.save(designName=design_name) assert len(li) == 3 assert li[0]["writeable"] is False assert li[1]["choices"] == ["", design_name] assert li[2]["writeable"] is True assert self.c.design.meta.choices == ["", design_name] self.check_expected_save(design_name) assert self.c.state.value == "Ready" assert self.c.design.value == design_name assert self.c.modified.value is False os.remove(self._get_design_filename(self.main_block_name, design_name)) self.c_part.attr.set_value("newv") assert self.c.modified.value is True assert (self.c.modified.alarm.message == "part2.attr.value = 'newv' not 'defaultv'") self.c.save(designName="") self.check_expected_save(design_name, attr="newv") design_filename = self._get_design_filename(self.main_block_name, design_name) assert self.c.design.value == "testSaveLayout" assert self.c._run_git_cmd.call_args_list == [ call("add", design_filename), call( "commit", "--allow-empty", "-m", "Saved mainBlock testSaveLayout", design_filename, ), call("add", design_filename), call( "commit", "--allow-empty", "-m", "Saved mainBlock testSaveLayout", design_filename, ), ] def move_child_block(self): new_layout = dict(name=["part2"], mri=["anything"], x=[10], y=[20], visible=[True]) self.b.layout.put_value(new_layout) def test_move_child_block_dict(self): assert self.b.layout.value.x == [0] self.move_child_block() assert self.b.layout.value.x == [10] def test_set_and_load_layout(self): new_layout = LayoutTable(name=["part2"], mri=["anything"], x=[10], y=[20], visible=[False]) self.c.set_layout(new_layout) assert self.c.parts["part2"].x == 10 assert self.c.parts["part2"].y == 20 assert self.c.parts["part2"].visible is False assert self.c.modified.value is True assert self.c.modified.alarm.message == "layout changed" # save the layout, modify and restore it design_name = "testSaveLayout" self.b.save(designName=design_name) assert self.c.modified.value is False self.check_expected_save(design_name, 10.0, 20.0, "false") self.c.parts["part2"].x = 30 self.c.set_design(design_name) assert self.c.parts["part2"].x == 10 def test_set_export_parts(self): context = Context(self.p) b = context.block_view("mainBlock") assert list(b) == [ "meta", "health", "state", "disable", "reset", "mri", "layout", "design", "exports", "modified", "save", "attr", ] assert b.attr.meta.tags == ["widget:textinput"] new_exports = ExportTable.from_rows([("part2.attr", "childAttr"), ("part2.reset", "childReset")]) self.c.set_exports(new_exports) assert self.c.modified.value is True assert self.c.modified.alarm.message == "exports changed" self.c.save(designName="testSaveLayout") assert self.c.modified.value is False # block has changed, get a new view b = context.block_view("mainBlock") assert list(b) == [ "meta", "health", "state", "disable", "reset", "mri", "layout", "design", "exports", "modified", "save", "attr", "childAttr", "childReset", ] assert self.c.state.value == "Ready" assert b.childAttr.value == "defaultv" assert self.c.modified.value is False m = MagicMock() b.childAttr.subscribe_value(m) # allow a subscription to come through context.sleep(0.1) m.assert_called_once_with("defaultv") m.reset_mock() self.c_part.attr.set_value("newv") assert b.childAttr.value == "newv" assert self.c_part.attr.value == "newv" assert self.c.modified.value is True assert (self.c.modified.alarm.message == "part2.attr.value = 'newv' not 'defaultv'") # allow a subscription to come through context.sleep(0.1) m.assert_called_once_with("newv") b.childAttr.put_value("again") assert b.childAttr.value == "again" assert self.c_part.attr.value == "again" assert self.c.modified.value is True assert (self.c.modified.alarm.message == "part2.attr.value = 'again' not 'defaultv'") # remove the field new_exports = ExportTable([], []) self.c.set_exports(new_exports) assert self.c.modified.value is True self.c.save() assert self.c.modified.value is False # block has changed, get a new view b = context.block_view("mainBlock") assert "childAttr" not in b
class TestTitlePart(unittest.TestCase): def setUp(self): self.o = TitlePart(value="My label") self.p = Process("proc") self.c = BasicController("mri") self.c.add_part(self.o) self.p.add_controller(self.c) self.p.start() self.b = self.p.block_view(self.c.mri) def tearDown(self): self.p.stop(1) def test_init(self): assert self.o.name == "label" assert self.o.attr.value == "My label" assert self.o.attr.meta.tags == [ "widget:textinput", "config:1"] assert self.b.meta.label == "My label" def test_setter(self): self.b.label.put_value("My label2") assert self.b.label.value == "My label2" assert self.b.meta.label == "My label2" def test_concurrency(self): q = Queue() # Subscribe to the whole block sub = Subscribe(id=0, path=["mri"], delta=True) sub.set_callback(q.put) self.c.handle_request(sub) # We should get first Delta through with initial value r = q.get().to_dict() assert r["id"] == 0 assert len(r["changes"]) == 1 assert len(r["changes"][0]) == 2 assert r["changes"][0][0] == [] assert r["changes"][0][1]["meta"]["label"] == "My label" assert r["changes"][0][1]["label"]["value"] == "My label" # Do a Put on the label put = Put(id=2, path=["mri", "label", "value"], value="New", get=True) put.set_callback(q.put) self.c.handle_request(put) # Check we got two updates before the return r = q.get().to_dict() assert r["id"] == 0 assert len(r["changes"]) == 2 assert len(r["changes"][0]) == 2 assert r["changes"][0][0] == ["label", "value"] assert r["changes"][0][1] == "New" assert len(r["changes"][0]) == 2 assert r["changes"][1][0] == ["label", "timeStamp"] r = q.get().to_dict() assert r["id"] == 0 assert len(r["changes"]) == 1 assert len(r["changes"][0]) == 2 assert r["changes"][0][0] == ["meta", "label"] assert r["changes"][0][1] == "New" # Then the return r3 = q.get().to_dict() assert r3["id"] == 2 assert r3["value"] is "New"
class TestPandaPulseTriggerPart(ChildTestCase): def setUp(self): self.process = Process("Process") self.context = Context(self.process) # Create a fake PandA with a pulse block self.panda = ManagerController("PANDA", "/tmp", use_git=False) controller = BasicController("PANDA:PULSE3") self.pulse_part = PulsePart("part") controller.add_part(self.pulse_part) self.process.add_controller(controller) self.panda.add_part( ChildPart("PULSE3", "PANDA:PULSE3", initial_visibility=True, stateful=False)) self.process.add_controller(self.panda) # And the detector self.config_dir = tmp_dir("config_dir") for c in detector_block("DET", config_dir=self.config_dir.value): self.process.add_controller(c) # Make the child block holding panda and pmac mri self.child = self.create_child_block( panda_pulse_trigger_block, self.process, mri="SCAN:PULSE", panda="PANDA", detector="DET", ) # And our part under test self.o = PandAPulseTriggerPart("detTrigger", "SCAN:PULSE") # Add in a scan block self.scan = RunnableController("SCAN", "/tmp", use_git=False) self.scan.add_part(DetectorChildPart("det", "DET", True)) self.scan.add_part(self.o) self.process.add_controller(self.scan) # Now start the process off and tell the panda which sequencer tables # to use self.process.start() exports = ExportTable.from_rows([ ("PULSE3.width", "detTriggerWidth"), ("PULSE3.step", "detTriggerStep"), ("PULSE3.delay", "detTriggerDelay"), ("PULSE3.pulses", "detTriggerPulses"), ]) self.panda.set_exports(exports) self.tmpdir = tempfile.mkdtemp() def tearDown(self): self.process.stop(timeout=2) shutil.rmtree(self.tmpdir) shutil.rmtree(self.config_dir.value) def check_pulse_mocks(self, width, step, delay, pulses): self.pulse_part.mocks["width"].assert_called_once_with( pytest.approx(width)) self.pulse_part.mocks["step"].assert_called_once_with( pytest.approx(step)) self.pulse_part.mocks["delay"].assert_called_once_with( pytest.approx(delay)) self.pulse_part.mocks["pulses"].assert_called_once_with(pulses) def test_configure_multiple_no_exposure(self): xs = LineGenerator("x", "mm", 0.0, 0.3, 4) ys = LineGenerator("y", "mm", 0.0, 0.1, 2) generator = CompoundGenerator([ys, xs], [], [], 1.0) generator.prepare() detectors = DetectorTable.from_rows([[True, "det", "DET", 0.0, 5]]) self.o.on_configure(self.context, generator, detectors) assert self.o.generator_duration == 1.0 assert self.o.frames_per_step == 5 # Detector would normally be configured by DetectorChildPart detector = self.process.block_view("DET") spg = StaticPointGenerator(5, axes=["det_frames_per_step"]) ex = SquashingExcluder(axes=["det_frames_per_step", "x"]) generatormultiplied = CompoundGenerator([ys, xs, spg], [ex], [], 0.2) detector.configure(generatormultiplied, self.tmpdir) self.o.on_post_configure() self.check_pulse_mocks(0.19899, 0.2, 0.000505, 5) def test_configure_multiple_no_exposure_with_zero_delay(self): xs = LineGenerator("x", "mm", 0.0, 0.3, 4) ys = LineGenerator("y", "mm", 0.0, 0.1, 2) generator = CompoundGenerator([ys, xs], [], [], 1.0) generator.prepare() detectors = DetectorTable.from_rows([[True, "det", "DET", 0.0, 5]]) # Set delay to zero (normally done in constructor) self.o.zero_delay = True self.o.on_configure(self.context, generator, detectors) assert self.o.generator_duration == 1.0 assert self.o.frames_per_step == 5 # Detector would normally be configured by DetectorChildPart detector = self.process.block_view("DET") spg = StaticPointGenerator(5, axes=["det_frames_per_step"]) ex = SquashingExcluder(axes=["det_frames_per_step", "x"]) generatormultiplied = CompoundGenerator([ys, xs, spg], [ex], [], 0.2) detector.configure(generatormultiplied, self.tmpdir) self.o.on_post_configure() self.check_pulse_mocks(0.19899, 0.2, 0.0, 5) def test_system(self): xs = LineGenerator("x", "mm", 0.0, 0.3, 4) ys = LineGenerator("y", "mm", 0.0, 0.1, 2) generator = CompoundGenerator([ys, xs], [], [], 1.0) generator.prepare() detectors = DetectorTable.from_rows([[True, "det", "DET", 0.0, 5]]) b = self.scan.block_view() b.configure(generator, self.tmpdir, detectors=detectors) self.check_pulse_mocks(0.19899, 0.2, 0.000505, 5) def test_system_defined_exposure(self): xs = LineGenerator("x", "mm", 0.0, 0.3, 4) ys = LineGenerator("y", "mm", 0.0, 0.1, 2) generator = CompoundGenerator([ys, xs], [], [], 1.0) generator.prepare() detectors = DetectorTable.from_rows([[True, "det", "DET", 0.1, 5]]) b = self.scan.block_view() b.configure(generator, self.tmpdir, detectors=detectors) self.check_pulse_mocks(0.1, 0.2, 0.05, 5) def test_on_validate_tweaks_zero_duration(self): points = StaticPointGenerator(10) generator = CompoundGenerator([points], [], [], 0.0) generator.prepare() # Disable the detector detectors = DetectorTable.from_rows([[False, "det", "DET", 0.0, 5]]) # Expected duration is 2 clock cycles expected_duration = 2 * 8.0e-9 b = self.scan.block_view() params = b.validate(generator, self.tmpdir, detectors=detectors) self.assertEqual(expected_duration, params["generator"]["duration"]) def test_on_validate_raises_AssertionError_for_negative_duration(self): xs = LineGenerator("x", "mm", 0.0, 0.3, 4) ys = LineGenerator("y", "mm", 0.0, 0.1, 2) generator = CompoundGenerator([ys, xs], [], [], -1.0) generator.prepare() # Disable the detector detectors = DetectorTable.from_rows([[False, "det", "DET", 0.0, 5]]) b = self.scan.block_view() self.assertRaises(AssertionError, b.validate, generator, self.tmpdir, detectors=detectors)
class TestCAParts(unittest.TestCase): def setUp(self): self.process = Process("proc") self.process.start() def create_block(self, p, mri="mri"): c = StatefulController(mri) c.add_part(p) self.process.add_controller(c) b = self.process.block_view(mri) return b def tearDown(self): self.process.stop(timeout=2) def test_caboolean(self, catools): from malcolm.modules.ca.parts import CABooleanPart class Initial(int): ok = True severity = 0 catools.caget.side_effect = [[Initial(0), Initial(0)]] b = self.create_block( CABooleanPart(name="attrname", description="desc", pv="pv", rbv_suffix="2") ) assert b.attrname.value is False assert b.attrname.meta.description == "desc" assert b.attrname.meta.writeable catools.caget.assert_called_once_with( ["pv2", "pv"], datatype=catools.DBR_LONG, format=catools.FORMAT_CTRL, throw=True, ) catools.caget.reset_mock() class Update(int): ok = True severity = 0 raw_stamp = (34, 4355) catools.caget.side_effect = [Update(1)] b.attrname.put_value(True) catools.caput.assert_called_once_with( "pv", 1, datatype=catools.DBR_LONG, timeout=10.0, wait=True ) catools.caget.assert_called_once_with( "pv2", datatype=catools.DBR_LONG, format=catools.FORMAT_TIME, throw=True ) assert b.attrname.value is True assert b.attrname.alarm.is_ok() assert b.attrname.timeStamp.to_time() == 34.000004355 def test_cachararray(self, catools): from malcolm.modules.ca.parts import CACharArrayPart class Initial(str): ok = True severity = 1 catools.caget.side_effect = [[Initial("long_and_bad_string")]] b = self.create_block( CACharArrayPart(name="cattr", description="desc", rbv="pvr") ) assert b.cattr.value == "long_and_bad_string" assert b.cattr.alarm.severity == AlarmSeverity.MINOR_ALARM catools.caget.assert_called_once_with( ["pvr"], datatype=catools.DBR_CHAR_STR, format=catools.FORMAT_CTRL, throw=True, ) def test_cachoice(self, catools): from malcolm.modules.ca.parts import CAChoicePart class Initial(int): ok = True severity = 0 enums = ["a", "b", "c"] catools.caget.side_effect = [[Initial(1), Initial(2)]] b = self.create_block( CAChoicePart(name="attrname", description="desc", pv="pv", rbv="rbv") ) assert b.attrname.value == "b" assert b.attrname.meta.description == "desc" assert b.attrname.meta.writeable catools.caget.assert_called_once_with( ["rbv", "pv"], datatype=catools.DBR_ENUM, format=catools.FORMAT_CTRL, throw=True, ) catools.caget.reset_mock() class Update(int): ok = True severity = 2 raw_stamp = (34, 4355) catools.caget.side_effect = [Update(0)] b.attrname.put_value("c") catools.caput.assert_called_once_with( "pv", 2, datatype=catools.DBR_ENUM, timeout=10.0, wait=True ) catools.caget.assert_called_once_with( "rbv", datatype=catools.DBR_ENUM, format=catools.FORMAT_TIME, throw=True ) assert b.attrname.value == "a" assert b.attrname.alarm.severity == AlarmSeverity.MAJOR_ALARM assert b.attrname.timeStamp.to_time() == 34.000004355 catools.caget.reset_mock() catools.caput.reset_mock() catools.caget.side_effect = [Update(1)] b.attrname.put_value(1) catools.caput.assert_called_once_with( "pv", 1, datatype=catools.DBR_ENUM, timeout=10.0, wait=True ) assert b.attrname.value == "b" def test_cadoublearray(self, catools): from malcolm.modules.ca.parts import CADoubleArrayPart class Initial(np.ndarray): ok = True severity = 0 precision = 5 units = "" lower_disp_limit = -1.0 upper_disp_limit = 10.0 initial = Initial(dtype=np.float64, shape=(3,)) initial[:] = np.arange(3) + 1.2 catools.caget.side_effect = [[initial]] b = self.create_block( CADoubleArrayPart(name="attrname", description="desc", pv="pv", timeout=-1) ) assert list(b.attrname.value) == [1.2, 2.2, 3.2] assert b.attrname.meta.description == "desc" assert b.attrname.meta.writeable assert b.attrname.meta.display.limitLow == -1.0 assert b.attrname.meta.display.limitHigh == 10.0 assert b.attrname.meta.display.precision == 5 catools.caget.assert_called_once_with( ["pv"], datatype=catools.DBR_DOUBLE, format=catools.FORMAT_CTRL, throw=True ) catools.caget.reset_mock() class Update(np.ndarray): ok = False catools.caget.side_effect = [Update(shape=(6,))] b.attrname.put_value([]) catools.caput.assert_called_once_with( "pv", ANY, datatype=catools.DBR_DOUBLE, timeout=None, wait=True ) assert list(catools.caput.call_args[0][1]) == [] catools.caget.assert_called_once_with( "pv", datatype=catools.DBR_DOUBLE, format=catools.FORMAT_TIME, throw=True ) assert list(b.attrname.value) == [1.2, 2.2, 3.2] assert b.attrname.alarm.severity == AlarmSeverity.UNDEFINED_ALARM def test_cawaveformtable(self, catools): from malcolm.modules.ca.parts import CAWaveformTablePart class Initial(np.ndarray): ok = True severity = 0 precision = 7 units = "" lower_disp_limit = 0.0 upper_disp_limit = 0.0 initialY = Initial(dtype=np.float64, shape=(3,)) initialY.lower_disp_limit = np.e initialY.upper_disp_limit = 10.0 initialY.name = "yPv" initialY[:] = np.arange(3) + 1.2 initialX = Initial(dtype=np.float64, shape=(3,)) initialX.upper_disp_limit = np.pi initialX.name = "xPv" initialX.units = "s" initialX[:] = (np.arange(3) + 1) ** 2 initial = {"yPv": initialY, "xPv": initialX} def mock_get(pvs, **kwargs): return_vals = [] for pv in pvs: return_vals.append(initial[pv]) return return_vals catools.caget.side_effect = mock_get c = self.create_block( CAWaveformTablePart( name="attrname", description="desc", pv_list=( "yPv", "xPv", ), name_list=( "yData", "xData", ), timeout=-1, ), "withDisplayFromPv", ) assert isinstance(c.attrname.value, Table) assert c.attrname.value["yData"] == [1.2, 2.2, 3.2] assert c.attrname.meta.description == "desc" assert not c.attrname.meta.writeable assert c.attrname.meta.elements["yData"].display.limitLow == np.e assert c.attrname.meta.elements["yData"].display.limitHigh == 10.0 assert c.attrname.meta.elements["yData"].display.precision == 7 assert c.attrname.meta.elements["xData"].display.limitLow == 0.0 assert c.attrname.meta.elements["xData"].display.limitHigh == np.pi assert c.attrname.meta.elements["xData"].display.units == "s" catools.caget.assert_called_with( ("yPv", "xPv"), datatype=catools.DBR_DOUBLE, format=catools.FORMAT_CTRL, throw=True, ) catools.caget.reset_mock() def test_cadouble(self, catools): from malcolm.modules.ca.parts import CADoublePart class Initial(float): ok = True severity = 0 precision = 99 lower_disp_limit = 189 upper_disp_limit = 1527 units = "tests" catools.caget.side_effect = [[Initial(5.2)], [Initial(5.2)]] b = self.create_block( CADoublePart( name="attrname", description="desc", rbv="pv", display_from_pv=False ), "noDisplayFromPv", ) assert b.attrname.value == 5.2 assert b.attrname.meta.description == "desc" assert not b.attrname.meta.writeable assert b.attrname.meta.display.limitLow == 0.0 assert b.attrname.meta.display.limitHigh == 0.0 assert b.attrname.meta.display.precision == 8 assert b.attrname.meta.display.units == "" catools.caget.assert_called_once_with( ["pv"], datatype=catools.DBR_DOUBLE, format=catools.FORMAT_CTRL, throw=True ) li = [] b.attrname.subscribe_value(li.append) b._context.sleep(0.1) assert li == [5.2] catools.camonitor.assert_called_once() callback = catools.camonitor.call_args[0][1] callback(Initial(8.7)) callback(Initial(8.8)) assert b.attrname.value == 8.8 # TODO: why does this seg fault on travis VMs when cothread is # stack sharing? b._context.sleep(0.1) assert li == [5.2, 8.7, 8.8] c = self.create_block( CADoublePart(name="attrname", description="desc", rbv="pv"), "withDisplayFromPv", ) assert c.attrname.meta.display.limitLow == 189 assert c.attrname.meta.display.limitHigh == 1527 assert c.attrname.meta.display.precision == 99 assert c.attrname.meta.display.units == "tests" def test_calongarray(self, catools): from malcolm.modules.ca.parts import CALongArrayPart class Initial(np.ndarray): ok = True severity = 0 initial = Initial(dtype=np.int32, shape=(4,)) initial[:] = [5, 6, 7, 8] catools.caget.side_effect = [[initial]] b = self.create_block( CALongArrayPart( name="attrname", description="desc", pv="pv", widget=Widget.TEXTINPUT ) ) assert list(b.attrname.value) == [5, 6, 7, 8] assert b.attrname.meta.tags == ["widget:textinput", "config:1"] assert b.attrname.meta.writeable catools.caget.assert_called_once_with( ["pv"], datatype=catools.DBR_LONG, format=catools.FORMAT_CTRL, throw=True ) catools.caget.reset_mock() class Update(np.ndarray): ok = True severity = 0 update = Update(shape=(2,), dtype=np.int32) update[:] = [4, 5] catools.caget.side_effect = [update] b.attrname.put_value([4, 4.2]) catools.caput.assert_called_once_with( "pv", ANY, datatype=catools.DBR_LONG, timeout=10.0, wait=True ) assert list(catools.caput.call_args[0][1]) == [4, 4] catools.caget.assert_called_once_with( "pv", datatype=catools.DBR_LONG, format=catools.FORMAT_TIME, throw=True ) assert list(b.attrname.value) == [4, 5] assert b.attrname.alarm.is_ok() def test_calong(self, catools): from malcolm.modules.ca.parts import CALongPart class Initial(int): ok = True severity = 0 catools.caget.side_effect = [[Initial(3)]] b = self.create_block(CALongPart(name="attrname", description="desc", pv="pv")) assert b.attrname.value == 3 assert b.attrname.meta.description == "desc" assert b.attrname.meta.writeable catools.caget.assert_called_once_with( ["pv"], datatype=catools.DBR_LONG, format=catools.FORMAT_CTRL, throw=True ) def test_castring(self, catools): from malcolm.modules.ca.parts import CAStringPart class Initial(str): ok = True severity = 0 catools.caget.side_effect = [[Initial("thing")]] b = self.create_block( CAStringPart(name="attrname", description="desc", rbv="pv") ) assert b.attrname.value == "thing" assert b.attrname.meta.description == "desc" assert not b.attrname.meta.writeable catools.caget.assert_called_once_with( ["pv"], datatype=catools.DBR_STRING, format=catools.FORMAT_CTRL, throw=True ) def test_init_no_pv_no_rbv(self, catools): from malcolm.modules.ca.parts import CABooleanPart # create test for no pv or rbv with self.assertRaises(ValueError): CABooleanPart(name="attrname", description="desc")
class TestSystemPVA(unittest.TestCase): def setUp(self): self.process = Process("proc") for controller in \ ticker_block(mri="TICKER", config_dir="/tmp") + \ pva_server_block(mri="PVA-SERVER"): self.process.add_controller(controller) self.process.start() self.process2 = Process("proc2") for controller in \ pva_client_block(mri="PVA-CLIENT") + \ proxy_block(mri="TICKER", comms="PVA-CLIENT"): self.process2.add_controller(controller) self.process2.start() def tearDown(self): self.process.stop(timeout=2) self.process2.stop(timeout=2) def make_generator(self): line1 = LineGenerator('y', 'mm', 0, 3, 3) line2 = LineGenerator('x', 'mm', 1, 2, 2) compound = CompoundGenerator([line1, line2], [], [], duration=0.05) return compound def check_blocks_equal(self): src_block = self.process.block_view("TICKER") block = self.process2.block_view("TICKER") for k in src_block: assert block[k].to_dict() == src_block[k].to_dict() def test_init(self): self.check_blocks_equal() def test_validate(self): block = self.process2.block_view("TICKER") generator = self.make_generator() params = block.validate(generator, axesToMove=["x", "y"]) assert params == dict( axesToMove=["x", "y"], exceptionStep=0, generator=generator.to_dict(), ) def test_configure(self): block = self.process2.block_view("TICKER") generator = self.make_generator() block.configure(generator, axesToMove=["x", "y"]) # TODO: ordering is not maintained in PVA, so need to wait before get # block._context.sleep(0.1) assert "Armed" == block.state.value self.check_blocks_equal() def test_exports(self): block = self.process2.block_view("TICKER") fields = [ 'meta', 'health', 'state', 'disable', 'reset', 'mri', 'layout', 'design', 'exports', 'modified', 'save', 'completedSteps', 'configuredSteps', 'totalSteps', 'validate', 'configure', 'run', 'abort', 'pause', 'resume' ] assert list(block) == fields generator = self.make_generator() block.configure(generator, axesToMove=["x", "y"]) block.run() # Export X t = ExportTable(source=["x.counter"], export=["xValue"]) block.exports.put_value(t) assert list(block) == fields + ["xValue"] assert block.xValue.value == 2.0 # Export Y t = ExportTable(source=["y.counter"], export=["yValue"]) block.exports.put_value(t) assert list(block) == fields + ["yValue"] assert block.yValue.value == 3.0 # Export Nothing t = ExportTable(source=[], export=[]) block.exports.put_value(t) assert list(block) == fields
class TestPMACTrajectoryPart(ChildTestCase): def setUp(self): self.process = Process("Process") self.context = Context(self.process) self.cs = self.create_child_block(cs_block, self.process, mri="PMAC:CS1", prefix="PV:CSPRE") self.child = self.create_child_block(pmac_trajectory_block, self.process, mri="PMAC:TRAJ", prefix="PV:PRE") self.o = PmacTrajectoryPart(name="pmac", mri="PMAC:TRAJ") self.process.start() def tearDown(self): del self.context self.process.stop(timeout=1) def test_init(self): registrar = Mock() self.o.setup(registrar) registrar.add_attribute_model.assert_called_once_with( "minTurnaround", self.o.min_turnaround, self.o.min_turnaround.set_value) def test_bad_units(self): with self.assertRaises(AssertionError) as cm: self.do_configure(["x", "y"], units="m") assert str(cm.exception) == "x: Expected scan units of 'm', got 'mm'" def resolutions_and_use_call(self, useB=True): return [ call.put('useA', True), call.put('useB', useB), call.put('useC', False), call.put('useU', False), call.put('useV', False), call.put('useW', False), call.put('useX', False), call.put('useY', False), call.put('useZ', False) ] def make_part_info(self, x_pos=0.5, y_pos=0.0, units="mm"): part_info = dict(xpart=[ MotorInfo(cs_axis="A", cs_port="CS1", acceleration=2.5, resolution=0.001, offset=0.0, max_velocity=1.0, current_position=x_pos, scannable="x", velocity_settle=0.0, units=units) ], ypart=[ MotorInfo(cs_axis="B", cs_port="CS1", acceleration=2.5, resolution=0.001, offset=0.0, max_velocity=1.0, current_position=y_pos, scannable="y", velocity_settle=0.0, units=units) ], brick=[ControllerInfo(i10=1705244)], cs1=[CSInfo(mri="PMAC:CS1", port="CS1")]) return part_info def do_configure(self, axes_to_scan, completed_steps=0, x_pos=0.5, y_pos=0.0, duration=1.0, units="mm"): part_info = self.make_part_info(x_pos, y_pos, units) steps_to_do = 3 * len(axes_to_scan) xs = LineGenerator("x", "mm", 0.0, 0.5, 3, alternate=True) ys = LineGenerator("y", "mm", 0.0, 0.1, 2) generator = CompoundGenerator([ys, xs], [], [], duration) generator.prepare() self.o.configure(self.context, completed_steps, steps_to_do, part_info, generator, axes_to_scan) def test_validate(self): generator = CompoundGenerator([], [], [], 0.0102) axesToMove = ["x"] part_info = self.make_part_info() ret = self.o.validate(part_info, generator, axesToMove) expected = 0.010166 assert ret.value.duration == expected @patch( "malcolm.modules.pmac.parts.pmactrajectorypart.INTERPOLATE_INTERVAL", 0.2) def test_configure(self): # Pretend to respond on demand values before they are actually set self.set_attributes(self.cs, demandA=-0.1375, demandB=0.0) self.do_configure(axes_to_scan=["x", "y"]) assert self.cs.handled_requests.mock_calls == [ call.put('deferMoves', True), call.put('csMoveTime', 0), call.put('demandA', -0.1375), call.put('demandB', 0.0), call.put('deferMoves', False) ] assert self.child.handled_requests.mock_calls == [ call.put('numPoints', 4000000), call.put('cs', 'CS1'), call.put('useA', False), call.put('useB', False), call.put('useC', False), call.put('useU', False), call.put('useV', False), call.put('useW', False), call.put('useX', False), call.put('useY', False), call.put('useZ', False), call.put('pointsToBuild', 1), call.put('timeArray', pytest.approx([2000])), call.put('userPrograms', pytest.approx([8])), call.put('velocityMode', pytest.approx([3])), call.post('buildProfile'), call.post('executeProfile'), ] + self.resolutions_and_use_call() + [ call.put('pointsToBuild', 16), # pytest.approx to allow sensible compare with numpy arrays call.put( 'positionsA', pytest.approx([ -0.125, 0.0, 0.125, 0.25, 0.375, 0.5, 0.625, 0.6375, 0.625, 0.5, 0.375, 0.25, 0.125, 0.0, -0.125, -0.1375 ])), call.put( 'positionsB', pytest.approx([ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.05, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1 ])), call.put( 'timeArray', pytest.approx([ 100000, 500000, 500000, 500000, 500000, 500000, 500000, 200000, 200000, 500000, 500000, 500000, 500000, 500000, 500000, 100000 ])), call.put( 'userPrograms', pytest.approx([1, 4, 1, 4, 1, 4, 2, 8, 1, 4, 1, 4, 1, 4, 2, 8 ])), call.put( 'velocityMode', pytest.approx([2, 0, 0, 0, 0, 0, 1, 0, 2, 0, 0, 0, 0, 0, 1, 3 ])), call.post('buildProfile') ] assert self.o.completed_steps_lookup == [ 0, 0, 1, 1, 2, 2, 3, 3, 3, 3, 4, 4, 5, 5, 6, 6 ] @patch("malcolm.modules.pmac.parts.pmactrajectorypart.PROFILE_POINTS", 4) @patch( "malcolm.modules.pmac.parts.pmactrajectorypart.INTERPOLATE_INTERVAL", 0.2) def test_update_step(self): # Pretend to respond on demand values before they are actually set self.set_attributes(self.cs, demandA=-0.1375, demandB=0.0) self.do_configure(axes_to_scan=["x", "y"], x_pos=0.0, y_pos=0.2) positionsA = self.child.handled_requests.put.call_args_list[-5][0][1] assert len(positionsA) == 4 assert positionsA[-1] == 0.25 assert self.o.end_index == 2 assert len(self.o.completed_steps_lookup) == 5 assert len(self.o.profile["time_array"]) == 1 self.o.registrar = Mock() self.child.handled_requests.reset_mock() self.o.update_step(3, self.context.block_view("PMAC:TRAJ")) self.o.registrar.report.assert_called_once() assert self.o.registrar.report.call_args[0][0].steps == 1 assert not self.o.loading assert self.child.handled_requests.mock_calls == [ call.put('pointsToBuild', 4), call.put('positionsA', pytest.approx([0.375, 0.5, 0.625, 0.6375])), call.put('positionsB', pytest.approx([0.0, 0.0, 0.0, 0.05])), call.put('timeArray', pytest.approx([500000, 500000, 500000, 200000])), call.put('userPrograms', pytest.approx([1, 4, 2, 8])), call.put('velocityMode', pytest.approx([0, 0, 1, 0])), call.post('appendProfile') ] assert self.o.end_index == 3 assert len(self.o.completed_steps_lookup) == 9 assert len(self.o.profile["time_array"]) == 1 def test_run(self): self.o.run(self.context) assert self.child.handled_requests.mock_calls == [ call.post('executeProfile') ] def test_reset(self): self.o.reset(self.context) assert self.child.handled_requests.mock_calls == [ call.post('abortProfile') ] def test_multi_run(self): # Pretend to respond on demand values before they are actually set self.set_attributes(self.cs, demandA=-0.1375) self.do_configure(axes_to_scan=["x"]) assert self.o.completed_steps_lookup == ([0, 0, 1, 1, 2, 2, 3, 3]) self.child.handled_requests.reset_mock() # Pretend to respond on demand values before they are actually set self.set_attributes(self.cs, demandA=0.6375) self.do_configure(axes_to_scan=["x"], completed_steps=3, x_pos=0.6375) assert self.child.handled_requests.mock_calls == [ call.put('numPoints', 4000000), call.put('cs', 'CS1'), call.put('useA', False), call.put('useB', False), call.put('useC', False), call.put('useU', False), call.put('useV', False), call.put('useW', False), call.put('useX', False), call.put('useY', False), call.put('useZ', False), call.put('pointsToBuild', 1), call.put('timeArray', pytest.approx([2000])), call.put('userPrograms', pytest.approx([8])), call.put('velocityMode', pytest.approx([3])), call.post('buildProfile'), call.post('executeProfile'), ] + self.resolutions_and_use_call(useB=False) + [ call.put('pointsToBuild', 8), call.put( 'positionsA', pytest.approx( [0.625, 0.5, 0.375, 0.25, 0.125, 0.0, -0.125, -0.1375])), call.put( 'timeArray', pytest.approx([ 100000, 500000, 500000, 500000, 500000, 500000, 500000, 100000 ])), call.put('userPrograms', pytest.approx([1, 4, 1, 4, 1, 4, 2, 8])), call.put('velocityMode', pytest.approx([2, 0, 0, 0, 0, 0, 1, 3])), call.post('buildProfile') ] @patch( "malcolm.modules.pmac.parts.pmactrajectorypart.INTERPOLATE_INTERVAL", 0.2) def test_long_steps_lookup(self): # Pretend to respond on demand values before they are actually set self.set_attributes(self.cs, demandA=0.6250637755102041) self.do_configure(axes_to_scan=["x"], completed_steps=3, x_pos=0.62506, duration=14.0) assert self.child.handled_requests.mock_calls[-6:] == [ call.put('pointsToBuild', 14), call.put( 'positionsA', pytest.approx([ 0.625, 0.5625, 0.5, 0.4375, 0.375, 0.3125, 0.25, 0.1875, 0.125, 0.0625, 0.0, -0.0625, -0.125, -0.12506377551020409 ])), call.put( 'timeArray', pytest.approx([ 7143, 3500000, 3500000, 3500000, 3500000, 3500000, 3500000, 3500000, 3500000, 3500000, 3500000, 3500000, 3500000, 7143 ])), call.put('userPrograms', pytest.approx([1, 0, 4, 0, 1, 0, 4, 0, 1, 0, 4, 0, 2, 8])), call.put('velocityMode', pytest.approx([2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 3])), call.post('buildProfile') ] assert self.o.completed_steps_lookup == ([ 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 6, 6 ]) @patch("malcolm.modules.pmac.parts.pmactrajectorypart.PROFILE_POINTS", 9) def test_split_in_a_long_step_lookup(self): # Pretend to respond on demand values before they are actually set self.set_attributes(self.cs, demandA=0.6250637755102041) self.do_configure(axes_to_scan=["x"], completed_steps=3, x_pos=0.62506, duration=14.0) # The last 6 calls show what trajectory we are building, ignore the # first 11 which are just the useX calls and cs selection assert self.child.handled_requests.mock_calls[-6:] == [ call.put('pointsToBuild', 9), call.put( 'positionsA', pytest.approx([ 0.625, 0.5625, 0.5, 0.4375, 0.375, 0.3125, 0.25, 0.1875, 0.125 ])), call.put( 'timeArray', pytest.approx([ 7143, 3500000, 3500000, 3500000, 3500000, 3500000, 3500000, 3500000, 3500000 ])), call.put('userPrograms', pytest.approx([1, 0, 4, 0, 1, 0, 4, 0, 1])), call.put('velocityMode', pytest.approx([2, 0, 0, 0, 0, 0, 0, 0, 0])), call.post('buildProfile') ] # The completed steps works on complete (not split) steps, so we expect # the last value to be the end of step 6, even though it doesn't # actually appear in the velocity arrays assert self.o.completed_steps_lookup == ([ 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 6 ]) # Mock out the registrar that would have been registered when we # attached to a controller self.o.registrar = Mock() # Now call update step and get it to generate the next lot of points # scanned can be any index into completed_steps_lookup so that there # are less than PROFILE_POINTS left to go in it self.o.update_step(scanned=2, child=self.process.block_view("PMAC:TRAJ")) # Expect the rest of the points assert self.child.handled_requests.mock_calls[-6:] == [ call.put('pointsToBuild', 5), call.put( 'positionsA', pytest.approx( [0.0625, 0.0, -0.0625, -0.125, -0.12506377551020409])), call.put('timeArray', pytest.approx([3500000, 3500000, 3500000, 3500000, 7143])), call.put('userPrograms', pytest.approx([0, 4, 0, 2, 8])), call.put('velocityMode', pytest.approx([0, 0, 0, 1, 3])), call.post('appendProfile') ] assert self.o.registrar.report.call_count == 1 assert self.o.registrar.report.call_args[0][0].steps == 3 # And for the rest of the lookup table to be added assert self.o.completed_steps_lookup == ([ 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 6, 6 ])
class TestRawMotorCSPart(unittest.TestCase): def setUp(self): self.process = Process("proc") self.o = RawMotorCSPart("cs", "PV:PRE") c = StatefulController("mri") c.add_part(self.o) self.process.add_controller(c) self.b = self.process.block_view("mri") self.addCleanup(self.process.stop) def do_init(self, catools): catools.caget.side_effect = [[ caenum(2), castr("I"), caenum(1), castr("A") ]] self.process.start() def test_init(self, catools): self.do_init(catools) catools.caget.assert_called_once_with( ["PV:PRE:CsPort", "PV:PRE:CsAxis", "PV:PRE:CsPort_RBV", "PV:PRE:CsAxis_RBV"], format=catools.FORMAT_CTRL) assert list(self.b) == [ 'meta', 'health', 'state', 'disable', 'reset', 'cs'] assert self.b.cs.value == "BRICK1CS1,A" def test_update_axis(self, catools): self.do_init(catools) update = castr("I") self.o._update_value(update, 1) assert self.b.cs.value == "BRICK1CS1,I" def test_update_port(self, catools): self.do_init(catools) update = caenum(2) self.o._update_value(update, 0) assert self.b.cs.value == "BRICK1CS2,A" def test_update_disconnect(self, catools): self.do_init(catools) update = caenum(0) self.o._update_value(update, 0) assert self.b.cs.value == "" def test_update_bad(self, catools): self.do_init(catools) update = castr("") update.ok = False self.o._update_value(update, 1) assert self.b.cs.value == "" assert self.b.cs.alarm.severity == AlarmSeverity.INVALID_ALARM def test_caput(self, catools): self.do_init(catools) catools.caget.side_effect = [[caenum(2), castr("Y")]] self.o.caput("BRICK1CS2,X") catools.caput.assert_called_once_with( ['PV:PRE:CsPort', 'PV:PRE:CsAxis'], (2, 'X'), wait=True ) assert self.b.cs.value == "BRICK1CS2,Y" def test_caput_none(self, catools): self.do_init(catools) catools.caget.side_effect = [[caenum(0), castr("")]] self.o.caput("") catools.caput.assert_called_once_with( ['PV:PRE:CsPort', 'PV:PRE:CsAxis'], (0, ''), wait=True ) assert self.b.cs.value == ""
class TestDetectorBlock(unittest.TestCase): def setUp(self): self.p = Process("proc") for c in detector_block("mri", config_dir="/tmp"): self.p.add_controller(c) self.p.start() self.b = self.p.block_view("mri") self.tmpdir = tempfile.mkdtemp() def tearDown(self): self.p.stop(timeout=2) shutil.rmtree(self.tmpdir) def test_init(self): assert list(self.b) == [ "meta", "health", "state", "disable", "reset", "mri", "layout", "design", "exports", "modified", "save", "completedSteps", "configuredSteps", "totalSteps", "validate", "configure", "run", "abort", "pause", "resume", "label", "datasets", "readoutTime", "frequencyAccuracy", "exposure", ] assert list(self.b.configure.meta.takes.elements) == [ "generator", "fileDir", "axesToMove", "breakpoints", "exposure", "formatName", "fileTemplate", ] assert self.b.label.value == "DemoDetector" def make_generator(self): linex = LineGenerator("stage_x", "mm", 0, 2, 3, alternate=True) liney = LineGenerator("stage_y", "mm", 0, 2, 2) compound = CompoundGenerator([liney, linex], [], [], 0.5) return compound def test_scan(self): self.b.configure(self.make_generator(), self.tmpdir) assert list(self.b.datasets.value.rows()) == [ [ "det.data", "det.h5", DatasetType.PRIMARY, 4, "/entry/data", "/entry/uid" ], [ "det.sum", "det.h5", DatasetType.SECONDARY, 4, "/entry/sum", "/entry/uid" ], [ "stage_y.value_set", "det.h5", DatasetType.POSITION_SET, 1, "/entry/stage_y_set", "", ], [ "stage_x.value_set", "det.h5", DatasetType.POSITION_SET, 1, "/entry/stage_x_set", "", ], ] filepath = os.path.join(self.tmpdir, "det.h5") with h5py.File(filepath, "r") as hdf: assert hdf["/entry/data"].shape == (1, 1, 120, 160) assert hdf["/entry/sum"].shape == (1, 1, 1, 1) assert hdf["/entry/uid"].shape == (1, 1, 1, 1) assert hdf["/entry/uid"][0][0][0][0] == 0 assert tuple(hdf["/entry/stage_x_set"]) == (0, 1, 2) assert tuple(hdf["/entry/stage_y_set"]) == (0, 2) fs = self.b.run_async() # Wait for 2 frames to be written and reported cothread.Sleep(1.3) assert self.b.completedSteps.value == 2 assert hdf["/entry/data"].shape == (1, 2, 120, 160) assert hdf["/entry/sum"].shape == (1, 2, 1, 1) assert hdf["/entry/uid"].shape == (1, 2, 1, 1) assert hdf["/entry/sum"][0][0][0][0] == 208036.0 assert hdf["/entry/uid"][0][0][0][0] == 1 assert hdf["/entry/sum"][0][1][0][0] == 970444.0 assert hdf["/entry/uid"][0][1][0][0] == 2 # pause self.b.pause(lastGoodStep=3) assert self.b.completedSteps.value == 3 # resume self.b.resume() # Wait for the rest before_end = time.time() fs.result(timeout=10) self.assertAlmostEqual(time.time() - before_end, 1.5, delta=0.25) # Check the rest of the data, including the blank assert hdf["/entry/data"].shape == (2, 3, 120, 160) assert hdf["/entry/sum"].shape == (2, 3, 1, 1) assert hdf["/entry/uid"].shape == (2, 3, 1, 1) assert hdf["/entry/sum"][0][2][0][0] == 0 assert hdf["/entry/uid"][0][2][0][0] == 0 assert hdf["/entry/sum"][1][2][0][0] == 1788628.0 assert hdf["/entry/uid"][1][2][0][0] == 7 assert hdf["/entry/sum"][1][0][0][0] == 208036.0 assert hdf["/entry/uid"][1][0][0][0] == 9 # Reset to close the file self.b.reset()
class PandABoxBlockMakerTest(unittest.TestCase): def setUp(self): self.client = Mock() self.process = Process() self.process.start() def tearDown(self): self.process.stop() def test_block_fields_adder(self): fields = OrderedDict() block_data = BlockData(2, "Adder description", fields) fields["INPA"] = FieldData("pos_mux", "", "Input A", ["A.OUT", "B.OUT"]) fields["INPB"] = FieldData("pos_mux", "", "Input B", ["A.OUT", "B.OUT"]) fields["DIVIDE"] = FieldData( "param", "enum", "Divide output", ["/1", "/2", "/4"] ) fields["OUT"] = FieldData("pos_out", "", "Output", ["No", "Capture"]) fields["HEALTH"] = FieldData("read", "enum", "What's wrong", ["OK", "Very Bad"]) o = PandABlockController(self.client, "MRI", "ADDER1", block_data, "/docs") self.process.add_controller(o) b = self.process.block_view("MRI:ADDER1") assert list(b) == [ "meta", "health", "icon", "label", "help", "inputs", "inpa", "inpb", "parameters", "divide", "outputs", "out", ] group = b.inputs assert group.meta.tags == ["widget:group", "config:1"] inpa = b.inpa assert inpa.meta.writeable is True assert inpa.meta.typeid == ChoiceMeta.typeid assert inpa.meta.tags == [ "group:inputs", "sinkPort:int32:ZERO", "widget:combo", "config:1", ] assert inpa.meta.choices == ["A.OUT", "B.OUT"] inpa.put_value("A.OUT") self.client.set_field.assert_called_once_with("ADDER1", "INPA", "A.OUT") self.client.reset_mock() divide = b.divide assert divide.meta.writeable is True assert divide.meta.typeid == ChoiceMeta.typeid assert divide.meta.tags == ["group:parameters", "widget:combo", "config:1"] assert divide.meta.choices == ["/1", "/2", "/4"] out = b.out assert out.meta.writeable is False assert out.meta.typeid == NumberMeta.typeid assert out.meta.dtype == "int32" assert out.meta.tags == [ "group:outputs", "sourcePort:int32:ADDER1.OUT", "widget:textupdate", ] queue = Queue() subscribe = Subscribe(path=["MRI:ADDER1", "out"], delta=True) subscribe.set_callback(queue.put) o.handle_request(subscribe) delta = queue.get(timeout=1) assert delta.changes[0][1]["value"] == 0 ts = TimeStamp() o.handle_changes({"OUT": "145"}, ts) delta = queue.get(timeout=1) assert delta.changes == [ [["value"], 145], [["timeStamp"], ts], ] subscribe = Subscribe(path=["MRI:ADDER1", "health"], delta=True) subscribe.set_callback(queue.put) o.handle_request(subscribe) delta = queue.get(timeout=1) assert delta.changes[0][1]["value"] == "OK" ts = TimeStamp() o.handle_changes({"HEALTH": "Very Bad"}, ts) delta = queue.get(timeout=1) assert delta.changes == [ [["value"], "Very Bad"], [["alarm"], Alarm.major("Very Bad")], [["timeStamp"], ts], ] o.handle_changes({"HEALTH": "OK"}, ts) delta = queue.get(timeout=1) assert delta.changes == [ [["value"], "OK"], [["alarm"], Alarm.ok], [["timeStamp"], ts], ] def test_block_fields_pulse(self): fields = OrderedDict() block_data = BlockData(4, "Pulse description", fields) fields["DELAY"] = FieldData("time", "", "Time", []) fields["INP"] = FieldData("bit_mux", "", "Input", ["ZERO", "X.OUT", "Y.OUT"]) fields["OUT"] = FieldData("bit_out", "", "Output", []) fields["ERR_PERIOD"] = FieldData("read", "bit", "Error", []) o = PandABlockController(self.client, "MRI", "PULSE2", block_data, "/docs") self.process.add_controller(o) b = self.process.block_view("MRI:PULSE2") assert list(b) == [ "meta", "health", "icon", "label", "help", "parameters", "delay", "delayUnits", "inputs", "inp", "inpDelay", "outputs", "out", "readbacks", "errPeriod", ] assert b.meta.label == "Pulse description 2" assert b.label.value == "Pulse description 2" # check setting label b.label.put_value("A new label") assert b.meta.label == "A new label" assert b.label.value == "A new label" self.client.set_field.assert_called_once_with( "*METADATA", "LABEL_PULSE2", "A new label" ) self.client.set_field.reset_mock() # check updated with nothing o.handle_changes(dict(LABEL=""), ts=TimeStamp()) assert b.meta.label == "Pulse description 2" assert b.label.value == "Pulse description 2" self.client.set_field.assert_not_called() # check updated with something from the server o.handle_changes(dict(LABEL="A server label"), ts=TimeStamp()) assert b.meta.label == "A server label" assert b.label.value == "A server label" self.client.set_field.assert_not_called() help = b.help assert help.value == "/docs/build/pulse_doc.html" delay = b.delay assert delay.meta.writeable is True assert delay.meta.typeid == NumberMeta.typeid assert delay.meta.dtype == "float64" assert delay.meta.tags == ["group:parameters", "widget:textinput", "config:2"] units = b.delayUnits assert units.meta.writeable is True assert units.meta.typeid == ChoiceMeta.typeid assert units.meta.tags == ["group:parameters", "widget:combo", "config:1"] assert units.meta.choices == ["s", "ms", "us"] inp = b.inp assert inp.meta.writeable is True assert inp.meta.typeid == ChoiceMeta.typeid assert inp.meta.tags == [ "group:inputs", "sinkPort:bool:ZERO", "widget:combo", "badgevalue:plus:inpDelay:MRI:PULSE2", "config:1", ] assert inp.meta.choices == ["ZERO", "X.OUT", "Y.OUT"] delay = b.inpDelay assert delay.meta.writeable is True assert delay.meta.typeid == NumberMeta.typeid assert delay.meta.dtype == "uint8" assert delay.meta.tags == ["group:inputs", "widget:textinput", "config:1"] out = b.out assert out.meta.writeable is False assert out.meta.typeid == BooleanMeta.typeid assert out.meta.tags == [ "group:outputs", "sourcePort:bool:PULSE2.OUT", "widget:led", ] err = b.errPeriod assert err.meta.writeable is False assert err.meta.typeid == BooleanMeta.typeid assert err.meta.tags == ["group:readbacks", "widget:led"] queue = Queue() subscribe = Subscribe(path=["MRI:PULSE2", "inp"], delta=True) subscribe.set_callback(queue.put) o.handle_request(subscribe) delta = queue.get() assert delta.changes[0][1]["value"] == "ZERO" ts = TimeStamp() o.handle_changes({"INP": "X.OUT"}, ts) delta = queue.get() assert delta.changes == [ [["value"], "X.OUT"], [["timeStamp"], ts], [ ["meta", "tags"], [ "group:inputs", "sinkPort:bool:ZERO", "widget:combo", "badgevalue:plus:inpDelay:MRI:PULSE2", "config:1", "linkedvalue:out:MRI:X", ], ], ] def test_block_fields_lut(self): fields = OrderedDict() block_data = BlockData(8, "Lut description", fields) fields["FUNC"] = FieldData("param", "lut", "Function", []) o = PandABlockController(self.client, "MRI", "LUT3", block_data, "/docs") self.process.add_controller(o) b = self.process.block_view("MRI:LUT3") func = b.func assert func.meta.writeable is True assert func.meta.typeid == StringMeta.typeid assert func.meta.tags == ["group:parameters", "widget:textinput", "config:1"] queue = Queue() subscribe = Subscribe(path=["MRI:LUT3"], delta=True) subscribe.set_callback(queue.put) o.handle_request(subscribe) delta = queue.get() assert delta.changes[0][1]["func"]["value"] == "" assert '<path id="OR"' in delta.changes[0][1]["icon"]["value"] # This is the correct FUNC.RAW value for !A&!B&!C&!D&!E self.client.get_field.return_value = "1" ts = TimeStamp() o.handle_changes({"FUNC": "!A&!B&!C&!D&!E"}, ts) self.client.get_field.assert_called_once_with("LUT3", "FUNC.RAW") delta = queue.get() assert delta.changes == [ [["func", "value"], "!A&!B&!C&!D&!E"], [["func", "timeStamp"], ts], [["icon", "value"], ANY], [["icon", "timeStamp"], ts], ] assert '<path id="OR"' not in delta.changes[2][1]
class TestScanBlock(unittest.TestCase): def setUp(self): self.p = Process("proc") for c in (detector_block("DETECTOR", config_dir="/tmp") + motion_block("MOTION", config_dir="/tmp") + scan_1det_block("SCANMRI", config_dir="/tmp")): self.p.add_controller(c) self.pub = PublishController("PUB") self.p.add_controller(self.pub) self.p.start() self.b = self.p.block_view("SCANMRI") self.bd = self.p.block_view("DETECTOR") self.tmpdir = tempfile.mkdtemp() def tearDown(self): self.p.stop(timeout=2) shutil.rmtree(self.tmpdir) def test_init(self): assert self.b.label.value == "Mapping x, y with demo detector" assert list(self.b.configure.meta.defaults["detectors"].rows()) == [[ True, "DET", "DETECTOR", 0.0, 1 ]] assert self.pub.published == [ "SCANMRI", "PUB", "DETECTOR", "MOTION", "MOTION:COUNTERX", "MOTION:COUNTERY", ] def make_generator(self): linex = LineGenerator("x", "mm", 0, 2, 3, alternate=True) liney = LineGenerator("y", "mm", 0, 2, 2) compound = CompoundGenerator([liney, linex], [], [], 0.1) return compound def test_validate(self): compound = self.make_generator() detectors = DetectorTable.from_rows([[True, "DET", "DETECTOR", 0.0, 1]]) ret = self.b.validate(compound, self.tmpdir, detectors=detectors) assert list(ret["detectors"].rows()) == [[ True, "DET", "DETECTOR", 0.098995, 1 ]] def prepare_half_run(self): compound = self.make_generator() self.b.configure(compound, self.tmpdir, axesToMove=["x"], fileTemplate="my-%s.h5") def test_configure(self): self.prepare_half_run() assert list(self.b.datasets.value.rows()) == [ [ "DET.data", "my-DET.h5", DatasetType.PRIMARY, 4, "/entry/data", "/entry/uid", ], [ "DET.sum", "my-DET.h5", DatasetType.SECONDARY, 4, "/entry/sum", "/entry/uid", ], [ "y.value_set", "my-DET.h5", DatasetType.POSITION_SET, 1, "/entry/y_set", "", ], [ "x.value_set", "my-DET.h5", DatasetType.POSITION_SET, 1, "/entry/x_set", "", ], ] for b in (self.b, self.bd): assert b.completedSteps.value == 0 assert b.configuredSteps.value == 3 assert b.totalSteps.value == 6 def test_run(self): self.prepare_half_run() assert self.b.state.value == "Armed" self.b.run() for b in (self.b, self.bd): assert b.completedSteps.value == 3 assert b.configuredSteps.value == 6 assert b.totalSteps.value == 6 assert b.state.value == "Armed" self.b.run() for b in (self.b, self.bd): assert b.completedSteps.value == 6 assert b.configuredSteps.value == 6 assert b.totalSteps.value == 6 assert b.state.value == "Finished" self.b.reset() for b in (self.b, self.bd): assert b.state.value == "Ready"