From 347247f45b2625ec90dfe455e92a7f479400b6f5 Mon Sep 17 00:00:00 2001 From: Julian Stirling Date: Wed, 20 Aug 2025 18:59:55 +0100 Subject: [PATCH] Add unit tests that simulates broken frames and catches the correct error. --- tests/test_camera.py | 58 +++++++++++++++++++++++++++++++++++++++++++ tests/test_cameras.py | 6 ++++- 2 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 tests/test_camera.py diff --git a/tests/test_camera.py b/tests/test_camera.py new file mode 100644 index 00000000..02bea9d2 --- /dev/null +++ b/tests/test_camera.py @@ -0,0 +1,58 @@ +"""Use the Simulation camera to test base camera functionality.""" + +import tempfile +from contextlib import contextmanager + +import pytest +from fastapi.testclient import TestClient +import numpy as np +from PIL import Image + +import labthings_fastapi as lt + +from openflexure_microscope_server.things.camera.simulation import SimulatedCamera + + +@contextmanager +def camera_server(camera: SimulatedCamera) -> lt.ThingClient: + """Add the camera to a ThingServer and start a TestClient application. + + The test client application is needed for the camera to have a blocking portal. + """ + with tempfile.TemporaryDirectory() as tmpdir: + server = lt.ThingServer(settings_folder=tmpdir) + server.add_thing(camera, "/camera/") + with TestClient(server.app): + yield server + + +def test_handle_broken_frame(): + """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 = 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 + with camera_server(camera): + portal = lt.get_blocking_portal(camera) + with pytest.raises(OSError, match="broken data stream when reading image file"): + for _i in range(15): + jpeg = camera.grab_jpeg(portal) + np.asarray(Image.open(jpeg.open())) diff --git a/tests/test_cameras.py b/tests/test_cameras.py index 2960bc36..06d282bf 100644 --- a/tests/test_cameras.py +++ b/tests/test_cameras.py @@ -1,4 +1,8 @@ -"""Generic tests for cameras, specific camera hardware reqipres hardware specific tests.""" +"""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 +on camera functionality using the simulation camera are in "test_camera". +""" import pytest