Test stitch all with mocking

This commit is contained in:
jaknapper 2026-05-15 17:05:08 +01:00
parent bd796f98a1
commit 0f842c622b
4 changed files with 53 additions and 41 deletions

View file

@ -11,7 +11,6 @@ import os
import threading import threading
import time import time
from datetime import datetime from datetime import datetime
from subprocess import SubprocessError
from types import TracebackType from types import TracebackType
from typing import ( from typing import (
Annotated, Annotated,
@ -717,7 +716,7 @@ class SmartScanThing(OFMThing):
except lt.exceptions.InvocationCancelledError: except lt.exceptions.InvocationCancelledError:
# Sleep for 1 second just to allow invocation logs to pass to user. # Sleep for 1 second just to allow invocation logs to pass to user.
time.sleep(1) time.sleep(1)
except SubprocessError as e: except ChildProcessError as e:
self.logger.error(f"Stitching failed: {e}", exc_info=e) self.logger.error(f"Stitching failed: {e}", exc_info=e)
@lt.action(use_global_lock=False) @lt.action(use_global_lock=False)
@ -744,8 +743,5 @@ class SmartScanThing(OFMThing):
# need stitching. # need stitching.
for scan in self.get_data_for_gallery(): for scan in self.get_data_for_gallery():
if scan.dzi is None: if scan.dzi is None:
try: self.logger.info(f"Stitching {scan.name}")
self.logger.info(f"Stitching {scan.name}") self.stitch_scan(scan_name=scan.name)
self.stitch_scan(scan_name=scan.name)
except (RuntimeError, ChildProcessError):
self.logger.warning(f"Couldn't stitch scan {scan.name}")

View file

@ -4,6 +4,7 @@ import json
import logging import logging
import os import os
import re import re
import tempfile
from collections.abc import Iterable from collections.abc import Iterable
from contextlib import contextmanager from contextlib import contextmanager
from typing import Optional from typing import Optional
@ -12,6 +13,10 @@ import pytest
from labthings_fastapi.testing import create_thing_without_server from labthings_fastapi.testing import create_thing_without_server
from openflexure_microscope_server.things.smart_scan import (
SmartScanThing,
)
from .shared_utils.lt_test_utils import LabThingsTestEnv from .shared_utils.lt_test_utils import LabThingsTestEnv
THIS_DIR = os.path.dirname(os.path.abspath(__file__)) THIS_DIR = os.path.dirname(os.path.abspath(__file__))
@ -105,3 +110,17 @@ def mock_picam_thing(mocker):
from openflexure_microscope_server.things.camera.picamera import StreamingPiCamera2 from openflexure_microscope_server.things.camera.picamera import StreamingPiCamera2
return create_thing_without_server(StreamingPiCamera2) return create_thing_without_server(StreamingPiCamera2)
@pytest.fixture
def smart_scan_thing(mocker):
"""Return a smart scan thing as a fixture."""
thing = create_thing_without_server(
SmartScanThing,
default_workflow="mock-_all_workflows",
mock_all_slots=True,
)
type(thing._thing_server_interface).application_config = mocker.PropertyMock(
return_value={"data_folder": tempfile.gettempdir()}
)
return thing

View file

@ -50,20 +50,6 @@ def _clear_scan_dir() -> None:
shutil.rmtree(SCAN_DIR) shutil.rmtree(SCAN_DIR)
@pytest.fixture
def smart_scan_thing(mocker):
"""Return a smart scan thing as a fixture."""
thing = create_thing_without_server(
SmartScanThing,
default_workflow="mock-_all_workflows",
mock_all_slots=True,
)
type(thing._thing_server_interface).application_config = mocker.PropertyMock(
return_value={"data_folder": tempfile.gettempdir()}
)
return thing
@pytest.fixture @pytest.fixture
def entered_smart_scan_thing(smart_scan_thing): def entered_smart_scan_thing(smart_scan_thing):
"""Yield a smart scan thing as a fixture that has been entred. """Yield a smart scan thing as a fixture that has been entred.

View file

@ -14,7 +14,6 @@ import pytest
from pydantic import BaseModel from pydantic import BaseModel
import labthings_fastapi as lt import labthings_fastapi as lt
from labthings_fastapi.testing import create_thing_without_server
from openflexure_microscope_server.stitching import ( from openflexure_microscope_server.stitching import (
FORBIDDEN_COMMANDS, FORBIDDEN_COMMANDS,
@ -25,7 +24,6 @@ from openflexure_microscope_server.stitching import (
StitchingSettings, StitchingSettings,
validate_command, validate_command,
) )
from openflexure_microscope_server.things.smart_scan import SmartScanThing
from ..shared_utils.lt_test_utils import LabThingsTestEnv from ..shared_utils.lt_test_utils import LabThingsTestEnv
@ -329,42 +327,55 @@ def test_final_stitching_command_error(mocker):
stitcher.run() stitcher.run()
def test_stitch_all_scans_continues_after_error(mocker, caplog): def test_stitch_all_scans_continues_after_error(mocker, caplog, smart_scan_thing):
"""Test stitching all continues after an error and logs as expected.""" """Test stitching all continues after an error and logs as expected."""
thing = create_thing_without_server(
SmartScanThing,
default_workflow="mock-_all_workflows",
mock_all_slots=True,
)
scans = [ scans = [
mocker.Mock(name="scan1", dzi=None), mocker.Mock(name="scan1", dzi=None),
mocker.Mock(name="scan2", dzi=None), mocker.Mock(name="scan2", dzi=None),
mocker.Mock(name="scan3", dzi=None), mocker.Mock(name="scan3", dzi=None),
] ]
scans[0].name = "scan1" scans[0].name = "scan1"
scans[1].name = "scan2" scans[1].name = "scan2"
scans[2].name = "scan3" scans[2].name = "scan3"
mocker.patch.object(thing, "get_data_for_gallery", return_value=scans) mocker.patch.object(
smart_scan_thing,
"get_data_for_gallery",
return_value=scans,
)
stitch_mock = mocker.patch.object( # ensure scan data exists for all scans
thing, scan_data = mocker.Mock()
"stitch_scan", scan_data.stitching_settings = StitchingSettings(
side_effect=[None, RuntimeError("fail"), None], overlap=0.45,
correlation_resize=0.5,
)
with smart_scan_thing:
mocker.patch.object(
smart_scan_thing._scan_dir_manager,
"get_scan_data",
return_value=scan_data,
)
def run_side_effect(self):
if "scan2" in str(self.images_dir):
raise ChildProcessError("test_message")
mocker.patch(
"openflexure_microscope_server.stitching.FinalStitcher.run",
autospec=True,
side_effect=run_side_effect,
) )
with caplog.at_level("INFO"): with caplog.at_level("INFO"):
thing.stitch_all_scans() smart_scan_thing.stitch_all_scans()
assert stitch_mock.call_count == 3 messages = [r.message for r in caplog.records]
messages = [record.message for record in caplog.records]
assert messages == [ assert messages == [
"Stitching scan1", "Stitching scan1",
"Stitching scan2", "Stitching scan2",
"Couldn't stitch scan scan2", "Stitching failed: test_message",
"Stitching scan3", "Stitching scan3",
] ]