feat: init

This commit is contained in:
2025-12-04 16:07:30 +08:00
commit 262583a57f
681 changed files with 117578 additions and 0 deletions

View File

@@ -0,0 +1,617 @@
# Copyright (c) 2021-2025 The University of Texas Southwestern Medical Center.
# All rights reserved.
import tkinter
# Redistribution and use in source and binary forms, with or without
# modification, are permitted for academic and research use only
# (subject to the limitations in the disclaimer below)
# provided that the following conditions are met:
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of the copyright holders nor the names of its
# contributors may be used to endorse or promote products derived from this
# software without specific prior written permission.
# NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY
# THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
# CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
# PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
# IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
# Standard library imports
from unittest.mock import MagicMock
# Third party imports
import pytest
# Local imports
from navigate.controller.sub_controllers import AcquireBarController
from navigate.view.popups.acquire_popup import (
AcquirePopUp,
)
from navigate.model.data_sources import FILE_TYPES
class TestAcquireBarController:
"""Tests for the AcquireBarController class"""
@pytest.fixture(autouse=True)
def setup_class(self, dummy_controller):
"""Setup for the TestAcquireBarController class
Parameters
----------
dummy_controller : DummyController
Instance of the DummyController class
"""
c = dummy_controller
v = dummy_controller.view
self.acquire_bar_controller = AcquireBarController(
view=v.acquire_bar, parent_controller=c
)
self.acquire_bar_controller.populate_experiment_values()
c.channels_tab_controller.populate_experiment_values()
c.camera_setting_controller = MagicMock()
def test_init(self):
"""Tests the initialization of the AcquireBarController class
Raises
------
AssertionError
If the initialization of the AcquireBarController class is not correct
"""
assert isinstance(self.acquire_bar_controller, AcquireBarController)
def test_attr(self):
"""Tests the attributes of the AcquireBarController class
Raises
------
AssertionError
If the attributes of the AcquireBarController class are not correct
"""
# Listing off attributes to check existence
attrs = ["mode", "is_save", "mode_dict"]
for attr in attrs:
assert hasattr(self.acquire_bar_controller, attr)
@pytest.mark.parametrize(
"mode,mode_expected,value_expected",
[
("live", "indeterminate", None),
("single", "determinate", 0),
("customized", "indeterminate", None),
("z-stack", "determinate", 0),
],
)
def test_progress_bar(self, mode, mode_expected, value_expected):
"""Tests the progress bar of the AcquireBarController class
Parameters
----------
mode : str
Mode of the progress bar
mode_expected : str
Expected mode of the progress bar
value_expected : int
Expected value of the progress bar
Raises
------
AssertionError
If the progress bar of the AcquireBarController class is not correct
"""
# Startup progress bars
images_received = 0
mode = mode
stop = False
self.acquire_bar_controller.progress_bar(
images_received=images_received,
microscope_state=self.acquire_bar_controller.parent_controller.configuration[
"experiment"
][
"MicroscopeState"
],
mode=mode,
stop=stop,
)
progress_mode = str(self.acquire_bar_controller.view.CurAcq["mode"])
ovr_mode = str(self.acquire_bar_controller.view.OvrAcq["mode"])
assert progress_mode == mode_expected, (
f"Wrong progress bar mode ({progress_mode}) "
f"relative to microscope mode ({mode})"
)
assert ovr_mode == mode_expected, (
f"Wrong progress bar mode ({progress_mode}) "
f"relative to microscope mode ({mode})"
)
if value_expected is not None:
progress_start = int(self.acquire_bar_controller.view.CurAcq["value"])
ovr_start = int(self.acquire_bar_controller.view.OvrAcq["value"])
assert (
progress_start == value_expected
), "Wrong starting value for progress bar"
assert ovr_start == value_expected, "Wrong starting value for progress bar"
# Updating progress bar
images_received = 1
while images_received < 6:
self.acquire_bar_controller.progress_bar(
images_received=images_received,
microscope_state=self.acquire_bar_controller.parent_controller.configuration[
"experiment"
][
"MicroscopeState"
],
mode=mode,
stop=stop,
)
making_progress = float(self.acquire_bar_controller.view.CurAcq["value"])
ovr_progress = float(self.acquire_bar_controller.view.OvrAcq["value"])
assert (
making_progress > 0
), f"Progress bar should be moving in {mode} mode (making_progress)"
assert (
ovr_progress > 0
), f"Progress bar should be moving in {mode} mode (ovr_progress)"
images_received += 1
# Stopping progress bar
self.acquire_bar_controller.progress_bar(
images_received=images_received,
microscope_state=self.acquire_bar_controller.parent_controller.configuration[
"experiment"
][
"MicroscopeState"
],
mode=mode,
stop=True,
)
after_stop = float(self.acquire_bar_controller.view.CurAcq["value"])
after_ovr = float(self.acquire_bar_controller.view.OvrAcq["value"])
assert after_stop == 0, "Progress Bar did not stop"
assert after_ovr == 0, "Progress Bar did not stop"
@pytest.mark.parametrize("mode", ["live", "single", "z-stack", "customized"])
def test_get_set_mode(self, mode):
"""Tests the get_mode and set_mode methods of the AcquireBarController class
Parameters
----------
mode : str
Mode of the progress bar
Raises
------
AssertionError
If the get_mode and set_mode methods of the
AcquireBarController class are not correct
"""
self.acquire_bar_controller.set_mode(mode)
test = self.acquire_bar_controller.get_mode()
assert test == mode, "Mode not set correctly"
# assert imaging mode is updated in the experiment
assert (
self.acquire_bar_controller.parent_controller.configuration["experiment"][
"MicroscopeState"
]["image_mode"]
== mode
)
def test_set_save(self):
"""Tests the set_save method of the AcquireBarController class
Raises
------
AssertionError
If the set_save method of the AcquireBarController class is not correct
"""
# Assuming save state starts as False
self.acquire_bar_controller.set_save_option(True)
assert self.acquire_bar_controller.is_save is True, "Save option not correct"
# Return value to False
self.acquire_bar_controller.set_save_option(False)
assert (
self.acquire_bar_controller.is_save is False
), "Save option did not return to original value"
def test_stop_acquire(self):
"""Tests the stop_acquire method of the AcquireBarController class
Raises
------
AssertionError
If the stop_acquire method of the AcquireBarController class is not correct
"""
# Stopping acquisition
self.acquire_bar_controller.stop_acquire()
assert self.acquire_bar_controller.view.acquire_btn["text"] == "Acquire"
@pytest.mark.parametrize(
"user_mode,expected_mode",
[
("Continuous Scan", "live"),
("Z-Stack", "z-stack"),
("Single Acquisition", "single"),
("Customized", "customized"),
],
)
def test_update_microscope_mode(self, user_mode, expected_mode):
"""Tests the update_microscope_mode method of the AcquireBarController class
Parameters
----------
user_mode : str
Mode of the progress bar
expected_mode : str
Expected state of the progress bar
Raises
------
AssertionError
If the update_microscope_mode method of
the AcquireBarController class is not correct
"""
# Assuming mode starts on live
self.acquire_bar_controller.mode = "live"
# Setting to mode specified by user
self.acquire_bar_controller.view.pull_down.set(user_mode)
# Generate event that calls update microscope mode
self.acquire_bar_controller.view.pull_down.event_generate(
"<<ComboboxSelected>>"
)
# Checking that new mode gets set by function
assert self.acquire_bar_controller.mode == expected_mode
assert (
self.acquire_bar_controller.parent_controller.configuration["experiment"][
"MicroscopeState"
]["image_mode"]
== expected_mode
)
# Resetting to live
self.acquire_bar_controller.view.pull_down.set("Continuous Scan")
self.acquire_bar_controller.view.pull_down.event_generate(
"<<ComboboxSelected>>"
)
assert self.acquire_bar_controller.mode == "live"
def test_populate_experiment_values(self):
"""Tests the populate_experiment_values method of the AcquireBarController class
Raises
------
AssertionError
If the populate_experiment_values method
of the AcquireBarController class is not correct
"""
# Calling function to populate values
self.acquire_bar_controller.populate_experiment_values()
# Checking values are what we expect
for key, value in self.acquire_bar_controller.saving_settings.items():
assert (
self.acquire_bar_controller.saving_settings[key]
== self.acquire_bar_controller.parent_controller.configuration[
"experiment"
]["Saving"][key]
)
# Assuming default value in exp file,
# can be altered TODO maybe set default to current date
assert self.acquire_bar_controller.saving_settings["date"] == "2022-06-07"
assert (
self.acquire_bar_controller.mode
== self.acquire_bar_controller.parent_controller.configuration[
"experiment"
]["MicroscopeState"]["image_mode"]
)
@pytest.mark.parametrize(
"text,is_acquiring, save,mode,file_types,choice",
[
("Stop", False, None, "live", [], None),
("Stop", True, None, "live", [], None),
("Acquire", True, True, "live", [], None),
("Acquire", False, True, "live", [], None),
("Acquire", False, False, "z-stack", [], None),
("Acquire", False, True, "z-stack", FILE_TYPES, "Done"),
("Acquire", False, True, "z-stack", FILE_TYPES, "Cancel"),
],
)
def test_launch_popup_window(
self, text, is_acquiring, save, mode, file_types, choice
):
"""Tests the launch_popup_window method of the AcquireBarController class
This is the largest test for this controller.
It will test multiple functions that are all used together
and difficult to isolate.
Funcs Tested:
launch_popup_window
update_file_type
launch_acquisition
update_experiment_values
acquire_pop.popup.dismiss # This will be double tested in view
Parameters
----------
text : str
Text of the button that is clicked
save : bool
Whether or not to save the image
mode : str
Mode of the progress bar
file_types : list
List of file types to save as
choice : str
Choice of the user in the popup window
Raises
------
AssertionError
If the launch_popup_window method of the
AcquireBarController class is not correct
"""
# Setup Gui for test
self.acquire_bar_controller.view.acquire_btn.configure(state="normal")
self.acquire_bar_controller.view.acquire_btn.configure(text=text)
self.acquire_bar_controller.is_save = save
self.acquire_bar_controller.set_mode(mode)
self.acquire_bar_controller.is_acquiring = is_acquiring
# Test based on setup, launches popup
self.acquire_bar_controller.view.acquire_btn.invoke()
# Checking things are what we expect
if text == "Stop":
assert self.acquire_bar_controller.view.acquire_btn["text"] == "Stop"
if is_acquiring:
assert (
str(self.acquire_bar_controller.view.acquire_btn["state"])
== "disabled"
)
res = self.acquire_bar_controller.parent_controller.pop()
assert res == "stop_acquire"
else:
assert (
str(self.acquire_bar_controller.view.acquire_btn["state"])
== "normal"
)
res = self.acquire_bar_controller.parent_controller.pop()
assert res == "Empty command list"
if text == "Acquire":
if is_acquiring:
assert (
str(self.acquire_bar_controller.view.acquire_btn["state"])
== "normal"
)
res = self.acquire_bar_controller.parent_controller.pop()
assert res == "Empty command list"
return
# First scenario Save is on and in live mode
if save is True and mode == "live":
assert self.acquire_bar_controller.view.acquire_btn["text"] == "Acquire"
assert (
str(self.acquire_bar_controller.view.acquire_btn["state"])
== "disabled"
)
res = self.acquire_bar_controller.parent_controller.pop()
print(res)
print(self.acquire_bar_controller.parent_controller.pop())
assert res == "acquire"
# Second scenario Save is off and mode is not live
if save is False and mode != "live":
assert self.acquire_bar_controller.view.acquire_btn["text"] == "Acquire"
assert (
str(self.acquire_bar_controller.view.acquire_btn["state"])
== "disabled"
)
res = self.acquire_bar_controller.parent_controller.pop()
assert res == "acquire"
# Third and final scenario Save is on and mode is not live
if save is True and mode != "live":
# Checking if popup created
assert isinstance(self.acquire_bar_controller.acquire_pop, AcquirePopUp)
assert self.acquire_bar_controller.acquire_pop.popup.winfo_exists() == 1
# Testing update_file_type if list exists
widgets = self.acquire_bar_controller.acquire_pop.get_widgets()
if len(file_types) > 0:
for file in file_types:
widgets["file_type"].set(file)
assert (
self.acquire_bar_controller.saving_settings["file_type"]
== file
)
# Resetting file type back to orginal
widgets["file_type"].set("TIFF")
assert (
self.acquire_bar_controller.saving_settings["file_type"]
== "TIFF"
)
# Check that loop thru saving settings is correct
for k, v in self.acquire_bar_controller.saving_settings.items():
if widgets.get(k, None):
value = widgets[k].get().strip()
assert value == v
# Grabbing buttons to test
buttons = self.acquire_bar_controller.acquire_pop.get_buttons()
if choice == "Cancel":
# Testing cancel button
buttons["Cancel"].invoke() # Call to dismiss popup
# Check toplevel gone
assert (
self.acquire_bar_controller.acquire_pop.popup.winfo_exists()
== 0
)
assert (
str(self.acquire_bar_controller.view.acquire_btn["state"])
== "normal"
)
elif choice == "Done":
# Testing done button
# Update experiment values test
# Changing popup vals to test update
# experiment values inside launch acquisition
widgets["user"].set("John")
widgets["tissue"].set("Heart")
widgets["celltype"].set("34T")
widgets["label"].set("BCB")
widgets["solvent"].set("uDISCO")
widgets["file_type"].set("OME-TIFF")
# Tab frame
for i in range(100):
self.acquire_bar_controller.acquire_pop.tab_frame.inputs[
"misc"
].insert(tkinter.END, f"L{i}")
self.acquire_bar_controller.acquire_pop.tab_frame.inputs[
"shear_data"
].set(True)
self.acquire_bar_controller.acquire_pop.tab_frame.inputs[
"shear_dimension"
].set("XZ")
self.acquire_bar_controller.acquire_pop.tab_frame.inputs[
"shear_angle"
].set(45)
self.acquire_bar_controller.acquire_pop.tab_frame.inputs[
"rotate_data"
].set(True)
self.acquire_bar_controller.acquire_pop.tab_frame.inputs[
"rotate_angle_x"
].set(90)
self.acquire_bar_controller.acquire_pop.tab_frame.inputs[
"rotate_angle_y"
].set(90)
self.acquire_bar_controller.acquire_pop.tab_frame.inputs[
"rotate_angle_z"
].set(90)
self.acquire_bar_controller.acquire_pop.tab_frame.inputs[
"down_sample_data"
].set(True)
self.acquire_bar_controller.acquire_pop.tab_frame.inputs[
"lateral_down_sample"
].set("2x")
self.acquire_bar_controller.acquire_pop.tab_frame.inputs[
"axial_down_sample"
].set("2x")
# Launch acquisition start/test
buttons["Done"].invoke() # Call to launch acquisition
# Check if update experiment values works correctly
pop_vals = self.acquire_bar_controller.acquire_pop.get_variables()
for k, v in self.acquire_bar_controller.saving_settings.items():
if pop_vals.get(k, None):
value = pop_vals[k].strip()
assert value == v
# Check command sent to controller
# and if acquire button changed to Stop
res = self.acquire_bar_controller.parent_controller.pop()
assert res == "acquire_and_save"
assert (
str(self.acquire_bar_controller.view.acquire_btn["state"])
== "disabled"
)
assert (
self.acquire_bar_controller.acquire_pop.popup.winfo_exists()
== 0
)
def test_frequent_start_and_stop_acquisition(self):
# set up
self.acquire_bar_controller.view.acquire_btn.configure(state="normal")
self.acquire_bar_controller.view.acquire_btn.configure(text="Acquire")
self.acquire_bar_controller.is_save = False
self.acquire_bar_controller.set_mode("live")
self.acquire_bar_controller.is_acquiring = False
# start acquisition
self.acquire_bar_controller.view.acquire_btn.invoke()
assert self.acquire_bar_controller.view.acquire_btn["text"] == "Acquire"
assert str(self.acquire_bar_controller.view.acquire_btn["state"]) == "disabled"
assert self.acquire_bar_controller.is_acquiring is True
# assert dummy_controller_to_test_acquire_bar.acquisition_count == 1
res = self.acquire_bar_controller.parent_controller.pop()
assert res == "acquire"
# not in acquisition, click the "Acquire" button several times
self.acquire_bar_controller.view.acquire_btn.invoke()
assert self.acquire_bar_controller.view.acquire_btn["text"] == "Acquire"
assert str(self.acquire_bar_controller.view.acquire_btn["state"]) == "disabled"
res = self.acquire_bar_controller.parent_controller.pop()
assert res == "Empty command list"
self.acquire_bar_controller.view.acquire_btn.invoke()
res = self.acquire_bar_controller.parent_controller.pop()
assert res == "Empty command list"
# in acquisition, click "Stop" button several times
self.acquire_bar_controller.view.acquire_btn.configure(state="normal")
self.acquire_bar_controller.view.acquire_btn.configure(text="Stop")
self.acquire_bar_controller.is_acquiring = True
self.acquire_bar_controller.view.acquire_btn.invoke()
assert self.acquire_bar_controller.view.acquire_btn["text"] == "Stop"
assert str(self.acquire_bar_controller.view.acquire_btn["state"]) == "disabled"
res = self.acquire_bar_controller.parent_controller.pop()
assert res == "stop_acquire"
self.acquire_bar_controller.view.acquire_btn.invoke()
res = self.acquire_bar_controller.parent_controller.pop()
assert res == "Empty command list"
self.acquire_bar_controller.view.acquire_btn.invoke()
res = self.acquire_bar_controller.parent_controller.pop()
assert res == "Empty command list"
self.acquire_bar_controller.view.acquire_btn.invoke()
res = self.acquire_bar_controller.parent_controller.pop()
assert res == "Empty command list"

