From 6ec60523cd88443b14f66d52cb373e30a1e57bc8 Mon Sep 17 00:00:00 2001 From: Julian Stirling Date: Mon, 22 Dec 2025 18:57:34 +0000 Subject: [PATCH] Ensure that each integration test tests more than just a lack of errors --- tests/integration_tests/test_actions.py | 93 ++++++++++++++++--------- 1 file changed, 62 insertions(+), 31 deletions(-) diff --git a/tests/integration_tests/test_actions.py b/tests/integration_tests/test_actions.py index 535a490a..3635efc8 100644 --- a/tests/integration_tests/test_actions.py +++ b/tests/integration_tests/test_actions.py @@ -12,51 +12,47 @@ it has been moved as it artificaially inflated coverage. """ import json +import os import numpy as np import piexif import pytest +from httpx import HTTPStatusError from PIL import Image from ..shared_utils.lt_test_utils import LabThingsTestEnv +THIS_DIR = os.path.dirname(os.path.abspath(__file__)) +REPO_ROOT = os.path.dirname(os.path.dirname(THIS_DIR)) +SIM_CONFIG = os.path.join(REPO_ROOT, "ofm_config_simulation.json") + @pytest.fixture def test_env(): """Yield a server with a very basic configuration.""" - thing_conf = { - "camera": { - "class": "openflexure_microscope_server.things.camera.simulation:SimulatedCamera", - "kwargs": { - "shape": (240, 320, 3), - "canvas_shape": (1000, 1500, 3), - "frame_interval": 0.01, - }, - }, - "stage": { - "class": "openflexure_microscope_server.things.stage.dummy:DummyStage", - "kwargs": {"step_time": 0.000001}, - }, - "autofocus": "openflexure_microscope_server.things.autofocus:AutofocusThing", - "camera_stage_mapping": "openflexure_microscope_server.things.camera_stage_mapping:CameraStageMapper", - } - with LabThingsTestEnv(things=thing_conf) as env: + with open(SIM_CONFIG, "r", encoding="utf-8") as f_obj: + config_dict = json.load(f_obj) + with LabThingsTestEnv(things=config_dict["things"]) as env: yield env def test_autofocus(test_env): """Test Fast Autofocus can run doesn't raise an exception.""" - # Adjust the time for stage is 100 microseconds rather than 1 microsecond. - test_env.get_thing_by_name("stage").step_time = 0.0001 + stage = test_env.get_thing_client("stage") autofocus = test_env.get_thing_client("autofocus") - _ = autofocus.fast_autofocus() + assert stage.position["z"] == 0 + # Autofocus 5 times and check each ends within 500 steps + for i in range(5): + autofocus.fast_autofocus() + assert abs(stage.position["z"]) < 500, f"Autofocus failed on iteration {i}" def test_grab_jpeg(test_env): """Check that grab_jpeg returns a blob that can be opened.""" camera = test_env.get_thing_client("camera") blob = camera.grab_jpeg() - _image = Image.open(blob.open()) + image = Image.open(blob.open()) + assert image.size == (820, 616) def test_capture_jpeg_metadata(test_env): @@ -68,33 +64,68 @@ def test_capture_jpeg_metadata(test_env): encoded_metadata = exif_dict["Exif"][piexif.ExifIFD.UserComment] metadata = json.loads(encoded_metadata) assert "position" in metadata["stage"] + assert metadata["stage"]["position"] == {"x": 0, "y": 0, "z": 0} + assert image.size == (820, 616) def test_stage(test_env): - """Test moving th stage forwards and backwards.""" + """Test moving the stage forwards and backwards.""" stage = test_env.get_thing_client("stage") start = stage.position move = {"x": 1, "y": 2, "z": 3} + move_back = {"x": -1, "y": -2, "z": -3} stage.move_relative(**move) pos = stage.position - for s, m, p in zip(start.values(), move.values(), pos.values(), strict=True): - assert s + m == p - stage.move_relative(**{k: -v for k, v in move.items()}) + + assert start["x"] + move["x"] == pos["x"] + assert start["y"] + move["y"] == pos["y"] + assert start["z"] + move["z"] == pos["z"] + + # Move back + stage.move_relative(**move_back) pos = stage.position - for s, p in zip(start.values(), pos.values(), strict=True): - assert s == p + + # Check nack at start. + assert start["x"] == pos["x"] + assert start["y"] == pos["y"] + assert start["z"] == pos["z"] def test_capture_array(test_env): """Capture array from simulation and check the size is as expected.""" camera = test_env.get_thing_client("camera") array = np.asarray(camera.capture_array()) - assert array.shape == (240, 320, 3) + assert array.shape == (616, 820, 3) def test_camera_stage_mapping_calibration(test_env): - """Check that camera stage mapping can run without an exception.""" + """Check that camera stage mapping runs and returns the expected result.""" camera = test_env.get_thing_client("camera") + # Remove camera settling time for speed. camera.settling_time = 0 - camera_stage_mapping = test_env.get_thing_client("camera_stage_mapping") - camera_stage_mapping.calibrate_xy() + csm = test_env.get_thing_client("camera_stage_mapping") + stage = test_env.get_thing_client("stage") + + # Check it starts uncalibrated + assert csm.calibration_required + assert csm.image_to_stage_displacement_matrix is None + assert csm.last_calibration is None + # And therefore that actions error + with pytest.raises(HTTPStatusError): + csm.move_in_image_coordinates(x=10) + + # Calibrate + csm.calibrate_xy() + + assert not csm.calibration_required + assert csm.image_to_stage_displacement_matrix is not None + assert isinstance(csm.last_calibration, dict) + # Check it returned home + assert stage.position == {"x": 0, "y": 0, "z": 0} + csm.move_in_image_coordinates(x=10, y=0) + assert stage.position == {"x": -5, "y": 0, "z": 0} + csm_matrix = csm.image_to_stage_displacement_matrix + expected_matrix = [[0, 0.5], [-0.5, 0]] + assert isinstance(csm_matrix, list) + # Check CSM is as expected to an absolute tolerance of 1e-3 + assert np.allclose(csm_matrix, expected_matrix, atol=1e-3)