Exemplo n.º 1
0
 def setUp(self):
     self.process = Process("Process")
     self.child = self.create_child_block(cs_block,
                                          self.process,
                                          mri="PMAC:CS1",
                                          pv_prefix="PV:PRE")
     self.set_attributes(self.child, port="PMAC2CS1")
     c = ManagerController("PMAC", "/tmp")
     c.add_part(CSPart(mri="PMAC:CS1", cs=1))
     self.process.add_controller(c)
     self.process.start()
     self.b = c.block_view()
Exemplo n.º 2
0
 def setUp(self):
     self.process = Process("Process")
     child = self.create_child_block(
         pmac_status_block, self.process, mri="my_mri", pv_prefix="PV:PRE"
     )
     self.set_attributes(child, servoFreq=2500.04)
     c = ManagerController("PMAC", "/tmp")
     self.o = PmacStatusPart(name="part", mri="my_mri", initial_visibility=True)
     c.add_part(self.o)
     self.process.add_controller(c)
     self.process.start()
     self.b = c.block_view()
Exemplo n.º 3
0
 def setUp(self):
     self.process = Process("Process")
     self.child = self.create_child_block(
         pmac_trajectory_block, self.process, mri="PMAC:TRAJ", pv_prefix="PV:PRE"
     )
     c = ManagerController("PMAC", "/tmp", use_git=False)
     self.o = PmacTrajectoryPart(name="pmac", mri="PMAC:TRAJ")
     c.add_part(self.o)
     self.process.add_controller(c)
     self.process.start()
     self.b = c.block_view()
     self.set_attributes(self.child, trajectoryProgVersion=2)
Exemplo n.º 4
0
class TestChildPart(unittest.TestCase):
    def checkState(self, state):
        assert self.c.state.value == state

    def makeChildBlock(self, block_mri):
        controller = BasicController(block_mri)
        controller.add_part(PortsPart(name="Connector%s" % block_mri[-1]))
        part = ChildPart(
            mri=block_mri,
            name="part%s" % block_mri,
            stateful=False,
            initial_visibility=True,
        )
        self.p.add_controller(controller)
        return part, controller

    def setUp(self):
        self.p = Process("process1")

        self.p1, self.c1 = self.makeChildBlock("child1")
        self.p2, self.c2 = self.makeChildBlock("child2")
        self.p3, self.c3 = self.makeChildBlock("child3")
        self.c1._block.sinkportConnector.set_value("Connector3")
        self.c2._block.sinkportConnector.set_value("Connector1")
        self.c3._block.sinkportConnector.set_value("Connector2")

        # create a root block for the child blocks to reside in
        self.c = ManagerController(mri="mainBlock", config_dir="/tmp")
        for part in [self.p1, self.p2, self.p3]:
            self.c.add_part(part)
        self.p.add_controller(self.c)

        # Start the process
        # check that do_initial_reset works asynchronously
        assert self.c.state.value == sm.DISABLED
        self.p.start()
        assert self.c.state.value == sm.READY

    def tearDown(self):
        self.p.stop(timeout=1)

    def test_init(self):
        for controller in (self.c1, self.c2, self.c3):
            b = self.p.block_view(controller.mri)
            assert b.sourceportConnector.value == ""
        assert self.c.exports.meta.elements["source"].choices == [
            "partchild1.health",
            "partchild1.sinkportConnector",
            "partchild1.sourceportConnector",
            "partchild2.health",
            "partchild2.sinkportConnector",
            "partchild2.sourceportConnector",
            "partchild3.health",
            "partchild3.sinkportConnector",
            "partchild3.sourceportConnector",
        ]
        assert len(self.c.port_info) == 3
        port_info = self.c.port_info["partchild1"]
        assert len(port_info) == 2
        info_in = port_info[0]
        assert isinstance(info_in, SinkPortInfo)
        assert info_in.name == "sinkportConnector"
        assert info_in.port == Port.INT32
        assert info_in.value == "Connector3"
        assert info_in.disconnected_value == ""
        info_out = port_info[1]
        assert isinstance(info_out, SourcePortInfo)
        assert info_out.name == "sourceportConnector"
        assert info_out.port == Port.INT32
        assert info_out.connected_value == "Connector1"

    def test_layout(self):
        b = self.p.block_view("mainBlock")

        new_layout = LayoutTable(
            name=["partchild1", "partchild2", "partchild3"],
            mri=["part1", "part2", "part3"],
            x=[10, 11, 12],
            y=[20, 21, 22],
            visible=[True, True, True],
        )
        b.layout.put_value(new_layout)
        assert self.c.parts["partchild1"].x == 10
        assert self.c.parts["partchild1"].y == 20
        assert self.c.parts["partchild1"].visible == AVisibleArray(True)
        assert self.c.parts["partchild2"].x == 11
        assert self.c.parts["partchild2"].y == 21
        assert self.c.parts["partchild2"].visible == AVisibleArray(True)
        assert self.c.parts["partchild3"].x == 12
        assert self.c.parts["partchild3"].y == 22
        assert self.c.parts["partchild3"].visible == AVisibleArray(True)

        new_layout.visible = [True, False, True]
        b.layout.put_value(new_layout)
        assert self.c.parts["partchild1"].visible == AVisibleArray(True)
        assert self.c.parts["partchild2"].visible == AVisibleArray(False)
        assert self.c.parts["partchild3"].visible == AVisibleArray(True)

    def test_sever_all_sink_ports(self):
        b = self.p.block_view("mainBlock")
        b1, b2, b3 = (self.c1.block_view(), self.c2.block_view(),
                      self.c3.block_view())
        new_layout = dict(name=["partchild1"],
                          mri=[""],
                          x=[0],
                          y=[0],
                          visible=[False])
        b.layout.put_value(new_layout)
        assert b1.sinkportConnector.value == ""
        assert b2.sinkportConnector.value == ""
        assert b3.sinkportConnector.value == "Connector2"

    def test_load_save(self):
        b1 = self.c1.block_view()
        context = Context(self.p)
        structure1 = self.p1.on_save(context)
        expected = dict(sinkportConnector="Connector3")
        assert structure1 == expected
        b1.sinkportConnector.put_value("blah")
        structure2 = self.p1.on_save(context)
        expected = dict(sinkportConnector="blah")
        assert structure2 == expected
        self.p1.on_load(context, dict(sinkportConnector="blah_again"))
        assert b1.sinkportConnector.value == "blah_again"
class TestKinematicsSavuPart(ChildTestCase):
    def setUp(self):

        self.process = Process("Process")
        self.context = Context(self.process)

        # Create a fake PandA
        self.panda = ManagerController("PANDA", "/tmp", use_git=False)
        self.busses = PositionsPart("busses")
        self.panda.add_part(self.busses)
        self.process.add_controller(self.panda)

        # TODO: Add axes to the positions table

        # And the PMAC
        pmac_block = make_block_creator(
            os.path.join(os.path.dirname(__file__), "..", "test_pmac", "blah"),
            "test_pmac_manager_block.yaml",
        )
        self.config_dir = tmp_dir("config_dir")
        self.pmac = self.create_child_block(
            pmac_block,
            self.process,
            mri_prefix="PMAC",
            config_dir=self.config_dir.value,
        )
        # These are the motors we are interested in
        self.child_x = self.process.get_controller("BL45P-ML-STAGE-01:X")
        self.child_y = self.process.get_controller("BL45P-ML-STAGE-01:Y")
        # Set up port and kinematics on CS1
        self.child_cs1 = self.process.get_controller("PMAC:CS1")
        self.set_attributes(self.child_cs1, port="CS1")
        self.set_attributes(self.child_cs1, qVariables="Q22=12345 Q23=999")
        self.set_attributes(self.child_cs1,
                            forwardKinematic="Q1=P1+10 Q5=Q22+4 Q7=8+4 RET ")
        # Set up variables on CS1
        self.child_status = self.process.get_controller("PMAC:STATUS")
        self.set_attributes(self.child_status, iVariables="I12=3")
        self.set_attributes(self.child_status, pVariables="")
        self.set_attributes(self.child_status, mVariables="M45=1 M42=561")

        # Make the child block holding panda and pmac mri
        self.child = self.create_child_block(
            panda_kinematicssavu_block,
            self.process,
            mri="SCAN:KINSAV",
            panda="PANDA",
            pmac="PMAC",
        )

        self.o = KinematicsSavuPart(name="kinsav",
                                    mri="SCAN:KINSAV",
                                    cs_port="CS1",
                                    cs_mri_suffix="CS1")
        self.context.set_notify_dispatch_request(
            self.o.notify_dispatch_request)
        self.process.start()
        self.set_attributes(self.pmac)

        self.completed_steps = 0
        # goal for these is 3000, 2000, True
        cols, rows, alternate = 3000, 2000, False
        self.steps_to_do = cols * rows
        xs = LineGenerator("x", "mm", 0.0, 0.1, cols, alternate=alternate)
        ys = LineGenerator("y", "mm", 0.0, 0.1, rows)
        self.generator = CompoundGenerator([ys, xs], [], [], 0.1)
        self.generator.prepare()

    def tearDown(self):
        self.process.stop(timeout=1)
        shutil.rmtree(self.config_dir.value)

    def set_motor_attributes(
        self,
        x_pos=0.5,
        y_pos=0.0,
        units="mm",
        x_acceleration=2.5,
        y_acceleration=2.5,
        x_velocity=1.0,
        y_velocity=1.0,
    ):
        # create some parts to mock the motion controller and 2 axes in a CS
        self.set_attributes(
            self.child_x,
            cs="CS1,A",
            accelerationTime=x_velocity / x_acceleration,
            resolution=0.001,
            offset=0.0,
            maxVelocity=x_velocity,
            readback=x_pos,
            velocitySettle=0.0,
            units=units,
            axisNumber=1,
        )
        self.set_attributes(
            self.child_y,
            cs="CS1,X",
            accelerationTime=y_velocity / y_acceleration,
            resolution=0.001,
            offset=0.0,
            maxVelocity=y_velocity,
            readback=y_pos,
            velocitySettle=0.0,
            units=units,
            axisNumber=23,
        )

    def test_file_creation(self):
        tmp_dir = mkdtemp() + os.path.sep
        data_name = "p00-1234"
        file_template = data_name + "-%s.h5"
        self.set_motor_attributes(0.5, 0.0, "mm")

        xs = LineGenerator("x", "mm", 0.0, 0.4, 5, alternate=False)
        ys = LineGenerator("y", "mm", 1.2, 1.6, 5)
        generator = CompoundGenerator([ys, xs], [], [], 1.0)
        generator.prepare()

        part_info = dict(HDF=[
            DatasetProducedInfo(
                "y.data",
                "kinematics_PANDABOX2.h5",
                DatasetType.POSITION_VALUE,
                2,
                "/entry/NDAttributes/INENC1.VAL",
                "/p/uid",
            ),
            DatasetProducedInfo(
                "x.data",
                "kinematics_PANDABOX.h5",
                DatasetType.POSITION_VALUE,
                0,
                "/entry/NDAttributes/INENC1.VAL",
                "/p/uid",
            ),
            DatasetProducedInfo(
                "y.max",
                "kinematics_PANDABOX2.h5",
                DatasetType.POSITION_MAX,
                0,
                "/entry/NDAttributes/INENC1_MAX.VAL",
                "/p/uid",
            ),
            DatasetProducedInfo(
                "y.min",
                "kinematics_PANDABOX2.h5",
                DatasetType.POSITION_MIN,
                0,
                "/entry/NDAttributes/INENC1_MIN.VAL",
                "/p/uid",
            ),
            DatasetProducedInfo(
                "x.max",
                "kinematics_PANDABOX.h5",
                DatasetType.POSITION_MAX,
                0,
                "/entry/NDAttributes/INENC1_MAX.VAL",
                "/p/uid",
            ),
            DatasetProducedInfo(
                "x.min",
                "kinematics_PANDABOX.h5",
                DatasetType.POSITION_MIN,
                0,
                "/entry/NDAttributes/INENC1_MIN.VAL",
                "/p/uid",
            ),
            DatasetProducedInfo("det.min", "fn1", DatasetType.SECONDARY, 0,
                                "/p/s2", "/p/uid"),
        ])

        kin_infos = [
            DatasetProducedInfo(
                "y.mean",
                "p00-1234-kinematics-vds.nxs",
                DatasetType.POSITION_VALUE,
                2,
                "/entry/y.mean",
                "",
            ),
            DatasetProducedInfo(
                "y.max",
                "p00-1234-kinematics-vds.nxs",
                DatasetType.POSITION_MAX,
                2,
                "/entry/y.max",
                "",
            ),
            DatasetProducedInfo(
                "y.min",
                "p00-1234-kinematics-vds.nxs",
                DatasetType.POSITION_MIN,
                2,
                "/entry/y.min",
                "",
            ),
            DatasetProducedInfo(
                "x.mean",
                "p00-1234-kinematics-vds.nxs",
                DatasetType.POSITION_VALUE,
                2,
                "/entry/x.mean",
                "",
            ),
            DatasetProducedInfo(
                "x.max",
                "p00-1234-kinematics-vds.nxs",
                DatasetType.POSITION_MAX,
                2,
                "/entry/x.max",
                "",
            ),
            DatasetProducedInfo(
                "x.min",
                "p00-1234-kinematics-vds.nxs",
                DatasetType.POSITION_MIN,
                2,
                "/entry/x.min",
                "",
            ),
        ]

        infos = self.o.on_configure(
            self.context,
            fileDir=tmp_dir,
            generator=generator,
            axesToMove=AXES,
            fileTemplate=file_template,
            part_info=part_info,
        )

        self.assertEqual(len(infos), len(kin_infos))
        for info in kin_infos:
            assert str(info) in [str(x) for x in infos]

        self.o.on_post_configure(self.context, part_info)

        # Check Savu file has been created and contains the correct entries
        savu_path = os.path.join(tmp_dir, data_name + "-savu.nxs")
        savu_file = h5py.File(savu_path, "r")

        # Check the forward kinematics program has been written
        useminmax_dataset = savu_file["/entry/inputs/use_minmax"]
        self.assertEqual(useminmax_dataset[0], True)

        # Check the forward kinematics program has been written
        program_dataset = savu_file["/entry/inputs/program"]
        self.assertEqual(program_dataset.shape, (1, ))
        self.assertEqual(program_dataset[0], "Q1=P1+10 Q5=Q22+4 Q7=8+4 RET ")

        # Check the Q and I program variables have been written
        variables_dataset = savu_file["/entry/inputs/variables"]
        self.assertEqual(variables_dataset.shape, (5, ))

        found = False
        for dataset in variables_dataset:
            if dataset[0] == "Q22":
                found = True
                self.assertEqual(dataset[1], 12345)
        self.assertTrue(found)

        # Check the p1 datasets have been written

        # Create raw data file first that the file will link to
        raw_path = os.path.join(tmp_dir, "kinematics_PANDABOX.h5")
        raw = h5py.File(raw_path, "w", libver="latest")
        raw.require_group("/entry/NDAttributes/")
        fmnd_mean = np.zeros((5, 5, 1, 1))
        fmnd_mean[0][0][0][0] = 61616
        fmnd_mean[1][2][0][0] = 10101
        raw.create_dataset("/entry/NDAttributes/INENC1.VAL", data=fmnd_mean)
        fmnd_max = np.zeros((5, 5, 1, 1))
        fmnd_max[0][0][0][0] = 27
        fmnd_max[3][4][0][0] = 18
        raw.create_dataset("/entry/NDAttributes/INENC1_MAX.VAL", data=fmnd_max)
        fmnd_max = np.zeros((5, 5, 1, 1))
        fmnd_max[0][0][0][0] = 54
        fmnd_max[3][4][0][0] = 76
        raw.create_dataset("/entry/NDAttributes/INENC1_MIN.VAL", data=fmnd_max)

        # Create raw data file first that the file will link to
        raw_path2 = os.path.join(tmp_dir, "kinematics_PANDABOX2.h5")
        raw2 = h5py.File(raw_path2, "w", libver="latest")
        raw2.require_group("/entry/NDAttributes/")
        fmnd_mean = np.zeros((5, 5, 1, 1))
        fmnd_mean[0][0][0][0] = 12345
        fmnd_mean[1][2][0][0] = 54321
        raw2.create_dataset("/entry/NDAttributes/INENC1.VAL", data=fmnd_mean)
        fmnd_max = np.zeros((5, 5, 1, 1))
        fmnd_max[0][0][0][0] = 99
        fmnd_max[3][4][0][0] = 88
        raw2.create_dataset("/entry/NDAttributes/INENC1_MAX.VAL",
                            data=fmnd_max)
        fmnd_max = np.zeros((5, 5, 1, 1))
        fmnd_max[0][0][0][0] = 76
        fmnd_max[3][4][0][0] = 44
        raw2.create_dataset("/entry/NDAttributes/INENC1_MIN.VAL",
                            data=fmnd_max)

        raw.swmr_mode = True
        raw2.swmr_mode = True

        # Check p1 mean and max datasets are there
        p1mean_dataset = savu_file["/entry/inputs/p1mean"]
        self.assertEqual(p1mean_dataset.shape, (5, 5, 1, 1))
        self.assertEqual(p1mean_dataset[0][0][0][0], 61616)
        self.assertEqual(p1mean_dataset[1][2][0][0], 10101)
        p1max_dataset = savu_file["/entry/inputs/p1max"]
        self.assertEqual(p1max_dataset.shape, (5, 5, 1, 1))
        self.assertEqual(p1max_dataset[0][0][0][0], 27)
        self.assertEqual(p1max_dataset[3][4][0][0], 18)
        savu_file.close()

        # Check the final vds file has been created
        # First create a fake Savu output file that the vds will link to
        raw_savu_path = os.path.join(tmp_dir, data_name + "-savuproc")
        os.mkdir(raw_savu_path)
        raw_savu_path = os.path.join(raw_savu_path,
                                     data_name + "-savu_processed.nxs")
        raw_savu = h5py.File(raw_savu_path, "w")
        raw_savu.require_group("/entry/final_result_qmean/")
        savu_proc = np.ones((9, 5, 5))
        savu_proc[0][0][0] = 555
        savu_proc[0][2][1] = 666
        raw_savu.create_dataset("/entry/final_result_qmean/data",
                                data=savu_proc)
        raw_savu.close()

        # Check xmean is there
        vds_path = os.path.join(tmp_dir, data_name + "-kinematics-vds.nxs")
        vds_file = h5py.File(vds_path, "r")
        xmean = vds_file["/entry/x.mean"]
        self.assertEqual(xmean.shape, (5, 5))
        self.assertEqual(xmean[0][0], 555)
        self.assertEqual(xmean[2][1], 666)
        vds_file.close()
        raw.close()
        raw2.close()

        rmtree(tmp_dir)
