"""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