Merge branch 'test-env' into 'v3'

Use new LabThingsTestEnv thoughout tests

See merge request openflexure/openflexure-microscope-server!454
This commit is contained in:
Julian Stirling 2026-01-06 13:10:25 +00:00
commit ff976e7187
52 changed files with 271 additions and 269 deletions

5
.gitignore vendored
View file

@ -69,7 +69,6 @@ target/
#Big-o files
*.data
tests/images/out/
#IDE files
.vscode/
@ -89,8 +88,8 @@ openflexure_settings/
/src/openflexure_microscope_server/static/
# Files created by test utilities
/tests/utilities/*.pstats
/tests/utilities/*.png
/tests/unit_tests/utilities/*.pstats
/tests/unit_tests/utilities/*.png
# Files created by simulator
openflexure/

View file

@ -213,7 +213,7 @@ server_integration_tests:
- job: build
artifacts: true
script:
- integration-tests/testfile.py
- tests/integration_tests/testfile.py
pages:
needs:

View file

@ -1,53 +0,0 @@
"""Utilities to help with testing the camera."""
import tempfile
from contextlib import contextmanager
from typing import Optional
from fastapi.testclient import TestClient
import labthings_fastapi as lt
@contextmanager
def camera_test_client_and_server(settings_folder: Optional[str] = None):
"""Yield a camera ThingClient and the associated camera server.
This is a context manager, not a pytest fixture, as it needs to be created
multiple times in some tests.
:param cam: The camera Thing to be used. If not supplied a new one will be created.
:param settings_folder: The settings folder for the camera, if none is supplied, new
temporary directory will be used as the settings folder.
"""
# Create a temp dir, if the setting folder is set it isn't really needed
# but doesn't add much overhead.
with tempfile.TemporaryDirectory() as tmpdir:
if settings_folder is None:
settings_folder = tmpdir
thing_conf = {
"camera": "openflexure_microscope_server.things.camera.picamera:StreamingPiCamera2",
}
server = lt.ThingServer(things=thing_conf, settings_folder=settings_folder)
with TestClient(server.app) as test_client:
client = lt.ThingClient.from_url("/camera/", client=test_client)
yield client, server
del server
@contextmanager
def camera_test_client(settings_folder: Optional[str] = None):
"""Yield a camera ThingClient on a camera server.
This is a context manager, not a pytest fixture, as it needs to be created
multiple times in some tests.
:param cam: The camera Thing to be used. If not supplied a new one will be created.
:param settings_folder: The settings folder for the camera, if none is supplied, new
temporary directory will be used as the settings folder.
"""
with camera_test_client_and_server(
settings_folder=settings_folder
) as client_and_server:
yield client_and_server[0]

View file

@ -1,35 +0,0 @@
"""Shared fixtures for picamera tests."""
import pytest
import labthings_fastapi as lt
from .cam_test_utils import camera_test_client_and_server
@pytest.fixture
def picamera_client_and_server() -> lt.ThingClient:
"""Initialise a test picamera_client and server for the StreamingPiCamera2 Thing.
This fixture:
* Sets up a ThingServer,
* Registers a StreamingPiCamera2 instance at the "camera" endpoint
* Yields a ThingClient and the server for interacting with it during tests.
* The picamera thing can be found at server.things["camera"]
"""
with camera_test_client_and_server() as client_and_server:
yield client_and_server
@pytest.fixture
def picamera_client(picamera_client_and_server) -> lt.ThingClient:
"""Initialise a test picamera_client for the StreamingPiCamera2 Thing.
This fixture:
* Sets up a ThingServer,
* Registers a StreamingPiCamera2 instance at the "camera" endpoint
* return a ThingClient for interacting with it during tests.
"""
return picamera_client_and_server[0]

Binary file not shown.

View file

@ -52,7 +52,7 @@ def _get_hashes(include_coverage: bool = False) -> dict[str, str]:
def run_tests() -> None:
"""Run the picamera tests, zip the coverage database, hash relevant source files."""
subprocess.run(["pytest", "hardware-specific-tests"], check=True)
subprocess.run(["pytest", "tests/hardware_specific_tests"], check=True)
shutil.copyfile(".coverage", COVERAGE_FILE)
hash_dict = _get_hashes(include_coverage=True)
with open(HASH_FILE, "w", encoding="utf-8") as json_file:

View file

@ -94,7 +94,7 @@ addopts = [
norecursedirs = [".git", "build", "node_modules"]
pythonpath = ["."]
testpaths = ["tests"]
testpaths = ["tests/unit_tests"]
[tool.ruff.format]
# Use native line endings for all files
@ -144,7 +144,7 @@ ignore = [
[tool.ruff.lint.per-file-ignores]
# All testing dirs
"{tests,integration-tests,hardware-specific-tests}/**" = [
"{tests}/**" = [
"B018", # Complaining about useless attribute access in tests, but we need them to check errors are raised
"ANN", # Tests are not typehinted for fixtures etc
"S101", # Allow asserts in tests

View file

@ -1,6 +1,4 @@
"""The unit-test suite for the OpenFlexure Microscope Server.
"""Tests for the OpenFlexure Server.
This package contains all of the unit tests that can be run without specific
hardware. See also the `hardware-specific-tests` directory and the
`integration-tests` directory for more testing!.
Each test suite should be run separately.
"""

View file

@ -34,13 +34,13 @@ As before, the server must be stopped before running these tests.
The camera test are very slow as they run on hardware. They can be run with:
pytest hardware-specific-tests
pytest tests/hardware_specific_tests
However, this will not archive the tests in the Git repository for reporting the coverage. For this, see the section above on reporting the coverage.
When writing and debugging these unit tests, it is often best to run a specific test and to use the `-s` flag to see the print statements. It is also often useful to use `--pdb` to drop you into a python debug session on any failure. For example, you might run:
pytest hardware-specific-tests/picamera2/test_exposure_time_drift.py::test_exposure_time_saves_and_loads -s --pdb
pytest tests/hardware_specific_tests/picamera2/test_exposure_time_drift.py::test_exposure_time_saves_and_loads -s --pdb
### CI explanation

View file

@ -0,0 +1,37 @@
"""Utilities to help with testing the camera."""
from contextlib import contextmanager
from typing import Optional
from openflexure_microscope_server.things.camera.picamera import StreamingPiCamera2
from ...shared_utils.lt_test_utils import LabThingsTestEnv
@contextmanager
def camera_test_env(settings_folder: Optional[str] = None):
"""Yield a test environment with a server that contains just the camera.
This is a context manager, not a pytest fixture, as it needs to be created
multiple times in some tests.
:param settings_folder: The settings folder for the camera, if none is supplied, new
temporary directory will be used as the settings folder.
"""
thing_conf = {"camera": StreamingPiCamera2}
with LabThingsTestEnv(things=thing_conf, settings_folder=settings_folder) as env:
yield env
@contextmanager
def camera_test_client(settings_folder: Optional[str] = None):
"""Yield a camera ThingClient on a camera server.
This is a context manager, not a pytest fixture, as it needs to be created
multiple times in some tests.
:param settings_folder: The settings folder for the camera, if none is supplied, new
temporary directory will be used as the settings folder.
"""
with camera_test_env(settings_folder=settings_folder) as env:
yield env.get_thing_client("camera")

View file

@ -0,0 +1,21 @@
"""Shared fixtures for picamera tests."""
import pytest
import labthings_fastapi as lt
from .cam_test_utils import camera_test_env
@pytest.fixture
def picamera_test_env() -> lt.ThingClient:
"""Initialise a test environment with only a StreamingPiCamera2 Thing."""
with camera_test_env() as env:
yield env
@pytest.fixture
def picamera_client() -> lt.ThingClient:
"""Initialise a test picamera_client (in a LabThings test env)."""
with camera_test_env() as env:
yield env.get_thing_client("camera")

View file

@ -3,13 +3,15 @@
import tempfile
from copy import deepcopy
from openflexure_microscope_server.things.camera.picamera import StreamingPiCamera2
from .cam_test_utils import camera_test_client
def test_calibration(picamera_client_and_server):
def test_calibration(picamera_test_env):
"""Check that full auto calibrate completes and set the expected values."""
picamera_client, server = picamera_client_and_server
picamera_thing = server.things["camera"]
picamera_client = picamera_test_env.get_thing_client("camera")
picamera_thing = picamera_test_env.get_thing_by_type(StreamingPiCamera2)
# Check the calibration_required property used by the calibration wizard
assert picamera_thing.calibration_required
# Save copy of default tuning file for end of test

View file

@ -19,11 +19,10 @@ import labthings_fastapi as lt
THIS_DIR: str = os.path.dirname(os.path.realpath(__file__))
WORKING_DIR: str = os.path.join(THIS_DIR, "working_dir")
REPO_DIR: str = os.path.dirname(THIS_DIR)
REPO_DIR: str = os.path.dirname(os.path.dirname(THIS_DIR))
CONFIG_FILE: str = os.path.join(REPO_DIR, "ofm_config_simulation.json")
SERVER_CMD: list[str] = [
"openflexure-microscope-server",
"--fallback",
"-c",
CONFIG_FILE,
]

View file

@ -0,0 +1 @@
"""Shared utilities for all test suites."""

View file

@ -3,13 +3,15 @@
import tempfile
import time
from types import TracebackType
from typing import Any, Optional, Self
from typing import Any, Mapping, Optional, Self, TypeVar
import requests
from fastapi.testclient import TestClient
import labthings_fastapi as lt
ThingSubclass = TypeVar("ThingSubclass", bound=lt.Thing)
ACTION_RUNNING_KEYWORDS = ["idle", "pending", "running"]
@ -29,7 +31,9 @@ class LabThingsTestEnv:
"""
def __init__(
self, things: dict[str, lt.Thing | str], settings_folder: Optional[str] = None
self,
things: Mapping[str, lt.Thing | str],
settings_folder: Optional[str] = None,
) -> None:
"""Initialise the test environment.
@ -41,7 +45,7 @@ class LabThingsTestEnv:
"""
self._server: Optional[lt.ThingServer]
self._test_client: Optional[TestClient]
self._thing_config = things
self._things_config = things
self._settings_folder = settings_folder
self._tmp_dir_obj: Optional[tempfile.TemporaryDirectory] = None
@ -51,7 +55,7 @@ class LabThingsTestEnv:
self._tmp_dir_obj = tempfile.TemporaryDirectory()
self._settings_folder = self._tmp_dir_obj.name
self._server = lt.ThingServer(
things=self._thing_config, settings_folder=self._settings_folder
things=self._things_config, settings_folder=self._settings_folder
)
self._test_client = TestClient(self._server.app)
self._test_client.__enter__()
@ -87,13 +91,61 @@ class LabThingsTestEnv:
if thing_name not in self.server.things:
raise ValueError(f"No Thing named {thing_name}")
def get_thing(self, thing_name: str) -> lt.Thing:
"""Get a Thing from the server by name."""
def get_thing_by_name(self, thing_name: str) -> lt.Thing:
"""Get a Thing from the server by name.
:param thing_name: The name of the thing to on the server.
:return: The Thing with the specified name.
"""
self.check_thing_exists(thing_name)
return self.server.things[thing_name]
def get_thing_by_type(self, thing_class: type[ThingSubclass]) -> ThingSubclass:
"""Get a thing by type.
:param thing_class: The subclass of thing to match.
:return: The Thing that matches the subclass.
:raises RuntimeError: If there are multiple things of the same type, or no
matching thing. If there are multiple things of this type use
``get_thing_by_name`` or ``get_all_things_by_type``.
"""
matching = self.get_all_things_by_type(thing_class)
n_things = len(matching)
if n_things == 0:
raise RuntimeError(f"No Thing of type {thing_class} on this server.")
if n_things > 1:
raise RuntimeError(
f"Cannot get Thing by type as there are {n_things} of type "
f"{thing_class} on this server."
)
return next(iter(matching.values()))
def get_all_things_by_type(
self, thing_class: type[ThingSubclass]
) -> Mapping[str, ThingSubclass]:
"""Get a dictionary of all things by matching a type.
:param thing_class: The subclass of thing to match.
:return: A dictionary of Things that match the subclass. If none match this
will be and empty dictionary.
"""
matching = {}
for thing_name, thing in self.server.things.items():
if isinstance(thing, thing_class):
matching[thing_name] = thing
return matching
def get_thing_client(self, thing_name: str) -> lt.ThingClient:
"""Get a ThingClient for a Thing by name."""
"""Get a ThingClient for a Thing by name.
:param thing_name: The name of the thing to on the server.
:return: A LabThings ThingClient for the Thing with the specified name.
"""
self.check_thing_exists(thing_name)
thing = self.server.things[thing_name]
return lt.ThingClient.from_url(thing.path, self.client)
@ -102,11 +154,27 @@ class LabThingsTestEnv:
self,
thing_name: str,
action_name: str,
action_kwargs: Optional[dict[str, Any]] = None,
action_kwargs: Optional[Mapping[str, Any]] = None,
) -> requests.Response:
"""Start an action and return the server response.
This response can be used to poll or cancel the action.
For most purposes the best way to run an action is to use ``get_thing_client``
to create a ThingClient. At this point any actions can be run with a similar
Python API to calling directly. However, using ThingClient blocks the test
thread.
This function provides an alternative way to start actions without blocking the
test thread. It will return the HTTP response, this response can be used to
poll or cancel the action. Use this method if you:
* Want to test cancelling an action.
* Want to inspect the actions logs exactly as they would come to a web client
* Direcltly interact with the HTTP API
:param thing_name: The name of the Thing on the server.
:param action_name: The name of the action to start.
:action_kwargs: The keyword inputs to the action.
:return: A Response object with the HTTP response.
"""
self.check_thing_exists(thing_name)
url = f"/{thing_name}/{action_name}"
@ -117,8 +185,11 @@ class LabThingsTestEnv:
def poll_action(
self, response: requests.Response, interval: float = 0.01
) -> dict[str, Any]:
"""Poll an action until it completes and return the final response data."""
) -> Mapping[str, Any]:
"""Poll an action until it completes and return the final response data.
:param response: The response from starting this action with ``start_action``.
"""
invocation_data = response.json()
if "status" not in invocation_data:
@ -135,17 +206,20 @@ class LabThingsTestEnv:
return invocation_data
def cancel_action(self, response: requests.Response) -> None:
"""Cancel an ongoing action."""
"""Cancel an ongoing action.
:param response: The response from starting this action with ``start_action``.
"""
invocation_data = response.json()
response = self.client.delete(_invocation_href(invocation_data))
response.raise_for_status()
def _get_link(obj: dict[str, Any], rel: str) -> dict[str, Any]:
def _get_link(obj: Mapping[str, Any], rel: str) -> Mapping[str, Any]:
"""Retrieve a link from an object's `links` list, by its `rel` attribute."""
return next(link for link in obj["links"] if link["rel"] == rel)
def _invocation_href(invocation_data: dict[str, Any]) -> str:
def _invocation_href(invocation_data: Mapping[str, Any]) -> str:
"""Get the invocation href from the invocation response data."""
return _get_link(invocation_data, "self")["href"]

View file

@ -1,79 +0,0 @@
"""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
@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_handle_broken_frame(camera_server):
"""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"]
# Money patch the mjpeg_stream grab_frame to break 1 in 5 frames.
frame_number = 0
original_grabber = camera.mjpeg_stream.grab_frame
async def flaky_grabber():
"""Break 1 in 5 frames."""
# Use a non-local variable to know the frame count.
nonlocal frame_number
frame = await original_grabber()
if frame_number % 5 == 2:
# Make a weird broken frame
frame = frame[:2000] + frame[:2000]
frame_number += 1
return frame
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.
for _i in range(15):
array = camera.grab_as_array()
assert isinstance(array, np.ndarray)
def test_simulation_cam_calibration(camera_server):
"""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

View file

@ -0,0 +1,6 @@
"""The unit-test suite for the OpenFlexure Microscope Server.
This package contains all of the unit tests that can be run without specific
hardware. See also the `hardware_specific_tests` directory and the
`integration_tests` directory for more testing!.
"""

View file

@ -0,0 +1,69 @@
"""Use the Simulation camera to test base camera functionality."""
import numpy as np
import pytest
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 ..shared_utils.lt_test_utils import LabThingsTestEnv
@pytest.fixture
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(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 = test_env.get_thing_by_type(SimulatedCamera)
# Money patch the mjpeg_stream grab_frame to break 1 in 5 frames.
frame_number = 0
original_grabber = camera.mjpeg_stream.grab_frame
async def flaky_grabber():
"""Break 1 in 5 frames."""
# Use a non-local variable to know the frame count.
nonlocal frame_number
frame = await original_grabber()
if frame_number % 5 == 2:
# Make a weird broken frame
frame = frame[:2000] + frame[:2000]
frame_number += 1
return frame
camera.mjpeg_stream.grab_frame = flaky_grabber
# 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):
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(test_env):
"""Test that the simulated camera can be calibrated and reports calibration correctly."""
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

View file

@ -1,6 +1,6 @@
"""Tests for camera classes. These tests focus on checking the camera APIs are equivalent.
Tests for specific camera hardware are in the hardware-specific-tests directory. Tests
Tests for specific camera hardware are in the hardware_specific_tests directory. Tests
on camera functionality using the simulation camera are in "test_camera".
"""

View file

@ -4,27 +4,23 @@ Rather than spinning up a full uvicorn webserver for each test these tests use
the FastAPI ``TestClient`` or directly communicate with the underlying
LabThings-FastAPI code. This increases speed of testing significantly.
For tests that require a full running server see the ``integration-tests``
directory in the root of the repository.
For tests that require a full running server see the ``integration_tests``
directory in the tests directory.
"""
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 ..shared_utils.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()

View file

@ -11,7 +11,7 @@ import pytest
from openflexure_microscope_server import server as ofm_server
THIS_DIR = os.path.dirname(os.path.abspath(__file__))
REPO_ROOT = os.path.dirname(THIS_DIR)
REPO_ROOT = os.path.dirname(os.path.dirname(THIS_DIR))
FULL_CONFIG = os.path.join(REPO_ROOT, "ofm_config_full.json")
SIM_CONFIG = os.path.join(REPO_ROOT, "ofm_config_simulation.json")

View file

@ -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 ..shared_utils.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:

View file

@ -22,7 +22,7 @@ from openflexure_microscope_server.stitching import (
StitcherValidationError,
)
from .utilities.lt_test_utils import LabThingsTestEnv
from ..shared_utils.lt_test_utils import LabThingsTestEnv
# A global logger pretending to the logger from a thing
LOGGER = logging.getLogger("mock-thing_logger")

View file

@ -16,7 +16,7 @@ from openflexure_microscope_server import utilities
# Useful for really finding the repo dir when we mock where it is.
THIS_DIR = os.path.dirname(__file__)
TRUE_REPO_DIR = os.path.dirname(THIS_DIR)
TRUE_REPO_DIR = os.path.dirname(os.path.dirname(THIS_DIR))
# For explicit version checking.
VER_STRING = "3.0.0-alpha4"