class TestPandaSeqTriggerPart(ChildTestCase):
    def setUp(self):
        self.process = Process("Process")
        self.context = Context(self.process)

        # Create a fake PandA
        self.panda = ManagerController("PANDA", "/tmp", use_git=False)
        self.busses = PositionsPart("busses")
        self.panda.add_part(self.busses)

        # Make 2 sequencers we can prod
        self.seq_parts = {}
        for i in (1, 2):
            controller = BasicController("PANDA:SEQ%d" % i)
            self.seq_parts[i] = SequencerPart("part")
            controller.add_part(self.seq_parts[i])
            self.process.add_controller(controller)
            self.panda.add_part(
                ChildPart(
                    "SEQ%d" % i,
                    "PANDA:SEQ%d" % i,
                    initial_visibility=True,
                    stateful=False,
                ))
        self.child_seq1 = self.process.get_controller("PANDA:SEQ1")
        self.child_seq2 = self.process.get_controller("PANDA:SEQ2")

        # And an srgate
        controller = BasicController("PANDA:SRGATE1")
        self.gate_part = GatePart("part")
        controller.add_part(self.gate_part)
        self.process.add_controller(controller)
        self.panda.add_part(
            ChildPart("SRGATE1",
                      "PANDA:SRGATE1",
                      initial_visibility=True,
                      stateful=False))
        self.process.add_controller(self.panda)

        # And the PMAC
        pmac_block = make_block_creator(
            os.path.join(os.path.dirname(__file__), "..", "test_pmac", "blah"),
            "test_pmac_manager_block.yaml",
        )
        self.config_dir = tmp_dir("config_dir")
        self.pmac = self.create_child_block(
            pmac_block,
            self.process,
            mri_prefix="PMAC",
            config_dir=self.config_dir.value,
        )
        # These are the motors we are interested in
        self.child_x = self.process.get_controller("BL45P-ML-STAGE-01:X")
        self.child_y = self.process.get_controller("BL45P-ML-STAGE-01:Y")
        self.child_cs1 = self.process.get_controller("PMAC:CS1")
        # CS1 needs to have the right port otherwise we will error
        self.set_attributes(self.child_cs1, port="CS1")

        # Make the child block holding panda and pmac mri
        self.child = self.create_child_block(
            panda_seq_trigger_block,
            self.process,
            mri="SCAN:PCOMP",
            panda="PANDA",
            pmac="PMAC",
        )

        # And our part under test
        self.o = PandASeqTriggerPart("pcomp", "SCAN:PCOMP")

        # Now start the process off and tell the panda which sequencer tables
        # to use
        self.process.start()
        exports = ExportTable.from_rows([
            ("SEQ1.table", "seqTableA"),
            ("SEQ2.table", "seqTableB"),
            ("SRGATE1.forceSet", "seqSetEnable"),
            ("SRGATE1.forceReset", "seqReset"),
        ])
        self.panda.set_exports(exports)

    def tearDown(self):
        self.process.stop(timeout=2)
        shutil.rmtree(self.config_dir.value)

    def set_motor_attributes(
        self,
        x_pos=0.5,
        y_pos=0.0,
        units="mm",
        x_acceleration=2.5,
        y_acceleration=2.5,
        x_velocity=1.0,
        y_velocity=1.0,
    ):
        # create some parts to mock the motion controller and 2 axes in a CS
        self.set_attributes(
            self.child_x,
            cs="CS1,A",
            accelerationTime=x_velocity / x_acceleration,
            resolution=0.001,
            offset=0.0,
            maxVelocity=x_velocity,
            readback=x_pos,
            velocitySettle=0.0,
            units=units,
        )
        self.set_attributes(
            self.child_y,
            cs="CS1,B",
            accelerationTime=y_velocity / y_acceleration,
            resolution=0.001,
            offset=0.0,
            maxVelocity=y_velocity,
            readback=y_pos,
            velocitySettle=0.0,
            units=units,
        )

    # Patch the super setup() method so we only get desired calls
    @patch("malcolm.modules.builtin.parts.ChildPart.setup")
    def test_setup(self, mocked_super_setup):
        mock_registrar = Mock(name="mock_registrar")
        call_list = [
            call(ReportStatusHook, self.o.on_report_status),
            call((ConfigureHook, SeekHook), self.o.on_configure),
            call(RunHook, self.o.on_run),
            call(ResetHook, self.o.on_reset),
            call((AbortHook, PauseHook), self.o.on_abort),
            call(PostRunArmedHook, self.o.post_inner_scan),
        ]

        self.o.setup(mock_registrar)

        mocked_super_setup.assert_called_once_with(mock_registrar)
        mock_registrar.hook.assert_has_calls(call_list, any_order=True)

    @patch(
        "malcolm.modules.ADPandABlocks.parts.pandaseqtriggerpart.DoubleBuffer",
        autospec=True,
    )
    def test_configure_and_run_prepare_components(self, buffer_class):
        buffer_instance = buffer_class.return_value
        buffer_instance.run.return_value = []

        xs = LineGenerator("x", "mm", 0.0, 0.3, 4, alternate=True)
        ys = LineGenerator("y", "mm", 0.0, 0.1, 2)
        generator = CompoundGenerator([ys, xs], [], [], 1.0)
        generator.prepare()
        completed_steps = 0
        steps_to_do = generator.size
        self.set_motor_attributes()
        axes_to_move = ["x", "y"]

        self.o.on_configure(self.context, completed_steps, steps_to_do, {},
                            generator, axes_to_move)

        assert self.o.generator is generator
        assert self.o.loaded_up_to == completed_steps
        assert self.o.scan_up_to == completed_steps + steps_to_do

        # Other unit tests check that the sequencer rows used here are correct
        buffer_instance.configure.assert_called_once()

        self.gate_part.enable_set.assert_not_called()
        buffer_instance.run.assert_not_called()

        # The SRGate should only be enabled by on_pre_run() here.
        self.o.on_pre_run(self.context)
        self.o.on_run(self.context)
        self.gate_part.enable_set.assert_called_once()
        buffer_instance.run.assert_called_once()

    @patch(
        "malcolm.modules.ADPandABlocks.parts.pandaseqtriggerpart.DoubleBuffer",
        autospec=True,
    )
    def test_configure_and_run_prepare_no_axes(self, buffer_class):
        buffer_instance = buffer_class.return_value
        buffer_instance.run.return_value = []

        generator = CompoundGenerator([StaticPointGenerator(size=1)], [], [],
                                      1.0)
        generator.prepare()

        completed_steps = 0
        steps_to_do = generator.size

        self.o.on_configure(self.context, completed_steps, steps_to_do, {},
                            generator, "")

        assert self.o.generator is generator
        assert self.o.loaded_up_to == completed_steps
        assert self.o.scan_up_to == completed_steps + steps_to_do

        buffer_instance.configure.assert_called_once()

        self.gate_part.enable_set.assert_not_called()
        buffer_instance.run.assert_not_called()

        # The SRGate should only be enabled by on_run() here.
        self.o.on_pre_run(self.context)
        self.o.on_run(self.context)
        self.gate_part.enable_set.assert_called_once()
        buffer_instance.run.assert_called_once()

    @patch(
        "malcolm.modules.ADPandABlocks.parts.pandaseqtriggerpart.PandASeqTriggerPart"
        ".on_abort",
        autospec=True,
    )
    def test_reset_triggers_abort(self, abort_method):
        abort_method.assert_not_called()
        self.o.on_reset(self.context)
        abort_method.assert_called_once()

    @patch(
        "malcolm.modules.ADPandABlocks.parts.pandaseqtriggerpart.DoubleBuffer.clean_up",
        autospec=True,
    )
    def test_abort_cleans_up_correctly(self, db_clean_up_method):
        xs = LineGenerator("x", "mm", 0.0, 0.3, 4, alternate=True)
        ys = LineGenerator("y", "mm", 0.0, 0.1, 2)
        generator = CompoundGenerator([ys, xs], [], [], 1.0)
        generator.prepare()
        completed_steps = 0
        steps_to_do = generator.size
        self.set_motor_attributes()
        axes_to_move = ["x", "y"]

        db_clean_up_method.assert_not_called()
        self.o.on_configure(self.context, completed_steps, steps_to_do, {},
                            generator, axes_to_move)
        db_clean_up_method.assert_called_once()

        self.gate_part.seq_reset.assert_not_called()
        self.o.on_abort(self.context)
        self.gate_part.seq_reset.assert_called_once()
        assert 2 == db_clean_up_method.call_count

    def test_abort_does_not_throw_exception_if_no_seq_reset_exported(self):
        original_exports = self.panda.exports.value.rows()
        exports = ExportTable.from_rows(
            [row for row in original_exports if row[1] != "seqReset"])
        self.panda.set_exports(exports)

        xs = LineGenerator("x", "mm", 0.0, 0.3, 4, alternate=True)
        ys = LineGenerator("y", "mm", 0.0, 0.1, 2)
        generator = CompoundGenerator([ys, xs], [], [], 1.0)
        generator.prepare()
        completed_steps = 0
        steps_to_do = generator.size
        self.set_motor_attributes()
        axes_to_move = ["x", "y"]

        self.o.on_configure(self.context, completed_steps, steps_to_do, {},
                            generator, axes_to_move)
        self.o.on_abort(self.context)

    @patch(
        "malcolm.modules.ADPandABlocks.parts.pandaseqtriggerpart.DoubleBuffer",
        autospec=True,
    )
    def get_sequencer_rows(self,
                           generator,
                           axes_to_move,
                           buffer_class,
                           steps=None):
        """Helper method for comparing table values."""

        buffer_instance = buffer_class.return_value
        generator.prepare()
        completed_steps = 0
        steps_to_do = steps if steps is not None else generator.size

        self.o.on_configure(self.context, completed_steps, steps_to_do, {},
                            generator, axes_to_move)

        rows_gen = buffer_instance.configure.call_args[0][0]
        rows = SequencerRows()
        for rs in rows_gen:
            rows.extend(rs)

        return rows

    def test_configure_continuous(self):
        xs = LineGenerator("x", "mm", 0.0, 0.3, 4, alternate=True)
        ys = LineGenerator("y", "mm", 0.0, 0.1, 2)
        generator = CompoundGenerator([ys, xs], [], [], 1.0)
        self.set_motor_attributes()
        axes_to_move = ["x", "y"]

        seq_rows = self.get_sequencer_rows(generator, axes_to_move)
        # Triggers
        GT = Trigger.POSA_GT
        IT = Trigger.IMMEDIATE
        LT = Trigger.POSA_LT
        # Half a frame
        hf = 62500000
        # Half how long to be blind for
        hb = 22500000
        expected = SequencerRows()
        expected.add_seq_entry(count=1,
                               trigger=LT,
                               position=50,
                               half_duration=hf,
                               live=1,
                               dead=0)
        expected.add_seq_entry(3, IT, 0, hf, 1, 0)
        expected.add_seq_entry(1, IT, 0, hb, 0, 1)
        expected.add_seq_entry(1, GT, -350, hf, 1, 0)
        expected.add_seq_entry(3, IT, 0, hf, 1, 0)
        expected.add_seq_entry(1, IT, 0, MIN_PULSE, 0, 1)
        expected.add_seq_entry(0, IT, 0, MIN_PULSE, 0, 0)

        assert seq_rows.as_tuples() == expected.as_tuples()

    def test_configure_motion_controller_trigger(self):
        xs = LineGenerator("x", "mm", 0.0, 0.3, 4, alternate=True)
        ys = LineGenerator("y", "mm", 0.0, 0.1, 2)
        generator = CompoundGenerator([ys, xs], [], [], 1.0)
        self.set_motor_attributes()
        self.set_attributes(self.child, rowTrigger="Motion Controller")
        self.set_attributes(self.child_seq1, bita="TTLIN1.VAL")
        self.set_attributes(self.child_seq2, bita="TTLIN1.VAL")
        axes_to_move = ["x", "y"]

        seq_rows = self.get_sequencer_rows(generator, axes_to_move)
        # Triggers
        B0 = Trigger.BITA_0
        B1 = Trigger.BITA_1
        IT = Trigger.IMMEDIATE
        # Half a frame
        hf = 62500000
        expected = SequencerRows()
        expected.add_seq_entry(count=1,
                               trigger=B1,
                               position=0,
                               half_duration=hf,
                               live=1,
                               dead=0)
        expected.add_seq_entry(3, IT, 0, hf, 1, 0)
        expected.add_seq_entry(1, B0, 0, MIN_PULSE, 0, 1)
        expected.add_seq_entry(1, B1, 0, hf, 1, 0)
        expected.add_seq_entry(3, IT, 0, hf, 1, 0)
        expected.add_seq_entry(1, IT, 0, MIN_PULSE, 0, 1)
        expected.add_seq_entry(0, IT, 0, MIN_PULSE, 0, 0)

        assert seq_rows.as_tuples() == expected.as_tuples()

    # AssertionError is thrown as inputs are not are not set for SEQ bitA.
    def test_configure_assert_stepped(self):
        xs = LineGenerator("x", "mm", 0.0, 0.3, 4, alternate=True)
        ys = LineGenerator("y", "mm", 0.0, 0.1, 2)
        generator = CompoundGenerator([ys, xs], [], [], 1.0, continuous=False)
        generator.prepare()
        completed_steps = 0
        steps_to_do = generator.size
        self.set_motor_attributes()
        self.set_attributes(self.child, rowTrigger="Motion Controller")
        axes_to_move = ["x", "y"]

        with self.assertRaises(AssertionError):
            self.o.on_configure(self.context, completed_steps, steps_to_do, {},
                                generator, axes_to_move)

    def test_configure_stepped(self):
        xs = LineGenerator("x", "mm", 0.0, 0.3, 4)
        ys = LineGenerator("y", "mm", 0.0, 0.2, 3)
        generator = CompoundGenerator([ys, xs], [], [], 1.0, continuous=False)
        generator.prepare()
        self.set_motor_attributes()
        self.set_attributes(self.child, rowTrigger="Motion Controller")
        self.set_attributes(self.child_seq1, bita="TTLIN1.VAL")
        self.set_attributes(self.child_seq2, bita="TTLIN1.VAL")
        axes_to_move = ["x", "y"]

        seq_rows = self.get_sequencer_rows(generator, axes_to_move)
        # Triggers
        B0 = Trigger.BITA_0
        B1 = Trigger.BITA_1
        IT = Trigger.IMMEDIATE
        # Half a frame
        hf = 62500000
        expected = SequencerRows()
        for i in range(11):
            expected.add_seq_entry(1, B1, 0, hf, 1, 0)
            expected.add_seq_entry(1, B0, 0, MIN_PULSE, 0, 1)
        expected.add_seq_entry(1, B1, 0, hf, 1, 0)
        expected.add_seq_entry(1, IT, 0, MIN_PULSE, 0, 1)
        expected.add_seq_entry(0, IT, 0, MIN_PULSE, 0, 0)

        assert seq_rows.as_tuples() == expected.as_tuples()

    def test_acquire_scan(self):
        generator = CompoundGenerator([StaticPointGenerator(size=5)], [], [],
                                      1.0)
        generator.prepare()

        seq_rows = self.get_sequencer_rows(generator, [])
        # Triggers
        IT = Trigger.IMMEDIATE
        # Half a frame
        hf = 62500000
        expected = SequencerRows()
        expected.add_seq_entry(count=5,
                               trigger=IT,
                               position=0,
                               half_duration=hf,
                               live=1,
                               dead=0)
        expected.add_seq_entry(1, IT, 0, MIN_PULSE, 0, 1)
        expected.add_seq_entry(0, IT, 0, MIN_PULSE, 0, 0)

        assert seq_rows.as_tuples() == expected.as_tuples()

    def test_configure_pcomp_row_trigger_with_single_point_rows(self):
        x_steps, y_steps = 1, 5
        xs = LineGenerator("x", "mm", 0.0, 0.5, x_steps, alternate=True)
        ys = LineGenerator("y", "mm", 0.0, 4, y_steps)
        generator = CompoundGenerator([ys, xs], [], [], 1.0)
        self.set_motor_attributes()
        axes_to_move = ["x", "y"]

        seq_rows = self.get_sequencer_rows(generator, axes_to_move)
        # Triggers
        GT = Trigger.POSA_GT
        LT = Trigger.POSA_LT
        IT = Trigger.IMMEDIATE
        # Half a frame
        hf = 62500000
        # Half blind
        hb = 75000000
        expected = SequencerRows()
        expected.add_seq_entry(count=1,
                               trigger=LT,
                               position=0,
                               half_duration=hf,
                               live=1,
                               dead=0)
        expected.add_seq_entry(1, IT, 0, hb, 0, 1)
        expected.add_seq_entry(1, GT, -500, hf, 1, 0)
        expected.add_seq_entry(1, IT, 0, hb, 0, 1)
        expected.add_seq_entry(1, LT, 0, hf, 1, 0)
        expected.add_seq_entry(1, IT, 0, hb, 0, 1)
        expected.add_seq_entry(1, GT, -500, hf, 1, 0)
        expected.add_seq_entry(1, IT, 0, hb, 0, 1)
        expected.add_seq_entry(1, LT, 0, hf, 1, 0)
        expected.add_seq_entry(1, IT, 0, MIN_PULSE, 0, 1)
        expected.add_seq_entry(0, IT, 0, MIN_PULSE, 0, 0)

        assert seq_rows.as_tuples() == expected.as_tuples()

    def test_configure_with_delay_after(self):
        # a test to show that delay_after inserts a "loop_back" turnaround
        delay = 1.0
        x_steps, y_steps = 3, 2
        xs = LineGenerator("x", "mm", 0.0, 0.5, x_steps, alternate=True)
        ys = LineGenerator("y", "mm", 0.0, 0.1, y_steps)
        generator = CompoundGenerator([ys, xs], [], [], 1.0, delay_after=delay)
        self.set_motor_attributes()
        axes_to_move = ["x", "y"]

        seq_rows = self.get_sequencer_rows(generator, axes_to_move)
        # Triggers
        GT = Trigger.POSA_GT
        IT = Trigger.IMMEDIATE
        LT = Trigger.POSA_LT
        # Half a frame
        hf = 62500000
        # Half how long to be blind for a single point
        hfb = 55625000
        # Half how long to be blind for end of row
        hrb = 56500000
        expected = SequencerRows()
        expected.add_seq_entry(count=1,
                               trigger=LT,
                               position=125,
                               half_duration=hf,
                               live=1,
                               dead=0)
        expected.add_seq_entry(1, IT, 0, hfb, 0, 1)
        expected.add_seq_entry(1, LT, -125, hf, 1, 0)
        expected.add_seq_entry(1, IT, 0, hfb, 0, 1)
        expected.add_seq_entry(1, LT, -375, hf, 1, 0)
        expected.add_seq_entry(1, IT, 0, hrb, 0, 1)
        expected.add_seq_entry(1, GT, -625, hf, 1, 0)
        expected.add_seq_entry(1, IT, 0, hfb, 0, 1)
        expected.add_seq_entry(1, GT, -375, hf, 1, 0)
        expected.add_seq_entry(1, IT, 0, hfb, 0, 1)
        expected.add_seq_entry(1, GT, -125, hf, 1, 0)
        expected.add_seq_entry(1, IT, 0, MIN_PULSE, 0, 1)
        expected.add_seq_entry(0, IT, 0, MIN_PULSE, 0, 0)

        assert seq_rows.as_tuples() == expected.as_tuples()

    def test_configure_with_zero_points(self):
        xs = LineGenerator("x", "mm", 0.0, 0.3, 4, alternate=True)
        ys = LineGenerator("y", "mm", 0.0, 0.1, 2)
        generator = CompoundGenerator([ys, xs], [], [], 1.0)
        self.set_motor_attributes()
        axes_to_move = ["x", "y"]

        seq_rows = self.get_sequencer_rows(generator, axes_to_move, steps=0)
        # Triggers
        IT = Trigger.IMMEDIATE
        expected = SequencerRows()
        expected.add_seq_entry(1, IT, 0, MIN_PULSE, 0, 1)
        expected.add_seq_entry(0, IT, 0, MIN_PULSE, 0, 0)

        assert seq_rows.as_tuples() == expected.as_tuples()

    def test_configure_with_one_point(self):
        xs = LineGenerator("x", "mm", 0.0, 0.3, 4, alternate=True)
        ys = LineGenerator("y", "mm", 0.0, 0.1, 2)
        generator = CompoundGenerator([ys, xs], [], [], 1.0)
        self.set_motor_attributes()
        axes_to_move = ["x", "y"]

        seq_rows = self.get_sequencer_rows(generator, axes_to_move, steps=1)
        # Triggers
        IT = Trigger.IMMEDIATE
        LT = Trigger.POSA_LT
        # Half a frame
        hf = 62500000
        expected = SequencerRows()
        expected.add_seq_entry(count=1,
                               trigger=LT,
                               position=50,
                               half_duration=hf,
                               live=1,
                               dead=0)
        expected.add_seq_entry(1, IT, 0, MIN_PULSE, 0, 1)
        expected.add_seq_entry(0, IT, 0, MIN_PULSE, 0, 0)

        assert seq_rows.as_tuples() == expected.as_tuples()

    def test_configure_long_pcomp_row_trigger(self):
        # Skip on GitHub Actions and GitLab CI
        if "CI" in os.environ:
            pytest.skip("performance test only")

        self.set_motor_attributes(
            0,
            0,
            "mm",
            x_velocity=300,
            y_velocity=300,
            x_acceleration=30,
            y_acceleration=30,
        )
        x_steps, y_steps = 4000, 1000
        xs = LineGenerator("x", "mm", 0.0, 10, x_steps, alternate=True)
        ys = LineGenerator("y", "mm", 0.0, 8, y_steps)
        generator = CompoundGenerator([ys, xs], [], [], 0.005)
        generator.prepare()
        completed_steps = 0
        steps_to_do = x_steps * y_steps
        self.set_motor_attributes()
        axes_to_move = ["x", "y"]

        start = datetime.now()
        self.o.on_configure(self.context, completed_steps, steps_to_do, {},
                            generator, axes_to_move)
        elapsed = datetime.now() - start
        assert elapsed.total_seconds() < 3.0

    def test_on_report_status_doing_pcomp(self):
        mock_context = MagicMock(name="context_mock")
        mock_child = MagicMock(name="child_mock")
        mock_child.rowTrigger.value = "Position Compare"
        mock_context.block_view.return_value = mock_child

        info = self.o.on_report_status(mock_context)

        assert info.trigger == MotionTrigger.NONE

    def test_on_report_status_not_doing_pcomp_is_row_gate(self):
        mock_context = MagicMock(name="context_mock")
        mock_child = MagicMock(name="child_mock")
        mock_child.rowTrigger.value = "Motion Controller"
        mock_context.block_view.return_value = mock_child

        info = self.o.on_report_status(mock_context)

        assert info.trigger == MotionTrigger.ROW_GATE
