From 0f842c622b38a74f2faf52e12767210b7b8b340e Mon Sep 17 00:00:00 2001 From: jaknapper Date: Fri, 15 May 2026 17:05:08 +0100 Subject: [PATCH] Test stitch all with mocking --- .../things/smart_scan.py | 10 ++-- tests/conftest.py | 19 +++++++ tests/unit_tests/test_smart_scan.py | 14 ----- tests/unit_tests/test_stitching.py | 51 +++++++++++-------- 4 files changed, 53 insertions(+), 41 deletions(-) diff --git a/src/openflexure_microscope_server/things/smart_scan.py b/src/openflexure_microscope_server/things/smart_scan.py index d6878b99..0bb1b478 100644 --- a/src/openflexure_microscope_server/things/smart_scan.py +++ b/src/openflexure_microscope_server/things/smart_scan.py @@ -11,7 +11,6 @@ import os import threading import time from datetime import datetime -from subprocess import SubprocessError from types import TracebackType from typing import ( Annotated, @@ -717,7 +716,7 @@ class SmartScanThing(OFMThing): except lt.exceptions.InvocationCancelledError: # Sleep for 1 second just to allow invocation logs to pass to user. time.sleep(1) - except SubprocessError as e: + except ChildProcessError as e: self.logger.error(f"Stitching failed: {e}", exc_info=e) @lt.action(use_global_lock=False) @@ -744,8 +743,5 @@ class SmartScanThing(OFMThing): # need stitching. for scan in self.get_data_for_gallery(): if scan.dzi is None: - try: - self.logger.info(f"Stitching {scan.name}") - self.stitch_scan(scan_name=scan.name) - except (RuntimeError, ChildProcessError): - self.logger.warning(f"Couldn't stitch scan {scan.name}") + self.logger.info(f"Stitching {scan.name}") + self.stitch_scan(scan_name=scan.name) diff --git a/tests/conftest.py b/tests/conftest.py index f7fb28c1..e5e10dfa 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -4,6 +4,7 @@ import json import logging import os import re +import tempfile from collections.abc import Iterable from contextlib import contextmanager from typing import Optional @@ -12,6 +13,10 @@ import pytest 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 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 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 diff --git a/tests/unit_tests/test_smart_scan.py b/tests/unit_tests/test_smart_scan.py index 566faea9..06ad9e7b 100644 --- a/tests/unit_tests/test_smart_scan.py +++ b/tests/unit_tests/test_smart_scan.py @@ -50,20 +50,6 @@ def _clear_scan_dir() -> None: 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 def entered_smart_scan_thing(smart_scan_thing): """Yield a smart scan thing as a fixture that has been entred. diff --git a/tests/unit_tests/test_stitching.py b/tests/unit_tests/test_stitching.py index d36d5002..017d9291 100644 --- a/tests/unit_tests/test_stitching.py +++ b/tests/unit_tests/test_stitching.py @@ -14,7 +14,6 @@ import pytest from pydantic import BaseModel import labthings_fastapi as lt -from labthings_fastapi.testing import create_thing_without_server from openflexure_microscope_server.stitching import ( FORBIDDEN_COMMANDS, @@ -25,7 +24,6 @@ from openflexure_microscope_server.stitching import ( StitchingSettings, validate_command, ) -from openflexure_microscope_server.things.smart_scan import SmartScanThing from ..shared_utils.lt_test_utils import LabThingsTestEnv @@ -329,42 +327,55 @@ def test_final_stitching_command_error(mocker): 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.""" - thing = create_thing_without_server( - SmartScanThing, - default_workflow="mock-_all_workflows", - mock_all_slots=True, - ) - scans = [ mocker.Mock(name="scan1", dzi=None), mocker.Mock(name="scan2", dzi=None), mocker.Mock(name="scan3", dzi=None), ] - scans[0].name = "scan1" scans[1].name = "scan2" 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( - thing, - "stitch_scan", - side_effect=[None, RuntimeError("fail"), None], + # ensure scan data exists for all scans + scan_data = mocker.Mock() + scan_data.stitching_settings = StitchingSettings( + 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"): - thing.stitch_all_scans() + smart_scan_thing.stitch_all_scans() - assert stitch_mock.call_count == 3 - - messages = [record.message for record in caplog.records] + messages = [r.message for r in caplog.records] assert messages == [ "Stitching scan1", "Stitching scan2", - "Couldn't stitch scan scan2", + "Stitching failed: test_message", "Stitching scan3", ]