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
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
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