Exemplo n.º 7
0
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")
        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")
        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)
Exemplo n.º 8
0
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 a root block for the ManagerController block to reside in
        if os.path.isdir("/tmp/mainBlock"):
            shutil.rmtree("/tmp/mainBlock")
        self.c = ManagerController('mainBlock', config_dir="/tmp")
        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)

    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 check_expected_save(self,
                            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("/tmp/mainBlock/testSaveLayout.json") 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)
        l = []
        c.subscribe(["mainBlock", "design", "meta"], l.append)
        # Wait for long enough for the other process to get a look in
        c.sleep(0.1)
        assert len(l) == 1
        assert l.pop()["choices"] == [""]
        b = c.block_view("mainBlock")
        b.save(design="testSaveLayout")
        assert len(l) == 3
        assert l[0]["writeable"] == False
        assert l[1]["choices"] == ["", "testSaveLayout"]
        assert l[2]["writeable"] == True
        assert self.c.design.meta.choices == ["", "testSaveLayout"]
        self.check_expected_save()
        assert self.c.state.value == "Ready"
        assert self.c.design.value == 'testSaveLayout'
        assert self.c.modified.value is False
        os.remove("/tmp/mainBlock/testSaveLayout.json")
        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(design="")
        self.check_expected_save(attr="newv")
        assert self.c.design.value == 'testSaveLayout'
        assert self.c._run_git_cmd.call_args_list == [
            call('add', '/tmp/mainBlock/testSaveLayout.json'),
            call('commit', '--allow-empty', '-m',
                 'Saved mainBlock testSaveLayout',
                 '/tmp/mainBlock/testSaveLayout.json'),
            call('add', '/tmp/mainBlock/testSaveLayout.json'),
            call('commit', '--allow-empty', '-m',
                 'Saved mainBlock testSaveLayout',
                 '/tmp/mainBlock/testSaveLayout.json')
        ]

    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 == False
        assert self.c.modified.value == True
        assert self.c.modified.alarm.message == "layout changed"

        # save the layout, modify and restore it
        self.b.save(design='testSaveLayout')
        assert self.c.modified.value is False
        self.check_expected_save(10.0, 20.0, "false")
        self.c.parts['part2'].x = 30
        self.c.set_design('testSaveLayout')
        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 == True
        assert self.c.modified.alarm.message == "exports changed"
        self.c.save(design='testSaveLayout')
        assert self.c.modified.value == 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 == False
        m = MagicMock()
        f = 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 == 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 == 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 == True
        self.c.save()
        assert self.c.modified.value == False
        # block has changed, get a new view
        b = context.block_view("mainBlock")
        assert "childAttr" not in b
