diff --git a/parallax/__init__.py b/parallax/__init__.py index 0c5014b..82e8f45 100644 --- a/parallax/__init__.py +++ b/parallax/__init__.py @@ -4,7 +4,7 @@ import os -__version__ = "1.0.1" +__version__ = "1.1.0" # allow multiple OpenMP instances os.environ["KMP_DUPLICATE_LIB_OK"] = "True" diff --git a/tests/test__setting.py b/tests/test__setting.py new file mode 100644 index 0000000..af8e467 --- /dev/null +++ b/tests/test__setting.py @@ -0,0 +1,11 @@ +import pytest +from PyQt5.QtWidgets import QApplication + +@pytest.fixture(scope="session") +def qapp(): + app = QApplication([]) + yield app + app.quit() + +print("\nInitializing QApplication for tests...") + diff --git a/tests/test_calculator.py b/tests/test_calculator.py new file mode 100644 index 0000000..be5ffa6 --- /dev/null +++ b/tests/test_calculator.py @@ -0,0 +1,123 @@ +import pytest +from unittest.mock import MagicMock +from PyQt5.QtWidgets import QLineEdit, QComboBox +import numpy as np +from parallax.calculator import Calculator + +@pytest.fixture +def setup_calculator(qtbot): + # Mocking the model and stage_controller + model = MagicMock() + reticle_selector = QComboBox() + stage_controller = MagicMock() + + # Mock the model's stages and transforms + model.stages = {'stage1': MagicMock(), 'stage2': MagicMock()} + model.transforms = {'stage1': (np.eye(4), [1, 1, 1]), 'stage2': (None, None)} + + # Initialize the Calculator widget + calculator = Calculator(model, reticle_selector, stage_controller) + qtbot.addWidget(calculator) + calculator.show() + + return calculator, model, stage_controller + +def test_set_current_reticle(setup_calculator, qtbot): + calculator, model, stage_controller = setup_calculator + + # Simulate selecting a reticle in the dropdown + calculator.reticle_selector.addItem("Global coords (A)") + calculator.reticle_selector.addItem("Global coords (B)") + calculator.reticle_selector.setCurrentIndex(1) + + # Trigger reticle change + calculator._setCurrentReticle() + + assert calculator.reticle == "B", "Reticle should be set to 'B'" + +@pytest.mark.parametrize("localX, localY, localZ, transM_LR, scale, expected_globalX, expected_globalY, expected_globalZ", [ + # Case 1: Complex transformation with scaling (your initial case) + (10775.0, 6252.0, 6418.0, + np.array([[0.991319402, 0.079625596, -0.104621259, -10801.14725], + [-0.092116645, 0.988422741, -0.120561228, 6332.440016], + [0.093810271, 0.129152044, 0.987177483, 6122.5096], + [0, 0, 0, 1]]), + np.array([0.994494443, -0.988947511, -0.995326937]), + -2.5, 4.2, 23.1), + + # Case 2: Basic translation vector without scaling + (100.0, 200.0, 300.0, + np.array([[1, 0, 0, 10], + [0, 1, 0, 20], + [0, 0, 1, 30], + [0, 0, 0, 1]]), + np.array([1, 1, 1]), + 110.0, 220.0, 330.0) +]) +def test_transform_local_to_global(setup_calculator, qtbot, localX, localY, localZ, transM_LR, scale, expected_globalX, expected_globalY, expected_globalZ): + calculator, model, stage_controller = setup_calculator + + # Mock the QLineEdit fields for stage1 + qtbot.keyClicks(calculator.findChild(QLineEdit, 'localX_stage1'), str(localX)) + qtbot.keyClicks(calculator.findChild(QLineEdit, 'localY_stage1'), str(localY)) + qtbot.keyClicks(calculator.findChild(QLineEdit, 'localZ_stage1'), str(localZ)) + + # Simulate conversion + calculator._convert('stage1', transM_LR, scale) + + # Retrieve the global coordinates from the UI + globalX = calculator.findChild(QLineEdit, 'globalX_stage1').text() + globalY = calculator.findChild(QLineEdit, 'globalY_stage1').text() + globalZ = calculator.findChild(QLineEdit, 'globalZ_stage1').text() + + # Convert text values to float for assertion + globalX = float(globalX) + globalY = float(globalY) + globalZ = float(globalZ) + + # Check if the transformed values match expected values + assert globalX == pytest.approx(expected_globalX, abs=5), f"Expected globalX to be {expected_globalX}, got {globalX}" + assert globalY == pytest.approx(expected_globalY, abs=5), f"Expected globalY to be {expected_globalY}, got {globalY}" + assert globalZ == pytest.approx(expected_globalZ, abs=5), f"Expected globalZ to be {expected_globalZ}, got {globalZ}" + +def test_transform_global_to_local(setup_calculator, qtbot): + calculator, model, stage_controller = setup_calculator + + # Mock the QLineEdit fields for stage1 + qtbot.keyClicks(calculator.findChild(QLineEdit, 'globalX_stage1'), "15.0") + qtbot.keyClicks(calculator.findChild(QLineEdit, 'globalY_stage1'), "25.0") + qtbot.keyClicks(calculator.findChild(QLineEdit, 'globalZ_stage1'), "35.0") + + # Mock transformation matrix and scale for stage1 + transM_LR = np.array([[1, 0, 0, 5], [0, 1, 0, 5], [0, 0, 1, 5], [0, 0, 0, 1]]) + scale = np.array([1, 1, 1]) + + # Simulate conversion + calculator._convert('stage1', transM_LR, scale) + + localX = calculator.findChild(QLineEdit, 'localX_stage1').text() + localY = calculator.findChild(QLineEdit, 'localY_stage1').text() + localZ = calculator.findChild(QLineEdit, 'localZ_stage1').text() + + assert localX == "10.00", f"Expected localX to be 10.00, got {localX}" + assert localY == "20.00", f"Expected localY to be 20.00, got {localY}" + assert localZ == "30.00", f"Expected localZ to be 30.00, got {localZ}" + +def test_clear_fields(setup_calculator, qtbot): + calculator, model, stage_controller = setup_calculator + + # Set some values in the QLineEdit fields + calculator.findChild(QLineEdit, 'localX_stage1').setText("10.0") + calculator.findChild(QLineEdit, 'localY_stage1').setText("20.0") + calculator.findChild(QLineEdit, 'localZ_stage1').setText("30.0") + + # Clear the fields + calculator._clear_fields('stage1') + + localX = calculator.findChild(QLineEdit, 'localX_stage1').text() + localY = calculator.findChild(QLineEdit, 'localY_stage1').text() + localZ = calculator.findChild(QLineEdit, 'localZ_stage1').text() + + assert localX == "", "Expected localX to be cleared" + assert localY == "", "Expected localY to be cleared" + assert localZ == "", "Expected localZ to be cleared" diff --git a/tests/test_calibration_camera.py b/tests/test_calibration_camera.py new file mode 100644 index 0000000..7ded53d --- /dev/null +++ b/tests/test_calibration_camera.py @@ -0,0 +1,147 @@ +import pytest +import numpy as np +from unittest.mock import MagicMock, patch +from parallax.calibration_camera import CalibrationStereo, OBJPOINTS + +@pytest.fixture +def mock_model(): + """Fixture to create a mock model for testing.""" + model = MagicMock() + return model + +@pytest.fixture +def intrinsic_data(): + """Fixture for mock intrinsic data for both cameras.""" + intrinsicA = [ + np.array([[1.54e+04, 0.0e+00, 2.0e+03], + [0.0e+00, 1.54e+04, 1.5e+03], + [0.0e+00, 0.0e+00, 1.0e+00]]), + np.array([[0., 0., 0., 0., 0.]]), # Distortion coefficients + (np.array([[-2.88], [-0.08], [-0.47]]),), # rvecA + (np.array([[1.08], [1.01], [58.70]]),) # tvecA + ] + intrinsicB = [ + np.array([[1.54e+04, 0.0e+00, 2.0e+03], + [0.0e+00, 1.54e+04, 1.5e+03], + [0.0e+00, 0.0e+00, 1.0e+00]]), + np.array([[0., 0., 0., 0., 0.]]), # Distortion coefficients + (np.array([[2.64], [-0.29], [0.35]]),), # rvecB + (np.array([[1.24], [0.33], [56.22]]),) # tvecB + ] + return intrinsicA, intrinsicB + +@pytest.fixture +def imgpoints_data(): + """Fixture for mock image points for both cameras.""" + imgpointsA = [ + np.array([ + [1786, 1755], [1837, 1756], [1887, 1757], [1937, 1758], [1987, 1759], + [2036, 1760], [2086, 1761], [2136, 1762], [2186, 1763], [2235, 1764], + [2285, 1765], [2334, 1766], [2383, 1767], [2432, 1768], [2481, 1769], + [2530, 1770], [2579, 1771], [2628, 1772], [2676, 1773], [2725, 1774], + [2773, 1775] + ], dtype=np.float32), + np.array([ + [2233, 2271], [2238, 2220], [2244, 2170], [2249, 2120], [2254, 2069], + [2259, 2019], [2264, 1968], [2269, 1918], [2275, 1866], [2280, 1816], + [2285, 1765], [2290, 1714], [2295, 1663], [2300, 1612], [2306, 1561], + [2311, 1509], [2316, 1459], [2321, 1407], [2327, 1355], [2332, 1304], + [2337, 1252] + ], dtype=np.float32) + ] + imgpointsB = [ + np.array([ + [1822, 1677], [1875, 1668], [1927, 1660], [1979, 1651], [2031, 1643], + [2083, 1635], [2135, 1626], [2187, 1618], [2239, 1609], [2290, 1601], + [2341, 1593], [2392, 1584], [2443, 1576], [2494, 1568], [2545, 1559], + [2596, 1551], [2647, 1543], [2698, 1535], [2748, 1526], [2799, 1518], + [2850, 1510] + ], dtype=np.float32), + np.array([ + [2494, 2081], [2478, 2031], [2463, 1982], [2448, 1933], [2432, 1884], + [2417, 1835], [2402, 1786], [2387, 1738], [2371, 1689], [2356, 1640], + [2341, 1593], [2326, 1544], [2311, 1496], [2296, 1449], [2281, 1401], + [2267, 1354], [2252, 1306], [2237, 1259], [2222, 1212], [2208, 1165], + [2193, 1118] + ], dtype=np.float32) + ] + return imgpointsA, imgpointsB + +@pytest.fixture +def setup_calibration_stereo(mock_model, intrinsic_data, imgpoints_data): + """Fixture to set up a CalibrationStereo instance.""" + camA = "22517664" + camB = "22468054" + imgpointsA, imgpointsB = imgpoints_data + intrinsicA, intrinsicB = intrinsic_data + + calib_stereo = CalibrationStereo( + model=mock_model, + camA=camA, + imgpointsA=imgpointsA, + intrinsicA=intrinsicA, + camB=camB, + imgpointsB=imgpointsB, + intrinsicB=intrinsicB + ) + return calib_stereo + +def test_calibrate_stereo(setup_calibration_stereo): + """Test the stereo calibration process.""" + calib_stereo = setup_calibration_stereo + + # Mock the cv2.stereoCalibrate method with expected results + mock_retval = 0.4707333710438937 + mock_R_AB = np.array([ + [0.92893564, 0.32633462, 0.17488367], + [-0.3667657, 0.7465183, 0.55515165], + [0.05061134, -0.57984149, 0.81315579] + ]) + mock_T_AB = np.array([[-10.35397621], [-32.59591834], [9.0524749]]) + mock_E_AB = np.array([ + [1.67041403, 12.14262756, -31.53105623], + [8.93319518, -3.04952897, 10.00252579], + [34.07699343, 2.90774401, -0.04753311] + ]) + mock_F_AB = np.array([ + [-5.14183388e-09, -3.73771847e-08, 1.56104641e-03], + [-2.74979764e-08, 9.38699691e-09, -4.33243885e-04], + [-1.56385384e-03, -7.71647099e-05, 1.00000000e+00] + ]) + + with patch('cv2.stereoCalibrate', return_value=(mock_retval, None, None, None, None, mock_R_AB, mock_T_AB, mock_E_AB, mock_F_AB)) as mock_stereo_calibrate: + retval, R_AB, T_AB, E_AB, F_AB = calib_stereo.calibrate_stereo() + + # Use np.allclose() to check that the values match the expected ones. + assert np.isclose(retval, mock_retval, atol=1e-6) + assert np.allclose(R_AB, mock_R_AB, atol=1e-6) + assert np.allclose(T_AB, mock_T_AB, atol=1e-6) + assert np.allclose(E_AB, mock_E_AB, atol=1e-6) + assert np.allclose(F_AB, mock_F_AB, atol=1e-6) + +def test_triangulation(setup_calibration_stereo, imgpoints_data): + """Test the triangulation function for consistency.""" + calib_stereo = setup_calibration_stereo + imgpointsA, imgpointsB = imgpoints_data + + # Use the OBJPOINTS as the expected points. + expected_points_3d = OBJPOINTS + + # Mock the triangulation result to match the expected object points. + homogeneous_coords = np.hstack([expected_points_3d, np.ones((expected_points_3d.shape[0], 1))]) + + with patch('cv2.triangulatePoints', return_value=homogeneous_coords.T) as mock_triangulate: + points_3d_hom = calib_stereo.triangulation( + calib_stereo.P_A, calib_stereo.P_B, imgpointsA[0], imgpointsB[0] + ) + + # Prevent division by zero by ensuring the last component isn't zero. + valid_indices = points_3d_hom[:, -1] != 0 + points_3d_hom = points_3d_hom[valid_indices] + points_3d_hom = points_3d_hom / points_3d_hom[:, -1].reshape(-1, 1) + + # Only compare valid points with the expected object points. + expected_valid_points = expected_points_3d[valid_indices] + + # Verify that the triangulation result is similar to the expected object points. + assert np.allclose(points_3d_hom[:, :3], expected_valid_points, atol=1e-2) \ No newline at end of file diff --git a/tests/test_coords_transformation.py b/tests/test_coords_transformation.py new file mode 100644 index 0000000..0930ae0 --- /dev/null +++ b/tests/test_coords_transformation.py @@ -0,0 +1,61 @@ +import numpy as np +import pytest +from parallax.coords_transformation import RotationTransformation + +@pytest.fixture +def transformer(): + return RotationTransformation() + +def test_roll(transformer): + # Test roll rotation around the x-axis + input_matrix = np.identity(3) + roll_angle = np.pi / 4 # 45 degrees + expected_output = np.array([[1, 0, 0], + [0, np.sqrt(2) / 2, -np.sqrt(2) / 2], + [0, np.sqrt(2) / 2, np.sqrt(2) / 2]]) + output = transformer.roll(input_matrix, roll_angle) + assert np.allclose(output, expected_output), "Roll transformation failed." + +def test_pitch(transformer): + # Test pitch rotation around the y-axis + input_matrix = np.identity(3) + pitch_angle = np.pi / 6 # 30 degrees + expected_output = np.array([[np.sqrt(3) / 2, 0, 0.5], + [0, 1, 0], + [-0.5, 0, np.sqrt(3) / 2]]) + output = transformer.pitch(input_matrix, pitch_angle) + assert np.allclose(output, expected_output), "Pitch transformation failed." + +def test_yaw(transformer): + # Test yaw rotation around the z-axis + input_matrix = np.identity(3) + yaw_angle = np.pi / 3 # 60 degrees + expected_output = np.array([[0.5, -np.sqrt(3) / 2, 0], + [np.sqrt(3) / 2, 0.5, 0], + [0, 0, 1]]) + output = transformer.yaw(input_matrix, yaw_angle) + assert np.allclose(output, expected_output), "Yaw transformation failed." + +def test_extract_angles(transformer): + # Test extraction of roll, pitch, yaw from rotation matrix + rotation_matrix = transformer.combineAngles(np.pi / 4, np.pi / 6, np.pi / 3) + roll, pitch, yaw = transformer.extractAngles(rotation_matrix) + + assert np.isclose(roll, np.pi / 4), f"Expected roll to be {np.pi / 4}, got {roll}" + assert np.isclose(pitch, np.pi / 6), f"Expected pitch to be {np.pi / 6}, got {pitch}" + assert np.isclose(yaw, np.pi / 3), f"Expected yaw to be {np.pi / 3}, got {yaw}" + +def test_fit_params(transformer): + # Test fitting parameters for transformation + measured_pts = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9], [10, 11, 12]]) + global_pts = np.array([[2, 3, 4], [5, 6, 7], [8, 9, 10], [11, 12, 13]]) + + origin, rotation_matrix, scale, avg_err = transformer.fit_params(measured_pts, global_pts) + + # Expected values based on the simplified test data + expected_origin = np.array([1, 1, 1]) + expected_scale = np.array([1, 1, 1]) + + assert np.allclose(origin, expected_origin), f"Expected origin {expected_origin}, got {origin}" + assert np.allclose(scale, expected_scale), f"Expected scale {expected_scale}, got {scale}" + assert avg_err < 1e-7, f"Expected avg error to be near 0, got {avg_err}" \ No newline at end of file diff --git a/tests/test_curr_bg_cmp_processor.py b/tests/test_curr_bg_cmp_processor.py new file mode 100644 index 0000000..08233e4 --- /dev/null +++ b/tests/test_curr_bg_cmp_processor.py @@ -0,0 +1,122 @@ +import pytest +import numpy as np +import cv2 +import os +from parallax.curr_bg_cmp_processor import CurrBgCmpProcessor +from parallax.mask_generator import MaskGenerator +from parallax.probe_detector import ProbeDetector + +# Define the folder containing your test images +IMG_SIZE = (1000, 750) +IMG_SIZE_ORIGINAL = (4000, 3000) + +# Helper function to load images from a folder +def load_images_from_folder(folder): + """Load and sort images from a specified folder.""" + images = [] + for filename in sorted(os.listdir(folder)): + img_path = os.path.join(folder, filename) + img = cv2.imread(img_path, cv2.IMREAD_GRAYSCALE) + if img is not None: + images.append(img) + return images + +@pytest.fixture(scope="session") +def sample_images(): + """Fixture to provide a way to process images into `curr_img`, `mask`.""" + def process_image(org_img, mask_generator): + """Resize, blur, and generate mask for a given original image.""" + resized_img = cv2.resize(org_img, IMG_SIZE) + curr_img = cv2.GaussianBlur(resized_img, (9, 9), 0) + mask = mask_generator.process(resized_img) + return curr_img, mask + + return process_image + +@pytest.fixture +def setup_curr_bg_cmp_processor(): + """Fixture to set up an instance of CurrBgCmpProcessor.""" + cam_name = "MockCam" + probeDetector = ProbeDetector(cam_name, (1000, 750)) + + processor = CurrBgCmpProcessor( + cam_name=cam_name, + ProbeDetector=probeDetector, + original_size=IMG_SIZE_ORIGINAL, + resized_size=IMG_SIZE, + reticle_zone=None, + ) + return processor + +# Tests +def test_first_cmp(setup_curr_bg_cmp_processor, sample_images): + """Test the first_cmp method with multiple images.""" + processor = setup_curr_bg_cmp_processor + + # Initialize the mask generator + mask_generator = MaskGenerator() # Replace with appropriate constructor + + # Load test images from the folder + base_dir = "tests/test_data/probe_detect_manager" + images = load_images_from_folder(base_dir) + + ret, precise_tip, tip = False, False, None + # Iterate over each frame and process it + for i, org_img in enumerate(images): + # Generate `curr_img` and `mask` for each frame using the `sample_images` fixture + curr_img, mask = sample_images(org_img, mask_generator) + + # Call the method to test for each frame + ret, precise_tip = processor.first_cmp(curr_img, mask, org_img) + + if precise_tip: + tip = processor.ProbeDetector.probe_tip + print(f"Frame {i}: Precise tip found: {precise_tip}, tip: {tip}") + break + + # Perform assertions + assert ret is not False, f"Return value of ret should not be None." + assert precise_tip is not False, f"Precise_tip should be detected." + assert isinstance(tip, tuple), "The tip should be a tuple." + assert len(tip) == 2, "The tip should contain two elements (x, y)." + + +def test_update_cmp(setup_curr_bg_cmp_processor, sample_images): + """Test the update_cmp method with multiple images.""" + processor = setup_curr_bg_cmp_processor + + # Initialize the mask generator + mask_generator = MaskGenerator() # Replace with appropriate constructor + + # Load test images from the folder + base_dir = "tests/test_data/probe_detect_manager" + images = load_images_from_folder(base_dir) + + is_first_detect = True + ret, precise_tip, tip = False, False, None + # Iterate over each frame and process it + for i, org_img in enumerate(images): + # Generate `curr_img` and `mask` for each frame using the `sample_images` fixture + curr_img, mask = sample_images(org_img, mask_generator) + + # Call the first_cmp method to set the initial state + if is_first_detect: + ret_, _ = processor.first_cmp(curr_img, mask, org_img) + if ret_: + is_first_detect = False + continue + + # Simulate the next frame (using the same or next image in the sequence) + ret, precise_tip = processor.update_cmp(curr_img, mask, org_img) + + print(f"Frame {i}: Precise tip found: {precise_tip}, tip: {tip}") + if precise_tip: + tip = processor.ProbeDetector.probe_tip + print(f"Frame {i}: Precise tip found: {precise_tip}, tip: {tip}") + break + + # Perform assertions + assert ret is not False, f"Return value of ret should not be None." + assert precise_tip is not False, f"Precise_tip should be detected." + assert isinstance(tip, tuple), "The tip should be a tuple." + assert len(tip) == 2, "The tip should contain two elements (x, y)." \ No newline at end of file diff --git a/tests/test_data/mask_generator/reticle/processed_image_debug_reticle1.png b/tests/test_data/mask_generator/reticle/processed_image_debug_reticle1.png new file mode 100644 index 0000000..b861475 Binary files /dev/null and b/tests/test_data/mask_generator/reticle/processed_image_debug_reticle1.png differ diff --git a/tests/test_data/mask_generator/reticle/processed_image_debug_reticle2.png b/tests/test_data/mask_generator/reticle/processed_image_debug_reticle2.png new file mode 100644 index 0000000..74d8027 Binary files /dev/null and b/tests/test_data/mask_generator/reticle/processed_image_debug_reticle2.png differ diff --git a/tests/test_data/mask_generator/reticle/reticle1.png b/tests/test_data/mask_generator/reticle/reticle1.png new file mode 100644 index 0000000..4589ec8 Binary files /dev/null and b/tests/test_data/mask_generator/reticle/reticle1.png differ diff --git a/tests/test_data/mask_generator/reticle/reticle2.png b/tests/test_data/mask_generator/reticle/reticle2.png new file mode 100644 index 0000000..a151b67 Binary files /dev/null and b/tests/test_data/mask_generator/reticle/reticle2.png differ diff --git a/tests/test_data/probe_detect_manager/cam0-02232024151540-0.jpg b/tests/test_data/probe_detect_manager/cam0-02232024151540-0.jpg new file mode 100644 index 0000000..e87afc2 Binary files /dev/null and b/tests/test_data/probe_detect_manager/cam0-02232024151540-0.jpg differ diff --git a/tests/test_data/probe_detect_manager/cam0-02232024151631-37.jpg b/tests/test_data/probe_detect_manager/cam0-02232024151631-37.jpg new file mode 100644 index 0000000..020ea2a Binary files /dev/null and b/tests/test_data/probe_detect_manager/cam0-02232024151631-37.jpg differ diff --git a/tests/test_data/probe_detect_manager/cam0-02232024151642-45.jpg b/tests/test_data/probe_detect_manager/cam0-02232024151642-45.jpg new file mode 100644 index 0000000..b91b4b4 Binary files /dev/null and b/tests/test_data/probe_detect_manager/cam0-02232024151642-45.jpg differ diff --git a/tests/test_data/probe_detect_manager/cam0-02232024151700-58.jpg b/tests/test_data/probe_detect_manager/cam0-02232024151700-58.jpg new file mode 100644 index 0000000..72392de Binary files /dev/null and b/tests/test_data/probe_detect_manager/cam0-02232024151700-58.jpg differ diff --git a/tests/test_data/probe_detect_manager/cam0-02232024151701-59.jpg b/tests/test_data/probe_detect_manager/cam0-02232024151701-59.jpg new file mode 100644 index 0000000..b63ce0b Binary files /dev/null and b/tests/test_data/probe_detect_manager/cam0-02232024151701-59.jpg differ diff --git a/tests/test_data/probe_detect_manager/cam0-02232024151702-60.jpg b/tests/test_data/probe_detect_manager/cam0-02232024151702-60.jpg new file mode 100644 index 0000000..ab589cf Binary files /dev/null and b/tests/test_data/probe_detect_manager/cam0-02232024151702-60.jpg differ diff --git a/tests/test_data/probe_detect_manager/cam0-02232024151704-61.jpg b/tests/test_data/probe_detect_manager/cam0-02232024151704-61.jpg new file mode 100644 index 0000000..8e17bc5 Binary files /dev/null and b/tests/test_data/probe_detect_manager/cam0-02232024151704-61.jpg differ diff --git a/tests/test_data/probe_detect_manager/cam0-02232024151705-62.jpg b/tests/test_data/probe_detect_manager/cam0-02232024151705-62.jpg new file mode 100644 index 0000000..34cdf62 Binary files /dev/null and b/tests/test_data/probe_detect_manager/cam0-02232024151705-62.jpg differ diff --git a/tests/test_data/probe_detect_manager/cam0-02232024151708-64.jpg b/tests/test_data/probe_detect_manager/cam0-02232024151708-64.jpg new file mode 100644 index 0000000..6160421 Binary files /dev/null and b/tests/test_data/probe_detect_manager/cam0-02232024151708-64.jpg differ diff --git a/tests/test_data/probe_detect_manager/cam0-02232024151709-65.jpg b/tests/test_data/probe_detect_manager/cam0-02232024151709-65.jpg new file mode 100644 index 0000000..563a8f1 Binary files /dev/null and b/tests/test_data/probe_detect_manager/cam0-02232024151709-65.jpg differ diff --git a/tests/test_data/probe_detect_manager/cam0-02232024151711-66.jpg b/tests/test_data/probe_detect_manager/cam0-02232024151711-66.jpg new file mode 100644 index 0000000..6854945 Binary files /dev/null and b/tests/test_data/probe_detect_manager/cam0-02232024151711-66.jpg differ diff --git a/tests/test_data/probe_detect_manager/cam0-02232024151712-67.jpg b/tests/test_data/probe_detect_manager/cam0-02232024151712-67.jpg new file mode 100644 index 0000000..0961703 Binary files /dev/null and b/tests/test_data/probe_detect_manager/cam0-02232024151712-67.jpg differ diff --git a/tests/test_data/probe_detect_manager/cam0-02232024151715-69.jpg b/tests/test_data/probe_detect_manager/cam0-02232024151715-69.jpg new file mode 100644 index 0000000..98993e0 Binary files /dev/null and b/tests/test_data/probe_detect_manager/cam0-02232024151715-69.jpg differ diff --git a/tests/test_data/probe_detect_manager/cam0-02232024151716-70.jpg b/tests/test_data/probe_detect_manager/cam0-02232024151716-70.jpg new file mode 100644 index 0000000..5a3677e Binary files /dev/null and b/tests/test_data/probe_detect_manager/cam0-02232024151716-70.jpg differ diff --git a/tests/test_data/probe_detect_manager/cam0-02232024151718-71.jpg b/tests/test_data/probe_detect_manager/cam0-02232024151718-71.jpg new file mode 100644 index 0000000..c16509c Binary files /dev/null and b/tests/test_data/probe_detect_manager/cam0-02232024151718-71.jpg differ diff --git a/tests/test_data/probe_detect_manager/cam0-02232024151726-77.jpg b/tests/test_data/probe_detect_manager/cam0-02232024151726-77.jpg new file mode 100644 index 0000000..a0aa778 Binary files /dev/null and b/tests/test_data/probe_detect_manager/cam0-02232024151726-77.jpg differ diff --git a/tests/test_data/probe_detect_manager/cam0-02232024151748-93.jpg b/tests/test_data/probe_detect_manager/cam0-02232024151748-93.jpg new file mode 100644 index 0000000..94b7391 Binary files /dev/null and b/tests/test_data/probe_detect_manager/cam0-02232024151748-93.jpg differ diff --git a/tests/test_data/probe_detect_manager/cam0-02232024151810-109.jpg b/tests/test_data/probe_detect_manager/cam0-02232024151810-109.jpg new file mode 100644 index 0000000..4c6fbc1 Binary files /dev/null and b/tests/test_data/probe_detect_manager/cam0-02232024151810-109.jpg differ diff --git a/tests/test_data/probe_detect_manager/cam0-02232024151821-117.jpg b/tests/test_data/probe_detect_manager/cam0-02232024151821-117.jpg new file mode 100644 index 0000000..5e0601a Binary files /dev/null and b/tests/test_data/probe_detect_manager/cam0-02232024151821-117.jpg differ diff --git a/tests/test_data/probe_detect_manager/cam0-02232024151827-122.jpg b/tests/test_data/probe_detect_manager/cam0-02232024151827-122.jpg new file mode 100644 index 0000000..dbf0a08 Binary files /dev/null and b/tests/test_data/probe_detect_manager/cam0-02232024151827-122.jpg differ diff --git a/tests/test_data/probe_detect_manager/cam0-02232024151853-141.jpg b/tests/test_data/probe_detect_manager/cam0-02232024151853-141.jpg new file mode 100644 index 0000000..52430ae Binary files /dev/null and b/tests/test_data/probe_detect_manager/cam0-02232024151853-141.jpg differ diff --git a/tests/test_data/probe_detect_manager/cam0-02232024151904-149.jpg b/tests/test_data/probe_detect_manager/cam0-02232024151904-149.jpg new file mode 100644 index 0000000..0356536 Binary files /dev/null and b/tests/test_data/probe_detect_manager/cam0-02232024151904-149.jpg differ diff --git a/tests/test_data/probe_detect_manager/cam0-02232024151937-173.jpg b/tests/test_data/probe_detect_manager/cam0-02232024151937-173.jpg new file mode 100644 index 0000000..a318ab1 Binary files /dev/null and b/tests/test_data/probe_detect_manager/cam0-02232024151937-173.jpg differ diff --git a/tests/test_data/probe_detect_manager/cam0-02232024152031-212.jpg b/tests/test_data/probe_detect_manager/cam0-02232024152031-212.jpg new file mode 100644 index 0000000..d9b1fb0 Binary files /dev/null and b/tests/test_data/probe_detect_manager/cam0-02232024152031-212.jpg differ diff --git a/tests/test_data/probe_detect_manager/cam0-02232024152131-256.jpg b/tests/test_data/probe_detect_manager/cam0-02232024152131-256.jpg new file mode 100644 index 0000000..860401f Binary files /dev/null and b/tests/test_data/probe_detect_manager/cam0-02232024152131-256.jpg differ diff --git a/tests/test_data/probe_detect_manager/cam0-02232024152206-282.jpg b/tests/test_data/probe_detect_manager/cam0-02232024152206-282.jpg new file mode 100644 index 0000000..54d55f7 Binary files /dev/null and b/tests/test_data/probe_detect_manager/cam0-02232024152206-282.jpg differ diff --git a/tests/test_data/probe_detect_manager/cam0-02232024152323-338.jpg b/tests/test_data/probe_detect_manager/cam0-02232024152323-338.jpg new file mode 100644 index 0000000..f2c039c Binary files /dev/null and b/tests/test_data/probe_detect_manager/cam0-02232024152323-338.jpg differ diff --git a/tests/test_data/probe_detect_manager/cam0-02232024152422-381.jpg b/tests/test_data/probe_detect_manager/cam0-02232024152422-381.jpg new file mode 100644 index 0000000..04c83ae Binary files /dev/null and b/tests/test_data/probe_detect_manager/cam0-02232024152422-381.jpg differ diff --git a/tests/test_data/probe_detect_manager/cam0-02232024152618-466.jpg b/tests/test_data/probe_detect_manager/cam0-02232024152618-466.jpg new file mode 100644 index 0000000..00620b3 Binary files /dev/null and b/tests/test_data/probe_detect_manager/cam0-02232024152618-466.jpg differ diff --git a/tests/test_data/probe_detect_manager/cam0-02232024152721-512.jpg b/tests/test_data/probe_detect_manager/cam0-02232024152721-512.jpg new file mode 100644 index 0000000..4cd6c3e Binary files /dev/null and b/tests/test_data/probe_detect_manager/cam0-02232024152721-512.jpg differ diff --git a/tests/test_mask_generator.py b/tests/test_mask_generator.py new file mode 100644 index 0000000..382864a --- /dev/null +++ b/tests/test_mask_generator.py @@ -0,0 +1,49 @@ +import pytest +import cv2 +import os +from parallax.mask_generator import MaskGenerator + +RETICLE_DIR = "tests/test_data/mask_generator/Reticle/" + +@pytest.mark.parametrize("image_path", [ + "tests/test_data/mask_generator/Reticle/reticle1.png", + "tests/test_data/mask_generator/Reticle/reticle2.png" +]) +def test_reticle_images(image_path): + """Test MaskGenerator on images with reticle.""" + # Load the image + image = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE) + assert image is not None, f"Failed to load image at {image_path}" + + # Create a MaskGenerator instance + mask_gen = MaskGenerator(initial_detect=True) + + # Process the test image + processed_image = mask_gen.process(image) + + # Debugging: Print if the image was processed or not + if processed_image is None: + print(f"Processing failed for {image_path}") + + # Assert that the processed image is not None + assert processed_image is not None, f"The processed image should not be None for {image_path}" + + # Save the processed image for debugging + save_path = os.path.join(RETICLE_DIR, f"processed_image_debug_{os.path.basename(image_path)}") + cv2.imwrite(save_path, processed_image) + print(f"Processed image saved at {save_path}") + + # Assert that the reticle exists + assert mask_gen.is_reticle_exist is True, f"Reticle should be detected in {image_path}" + + +def test_mask_generator_no_image(): + """Test the behavior of MaskGenerator when no image is provided.""" + # Create a MaskGenerator instance + mask_gen = MaskGenerator(initial_detect=True) + + # Process with None input + result = mask_gen.process(None) + + # Assert that the result is None + assert result is None, "The result should be None when processing a None image." diff --git a/tests/test_model.py b/tests/test_model.py new file mode 100644 index 0000000..551831c --- /dev/null +++ b/tests/test_model.py @@ -0,0 +1,100 @@ +# test_model.py +import pytest +from unittest.mock import patch, MagicMock +from parallax.model import Model +from parallax.camera import MockCamera, PySpinCamera +from parallax.stage_listener import Stage, StageInfo + +@pytest.fixture +def model(): + """Fixture to initialize the Model object.""" + return Model(version="V2", bundle_adjustment=False) + +def test_scan_for_cameras(model): + """Test scanning for cameras and updating the model's camera list.""" + # Create a mock PySpin camera object. + mock_camera_pyspin = MagicMock() + + # Correct the patch target to where `list_cameras` is used in `model.py` + with patch('parallax.model.list_cameras', return_value=[MockCamera(), PySpinCamera(mock_camera_pyspin)]) as mock_list_cameras: + # Call the method to scan for cameras. + model.scan_for_cameras() + + # Print the serial numbers for debugging. + print("Serial numbers of detected cameras: ", model.cameras_sn) + print("Number of Mock Cameras: ", model.nMockCameras) + print("Number of PySpin Cameras: ", model.nPySpinCameras) + + # Verify that both cameras are present in the model's list. + assert len(model.cameras) == 2, "The model should have 2 cameras." + assert isinstance(model.cameras[0], MockCamera), "The first camera should be a MockCamera." + assert isinstance(model.cameras[1], PySpinCamera), "The second camera should be a PySpinCamera." + + # Check the counts of each type of camera. + assert model.nMockCameras == 1, "There should be 1 MockCamera." + assert model.nPySpinCameras == 1, "There should be 1 PySpinCamera." + + # Verify that the camera serial numbers are correctly updated. + assert len(model.cameras_sn) == 2, "The model should have serial numbers for 2 cameras." + +def test_add_mock_cameras(model): + """Test adding mock cameras to the model.""" + # Add 3 mock cameras. + model.add_mock_cameras(n=2) + + # Verify that 3 mock cameras were added. + assert len(model.cameras) == 2 + for camera in model.cameras: + assert isinstance(camera, MockCamera) + +def test_scan_for_usb_stages(model): + """Test scanning for USB-connected stages and updating the model's stages.""" + # Mock the `StageInfo` class and its `get_instances` method. + mock_stage_info = MagicMock(spec=StageInfo) + mock_stage_info.get_instances.return_value = [{'sn': 'stage_1'}, {'sn': 'stage_2'}] + + with patch('parallax.stage_listener.StageInfo', return_value=mock_stage_info): + # Call the method to scan for USB stages. + model.scan_for_usb_stages() + + # Check if stages were initialized and added correctly. + assert len(model.stages) == 2 + assert model.nStages == 2 + + # Check if each stage is an instance of `Stage`. + for stage_sn, stage in model.stages.items(): + assert isinstance(stage, Stage) + assert stage.sn == stage_sn + +def test_add_stage(model): + """Test adding a stage to the model.""" + mock_stage = MagicMock(spec=Stage) + mock_stage.sn = 'stage_1' + + # Add a stage to the model. + model.add_stage(mock_stage) + + # Verify that the stage is added to the stages dictionary. + assert 'stage_1' in model.stages + assert model.stages['stage_1'] == mock_stage + +def test_add_stage_calib_info(model): + """Test adding calibration information for a specific stage.""" + stage_sn = 'stage_1' + calib_info = { + 'detection_status': True, + 'transM': [[1, 0, 0], [0, 1, 0], [0, 0, 1]], + 'L2_err': 0.01, + 'scale': [1, 1, 1], + 'dist_traveled': 100, + 'status_x': 'OK', + 'status_y': 'OK', + 'status_z': 'OK' + } + + # Add calibration info for the stage. + model.add_stage_calib_info(stage_sn, calib_info) + + # Verify that the calibration info is stored correctly. + assert stage_sn in model.stages_calib + assert model.stages_calib[stage_sn] == calib_info diff --git a/tests/test_probe_detect_manager.py b/tests/test_probe_detect_manager.py new file mode 100644 index 0000000..7ff41c4 --- /dev/null +++ b/tests/test_probe_detect_manager.py @@ -0,0 +1,77 @@ +import pytest +import cv2 +import os +import time +from PyQt5.QtCore import QCoreApplication, QEventLoop +from parallax.probe_detect_manager import ProbeDetectManager + +# Define the folder containing your test images +IMAGE_FOLDER = "tests/test_data/probe_detect_manager" +IMG_SIZE = (1000, 750) +IMG_SIZE_ORIGINAL = (4000, 3000) +processed_frames = False + +# Helper function to load images from a folder +def load_images_from_folder(folder): + images = [] + for filename in sorted(os.listdir(folder)): + img_path = os.path.join(folder, filename) + img = cv2.imread(img_path, cv2.IMREAD_GRAYSCALE) + if img is not None: + images.append(img) + return images + +@pytest.fixture() +def probe_detect_manager_instance(mocker): # Ensure qapp fixture is included + """Fixture to initialize ProbeDetectManager with a mock model and worker thread.""" + # Mock the model to avoid using the actual model implementation + mock_model = mocker.Mock() + + # Mock the return values for get_coords_axis and get_coords_for_debug + mock_model.get_coords_axis.return_value = [[(100, 200), (150, 250), (200, 300)]] + mock_model.get_coords_for_debug.return_value = [[(120, 220), (170, 270), (220, 320)]] + + mock_model.get_stage.return_value = mocker.Mock(stage_x=1000, stage_y=750, stage_z=500) + + # Initialize ProbeDetectManager + camera_name = "CameraA" + probe_detect_manager = ProbeDetectManager(mock_model, camera_name) + + # Call init_thread to initialize the worker and thread + probe_detect_manager.start() + + # Yield control back to the test + yield probe_detect_manager + + # Cleanup + print("Cleaning up ProbeDetectManager fixture...") + probe_detect_manager.stop() + +def test_found_coords(probe_detect_manager_instance): # Added 'qapp' fixture + """Test the probe detection pipeline using test images and check signals.""" + # Load test images from the folder + images = load_images_from_folder(IMAGE_FOLDER) + + found = False + global processed_frames + processed_frames = False + + probe_detect_manager_instance.start_detection("SN12345") + + # Test for coordinates found + for i, frame in enumerate(images): + print(f"Processing frame {i}") + probe_detect_manager_instance.process(frame, i) + + # Check if probeDetect exists and probe_tip_org is set + if probe_detect_manager_instance.worker.probeDetect and probe_detect_manager_instance.worker.probeDetect.probe_tip_org: + found = True # Set found to True when probe tip is detected + print("Detected coordinates:", probe_detect_manager_instance.worker.probeDetect.probe_tip_org) + break # Exit the loop when a valid detection is found + + time.sleep(0.3) # Pause before processing the next frame + + # Assert that at least one frame detected the probe tip + probe_detect_manager_instance.stop_detection("SN12345") + + assert found, "No probe tip was detected in the frames." \ No newline at end of file diff --git a/tests/test_reticle_detect_manager.py b/tests/test_reticle_detect_manager.py index dd4f9aa..bd3c552 100644 --- a/tests/test_reticle_detect_manager.py +++ b/tests/test_reticle_detect_manager.py @@ -149,5 +149,4 @@ def test_reticle_detect_manager_set_name(test_frame, qt_application): assert detect_manager.worker.name == new_camera_name, "Worker's camera name was not updated" # Stop the ReticleDetectManager - detect_manager.stop() - + detect_manager.stop() \ No newline at end of file diff --git a/tests/test_screen_coords_mapper.py b/tests/test_screen_coords_mapper.py new file mode 100644 index 0000000..ab77033 --- /dev/null +++ b/tests/test_screen_coords_mapper.py @@ -0,0 +1,123 @@ +import pytest +import numpy as np +from PyQt5.QtWidgets import QLineEdit, QComboBox, QWidget, QVBoxLayout +from parallax.screen_coords_mapper import ScreenCoordsMapper +from PyQt5.QtWidgets import QApplication + +@pytest.fixture(scope="session") +def qapp(): + app = QApplication([]) + yield app + app.quit() + +# Mocking the model with necessary methods +class MockModel: + def __init__(self): + self.bundle_adjustment = False + self.stereo_calib_instance = {} + self.best_camera_pair = ("CameraA", "CameraB") + self.reticle_metadata = { + "reticle1": { + "rot": 0, + "rotmat": np.eye(3), + "offset_x": 0, + "offset_y": 0, + "offset_z": 0 + } + } + # Mock detected points for cameras + self.detected_pts = { + "CameraA": (100, 200), + "CameraB": (150, 250) + } + + def add_pts(self, camera_name, pos): + self.detected_pts[camera_name] = pos + + def get_reticle_metadata(self, reticle_name): + return self.reticle_metadata.get(reticle_name, {}) + + def get_stereo_calib_instance(self, key): + return MockStereoInstance() + + def get_cameras_detected_pts(self): + return self.detected_pts + +class MockStereoInstance: + def get_global_coords(self, camA, tip_coordsA, camB, tip_coordsB): + # Mock global coordinates based on input + return np.array([[10.0, 20.0, 30.0]]) + +@pytest.fixture +def setup_screen_coords_mapper(qtbot): + # Create a parent QWidget to hold all the UI elements + parent_widget = QWidget() + layout = QVBoxLayout(parent_widget) + + # Mock UI elements + x = QLineEdit() + y = QLineEdit() + z = QLineEdit() + reticle_selector = QComboBox() + reticle_selector.addItem("Proj Global coords") + reticle_selector.addItem("reticle1 (Test)") + + # Add UI elements to the layout + layout.addWidget(x) + layout.addWidget(y) + layout.addWidget(z) + layout.addWidget(reticle_selector) + + # Initialize the mock model and ScreenCoordsMapper + mock_model = MockModel() + screen_widgets = [] # No actual screen widgets in this test + screen_coords_mapper = ScreenCoordsMapper(mock_model, screen_widgets, reticle_selector, x, y, z) + + # Add the parent widget to qtbot to ensure it stays alive during the test + qtbot.addWidget(parent_widget) + + # Return both the ScreenCoordsMapper instance and the parent_widget to keep everything alive + return screen_coords_mapper, x, y, z, parent_widget + +def test_clicked_position_without_reticle_adjustment(setup_screen_coords_mapper, qtbot): + screen_coords_mapper, x, y, z, _ = setup_screen_coords_mapper # Added _ to capture parent_widget + + # Mock camera name and click position + camera_name = "CameraA" + pos = (100, 200) + + # Simulate clicking on the screen without reticle adjustment + screen_coords_mapper._clicked_position(camera_name, pos) + + # Check if the global coordinates were set correctly + assert x.text() == "10000.0" + assert y.text() == "20000.0" + assert z.text() == "30000.0" + +def test_global_coords_calculation_stereo(setup_screen_coords_mapper): + screen_coords_mapper, _, _, _, _ = setup_screen_coords_mapper # Unpack the 5th value (parent_widget) + + # Mock camera name and click position + camera_name = "CameraA" + pos = (100, 200) + + # Call the method for calculating global coordinates using stereo calibration + global_coords = screen_coords_mapper._get_global_coords_stereo(camera_name, pos) + + # Check if the global coordinates were calculated correctly + assert np.allclose(global_coords, np.array([10.0, 20.0, 30.0])) + +def test_reticle_adjustment(setup_screen_coords_mapper): + screen_coords_mapper, _, _, _, _ = setup_screen_coords_mapper # Unpack the 5th value (parent_widget) + + # Mock global points + global_pts = np.array([10000, 20000, 30000]) + + # Simulate selecting a reticle in the UI + screen_coords_mapper.reticle = "reticle1" + + # Apply reticle adjustments + adjusted_coords = screen_coords_mapper._apply_reticle_adjustments(global_pts) + + # Check if the reticle adjustments were applied correctly + assert np.allclose(adjusted_coords, [10000, 20000, 30000]) diff --git a/tests/test_stage_ui.py b/tests/test_stage_ui.py new file mode 100644 index 0000000..331163e --- /dev/null +++ b/tests/test_stage_ui.py @@ -0,0 +1,98 @@ +import pytest +from PyQt5.QtWidgets import QComboBox, QLabel, QWidget, QVBoxLayout +from PyQt5.QtCore import QCoreApplication +from unittest.mock import Mock +from parallax.stage_ui import StageUI + +class MockModel: + """Mock model for testing the StageUI class.""" + def __init__(self): + # Stages will contain mock stage data with serial numbers and coordinates + self.stages = { + 'stage1': Mock(sn='SN12345', stage_x=100, stage_y=200, stage_z=300, stage_x_global=110, stage_y_global=210, stage_z_global=310), + 'stage2': Mock(sn='SN54321', stage_x=400, stage_y=500, stage_z=600, stage_x_global=410, stage_y_global=510, stage_z_global=610) + } + + def get_stage(self, stage_id): + return self.stages.get(stage_id) + +@pytest.fixture +def mock_ui(qtbot): + """Fixture to create a real UI with proper QWidget elements for testing.""" + # Create a real QWidget as the parent for UI components + ui = QWidget() + ui.setLayout(QVBoxLayout()) + + # Add real UI components + ui.stage_selector = QComboBox() + ui.reticle_selector = QComboBox() + ui.stage_sn = QLabel() + ui.local_coords_x = QLabel() + ui.local_coords_y = QLabel() + ui.local_coords_z = QLabel() + ui.global_coords_x = QLabel() + ui.global_coords_y = QLabel() + ui.global_coords_z = QLabel() + + # Add them to the layout + ui.layout().addWidget(ui.stage_selector) + ui.layout().addWidget(ui.reticle_selector) + ui.layout().addWidget(ui.stage_sn) + ui.layout().addWidget(ui.local_coords_x) + ui.layout().addWidget(ui.local_coords_y) + ui.layout().addWidget(ui.local_coords_z) + ui.layout().addWidget(ui.global_coords_x) + ui.layout().addWidget(ui.global_coords_y) + ui.layout().addWidget(ui.global_coords_z) + + qtbot.addWidget(ui) # Add the widget to qtbot for handling the event loop + return ui + +@pytest.fixture +def stage_ui(mock_ui, qtbot): + """Fixture to create a StageUI object for testing.""" + model = MockModel() + stage_ui = StageUI(model, mock_ui) + qtbot.addWidget(stage_ui) # Add the StageUI to qtbot for testing + return stage_ui + +def test_initialization(stage_ui, qtbot): + """Test that the StageUI initializes properly.""" + # Check if the stage selector has been initialized with the correct stage + qtbot.addWidget(stage_ui) + assert stage_ui.selected_stage.sn == 'SN12345', "Initial stage should be SN12345" + assert stage_ui.reticle == "Global coords" + assert stage_ui.previous_stage_id == stage_ui.get_current_stage_id() + +def test_update_stage_selector(stage_ui, mock_ui): + """Test that the stage selector is updated with available stages.""" + stage_ui.update_stage_selector() + assert mock_ui.stage_selector.count() == 2 + assert mock_ui.stage_selector.itemText(0) == "Probe stage1" + assert mock_ui.stage_selector.itemText(1) == "Probe stage2" + +def test_update_stage_sn(stage_ui, mock_ui): + """Test that selecting a stage updates the serial number in the UI.""" + mock_ui.stage_selector.setCurrentIndex(0) + stage_ui.updateStageSN() + assert mock_ui.stage_sn.text() == " SN12345" + + mock_ui.stage_selector.setCurrentIndex(1) + stage_ui.updateStageSN() + assert mock_ui.stage_sn.text() == " SN54321" + +def test_update_stage_local_coords(stage_ui, mock_ui): + """Test that selecting a stage updates the local coordinates in the UI.""" + mock_ui.stage_selector.setCurrentIndex(0) # Select the first stage + stage_ui.updateStageLocalCoords() + assert mock_ui.local_coords_x.text() == "100" + assert mock_ui.local_coords_y.text() == "200" + assert mock_ui.local_coords_z.text() == "300" + +def test_update_stage_global_coords(stage_ui, mock_ui): + """Test that selecting a stage updates the global coordinates in the UI.""" + mock_ui.stage_selector.setCurrentIndex(0) # Select the first stage + stage_ui.updateStageGlobalCoords() + assert mock_ui.global_coords_x.text() == "110" + assert mock_ui.global_coords_y.text() == "210" + assert mock_ui.global_coords_z.text() == "310" diff --git a/tests/test_user_setting_manager.py b/tests/test_user_setting_manager.py new file mode 100644 index 0000000..797ea7d --- /dev/null +++ b/tests/test_user_setting_manager.py @@ -0,0 +1,133 @@ +import pytest +import os +import json +from unittest import mock +from parallax.user_setting_manager import UserSettingsManager + + +@pytest.fixture +def settings_file(tmpdir): + """Fixture to create a temporary settings file.""" + settings_path = os.path.join(tmpdir, "settings.json") + with open(settings_path, "w") as f: + json.dump({}, f) # Initialize with an empty settings file + return settings_path + + +@pytest.fixture +def settings_manager(settings_file): + """Fixture to create a UserSettingsManager with a temporary settings file.""" + with mock.patch("parallax.user_setting_manager.os.path.join", return_value=settings_file): + return UserSettingsManager() + + +def test_load_empty_settings(settings_manager): + """Test loading an empty settings file.""" + settings = settings_manager.load_settings() + assert settings == {}, "Expected empty settings from an empty file." + + +def test_save_user_configs(settings_manager, tmpdir): + """Test saving user configurations to settings.json.""" + nColumn = 3 + directory = str(tmpdir) + width = 1920 + height = 1080 + + settings_manager.save_user_configs(nColumn, directory, width, height) + + with open(settings_manager.settings_file, "r") as f: + saved_settings = json.load(f) + + assert saved_settings["main"]["nColumn"] == nColumn + assert saved_settings["main"]["directory"] == directory + assert saved_settings["main"]["width"] == width + assert saved_settings["main"]["height"] == height + + +def test_load_mainWindow_settings(settings_manager, tmpdir): + """Test loading main window settings from settings.json.""" + # Save some test configurations + nColumn = 2 + directory = str(tmpdir) + width = 1280 + height = 720 + settings_manager.save_user_configs(nColumn, directory, width, height) + + # Reload the settings after saving (explicitly call load_settings) + settings_manager.settings = settings_manager.load_settings() + + # Load the saved settings + loaded_nColumn, loaded_directory, loaded_width, loaded_height = settings_manager.load_mainWindow_settings() + + # Assert and print loaded settings + print(f"Loaded settings: nColumn={loaded_nColumn}, directory={loaded_directory}, width={loaded_width}, height={loaded_height}") + + assert loaded_directory == directory + assert loaded_width == width + assert loaded_height == height + assert loaded_nColumn == nColumn + + +def test_load_settings_item(settings_manager, tmpdir): + """Test loading a specific setting item from a category.""" + nColumn = 4 + directory = str(tmpdir) + width = 1366 + height = 768 + settings_manager.save_user_configs(nColumn, directory, width, height) + + # Test loading the entire "main" category + main_settings = settings_manager.load_settings_item("main") + assert main_settings is not None + assert main_settings["nColumn"] == nColumn + assert main_settings["directory"] == directory + + # Test loading a specific item in the "main" category + nColumn_value = settings_manager.load_settings_item("main", "nColumn") + assert nColumn_value == nColumn + + # Test loading an item that doesn't exist + invalid_item = settings_manager.load_settings_item("main", "non_existent_item") + assert invalid_item is None + + +def test_update_user_configs_settingMenu(settings_manager, mocker, tmpdir): + """Test updating a setting for a microscope group.""" + # Mock the ScreenWidget and QGroupBox to simulate the UI elements + mock_screen = mocker.Mock() + mock_screen.get_camera_name.return_value = "SN12345" + + mock_group_box = mocker.Mock() + mock_group_box.findChild.return_value = mock_screen + + # Test updating the exposure setting for the camera "SN12345" + item = "exposure" + value = 500 + + settings_manager.update_user_configs_settingMenu(mock_group_box, item, value) + + with open(settings_manager.settings_file, "r") as f: + settings = json.load(f) + + assert "SN12345" in settings + assert settings["SN12345"][item] == value + + +def test_load_settings_nonexistent_file(settings_manager): + """Test behavior when the settings file doesn't exist.""" + # Patch os.path.exists to simulate non-existent settings file + with mock.patch("os.path.exists", return_value=False): + settings = settings_manager.load_settings() + assert settings == {}, "Expected an empty dictionary when settings file does not exist." + + +def test_load_mainWindow_settings_default(settings_manager): + """Test loading default main window settings when no settings are saved.""" + # Patch the settings file to simulate it being empty or non-existent + with mock.patch.object(settings_manager, 'settings', {}): + nColumn, directory, width, height = settings_manager.load_mainWindow_settings() + assert nColumn == 1 + assert directory == "" + assert width == 1400 + assert height == 1000