Merge branch 'stitch-all-logging' into 'v3'

Log during stitch all and handle errors

Closes #710

See merge request openflexure/openflexure-microscope-server!593
This commit is contained in:
Joe Knapper 2026-05-15 16:36:15 +00:00
commit 4e58d613d0
4 changed files with 75 additions and 16 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,4 +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:
self.logger.info(f"Stitching {scan.name}")
self.stitch_scan(scan_name=scan.name) self.stitch_scan(scan_name=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

@ -325,3 +325,57 @@ def test_final_stitching_command_error(mocker):
stitcher._extra_args = ["ERROR"] stitcher._extra_args = ["ERROR"]
with pytest.raises(ChildProcessError): with pytest.raises(ChildProcessError):
stitcher.run() 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",
]