class TestPandaSeqTriggerPart(ChildTestCase):
    def setUp(self):
        self.process = Process("Process")
        self.context = Context(self.process)

        # Create a fake PandA
        self.panda = ManagerController("PANDA", "/tmp")
        self.busses = PositionsPart("busses")
        self.panda.add_part(self.busses)

        # Make 2 sequencers we can prod
        self.seq_parts = {}
        for i in (1, 2):
            controller = BasicController("PANDA:SEQ%d" % i)
            self.seq_parts[i] = SequencerPart("part")
            controller.add_part(self.seq_parts[i])
            self.process.add_controller(controller)
            self.panda.add_part(
                ChildPart(
                    "SEQ%d" % i,
                    "PANDA:SEQ%d" % i,
                    initial_visibility=True,
                    stateful=False,
                ))
        self.child_seq1 = self.process.get_controller("PANDA:SEQ1")
        self.child_seq2 = self.process.get_controller("PANDA:SEQ2")

        # And an srgate
        controller = BasicController("PANDA:SRGATE1")
        self.gate_part = GatePart("part")
        controller.add_part(self.gate_part)
        self.process.add_controller(controller)
        self.panda.add_part(
            ChildPart("SRGATE1",
                      "PANDA:SRGATE1",
                      initial_visibility=True,
                      stateful=False))
        self.process.add_controller(self.panda)

        # And the PMAC
        pmac_block = make_block_creator(
            os.path.join(os.path.dirname(__file__), "..", "test_pmac", "blah"),
            "test_pmac_manager_block.yaml",
        )
        self.pmac = self.create_child_block(pmac_block,
                                            self.process,
                                            mri_prefix="PMAC",
                                            config_dir="/tmp")
        # These are the motors we are interested in
        self.child_x = self.process.get_controller("BL45P-ML-STAGE-01:X")
        self.child_y = self.process.get_controller("BL45P-ML-STAGE-01:Y")
        self.child_cs1 = self.process.get_controller("PMAC:CS1")
        # CS1 needs to have the right port otherwise we will error
        self.set_attributes(self.child_cs1, port="CS1")

        # Make the child block holding panda and pmac mri
        self.child = self.create_child_block(
            panda_seq_trigger_block,
            self.process,
            mri="SCAN:PCOMP",
            panda="PANDA",
            pmac="PMAC",
        )

        # And our part under test
        self.o = PandASeqTriggerPart("pcomp", "SCAN:PCOMP")

        # Now start the process off and tell the panda which sequencer tables
        # to use
        self.process.start()
        exports = ExportTable.from_rows([
            ("SEQ1.table", "seqTableA"),
            ("SEQ2.table", "seqTableB"),
            ("SRGATE1.forceSet", "seqSetEnable"),
        ])
        self.panda.set_exports(exports)

    def tearDown(self):
        self.process.stop(timeout=2)

    def set_motor_attributes(
        self,
        x_pos=0.5,
        y_pos=0.0,
        units="mm",
        x_acceleration=2.5,
        y_acceleration=2.5,
        x_velocity=1.0,
        y_velocity=1.0,
    ):
        # create some parts to mock the motion controller and 2 axes in a CS
        self.set_attributes(
            self.child_x,
            cs="CS1,A",
            accelerationTime=x_velocity / x_acceleration,
            resolution=0.001,
            offset=0.0,
            maxVelocity=x_velocity,
            readback=x_pos,
            velocitySettle=0.0,
            units=units,
        )
        self.set_attributes(
            self.child_y,
            cs="CS1,B",
            accelerationTime=y_velocity / y_acceleration,
            resolution=0.001,
            offset=0.0,
            maxVelocity=y_velocity,
            readback=y_pos,
            velocitySettle=0.0,
            units=units,
        )

    def test_configure_continuous(self):
        xs = LineGenerator("x", "mm", 0.0, 0.3, 4, alternate=True)
        ys = LineGenerator("y", "mm", 0.0, 0.1, 2)
        generator = CompoundGenerator([ys, xs], [], [], 1.0)
        generator.prepare()
        completed_steps = 0
        steps_to_do = 8
        self.set_motor_attributes()
        axes_to_move = ["x", "y"]
        self.o.on_configure(self.context, completed_steps, steps_to_do, {},
                            generator, axes_to_move)
        assert self.o.generator is generator
        assert self.o.loaded_up_to == completed_steps
        assert self.o.scan_up_to == completed_steps + steps_to_do
        # Triggers
        GT = Trigger.POSA_GT
        IT = Trigger.IMMEDIATE
        LT = Trigger.POSA_LT
        # Half a frame
        hf = 62500000
        # Half how long to be blind for
        hb = 22500000
        self.seq_parts[1].table_set.assert_called_once()
        table = self.seq_parts[1].table_set.call_args[0][0]
        assert table.repeats == [1, 3, 1, 1, 3, 1]
        assert table.trigger == [LT, IT, IT, GT, IT, IT]
        assert table.position == [50, 0, 0, -350, 0, 0]
        assert table.time1 == [hf, hf, hb, hf, hf, 125000000]
        assert table.outa1 == [1, 1, 0, 1, 1, 0]  # Live
        assert table.outb1 == [0, 0, 1, 0, 0, 1]  # Dead
        assert (table.outc1 == table.outd1 == table.oute1 == table.outf1 ==
                [0, 0, 0, 0, 0, 0])
        assert table.time2 == [hf, hf, hb, hf, hf, 125000000]
        assert (table.outa2 == table.outb2 == table.outc2 == table.outd2 ==
                table.oute2 == table.outf2 == [0, 0, 0, 0, 0, 0])
        # Check we didn't press the gate part
        self.gate_part.enable_set.assert_not_called()
        self.o.on_run(self.context)
        # Check we pressed the gate part
        self.gate_part.enable_set.assert_called_once()

    def test_configure_motion_controller_trigger(self):
        xs = LineGenerator("x", "mm", 0.0, 0.3, 4, alternate=True)
        ys = LineGenerator("y", "mm", 0.0, 0.1, 2)
        generator = CompoundGenerator([ys, xs], [], [], 1.0)
        generator.prepare()
        completed_steps = 0
        steps_to_do = 8
        self.set_motor_attributes()
        self.set_attributes(self.child, rowTrigger="Motion Controller")
        self.set_attributes(self.child_seq1, bita="TTLIN1.VAL")
        self.set_attributes(self.child_seq2, bita="TTLIN1.VAL")
        axes_to_move = ["x", "y"]
        self.o.on_configure(self.context, completed_steps, steps_to_do, {},
                            generator, axes_to_move)
        assert self.o.generator is generator
        assert self.o.loaded_up_to == completed_steps
        assert self.o.scan_up_to == completed_steps + steps_to_do
        # Triggers
        B0 = Trigger.BITA_0
        B1 = Trigger.BITA_1
        IT = Trigger.IMMEDIATE
        # Half a frame
        hf = 62500000
        self.seq_parts[1].table_set.assert_called_once()
        table = self.seq_parts[1].table_set.call_args[0][0]
        assert table.repeats == [1, 3, 1, 1, 3, 1]
        assert table.trigger == [B1, IT, B0, B1, IT, IT]
        assert table.time1 == [hf, hf, 1250, hf, hf, 125000000]
        assert table.position == [0, 0, 0, 0, 0, 0]
        assert table.outa1 == [1, 1, 0, 1, 1, 0]  # Live
        assert table.outb1 == [0, 0, 1, 0, 0, 1]  # Dead
        assert (table.outc1 == table.outd1 == table.oute1 == table.outf1 ==
                [0, 0, 0, 0, 0, 0])
        assert table.time2 == [hf, hf, 1250, hf, hf, 125000000]
        assert (table.outa2 == table.outb2 == table.outc2 == table.outd2 ==
                table.oute2 == table.outf2 == [0, 0, 0, 0, 0, 0])
        # Check we didn't press the gate part
        self.gate_part.enable_set.assert_not_called()
        self.o.on_run(self.context)
        # Check we pressed the gate part
        self.gate_part.enable_set.assert_called_once()

    def test_configure_stepped(self):
        xs = LineGenerator("x", "mm", 0.0, 0.3, 4, alternate=True)
        ys = LineGenerator("y", "mm", 0.0, 0.1, 2)
        generator = CompoundGenerator([ys, xs], [], [], 1.0, continuous=False)
        generator.prepare()
        completed_steps = 0
        steps_to_do = 8
        self.set_motor_attributes()
        axes_to_move = ["x", "y"]
        with self.assertRaises(AssertionError):
            self.o.on_configure(self.context, completed_steps, steps_to_do, {},
                                generator, axes_to_move)

    def test_acquire_scan(self):
        generator = CompoundGenerator([StaticPointGenerator(size=5)], [], [],
                                      1.0)
        generator.prepare()
        completed_steps = 0
        steps_to_do = 5
        self.o.on_configure(self.context, completed_steps, steps_to_do, {},
                            generator, [])
        assert self.o.generator is generator
        assert self.o.loaded_up_to == completed_steps
        assert self.o.scan_up_to == completed_steps + steps_to_do
        # Triggers
        IT = Trigger.IMMEDIATE
        # Half a frame
        hf = 62500000
        self.seq_parts[1].table_set.assert_called_once()
        table = self.seq_parts[1].table_set.call_args[0][0]
        assert table.repeats == [5, 1]
        assert table.trigger == [IT, IT]
        assert table.position == [0, 0]
        assert table.time1 == [hf, 125000000]
        assert table.outa1 == [1, 0]  # Live
        assert table.outb1 == [0, 1]  # Dead
        assert table.outc1 == table.outd1 == table.oute1 == table.outf1 == [
            0, 0
        ]
        assert table.time2 == [hf, 125000000]
        assert (table.outa2 == table.outb2 == table.outc2 == table.outd2 ==
                table.oute2 == table.outf2 == [0, 0])
        # Check we didn't press the gate part
        self.gate_part.enable_set.assert_not_called()

    def test_configure_single_point_multi_frames(self):
        # This test uses PCAP to generate a static point test.
        # The test moves the motors to a new position and then generates
        # 5 triggers at that position

        xs = LineGenerator("x", "mm", 0.0, 0.0, 5, alternate=True)
        ys = LineGenerator("y", "mm", 1.0, 1.0, 1)
        generator = CompoundGenerator([ys, xs], [], [], 1.0)
        generator.prepare()

        steps_to_do = 5
        self.assertEqual(steps_to_do, generator.size)

        completed_steps = 0
        self.set_motor_attributes()
        axes_to_move = ["x", "y"]

        self.o.on_configure(self.context, completed_steps, steps_to_do, {},
                            generator, axes_to_move)

    def test_configure_pcomp_row_trigger_with_single_point_rows(self):
        x_steps, y_steps = 1, 5
        xs = LineGenerator("x", "mm", 0.0, 0.5, x_steps, alternate=True)
        ys = LineGenerator("y", "mm", 0.0, 4, y_steps)
        generator = CompoundGenerator([ys, xs], [], [], 1.0)
        generator.prepare()
        completed_steps = 0
        steps_to_do = x_steps * y_steps
        self.set_motor_attributes()
        axes_to_move = ["x", "y"]

        self.o.on_configure(self.context, completed_steps, steps_to_do, {},
                            generator, axes_to_move)

        # Triggers
        GT = Trigger.POSA_GT
        LT = Trigger.POSA_LT
        IT = Trigger.IMMEDIATE
        # Half a frame
        hf = 62500000
        # Half blind
        hb = 75000000
        self.seq_parts[1].table_set.assert_called_once()
        table = self.seq_parts[1].table_set.call_args[0][0]
        assert table.repeats == [1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
        assert table.trigger == [LT, IT, GT, IT, LT, IT, GT, IT, LT, IT]
        assert table.time1 == [hf, hb, hf, hb, hf, hb, hf, hb, hf, 125000000]
        assert table.position == [0, 0, -500, 0, 0, 0, -500, 0, 0, 0]
        assert table.outa1 == [1, 0, 1, 0, 1, 0, 1, 0, 1, 0]  # Live
        assert table.outb1 == [0, 1, 0, 1, 0, 1, 0, 1, 0, 1]  # Dead
        assert (table.outc1 == table.outd1 == table.oute1 == table.outf1 ==
                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0])
        assert table.time2 == [hf, hb, hf, hb, hf, hb, hf, hb, hf, 125000000]
        assert (table.outa2 == table.outb2 == table.outc2 == table.outd2 ==
                table.oute2 == table.outf2 == [0, 0, 0, 0, 0, 0, 0, 0, 0, 0])
        # Check we didn't press the gate part
        self.gate_part.enable_set.assert_not_called()
        self.o.on_run(self.context)
        # Check we pressed the gate part
        self.gate_part.enable_set.assert_called_once()

    def test_configure_with_delay_after(self):
        # a test to show that delay_after inserts a "loop_back" turnaround
        delay = 1.0
        x_steps, y_steps = 3, 2
        xs = LineGenerator("x", "mm", 0.0, 0.5, x_steps, alternate=True)
        ys = LineGenerator("y", "mm", 0.0, 0.1, y_steps)
        generator = CompoundGenerator([ys, xs], [], [], 1.0, delay_after=delay)
        generator.prepare()
        completed_steps = 0
        steps_to_do = x_steps * y_steps
        self.set_motor_attributes()
        axes_to_move = ["x", "y"]
        self.o.on_configure(self.context, completed_steps, steps_to_do, {},
                            generator, axes_to_move)
        assert self.o.generator is generator
        assert self.o.loaded_up_to == completed_steps
        assert self.o.scan_up_to == completed_steps + steps_to_do
        # Triggers
        GT = Trigger.POSA_GT
        IT = Trigger.IMMEDIATE
        LT = Trigger.POSA_LT
        # Half a frame
        hf = 62500000
        # Half how long to be blind for a single point
        hfb = 55625000
        # Half how long to be blind for end of row
        hrb = 56500000
        self.seq_parts[1].table_set.assert_called_once()
        table = self.seq_parts[1].table_set.call_args[0][0]
        assert table.repeats == [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
        assert table.trigger == [
            LT, IT, LT, IT, LT, IT, GT, IT, GT, IT, GT, IT
        ]
        assert table.position == [
            125, 0, -125, 0, -375, 0, -625, 0, -375, 0, -125, 0
        ]
        assert table.time1 == [
            hf,
            hfb,
            hf,
            hfb,
            hf,
            hrb,
            hf,
            hfb,
            hf,
            hfb,
            hf,
            125000000,
        ]
        assert table.outa1 == [1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0]  # Live
        assert table.outb1 == [0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1]  # Dead
        assert (table.outc1 == table.outd1 == table.oute1 == table.outf1 ==
                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0])
        assert table.time2 == [
            hf,
            hfb,
            hf,
            hfb,
            hf,
            hrb,
            hf,
            hfb,
            hf,
            hfb,
            hf,
            125000000,
        ]
        assert (table.outa2 == table.outb2 == table.outc2 == table.outd2 ==
                table.oute2 == table.outf2 ==
                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0])
        # Check we didn't press the gate part
        self.gate_part.enable_set.assert_not_called()
        self.o.on_run(self.context)
        # Check we pressed the gate part
        self.gate_part.enable_set.assert_called_once()

    def test_configure_long_pcomp_row_trigger(self):
        # Skip on GitHub Actions and GitLab CI
        if "CI" in os.environ:
            pytest.skip("performance test only")

        self.set_motor_attributes(
            0,
            0,
            "mm",
            x_velocity=300,
            y_velocity=300,
            x_acceleration=30,
            y_acceleration=30,
        )
        x_steps, y_steps = 4000, 1000
        xs = LineGenerator("x", "mm", 0.0, 10, x_steps, alternate=True)
        ys = LineGenerator("y", "mm", 0.0, 8, y_steps)
        generator = CompoundGenerator([ys, xs], [], [], 0.005)
        generator.prepare()
        completed_steps = 0
        steps_to_do = x_steps * y_steps
        self.set_motor_attributes()
        axes_to_move = ["x", "y"]

        start = datetime.now()
        self.o.on_configure(self.context, completed_steps, steps_to_do, {},
                            generator, axes_to_move)
        elapsed = datetime.now() - start
        assert elapsed.total_seconds() < 3.0
Exemplo n.º 10
0
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