diff --git a/news/gui-tests.rst b/news/gui-tests.rst new file mode 100644 index 0000000..f1b5476 --- /dev/null +++ b/news/gui-tests.rst @@ -0,0 +1,23 @@ +**Added:** + +* added pytest for gui files + +**Changed:** + +* + +**Deprecated:** + +* + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +* diff --git a/src/GUI/code/GUI_CA_exp_creator.py b/src/GUI/code/GUI_CA_exp_creator.py index 3b7749d..970a548 100644 --- a/src/GUI/code/GUI_CA_exp_creator.py +++ b/src/GUI/code/GUI_CA_exp_creator.py @@ -203,6 +203,7 @@ def config_writer_CA( Parameters ------ + stp_num exp_type: string, experiment type rt: int, rest time diff --git a/src/GUI/code/GUI_config_reader.py b/src/GUI/code/GUI_config_reader.py index 0447aa9..5b7fc6e 100644 --- a/src/GUI/code/GUI_config_reader.py +++ b/src/GUI/code/GUI_config_reader.py @@ -138,6 +138,6 @@ def check_config_inputs(arg): if __name__ == "__main__": # used for debugging. Does the function load the configs? - data = parse_config_file("pytentiostat/tests/static") + data = parse_config_file("C:/Users/user/PycharmProjects/pytentiostat/tests/static/config.yml") data_out_name = data["general_parameters"]["data_output_filename"] print(data_out_name) diff --git a/src/GUI/designer_files/buffer.py b/src/GUI/designer_files/buffer.py index f998936..3edbe89 100644 --- a/src/GUI/designer_files/buffer.py +++ b/src/GUI/designer_files/buffer.py @@ -1,6 +1,12 @@ ## CA -from Adv_params_GUI import Ui_Adv_Params -from Load_config_GUI import Ui_Load +from PyQt6 import QtWidgets + +from src.GUI.code.Adv_params_GUI import Ui_Adv_Params +from src.GUI.code.CA_GUI import Ui_CA +from src.GUI.code.CV_GUI import Ui_CV +from src.GUI.code.Exp_type_GUI import Ui_Experiment +from src.GUI.code.GUI_load_config import Ui_Load +from src.GUI.code.LSV_GUI import Ui_LSV class Ui_CA(object): @@ -31,12 +37,6 @@ def AP_window(self): return self.AP, self.window -from Adv_params_GUI import Ui_Adv_Params - -## CV -from Load_config_GUI import Ui_Load # more - - class Ui_CV(object): """Initializes the 'Load config file' window. @@ -94,16 +94,6 @@ def AP_window(self): return self.AP, self.window -from CA_GUI import Ui_CA -from CV_GUI import Ui_CV -from Exp_type_GUI import Ui_Experiment -from Load_config_GUI import Ui_Load - -## main -## main -from LSV_GUI import Ui_LSV - - class Ui_MainWindow(object): def show_exp(self): """Initializes the 'Experiment Type' window. diff --git a/src/pytentiostat/main.py b/src/pytentiostat/main.py index 60badf8..6099d9b 100644 --- a/src/pytentiostat/main.py +++ b/src/pytentiostat/main.py @@ -1,4 +1,5 @@ # Pytentiostat function files +import argparse import sys import matplotlib.pyplot as plt @@ -7,6 +8,24 @@ from pytentiostat.operator import experiment from pytentiostat.reporter import save_data_to_file from pytentiostat.routines import closing_routine, startup_routine +from pytentiostat.version import __version__ + + +def parser(): + parser = argparse.ArgumentParser( + description="Pytentiostat Command Line Interface. To use, first plug " + "in the potentiostat, then run the software by typing" + "pytentiostat at the command line and follow the prompts." + "For more information, see the documentation at " + "juami.github.io/pytentiostat" + ) + parser.add_argument( + "--version", + action="store_true", + help="Show the version of pytentiostat and exit", + ) + args = parser.parse_args() + return args class BoardCom: @@ -16,77 +35,91 @@ def __init__(self): ) -try: - # Initialize Experiment - board_instance = BoardCom() - # com, board, a0, a2, d9 = startup_routine() -except KeyboardInterrupt: - sys.exit( - "Connection interrupted by user. Make sure the potentiostat " - "is plugged in then restart the software.\n" - "Exiting..." - ) +def main(): + args = parser() + if args.version: + print(f"{__version__}") + sys.exit(0) -board_objects = ( - board_instance.pin_a0, - board_instance.pin_a2, - board_instance.pin_d9, -) -parse = input("Press enter to load the config file.") -config_data = parse_config_file() -while True: - while True: - # Run the experiment and get the config_data - start = input("Press enter to start the experiment.") - times, voltages, currents, interrupt = experiment( - config_data, *board_objects + try: + # Initialize Experiment + board_instance = BoardCom() + # com, board, a0, a2, d9 = startup_routine() + except KeyboardInterrupt: + sys.exit( + "Connection interrupted by user. Make sure the potentiostat " + "is plugged in then restart the software.\n" + "Exiting..." ) - if interrupt: - save = input( - "Experiment interrupted. Would you " - "like to save the data? [y/n]: " - ) - if save.lower() == "y": - temp_data = zip(times, voltages, currents) - save_data_to_file(config_data, temp_data) - print("Saved.") - reconfig = input( - "\nIf you want to do a different experiment, edit and " - "save the config file then type" - ' "new" and press enter.\n' - 'If you need to reconnect the potentiostat, type "reconnect" ' - "then press enter.\n" - "To close, just press enter. \n" + + board_objects = ( + board_instance.pin_a0, + board_instance.pin_a2, + board_instance.pin_d9, + ) + input("Press enter to load the config file.") + config_data = parse_config_file() + while True: + while True: + # Run the experiment and get the config_data + input("Press enter to start the experiment.") + times, voltages, currents, interrupt = experiment( + config_data, *board_objects ) - if reconfig.lower() == "new": - plt.close() - config_data = parse_config_file() - elif reconfig.lower() == "reconnect": - plt.close() - closing_routine(board_instance.board, board_instance.pin_d9) - board_instance.__init__() - board_objects = ( - board_instance.pin_a0, - board_instance.pin_a2, - board_instance.pin_d9, + if interrupt: + save = input( + "Experiment interrupted. Would you " + "like to save the data? [y/n]: " ) + if save.lower() == "y": + temp_data = zip(times, voltages, currents) + save_data_to_file(config_data, temp_data) + print("Saved.") + reconfig = input( + "\nIf you want to do a different experiment, edit and " + "save the config file then type" + ' "new" and press enter.\n' + "If you need to reconnect the potentiostat, type " + '"reconnect" then press enter.\n' + "To close, just press enter. \n" + ) + if reconfig.lower() == "new": + plt.close() + config_data = parse_config_file() + elif reconfig.lower() == "reconnect": + plt.close() + closing_routine( + board_instance.board, board_instance.pin_d9 + ) + board_instance.__init__() + board_objects = ( + board_instance.pin_a0, + board_instance.pin_a2, + board_instance.pin_d9, + ) + else: + closing_routine( + board_instance.board, board_instance.pin_d9 + ) + sys.exit(0) else: - closing_routine(board_instance.board, board_instance.pin_d9) - sys.exit(0) - else: + break + + # Generate a config_data report + if not interrupt: + collected_data = zip(times, voltages, currents) + save_data_to_file(config_data, collected_data) + print("Saved.") + stop = input("\nWould you like to repeat the last experiment? [y/n]: ") + if stop.lower() != "y": break + else: + interrupt = False + plt.close() + + # Wrap things up + closing_routine(board_instance.board, board_instance.pin_d9) - # Generate a config_data report - if not interrupt: - collected_data = zip(times, voltages, currents) - save_data_to_file(config_data, collected_data) - print("Saved.") - stop = input("\nWould you like to repeat the last experiment? [y/n]: ") - if stop.lower() != "y": - break - else: - interrupt = False - plt.close() -# Wrap things up -closing_routine(board_instance.board, board_instance.pin_d9) +if __name__ == "__main__": + main() diff --git a/tests/test_adv_params_gui.py b/tests/test_adv_params_gui.py new file mode 100644 index 0000000..7f28e65 --- /dev/null +++ b/tests/test_adv_params_gui.py @@ -0,0 +1,116 @@ +import sys +from unittest.mock import MagicMock + +import pytest +from PySide6.QtWidgets import QApplication + +from src.GUI.code.Adv_params_GUI import Ui_Adv_Params + + +@pytest.fixture +def app(qtbot): + """Create a QApplication instance for testing.""" + app = QApplication(sys.argv) + yield app + app.quit() + + +@pytest.fixture +def ui(app): + """Create an instance of the UI for testing.""" + main_window = MagicMock() + ui = Ui_Adv_Params() + ui.setupUi(main_window) + return ui, main_window + + +def test_ui_initialization(ui): + """Test the initialization of the UI components.""" + ui, main_window = ui + + # Check if the main window is set up correctly + assert main_window.objectName() == "Adv_Params" + assert main_window.windowTitle() == "Adv Params" + + +def test_labels(ui): + """Test the labels in the UI.""" + ui, main_window = ui + + # Check the text of the labels + assert ui.experiment_conversion_factor_label.text() == "Conversion Factor" + assert ui.experiment_averag_number_label.text() == "Average Number" + assert ui.experiment_shunt_resistor_label.text() == "Shunt Resistor /mohm" + assert ui.advanced_parameters_label.text() == "Advanced Parameters" + assert ui.experiment_time_step_label.text() == "Time Step /s" + assert ui.experiment_setpoint_gain_label.text() == "Setpoint Gain" + assert ui.experiment_setpoint_offset_label.text() == "Setpoint Offset" + + +def test_line_edit_initialization(ui): + """Test the initialization of line edit fields.""" + ui, main_window = ui + + # Check if the line edits are initialized correctly + assert ( + ui.experiment_conversion_factor.objectName() + == "experiment_conversion_factor" + ) + assert ( + ui.experiment_setpoint_gain.objectName() == "experiment_setpoint_gain" + ) + assert ( + ui.experiment_setpoint_offset.objectName() + == "experiment_setpoint_offset" + ) + assert ( + ui.experiment_shunt_resistor.objectName() + == "experiment_shunt_resistor" + ) + assert ui.experiment_time_step.objectName() == "experiment_time_step" + assert ( + ui.experiment_averag_number.objectName() == "experiment_average_number" + ) + + +def test_button_initialization(ui): + """Test the initialization of buttons.""" + ui, main_window = ui + + # Check if the save button is initialized correctly + assert ( + ui.save_experiment_file_button.objectName() + == "save_experiment_file_button" + ) + assert ui.save_experiment_file_button.text() == "Save " + + +def test_line_edit_properties(ui): + """Test the properties of line edit fields.""" + ui, main_window = ui + + assert ui.experiment_conversion_factor.isAcceptDrops() is False + assert ui.experiment_conversion_factor.alignment() == 0x0004 # AlignCenter + assert ui.experiment_setpoint_gain.isAcceptDrops() is False + assert ui.experiment_setpoint_offset.isAcceptDrops() is False + assert ui.experiment_shunt_resistor.isAcceptDrops() is False + assert ui.experiment_time_step.isAcceptDrops() is False + assert ui.experiment_averag_number.isAcceptDrops() is False + + +def test_label_font_properties(ui): + """Test the font properties of labels.""" + ui, main_window = ui + + # Check if the labels have the correct font properties + assert ui.experiment_conversion_factor_label.font().bold() is True + assert ui.experiment_averag_number_label.font().bold() is True + assert ui.experiment_shunt_resistor_label.font().bold() is True + assert ui.advanced_parameters_label.font().bold() is True + assert ui.experiment_time_step_label.font().bold() is True + assert ui.experiment_setpoint_gain_label.font().bold() is True + assert ui.experiment_setpoint_offset_label.font().bold() is True + + +if __name__ == "__main__": + pytest.main() diff --git a/tests/test_gui_ca_exp_creator.py b/tests/test_gui_ca_exp_creator.py new file mode 100644 index 0000000..cb82873 --- /dev/null +++ b/tests/test_gui_ca_exp_creator.py @@ -0,0 +1,263 @@ +import os +import tempfile +from unittest.mock import MagicMock + +import pytest + +from src.GUI.code.GUI_CA_exp_creator import ( + CA_main, + CA_window_writer, + check, + config_writer_CA, + get_AP_parameters, + get_parameters, + load_APwindow, + load_folder, + plot, + preview_CA, + save_file, + time_converter, + write_to_file_CA, +) + +# Sample data for testing +sample_AP_params = [4.798, 1.03, 0, 0.202, 0.003, 9] +sample_exp_params = { + "experiment_file_name": "test_output", + "experiment_file_path": tempfile.gettempdir(), + "experiment_rest_time": "10", + "experiment_step_number": "5", + "experiment_voltage": "0.5", + "experiment_time": "60", +} + + +# Mocking the UI class +class MockUiCA: + def __init__(self): + self.experiment_file_path = MagicMock() + self.load_folder_name = MagicMock(return_value=tempfile.gettempdir()) + self.AP_window = MagicMock(return_value=(MagicMock(), MagicMock())) + self.experiment_queue = MagicMock() + self.plot_area = MagicMock() + self.save_experiment_file_button = MagicMock() + + +class MockUiExperiment: + def __init__(self): + self.experiment_file_name = MagicMock() + self.experiment_file_path = MagicMock() + self.experiment_rest_time = MagicMock() + self.experiment_step_number = MagicMock() + self.experiment_voltage = MagicMock() + self.experiment_time = MagicMock() + self.experiment_duration = MagicMock() + self.experiment_averag_number = MagicMock() + self.experiment_conversion_factor = MagicMock() + self.experiment_setpoint_gain = MagicMock() + self.experiment_setpoint_offset = MagicMock() + self.experiment_shunt_resistor = MagicMock() + self.experiment_time_step = MagicMock() + + +# Test for load_folder function +def test_load_folder(): + CA = MockUiCA() + load_folder(CA) + assert CA.experiment_file_path.setText.called + + +# Test for get_AP_parameters function +def test_get_AP_parameters(): + exp = MockUiExperiment() + exp.experiment_conversion_factor.text.return_value = "1.0" + exp.experiment_setpoint_gain.text.return_value = "2.0" + exp.experiment_setpoint_offset.text.return_value = "0.1" + exp.experiment_shunt_resistor.text.return_value = "0.202" + exp.experiment_time_step.text.return_value = "0.003" + exp.experiment_averag_number.text.return_value = "9" + + AP_parameters = get_AP_parameters(exp) + assert AP_parameters == [1.0, 2.0, 0.1, 0.202, 0.003, 9] + + +# Test for load_APwindow function +def test_load_APwindow(): + CA = MockUiCA() + AP_params = sample_AP_params + AP_window_mock, _ = CA.AP_window() + + load_APwindow(CA, AP_params) + + assert AP_window_mock.experiment_conversion_factor.setText.called + assert AP_window_mock.experiment_setpoint_gain.setText.called + assert AP_window_mock.experiment_setpoint_offset.setText.called + assert AP_window_mock.experiment_shunt_resistor.setText.called + assert AP_window_mock.experiment_time_step.setText.called + assert AP_window_mock.experiment_averag_number.setText.called + + # Check if the correct values were set + assert AP_window_mock.experiment_conversion_factor.setText.call_args[0][ + 0 + ] == str(AP_params[0]) + assert AP_window_mock.experiment_setpoint_gain.setText.call_args[0][ + 0 + ] == str(AP_params[1]) + assert AP_window_mock.experiment_setpoint_offset.setText.call_args[0][ + 0 + ] == str(AP_params[2]) + assert AP_window_mock.experiment_shunt_resistor.setText.call_args[0][ + 0 + ] == str(AP_params[3]) + assert AP_window_mock.experiment_time_step.setText.call_args[0][0] == str( + AP_params[4] + ) + assert AP_window_mock.experiment_averag_number.setText.call_args[0][ + 0 + ] == str(AP_params[5]) + + +# Test for time_converter function +def test_time_converter(): + assert time_converter(3661) == "1:01:01" + assert time_converter(-1) == "0:00:00" + + +# Test for preview_CA function +def test_preview_CA(): + exp = MockUiExperiment() + grid = MagicMock() + exp.experiment_step_number.text.return_value = "5" + exp.experiment_voltage.text.return_value = "0.5" + exp.experiment_time.text.return_value = "60" + + preview_CA(exp, grid) + assert exp.experiment_duration.setText.called + + +# Test for save_file function +def test_save_file(): + config_data = {"test_key": "test_value"} + with tempfile.NamedTemporaryFile(delete=False) as temp_file: + config_path_name = temp_file.name + save_file(config_path_name, config_data, 0) + + with open(config_path_name, "r") as file: + contents = file.read() + assert "#General parameters to edit for an experiment" in contents + os.remove(config_path_name) + + +# Test for config_writer_CA function +def test_config_writer_CA(): + exp_type = "CA" + rt = 10 + stp_num = 5 + data_out_name = "test_output" + data_out_path = tempfile.gettempdir() + conv_fact = 1.0 + set_gain = 2.0 + set_offset = 0.1 + sr = 0.202 + ts = 0.003 + avg_num = 9 + ca_v = 0.5 + ca_time = 60 + + config_writer_CA( + exp_type, + rt, + stp_num, + data_out_name, + data_out_path, + conv_fact, + set_gain, + set_offset, + sr, + ts, + avg_num, + ca_v, + ca_time, + ) + config_file_path = os.path.join( + data_out_path, f"{data_out_name}_{exp_type}_config.yml" + ) + assert os.path.exists(config_file_path) + os.remove(config_file_path) + + +# Test for check function +def test_check(): + assert check("123") is True + assert check("-123") is True + assert check("12.3") is True + assert check("abc") is False + assert check("") is False + + +# Test for get_parameters function +def test_get_parameters(): + exp = MockUiExperiment() + exp.experiment_file_name.text.return_value = "test_output" + exp.experiment_file_path.text.return_value = tempfile.gettempdir() + exp.experiment_rest_time.text.return_value = "10" + exp.experiment_step_number.text.return_value = "5" + exp.experiment_voltage.text.return_value = "0.5" + exp.experiment_time.text.return_value = "60" + + AP_parameters = sample_AP_params + config_params = get_parameters(exp, "CA", AP_parameters) + assert config_params[0] == "CA" + assert config_params[3] == "test_output" + + +# Test for write_to_file_CA function +def test_write_to_file_CA(): + ui = MagicMock() + CA = MockUiCA() + CA_window = MagicMock() + AP_params = sample_AP_params + + write_to_file_CA(ui, CA, CA_window, AP_params) + assert CA_window.close.called + + +# Test for CA_window_writer function +def test_CA_window_writer(): + exp = MockUiExperiment() + config_data = { + "general_parameters": { + "data_output_filename": "test_output", + "data_output_path": tempfile.gettempdir(), + "rest_time": 10, + "step_number": 5, + }, + "chronoamperometry": { + "voltage": 0.5, + "time": 60, + }, + } + + CA_window_writer(exp, config_data) + assert exp.experiment_file_name.setText.called + assert exp.experiment_file_path.setText.called + + +# Test for CA_main function +def test_CA_main(): + ui = MagicMock() + config_data = None + CA = CA_main(ui, config_data) + assert CA is not None + + +# Test for plot function +def test_plot(): + x = [0, 1, 2] + y = [0, 1, 0] + canvas = plot(x, y) + assert canvas is not None + + +if __name__ == "__main__": + pytest.main() diff --git a/tests/test_gui_config_reader.py b/tests/test_gui_config_reader.py new file mode 100644 index 0000000..503af04 --- /dev/null +++ b/tests/test_gui_config_reader.py @@ -0,0 +1,154 @@ +import os + +import pytest +import yaml + +from src.GUI.code.GUI_config_reader import ( + check_config_inputs, + get_adv_params, + get_ca_params, + get_cv_params, + get_exp_time, + get_exp_type, + get_lsv_params, + get_output_params, + get_rest, + get_steps, + parse_config_file, +) + +# Sample configuration data for testing +sample_config = { + "general_parameters": { + "data_output_filename": "test_output", + "data_output_path": "desktop", + "experiment_type": "cyclic_voltammetry", + "rest_time": 10, + "step_number": 5, + }, + "linear_sweep_voltammetry": { + "start_voltage": 0.0, + "end_voltage": 1.0, + "sweep_rate": 0.1, + }, + "chronoamperometry": { + "voltage": 0.5, + "time": 60, + }, + "cyclic_voltammetry": { + "start_voltage": -0.5, + "first_turnover_voltage": 0.0, + "second_turnover_voltage": 0.5, + "sweep_rate": 0.1, + "number_of_cycles": 3, + }, + "advanced_parameters": { + "conversion_factor": 1.0, + "setpoint_gain": 2.0, + "setpoint_offset": 0.1, + "shunt_resistor": 1000, + "time_step": 0.01, + "average_number": 10, + }, +} + + +# Create a temporary YAML file for testing +@pytest.fixture +def temp_config_file(tmp_path): + config_file = tmp_path / "config.yml" + with open(config_file, "w") as file: + yaml.dump(sample_config, file) + return config_file + + +def test_parse_config_file(temp_config_file): + config_data = parse_config_file(temp_config_file) + assert config_data == sample_config + + +def test_get_output_params(temp_config_file): + config_data = parse_config_file(temp_config_file) + out_name_ts, out_path = get_output_params(config_data) + assert out_name_ts.startswith("test_output_") + assert out_path == os.path.join(os.path.expanduser("~"), "Desktop") + + +def test_get_lsv_params(temp_config_file): + config_data = parse_config_file(temp_config_file) + start_voltage, end_voltage, sweep_rate = get_lsv_params(config_data) + assert start_voltage == 0.0 + assert end_voltage == 1.0 + assert sweep_rate == 0.1 + + +def test_get_ca_params(temp_config_file): + config_data = parse_config_file(temp_config_file) + voltage, time = get_ca_params(config_data) + assert voltage == 0.5 + assert time == 60 + + +def test_get_cv_params(temp_config_file): + config_data = parse_config_file(temp_config_file) + ( + start_voltage, + first_turnover, + second_turnover, + sweep_rate, + cycle_number, + ) = get_cv_params(config_data) + assert start_voltage == -0.5 + assert first_turnover == 0.0 + assert second_turnover == 0.5 + assert sweep_rate == 0.1 + assert cycle_number == 3 + + +def test_get_exp_type(temp_config_file): + config_data = parse_config_file(temp_config_file) + exp_type = get_exp_type(config_data) + assert exp_type == "cyclic_voltammetry" + + +def test_get_exp_time(temp_config_file): + config_data = parse_config_file(temp_config_file) + exp_time = get_exp_time(config_data) + assert exp_time == 60 + + +def test_get_rest(temp_config_file): + config_data = parse_config_file(temp_config_file) + rest_time = get_rest(config_data) + assert rest_time == 10 + + +def test_get_steps(temp_config_file): + config_data = parse_config_file(temp_config_file) + step_number = get_steps(config_data) + assert step_number == 5 + + +def test_get_adv_params(temp_config_file): + config_data = parse_config_file(temp_config_file) + ( + conversion_factor, + set_gain, + set_offset, + shunt_resistor, + time_step, + average_number, + ) = get_adv_params(config_data) + assert conversion_factor == 1.0 + assert set_gain == 2.0 + assert set_offset == 0.1 + assert shunt_resistor == 1000 + assert time_step == 0.01 + assert average_number == 10 + + +def test_check_config_inputs(): + assert check_config_inputs(10) is True + assert check_config_inputs("10") is True + assert check_config_inputs("abc") is False + assert check_config_inputs(None) is False diff --git a/tests/test_gui_cv_exp_creator.py b/tests/test_gui_cv_exp_creator.py new file mode 100644 index 0000000..9caeb53 --- /dev/null +++ b/tests/test_gui_cv_exp_creator.py @@ -0,0 +1,335 @@ +import os +import tempfile +from unittest.mock import MagicMock + +import numpy as np +import pytest + +from src.GUI.code.GUI_CV_exp_creator import ( + CV_data, + CV_main, + CV_window_writer, + check, + config_writer_CV, + get_AP_parameters, + get_parameters, + load_APwindow, + load_folder, + plot, + preview_CV, + save_file, + time_converter, + write_to_file_CV, +) + +# Sample data for testing +sample_AP_params = [4.798, 1.03, 0, 0.202, 0.003, 9] +sample_exp_params = { + "experiment_file_name": "test_output", + "experiment_file_path": tempfile.gettempdir(), + "experiment_rest_time": "10", + "experiment_step_number": "5", + "experiment_start_voltage": "0.5", + "experiment_first_vertex_voltage": "1.0", + "experiment_second_vertex_voltage": "0.0", + "experiment_sweep_rate": "0.1", + "experiment_cycle_number": "3", +} + + +# Mocking the UI class +class MockUiCV: + def __init__(self): + self.experiment_file_path = MagicMock() + self.load_folder_name = MagicMock(return_value=tempfile.gettempdir()) + self.AP_window = MagicMock(return_value=(MagicMock(), MagicMock())) + self.plot_area = MagicMock() + self.save_experiment_file_button = MagicMock() + self.experiment_queue = MagicMock() + + +class MockUiExperiment: + def __init__(self): + self.experiment_file_name = MagicMock() + self.experiment_file_path = MagicMock() + self.experiment_rest_time = MagicMock() + self.experiment_step_number = MagicMock() + self.experiment_start_voltage = MagicMock() + self.experiment_first_vertex_voltage = MagicMock() + self.experiment_second_vertex_voltage = MagicMock() + self.experiment_sweep_rate = MagicMock() + self.experiment_cycle_number = MagicMock() + self.experiment_duration = MagicMock() + self.experiment_averag_number = MagicMock() + self.experiment_conversion_factor = MagicMock() + self.experiment_setpoint_gain = MagicMock() + self.experiment_setpoint_offset = MagicMock() + self.experiment_shunt_resistor = MagicMock() + self.experiment_time_step = MagicMock() + + +# Test for load_folder function +def test_load_folder(): + CV = MockUiCV() + load_folder(CV) + assert CV.experiment_file_path.setText.called + + +# Test for get_AP_parameters function +def test_get_AP_parameters(): + exp = MockUiExperiment() + exp.experiment_conversion_factor.text.return_value = "1.0" + exp.experiment_setpoint_gain.text.return_value = "2.0" + exp.experiment_setpoint_offset.text.return_value = "0.1" + exp.experiment_shunt_resistor.text.return_value = "0.202" + exp.experiment_time_step.text.return_value = "0.003" + exp.experiment_averag_number.text.return_value = "9" + + AP_parameters = get_AP_parameters(exp) + assert AP_parameters == [1.0, 2.0, 0.1, 0.202, 0.003, 9] + + +# Test for load_APwindow function +def test_load_APwindow(): + CV = MockUiCV() + AP_params = sample_AP_params + AP_window_mock, _ = CV.AP_window() + + load_APwindow(CV, AP_params) + + assert AP_window_mock.experiment_conversion_factor.setText.called + assert AP_window_mock.experiment_setpoint_gain.setText.called + assert AP_window_mock.experiment_setpoint_offset.setText.called + assert AP_window_mock.experiment_shunt_resistor.setText.called + assert AP_window_mock.experiment_time_step.setText.called + assert AP_window_mock.experiment_averag_number.setText.called + + # Check if the correct values were set + assert AP_window_mock.experiment_conversion_factor.setText.call_args[0][ + 0 + ] == str(AP_params[0]) + assert AP_window_mock.experiment_setpoint_gain.setText.call_args[0][ + 0 + ] == str(AP_params[1]) + assert AP_window_mock.experiment_setpoint_offset.setText.call_args[0][ + 0 + ] == str(AP_params[2]) + assert AP_window_mock.experiment_shunt_resistor.setText.call_args[0][ + 0 + ] == str(AP_params[3]) + assert AP_window_mock.experiment_time_step.setText.call_args[0][0] == str( + AP_params[4] + ) + assert AP_window_mock.experiment_averag_number.setText.call_args[0][ + 0 + ] == str(AP_params[5]) + + +# Test for time_converter function +def test_time_converter(): + assert time_converter(3661) == "1:01:01" + assert time_converter(-1) == "0:00:00" + + +# Test for CV_data function +def test_CV_data(): + # Test case 1: Normal case + stp_num = 6 + cv_sv = 0.0 + cv_ftv = 1.0 + cv_stv = 0.0 + cv_sr = 100.0 + + times_step_list, steps_list = CV_data( + stp_num, cv_sv, cv_ftv, cv_stv, cv_sr + ) + + expected_length = stp_num + assert ( + len(times_step_list) - 1 == expected_length + ), f"Expected {expected_length} times steps, got {len(times_step_list)}" + assert ( + len(steps_list) - 1 == expected_length + ), f"Expected {expected_length} steps, got {len(steps_list)}" + + # Test case 2: Negative voltages + stp_num = 6 + cv_sv = -1.0 + cv_ftv = 0.0 + cv_stv = -0.5 + cv_sr = 100.0 + + times_step_list, steps_list = CV_data( + stp_num, cv_sv, cv_ftv, cv_stv, cv_sr + ) + + # Check if the function handles negative voltages correctly + assert ( + len(times_step_list) - 1 == expected_length + ), f"Expected {expected_length} times steps, got {len(times_step_list)}" + assert ( + len(steps_list) - 1 == expected_length + ), f"Expected {expected_length} steps, got {len(steps_list)}" + + # Additional checks on the values in the lists + assert np.all( + steps_list >= min(cv_sv, cv_ftv, cv_stv) + ), "Steps should not be less than the minimum voltage" + assert np.all( + steps_list <= max(cv_sv, cv_ftv, cv_stv) + ), "Steps should not be greater than the maximum voltage" + + +# Test for preview_CV function +def test_preview_CV(): + exp = MockUiExperiment() + grid = MagicMock() + exp.experiment_step_number.text.return_value = "5" + exp.experiment_start_voltage.text.return_value = "0.0" + exp.experiment_first_vertex_voltage.text.return_value = "1.0" + exp.experiment_second_vertex_voltage.text.return_value = "0.0" + exp.experiment_sweep_rate.text.return_value = "0.1" + exp.experiment_cycle_number.text.return_value = "3" + + preview_CV(exp, grid) + assert exp.experiment_duration.setText.called + + +# Test for save_file function +def test_save_file(): + config_data = {"test_key": "test_value"} + with tempfile.NamedTemporaryFile(delete=False) as temp_file: + config_path_name = temp_file.name + save_file(config_path_name, config_data, 0) + + with open(config_path_name, "r") as file: + contents = file.read() + assert "#General parameters to edit for an experiment" in contents + os.remove(config_path_name) + + +# Test for config_writer_CV function +def test_config_writer_CV(): + exp_type = "CV" + rt = 10 + stp_num = 6 + data_out_name = "test_output" + data_out_path = tempfile.gettempdir() + conv_fact = 1.0 + set_gain = 2.0 + set_offset = 0.1 + sr = 0.202 + ts = 0.003 + avg_num = 9 + cv_sv = 0.0 + cv_ftv = 1.0 + cv_stv = 0.0 + cv_sr = 0.1 + cyc_num = 3 + + config_writer_CV( + exp_type, + rt, + stp_num, + data_out_name, + data_out_path, + conv_fact, + set_gain, + set_offset, + sr, + ts, + avg_num, + cv_sv, + cv_ftv, + cv_stv, + cv_sr, + cyc_num, + ) + config_file_path = os.path.join( + data_out_path, f"{data_out_name}_{exp_type}_config.yml" + ) + assert os.path.exists(config_file_path) + os.remove(config_file_path) + + +# Test for check function +def test_check(): + assert check("123") is True + assert check("-123") is True + assert check("12.3") is True + assert check("abc") is False + assert check("") is False + + +# Test for get_parameters function +def test_get_parameters(): + exp = MockUiExperiment() + exp.experiment_file_name.text.return_value = "test_output" + exp.experiment_file_path.text.return_value = tempfile.gettempdir() + exp.experiment_rest_time.text.return_value = "10" + exp.experiment_step_number.text.return_value = "5" + exp.experiment_start_voltage.text.return_value = "0.0" + exp.experiment_first_vertex_voltage.text.return_value = "1.0" + exp.experiment_second_vertex_voltage.text.return_value = "0.0" + exp.experiment_sweep_rate.text.return_value = "0.1" + exp.experiment_cycle_number.text.return_value = "3" + + AP_parameters = sample_AP_params + config_params = get_parameters(exp, "CV", AP_parameters) + assert config_params[0] == "CV" + assert config_params[3] == "test_output" + + +# Test for write_to_file_CV function +def test_write_to_file_CV(): + ui = MagicMock() + CV = MockUiCV() + CV_window = MagicMock() + AP_params = sample_AP_params + + write_to_file_CV(ui, CV, CV_window, AP_params) + assert CV_window.close.called + + +# Test for CV_window_writer function +def test_CV_window_writer(): + exp = MockUiExperiment() + config_data = { + "general_parameters": { + "data_output_filename": "test_output", + "data_output_path": tempfile.gettempdir(), + "rest_time": 10, + "step_number": 5, + }, + "cyclic_voltammetry": { + "start_voltage": 0.0, + "first_turnover_voltage": 1.0, + "second_turnover_voltage": 0.0, + "sweep_rate": 0.1, + "number_of_cycles": 3, + }, + } + + CV_window_writer(exp, config_data) + assert exp.experiment_file_name.setText.called + assert exp.experiment_file_path.setText.called + + +# Test for CV_main function +def test_CV_main(): + ui = MagicMock() + config_data = None # or provide a sample config_data if needed + CV = CV_main(ui, config_data) + assert CV is not None + + +# Test for plot function +def test_plot(): + x = [0, 1, 2] + y = [0, 1, 0] + canvas = plot(x, y) + assert canvas is not None + + +if __name__ == "__main__": + pytest.main() diff --git a/tests/test_main.py b/tests/test_main.py new file mode 100644 index 0000000..aee77ef --- /dev/null +++ b/tests/test_main.py @@ -0,0 +1,87 @@ +import os +import sys +from unittest.mock import MagicMock, patch + +import pytest +from PySide6.QtWidgets import QApplication + +# Assuming the main.py is structured as a module +from src.GUI.code.main import disconnect_port, find_port, start_exp + +sys.path.insert( + 0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../src")) +) + + +@pytest.fixture +def app(qtbot): + """Create a QApplication instance for testing.""" + app = QApplication(sys.argv) + yield app + app.quit() + + +@pytest.fixture +def mock_ui(): + """Mock the UI components.""" + ui = MagicMock() + return ui + + +def test_find_port(app, mock_ui): + """Test the find_port function.""" + with patch( + "src.GUI.code.GUI_routines.find_port_main", + return_value=(True, (MagicMock(), None, None, None)), + ): + find_port() + assert mock_ui.find_potentiostat_button.clicked.call_count == 1 + + +def test_disconnect_port(app, mock_ui): + """Test the disconnect_port function.""" + with patch( + "src.GUI.code.GUI_routines.disconnect_port_main" + ) as mock_disconnect: + disconnect_port() + mock_disconnect.assert_called_once_with(mock_ui, None, None) + + +def test_add_experiment(app, mock_ui): + """Test the add experiment button functionality.""" + with patch("src.GUI.code.GUI_file_operation.add_exp") as mock_add_exp: + mock_ui.add_experiment_button.clicked.emit() + mock_add_exp.assert_called_once_with(mock_ui) + + +def test_load_experiment(app, mock_ui): + """Test the load experiment button functionality.""" + with patch("src.GUI.code.GUI_file_operation.load_file") as mock_load_file: + mock_ui.load_experiment_button.clicked.emit() + mock_load_file.assert_called_once_with(mock_ui) + + +def test_edit_experiment(app, mock_ui): + """Test the edit experiment button functionality.""" + with patch("src.GUI.code.GUI_file_operation.edit_file") as mock_edit_file: + mock_ui.edit_experiment_button.clicked.emit() + mock_edit_file.assert_called_once_with(mock_ui) + + +def test_remove_experiment(app, mock_ui): + """Test the remove experiment button functionality.""" + with patch( + "src.GUI.code.GUI_file_operation.remove_file" + ) as mock_remove_file: + mock_ui.remove_experiment_button.clicked.emit() + mock_remove_file.assert_called_once_with(mock_ui) + + +def test_start_exp(app, mock_ui): + """Test the start experiment functionality.""" + grid = MagicMock() + with patch("src.GUI.code.GUI_run_exp.run_exp_main") as mock_run_exp: + start_exp(grid) + mock_run_exp.assert_called_once_with( + mock_ui, None, (None, None, None, None), grid + )