View File

@@ -0,0 +1,202 @@
# Copyright (c) 2021-2025 The University of Texas Southwestern Medical Center.
# All rights reserved.
# Redistribution and use in source and binary forms, with or without
# modification, are permitted for academic and research use only
# (subject to the limitations in the disclaimer below)
# provided that the following conditions are met:
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of the copyright holders nor the names of its
# contributors may be used to endorse or promote products derived from this
# software without specific prior written permission.
# NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY
# THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
# CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
# PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
# IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
# Standard library imports
# Third party imports
import pytest
import numpy as np
# Local application imports
from navigate.controller.sub_controllers import AutofocusPopupController
from navigate.view.popups.autofocus_setting_popup import AutofocusPopup
class TestAutofocusPopupController:
"""Class for testing autofocus popup controller
Methods
-------
test_init()
Tests that the controller is initialized correctly
test_attr()
Tests that the attributes are initialized correctly
test_populate_experiment_values()
Tests that the values are populated correctly
test_update_experiment_values()
Tests that the values are updated correctly
test_start_autofocus()
Tests that the start autofocus function works correctly
test_display_plot()
Tests that the display plot function works correctly
"""
@pytest.fixture(autouse=True)
def setup_class(self, dummy_controller):
"""Setup for testing autofocus popup controller
Parameters
----------
dummy_controller : DummyController
Dummy controller for testing
"""
autofocus_popup = AutofocusPopup(dummy_controller.view)
self.autofocus_controller = AutofocusPopupController(
autofocus_popup, dummy_controller
)
def test_init(self):
"""Tests that the controller is initialized correctly
Raises
------
AssertionError
If the controller is not initialized correctly
"""
assert isinstance(self.autofocus_controller, AutofocusPopupController)
assert self.autofocus_controller.view.popup.winfo_exists() == 1
def test_attr(self):
"""Tests that the attributes are initialized correctly
Raises
------
AssertionError
If the attributes are not initialized correctly
"""
# Listing off attributes to check existence
attrs = [
"autofocus_fig",
"autofocus_coarse",
"widgets",
"setting_dict",
]
for attr in attrs:
assert hasattr(self.autofocus_controller, attr)
def test_populate_experiment_values(self):
"""Tests that the values are populated correctly
Raises
------
AssertionError
If the values are not populated correctly
"""
microscope_name = self.autofocus_controller.microscope_name
device = self.autofocus_controller.widgets["device"].get()
device_ref = self.autofocus_controller.widgets["device_ref"].get()
for k in self.autofocus_controller.widgets:
if k != "device" and k != "device_ref":
assert self.autofocus_controller.widgets[k].get() == str(
self.autofocus_controller.setting_dict[microscope_name][device][
device_ref
][k]
)
# Some values are ints but Tkinter only uses strings
def test_update_experiment_values(self):
"""Tests that the values are updated correctly
Raises
------
AssertionError
If the values are not updated correctly
"""
# Changing values
self.autofocus_controller.widgets["coarse_range"].set(200)
self.autofocus_controller.widgets["coarse_step_size"].set(30)
self.autofocus_controller.view.setting_vars["coarse_selected"].set(False)
self.autofocus_controller.widgets["fine_range"].set(25)
self.autofocus_controller.widgets["fine_step_size"].set(2)
self.autofocus_controller.view.setting_vars["fine_selected"].set(False)
microscope_name = self.autofocus_controller.microscope_name
device = self.autofocus_controller.widgets["device"].get()
device_ref = self.autofocus_controller.widgets["device_ref"].get()
# Checking values match
for k in self.autofocus_controller.widgets:
if k != "device" and k != "device_ref":
assert self.autofocus_controller.widgets[k].get() == str(
self.autofocus_controller.setting_dict[microscope_name][device][
device_ref
][k]
)
for k in self.autofocus_controller.view.setting_vars:
assert (
self.autofocus_controller.view.setting_vars[k].get()
== self.autofocus_controller.setting_dict[microscope_name][device][
device_ref
][k]
)
def test_start_autofocus(self):
"""Tests that the start autofocus function works correctly
Raises
------
AssertionError
If the start autofocus function does not work correctly
"""
# Calling function
self.autofocus_controller.start_autofocus()
# Checking message sent
res = self.autofocus_controller.parent_controller.pop()
assert res == "autofocus"
self.autofocus_controller.parent_controller.clear()
def test_display_plot(self):
"""Tests that the display plot function works correctly
Todo: Retrieve data from axessubplot instance and
check that it is correct
Raises
------
AssertionError
If the display plot function does not work correctly
"""
# Make this robust by sending data and then
# checking each plot is plotting correct data low priority
x_data = np.linspace(start=69750.0, stop=70250.0, num=101)
y_data = np.random.rand(101)
data = [x_data, y_data]
self.autofocus_controller.display_plot([data, False, True])
pass

View File

