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
예제 #2
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)
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
예제 #4
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 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