diff --git a/src/openflexure_microscope_server/things/smart_scan.py b/src/openflexure_microscope_server/things/smart_scan.py index 3f2c6dfb..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,4 +743,5 @@ class SmartScanThing(OFMThing): # need stitching. for scan in self.get_data_for_gallery(): if scan.dzi is None: + 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 ead2276b..017d9291 100644 --- a/tests/unit_tests/test_stitching.py +++ b/tests/unit_tests/test_stitching.py @@ -325,3 +325,57 @@ def test_final_stitching_command_error(mocker): stitcher._extra_args = ["ERROR"] with pytest.raises(ChildProcessError): stitcher.run() + + +def test_stitch_all_scans_continues_after_error(mocker, caplog, smart_scan_thing): + """Test stitching all continues after an error and logs as expected.""" + 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( + smart_scan_thing, + "get_data_for_gallery", + return_value=scans, + ) + + # 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"): + smart_scan_thing.stitch_all_scans() + + messages = [r.message for r in caplog.records] + + assert messages == [ + "Stitching scan1", + "Stitching scan2", + "Stitching failed: test_message", + "Stitching scan3", + ]