openflexure-microscope-server/tests/test_cameras.py

134 lines
5.1 KiB
Python

"""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
from openflexure_microscope_server.things.camera import BaseCamera
from openflexure_microscope_server.things.camera.simulation import SimulatedCamera
from openflexure_microscope_server.things.camera.opencv import OpenCVCamera
@pytest.fixture
def mock_picam_thing(mocker):
"""Import PiCamera without hardware well enough to get a ThingDescription."""
dummy_cam = mocker.Mock()
mock_picamera2 = mocker.MagicMock()
mock_picamera2.return_value.__enter__.return_value = dummy_cam
mock_picamera2.return_value.__exit__.return_value = None
mocker.patch.dict(
"sys.modules",
{
"picamera2": mock_picamera2,
"picamera2.encoders": mocker.Mock(),
"picamera2.outputs": mocker.Mock(),
},
)
from openflexure_microscope_server.things.camera.picamera import StreamingPiCamera2
return StreamingPiCamera2()
def _get_clean_camera_description(camera_thing):
"""Return actions and properties for a camera Thing, separating those exposed to the UI.
Return cleaned sets of actions and properties for camera_thing, excluding
calibration actions exposed to the UI as primary or secondary calibration actions,
and excluding the properties exposed in manual camera settings.
:returns: Dict with keys:
* "actions" - Actions not explicitly exposed to the UI by this camera.
* "properties" - Properties not explicitly exposed to the UI as manual camera
settings.
* "calibration_actions" - Actions explicitly exposed to the UI by this camera.
* "manual_properties" - Properties explicitly exposed to the UI as manual
camera settings.
"""
camera_thing.path = "/mock/"
td = camera_thing.thing_description()
actions = set(td.actions.keys())
properties = set(td.properties.keys())
# Calibration actions are camera specific
primary = [btn.action for btn in camera_thing.primary_calibration_actions]
secondary = [btn.action for btn in camera_thing.secondary_calibration_actions]
calibration_actions = set(primary + secondary)
actions -= calibration_actions
# Manual properties are also camera specific
manual_props = {ctrl.property_name for ctrl in camera_thing.manual_camera_settings}
properties -= manual_props
return {
"actions": actions,
"properties": properties,
"calibration_actions": calibration_actions,
"manual_properties": manual_props,
}
def test_thing_description_equivalence(mock_picam_thing):
"""Ensure the built-in camera Thing subclasses retain consistent core functionality.
This test verifies that extra actions are not unintentionally introduced to
camera child classes. Any addition of actions must be accompanied by an update
to this test, prompting discussion of whether the action belongs in the subclass
or the base camera class.
"""
base_td = BaseCamera().thing_description()
base_actions = set(base_td.actions.keys())
base_props = set(base_td.properties.keys())
sim_description = _get_clean_camera_description(SimulatedCamera())
opencv_description = _get_clean_camera_description(OpenCVCamera())
picamera_description = _get_clean_camera_description(mock_picam_thing)
# Note. These are the actions and properties that are not exposed to the UI via
# `primary_calibration_actions`, `secondary_calibration_actions`, or
# `manual_camera_settings`.
sim_actions = sim_description["actions"]
sim_props = sim_description["properties"]
opencv_actions = opencv_description["actions"]
opencv_props = opencv_description["properties"]
picamera_actions = picamera_description["actions"]
picamera_props = picamera_description["properties"]
# Camera actions and properties should generally be equivalent except for exposed
# manual settings and calibration actions.
assert opencv_actions == sim_actions == base_actions
assert opencv_props == sim_props == base_props
# For now PiCamera has a number of extra actions and properties. These should be
# reduced over time by creating a way to use the functionality in a way as clearly
# defined as PiCamera specific, or by replicating the functionality for other
# cameras.
picamera_extra_actions = {
"flat_lens_shading_chrominance",
"set_static_green_equalisation",
"stop_streaming",
"reset_ccm",
}
picamera_extra_props = {
"mjpeg_bitrate",
"colour_correction_matrix",
"lens_shading_tables",
"sensor_resolution",
"lens_shading_is_static",
"capture_metadata",
"camera_configuration",
"stream_resolution",
"tuning",
"sensor_modes",
"sensor_mode",
}
assert picamera_actions - base_actions == picamera_extra_actions
assert picamera_props - base_props == picamera_extra_props