@@ -0,0 +1,607 @@
# Copyright (c) 2021-2025 The University of Texas Southwestern Medical Center.
# All rights reserved.
# Redistribution and use in source and binary forms, with or without
# modification, are permitted for academic and research use only
# (subject to the limitations in the disclaimer below)
# provided that the following conditions are met:
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of the copyright holders nor the names of its
# contributors may be used to endorse or promote products derived from this
# software without specific prior written permission.
# NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY
# THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
# CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
# PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
# IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
# Standard Imports
# Third Party Imports
import pytest
import random
# Local Imports
from navigate.controller.sub_controllers.camera_settings import (
CameraSettingController,
)
class TestCameraSettingController:
@pytest.fixture(autouse=True)
def setup_class(self, dummy_controller):
c = dummy_controller
v = dummy_controller.view
self.camera_settings = CameraSettingController(
v.settings.camera_settings_tab, c
)
def test_init(self):
assert isinstance(self.camera_settings, CameraSettingController)
# Setup, going to check what the default values widgets are set too
microscope_name = self.camera_settings.parent_controller.configuration[
"experiment"
]["MicroscopeState"]["microscope_name"]
camera_config_dict = self.camera_settings.parent_controller.configuration[
"configuration"
]["microscopes"][microscope_name]["camera"]
# Default Values
assert (
self.camera_settings.default_pixel_size
== camera_config_dict["pixel_size_in_microns"]
)
assert self.camera_settings.default_height == camera_config_dict["y_pixels"]
assert self.camera_settings.default_width == camera_config_dict["x_pixels"]
# Camera Mode
assert list(self.camera_settings.mode_widgets["Sensor"].widget["values"]) == [
"Normal",
"Light-Sheet",
]
assert (
str(self.camera_settings.mode_widgets["Sensor"].widget["state"])
== "readonly"
)
# Readout Mode
assert list(self.camera_settings.mode_widgets["Readout"].widget["values"]) == [
"Top-to-Bottom",
"Bottom-to-Top",
"Bidirectional",
"Rev. Bidirectional",
]
assert (
str(self.camera_settings.mode_widgets["Readout"].widget["state"])
== "disabled"
)
# Pixels
assert (
str(self.camera_settings.mode_widgets["Pixels"].widget["state"])
== "disabled"
)
assert self.camera_settings.mode_widgets["Pixels"].widget.get() == ""
assert self.camera_settings.mode_widgets["Pixels"].widget.cget("from") == 1
assert (
self.camera_settings.mode_widgets["Pixels"].widget.cget("to")
== self.camera_settings.default_height / 2
)
assert self.camera_settings.mode_widgets["Pixels"].widget.cget("increment") == 1
# Framerate
assert (
str(self.camera_settings.framerate_widgets["exposure_time"].widget["state"])
== "disabled"
)
assert (
str(self.camera_settings.framerate_widgets["readout_time"].widget["state"])
== "disabled"
)
assert (
str(self.camera_settings.framerate_widgets["max_framerate"].widget["state"])
== "disabled"
)
# Set range value
assert (
self.camera_settings.roi_widgets["Width"].widget.cget("to")
== self.camera_settings.default_width
)
assert self.camera_settings.roi_widgets["Width"].widget.cget("from") == 2
assert self.camera_settings.roi_widgets["Width"].widget.cget("increment") == 2
assert (
self.camera_settings.roi_widgets["Height"].widget.cget("to")
== self.camera_settings.default_height
)
assert self.camera_settings.roi_widgets["Height"].widget.cget("from") == 2
assert self.camera_settings.roi_widgets["Height"].widget.cget("increment") == 2
# Set binning options
assert list(self.camera_settings.roi_widgets["Binning"].widget["values"]) == [
"{}x{}".format(i, i) for i in [1, 2, 4]
]
assert (
str(self.camera_settings.roi_widgets["Binning"].widget["state"])
== "readonly"
)
# FOV
assert (
str(self.camera_settings.roi_widgets["FOV_X"].widget["state"]) == "disabled"
)
assert (
str(self.camera_settings.roi_widgets["FOV_Y"].widget["state"]) == "disabled"
)
def test_attr(self):
attrs = [
"in_initialization",
"resolution_value",
"mode",
"solvent",
"mode_widgets",
"framerate_widgets",
"roi_widgets",
"roi_btns",
"default_pixel_size",
"default_width",
"default_height",
"pixel_event_id",
]
for attr in attrs:
assert hasattr(self.camera_settings, attr)
def test_populate_experiment_values(self):
microscope_name = self.camera_settings.parent_controller.configuration[
"experiment"
]["MicroscopeState"]["microscope_name"]
self.camera_settings.parent_controller.configuration["experiment"][
"CameraParameters"
][microscope_name]["readout_time"] = 0.1
# Populate widgets with values from experiment file and check
self.camera_settings.populate_experiment_values()
camera_setting_dict = self.camera_settings.parent_controller.configuration[
"experiment"
]["CameraParameters"][microscope_name]
# Checking values altered are correct
assert dict(self.camera_settings.camera_setting_dict) == dict(
self.camera_settings.parent_controller.configuration["experiment"][
"CameraParameters"
][microscope_name]
)
assert (
str(self.camera_settings.mode_widgets["Sensor"].get())
== camera_setting_dict["sensor_mode"]
)
if camera_setting_dict["sensor_mode"] == "Normal":
pass
# assert str(self.camera_settings.mode_widgets[
# "Readout"].get()) == ""
# assert str(self.camera_settings.mode_widgets[
# "Pixels"].get()) == ""
elif camera_setting_dict["sensor_mode"] == "Light-Sheet":
assert (
str(self.camera_settings.mode_widgets["Readout"].get())
== self.camera_settings.camera_setting_dict["readout_direction"]
)
assert (
str(self.camera_settings.mode_widgets["Pixels"].get())
== self.camera_settings.camera_setting_dict["number_of_pixels"]
)
# ROI
assert (
self.camera_settings.roi_widgets["Width"].get()
== camera_setting_dict["x_pixels"]
)
assert (
self.camera_settings.roi_widgets["Height"].get()
== camera_setting_dict["y_pixels"]
)
assert self.camera_settings.roi_widgets[
"Top_X"
].get() == camera_setting_dict.get("top_x", 0)
assert self.camera_settings.roi_widgets[
"Top_Y"
].get() == camera_setting_dict.get("top_y", 0)
if camera_setting_dict.get("is_centered", True):
assert (
str(self.camera_settings.roi_widgets["Top_X"].widget["state"])
== "disabled"
)
assert (
str(self.camera_settings.roi_widgets["Top_Y"].widget["state"])
== "disabled"
)
# Binning
assert (
str(self.camera_settings.roi_widgets["Binning"].get())
== camera_setting_dict["binning"]
)
# Exposure Time
channels = self.camera_settings.parent_controller.configuration["experiment"][
"MicroscopeState"
]["channels"]
exposure_time = channels[list(channels.keys())[0]]["camera_exposure_time"]
assert (
self.camera_settings.framerate_widgets["exposure_time"].get()
== exposure_time
)
assert (
self.camera_settings.framerate_widgets["frames_to_average"].get()
== camera_setting_dict["frames_to_average"]
)
assert self.camera_settings.in_initialization is False
@pytest.mark.parametrize("mode", ["Normal", "Light-Sheet"])
def test_update_experiment_values(self, mode):
microscope_name = self.camera_settings.parent_controller.configuration[
"experiment"
]["MicroscopeState"]["microscope_name"]
# Setup basic default experiment
self.camera_settings.camera_setting_dict = (
self.camera_settings.parent_controller.configuration["experiment"][
"CameraParameters"
][microscope_name]
)
# Setting up new values in widgets
self.camera_settings.mode_widgets["Sensor"].set(mode)
self.camera_settings.roi_widgets["Binning"].set("4x4")
if mode == "Light-Sheet":
self.camera_settings.mode_widgets["Readout"].set("Bottom-to-Top")
self.camera_settings.mode_widgets["Pixels"].set(15)
self.camera_settings.roi_widgets["Binning"].set("1x1")
width, height = random.randint(1, 2000), random.randint(1, 2000)
self.camera_settings.roi_widgets["Width"].set(width)
self.camera_settings.roi_widgets["Height"].set(height)
self.camera_settings.framerate_widgets["frames_to_average"].set(5)
# Update experiment dict and assert
self.camera_settings.update_experiment_values()
assert self.camera_settings.camera_setting_dict["sensor_mode"] == mode
if mode == "Light-Sheet":
assert (
self.camera_settings.camera_setting_dict["readout_direction"]
== "Bottom-to-Top"
)
assert (
int(self.camera_settings.camera_setting_dict["number_of_pixels"]) == 15
)
step_width = self.camera_settings.step_width
step_height = self.camera_settings.step_height
set_width = int(width // step_width) * step_width
set_height = int(height // step_height) * step_height
if mode == "Light-Sheet":
assert self.camera_settings.camera_setting_dict["binning"] == "1x1"
assert self.camera_settings.camera_setting_dict["img_x_pixels"] == set_width
assert (
self.camera_settings.camera_setting_dict["img_y_pixels"] == set_height
)
binning = 1
else:
assert self.camera_settings.camera_setting_dict["binning"] == "4x4"
# make sure image size is divisible by step_width and step_height
assert self.camera_settings.camera_setting_dict["img_x_pixels"] == (
set_width // 4
) - (set_width // 4 % step_width)
assert self.camera_settings.camera_setting_dict["img_y_pixels"] == (
set_height // 4
) - (set_height // 4 % step_height)
binning = 4
# make sure x, y pixels are img_x, img_y pixels * binning
assert (
self.camera_settings.camera_setting_dict["x_pixels"]
== self.camera_settings.camera_setting_dict["img_x_pixels"] * binning
)
assert (
self.camera_settings.camera_setting_dict["y_pixels"]
== self.camera_settings.camera_setting_dict["img_y_pixels"] * binning
)
assert (
self.camera_settings.camera_setting_dict["pixel_size"]
== self.camera_settings.default_pixel_size
)
assert self.camera_settings.camera_setting_dict["frames_to_average"] == 5
@pytest.mark.parametrize("mode", ["Normal", "Light-Sheet"])
def test_update_sensor_mode(self, mode):
self.camera_settings.populate_experiment_values()
microscope_name = self.camera_settings.parent_controller.configuration[
"experiment"
]["MicroscopeState"]["microscope_name"]
camera_setting_dict = self.camera_settings.parent_controller.configuration[
"experiment"
]["CameraParameters"][microscope_name]
# Set mode
self.camera_settings.mode_widgets["Sensor"].widget.set(mode)
self.camera_settings.mode_widgets["Sensor"].widget.event_generate(
"<<ComboboxSelected>>"
)
# Call update
# self.camera_settings.update_sensor_mode()
# Check values
if mode == "Normal":
assert str(self.camera_settings.mode_widgets["Readout"].get()) == " "
assert (
str(self.camera_settings.mode_widgets["Readout"].widget["state"])
== "disabled"
)
assert (
str(self.camera_settings.mode_widgets["Pixels"].widget["state"])
== "disabled"
)
assert str(self.camera_settings.mode_widgets["Pixels"].widget.get()) == ""
if mode == "Light-Sheet":
assert (
str(self.camera_settings.mode_widgets["Readout"].get())
== camera_setting_dict["readout_direction"]
)
assert (
str(self.camera_settings.mode_widgets["Readout"].widget["state"])
== "readonly"
)
assert (
str(self.camera_settings.mode_widgets["Pixels"].widget["state"])
== "normal"
)
assert int(self.camera_settings.mode_widgets["Pixels"].widget.get()) == int(
self.camera_settings.camera_setting_dict["number_of_pixels"]
)
def test_update_exposure_time(self):
# Call funciton
self.camera_settings.update_exposure_time(35)
# Check
assert self.camera_settings.framerate_widgets["exposure_time"].get() == 35
@pytest.mark.parametrize("name", ["All", "1600", "1024", "512"])
def test_update_roi(self, name):
# Call button to check if handler setup correctly
self.camera_settings.roi_btns[name].invoke()
# Check
if name == "All":
name = "2048"
assert str(self.camera_settings.roi_widgets["Width"].get()) == name
assert str(self.camera_settings.roi_widgets["Height"].get()) == name
def test_update_fov(self):
self.camera_settings.populate_experiment_values()
# Change invoke
self.camera_settings.in_initialization = False
self.camera_settings.roi_widgets["Width"].widget.set(2048)
self.camera_settings.roi_widgets["Height"].widget.set(2048)
xFov = int(self.camera_settings.roi_widgets["FOV_X"].get())
yFov = int(self.camera_settings.roi_widgets["FOV_Y"].get())
self.camera_settings.roi_widgets["Width"].widget.set(1600)
self.camera_settings.roi_widgets["Height"].widget.set(1600)
# need these since we switched to read events
self.camera_settings.roi_widgets["Width"].get_variable().get()
self.camera_settings.roi_widgets["Height"].get_variable().get()
# Check
assert xFov != int(self.camera_settings.roi_widgets["FOV_X"].get())
assert yFov != int(self.camera_settings.roi_widgets["FOV_Y"].get())
# Reset
self.camera_settings.roi_widgets["Width"].widget.set(2048)
self.camera_settings.roi_widgets["Height"].widget.set(2048)
# need these since we switched to read events
self.camera_settings.roi_widgets["Width"].get_variable().get()
self.camera_settings.roi_widgets["Height"].get_variable().get()
assert int(self.camera_settings.roi_widgets["FOV_X"].get()) == 13066
assert int(self.camera_settings.roi_widgets["FOV_Y"].get()) == 13066
@pytest.mark.parametrize("mode", ["live", "z-stack", "stop", "single"])
@pytest.mark.parametrize("readout", ["Normal", "Light-Sheet"])
def test_set_mode(self, mode, readout):
# Populate widgets with values from experiment file
self.camera_settings.populate_experiment_values()
# Set mode
self.camera_settings.mode_widgets["Sensor"].widget.set(readout)
self.camera_settings.update_sensor_mode()
self.camera_settings.set_mode(mode)
# Check
assert self.camera_settings.mode == mode
if mode != "stop":
state = "disabled"
else:
state = "normal"
if mode != "stop":
state_readonly = "disabled"
else:
state_readonly = "readonly"
assert (
str(self.camera_settings.mode_widgets["Sensor"].widget["state"])
== state_readonly
)
if str(self.camera_settings.mode_widgets["Sensor"].get()) == "Light-Sheet":
assert (
str(self.camera_settings.mode_widgets["Readout"].widget["state"])
== state_readonly
)
if mode == "live":
assert (
str(self.camera_settings.mode_widgets["Pixels"].widget["state"])
== "normal"
)
else:
assert (
str(self.camera_settings.mode_widgets["Pixels"].widget["state"])
== state
)
else:
assert (
str(self.camera_settings.mode_widgets["Readout"].widget["state"])
== "disabled"
)
assert (
str(self.camera_settings.mode_widgets["Pixels"].widget["state"])
== "disabled"
)
assert (
str(
self.camera_settings.framerate_widgets["frames_to_average"].widget[
"state"
]
)
== state
)
assert str(self.camera_settings.roi_widgets["Width"].widget["state"]) == state
assert str(self.camera_settings.roi_widgets["Height"].widget["state"]) == state
assert (
str(self.camera_settings.roi_widgets["Binning"].widget["state"])
== state_readonly
)
for btn_name in self.camera_settings.roi_btns:
assert str(self.camera_settings.roi_btns[btn_name]["state"]) == state
@pytest.mark.parametrize("zoom", ["0.63x", "1x", "2x", "3x", "4x", "5x", "6x"])
def test_calculate_physical_dimensions(self, zoom):
self.camera_settings.parent_controller.configuration["experiment"][
"MicroscopeState"
]["zoom"] = zoom
self.camera_settings.populate_experiment_values()
# Calling
self.camera_settings.calculate_physical_dimensions()
pixel_size = self.camera_settings.default_pixel_size
x_pixel = float(self.camera_settings.roi_widgets["Width"].get())
y_pixel = float(self.camera_settings.roi_widgets["Height"].get())
microscope_state_dict = self.camera_settings.parent_controller.configuration[
"experiment"
]["MicroscopeState"]
zoom = microscope_state_dict["zoom"]
microscope_name = microscope_state_dict["microscope_name"]
pixel_size = self.camera_settings.parent_controller.configuration[
"configuration"
]["microscopes"][microscope_name]["zoom"]["pixel_size"][zoom]
dim_x = x_pixel * pixel_size
dim_y = y_pixel * pixel_size
assert float(self.camera_settings.roi_widgets["FOV_X"].get()) == float(
int(dim_x)
)
assert float(self.camera_settings.roi_widgets["FOV_Y"].get()) == float(
int(dim_y)
)
# Reset to zoom of 1
self.camera_settings.parent_controller.configuration["experiment"][
"MicroscopeState"
]["zoom"] = "1x"
assert (
self.camera_settings.parent_controller.configuration["experiment"][
"MicroscopeState"
]["zoom"]
== "1x"
)
def test_calculate_readout_time(self):
"""
TODO need more info about camera before testing
"""
pass
@pytest.mark.parametrize(
"mode", ["single", "live", "customized", "z-stack", "stop"]
)
def test_update_number_of_pixels(self, mode):
import random
self.camera_settings.populate_experiment_values()
self.camera_settings.mode = mode
self.camera_settings.mode_widgets["Pixels"].set("")
assert self.camera_settings.camera_setting_dict["number_of_pixels"] != ""
n_pixels = random.randint(1, 100)
self.camera_settings.mode_widgets["Pixels"].set(n_pixels)
# Check
assert self.camera_settings.camera_setting_dict["number_of_pixels"] == int(
n_pixels
)
if mode != "live" and mode != "stop":
assert (
self.camera_settings.camera_setting_dict["number_of_pixels"] == n_pixels
)
@pytest.mark.parametrize(
"x_pixels, y_pixels", [(512, 512), (4096, 4096), (2304, 1024), (1024, 2048)]
)
def test_update_camera_device_related_setting(self, x_pixels, y_pixels):
self.camera_settings.populate_experiment_values()
microscope_name = self.camera_settings.parent_controller.configuration[
"experiment"
]["MicroscopeState"]["microscope_name"]
camera_config = self.camera_settings.parent_controller.configuration[
"configuration"
]["microscopes"][microscope_name]["camera"]
default_x_pixels = camera_config["x_pixels"]
default_y_pixels = camera_config["y_pixels"]
camera_config["x_pixels"] = x_pixels
camera_config["y_pixels"] = y_pixels
self.camera_settings.update_camera_device_related_setting()
assert self.camera_settings.roi_widgets["Width"].get() == min(
self.camera_settings.camera_setting_dict["x_pixels"], x_pixels
)
assert self.camera_settings.roi_widgets["Height"].get() == min(
self.camera_settings.camera_setting_dict["y_pixels"], y_pixels
)
assert self.camera_settings.roi_widgets["Width"].widget["to"] == x_pixels
assert self.camera_settings.roi_widgets["Height"].widget["to"] == y_pixels
camera_config["x_pixels"] = default_x_pixels
camera_config["y_pixels"] = default_y_pixels

View File

@@ -0,0 +1,813 @@
# Copyright (c) 2021-2025 The University of Texas Southwestern Medical Center.
# All rights reserved.
# Redistribution and use in source and binary forms, with or without
# modification, are permitted for academic and research use only
# (subject to the limitations in the disclaimer below)
# provided that the following conditions are met:
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of the copyright holders nor the names of its
# contributors may be used to endorse or promote products derived from this
# software without specific prior written permission.
# NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY
# THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
# CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
# PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
# IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
from navigate.controller.sub_controllers.camera_view import CameraViewController
import pytest
import random
from unittest.mock import MagicMock
import numpy as np
class TestCameraViewController:
@pytest.fixture(autouse=True)
def setup_class(self, dummy_controller):
c = dummy_controller
self.v = dummy_controller.view
c.model = MagicMock()
c.model.get_offset_variance_maps = MagicMock(return_value=[None, None])
self.camera_view = CameraViewController(self.v.camera_waveform.camera_tab, c)
self.microscope_state = {
"channels": {
"channel_1": {
"is_selected": True,
"laser": "488nm",
"laser_index": 0,
"camera_exposure_time": 200.0,
"laser_power": 20.0,
"interval_time": 1.0,
"defocus": 100.0,
"filter_wheel_0": "Empty-Alignment",
"filter_position_0": 0,
"filter_wheel_1": "Empty-Alignment",
"filter_position_1": 0,
},
"channel_2": {
"is_selected": True,
"laser": "488nm",
"laser_index": 0,
"camera_exposure_time": 200.0,
"laser_power": 20.0,
"interval_time": 1.0,
"defocus": 100.0,
"filter_wheel_0": "Empty-Alignment",
"filter_position_0": 0,
"filter_wheel_1": "Empty-Alignment",
"filter_position_1": 0,
},
"channel_3": {
"is_selected": True,
"laser": "488nm",
"laser_index": 0,
"camera_exposure_time": 200.0,
"laser_power": 20.0,
"interval_time": 1.0,
"defocus": 100.0,
"filter_wheel_0": "Empty-Alignment",
"filter_position_0": 0,
"filter_wheel_1": "Empty-Alignment",
"filter_position_1": 0,
},
},
"number_z_steps": np.random.randint(10, 100),
"stack_cycling_mode": "per_stack",
"image_mode": "z-stack",
}
def test_init(self):
assert isinstance(self.camera_view, CameraViewController)
def test_update_display_state(self):
pass
def test_get_absolute_position(self, monkeypatch):
def mock_winfo_pointerx():
self.x = int(random.random())
return self.x
def mock_winfo_pointery():
self.y = int(random.random())
return self.y
monkeypatch.setattr(self.v, "winfo_pointerx", mock_winfo_pointerx)
monkeypatch.setattr(self.v, "winfo_pointery", mock_winfo_pointery)
# call the function under test
x, y = self.camera_view.get_absolute_position()
# make assertions about the return value
assert x == self.x
assert y == self.y
def test_popup_menu(self, monkeypatch):
# create a fake event object
self.startx = int(random.random())
self.starty = int(random.random())
event = type("Event", (object,), {"x": self.startx, "y": self.starty})()
self.grab_released = False
self.x = int(random.random())
self.y = int(random.random())
self.absx = int(random.random())
self.absy = int(random.random())
# monkey patch the get_absolute_position method to return specific values
def mock_get_absolute_position():
self.absx = int(random.random())
self.absy = int(random.random())
return self.absx, self.absy
monkeypatch.setattr(
self.camera_view, "get_absolute_position", mock_get_absolute_position
)
def mock_tk_popup(x, y):
self.x = x
self.y = y
def mock_grab_release():
self.grab_released = True
monkeypatch.setattr(self.camera_view.menu, "tk_popup", mock_tk_popup)
monkeypatch.setattr(self.camera_view.menu, "grab_release", mock_grab_release)
# call the function under test
self.camera_view.popup_menu(event)
# make assertions about the state of the view object
assert self.camera_view.move_to_x == self.startx
assert self.camera_view.move_to_y == self.starty
assert self.x == self.absx
assert self.y == self.absy
assert self.grab_released is True
@pytest.mark.parametrize("name", ["minmax", "image"])
@pytest.mark.parametrize("data", [[random.randint(0, 49), random.randint(50, 100)]])
def test_initialize(self, name, data):
self.camera_view.initialize(name, data)
# Checking values
if name == "minmax":
assert self.camera_view.image_palette["Min"].get() == data[0]
assert self.camera_view.image_palette["Max"].get() == data[1]
if name == "image":
assert self.camera_view.image_metrics["Frames"].get() == data[0]
def test_set_mode(self):
# Test default mode
self.camera_view.set_mode()
assert self.camera_view.mode == ""
assert self.camera_view.menu.entrycget("Move Here", "state") == "disabled"
# Test 'live' mode
self.camera_view.set_mode("live")
assert self.camera_view.mode == "live"
assert self.camera_view.menu.entrycget("Move Here", "state") == "normal"
# Test 'stop' mode
self.camera_view.set_mode("stop")
assert self.camera_view.mode == "stop"
assert self.camera_view.menu.entrycget("Move Here", "state") == "normal"
# Test invalid mode
self.camera_view.set_mode("invalid")
assert self.camera_view.mode == "invalid"
assert self.camera_view.menu.entrycget("Move Here", "state") == "disabled"
@pytest.mark.parametrize("mode", ["stop", "live"])
def test_move_stage(self, mode):
# Setup to check formula inside func is correct
microscope_name = self.camera_view.parent_controller.configuration[
"experiment"
]["MicroscopeState"]["microscope_name"]
zoom_value = self.camera_view.parent_controller.configuration["experiment"][
"MicroscopeState"
]["zoom"]
pixel_size = self.camera_view.parent_controller.configuration["configuration"][
"microscopes"
][microscope_name]["zoom"]["pixel_size"][zoom_value]
current_center_x = (
self.camera_view.zoom_rect[0][0] + self.camera_view.zoom_rect[0][1]
) / 2
current_center_y = (
self.camera_view.zoom_rect[1][0] + self.camera_view.zoom_rect[1][1]
) / 2
self.camera_view.move_to_x = int(random.random())
self.camera_view.move_to_y = int(random.random())
# This is the formula to check
offset_x = (
(self.camera_view.move_to_x - current_center_x)
/ self.camera_view.zoom_scale
* self.camera_view.canvas_width_scale
* pixel_size
)
offset_y = (
(self.camera_view.move_to_y - current_center_y)
/ self.camera_view.zoom_scale
* self.camera_view.canvas_width_scale
* pixel_size
)
# Set the mode to check if statements
self.camera_view.mode = mode
# Act
self.camera_view.move_stage()
# Check
assert self.camera_view.parent_controller.pop() == "get_stage_position"
res = self.camera_view.parent_controller.pop()
if mode == "stop":
assert res == "move_stage_and_acquire_image"
else:
assert res == "move_stage_and_update_info"
# Checking that move stage properly changed pos
new_pos = self.camera_view.parent_controller.pop()
self.camera_view.parent_controller.stage_pos["x"] -= offset_x
self.camera_view.parent_controller.stage_pos["y"] += offset_y
assert new_pos == self.camera_view.parent_controller.stage_pos
def test_reset_display(self, monkeypatch):
# Mock process image function
process_image_called = False
def mock_process_image():
nonlocal process_image_called
process_image_called = True
monkeypatch.setattr(self.camera_view, "process_image", mock_process_image)
# Reset and check
self.camera_view.reset_display()
assert np.array_equal(
self.camera_view.zoom_rect,
np.array(
[
[0, self.camera_view.view.canvas_width],
[0, self.camera_view.view.canvas_height],
]
),
)
assert np.array_equal(self.camera_view.zoom_offset, np.array([[0], [0]]))
assert self.camera_view.zoom_value == 1
assert self.camera_view.zoom_scale == 1
assert self.camera_view.zoom_width == self.camera_view.view.canvas_width
assert self.camera_view.zoom_height == self.camera_view.view.canvas_height
assert process_image_called is True
def test_process_image(self):
self.camera_view.image = np.random.randint(0, 256, (600, 800))
self.camera_view.digital_zoom = MagicMock()
# self.camera_view.detect_saturation = MagicMock()
self.camera_view.down_sample_image = MagicMock()
self.camera_view.scale_image_intensity = MagicMock()
self.camera_view.add_crosshair = MagicMock()
self.camera_view.apply_lut = MagicMock()
self.camera_view.populate_image = MagicMock()
self.camera_view.process_image()
self.camera_view.digital_zoom.assert_called()
# self.camera_view.detect_saturation.assert_called()
self.camera_view.down_sample_image.assert_called()
self.camera_view.scale_image_intensity.assert_called()
self.camera_view.add_crosshair.assert_called()
self.camera_view.apply_lut.assert_called()
self.camera_view.populate_image.assert_called()
@pytest.mark.parametrize("num,value", [(4, 0.95), (5, 1.05)])
def test_mouse_wheel(self, num, value):
# Test zoom in and out
event = MagicMock()
event.num = num
event.x = int(random.random())
event.y = int(random.random())
event.delta = 120
self.camera_view.zoom_value = 1
self.camera_view.zoom_scale = 1
self.camera_view.zoom_width = self.camera_view.view.canvas_width
self.camera_view.zoom_height = self.camera_view.view.canvas_height
self.camera_view.reset_display = MagicMock()
self.camera_view.process_image = MagicMock()
self.camera_view.mouse_wheel(event)
assert self.camera_view.zoom_value == value
assert self.camera_view.zoom_scale == value
assert self.camera_view.zoom_width == self.camera_view.view.canvas_width / value
assert (
self.camera_view.zoom_height == self.camera_view.view.canvas_height / value
)
if (
self.camera_view.zoom_width > self.camera_view.view.canvas_width
or self.camera_view.zoom_height > self.camera_view.view.canvas_height
):
assert self.camera_view.reset_display.called is True
self.camera_view.process_image.assert_called()
else:
assert self.camera_view.reset_display.called is False
not self.camera_view.process_image.assert_called()
@pytest.mark.skip("AssertionError: Expected 'mock' to have been called.")
def test_digital_zoom(self):
# Setup
a, b, c, d, e, f = [random.randint(1, 100) for _ in range(6)]
g, h = [random.randint(100, 400) for _ in range(2)]
i, j = [random.randint(500, 1000) for _ in range(2)]
val, scale, widthsc, heightsc = [random.randint(2, 4) for _ in range(4)]
self.camera_view.zoom_rect = np.array([[a, b], [c, d]])
self.camera_view.zoom_offset = np.array([[e], [f]])
self.camera_view.zoom_value = val
self.camera_view.zoom_scale = scale
self.camera_view.zoom_width = g # 300
self.camera_view.zoom_height = h # 400
self.camera_view.view.canvas_width = i # 800
self.camera_view.view.canvas_height = j # 600
self.camera_view.canvas_width_scale = widthsc
self.camera_view.canvas_height_scale = heightsc
self.camera_view.image = np.random.randint(0, 256, (600, 800))
self.camera_view.reset_display = MagicMock()
# Preprocess
new_zoom_rec = self.camera_view.zoom_rect - self.camera_view.zoom_offset
new_zoom_rec *= self.camera_view.zoom_value
new_zoom_rec += self.camera_view.zoom_offset
x_start_index = int(-new_zoom_rec[0][0] / self.camera_view.zoom_scale)
x_end_index = int(x_start_index + self.camera_view.zoom_width)
y_start_index = int(-new_zoom_rec[1][0] / self.camera_view.zoom_scale)
y_end_index = int(y_start_index + self.camera_view.zoom_height)
crosshair_x = (new_zoom_rec[0][0] + new_zoom_rec[0][1]) / 2
crosshair_y = (new_zoom_rec[1][0] + new_zoom_rec[1][1]) / 2
if crosshair_x < 0 or crosshair_x >= self.camera_view.view.canvas_width:
crosshair_x = -1
if crosshair_y < 0 or crosshair_y >= self.camera_view.view.canvas_height:
crosshair_y = -1
new_image = self.camera_view.image[
y_start_index
* self.camera_view.canvas_height_scale : y_end_index
* self.camera_view.canvas_height_scale,
x_start_index
* self.camera_view.canvas_width_scale : x_end_index
* self.camera_view.canvas_width_scale,
]
# Call func
self.camera_view.digital_zoom()
# Check zoom rec
assert np.array_equal(self.camera_view.zoom_rect, new_zoom_rec)
# Check zoom offset
assert np.array_equal(self.camera_view.zoom_offset, np.array([[0], [0]]))
# Check zoom_value
assert self.camera_view.zoom_value == 1
# Check zoom_image
assert np.array_equal(self.camera_view.zoom_image, new_image)
# Check crosshairs
assert self.camera_view.crosshair_x == int(crosshair_x)
assert self.camera_view.crosshair_y == int(crosshair_y)
# Check reset display
self.camera_view.reset_display.assert_called()
@pytest.mark.parametrize("onoff", [True, False])
def test_left_click(self, onoff):
self.camera_view.add_crosshair = MagicMock()
self.camera_view.digital_zoom = MagicMock()
# self.camera_view.detect_saturation = MagicMock()
self.camera_view.down_sample_image = MagicMock()
self.camera_view.transpose_image = MagicMock()
self.camera_view.scale_image_intensity = MagicMock()
self.camera_view.apply_lut = MagicMock()
self.camera_view.populate_image = MagicMock()
event = MagicMock()
self.camera_view.image = np.random.randint(0, 256, (600, 800))
self.camera_view.apply_cross_hair = onoff
self.camera_view.left_click(event)
self.camera_view.add_crosshair.assert_called()
self.camera_view.populate_image.assert_called()
assert self.camera_view.apply_cross_hair == (not onoff)
@pytest.mark.parametrize("frames", [0, 1, 2, 10])
def test_update_max_count(self, frames):
self.camera_view.image_metrics["Frames"].set(frames)
idx = 0
temp = [0] * 40
while idx < 40:
self.camera_view._last_frame_display_max = np.random.randint(0, 500)
temp[idx] = self.camera_view._last_frame_display_max
idx += 1
# Act
self.camera_view.update_max_counts()
assert self.camera_view._max_intensity_history_idx == idx % 32
assert (
self.camera_view.max_intensity_history[
self.camera_view._max_intensity_history_idx - 1
]
== self.camera_view._last_frame_display_max
)
# Assert
if frames == 0:
assert self.camera_view.image_metrics["Frames"].get() == 1
frame_num = 1
else:
frame_num = frames
if frame_num <= idx:
rolling_average = sum(temp[idx - frame_num : idx]) / frame_num
else:
rolling_average = sum(temp[:idx]) / frame_num
assert self.camera_view.image_metrics["Image"].get() == round(
rolling_average, 0
)
def test_down_sample_image(self, monkeypatch):
import cv2
# create a test image
test_image = np.random.rand(100, 100)
self.zoom_image = test_image
# set the widget size
widget = type("MyWidget", (object,), {"widget": self.camera_view.view})
event = type(
"MyEvent",
(object,),
{
"widget": widget,
"width": np.random.randint(5, 1000),
"height": np.random.randint(5, 1000),
},
)
self.camera_view.resize(event)
# monkeypatch cv2.resize
def mocked_resize(src, dsize, interpolation=1):
return np.ones((dsize[0], dsize[1]))
monkeypatch.setattr(cv2, "resize", mocked_resize)
# call the function
down_sampled_image = self.camera_view.down_sample_image(test_image)
# assert that the image has been resized correctly
assert np.shape(down_sampled_image) == (
self.camera_view.view.canvas_width,
self.camera_view.view.canvas_height,
)
# assert that the image has not been modified
assert not np.array_equal(down_sampled_image, test_image)
@pytest.mark.parametrize("auto", [True, False])
def test_scale_image_intensity(self, auto):
# Create a test image
test_image = np.random.rand(100, 100)
# Set autoscale to True
self.camera_view.autoscale = auto
if auto is False:
self.camera_view.max_counts = 1.5
self.camera_view.min_counts = 0.5
# Call the function
scaled_image = self.camera_view.scale_image_intensity(test_image)
# Assert that max_counts and min_counts have been set correctly
if auto is True:
assert self.camera_view._last_frame_display_max == np.max(test_image)
# Assert that the image has been scaled correctly
assert np.min(scaled_image) >= 0
assert np.max(scaled_image) <= 255
def test_populate_image(self, monkeypatch):
from PIL import Image, ImageTk
import cv2
# Create test image
self.camera_view.cross_hair_image = np.random.rand(100, 100)
self.camera_view.ilastik_seg_mask = np.random.rand(100, 100)
# Set display_mask_flag to True
self.camera_view.display_mask_flag = True
# Monkeypatch the Image.fromarray() method of PIL
def mocked_fromarray(arr):
return arr
monkeypatch.setattr(Image, "fromarray", mocked_fromarray)
# Monkeypatch the cv2.resize() function
def mocked_resize(arr, size):
return arr
monkeypatch.setattr(cv2, "resize", mocked_resize)
# Monkeypatch the Image.blend() method of PIL
def mocked_blend(img1, img2, alpha):
return img1 * alpha + img2 * (1 - alpha)
monkeypatch.setattr(Image, "blend", mocked_blend)
def mocked_PhotoImage(img):
mock_photo = MagicMock()
# Set up width and height methods to return appropriate dimensions
mock_photo.width.return_value = 100
mock_photo.height.return_value = 100
return mock_photo
monkeypatch.setattr(ImageTk, "PhotoImage", mocked_PhotoImage)
self.camera_view.canvas.create_image = MagicMock()
self.camera_view.image_cache_flag = True
# Call the function
self.camera_view.populate_image(self.camera_view.cross_hair_image)
# Assert that the tk_image has been created correctly
assert self.camera_view._img_buf is not None
# Set display_mask_flag to True
self.camera_view.display_mask_flag = False
# Call the function
self.camera_view.populate_image(self.camera_view.cross_hair_image)
def test_initialize_non_live_display(self):
# Create test buffer and microscope_state
camera_parameters = {
"img_x_pixels": np.random.randint(1, 200),
"img_y_pixels": np.random.randint(1, 200),
}
# Call the function
self.camera_view.initialize_non_live_display(
self.microscope_state, camera_parameters
)
# Assert that the variables have been set correctly
assert self.camera_view.image_count == 0
assert self.camera_view.slice_index == 0
assert self.camera_view.number_of_channels == len(
self.microscope_state["channels"]
)
assert (
self.camera_view.number_of_slices == self.microscope_state["number_z_steps"]
)
assert (
self.camera_view.total_images_per_volume
== self.camera_view.number_of_channels * self.camera_view.number_of_slices
)
assert self.camera_view.original_image_width == int(
camera_parameters["img_x_pixels"]
)
assert self.camera_view.original_image_height == int(
camera_parameters["img_y_pixels"]
)
assert self.camera_view.canvas_width_scale == float(
self.camera_view.original_image_width / self.camera_view.canvas_width
)
assert self.camera_view.canvas_height_scale == float(
self.camera_view.original_image_height / self.camera_view.canvas_height
)
def test_identify_channel_index_and_slice(self):
# Not currently in use
pass
def test_retrieve_image_slice_from_volume(self):
# Not currently in use
pass
@pytest.mark.parametrize("transpose", [True, False])
def test_display_image(self, transpose):
"""Test the display of an image on the camera view
TODO: The recent refactor makes this test non-functional. It needs to be updated
The newer code does not use the camera_view.image attribute after the
transpose operation, so any transpose/image flipping is not reflected in the
final image. The test should be updated to reflect this.,
"""
self.camera_view.initialize_non_live_display(
self.microscope_state, {"img_x_pixels": 50, "img_y_pixels": 100}
)
self.camera_view.digital_zoom = MagicMock()
# self.camera_view.detect_saturation = MagicMock()
self.camera_view.down_sample_image = MagicMock()
self.camera_view.scale_image_intensity = MagicMock()
self.camera_view.apply_lut = MagicMock()
self.camera_view.populate_image = MagicMock()
images = np.random.rand(10, 100, 50)
self.camera_view.transpose = transpose
count = 0
self.camera_view.image_count = count
self.camera_view.image_metrics = {"Channel": MagicMock()}
self.camera_view.update_max_counts = MagicMock()
self.camera_view.flip_flags = {"x": False, "y": False}
image_id = np.random.randint(0, 10)
self.camera_view.try_to_display_image(images[image_id])
assert (
self.camera_view.spooled_images.size_y,
self.camera_view.spooled_images.size_x,
) == np.shape(images[image_id])
assert self.camera_view.image_count == count + 1
self.camera_view.flip_flags = {"x": True, "y": False}
image_id = np.random.randint(0, 10)
self.camera_view.try_to_display_image(images[image_id])
# assert np.shape(self.camera_view.image) == np.shape(images[image_id])
# if not transpose:
# assert (self.camera_view.image == images[image_id][:, ::-1]).all()
# else:
# assert (self.camera_view.image == images[image_id][:, ::-1].T).all()
assert self.camera_view.image_count == count + 2
self.camera_view.flip_flags = {"x": False, "y": True}
image_id = np.random.randint(0, 10)
self.camera_view.try_to_display_image(images[image_id])
# assert np.shape(self.camera_view.image) == np.shape(images[image_id])
# if not transpose:
# assert (self.camera_view.image == images[image_id][::-1, :]).all()
# else:
# assert (self.camera_view.image == images[image_id][::-1, :].T).all()
assert self.camera_view.image_count == count + 3
self.camera_view.flip_flags = {"x": True, "y": True}
image_id = np.random.randint(0, 10)
self.camera_view.try_to_display_image(images[image_id])
# assert np.shape(self.camera_view.image) == np.shape(images[image_id])
# if not transpose:
# assert (self.camera_view.image == images[image_id][::-1, ::-1]).all()
# else:
# assert (self.camera_view.image == images[image_id][::-1, ::-1].T).all()
assert self.camera_view.image_count == count + 4
def test_add_crosshair(self):
# Arrange
x = self.camera_view.canvas_width
y = self.camera_view.canvas_height
image = np.random.rand(x, y)
self.camera_view.apply_cross_hair = True
# Act
image2 = self.camera_view.add_crosshair(image)
# Assert
assert np.all(image2[:, self.camera_view.zoom_rect[0][1] // 2] == 255)
assert np.all(image2[self.camera_view.zoom_rect[1][1] // 2, :] == 255)
def test_apply_LUT(self):
# Someone else with better numpy understanding will need to do this TODO
pass
def test_update_LUT(self):
# Same as apply LUT TODO
pass
def test_toggle_min_max_button(self):
# Setup for true path
self.camera_view.image_palette["Autoscale"].set(True)
# Act by calling function
self.camera_view.toggle_min_max_buttons()
# Assert things are correct
assert str(self.camera_view.image_palette["Min"].widget["state"]) == "disabled"
assert str(self.camera_view.image_palette["Max"].widget["state"]) == "disabled"
# Setup for false path
self.camera_view.image_palette["Autoscale"].set(False)
# Mock function call to isolate
self.camera_view.update_min_max_counts = MagicMock()
# Act by calling function
self.camera_view.toggle_min_max_buttons()
# Assert things are correct and called
assert str(self.camera_view.image_palette["Min"].widget["state"]) == "normal"
assert str(self.camera_view.image_palette["Max"].widget["state"]) == "normal"
self.camera_view.update_min_max_counts.assert_called()
def test_transpose_image(self):
# Create test data
self.camera_view.image_palette["Flip XY"].set(True)
self.camera_view.transpose = None
# Call the function
self.camera_view.update_transpose_state()
# Assert the output
assert self.camera_view.transpose is True
# Create test data
self.camera_view.image_palette["Flip XY"].set(False)
self.camera_view.transpose = None
# Call the function
self.camera_view.update_transpose_state()
# Assert the output
assert self.camera_view.transpose is False
def test_update_min_max_counts(self):
# Create test data
min = np.random.randint(0, 10)
max = np.random.randint(0, 10)
self.camera_view.image_palette["Min"].set(min)
self.camera_view.image_palette["Max"].set(max)
self.camera_view.min_counts = None
self.camera_view.max_counts = None
# Call the function
self.camera_view.update_min_max_counts()
# Assert the output
assert self.camera_view.min_counts == min
assert self.camera_view.max_counts == max
def test_set_mask_color_table(self):
# This is beyond me currently TODO
pass
def test_display_mask(self, monkeypatch):
import cv2
# Create test data
self.camera_view.ilastik_seg_mask = None
self.camera_view.ilastik_mask_ready_lock.acquire()
mask = np.zeros((5, 5), dtype=np.uint8)
self.camera_view.mask_color_table = np.zeros((256, 1, 3), dtype=np.uint8)
# Define the monkeypatch
def mock_applyColorMap(mask, mask_color_table):
return mask
# Apply the monkeypatch
monkeypatch.setattr(cv2, "applyColorMap", mock_applyColorMap)
# Call the function
self.camera_view.display_mask(mask)
# Assert the output
assert (self.camera_view.ilastik_seg_mask == mask).all()
assert not self.camera_view.ilastik_mask_ready_lock.locked()
def test_update_canvas_size(self):
self.camera_view.view.canvas["width"] = random.randint(1, 2000)
self.camera_view.view.canvas["height"] = random.randint(1, 2000)
self.camera_view.update_canvas_size()
assert self.camera_view.canvas_width > 0
assert self.camera_view.canvas_height > 0

View File

@@ -0,0 +1,168 @@
# Copyright (c) 2021-2025 The University of Texas Southwestern Medical Center.
# All rights reserved.
# Redistribution and use in source and binary forms, with or without
# modification, are permitted for academic and research use only (subject to the
# limitations in the disclaimer below) provided that the following conditions are met:
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of the copyright holders nor the names of its
# contributors may be used to endorse or promote products derived from this
# software without specific prior written permission.
# NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY
# THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
# CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
# PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
# IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
import pytest
class TestChannelSettingController:
@pytest.fixture(autouse=True)
def setup_class(self, dummy_controller):
from navigate.controller.sub_controllers.channels_tab import (
ChannelsTabController,
)
from navigate.controller.sub_controllers.channels_settings import (
ChannelSettingController,
)
self.ctc = ChannelsTabController(
dummy_controller.view.settings.channels_tab, dummy_controller
)
self.ctc.commands = []
self.ctc.execute = lambda command: self.ctc.commands.append(command)
self.channel_setting = ChannelSettingController(
self.ctc.view.channel_widgets_frame,
self.ctc,
dummy_controller.configuration_controller,
)
self.channel_setting.populate_experiment_values(
dummy_controller.configuration["experiment"]["MicroscopeState"]["channels"]
)
@pytest.mark.parametrize(
"mode,state,state_readonly",
[("stop", "normal", "readonly"), ("live", "disabled", "readonly")],
)
def test_set_mode(self, mode, state, state_readonly):
self.channel_setting.set_mode(mode)
for i in range(5):
assert str(self.channel_setting.view.channel_checks[i]["state"]) == state
# interval widget is disabled now
assert (
str(self.channel_setting.view.interval_spins[i]["state"]) == "disabled"
)
if mode == "stop":
assert (
str(self.channel_setting.view.laser_pulldowns[i]["state"])
== state_readonly
)
else:
assert (
str(self.channel_setting.view.laser_pulldowns[i]["state"])
== "disabled"
)
if self.channel_setting.mode != "live" or (
self.channel_setting.mode == "live"
and not self.channel_setting.view.channel_variables[i].get()
):
assert (
str(self.channel_setting.view.exptime_pulldowns[i]["state"])
== state
)
if not self.channel_setting.view.channel_variables[i].get():
assert (
str(self.channel_setting.view.laserpower_pulldowns[i]["state"])
== state
)
assert (
str(self.channel_setting.view.filterwheel_pulldowns[i]["state"])
== state_readonly
)
assert str(self.channel_setting.view.defocus_spins[i]["state"]) == state
def test_channel_callback(self):
import random
self.channel_setting.in_initialization = False
channel_dict = (
self.channel_setting.parent_controller.parent_controller.configuration[
"experiment"
]["MicroscopeState"]["channels"]
)
# shuffle the channels
new_channel_dict = {
k: v
for k, v in zip(
channel_dict.keys(),
random.choices(channel_dict.values(), k=len(channel_dict.keys())),
)
}
self.channel_setting.populate_experiment_values(channel_dict)
for channel_id in range(self.channel_setting.num):
vals = self.channel_setting.get_vals_by_channel(channel_id)
channel_key = f"channel_{str(channel_id + 1)}"
try:
setting_dict = channel_dict[channel_key]
new_setting_dict = new_channel_dict[channel_key]
except KeyError:
continue
# Test channel callback through trace writes
for k in setting_dict.keys():
if k == "laser_index" or k.startswith("filter_position"):
continue
if k == "defocus":
new_val = float(random.randint(1, 10))
else:
new_val = new_setting_dict[k]
vals[k].set(new_val)
assert str(vals[k].get()) == str(new_val)
if k != "defocus":
assert setting_dict[k] == new_setting_dict[k]
if k == "laser":
assert (
setting_dict["laser_index"] == new_setting_dict["laser_index"]
)
elif k == "filter":
assert (
setting_dict["filter_position"]
== new_setting_dict["filter_position"]
)
elif k == "camera_exposure_time" or k == "is_selected":
assert (
self.channel_setting.parent_controller.commands.pop()
== "recalculate_timepoint"
)
self.channel_setting.parent_controller.commands = [] # reset
def test_get_vals_by_channel(self):
# Not needed to test IMO
pass
def test_get_index(self):
# Not needed to test IMO
pass

View File

@@ -0,0 +1,385 @@
# Copyright (c) 2021-2025 The University of Texas Southwestern Medical Center.
# All rights reserved.
# Redistribution and use in source and binary forms, with or without
# modification, are permitted for academic and research use only (subject to the
# limitations in the disclaimer below) provided that the following conditions are met:
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of the copyright holders nor the names of its
# contributors may be used to endorse or promote products derived from this
# software without specific prior written permission.
# NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY
# THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
# CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
# PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
# IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
import random
import copy
import pytest
import numpy as np
from unittest.mock import patch
@pytest.fixture
def channels_tab_controller(dummy_controller):
from navigate.controller.sub_controllers.channels_tab import (
ChannelsTabController,
)
return ChannelsTabController(
dummy_controller.view.settings.channels_tab, dummy_controller
)
def test_update_z_steps(channels_tab_controller):
# Calculate params
z_start, f_start = random.randint(1, 1000), random.randint(1, 1000)
z_end, f_end = random.randint(1, 1000), random.randint(1, 1000)
if z_end < z_start:
# Sort so we are always going low to high
tmp = z_start
tmp_f = f_start
z_start = z_end
f_start = f_end
z_end = tmp
f_end = tmp_f
step_size = max(1, min(random.randint(1, 10), (z_end - z_start) // 2))
# Set params
channels_tab_controller.microscope_state_dict = (
channels_tab_controller.parent_controller.configuration["experiment"][
"MicroscopeState"
]
)
channels_tab_controller.in_initialization = False
channels_tab_controller.stack_acq_vals["start_position"].set(z_start)
channels_tab_controller.stack_acq_vals["start_focus"].set(f_start)
channels_tab_controller.stack_acq_vals["end_position"].set(z_end)
channels_tab_controller.stack_acq_vals["end_focus"].set(f_end)
channels_tab_controller.stack_acq_vals["step_size"].set(step_size)
# Run
channels_tab_controller.update_z_steps()
# Verify
number_z_steps = int(np.ceil(np.abs((z_start - z_end) / step_size)))
assert (
int(channels_tab_controller.stack_acq_vals["number_z_steps"].get())
== number_z_steps
)
# test flip_z is True
microscope_name = (
channels_tab_controller.parent_controller.configuration_controller.microscope_name
)
stage_config = channels_tab_controller.parent_controller.configuration[
"configuration"
]["microscopes"][microscope_name]["stage"]
stage_config["flip_z"] = True
channels_tab_controller.z_origin = (z_start + z_end) / 2
channels_tab_controller.stack_acq_vals["start_position"].set(z_end)
channels_tab_controller.stack_acq_vals["start_focus"].set(f_end)
channels_tab_controller.stack_acq_vals["end_position"].set(z_start)
channels_tab_controller.stack_acq_vals["end_focus"].set(f_start)
channels_tab_controller.update_z_steps()
assert channels_tab_controller.stack_acq_vals["step_size"].get() == step_size
assert channels_tab_controller.microscope_state_dict["step_size"] == -1 * step_size
assert (
channels_tab_controller.stack_acq_vals["number_z_steps"].get() == number_z_steps
)
stage_config["flip_z"] = False
def test_update_start_position(channels_tab_controller):
z, f = random.randint(0, 1000), random.randint(0, 1000)
channels_tab_controller.parent_controller.configuration["experiment"][
"StageParameters"
]["z"] = z
channels_tab_controller.parent_controller.configuration["experiment"][
"StageParameters"
]["f"] = f
channels_tab_controller.update_start_position()
assert channels_tab_controller.z_origin == z
assert channels_tab_controller.focus_origin == f
assert int(channels_tab_controller.stack_acq_vals["start_position"].get()) == 0
assert int(channels_tab_controller.stack_acq_vals["start_focus"].get()) == 0
# test flip_z is True
microscope_name = (
channels_tab_controller.parent_controller.configuration_controller.microscope_name
)
stage_config = channels_tab_controller.parent_controller.configuration[
"configuration"
]["microscopes"][microscope_name]["stage"]
stage_config["flip_z"] = True
channels_tab_controller.update_start_position()
assert channels_tab_controller.z_origin == z
assert channels_tab_controller.focus_origin == f
assert int(channels_tab_controller.stack_acq_vals["end_position"].get()) == 0
assert int(channels_tab_controller.stack_acq_vals["end_focus"].get()) == 0
stage_config["flip_z"] = False
def test_update_end_position(channels_tab_controller):
configuration = channels_tab_controller.parent_controller.configuration
# Initialize
z, f = random.randint(0, 1000), random.randint(0, 1000)
z_shift, f_shift = random.randint(1, 500), random.randint(1, 500)
configuration["experiment"]["StageParameters"]["z"] = z + z_shift
configuration["experiment"]["StageParameters"]["f"] = f + f_shift
print(f"z: {z} z-shift: {z_shift} f: {f} f-shift: {f_shift}")
print(f'z-dict: {configuration["experiment"]["StageParameters"]["z"]}')
print(f'f-dict: {configuration["experiment"]["StageParameters"]["f"]}')
# Step backwards and record results
channels_tab_controller.z_origin = z - z_shift
channels_tab_controller.focus_origin = f - f_shift
channels_tab_controller.update_end_position()
z_origin_minus = copy.deepcopy(channels_tab_controller.z_origin)
f_origin_minus = copy.deepcopy(channels_tab_controller.focus_origin)
start_position_minus = copy.deepcopy(
channels_tab_controller.stack_acq_vals["start_position"].get()
)
end_position_minus = copy.deepcopy(
channels_tab_controller.stack_acq_vals["end_position"].get()
)
start_focus_minus = copy.deepcopy(
channels_tab_controller.stack_acq_vals["start_focus"].get()
)
end_focus_minus = copy.deepcopy(
channels_tab_controller.stack_acq_vals["end_focus"].get()
)
print("back")
print(f"z: {z} z-shift: {z_shift} f: {f} f-shift: {f_shift}")
print(f'z-dict: {configuration["experiment"]["StageParameters"]["z"]}')
print(f'f-dict: {configuration["experiment"]["StageParameters"]["f"]}')
# Step forward
configuration["experiment"]["StageParameters"]["z"] = z - z_shift
configuration["experiment"]["StageParameters"]["f"] = f - f_shift
channels_tab_controller.z_origin = z + z_shift
channels_tab_controller.focus_origin = f + f_shift
channels_tab_controller.update_end_position()
print("forward")
print(f"z: {z} z-shift: {z_shift} f: {f} f-shift: {f_shift}")
print(f'z-dict: {configuration["experiment"]["StageParameters"]["z"]}')
print(f'f-dict: {configuration["experiment"]["StageParameters"]["f"]}')
# Ensure we achieve the same origin
assert channels_tab_controller.z_origin == z_origin_minus
assert channels_tab_controller.focus_origin == f_origin_minus
assert (
channels_tab_controller.stack_acq_vals["start_position"].get()
== start_position_minus
)
assert (
channels_tab_controller.stack_acq_vals["end_position"].get()
== end_position_minus
)
assert (
channels_tab_controller.stack_acq_vals["start_focus"].get() == start_focus_minus
)
assert channels_tab_controller.stack_acq_vals["end_focus"].get() == end_focus_minus
# test flip_z is True
microscope_name = (
channels_tab_controller.parent_controller.configuration_controller.microscope_name
)
stage_config = channels_tab_controller.parent_controller.configuration[
"configuration"
]["microscopes"][microscope_name]["stage"]
stage_config["flip_z"] = True
# forward
channels_tab_controller.z_origin = z
channels_tab_controller.focus_origin = f
configuration["experiment"]["StageParameters"]["z"] = z - 2 * z_shift
configuration["experiment"]["StageParameters"]["f"] = f - 2 * f_shift
channels_tab_controller.update_end_position()
assert channels_tab_controller.z_origin == z - z_shift
assert channels_tab_controller.focus_origin == f - f_shift
assert channels_tab_controller.stack_acq_vals["start_position"].get() == z_shift
assert channels_tab_controller.stack_acq_vals["end_position"].get() == -1 * z_shift
assert channels_tab_controller.stack_acq_vals["start_focus"].get() == f_shift
assert channels_tab_controller.stack_acq_vals["end_focus"].get() == -1 * f_shift
# backward
channels_tab_controller.z_origin = z
channels_tab_controller.focus_origin = f
configuration["experiment"]["StageParameters"]["z"] = z + 2 * z_shift
configuration["experiment"]["StageParameters"]["f"] = f + 2 * f_shift
channels_tab_controller.update_end_position()
assert channels_tab_controller.z_origin == z + z_shift
assert channels_tab_controller.focus_origin == f + f_shift
assert channels_tab_controller.stack_acq_vals["start_position"].get() == z_shift
assert channels_tab_controller.stack_acq_vals["end_position"].get() == -1 * z_shift
assert channels_tab_controller.stack_acq_vals["start_focus"].get() == f_shift
assert channels_tab_controller.stack_acq_vals["end_focus"].get() == -1 * f_shift
stage_config["flip_z"] = False
def test_update_start_update_end_position(channels_tab_controller):
configuration = channels_tab_controller.parent_controller.configuration
channels_tab_controller.microscope_state_dict = configuration["experiment"][
"MicroscopeState"
]
channels_tab_controller.in_initialization = False
# Initialize
z, f = random.randint(0, 1000), random.randint(0, 1000)
z_shift, f_shift = random.randint(1, 500), random.randint(1, 500)
configuration["experiment"]["StageParameters"]["z"] = z - z_shift
configuration["experiment"]["StageParameters"]["f"] = f - f_shift
channels_tab_controller.update_start_position()
print(f"z: {z} z-shift: {z_shift} f: {f} f-shift: {f_shift}")
print(f'z-dict: {configuration["experiment"]["StageParameters"]["z"]}')
print(f'f-dict: {configuration["experiment"]["StageParameters"]["f"]}')
# Step forward and record results
configuration["experiment"]["StageParameters"]["z"] = z + z_shift
configuration["experiment"]["StageParameters"]["f"] = f + f_shift
channels_tab_controller.update_end_position()
z_origin_minus = copy.deepcopy(channels_tab_controller.z_origin)
f_origin_minus = copy.deepcopy(channels_tab_controller.focus_origin)
start_position_minus = copy.deepcopy(
channels_tab_controller.stack_acq_vals["start_position"].get()
)
end_position_minus = copy.deepcopy(
channels_tab_controller.stack_acq_vals["end_position"].get()
)
start_focus_minus = copy.deepcopy(
channels_tab_controller.stack_acq_vals["start_focus"].get()
)
end_focus_minus = copy.deepcopy(
channels_tab_controller.stack_acq_vals["end_focus"].get()
)
print("back")
print(f"z: {z} z-shift: {z_shift} f: {f} f-shift: {f_shift}")
print(f'z-dict: {configuration["experiment"]["StageParameters"]["z"]}')
print(f'f-dict: {configuration["experiment"]["StageParameters"]["f"]}')
channels_tab_controller.update_start_position()
# Step back
configuration["experiment"]["StageParameters"]["z"] = z - z_shift
configuration["experiment"]["StageParameters"]["f"] = f - f_shift
channels_tab_controller.update_end_position()
print("forward")
print(f"z: {z} z-shift: {z_shift} f: {f} f-shift: {f_shift}")
print(f'z-dict: {configuration["experiment"]["StageParameters"]["z"]}')
print(f'f-dict: {configuration["experiment"]["StageParameters"]["f"]}')
# Ensure we achieve the same origin
assert channels_tab_controller.z_origin == z_origin_minus
assert channels_tab_controller.focus_origin == f_origin_minus
assert (
channels_tab_controller.stack_acq_vals["start_position"].get()
== start_position_minus
)
assert (
channels_tab_controller.stack_acq_vals["end_position"].get()
== end_position_minus
)
assert (
channels_tab_controller.stack_acq_vals["start_focus"].get() == start_focus_minus
)
assert channels_tab_controller.stack_acq_vals["end_focus"].get() == end_focus_minus
# test flip_z is true
microscope_name = (
channels_tab_controller.parent_controller.configuration_controller.microscope_name
)
stage_config = channels_tab_controller.parent_controller.configuration[
"configuration"
]["microscopes"][microscope_name]["stage"]
stage_config["flip_z"] = True
configuration = channels_tab_controller.parent_controller.configuration
z, f = random.randint(0, 1000), random.randint(0, 1000)
z_shift, f_shift = random.randint(1, 500), random.randint(1, 500)
configuration["experiment"]["StageParameters"]["z"] = z - z_shift
configuration["experiment"]["StageParameters"]["f"] = f - f_shift
channels_tab_controller.update_start_position()
configuration["experiment"]["StageParameters"]["z"] = z + z_shift
configuration["experiment"]["StageParameters"]["f"] = f + f_shift
channels_tab_controller.update_end_position()
assert channels_tab_controller.z_origin == z
assert channels_tab_controller.focus_origin == f
assert channels_tab_controller.stack_acq_vals["start_position"].get() == z_shift
assert channels_tab_controller.stack_acq_vals["end_position"].get() == -1 * z_shift
assert channels_tab_controller.stack_acq_vals["start_focus"].get() == f_shift
assert channels_tab_controller.stack_acq_vals["end_focus"].get() == -1 * f_shift
assert configuration["experiment"]["MicroscopeState"]["start_position"] == z_shift
assert (
configuration["experiment"]["MicroscopeState"]["end_position"] == -1 * z_shift
)
assert configuration["experiment"]["MicroscopeState"]["abs_z_start"] == z - z_shift
assert configuration["experiment"]["MicroscopeState"]["abs_z_end"] == z + z_shift
assert configuration["experiment"]["MicroscopeState"]["start_focus"] == f_shift
assert configuration["experiment"]["MicroscopeState"]["end_focus"] == -1 * f_shift
configuration["experiment"]["StageParameters"]["z"] = z + z_shift
configuration["experiment"]["StageParameters"]["f"] = f + f_shift
channels_tab_controller.update_start_position()
configuration["experiment"]["StageParameters"]["z"] = z - z_shift
configuration["experiment"]["StageParameters"]["f"] = f - f_shift
channels_tab_controller.update_end_position()
assert channels_tab_controller.z_origin == z
assert channels_tab_controller.focus_origin == f
assert channels_tab_controller.stack_acq_vals["start_position"].get() == z_shift
assert channels_tab_controller.stack_acq_vals["end_position"].get() == -1 * z_shift
assert channels_tab_controller.stack_acq_vals["start_focus"].get() == f_shift
assert channels_tab_controller.stack_acq_vals["end_focus"].get() == -1 * f_shift
assert configuration["experiment"]["MicroscopeState"]["start_position"] == z_shift
assert (
configuration["experiment"]["MicroscopeState"]["end_position"] == -1 * z_shift
)
assert configuration["experiment"]["MicroscopeState"]["abs_z_start"] == z - z_shift
assert configuration["experiment"]["MicroscopeState"]["abs_z_end"] == z + z_shift
assert configuration["experiment"]["MicroscopeState"]["start_focus"] == f_shift
assert configuration["experiment"]["MicroscopeState"]["end_focus"] == -1 * f_shift
stage_config["flip_z"] = False
@pytest.mark.parametrize("is_multiposition", [True, False])
def test_toggle_multiposition(channels_tab_controller, is_multiposition):
channels_tab_controller.populate_experiment_values()
channels_tab_controller.is_multiposition_val.set(is_multiposition)
with patch.object(channels_tab_controller, "update_timepoint_setting") as uts:
channels_tab_controller.toggle_multiposition()
assert channels_tab_controller.is_multiposition == is_multiposition
assert (
channels_tab_controller.microscope_state_dict["is_multiposition"]
== is_multiposition
)
uts.assert_called()

View File

@@ -0,0 +1,259 @@
# Copyright (c) 2021-2025 The University of Texas Southwestern Medical Center.
# All rights reserved.
# Redistribution and use in source and binary forms, with or without
# modification, are permitted for academic and research use only
# (subject to the limitations in the disclaimer below)
# provided that the following conditions are met:
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of the copyright holders nor the names of its
# contributors may be used to endorse or promote products derived from this
# software without specific prior written permission.
# NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY
# THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
# CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
# PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
# IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
# Standard Library Imports
import unittest
from unittest.mock import MagicMock, patch
import tkinter as tk
# Third Party Imports
import pytest
# Local Imports
from navigate.controller.sub_controllers.menus import (
MenuController,
FakeEvent,
)
class TestFakeEvent(unittest.TestCase):
def test_fake_event_creation(self):
fake_event = FakeEvent(char="a", keysym="A")
self.assertEqual(fake_event.char, "a")
self.assertEqual(fake_event.keysym, "A")
self.assertEqual(fake_event.state, 0)
class TestStageMovement(unittest.TestCase):
def setUp(self):
# Create a mock parent controller and view
self.root = tk.Tk()
self.parent_controller = MagicMock()
self.parent_controller.stage_controller = MagicMock()
self.view = MagicMock()
self.view.root = self.root
# Initialize the menu controller
self.mc = MenuController(self.view, self.parent_controller)
# Mock the histogram configuration entry.
self.parent_controller.configuration["gui"]["histogram"] = MagicMock()
self.parent_controller.configuration["gui"]["histogram"].get.return_value = True
def tearDown(self):
self.root.destroy()
def test_initialize_menus(self):
self.mc.initialize_menus()
def test_stage_movement_with_ttk_entry(self):
self.mc.parent_controller.view.focus_get.return_value = MagicMock(
widgetName="ttk::entry"
)
self.mc.stage_movement("a")
self.mc.parent_controller.stage_controller.stage_key_press.assert_not_called()
def test_stage_movement_with_ttk_combobox(self):
self.mc.parent_controller.view.focus_get.return_value = MagicMock(
widgetName="ttk::combobox"
)
self.mc.stage_movement("a")
self.mc.parent_controller.stage_controller.stage_key_press.assert_not_called()
def test_stage_movement_with_other_widget(self):
self.mc.parent_controller.view.focus_get.return_value = MagicMock(
widgetName="other_widget"
)
self.mc.stage_movement("a")
self.mc.parent_controller.stage_controller.stage_key_press.assert_called_with(
self.mc.fake_event
)
def test_stage_movement_with_key_error(self):
self.mc.parent_controller.view.focus_get.side_effect = KeyError
# Test that no exception is raised
try:
self.mc.stage_movement("a")
except KeyError:
self.fail("stage_movement() raised KeyError unexpectedly!")
def test_stage_movement_with_no_focus(self):
self.mc.parent_controller.view.focus_get.return_value = None
self.mc.stage_movement("a")
self.mc.parent_controller.stage_controller.stage_key_press.assert_called_with(
self.mc.fake_event
)
class TestMenuController(unittest.TestCase):
@pytest.fixture(autouse=True)
def setup_class(self, dummy_controller):
c = dummy_controller
v = dummy_controller.view
self.menu_controller = MenuController(v, c)
def test_attributes(self):
methods = dir(MenuController)
desired_methods = [
"initialize_menus",
"populate_menu",
"new_experiment",
"load_experiment",
"save_experiment",
"load_images",
"popup_camera_map_setting",
"popup_ilastik_setting",
"popup_help",
"toggle_stage_limits",
"popup_autofocus_setting",
"popup_waveform_setting",
"popup_microscope_setting",
"toggle_save",
"acquire_data",
"not_implemented",
"stage_movement",
"switch_tabs",
]
for method in desired_methods:
assert method in methods
def test_popup_camera_map_setting(self):
assert (
hasattr(
self.menu_controller.parent_controller, "camera_map_popup_controller"
)
is False
)
self.menu_controller.popup_camera_map_setting()
assert (
hasattr(
self.menu_controller.parent_controller, "camera_map_popup_controller"
)
is True
)
def test_autofocus_settings(self):
assert (
hasattr(self.menu_controller.parent_controller, "af_popup_controller")
is False
)
self.menu_controller.popup_autofocus_setting()
assert (
hasattr(self.menu_controller.parent_controller, "af_popup_controller")
is True
)
def test_popup_waveform_setting(self):
# TODO: Incomplete.
assert (
hasattr(self.menu_controller.parent_controller, "waveform_popup_controller")
is False
)
def test_popup_microscope_setting(self):
# TODO: Incomplete. DummyController has no attribute 'model'
assert (
hasattr(
self.menu_controller.parent_controller, "microscope_popup_controller"
)
is False
)
def test_toggle_save(self):
class MockWidget:
def __int__(self):
self.value = False
def set(self, value):
self.value = value
def get(self):
return self.value
channel_tab_controller = MagicMock()
self.menu_controller.parent_controller.channels_tab_controller = (
channel_tab_controller
)
channel_tab_controller.timepoint_vals = {"is_save": MockWidget()}
temp = self.menu_controller.view.settings.channels_tab.stack_timepoint_frame
temp.save_data.get = MagicMock(return_value=False)
self.menu_controller.toggle_save()
assert channel_tab_controller.timepoint_vals["is_save"].get() is True
temp = self.menu_controller.view.settings.channels_tab.stack_timepoint_frame
temp.save_data.get = MagicMock(return_value=True)
self.menu_controller.toggle_save()
assert channel_tab_controller.timepoint_vals["is_save"].get() is False
def test_stage_movement(self):
# TODO: DummyController does not have a stage controller.
pass
def test_switch_tabs(self):
for i in range(1, 4):
self.menu_controller.switch_tabs(window="left", tab=i)
assert (
self.menu_controller.parent_controller.view.settings.index("current")
== i - 1
)
@patch("src.navigate.controller.sub_controllers.menus.platform.system")
@patch("src.navigate.controller.sub_controllers.menus.subprocess.check_call")
def test_open_folder(self, mock_check_call, mock_system):
mock_system.return_value = "Darwin"
self.menu_controller.open_folder("test_path")
mock_check_call.assert_called_once_with(["open", "--", "test_path"])
mock_check_call.reset_mock()
mock_system.return_value = "Windows"
self.menu_controller.open_folder("test_path")
mock_check_call.assert_called_once_with(["explorer", "test_path"])
mock_check_call.reset_mock()
mock_system.return_value = "Linux"
self.menu_controller.open_folder("test_path")
self.assertEqual(mock_check_call.call_count, 0)
@patch("src.navigate.controller.sub_controllers.menus.os.path.join")
def test_open_log_files(self, mock_join):
with patch.object(self.menu_controller, "open_folder") as mock_open_folder:
mock_join.return_value = "joined_path"
self.menu_controller.open_log_files()
mock_open_folder.assert_called_once_with("joined_path")
@patch("src.navigate.controller.sub_controllers.menus.os.path.join")
def test_open_configuration_files(self, mock_join):
with patch.object(self.menu_controller, "open_folder") as mock_open_folder:
mock_join.return_value = "joined_path"
self.menu_controller.open_configuration_files()
mock_open_folder.assert_called_once_with("joined_path")

View File

@@ -0,0 +1,171 @@
# Copyright (c) 2021-2025 The University of Texas Southwestern Medical Center.
# All rights reserved.
# Redistribution and use in source and binary forms, with or without
# modification, are permitted for academic and research use only
# (subject to the limitations in the disclaimer below)
# provided that the following conditions are met:
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of the copyright holders nor the names of its
# contributors may be used to endorse or promote products derived from this
# software without specific prior written permission.
# NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY
# THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
# CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
# PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
# IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
# Standard library imports
# Third party imports
import pytest
import unittest
from unittest.mock import MagicMock, patch
import pandas as pd
# Local application imports
from navigate.controller.sub_controllers.multiposition import MultiPositionController
@pytest.fixture
def multiposition_controller(dummy_controller):
# Create a copy/clone of the dummy_controller to avoid side effects
isolated_controller = MagicMock()
isolated_controller.configuration = dummy_controller.configuration
# Create a mock pt attribute for the multiposition_tab
isolated_controller.view.settings.multiposition_tab.pt = MagicMock()
isolated_controller.view.settings.multiposition_tab.pt.model = MagicMock()
isolated_controller.view.settings.multiposition_tab.pt.model.df = pd.DataFrame()
# Add other required mock attributes and methods
isolated_controller.view.settings.multiposition_tab.pt.redraw = MagicMock()
isolated_controller.view.settings.multiposition_tab.pt.tableChanged = MagicMock()
isolated_controller.view.settings.multiposition_tab.pt.resetColors = MagicMock()
isolated_controller.view.settings.multiposition_tab.pt.update_rowcolors = (
MagicMock()
)
# Mock the master and tiling buttons
isolated_controller.view.settings.multiposition_tab.master = MagicMock()
isolated_controller.view.settings.multiposition_tab.master.tiling_buttons = (
MagicMock()
)
isolated_controller.view.settings.multiposition_tab.master.tiling_buttons.buttons = {
"tiling": MagicMock(),
"save_data": MagicMock(),
"load_data": MagicMock(),
"eliminate_tiles": MagicMock(),
}
# This is the important part - configure the stage axes
isolated_controller.configuration_controller = MagicMock()
isolated_controller.configuration_controller.stage_axes = [
"x",
"y",
"z",
"theta",
"f",
]
return MultiPositionController(
isolated_controller.view.settings.multiposition_tab, isolated_controller
)
@patch("navigate.controller.sub_controllers.multiposition.filedialog.askopenfilenames")
@patch("navigate.controller.sub_controllers.multiposition.yaml.safe_load")
@patch("builtins.open", new_callable=unittest.mock.mock_open, read_data="dummy content")
def test_load_positions_yaml(
mock_file, mock_safe_load, mock_askopen, multiposition_controller
):
"""Test loading positions from YAML"""
controller = multiposition_controller
table = controller.table
mock_askopen.return_value = ("dummy_file.yml",)
mock_safe_load.return_value = [
["X", "Y", "Z", "THETA", "F"],
[0, 0, 0, 0, 0],
[100, 200, 300, 400, 500],
]
controller.load_positions()
mock_file.assert_called_once_with("dummy_file.yml", "r")
expected = pd.DataFrame(
[[0, 0, 0, 0, 0], [100, 200, 300, 400, 500]],
columns=["X", "Y", "Z", "THETA", "F"],
)
pd.testing.assert_frame_equal(table.model.df, expected)
@patch("navigate.controller.sub_controllers.multiposition.filedialog.askopenfilenames")
@patch("navigate.controller.sub_controllers.multiposition.pd.read_csv")
def test_load_positions_csv(mock_read_csv, mock_askopen, multiposition_controller):
"""Test loading positions from CSV"""
controller = multiposition_controller
table = controller.table
mock_askopen.return_value = ("dummy_file.csv",)
mock_read_csv.return_value = pd.DataFrame(
{"X": [1, 2], "Y": [3, 4], "Z": [5, 6], "THETA": [0, 0], "F": [0, 0]}
)
controller.load_positions()
expected = pd.DataFrame(
{"X": [1, 2], "Y": [3, 4], "Z": [5, 6], "THETA": [0, 0], "F": [0, 0]}
)
pd.testing.assert_frame_equal(table.model.df, expected)
@patch("navigate.controller.sub_controllers.multiposition.filedialog.asksaveasfilename")
@patch("navigate.controller.sub_controllers.multiposition.save_yaml_file")
def test_export_positions_yaml(mock_save_yaml, mock_asksave, multiposition_controller):
"""Test exporting positions to YAML"""
controller = multiposition_controller
table = controller.table
table.model.df = pd.DataFrame(
{"X": [1, 2], "Y": [3, 4], "Z": [5, 6], "THETA": [0, 0], "F": [0, 0]}
)
mock_asksave.return_value = "/tmp/output.yml"
controller.export_positions()
mock_save_yaml.assert_called_once()
@patch("navigate.controller.sub_controllers.multiposition.filedialog.asksaveasfilename")
def test_export_positions_csv(mock_asksave, multiposition_controller):
"""Test exporting positions to CSV"""
controller = multiposition_controller
table = controller.table
df = pd.DataFrame(
{"X": [1, 2], "Y": [3, 4], "Z": [5, 6], "THETA": [0, 0], "F": [0, 0]}
)
table.model.df = df
table.model.df.to_csv = MagicMock()
mock_asksave.return_value = "/tmp/output.csv"
controller.export_positions()
table.model.df.to_csv.assert_called_once_with("/tmp/output.csv", index=False)

View File

@@ -0,0 +1,376 @@
# Copyright (c) 2021-2025 The University of Texas Southwestern Medical Center.
# All rights reserved.
# Redistribution and use in source and binary forms, with or without
# modification, are permitted for academic and research use only (subject to the
# limitations in the disclaimer below) provided that the following conditions are met:
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of the copyright holders nor the names of its
# contributors may be used to endorse or promote products derived from this
# software without specific prior written permission.
# NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY
# THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
# CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
# PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
# IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
import pytest
from unittest.mock import MagicMock, call
import numpy as np
AXES = ["x", "y", "z", "theta", "f"]
CAXES = ["xy", "z", "theta", "f"]
def pos_dict(v, axes=AXES):
return {k: v for k in axes}
@pytest.fixture
def stage_controller(dummy_controller):
from navigate.controller.sub_controllers.stages import StageController
dummy_controller.camera_view_controller = MagicMock()
stage_controller = StageController(
dummy_controller.view.settings.stage_control_tab,
dummy_controller,
)
dummy_controller.view.settings.stage_control_tab.focus_get = MagicMock(
return_value=True
)
return stage_controller
# test before set position variables to MagicMock()
def test_set_position(stage_controller):
widgets = stage_controller.view.get_widgets()
vals = {}
for axis in AXES:
widgets[axis].widget.trigger_focusout_validation = MagicMock()
vals[axis] = np.random.randint(0, 9)
stage_controller.view.get_widgets = MagicMock(return_value=widgets)
stage_controller.show_verbose_info = MagicMock()
position = {
"x": np.random.random(),
"y": np.random.random(),
"z": np.random.random(),
}
stage_controller.set_position(position)
for axis in position.keys():
assert float(stage_controller.widget_vals[axis].get()) == position[axis]
assert widgets[axis].widget.trigger_focusout_validation.called
assert stage_controller.stage_setting_dict[axis] == position.get(axis, 0)
stage_controller.show_verbose_info.assert_has_calls(
[call("Stage position changed"), call("Set stage position")]
)
def test_set_position_silent(stage_controller):
widgets = stage_controller.view.get_widgets()
vals = {}
for axis in AXES:
widgets[axis].widget.trigger_focusout_validation = MagicMock()
vals[axis] = np.random.randint(0, 9)
stage_controller.view.get_widgets = MagicMock(return_value=widgets)
stage_controller.show_verbose_info = MagicMock()
position = {
"x": np.random.random(),
"y": np.random.random(),
"z": np.random.random(),
}
stage_controller.set_position_silent(position)
for axis in position.keys():
assert float(stage_controller.widget_vals[axis].get()) == position[axis]
widgets[axis].widget.trigger_focusout_validation.assert_called_once()
assert stage_controller.stage_setting_dict[axis] == position.get(axis, 0)
stage_controller.show_verbose_info.assert_has_calls([call("Set stage position")])
assert (
call("Stage position changed")
not in stage_controller.show_verbose_info.mock_calls
)
@pytest.mark.parametrize(
"flip_x, flip_y",
[(False, False), (True, False), (True, True), (False, True), (True, True)],
)
def test_stage_key_press(stage_controller, flip_x, flip_y):
microscope_name = (
stage_controller.parent_controller.configuration_controller.microscope_name
)
stage_config = stage_controller.parent_controller.configuration["configuration"][
"microscopes"
][microscope_name]["stage"]
stage_config["flip_x"] = flip_x
stage_config["flip_y"] = flip_y
stage_controller.initialize()
x = round(np.random.random(), 1)
y = round(np.random.random(), 1)
increment = round(np.random.random() + 1, 1)
stage_controller.widget_vals["xy_step"].get = MagicMock(return_value=increment)
stage_controller.widget_vals["x"].get = MagicMock(return_value=x)
stage_controller.widget_vals["x"].set = MagicMock()
stage_controller.widget_vals["y"].get = MagicMock(return_value=y)
stage_controller.widget_vals["y"].set = MagicMock()
event = MagicMock()
axes_map = {"w": "y", "a": "x", "s": "y", "d": "x"}
for char, xs, ys in zip(
["w", "a", "s", "d"],
[0, -increment, 0, increment],
[increment, 0, -increment, 0],
):
event.char = char
# <a> instead of <Control+a>
event.state = 0
axis = axes_map[char]
if axis == "x":
temp = x + xs * (-1 if flip_x else 1)
else:
temp = y + ys * (-1 if flip_y else 1)
stage_controller.stage_key_press(event)
stage_controller.widget_vals[axis].set.assert_called_once_with(temp)
stage_controller.widget_vals[axis].set.reset_mock()
stage_controller.widget_vals[axis].get.reset_mock()
stage_controller.widget_vals["xy_step"].get.reset_mock()
stage_config["flip_x"] = False
stage_config["flip_y"] = False
def test_get_position(stage_controller):
import tkinter as tk
vals = {}
for axis in AXES:
vals[axis] = np.random.randint(0, 9)
stage_controller.widget_vals[axis].get = MagicMock(return_value=vals[axis])
step_vals = {}
for axis in CAXES:
step_vals[axis] = np.random.randint(1, 9)
stage_controller.widget_vals[axis + "_step"].get = MagicMock(
return_value=step_vals[axis]
)
stage_controller.position_min = pos_dict(0)
stage_controller.position_max = pos_dict(10)
position = stage_controller.get_position()
assert position == {k: vals[k] for k in AXES}
stage_controller.position_min = pos_dict(2)
vals = {}
for axis in AXES:
vals[axis] = np.random.choice(
np.concatenate((np.arange(-9, 0), np.arange(10, 20)))
)
stage_controller.widget_vals[axis].get = MagicMock(return_value=vals[axis])
position = stage_controller.get_position()
assert position is None
stage_controller.widget_vals["x"].get.side_effect = tk.TclError
position = stage_controller.get_position()
assert position is None
@pytest.mark.parametrize(
"flip_x, flip_y, flip_z",
[
(False, False, False),
(True, False, False),
(True, True, False),
(False, True, True),
(True, True, True),
],
)
def test_up_btn_handler(stage_controller, flip_x, flip_y, flip_z):
microscope_name = (
stage_controller.parent_controller.configuration_controller.microscope_name
)
stage_config = stage_controller.parent_controller.configuration["configuration"][
"microscopes"
][microscope_name]["stage"]
stage_config["flip_x"] = flip_x
stage_config["flip_y"] = flip_y
stage_config["flip_z"] = flip_z
stage_controller.initialize()
flip_flags = (
stage_controller.parent_controller.configuration_controller.stage_flip_flags
)
vals = {}
for axis in AXES:
vals[axis] = np.random.randint(1, 9)
stage_controller.widget_vals[axis].get = MagicMock(return_value=vals[axis])
stage_controller.widget_vals[axis].set = MagicMock()
step_vals = {}
for axis in CAXES:
step_vals[axis] = np.random.randint(1, 9)
stage_controller.widget_vals[axis + "_step"].get = MagicMock(
return_value=step_vals[axis]
)
stage_controller.position_max = pos_dict(10)
# Test for each axis
for axis in AXES:
pos = stage_controller.widget_vals[axis].get()
if axis == "x" or axis == "y":
step = stage_controller.widget_vals["xy_step"].get()
else:
step = stage_controller.widget_vals[axis + "_step"].get()
temp = pos + step * (-1 if flip_flags[axis] else 1)
if temp > stage_controller.position_max[axis]:
temp = stage_controller.position_max[axis]
stage_controller.up_btn_handler(axis)()
stage_controller.widget_vals[axis].set.assert_called_once_with(temp)
# Test for out of limit condition
for axis in AXES:
stage_controller.widget_vals[axis].set.reset_mock()
stage_controller.widget_vals[axis].get.return_value = 10
stage_controller.up_btn_handler(axis)()
if flip_flags[axis] is False:
stage_controller.widget_vals[axis].set.assert_not_called()
stage_config["flip_x"] = False
stage_config["flip_y"] = False
stage_config["flip_z"] = False
@pytest.mark.parametrize(
"flip_x, flip_y, flip_z",
[
(False, False, False),
(True, False, False),
(True, True, False),
(False, True, True),
(True, True, True),
],
)
def test_down_btn_handler(stage_controller, flip_x, flip_y, flip_z):
microscope_name = (
stage_controller.parent_controller.configuration_controller.microscope_name
)
stage_config = stage_controller.parent_controller.configuration["configuration"][
"microscopes"
][microscope_name]["stage"]
stage_config["flip_x"] = flip_x
stage_config["flip_y"] = flip_y
stage_config["flip_z"] = flip_z
stage_controller.initialize()
flip_flags = (
stage_controller.parent_controller.configuration_controller.stage_flip_flags
)
vals = {}
for axis in AXES:
vals[axis] = np.random.randint(1, 9)
stage_controller.widget_vals[axis].get = MagicMock(return_value=vals[axis])
stage_controller.widget_vals[axis].set = MagicMock()
step_vals = {}
for axis in CAXES:
step_vals[axis] = np.random.randint(1, 9)
stage_controller.widget_vals[axis + "_step"].get = MagicMock(
return_value=step_vals[axis]
)
stage_controller.position_min = pos_dict(0)
# Test for each axis
for axis in AXES:
pos = stage_controller.widget_vals[axis].get()
if axis == "x" or axis == "y":
step = stage_controller.widget_vals["xy_step"].get()
else:
step = stage_controller.widget_vals[axis + "_step"].get()
temp = pos - step * (-1 if flip_flags[axis] else 1)
if temp < stage_controller.position_min[axis]:
temp = stage_controller.position_min[axis]
stage_controller.down_btn_handler(axis)()
stage_controller.widget_vals[axis].set.assert_called_once_with(temp)
# Test for out of limit condition
for axis in ["x", "y", "z", "theta", "f"]:
stage_controller.widget_vals[axis].set.reset_mock()
stage_controller.widget_vals[axis].get.return_value = 0
stage_controller.down_btn_handler(axis)()
if flip_flags[axis] is False:
stage_controller.widget_vals[axis].set.assert_not_called()
stage_config["flip_x"] = False
stage_config["flip_y"] = False
stage_config["flip_z"] = False
def test_stop_button_handler(stage_controller):
stage_controller.view.after = MagicMock()
stage_controller.stop_button_handler()
stage_controller.view.after.assert_called_once()
def test_position_callback(stage_controller):
stage_controller.show_verbose_info = MagicMock()
stage_controller.view.after = MagicMock()
vals = {}
widgets = stage_controller.view.get_widgets()
for axis in AXES:
vals[axis] = np.random.randint(1, 9)
stage_controller.widget_vals[axis].get = MagicMock(return_value=vals[axis])
stage_controller.widget_vals[axis].set = MagicMock()
widgets[axis].widget.set(vals[axis])
widgets[axis].widget.trigger_focusout_validation = MagicMock()
stage_controller.position_min = pos_dict(0)
stage_controller.position_max = pos_dict(10)
stage_controller.stage_setting_dict = {}
for axis in AXES:
callback = stage_controller.position_callback(axis)
# Test case 1: Position variable is within limits
widgets[axis].widget.get = MagicMock(return_value=vals[axis])
callback()
stage_controller.view.after.assert_called()
stage_controller.view.after.reset_mock()
assert stage_controller.stage_setting_dict[axis] == vals[axis]
# Test case 2: Position variable is outside limits
widgets[axis].widget.get = MagicMock(return_value=11)
callback()
stage_controller.view.after.assert_called_once()
stage_controller.view.after.reset_mock()

View File

@@ -0,0 +1,215 @@
import random
import pytest
@pytest.fixture(scope="module")
def tiling_wizard_controller(dummy_view, dummy_controller):
from navigate.view.popups.tiling_wizard_popup2 import TilingWizardPopup
from navigate.controller.sub_controllers.tiling import (
TilingWizardController,
)
tiling_wizard = TilingWizardPopup(dummy_view)
class SubController:
def __init__(self):
self.parent_controller = dummy_controller
return TilingWizardController(tiling_wizard, SubController())
def test_traces(tiling_wizard_controller):
"""TODO: Find a way to access the actual lambda functions.
If we can, inspect.getsource(myfunc) should provide us the lambda definition.
"""
def assert_one_trace(var):
tinfo = var.trace_info()
assert len(tinfo) >= 1
assert tinfo[0][0][0] == "write"
assert "lambda" in tinfo[0][1]
for ax in ["x", "y", "z", "f"]:
# self.variables["x_start"], etc. should all be bound to two lambda functions
# calling calculate_distance() and update_fov()
for bound in ["start", "end"]:
tinfo = tiling_wizard_controller.variables[f"{ax}_{bound}"].trace_info()
assert len(tinfo) >= 1
for ti in tinfo:
assert ti[0][0] == "write"
assert "lambda" in ti[1]
# fov should be bound to one lambda, calling calculate_tiles()
assert_one_trace(tiling_wizard_controller.variables[f"{ax}_fov"])
# dist should be bound to one lambda, calling calculate_tiles()
assert_one_trace(tiling_wizard_controller.variables[f"{ax}_dist"])
# Special cases
assert_one_trace(tiling_wizard_controller.variables["percent_overlap"])
assert_one_trace(
tiling_wizard_controller.cam_settings_widgets["FOV_X"].get_variable()
)
assert_one_trace(
tiling_wizard_controller.cam_settings_widgets["FOV_Y"].get_variable()
)
assert_one_trace(
tiling_wizard_controller.stack_acq_widgets["start_position"].get_variable()
)
assert_one_trace(
tiling_wizard_controller.stack_acq_widgets["end_position"].get_variable()
)
# Channels tab controller binds these a bunch
# assert_one_trace(
# tiling_wizard_controller.stack_acq_widgets["start_focus"].get_variable()
# )
# assert_one_trace(
# tiling_wizard_controller.stack_acq_widgets["end_focus"].get_variable()
# )
def test_update_total_tiles(tiling_wizard_controller):
tiling_wizard_controller.update_total_tiles()
assert True
@pytest.mark.parametrize("axis", ["x", "y", "z", "f"])
def test_calculate_tiles(tiling_wizard_controller, axis):
from navigate.tools.multipos_table_tools import calc_num_tiles
ov, dist, fov = random.random(), random.random() * 100, random.random() * 10
tiling_wizard_controller._percent_overlap = ov * 100
tiling_wizard_controller.variables[f"{axis}_dist"].set(dist)
tiling_wizard_controller.variables[f"{axis}_fov"].set(fov)
tiling_wizard_controller.calculate_tiles(axis)
if axis == "x" or axis == "y":
dist += fov
assert int(
tiling_wizard_controller.variables[f"{axis}_tiles"].get()
) == calc_num_tiles(dist, ov, fov)
@pytest.mark.parametrize("axis", ["x", "y", "z", "f"])
def test_calculate_distance(tiling_wizard_controller, axis):
start, end = random.random() * 10, random.random() * 100
tiling_wizard_controller.variables[axis + "_start"].set(start)
tiling_wizard_controller.variables[axis + "_end"].set(end)
tiling_wizard_controller.calculate_distance(axis)
assert float(tiling_wizard_controller.variables[axis + "_dist"].get()) == abs(
start - end
)
def test_update_overlap(tiling_wizard_controller):
tiling_wizard_controller.variables["percent_overlap"].set("")
tiling_wizard_controller.update_overlap()
tiling_wizard_controller.variables["percent_overlap"].set("10")
tiling_wizard_controller.update_overlap()
assert True
@pytest.mark.parametrize("axis", ["x", "y", "z", "f"])
def test_update_fov(tiling_wizard_controller, axis):
import random
from navigate.tools.multipos_table_tools import sign
if axis == "y":
tiling_wizard_controller.cam_settings_widgets["FOV_X"].set(
int(random.random() * 1000)
)
tiling_wizard_controller.variables["x_start"].set(random.random() * 10)
tiling_wizard_controller.variables["x_end"].set(random.random() * 1000)
var = float(
tiling_wizard_controller.cam_settings_widgets["FOV_X"].get()
) * sign(
float(tiling_wizard_controller.variables["x_end"].get())
- float(tiling_wizard_controller.variables["x_start"].get())
)
elif axis == "x":
tiling_wizard_controller.cam_settings_widgets["FOV_Y"].set(
int(random.random() * 1000)
)
tiling_wizard_controller.variables["y_start"].set(random.random() * 10)
tiling_wizard_controller.variables["y_end"].set(random.random() * 1000)
var = float(
tiling_wizard_controller.cam_settings_widgets["FOV_Y"].get()
) * sign(
float(tiling_wizard_controller.variables["y_end"].get())
- float(tiling_wizard_controller.variables["y_start"].get())
)
elif axis == "z":
tiling_wizard_controller.stack_acq_widgets["start_position"].set(
random.random() * 10
)
tiling_wizard_controller.stack_acq_widgets["end_position"].set(
random.random() * 1000
)
var = float(
tiling_wizard_controller.stack_acq_widgets["end_position"].get()
) - float(tiling_wizard_controller.stack_acq_widgets["start_position"].get())
elif axis == "f":
tiling_wizard_controller.stack_acq_widgets["start_focus"].set(
random.random() * 10
)
tiling_wizard_controller.stack_acq_widgets["end_focus"].set(
random.random() * 1000
)
var = float(
tiling_wizard_controller.stack_acq_widgets["end_focus"].get()
) - float(tiling_wizard_controller.stack_acq_widgets["start_focus"].get())
tiling_wizard_controller.update_fov(axis)
assert float(tiling_wizard_controller.variables[f"{axis}_fov"].get()) == abs(var)
def test_set_table(tiling_wizard_controller):
# from navigate.tools.multipos_table_tools import compute_tiles_from_bounding_box
tiling_wizard_controller.set_table()
x_start = float(tiling_wizard_controller.variables["x_start"].get())
x_stop = float(tiling_wizard_controller.variables["x_end"].get())
y_start = float(tiling_wizard_controller.variables["y_start"].get())
y_stop = float(tiling_wizard_controller.variables["y_end"].get())
# shift z by coordinate origin of local z-stack
z_start = float(tiling_wizard_controller.variables["z_start"].get()) - float(
tiling_wizard_controller.stack_acq_widgets["start_position"].get()
)
z_stop = float(tiling_wizard_controller.variables["z_end"].get()) - float(
tiling_wizard_controller.stack_acq_widgets["end_position"].get()
)
# Default to fixed theta
r_start = tiling_wizard_controller.stage_position_vars["theta"].get()
r_stop = tiling_wizard_controller.stage_position_vars["theta"].get()
f_start = float(tiling_wizard_controller.variables["f_start"].get()) - float(
tiling_wizard_controller.stack_acq_widgets["start_focus"].get()
)
f_stop = float(tiling_wizard_controller.variables["f_end"].get()) - float(
tiling_wizard_controller.stack_acq_widgets["end_focus"].get()
)
# for consistency, always go from low to high
def sort_vars(a, b):
if a > b:
return b, a
return a, b
x_start, x_stop = sort_vars(x_start, x_stop)
y_start, y_stop = sort_vars(y_start, y_stop)
z_start, z_stop = sort_vars(z_start, z_stop)
r_start, r_stop = sort_vars(r_start, r_stop)
f_start, f_stop = sort_vars(f_start, f_stop)
assert tiling_wizard_controller.multipoint_table.model.df["X"].min() == x_start
assert tiling_wizard_controller.multipoint_table.model.df["Y"].min() == y_start
assert tiling_wizard_controller.multipoint_table.model.df["Z"].min() == z_start
assert tiling_wizard_controller.multipoint_table.model.df["THETA"].min() == r_start
assert tiling_wizard_controller.multipoint_table.model.df["F"].min() == f_start

View File

@@ -0,0 +1,178 @@
import pytest
import random
from unittest.mock import MagicMock
@pytest.fixture(scope="module")
def waveform_popup_controller(dummy_view, dummy_controller):
from navigate.controller.sub_controllers.waveform_popup import (
WaveformPopupController,
)
from navigate.view.popups.waveform_parameter_popup_window import (
WaveformParameterPopupWindow,
)
waveform_constants_popup = WaveformParameterPopupWindow(
dummy_view, dummy_controller.configuration_controller
)
return WaveformPopupController(
waveform_constants_popup,
dummy_controller,
dummy_controller.waveform_constants_path,
)
def test_populate_experiment_values(waveform_popup_controller):
exp_dict = waveform_popup_controller.parent_controller.configuration["experiment"][
"MicroscopeState"
]
resolution = exp_dict["microscope_name"]
zoom = exp_dict["zoom"]
waveform_constants = waveform_popup_controller.parent_controller.configuration[
"waveform_constants"
]
widgets = waveform_popup_controller.view.get_widgets()
def assert_widget_values():
resolution = exp_dict["microscope_name"]
zoom = exp_dict["zoom"]
assert widgets["Mode"].get() == resolution
assert widgets["Mag"].get() == zoom
# remote focus
remote_focus_dict = waveform_constants["remote_focus_constants"][resolution][
zoom
]
for k in remote_focus_dict.keys():
assert widgets[k + " Amp"].get() == remote_focus_dict[k]["amplitude"]
assert widgets[k + " Off"].get() == remote_focus_dict[k]["offset"]
# galvo
galvo_dict = waveform_constants["galvo_constants"]
for g in galvo_dict.keys():
if resolution in [galvo_dict[g].keys()]:
galvo_info = galvo_dict[g][resolution][zoom]
assert widgets[g + " Amp"].get() == galvo_info["amplitude"]
assert widgets[g + " Off"].get() == galvo_info["offset"]
# delay, fly back time, settle duraation, smoothing
assert widgets["Delay"].get() == str(
waveform_constants["other_constants"]["remote_focus_delay"]
)
assert widgets["Ramp_falling"].get() == str(
waveform_constants["other_constants"]["remote_focus_ramp_falling"]
)
assert widgets["Duty"].get() == str(
waveform_constants["other_constants"]["remote_focus_settle_duration"]
)
assert widgets["Smoothing"].get() == str(
waveform_constants["other_constants"]["percent_smoothing"]
)
# default values
waveform_popup_controller.populate_experiment_values()
assert_widget_values()
# change resolution and/or zoom
for microscope_name in waveform_constants["remote_focus_constants"].keys():
for z in waveform_constants["remote_focus_constants"][microscope_name].keys():
exp_dict["microscope_name"] = microscope_name
exp_dict["zoom"] = z
waveform_popup_controller.populate_experiment_values()
assert_widget_values()
exp_dict["microscope_name"] = resolution
exp_dict["zoom"] = zoom
waveform_popup_controller.populate_experiment_values()
assert_widget_values()
# update waveform_constants
for k in waveform_constants["remote_focus_constants"][resolution][zoom].keys():
amplitude = round(random.random() * 5, 2)
offset = round(random.random() * 5, 2)
temp = waveform_constants["remote_focus_constants"][resolution][zoom][k]
temp["amplitude"] = amplitude
temp["offset"] = offset
# update galvo
for g in waveform_constants["galvo_constants"].keys():
amplitude = round(random.random() * 5, 2)
offset = round(random.random() * 5, 2)
temp = waveform_constants["galvo_constants"][g][resolution][zoom]
temp["amplitude"] = amplitude
temp["offset"] = offset
for k in [
"remote_focus_ramp_falling",
"remote_focus_settle_duration",
"percent_smoothing",
"remote_focus_delay",
]:
waveform_constants["other_constants"][k] = round(random.random() * 100, 2)
waveform_popup_controller.populate_experiment_values(force_update=True)
assert_widget_values()
def test_show_laser_info(waveform_popup_controller):
waveform_popup_controller.show_laser_info()
assert True
def test_configure_widget_range(waveform_popup_controller):
waveform_popup_controller.configure_widget_range()
assert True
def test_estimate_galvo_setting_empty_string(waveform_popup_controller):
"""Test if the function returns without calling the camera setting controller."""
# Galvo name
galvo_name = "galvo_0"
# Mocked camera setting controller
waveform_popup_controller.parent_controller.camera_setting_controller = MagicMock()
waveform_popup_controller.parent_controller.camera_setting_controller.mode_widgets[
"Pixels"
].get = MagicMock(return_value="")
waveform_popup_controller.parent_controller.camera_setting_controller.framerate_widgets[
"exposure_time"
].get = MagicMock()
waveform_popup_controller.estimate_galvo_setting(galvo_name)
waveform_popup_controller.parent_controller.camera_setting_controller.framerate_widgets[
"exposure_time"
].get.assert_not_called()
def test_estimate_galvo_setting_with_string(waveform_popup_controller):
"""Test if the function calls the camera setting controller."""
# Galvo name
galvo_name = "galvo_0"
number_of_pixels = 50
# Mocked camera setting controller
waveform_popup_controller.parent_controller.camera_setting_controller = MagicMock()
waveform_popup_controller.parent_controller.camera_setting_controller.mode_widgets[
"Pixels"
].get = MagicMock(return_value=str(number_of_pixels))
waveform_popup_controller.parent_controller.camera_setting_controller.framerate_widgets[
"exposure_time"
].get = MagicMock()
# Mocked model
waveform_popup_controller.parent_controller.model = MagicMock()
mock_model = waveform_popup_controller.parent_controller.model
mock_model.get_camera_line_interval_and_exposure_time = MagicMock(
return_value=(0.05, 50, 500)
)
# Mocked view
waveform_popup_controller.view = MagicMock()
waveform_popup_controller.view.inputs[galvo_name].widget.set = MagicMock()
# Call the function
waveform_popup_controller.estimate_galvo_setting(galvo_name)
# Check to see what the view was called with.
waveform_popup_controller.view.inputs[galvo_name].widget.set.assert_called_once()