diff --git a/tests/test_cameras.py b/tests/test_cameras.py new file mode 100644 index 00000000..f498d082 --- /dev/null +++ b/tests/test_cameras.py @@ -0,0 +1,133 @@ +"""Generic tests for cameras, specific camera hardware reqipres hardware specific tests.""" + +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(), + }, + ) + mocker.patch( + "openflexure_microscope_server.things.camera.picamera.recalibrate_utils.load_default_tuning", + return_value={"mock": "tuning"}, + ) + 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 extra actions and properties are not added to camera Things without explicit approval. + + 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 review and justification for why the action does not + belong to the general base 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