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

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()

View File

@@ -0,0 +1,787 @@
from pathlib import Path
from types import SimpleNamespace
from unittest.mock import MagicMock, ANY
import pytest
import numpy
import multiprocessing as mp
import logging
import platform
from logging.handlers import QueueHandler
from navigate.log_files.log_functions import log_setup
class _NullQueue:
"""Minimal queue-like sink for logging; avoids mp feeder threads on Windows."""
def put(self, _): # QueueHandler calls .put()
pass
def close(self):
pass
def cancel_join_thread(self):
pass
class DummySplashScreen:
def destroy(self):
pass
def _normalize_log_setup(start_listener):
"""Call log_setup and normalize its return to (log_queue, log_listener)."""
from navigate.log_files.log_functions import log_setup
try:
res = log_setup("logging.yml", logging_path=None, start_listener=start_listener)
except Exception:
res = None
# Accept (queue, listener), or queue-only, or None.
if isinstance(res, tuple) and len(res) == 2:
return res[0], res[1]
if res is not None and hasattr(res, "put"):
return res, None
# Fallbacks:
if platform.system() == "Windows":
# Avoid mp.Queue on Windows to prevent hangs in CI.
return _NullQueue(), None
# Non-Windows: a real mp.Queue is fine without a listener.
try:
return mp.Queue(), None
except Exception:
return _NullQueue(), None
def _remove_queue_handlers(target_queue=None):
# Detach and close any QueueHandler that targets target_queue.
def strip_handlers(logger):
for h in list(getattr(logger, "handlers", [])):
if isinstance(h, QueueHandler) and (
target_queue is None or getattr(h, "queue", None) is target_queue
):
try:
logger.removeHandler(h)
except Exception:
pass
try:
h.close()
except Exception:
pass
try:
strip_handlers(logging.getLogger())
for name, obj in logging.root.manager.loggerDict.items():
if isinstance(obj, logging.Logger):
strip_handlers(logging.getLogger(name))
except Exception:
pass
@pytest.fixture(scope="module")
def controller(tk_root):
from navigate.controller.controller import Controller
base_directory = Path.joinpath(
Path(__file__).resolve().parent.parent.parent, "src", "navigate"
)
configuration_directory = Path.joinpath(base_directory, "config")
configuration_path = Path.joinpath(configuration_directory, "configuration.yaml")
experiment_path = Path.joinpath(configuration_directory, "experiment.yml")
waveform_constants_path = Path.joinpath(
configuration_directory, "waveform_constants.yml"
)
rest_api_path = Path.joinpath(configuration_directory, "rest_api_config.yml")
waveform_templates_path = Path.joinpath(
configuration_directory, "waveform_templates.yml"
)
gui_configuration_path = Path.joinpath(
configuration_directory, "gui_configuration.yml"
)
multi_positions_path = Path.joinpath(configuration_directory, "multi_positions.yml")
args = SimpleNamespace(synthetic_hardware=True)
start_listener = platform.system() != "Windows"
log_queue, log_listener = _normalize_log_setup(start_listener)
controller = Controller(
root=tk_root,
splash_screen=DummySplashScreen(),
configuration_path=configuration_path,
experiment_path=experiment_path,
waveform_constants_path=waveform_constants_path,
rest_api_path=rest_api_path,
waveform_templates_path=waveform_templates_path,
gui_configuration_path=gui_configuration_path,
multi_positions_path=multi_positions_path,
log_queue=log_queue,
args=args,
)
# To make sure the testcases won't hang on because of the model.event_queue
# The changes here won't affect other testcases,
# because the testcases from other files use DummyController
# and DummyModel instead of this controller fixture
controller.model = MagicMock()
controller.threads_pool = MagicMock()
controller.model.get_offset_variance_maps.return_value = (None, None)
yield controller
try:
controller.execute("exit")
except SystemExit:
pass
# Tear down the controller properly
q = getattr(controller, "event_queue", None)
if q is not None:
try:
q.close()
except Exception:
pass
try:
q.cancel_join_thread()
except Exception:
pass
# Close any Pipes
if getattr(controller, "show_img_pipe", None):
try:
controller.show_img_pipe.close()
except Exception:
pass
try:
controller.manager.shutdown()
except Exception:
pass
# Detach QueueHandlers first so no more puts go to log_queue
_remove_queue_handlers(log_queue)
# Stop the queue listener (only if started)
try:
if start_listener and log_listener:
try:
log_listener.enqueue_sentinel()
except Exception:
pass
try:
log_listener.stop()
except Exception:
pass
except Exception:
pass
# Close the logging queue and skip join on Windows
if platform.system() == "Windows":
try:
log_queue.close()
except Exception:
pass
try:
log_queue.cancel_join_thread()
except Exception:
pass
logging.shutdown()
# As a last resort on Windows, hard-terminate any alive mp children
if platform.system() == "Windows":
try:
children = list(mp.active_children())
for p in children:
try:
p.terminate()
except Exception:
pass
for p in children:
try:
p.join(timeout=5)
except Exception:
pass
except Exception:
pass
def test_update_buffer(controller):
camera_parameters = controller.configuration["experiment"]["CameraParameters"]
controller.update_buffer()
assert controller.img_width == camera_parameters["img_x_pixels"]
assert controller.img_height == camera_parameters["img_y_pixels"]
# Make sure that the get_data_buffer method is not called.
assert controller.model.get_data_buffer.called is False
# Change the buffer size
microscope_name = controller.configuration["experiment"]["MicroscopeState"][
"microscope_name"
]
controller.configuration["experiment"]["CameraParameters"][microscope_name][
"img_x_pixels"
] = 100
controller.configuration["experiment"]["CameraParameters"][microscope_name][
"img_y_pixels"
] = 100
controller.update_buffer()
# Make sure that the get_data_buffer method is called.
assert controller.model.get_data_buffer.called is True
# Confirm that the buffer size has been updated.
assert controller.img_width == 100
assert controller.img_height == 100
assert True
def test_change_microscope(controller):
# Get the microscopes from the configuration file
microscopes = controller.configuration["configuration"]["microscopes"]
# Iterate through the microscopes and change the microscope
for microscope_name in microscopes.keys():
# Patch the configuration_controller
controller.configuration_controller.change_microscope = MagicMock()
# Default zoom is '0.63x'
zoom = microscopes[microscope_name]["zoom"]["position"].keys()[0]
controller.configuration["experiment"]["MicroscopeState"]["zoom"] = zoom
# Change the microscope without passing the zoom
controller.change_microscope(microscope_name)
assert (
controller.configuration["experiment"]["MicroscopeState"]["microscope_name"]
== microscope_name
)
# Confirm that the zoom has not changed.
assert controller.configuration["experiment"]["MicroscopeState"]["zoom"] == zoom
# Call it and pass the zoom
zoom = microscopes[microscope_name]["zoom"]["position"].keys()[-1]
controller.change_microscope(microscope_name, zoom)
assert controller.configuration["experiment"]["MicroscopeState"]["zoom"] == zoom
# Make sure that the configuration_controller has been called.
assert controller.configuration_controller.change_microscope.called is True
# Have configuration controller return False
controller.configuration_controller.change_microscope.return_value = False
# Patch the stage controller, channels_tab_controller...
controller.stage_controller.initialize = MagicMock()
controller.channels_tab_controller.initialize = MagicMock()
camera_setting_controller = controller.camera_setting_controller
camera_setting_controller.update_camera_device_related_setting = MagicMock()
camera_setting_controller.calculate_physical_dimensions = MagicMock()
controller.camera_view_controller.update_snr = MagicMock()
# Call change microscope, assert patched methods are not called
controller.change_microscope(microscope_name)
assert controller.stage_controller.initialize.called is False
assert controller.channels_tab_controller.initialize.called is False
assert (
camera_setting_controller.update_camera_device_related_setting.called
is False
)
assert camera_setting_controller.calculate_physical_dimensions.called is False
assert controller.camera_view_controller.update_snr.called is False
# Have configuration controller return True
controller.configuration_controller.change_microscope.return_value = True
# Call change microscope, assert patched methods are called
controller.change_microscope(microscope_name)
assert controller.stage_controller.initialize.called is True
assert controller.channels_tab_controller.initialize.called is True
assert (
camera_setting_controller.update_camera_device_related_setting.called
is True
)
assert camera_setting_controller.calculate_physical_dimensions.called is True
assert controller.camera_view_controller.update_snr.called is True
# Test waveform popup controller.
controller.waveform_popup_controller = MagicMock()
controller.change_microscope(microscope_name)
assert (
controller.waveform_popup_controller.populate_experiment_values.called
is True
)
assert True
def test_initialize_cam_view(controller):
minmax_values = [0, 2**16 - 1]
image_metrics = [1, 0, 0]
controller.initialize_cam_view()
assert (
controller.camera_view_controller.image_palette["Min"].get() == minmax_values[0]
)
assert (
controller.camera_view_controller.image_palette["Max"].get() == minmax_values[1]
)
assert (
controller.camera_view_controller.image_metrics["Frames"].get()
== image_metrics[0]
)
assert True
def test_populate_experiment_setting(controller):
controller.populate_experiment_setting(in_initialize=False)
assert True
def test_prepare_acquire_data(controller):
# Test without warning message
controller.set_mode_of_sub = MagicMock()
assert controller.prepare_acquire_data() is True
assert controller.set_mode_of_sub.called is True
# Test with warning message. Challenging since controller is local.
# with patch('controller.tkinter.messagebox.showerror') as mock_showerror:
# controller.update_experiment_setting.return_value = "Warning!"
# assert controller.prepare_acquire_data() is False
# mock_showerror.assert_called_once()
assert True
def test_set_mode_of_sub(controller):
modes = ["customized", "stop", "live"]
for mode in modes:
controller.set_mode_of_sub(mode=mode)
assert True
def test_execute_stop_stage(controller):
controller.threads_pool.createThread = MagicMock()
controller.execute(command="stop_stage")
assert controller.threads_pool.createThread.called is True
assert True
def test_execute_move_stage_and_update_info(controller):
positions = {"x": 51, "y": 52.0, "z": -530.3, "theta": 1, "f": 0}
controller.execute("move_stage_and_update_info", positions)
for axis, value in positions.items():
assert (
float(controller.stage_controller.widget_vals[axis].get())
== positions[axis]
)
assert True
def test_execute_move_stage_and_acquire_image(controller):
positions = {"x": 51, "y": 52.0, "z": -530.3, "theta": 1, "f": 0}
controller.model.move_stage = MagicMock()
controller.threads_pool.createThread = MagicMock()
controller.execute("move_stage_and_acquire_image", positions)
assert controller.model.move_stage.called is True
for axis, value in positions.items():
assert (
float(controller.stage_controller.widget_vals[axis].get())
== positions[axis]
)
assert controller.threads_pool.createThread.called is True
assert True
def test_execute_get_stage_position(controller):
# Set the positions in the GUI
set_positions = {"x": 51, "y": 52.0, "z": -530.3, "theta": 1, "f": 0}
controller.execute("move_stage_and_update_info", set_positions)
# Get the positions from the GUI
get_positions = controller.execute("get_stage_position")
assert type(get_positions) is dict
axes = ["x", "y", "z", "theta", "f"]
for axis in axes:
assert axis in get_positions.keys()
# assert that get_positions is equal to set_positions
assert get_positions == set_positions
assert True
def test_execute_mark_position(controller):
set_positions = {"x": 51, "y": 52.0, "z": -530.3, "theta": 1, "f": 0}
controller.execute("mark_position", set_positions)
# Get the positions from the multiposition table. Returns a list of lists.
get_positions = controller.multiposition_tab_controller.get_positions()
# Assert that the last position in get_positions is equal to set_positions
assert get_positions[-1] == [
set_positions["x"],
set_positions["y"],
set_positions["z"],
set_positions["theta"],
set_positions["f"],
]
assert True
def test_execute_resolution(controller):
pass
def test_execute_set_save(controller):
for save_data in [True, False]:
controller.execute("set_save", save_data)
assert controller.acquire_bar_controller.is_save == save_data
assert (
controller.configuration["experiment"]["MicroscopeState"]["is_save"]
== save_data
)
assert True
def test_execute_update_setting(controller):
controller.threads_pool.createThread = MagicMock()
args = ["resolution", {"resolution_mode", "1x"}]
controller.execute("update_setting", args)
assert controller.threads_pool.createThread.called is True
assert True
def test_execute_stage_limits(controller):
controller.threads_pool.createThread = MagicMock()
for stage_limits in [True, False]:
controller.threads_pool.createThread.reset_mock()
controller.execute("stage_limits", stage_limits)
assert controller.stage_controller.stage_limits == stage_limits
assert controller.threads_pool.createThread.called is True
assert True
def test_execute_autofocus(controller):
# Create mock objects
controller.threads_pool.createThread = MagicMock()
# Test non-acquiring case
controller.acquire_bar_controller.is_acquiring = False
controller.execute("autofocus")
controller.threads_pool.createThread.assert_called_with(
resourceName="camera",
target=controller.capture_image,
args=("autofocus", "live"),
)
# Test the acquiring case
controller.acquire_bar_controller.mode = "live"
controller.acquire_bar_controller.is_acquiring = True
controller.threads_pool.createThread.reset_mock()
controller.execute("autofocus")
controller.threads_pool.createThread.assert_called_once()
controller.threads_pool.createThread.assert_any_call(
resourceName="model", target=ANY
)
args, kwargs = controller.threads_pool.createThread.call_args
assert kwargs["resourceName"] == "model"
# Confirm that the lambda is callable.
assert callable(kwargs["target"])
assert True
def test_execute_eliminate_tiles(controller):
controller.threads_pool.createThread = MagicMock()
# Populate Feature List
controller.menu_controller.feature_list_names = ["Remove Empty Tiles"]
# Set the mode to live
controller.acquire_bar_controller.set_mode("live")
# Execute the command
controller.execute("eliminate_tiles")
assert controller.acquire_bar_controller.get_mode() == "customized"
# Assert that the thread pool is called
assert controller.threads_pool.createThread.called is True
assert True
def test_execute_load_features(controller):
controller.threads_pool.createThread = MagicMock()
controller.execute("load_features")
controller.threads_pool.createThread.assert_any_call("model", ANY)
args, kwargs = controller.threads_pool.createThread.call_args
assert args[0] == "model"
assert callable(args[1])
assert True
def test_execute_acquire_and_save_return(controller):
# Prepare mock objects.
controller.prepare_acquire_data = MagicMock()
controller.acquire_bar_controller.stop_acquire = MagicMock()
controller.camera_setting_controller.calculate_physical_dimensions = MagicMock()
# Prepare mock returns
controller.prepare_acquire_data.return_value = False
# Test and make sure return is called
controller.execute("acquire_and_save")
assert controller.acquire_bar_controller.stop_acquire.called is True
assert (
controller.camera_setting_controller.calculate_physical_dimensions.called
is False
)
assert True
def test_execute_acquire_and_acquire_and_save(controller):
# The modes "customized" & "live" results in the thread not being called.
# TODO: Figure out why the thread is not being called.
# controller.plugin_acquisition_modes = {}
# controller.threads_pool.createThread = MagicMock()
#
# for statement in ["acquire", "acquire_and_save"]:
# for mode in ["z-stack", "single"]:
# controller.acquire_bar_controller.mode = mode
# controller.execute(statement)
# controller.threads_pool.createThread.assert_called_with(
# "camera",
# controller.capture_image,
# args=(
# "acquire",
# controller.acquire_bar_controller.mode,
# ),
# )
# controller.stop_acquisition_flag = True
# controller.threads_pool.createThread.reset_mock()
pass
def test_execute_stop_acquire(controller):
# TODO: Currently hangs indefinitely.
# Prepare mock objects.
# controller.show_img_pipe.poll = MagicMock()
# controller.show_img_pipe.recv = MagicMock()
# controller.sloppy_stop = MagicMock()
# controller.threads_pool.createThread = MagicMock()
#
# # Prepare mock returns
# controller.show_img_pipe.poll.return_value = True
#
# # Test and make sure return is called
# controller.stop_acquisition_flag = False
# controller.execute("acquire", "continuous")
# controller.execute("stop_acquire")
pass
def test_execute_exit(controller):
# Essentially already tested by teardown of controller fixture.
pass
def test_execute_adaptive_optics(controller):
controller.threads_pool.createThread = MagicMock()
for command in [
"flatten_mirror",
"zero_mirror",
"set_mirror",
"set_mirror_from_wcs",
]:
controller.execute(command)
controller.threads_pool.createThread.assert_called_with("model", ANY)
args, kwargs = controller.threads_pool.createThread.call_args
assert args[0] == "model"
assert callable(args[1])
controller.execute("tony_wilson")
controller.threads_pool.createThread.assert_called_with(
"camera", controller.capture_image, args=("tony_wilson", "live")
)
def test_execute_random(controller):
controller.threads_pool.createThread = MagicMock()
for command in ["random1", "random2"]:
controller.execute(command)
controller.threads_pool.createThread.assert_called_with("model", ANY)
args, kwargs = controller.threads_pool.createThread.call_args
assert args[0] == "model"
assert callable(args[1])
assert True
def test_capture_image(controller):
count = 0
def get_image_id():
nonlocal count
count += 1
if count >= 10:
return "stop"
return numpy.random.randint(0, 10)
microscope_name = controller.configuration["experiment"]["MicroscopeState"][
"microscope_name"
]
width = controller.configuration["experiment"]["CameraParameters"][microscope_name][
"img_x_pixels"
]
height = controller.configuration["experiment"]["CameraParameters"][
microscope_name
]["img_y_pixels"]
images = numpy.random.rand(10, width, height)
controller.data_buffer = images
work_thread = MagicMock()
work_thread.join = MagicMock()
controller.threads_pool.createThread = MagicMock()
controller.threads_pool.createThread.return_value = work_thread
controller.show_img_pipe.recv = get_image_id
controller.show_img_pipe.poll = MagicMock()
controller.show_img_pipe.poll.return_value = False
# Deal with stop_acquire
controller.sloppy_stop = MagicMock()
controller.menu_controller.feature_id_val.set = MagicMock()
# Deal with camera view controller trying to launch a thread
controller.camera_view_controller.is_displaying_image = MagicMock()
controller.camera_view_controller.is_displaying_image.return_value = True
for command in ["acquire"]: # "autofocus"
for mode in ["continuous", "live", "z-stack", "single"]:
controller.capture_image(command, mode)
# Evaluate calls
controller.threads_pool.createThread.assert_called_with("model", ANY)
args, kwargs = controller.threads_pool.createThread.call_args
assert args[0] == "model"
assert callable(args[1])
controller.stop_acquisition_flag = True
controller.threads_pool.createThread.reset_mock()
assert controller.acquire_bar_controller.framerate != 0
assert controller.camera_setting_controller.framerate_widgets[
"max_framerate"
].get() != str(0)
assert True
def test_launch_additional_microscope():
# This looks awful to test...
pass
def test_move_stage(controller):
pos_dict = {"x": 1, "y": 2.0, "z": 3.14, "theta": 400, "f": 5.01}
controller.model.move_stage = MagicMock()
controller.move_stage(pos_dict)
controller.model.move_stage.assert_called_with(pos_dict)
def test_stop_stage(controller):
controller.model.stop_stage = MagicMock()
controller.stop_stage()
controller.model.stop_stage.assert_called_with()
def test_update_stage_controller_silent(controller):
pos_dict = {"x": 1, "y": 2.0, "z": 3.14, "theta": 400, "f": 5.01}
controller.update_stage_controller_silent(pos_dict)
for axis, value in pos_dict.items():
assert (
float(controller.stage_controller.widget_vals[axis].get()) == pos_dict[axis]
)
@pytest.mark.parametrize(
"acquisition_mode, sensor_mode, readout_direction, template_name, "
"expected_template_name",
[
("live", "Normal", "", "Bidirectional", "Default"),
("z-stack", "Normal", "", "Bidirectional", "Default"),
("customized", "Normal", "", "Bidirectional", "Bidirectional"),
("live", "Light-Sheet", "Top-To-Bottom", "Bidirectional", "Default"),
("live", "Light-Sheet", "Bidirectional", "Bidirectional", "Bidirectional"),
(
"customized",
"Light-Sheet",
"Bidirectional",
"Bidirectional",
"Bidirectional",
),
("z-stack", "Light-Sheet", "Bidirectional", "Default", "Bidirectional"),
("z-stack", "Light-Sheet", "Top-To-Bottom", "Default", "Default"),
],
)
def test_waveform_template(
controller,
acquisition_mode,
sensor_mode,
readout_direction,
template_name,
expected_template_name,
):
controller.configuration["experiment"]["MicroscopeState"][
"waveform_template"
] = template_name
controller.configuration["experiment"]["MicroscopeState"][
"image_mode"
] = acquisition_mode
microscope_name = controller.configuration["experiment"]["MicroscopeState"][
"microscope_name"
]
controller.configuration["experiment"]["CameraParameters"][microscope_name][
"number_of_pixels"
] = 10
controller.populate_experiment_setting(in_initialize=True)
controller.camera_setting_controller.mode_widgets["Readout"].set(readout_direction)
controller.camera_setting_controller.mode_widgets["Sensor"].set(sensor_mode)
controller.update_experiment_setting()
assert (
controller.configuration["experiment"]["MicroscopeState"]["waveform_template"]
== expected_template_name
)
assert True