diff --git a/src/openflexure_microscope_server/things/stage/__init__.py b/src/openflexure_microscope_server/things/stage/__init__.py index d48722f5..f7c58d0e 100644 --- a/src/openflexure_microscope_server/things/stage/__init__.py +++ b/src/openflexure_microscope_server/things/stage/__init__.py @@ -42,13 +42,14 @@ class JogCommand: None for stop motion. """ super().__init__() - self.displacement = displacement + self.displacement = None if displacement is None else tuple(displacement) def __repr__(self) -> str: """Represent the command as a string.""" + class_name = type(self).__name__ if self.displacement is None: - return "STOP" - return f"{self.displacement}" + return f"<{class_name}>STOP" + return f"<{class_name}>{self.displacement}" class JogQueue(queue.Queue[JogCommand]): @@ -227,13 +228,13 @@ class BaseStage(lt.Thing): Make sure to use and update ``self._hardware_position`` not ``self.position``. """ raise NotImplementedError( - "StageThings must define their own move_absolute method" + "StageThings must define their own _hardware_move_absolute method" ) def _hardware_start_move_relative(self, displacement: Sequence[int]) -> None: """Start a relative move.""" raise NotImplementedError( - "StageThings must define their own hardware_start_move_relative method" + "StageThings must define their own _hardware_start_move_relative method" ) def _hardware_stop(self) -> None: @@ -250,7 +251,7 @@ class BaseStage(lt.Thing): def _estimate_move_duration(self, displacement: Sequence[int]) -> float: """Calculate the expected duration of a move with the given displacement.""" raise NotImplementedError( - "StageThings must define their own _estimate_move_duration" + "StageThings must define their own _estimate_move_duration method" ) @lt.action diff --git a/tests/shared_utils/__init__.py b/tests/shared_utils/__init__.py index f0a5ab00..dda3b725 100644 --- a/tests/shared_utils/__init__.py +++ b/tests/shared_utils/__init__.py @@ -1 +1,10 @@ """Shared utilities for all test suites.""" + +import functools + + +def get_method_name(method): + """Reliably get a methods name even if wrapped.""" + while isinstance(method, functools.partial): + method = method.func + return getattr(method, "__name__", type(method).__name__) diff --git a/tests/unit_tests/test_stage.py b/tests/unit_tests/test_stage.py index 3ffdbf72..552b1b39 100644 --- a/tests/unit_tests/test_stage.py +++ b/tests/unit_tests/test_stage.py @@ -17,6 +17,7 @@ from openflexure_microscope_server.things.stage import ( ) from openflexure_microscope_server.things.stage.dummy import DummyStage +from ..shared_utils import get_method_name from ..shared_utils.lt_test_utils import LabThingsTestEnv # Keep the size and number of moves fairly small or the tests can take forever @@ -35,6 +36,26 @@ def dummy_stage(): return create_thing_without_server(DummyStage, step_time=0.000001) +def test_not_implemented_methods(): + """Check all methods a child class should implement raise NotImplemented.""" + stage = create_thing_without_server(BaseStage) + methods_and_args = [ + (stage.update_position, ()), + (stage._hardware_move_relative, ()), + (stage._hardware_move_absolute, ()), + (stage._hardware_start_move_relative, ((1, 2, 3),)), + (stage._hardware_stop, ()), + (stage._poll_moving, ()), + (stage._estimate_move_duration, ((1, 2, 3),)), + (stage.set_zero_position, ()), + ] + for method, args in methods_and_args: + method_name = get_method_name(method) + msg = f"StageThings must define their own {method_name} method" + with pytest.raises(NotImplementedError, match=msg): + method(*args) + + def test_override_base_movement(): """Child classes of stage should implement functions in the hardware reference frame. @@ -303,6 +324,16 @@ def test_thing_description_equivalence(dummy_stage, mocker): assert sanga_properties == dummy_properties == base_properties +def test_job_repr(): + """Test when printing a jog command the result is as expected.""" + # Jog should represent itself the same whether displacement is set with a list or a + # tuple. + assert str(JogCommand([1, 2, 3])) == "(1, 2, 3)" + assert str(JogCommand((1, 2, 3))) == "(1, 2, 3)" + # Stop should be very clear. + assert str(JogCommand(None)) == "STOP" + + def test_get_jog_from_queue_most_recent(dummy_stage): """Test that the jog queue gives the most recent Jog Command.""" # Try to stack 4 moves in the queue, only 1 should be queued. @@ -312,6 +343,6 @@ def test_get_jog_from_queue_most_recent(dummy_stage): command = dummy_stage._get_from_jog_queue(0.001) assert isinstance(command, JogCommand) # Should be the last one queued - assert command.displacement == [3, 3, 3] + assert command.displacement == (3, 3, 3) # Nothing else is queued assert dummy_stage._get_from_jog_queue(0.001) is None