From b70d36c629466b0d71cfcea50ea2c7633a8f83fa Mon Sep 17 00:00:00 2001 From: Julian Stirling Date: Wed, 17 Dec 2025 18:58:35 +0000 Subject: [PATCH] Move main test suite to new use new test environment. --- tests/test_camera.py | 70 ++++++++++++++++---------------------- tests/test_dummy_server.py | 65 +++++++++++------------------------ tests/test_stage.py | 26 +++++--------- 3 files changed, 57 insertions(+), 104 deletions(-) diff --git a/tests/test_camera.py b/tests/test_camera.py index 07fb1181..9275bdf2 100644 --- a/tests/test_camera.py +++ b/tests/test_camera.py @@ -1,38 +1,32 @@ """Use the Simulation camera to test base camera functionality.""" -import tempfile - import numpy as np import pytest -from fastapi.testclient import TestClient from PIL import Image import labthings_fastapi as lt +from openflexure_microscope_server.things.camera.simulation import SimulatedCamera +from openflexure_microscope_server.things.stage.dummy import DummyStage + +from .utilities.lt_test_utils import LabThingsTestEnv + @pytest.fixture -def camera_server() -> lt.ThingClient: - """Add the camera to a ThingServer and start a TestClient application. - - The test client will be needed for the camera to run async frame generation code. - """ - with tempfile.TemporaryDirectory() as tmpdir: - thing_conf = { - "camera": "openflexure_microscope_server.things.camera.simulation:SimulatedCamera", - "stage": "openflexure_microscope_server.things.stage.dummy:DummyStage", - } - - server = lt.ThingServer(things=thing_conf, settings_folder=tmpdir) - yield server +def test_env() -> lt.ThingClient: + """Yield a test environment with the Simulated Camera and Dummy Stage.""" + thing_conf = {"camera": SimulatedCamera, "stage": DummyStage} + with LabThingsTestEnv(things=thing_conf) as env: + yield env -def test_handle_broken_frame(camera_server): +def test_handle_broken_frame(test_env): """Monkey patch the the mjpeg steam so 1 in 5 frames are broken, then test operation. This simulates the very occasional broken frames that can occur when grabbing directly from the MJPEG stream. """ - camera = camera_server.things["camera"] + camera = test_env.get_thing_by_type(SimulatedCamera) # Money patch the mjpeg_stream grab_frame to break 1 in 5 frames. frame_number = 0 @@ -51,29 +45,25 @@ def test_handle_broken_frame(camera_server): camera.mjpeg_stream.grab_frame = flaky_grabber - with TestClient(camera_server.app): - # Check that this does cause broken frames. - # The noqa is because we don't know exactly when the error is thrown so we - # can't have a single simple statement in the pytest raises. - with pytest.raises( # noqa PT012 - OSError, match="broken data stream when reading image file" - ): - for _i in range(15): - jpeg = camera.grab_jpeg() - np.asarray(Image.open(jpeg.open())) - - # Check that grab_as_array handles the broken frames and completes without - # the same error. + # Check that this does cause broken frames. + # The noqa is because we don't know exactly when the error is thrown so we + # can't have a single simple statement in the pytest raises. + with pytest.raises(OSError, match="broken data stream when reading image file"): # noqa PT012 for _i in range(15): - array = camera.grab_as_array() - assert isinstance(array, np.ndarray) + jpeg = camera.grab_jpeg() + np.asarray(Image.open(jpeg.open())) + + # Check that grab_as_array handles the broken frames and completes without + # the same error. + for _i in range(15): + array = camera.grab_as_array() + assert isinstance(array, np.ndarray) -def test_simulation_cam_calibration(camera_server): +def test_simulation_cam_calibration(test_env): """Test that the simulated camera can be calibrated and reports calibration correctly.""" - camera = camera_server.things["camera"] - with TestClient(camera_server.app): - assert camera.calibration_required - camera.full_auto_calibrate() - assert not camera.calibration_required - assert camera.background_detector_status.ready + camera = test_env.get_thing_by_type(SimulatedCamera) + assert camera.calibration_required + camera.full_auto_calibrate() + assert not camera.calibration_required + assert camera.background_detector_status.ready diff --git a/tests/test_dummy_server.py b/tests/test_dummy_server.py index 38808df3..e26e3d62 100644 --- a/tests/test_dummy_server.py +++ b/tests/test_dummy_server.py @@ -9,22 +9,18 @@ directory in the root of the repository. """ import json -import os -import tempfile import numpy as np import piexif import pytest -from fastapi.testclient import TestClient from PIL import Image -import labthings_fastapi as lt +from .utilities.lt_test_utils import LabThingsTestEnv @pytest.fixture -def thing_server(): +def test_env(): """Yield a server with a very basic configuration.""" - temp_folder = tempfile.TemporaryDirectory() thing_conf = { "camera": { "class": "openflexure_microscope_server.things.camera.simulation:SimulatedCamera", @@ -41,51 +37,28 @@ def thing_server(): "autofocus": "openflexure_microscope_server.things.autofocus:AutofocusThing", "camera_stage_mapping": "openflexure_microscope_server.things.camera_stage_mapping:CameraStageMapper", } - - server = lt.ThingServer(things=thing_conf, settings_folder=temp_folder.name) - - assert os.path.exists(os.path.join(temp_folder.name, "camera")) - # Note: yield is important. If return is used the temp folder gets deleted - # before the test runs. Silence PT022 as ruff doesn't think yield is needed. - yield server # noqa: PT022 + with LabThingsTestEnv(things=thing_conf) as env: + yield env -@pytest.fixture -def client(thing_server): - """Yield a FastAPI TestClient for the server.""" - with TestClient(thing_server.app) as client: - yield client - - -@pytest.fixture -def slower_client(thing_server): - """Yield a FastAPI TestClient for the server with a slower moving stage. - - The step time for the stage is 100 microseconds rather than - 1 microsecond. - """ - thing_server.things["stage"].step_time = 0.0001 - with TestClient(thing_server.app) as client: - yield client - - -def test_autofocus(slower_client): +def test_autofocus(test_env): """Test Fast Autofocus can run doesn't raise an exception.""" - client = slower_client - autofocus = lt.ThingClient.from_url("/autofocus/", client) + # Adjust the time for stage is 100 microseconds rather than 1 microsecond. + test_env.get_thing_by_name["stage"].step_time = 0.0001 + autofocus = test_env.get_thing_client("autofocus") _ = autofocus.fast_autofocus() -def test_grab_jpeg(client): +def test_grab_jpeg(test_env): """Check that grab_jpeg returns a blob that can be opened.""" - camera = lt.ThingClient.from_url("/camera/", client) + camera = test_env.get_thing_client("camera") blob = camera.grab_jpeg() _image = Image.open(blob.open()) -def test_capture_jpeg_metadata(client): +def test_capture_jpeg_metadata(test_env): """Check that the position is encoded into the image metadata.""" - camera = lt.ThingClient.from_url("/camera/", client) + camera = test_env.get_thing_client("camera") blob = camera.capture_jpeg() image = Image.open(blob.open()) exif_dict = piexif.load(image.info["exif"]) @@ -94,9 +67,9 @@ def test_capture_jpeg_metadata(client): assert "position" in metadata["stage"] -def test_stage(client): +def test_stage(test_env): """Test moving th stage forwards and backwards.""" - stage = lt.ThingClient.from_url("/stage/", client) + stage = test_env.get_thing_client("stage") start = stage.position move = {"x": 1, "y": 2, "z": 3} stage.move_relative(**move) @@ -109,16 +82,16 @@ def test_stage(client): assert s == p -def test_capture_array(client): +def test_capture_array(test_env): """Capture array from simulation and check the size is as expected.""" - camera = lt.ThingClient.from_url("/camera/", client) + camera = test_env.get_thing_client("camera") array = np.asarray(camera.capture_array()) assert array.shape == (240, 320, 3) -def test_camera_stage_mapping_calibration(client): +def test_camera_stage_mapping_calibration(test_env): """Check that camera stage mapping can run without an exception.""" - camera = lt.ThingClient.from_url("/camera/", client) + camera = test_env.get_thing_client("camera") camera.settling_time = 0 - camera_stage_mapping = lt.ThingClient.from_url("/camera_stage_mapping/", client) + camera_stage_mapping = test_env.get_thing_client("camera_stage_mapping") camera_stage_mapping.calibrate_xy() diff --git a/tests/test_stage.py b/tests/test_stage.py index 22ec73dc..36acba7b 100644 --- a/tests/test_stage.py +++ b/tests/test_stage.py @@ -1,10 +1,8 @@ """Test the stage without creating a full HTTP server and socket connection.""" import itertools -import tempfile import pytest -from fastapi.testclient import TestClient from httpx import HTTPStatusError from hypothesis import HealthCheck, given, settings from hypothesis import strategies as st @@ -12,12 +10,15 @@ from hypothesis import strategies as st import labthings_fastapi as lt from labthings_fastapi.testing import create_thing_without_server +from openflexure_microscope_server.things.camera.simulation import SimulatedCamera from openflexure_microscope_server.things.stage import ( BaseStage, RedefinedBaseMovementError, ) from openflexure_microscope_server.things.stage.dummy import DummyStage +from .utilities.lt_test_utils import LabThingsTestEnv + # Keep the size and number of moves fairly small or the tests can take forever point3d = st.tuples( st.integers(min_value=-100, max_value=100), @@ -34,18 +35,6 @@ def dummy_stage(): return create_thing_without_server(DummyStage, step_time=0.000001) -@pytest.fixture -def stage_server(): - """Yield a server with a very basic configuration.""" - thing_conf = { - "camera": "openflexure_microscope_server.things.camera.simulation:SimulatedCamera", - "stage": "openflexure_microscope_server.things.stage.dummy:DummyStage", - } - with tempfile.TemporaryDirectory() as tmpdir: - server = lt.ThingServer(things=thing_conf, settings_folder=tmpdir) - yield server - - def test_override_base_movement(): """Child classes of stage should implement functions in the hardware reference frame. @@ -157,11 +146,12 @@ def test_direction_inversion(dummy_stage): assert dummy_stage.axis_inverted == {"x": True, "y": False, "z": False} -def test_direction_errors_local_and_http(stage_server): +def test_direction_errors_local_and_http(): """Check for expected errors both locally and over http.""" - dummy_stage = stage_server.things["stage"] - with TestClient(stage_server.app) as test_client: - stage_client = lt.ThingClient.from_url("/stage/", client=test_client) + thing_conf = {"camera": SimulatedCamera, "stage": DummyStage} + with LabThingsTestEnv(things=thing_conf) as test_env: + dummy_stage = test_env.get_thing_by_type(DummyStage) + stage_client = test_env.get_thing_client("stage") assert stage_client.axis_inverted == {"x": True, "y": False, "z": False} # Can't set an arbitrary value via a client as read only: