Update and simplify smart scan tests
This commit is contained in:
parent
4ad1fff656
commit
3ea7be6531
6 changed files with 27 additions and 167 deletions
|
|
@ -1,5 +0,0 @@
|
||||||
"""A package of test module providing mock Things for testing.
|
|
||||||
|
|
||||||
The mock things do not subclass the original things to minimise the inflation of
|
|
||||||
test coverage.
|
|
||||||
"""
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
"""Testing submodule with mock Autofocus Things.
|
|
||||||
|
|
||||||
These mocks are designed to be inserted as dependencies to provide specific
|
|
||||||
functionality and return values.
|
|
||||||
|
|
||||||
The mocks do not subclass Things. Instead, they return predefined
|
|
||||||
answers to functions.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class MockAutoFocusThing:
|
|
||||||
"""A mock autofocus Thing that imports no code from AutofocusThing.
|
|
||||||
|
|
||||||
The class needs functionality added to it over time as more complex
|
|
||||||
mocking is needed. It imports no code from AutofocusThing so that coverage
|
|
||||||
is not artificially inflated.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Counter for checking functions were called
|
|
||||||
mock_call_count = {"looping_autofocus": 0}
|
|
||||||
|
|
||||||
def looping_autofocus(self, dz: int = 2000, start: str = "centre") -> None: # noqa: ARG002
|
|
||||||
"""Mock autofocus with no return."""
|
|
||||||
self.mock_call_count["looping_autofocus"] += 1
|
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
"""Testing submodule with mock Camera Things.
|
|
||||||
|
|
||||||
These mocks are designed to be inserted as dependencies to provide specific
|
|
||||||
functionality and return values.
|
|
||||||
|
|
||||||
The mocks do not subclass Things. Instead, they return predefined
|
|
||||||
answers to functions.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from unittest.mock import Mock, PropertyMock
|
|
||||||
|
|
||||||
from openflexure_microscope_server.background_detect import (
|
|
||||||
BackgroundDetectorStatus,
|
|
||||||
ColourChannelDetectSettings,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class MockCameraThing(Mock):
|
|
||||||
"""A mock camera Thing that imports no code from ``BaseCamera``.
|
|
||||||
|
|
||||||
The class needs functionality added to it over time as more complex
|
|
||||||
mocking is needed. It imports no code from ``BaseCamera`` or any other
|
|
||||||
camera Thing, so that coverage is not artificially inflated.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
"""Initialise the mock camera, args and kwargs are the mock args and kwargs."""
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
self.background_detector_status = PropertyMock(
|
|
||||||
return_value=BackgroundDetectorStatus(
|
|
||||||
ready=True,
|
|
||||||
settings=ColourChannelDetectSettings().model_dump(),
|
|
||||||
settings_schema={"fake": "schema"},
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
"""Testing submodule with mock Camera Stage Mapping Things.
|
|
||||||
|
|
||||||
These mocks are designed to be inserted as dependencies to provide specific
|
|
||||||
functionality and return values.
|
|
||||||
|
|
||||||
The mocks do not subclass Things. Instead, they return predefined
|
|
||||||
answers to functions.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class MockCSMThing:
|
|
||||||
"""A mock CSM Thing that imports no code from CameraStageMapper.
|
|
||||||
|
|
||||||
The class needs functionality added to it over time as more complex
|
|
||||||
mocking is needed. It imports no code from CameraStageMapper so that coverage
|
|
||||||
is not artificially inflated.
|
|
||||||
"""
|
|
||||||
|
|
||||||
image_resolution = (123, 456)
|
|
||||||
image_to_stage_displacement_matrix = [
|
|
||||||
[0.03061156624485296, 1.8031242270940833],
|
|
||||||
[1.773236372778601, 0.006660431608601435],
|
|
||||||
]
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
"""Testing submodule with mocks StageThings.
|
|
||||||
|
|
||||||
These mocks are designed to be inserted as dependencies to provide specific
|
|
||||||
functionality and return values.
|
|
||||||
|
|
||||||
The mocks do not subclass existing Things. Instead, they return predefined
|
|
||||||
answers to functions.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class MockStageThing:
|
|
||||||
"""A mock Thing for a stage that imports no code from BaseStage.
|
|
||||||
|
|
||||||
The class needs functionality added to it over time as more complex
|
|
||||||
mocking is needed. It imports no code from BaseStage so that coverage
|
|
||||||
is not artificially inflated.
|
|
||||||
"""
|
|
||||||
|
|
||||||
position = {"x": 3635, "y": 10, "z": 617}
|
|
||||||
|
|
@ -35,11 +35,6 @@ from openflexure_microscope_server.things.smart_scan import (
|
||||||
SmartScanThing,
|
SmartScanThing,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .mock_things.mock_autofocus import MockAutoFocusThing
|
|
||||||
from .mock_things.mock_camera import MockCameraThing
|
|
||||||
from .mock_things.mock_csm import MockCSMThing
|
|
||||||
from .mock_things.mock_stage import MockStageThing
|
|
||||||
|
|
||||||
# Use our own dir in the root temp dir not a dynamically generated one so we
|
# Use our own dir in the root temp dir not a dynamically generated one so we
|
||||||
# have some control of when it is deleted
|
# have some control of when it is deleted
|
||||||
SCAN_DIR = os.path.join(tempfile.gettempdir(), "scans")
|
SCAN_DIR = os.path.join(tempfile.gettempdir(), "scans")
|
||||||
|
|
@ -54,7 +49,9 @@ def _clear_scan_dir() -> None:
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def smart_scan_thing():
|
def smart_scan_thing():
|
||||||
"""Return a smart scan thing as a fixture."""
|
"""Return a smart scan thing as a fixture."""
|
||||||
return create_thing_without_server(SmartScanThing, scans_folder=SCAN_DIR)
|
return create_thing_without_server(
|
||||||
|
SmartScanThing, scans_folder=SCAN_DIR, mock_all_slots=True
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_initial_properties(smart_scan_thing):
|
def test_initial_properties(smart_scan_thing):
|
||||||
|
|
@ -156,7 +153,9 @@ def test_delete_all_scans(smart_scan_thing, caplog):
|
||||||
assert len(caplog.records) == 0
|
assert len(caplog.records) == 0
|
||||||
|
|
||||||
|
|
||||||
def _run_only_outer_scan(adjust_initial_state: Optional[Callable] = None):
|
def _run_only_outer_scan(
|
||||||
|
smart_scan_thing, mocker, adjust_initial_state: Optional[Callable] = None
|
||||||
|
):
|
||||||
"""Create a subclass of SmartScanThing to mock _run_scan and run sample_scan.
|
"""Create a subclass of SmartScanThing to mock _run_scan and run sample_scan.
|
||||||
|
|
||||||
This should do all the set up for a scan, move into the mocked
|
This should do all the set up for a scan, move into the mocked
|
||||||
|
|
@ -170,69 +169,39 @@ def _run_only_outer_scan(adjust_initial_state: Optional[Callable] = None):
|
||||||
This seems hard to do with a fixture so it is being done with a private
|
This seems hard to do with a fixture so it is being done with a private
|
||||||
function
|
function
|
||||||
"""
|
"""
|
||||||
# cancel handle shouldn't be used. Set to arbitrary value for checking
|
|
||||||
af_mock = MockAutoFocusThing()
|
|
||||||
stage_mock = MockStageThing()
|
|
||||||
cam_mock = MockCameraThing()
|
|
||||||
csm_mock = MockCSMThing()
|
|
||||||
|
|
||||||
class MockedSmartScanThing(SmartScanThing):
|
def check_locked(*_args, **_kwargs):
|
||||||
"""Mocked version of SmartScanThing with a patched _run_scan method."""
|
"""Check the scan is locked."""
|
||||||
|
assert smart_scan_thing._scan_lock.locked()
|
||||||
|
|
||||||
# Counter for checking functions were called
|
mocker.patch.object(smart_scan_thing, "_run_scan", side_effect=check_locked)
|
||||||
mock_call_count = {"_run_scan": 0}
|
|
||||||
|
|
||||||
def _run_scan(self):
|
|
||||||
self.mock_call_count["_run_scan"] += 1
|
|
||||||
|
|
||||||
"""Check scan vars are set up as expected"""
|
|
||||||
assert not self._scan_lock.acquire(timeout=0.1)
|
|
||||||
assert self._autofocus is af_mock
|
|
||||||
assert self._stage is stage_mock
|
|
||||||
assert self._cam is cam_mock
|
|
||||||
assert self._csm is csm_mock
|
|
||||||
|
|
||||||
# mock smart scan thing
|
|
||||||
mock_ss_thing = create_thing_without_server(
|
|
||||||
MockedSmartScanThing, scans_folder=SCAN_DIR
|
|
||||||
)
|
|
||||||
|
|
||||||
if adjust_initial_state is not None:
|
if adjust_initial_state is not None:
|
||||||
adjust_initial_state(mock_ss_thing)
|
adjust_initial_state(smart_scan_thing)
|
||||||
|
|
||||||
exec_info = None
|
exec_info = None
|
||||||
try:
|
try:
|
||||||
mock_ss_thing.sample_scan(
|
smart_scan_thing.sample_scan(scan_name="FooBar")
|
||||||
autofocus=af_mock,
|
|
||||||
stage=stage_mock,
|
|
||||||
cam=cam_mock,
|
|
||||||
csm=csm_mock,
|
|
||||||
scan_name="FooBar",
|
|
||||||
)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
exec_info = e
|
exec_info = e
|
||||||
|
|
||||||
assert mock_ss_thing._scan_lock.acquire(timeout=0.1)
|
assert not smart_scan_thing._scan_lock.locked()
|
||||||
mock_ss_thing._scan_lock.release()
|
|
||||||
assert mock_ss_thing._autofocus is None
|
|
||||||
assert mock_ss_thing._stage is None
|
|
||||||
assert mock_ss_thing._cam is None
|
|
||||||
assert mock_ss_thing._csm is None
|
|
||||||
|
|
||||||
# Return the mock thing for further state testing, and the
|
# Return the mock thing for further state testing, and the
|
||||||
# exec_info of any uncaught exceptions that were raised
|
# exec_info of any uncaught exceptions that were raised
|
||||||
return mock_ss_thing, exec_info
|
return smart_scan_thing, exec_info
|
||||||
|
|
||||||
|
|
||||||
def test_outer_scan():
|
def test_outer_scan(smart_scan_thing, mocker):
|
||||||
"""Test setup and teardown of the scan."""
|
"""Test setup and teardown of the scan."""
|
||||||
mock_ss_thing, exec_info = _run_only_outer_scan()
|
mock_ss_thing, exec_info = _run_only_outer_scan(smart_scan_thing, mocker)
|
||||||
assert exec_info is None
|
assert exec_info is None
|
||||||
# Checked the mocked _run_scan was run exactly once
|
# Checked the mocked _run_scan was run exactly once
|
||||||
assert mock_ss_thing.mock_call_count["_run_scan"] == 1
|
assert mock_ss_thing._run_scan.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
def test_outer_scan_wo_sample_skip():
|
def test_outer_scan_wo_sample_skip(smart_scan_thing, mocker):
|
||||||
"""Test setup and teardown of the scan."""
|
"""Test setup and teardown of the scan."""
|
||||||
|
|
||||||
def _set_skip_background(mock_ss_thing):
|
def _set_skip_background(mock_ss_thing):
|
||||||
|
|
@ -240,11 +209,13 @@ def test_outer_scan_wo_sample_skip():
|
||||||
# __dict__ to avoid triggering property emits that require a server
|
# __dict__ to avoid triggering property emits that require a server
|
||||||
mock_ss_thing.__dict__["skip_background"] = False
|
mock_ss_thing.__dict__["skip_background"] = False
|
||||||
|
|
||||||
mock_ss_thing, exec_info = _run_only_outer_scan(_set_skip_background)
|
mock_ss_thing, exec_info = _run_only_outer_scan(
|
||||||
|
smart_scan_thing, mocker, _set_skip_background
|
||||||
|
)
|
||||||
|
|
||||||
assert exec_info is None
|
assert exec_info is None
|
||||||
# Checked the mocked _run_scan was run exactly once
|
# Checked the mocked _run_scan was run exactly once
|
||||||
assert mock_ss_thing.mock_call_count["_run_scan"] == 1
|
assert mock_ss_thing._run_scan.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
MOCK_SCAN_NAME = "test_name_0001"
|
MOCK_SCAN_NAME = "test_name_0001"
|
||||||
|
|
@ -281,17 +252,14 @@ def scan_thing_mocked_for_scan_data(smart_scan_thing, mocker):
|
||||||
"_calc_displacement_from_test_image",
|
"_calc_displacement_from_test_image",
|
||||||
return_value=[100, 100],
|
return_value=[100, 100],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
smart_scan_thing._stage.position = MOCK_START_POS
|
||||||
|
|
||||||
mock_ongoing_scan = mocker.Mock()
|
mock_ongoing_scan = mocker.Mock()
|
||||||
mock_ongoing_scan.name = MOCK_SCAN_NAME
|
mock_ongoing_scan.name = MOCK_SCAN_NAME
|
||||||
mock_ongoing_scan.images_dir = MOCK_SCAN_DIR
|
mock_ongoing_scan.images_dir = MOCK_SCAN_DIR
|
||||||
mock_stage = mocker.Mock()
|
|
||||||
mock_stage.position = MOCK_START_POS
|
|
||||||
|
|
||||||
mock_autofocus = mocker.Mock()
|
|
||||||
|
|
||||||
smart_scan_thing._ongoing_scan = mock_ongoing_scan
|
smart_scan_thing._ongoing_scan = mock_ongoing_scan
|
||||||
smart_scan_thing._stage = mock_stage
|
|
||||||
smart_scan_thing._autofocus = mock_autofocus
|
|
||||||
yield smart_scan_thing
|
yield smart_scan_thing
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -333,7 +301,6 @@ def scan_thing_mocked_for_run_scan(scan_thing_mocked_for_scan_data, mocker):
|
||||||
Mocks and _cam is a MockCameraThing
|
Mocks and _cam is a MockCameraThing
|
||||||
"""
|
"""
|
||||||
scan_thing = scan_thing_mocked_for_scan_data
|
scan_thing = scan_thing_mocked_for_scan_data
|
||||||
scan_thing._cam = MockCameraThing()
|
|
||||||
mocker.patch.object(scan_thing, "_main_scan_loop")
|
mocker.patch.object(scan_thing, "_main_scan_loop")
|
||||||
mocker.patch.object(scan_thing, "_return_to_starting_position")
|
mocker.patch.object(scan_thing, "_return_to_starting_position")
|
||||||
mocker.patch.object(scan_thing, "_perform_final_stitch")
|
mocker.patch.object(scan_thing, "_perform_final_stitch")
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue