From f6d4b368d00112f0a982846f6f3c5dc68b751e86 Mon Sep 17 00:00:00 2001 From: Julian Stirling Date: Wed, 17 Dec 2025 14:03:45 +0000 Subject: [PATCH] Update tests for not having cancel hooks --- tests/test_smart_scan.py | 5 -- tests/test_stitching.py | 111 ++++++++++++++++++++++++--------------- 2 files changed, 68 insertions(+), 48 deletions(-) diff --git a/tests/test_smart_scan.py b/tests/test_smart_scan.py index f753250f..4220db4a 100644 --- a/tests/test_smart_scan.py +++ b/tests/test_smart_scan.py @@ -174,7 +174,6 @@ def _run_only_outer_scan(adjust_initial_state: Optional[Callable] = None): function """ # cancel handle shouldn't be used. Set to arbitrary value for checking - cancel_mock = 1 # not called af_mock = MockAutoFocusThing() stage_mock = MockStageThing() 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""" assert not self._scan_lock.acquire(timeout=0.1) - assert self._cancel is cancel_mock assert self._scan_logger is LOGGER assert self._autofocus is af_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 try: mock_ss_thing.sample_scan( - cancel=cancel_mock, # Shouldn't be used, can be checked logger=LOGGER, autofocus=af_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) 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._autofocus 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._cam = MockCameraThing() - mocker.patch.object(scan_thing, "_cancel") mocker.patch.object(scan_thing, "_main_scan_loop") mocker.patch.object(scan_thing, "_return_to_starting_position") mocker.patch.object(scan_thing, "_perform_final_stitch") diff --git a/tests/test_stitching.py b/tests/test_stitching.py index 55d8df67..0c9b52e7 100644 --- a/tests/test_stitching.py +++ b/tests/test_stitching.py @@ -6,9 +6,8 @@ generated, and the subprocess calling works as expected. import logging import os -import threading +import re import time -import uuid from copy import copy import pytest @@ -23,8 +22,10 @@ from openflexure_microscope_server.stitching import ( StitcherValidationError, ) -# A global logger pretending to be an Invocation Logger -LOGGER = logging.getLogger("mock-invocation_logger") +from .utilities.lt_test_utils import LabThingsTestEnv + +# 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") THIS_DIR: str = os.path.dirname(os.path.realpath(__file__)) 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) 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.start() # 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): stitcher.start() # Wait for it to complete - stitcher.wait(cancel_hook) + stitcher.wait() # It is now not running assert not stitcher.running 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.""" mock_cmd = f"python {MOCK_STITCHER}" mocker.patch("openflexure_microscope_server.stitching.STITCHING_CMD", mock_cmd) - with caplog.at_level(logging.INFO): - 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"] - cancel_hook = lt.deps.CancelHook(id=uuid.uuid4()) - t_start = time.time() - # Start stitching - stitcher.start() - # Run wait() in a thread. - thread = threading.Thread(target=stitcher.wait, kwargs={"cancel": cancel_hook}) - thread.start() - # Sleep long enough for at least 1 log. - time.sleep(0.5) - # use set() to cancel! - cancel_hook.set() - thread.join() - # 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 + t_start = time.time() + # Start the action + response = stitching_test_env.start_action("stitcher", "run_preview") + # 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) + + # If it wasn't cancelled it would hang for 10 s. Here we check the cancel killed + # it within 2s. + assert time.time() - t_start < 2 + assert invocation_data["status"] == "cancelled" + logs = invocation_data["log"] + assert len(logs) == 1 + assert re.match(r"^Invocation [0-9a-f-]+ was cancelled", logs[0]["message"]) 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 ) # 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 # stitcher logs # "Stitching complete" when it ends. 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] -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.""" mock_cmd = f"python {MOCK_STITCHER}" mocker.patch("openflexure_microscope_server.stitching.STITCHING_CMD", mock_cmd) - with caplog.at_level(logging.INFO): - stitcher = FinalStitcher(FAKE_DIR, logger=LOGGER) - cancel_hook = lt.deps.CancelHook(id=uuid.uuid4()) + # Start the action + response = stitching_test_env.start_action("stitcher", "run_final") + # 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. - thread = threading.Thread(target=stitcher.run, kwargs={"cancel": cancel_hook}) - thread.start() - # Sleep long enough for at least 1 log. - time.sleep(0.5) - # 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" + assert invocation_data["status"] == "cancelled" + logs = invocation_data["log"] + assert len(logs) < len(FINAL_EXPECTED_COMMAND) + 1 + assert logs[-2]["message"] == "Stitching cancelled by user" + assert re.match(r"^Invocation [0-9a-f-]+ was cancelled", logs[-1]["message"]) def test_final_stitching_command_error(caplog, mocker): @@ -301,4 +326,4 @@ def test_final_stitching_command_error(caplog, mocker): # than echo. stitcher._extra_args = ["ERROR"] with pytest.raises(ChildProcessError): - stitcher.run(cancel=lt.deps.CancelHook(id=uuid.uuid4())) + stitcher.run()