Add some basic tests for base stage implementation

This commit is contained in:
Julian Stirling 2026-02-16 21:37:05 +00:00
parent 10f73cfb76
commit 8a77c09305
3 changed files with 48 additions and 7 deletions

View file

@ -42,13 +42,14 @@ class JogCommand:
None for stop motion. None for stop motion.
""" """
super().__init__() super().__init__()
self.displacement = displacement self.displacement = None if displacement is None else tuple(displacement)
def __repr__(self) -> str: def __repr__(self) -> str:
"""Represent the command as a string.""" """Represent the command as a string."""
class_name = type(self).__name__
if self.displacement is None: if self.displacement is None:
return "<JogCommand>STOP" return f"<{class_name}>STOP"
return f"<JogCommand>{self.displacement}" return f"<{class_name}>{self.displacement}"
class JogQueue(queue.Queue[JogCommand]): 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``. Make sure to use and update ``self._hardware_position`` not ``self.position``.
""" """
raise NotImplementedError( 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: def _hardware_start_move_relative(self, displacement: Sequence[int]) -> None:
"""Start a relative move.""" """Start a relative move."""
raise NotImplementedError( 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: def _hardware_stop(self) -> None:
@ -250,7 +251,7 @@ class BaseStage(lt.Thing):
def _estimate_move_duration(self, displacement: Sequence[int]) -> float: def _estimate_move_duration(self, displacement: Sequence[int]) -> float:
"""Calculate the expected duration of a move with the given displacement.""" """Calculate the expected duration of a move with the given displacement."""
raise NotImplementedError( raise NotImplementedError(
"StageThings must define their own _estimate_move_duration" "StageThings must define their own _estimate_move_duration method"
) )
@lt.action @lt.action

View file

@ -1 +1,10 @@
"""Shared utilities for all test suites.""" """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__)

View file

@ -17,6 +17,7 @@ from openflexure_microscope_server.things.stage import (
) )
from openflexure_microscope_server.things.stage.dummy import DummyStage from openflexure_microscope_server.things.stage.dummy import DummyStage
from ..shared_utils import get_method_name
from ..shared_utils.lt_test_utils import LabThingsTestEnv from ..shared_utils.lt_test_utils import LabThingsTestEnv
# Keep the size and number of moves fairly small or the tests can take forever # 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) 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(): def test_override_base_movement():
"""Child classes of stage should implement functions in the hardware reference frame. """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 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])) == "<JogCommand>(1, 2, 3)"
assert str(JogCommand((1, 2, 3))) == "<JogCommand>(1, 2, 3)"
# Stop should be very clear.
assert str(JogCommand(None)) == "<JogCommand>STOP"
def test_get_jog_from_queue_most_recent(dummy_stage): def test_get_jog_from_queue_most_recent(dummy_stage):
"""Test that the jog queue gives the most recent Jog Command.""" """Test that the jog queue gives the most recent Jog Command."""
# Try to stack 4 moves in the queue, only 1 should be queued. # 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) command = dummy_stage._get_from_jog_queue(0.001)
assert isinstance(command, JogCommand) assert isinstance(command, JogCommand)
# Should be the last one queued # Should be the last one queued
assert command.displacement == [3, 3, 3] assert command.displacement == (3, 3, 3)
# Nothing else is queued # Nothing else is queued
assert dummy_stage._get_from_jog_queue(0.001) is None assert dummy_stage._get_from_jog_queue(0.001) is None