Update tests for not having cancel hooks

This commit is contained in:
Julian Stirling 2025-12-17 14:03:45 +00:00
parent 8f805c8e68
commit f6d4b368d0
2 changed files with 68 additions and 48 deletions

View file

@ -174,7 +174,6 @@ def _run_only_outer_scan(adjust_initial_state: Optional[Callable] = None):
function function
""" """
# cancel handle shouldn't be used. Set to arbitrary value for checking # cancel handle shouldn't be used. Set to arbitrary value for checking
cancel_mock = 1 # not called
af_mock = MockAutoFocusThing() af_mock = MockAutoFocusThing()
stage_mock = MockStageThing() stage_mock = MockStageThing()
cam_mock = MockCameraThing() cam_mock = MockCameraThing()
@ -191,7 +190,6 @@ def _run_only_outer_scan(adjust_initial_state: Optional[Callable] = None):
"""Check scan vars are set up as expected""" """Check scan vars are set up as expected"""
assert not self._scan_lock.acquire(timeout=0.1) assert not self._scan_lock.acquire(timeout=0.1)
assert self._cancel is cancel_mock
assert self._scan_logger is LOGGER assert self._scan_logger is LOGGER
assert self._autofocus is af_mock assert self._autofocus is af_mock
assert self._stage is stage_mock assert self._stage is stage_mock
@ -209,7 +207,6 @@ def _run_only_outer_scan(adjust_initial_state: Optional[Callable] = None):
exec_info = None exec_info = None
try: try:
mock_ss_thing.sample_scan( mock_ss_thing.sample_scan(
cancel=cancel_mock, # Shouldn't be used, can be checked
logger=LOGGER, logger=LOGGER,
autofocus=af_mock, autofocus=af_mock,
stage=stage_mock, stage=stage_mock,
@ -222,7 +219,6 @@ def _run_only_outer_scan(adjust_initial_state: Optional[Callable] = None):
assert mock_ss_thing._scan_lock.acquire(timeout=0.1) assert mock_ss_thing._scan_lock.acquire(timeout=0.1)
mock_ss_thing._scan_lock.release() mock_ss_thing._scan_lock.release()
assert mock_ss_thing._cancel is None
assert mock_ss_thing._scan_logger is None assert mock_ss_thing._scan_logger is None
assert mock_ss_thing._autofocus is None assert mock_ss_thing._autofocus is None
assert mock_ss_thing._stage is None assert mock_ss_thing._stage is None
@ -342,7 +338,6 @@ def scan_thing_mocked_for_run_scan(scan_thing_mocked_for_scan_data, mocker):
""" """
scan_thing = scan_thing_mocked_for_scan_data scan_thing = scan_thing_mocked_for_scan_data
scan_thing._cam = MockCameraThing() scan_thing._cam = MockCameraThing()
mocker.patch.object(scan_thing, "_cancel")
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")

View file

@ -6,9 +6,8 @@ generated, and the subprocess calling works as expected.
import logging import logging
import os import os
import threading import re
import time import time
import uuid
from copy import copy from copy import copy
import pytest import pytest
@ -23,8 +22,10 @@ from openflexure_microscope_server.stitching import (
StitcherValidationError, StitcherValidationError,
) )
# A global logger pretending to be an Invocation Logger from .utilities.lt_test_utils import LabThingsTestEnv
LOGGER = logging.getLogger("mock-invocation_logger")
# A global logger pretending to the logger from a thing
LOGGER = logging.getLogger("mock-thing_logger")
FAKE_DIR: list[str] = os.path.join("a", "dir", "that", "is", "fake") FAKE_DIR: list[str] = os.path.join("a", "dir", "that", "is", "fake")
THIS_DIR: str = os.path.dirname(os.path.realpath(__file__)) THIS_DIR: str = os.path.dirname(os.path.realpath(__file__))
MOCK_STITCHER: str = os.path.join(THIS_DIR, "mock_stitching", "mock-stitch.py") MOCK_STITCHER: str = os.path.join(THIS_DIR, "mock_stitching", "mock-stitch.py")
@ -200,7 +201,6 @@ def test_preview_stitching_command(caplog, mocker):
mocker.patch("openflexure_microscope_server.stitching.STITCHING_CMD", mock_cmd) mocker.patch("openflexure_microscope_server.stitching.STITCHING_CMD", mock_cmd)
with caplog.at_level(logging.INFO): with caplog.at_level(logging.INFO):
cancel_hook = lt.deps.CancelHook(id=uuid.uuid4())
stitcher = PreviewStitcher(FAKE_DIR, overlap=0.1, correlation_resize=0.5) stitcher = PreviewStitcher(FAKE_DIR, overlap=0.1, correlation_resize=0.5)
stitcher.start() stitcher.start()
# Should take a second or so to run so will still be running # Should take a second or so to run so will still be running
@ -209,38 +209,64 @@ def test_preview_stitching_command(caplog, mocker):
with pytest.raises(RuntimeError): with pytest.raises(RuntimeError):
stitcher.start() stitcher.start()
# Wait for it to complete # Wait for it to complete
stitcher.wait(cancel_hook) stitcher.wait()
# It is now not running # It is now not running
assert not stitcher.running assert not stitcher.running
assert len(caplog.records) == 0 assert len(caplog.records) == 0
def test_preview_stitching_cancelled(caplog, mocker): class StitchingTestThing(lt.Thing):
"""A Thing for running stitching in invocation threads.
This is needed to check cancellation behaviour.
"""
@lt.action
def run_preview(self):
"""Run the preview stitcher."""
stitcher = PreviewStitcher(FAKE_DIR, overlap=0.1, correlation_resize=0.5)
# Send in the argument HANG to mock-stitch and it just hang for 10s
stitcher._extra_args = ["HANG"]
stitcher.start()
stitcher.wait()
@lt.action
def run_final(self):
"""Run the final stitcher."""
stitcher = FinalStitcher(FAKE_DIR, logger=self.logger)
# Send in the argument HANG to mock-stitch and it just hang for 10s
stitcher.run()
@pytest.fixture
def stitching_test_env():
"""Return a test environment for a server with just StitchingTestThing."""
with LabThingsTestEnv(things={"stitcher": StitchingTestThing}) as env:
yield env
def test_preview_stitching_cancelled(stitching_test_env, mocker):
"""Check that preview stitch can be cancelled.""" """Check that preview stitch can be cancelled."""
mock_cmd = f"python {MOCK_STITCHER}" mock_cmd = f"python {MOCK_STITCHER}"
mocker.patch("openflexure_microscope_server.stitching.STITCHING_CMD", mock_cmd) mocker.patch("openflexure_microscope_server.stitching.STITCHING_CMD", mock_cmd)
with caplog.at_level(logging.INFO): t_start = time.time()
stitcher = PreviewStitcher(FAKE_DIR, overlap=0.1, correlation_resize=0.5) # Start the action
# Send in the argument HANG to mock-stitch and it just hang for 10s response = stitching_test_env.start_action("stitcher", "run_preview")
stitcher._extra_args = ["HANG"] # Sleep long enough for at least 1 log.
cancel_hook = lt.deps.CancelHook(id=uuid.uuid4()) time.sleep(0.5)
t_start = time.time() # Cancel using a DELETE request
# Start stitching stitching_test_env.cancel_action(response)
stitcher.start() invocation_data = stitching_test_env.poll_action(response)
# Run wait() in a thread.
thread = threading.Thread(target=stitcher.wait, kwargs={"cancel": cancel_hook}) # If it wasn't cancelled it would hang for 10 s. Here we check the cancel killed
thread.start() # it within 2s.
# Sleep long enough for at least 1 log. assert time.time() - t_start < 2
time.sleep(0.5) assert invocation_data["status"] == "cancelled"
# use set() to cancel! logs = invocation_data["log"]
cancel_hook.set() assert len(logs) == 1
thread.join() assert re.match(r"^Invocation [0-9a-f-]+ was cancelled", logs[0]["message"])
# If it wasn't cancelled it would hang for 10 s. Here we check the cancel killed
# it within 2s.
assert t_start - time.time() < 2
assert len(caplog.records) == 0
def test_final_stitching_command(caplog, mocker): def test_final_stitching_command(caplog, mocker):
@ -255,7 +281,7 @@ def test_final_stitching_command(caplog, mocker):
FAKE_DIR, logger=LOGGER, overlap=0.1, correlation_resize=0.5 FAKE_DIR, logger=LOGGER, overlap=0.1, correlation_resize=0.5
) )
# For the final stitcher it will always complete before returning. # For the final stitcher it will always complete before returning.
stitcher.run(cancel=lt.deps.CancelHook(id=uuid.uuid4())) stitcher.run()
# The mock command logs the inputs (but not the initial command) and the # The mock command logs the inputs (but not the initial command) and the
# stitcher logs # "Stitching complete" when it ends. # stitcher logs # "Stitching complete" when it ends.
assert len(caplog.records) == len(FINAL_EXPECTED_COMMAND) assert len(caplog.records) == len(FINAL_EXPECTED_COMMAND)
@ -267,26 +293,25 @@ def test_final_stitching_command(caplog, mocker):
assert msg == FINAL_EXPECTED_COMMAND[i + 1] assert msg == FINAL_EXPECTED_COMMAND[i + 1]
def test_final_stitching_command_cancelled(caplog, mocker): def test_final_stitching_command_cancelled(stitching_test_env, mocker):
"""Check that final stitch can be cancelled.""" """Check that final stitch can be cancelled."""
mock_cmd = f"python {MOCK_STITCHER}" mock_cmd = f"python {MOCK_STITCHER}"
mocker.patch("openflexure_microscope_server.stitching.STITCHING_CMD", mock_cmd) mocker.patch("openflexure_microscope_server.stitching.STITCHING_CMD", mock_cmd)
with caplog.at_level(logging.INFO): # Start the action
stitcher = FinalStitcher(FAKE_DIR, logger=LOGGER) response = stitching_test_env.start_action("stitcher", "run_final")
cancel_hook = lt.deps.CancelHook(id=uuid.uuid4()) # Sleep long enough for at least 1 log.
time.sleep(0.5)
# Cancel using a DELETE request
stitching_test_env.cancel_action(response)
invocation_data = stitching_test_env.poll_action(response)
# Start stitching in a thread. assert invocation_data["status"] == "cancelled"
thread = threading.Thread(target=stitcher.run, kwargs={"cancel": cancel_hook}) logs = invocation_data["log"]
thread.start() assert len(logs) < len(FINAL_EXPECTED_COMMAND) + 1
# Sleep long enough for at least 1 log. assert logs[-2]["message"] == "Stitching cancelled by user"
time.sleep(0.5) assert re.match(r"^Invocation [0-9a-f-]+ was cancelled", logs[-1]["message"])
# use set() to cancel!
cancel_hook.set()
thread.join()
assert len(caplog.records) < len(FINAL_EXPECTED_COMMAND) + 1
assert caplog.records[-1].message == "Stitching cancelled by user"
def test_final_stitching_command_error(caplog, mocker): def test_final_stitching_command_error(caplog, mocker):
@ -301,4 +326,4 @@ def test_final_stitching_command_error(caplog, mocker):
# than echo. # than echo.
stitcher._extra_args = ["ERROR"] stitcher._extra_args = ["ERROR"]
with pytest.raises(ChildProcessError): with pytest.raises(ChildProcessError):
stitcher.run(cancel=lt.deps.CancelHook(id=uuid.uuid4())) stitcher.run()