Move main test suite to new use new test environment.

This commit is contained in:
Julian Stirling 2025-12-17 18:58:35 +00:00
parent 0d09dc58da
commit b70d36c629
3 changed files with 57 additions and 104 deletions

View file

@ -1,38 +1,32 @@
"""Use the Simulation camera to test base camera functionality.""" """Use the Simulation camera to test base camera functionality."""
import tempfile
import numpy as np import numpy as np
import pytest import pytest
from fastapi.testclient import TestClient
from PIL import Image from PIL import Image
import labthings_fastapi as lt 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 @pytest.fixture
def camera_server() -> lt.ThingClient: def test_env() -> lt.ThingClient:
"""Add the camera to a ThingServer and start a TestClient application. """Yield a test environment with the Simulated Camera and Dummy Stage."""
thing_conf = {"camera": SimulatedCamera, "stage": DummyStage}
The test client will be needed for the camera to run async frame generation code. with LabThingsTestEnv(things=thing_conf) as env:
""" yield env
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_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. """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 This simulates the very occasional broken frames that can occur when grabbing
directly from the MJPEG stream. 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. # Money patch the mjpeg_stream grab_frame to break 1 in 5 frames.
frame_number = 0 frame_number = 0
@ -51,29 +45,25 @@ def test_handle_broken_frame(camera_server):
camera.mjpeg_stream.grab_frame = flaky_grabber camera.mjpeg_stream.grab_frame = flaky_grabber
with TestClient(camera_server.app): # Check that this does cause broken frames.
# Check that this does cause broken frames. # The noqa is because we don't know exactly when the error is thrown so we
# 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.
# 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
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.
for _i in range(15): for _i in range(15):
array = camera.grab_as_array() jpeg = camera.grab_jpeg()
assert isinstance(array, np.ndarray) 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.""" """Test that the simulated camera can be calibrated and reports calibration correctly."""
camera = camera_server.things["camera"] camera = test_env.get_thing_by_type(SimulatedCamera)
with TestClient(camera_server.app): assert camera.calibration_required
assert camera.calibration_required camera.full_auto_calibrate()
camera.full_auto_calibrate() assert not camera.calibration_required
assert not camera.calibration_required assert camera.background_detector_status.ready
assert camera.background_detector_status.ready

View file

@ -9,22 +9,18 @@ directory in the root of the repository.
""" """
import json import json
import os
import tempfile
import numpy as np import numpy as np
import piexif import piexif
import pytest import pytest
from fastapi.testclient import TestClient
from PIL import Image from PIL import Image
import labthings_fastapi as lt from .utilities.lt_test_utils import LabThingsTestEnv
@pytest.fixture @pytest.fixture
def thing_server(): def test_env():
"""Yield a server with a very basic configuration.""" """Yield a server with a very basic configuration."""
temp_folder = tempfile.TemporaryDirectory()
thing_conf = { thing_conf = {
"camera": { "camera": {
"class": "openflexure_microscope_server.things.camera.simulation:SimulatedCamera", "class": "openflexure_microscope_server.things.camera.simulation:SimulatedCamera",
@ -41,51 +37,28 @@ def thing_server():
"autofocus": "openflexure_microscope_server.things.autofocus:AutofocusThing", "autofocus": "openflexure_microscope_server.things.autofocus:AutofocusThing",
"camera_stage_mapping": "openflexure_microscope_server.things.camera_stage_mapping:CameraStageMapper", "camera_stage_mapping": "openflexure_microscope_server.things.camera_stage_mapping:CameraStageMapper",
} }
with LabThingsTestEnv(things=thing_conf) as env:
server = lt.ThingServer(things=thing_conf, settings_folder=temp_folder.name) yield env
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
@pytest.fixture def test_autofocus(test_env):
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):
"""Test Fast Autofocus can run doesn't raise an exception.""" """Test Fast Autofocus can run doesn't raise an exception."""
client = slower_client # Adjust the time for stage is 100 microseconds rather than 1 microsecond.
autofocus = lt.ThingClient.from_url("/autofocus/", client) test_env.get_thing_by_name["stage"].step_time = 0.0001
autofocus = test_env.get_thing_client("autofocus")
_ = autofocus.fast_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.""" """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() blob = camera.grab_jpeg()
_image = Image.open(blob.open()) _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.""" """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() blob = camera.capture_jpeg()
image = Image.open(blob.open()) image = Image.open(blob.open())
exif_dict = piexif.load(image.info["exif"]) exif_dict = piexif.load(image.info["exif"])
@ -94,9 +67,9 @@ def test_capture_jpeg_metadata(client):
assert "position" in metadata["stage"] assert "position" in metadata["stage"]
def test_stage(client): def test_stage(test_env):
"""Test moving th stage forwards and backwards.""" """Test moving th stage forwards and backwards."""
stage = lt.ThingClient.from_url("/stage/", client) stage = test_env.get_thing_client("stage")
start = stage.position start = stage.position
move = {"x": 1, "y": 2, "z": 3} move = {"x": 1, "y": 2, "z": 3}
stage.move_relative(**move) stage.move_relative(**move)
@ -109,16 +82,16 @@ def test_stage(client):
assert s == p 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.""" """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()) array = np.asarray(camera.capture_array())
assert array.shape == (240, 320, 3) 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.""" """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.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() camera_stage_mapping.calibrate_xy()

View file

@ -1,10 +1,8 @@
"""Test the stage without creating a full HTTP server and socket connection.""" """Test the stage without creating a full HTTP server and socket connection."""
import itertools import itertools
import tempfile
import pytest import pytest
from fastapi.testclient import TestClient
from httpx import HTTPStatusError from httpx import HTTPStatusError
from hypothesis import HealthCheck, given, settings from hypothesis import HealthCheck, given, settings
from hypothesis import strategies as st from hypothesis import strategies as st
@ -12,12 +10,15 @@ from hypothesis import strategies as st
import labthings_fastapi as lt import labthings_fastapi as lt
from labthings_fastapi.testing import create_thing_without_server 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 ( from openflexure_microscope_server.things.stage import (
BaseStage, BaseStage,
RedefinedBaseMovementError, RedefinedBaseMovementError,
) )
from openflexure_microscope_server.things.stage.dummy import DummyStage 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 # Keep the size and number of moves fairly small or the tests can take forever
point3d = st.tuples( point3d = st.tuples(
st.integers(min_value=-100, max_value=100), 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) 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(): def test_override_base_movement():
"""Child classes of stage should implement functions in the hardware reference frame. """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} 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.""" """Check for expected errors both locally and over http."""
dummy_stage = stage_server.things["stage"] thing_conf = {"camera": SimulatedCamera, "stage": DummyStage}
with TestClient(stage_server.app) as test_client: with LabThingsTestEnv(things=thing_conf) as test_env:
stage_client = lt.ThingClient.from_url("/stage/", client=test_client) 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} assert stage_client.axis_inverted == {"x": True, "y": False, "z": False}
# Can't set an arbitrary value via a client as read only: # Can't set an arbitrary value via a client as read only: