"""Unit tests for the IDS Camera device.""" import threading from unittest import mock import numpy as np import pytest from csaxs_bec.devices.ids_cameras.ids_camera import IDSCamera @pytest.fixture(scope="function") def ids_camera(): """Fixture for creating an instance of the IDSCamera.""" camera = IDSCamera( name="test_camera", camera_id=1, prefix="test:", scan_info=None, m_n_colormode=1, bits_per_pixel=24, live_mode=False, ) # Mock camera connection and attributes camera.cam = mock.Mock() camera.cam._connected = True camera.cam.cam = mock.Mock() camera.cam.cam.width.value = 2 camera.cam.cam.height.value = 2 yield camera def test_mask_setter_getter(ids_camera): """Test the mask setter and getter methods.""" mask = np.zeros((2, 2), dtype=np.uint8) mask[0, 0] = 1 ids_camera.mask = mask assert np.array_equal(ids_camera.mask, mask) def test_mask_setter_invalid_shape(ids_camera): """Test the mask setter with an invalid shape.""" with pytest.raises(ValueError): ids_camera.mask = np.zeros((3, 3), dtype=np.uint8) # Exceeds mocked camera dimensions def test_on_connected_sets_mask_and_live_mode(ids_camera): """Test the on_connected method to ensure it sets the mask and live mode.""" ids_camera.cam.on_connect = mock.Mock() ids_camera.on_connected() ids_camera.cam.on_connect.assert_called_once() expected_mask = np.ones((2, 2), dtype=np.uint8) assert np.array_equal(ids_camera.mask, expected_mask) def test_on_trigger_roi_signal(ids_camera): """Test the on_trigger method to ensure it processes the ROI signal correctly.""" ids_camera.start_live_mode() test_image = np.array([[2, 4], [6, 8]]) test_mask = np.array([[1, 0], [0, 1]], dtype=np.uint8) ids_camera.mask = test_mask mock_image = mock.Mock() mock_image.data = test_image ids_camera.image.get = mock.Mock(return_value=mock_image) ids_camera.roi_signal.put = mock.Mock(side_effect=ids_camera.roi_signal.put) ids_camera.on_trigger() expected_value = (2 * 1 + 4 * 0 + 6 * 0 + 8 * 1) / (np.sum(test_mask) * 1) result = ids_camera.roi_signal.get() assert np.isclose( result.content["signals"][ids_camera.roi_signal.name]["value"], expected_value, atol=1e-6 ) def test_get_last_image(ids_camera): """Test the get_last_image method to ensure it returns the last captured image.""" test_image = np.array([[1, 2], [3, 4]], dtype=np.uint8) mock_image = mock.Mock() mock_image.data = test_image ids_camera.image.get = mock.Mock(return_value=mock_image) result = ids_camera.get_last_image() assert np.array_equal(result, test_image) def test_get_live_fps_none_before_frames(ids_camera): """No frames pushed yet -> get_live_fps() returns None.""" assert ids_camera.get_live_fps() is None def test_get_live_fps_computes_rate(ids_camera): """get_live_fps() computes rate from the recorded push timestamps.""" ids_camera._live_frame_times.extend([0.0, 0.2, 0.4, 0.6, 0.8]) assert ids_camera.get_live_fps() == pytest.approx(5.0) def test_live_mode_loop_records_frame_times(ids_camera): """_live_mode_loop() appends one timestamp per pushed frame.""" ids_camera.cam.get_image_data = mock.Mock(return_value=np.zeros((2, 2), dtype=np.uint8)) stop_event = threading.Event() calls = {"n": 0} def fake_wait(timeout): calls["n"] += 1 if calls["n"] >= 3: stop_event.set() return False stop_event.wait = fake_wait ids_camera._live_mode_loop(stop_event) assert len(ids_camera._live_frame_times) == 3 assert ids_camera.get_live_fps() is not None def test_on_destroy(ids_camera): """Test the on_destroy method to ensure it cleans up resources.""" ids_camera.cam.on_disconnect = mock.Mock() ids_camera.on_destroy() ids_camera.cam.on_disconnect.assert_called_once() def test_push_preview_image_compensates_rotation_and_transpose(): """push_preview_image() is meant for data already display-oriented (e.g. built from get_last_image() frames, which already have num_rotation_90/ transpose applied by PreviewSignal.put()). Pushing it through must undo that transform first so put()'s own transform doesn't apply it twice -- net effect: what comes back out via .get() matches what went in. """ camera = IDSCamera( name="test_camera_rot", camera_id=1, prefix="test:", scan_info=None, m_n_colormode=1, bits_per_pixel=24, live_mode=False, num_rotation_90=3, transpose=True, ) camera.cam = mock.Mock() camera.cam._connected = True display_oriented = np.arange(12, dtype=np.uint8).reshape(3, 4) camera.push_preview_image(display_oriented) result = camera.image.get().data assert np.array_equal(result, display_oriented) def test_push_smear_preview_no_rotation_compensation(ids_camera): """smear_preview has no rotation_90/transpose configured, so pushed data passes straight through unmodified -- unlike push_preview_image, no compensation is needed (or applied).""" data = np.arange(4, dtype=np.uint8).reshape(2, 2) ids_camera.push_smear_preview(data) result = ids_camera.smear_preview.get().data assert np.array_equal(result, data)