From 5c63a0e87d07d78c2656ec116815fd09e443ccf1 Mon Sep 17 00:00:00 2001 From: "Dr.-Ing. Amilcar do Carmo Lucas" Date: Thu, 16 Jan 2025 04:03:48 +0100 Subject: [PATCH] IMPROVEMENT: Add more tests --- .github/workflows/unit-tests.yml | 7 +- tests/test__main__.py | 67 ++++++++++ tests/test_ardupilot_methodic_configurator.py | 35 ------ tests/test_argparse_check_range.py | 91 ++++++++++++++ ...test_frontend_tkinter_template_overview.py | 118 ++++++++++++++++++ tests/test_internationalization.py | 48 +++++++ 6 files changed, 330 insertions(+), 36 deletions(-) create mode 100755 tests/test__main__.py delete mode 100755 tests/test_ardupilot_methodic_configurator.py create mode 100755 tests/test_argparse_check_range.py create mode 100755 tests/test_frontend_tkinter_template_overview.py create mode 100755 tests/test_internationalization.py diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index f3745d0..992e5e2 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -55,8 +55,13 @@ jobs: run: | cd tests python -m coverage run -m unittest test_annotate_params.py - python -m coverage run -m unittest test_ardupilot_methodic_configurator.py + python -m coverage run -m unittest test_argparse_check_range.py python -m coverage run -m unittest test_backend_filesystem.py + python -m coverage run -m unittest test_battery_cell_voltages.py + python -m coverage run -m unittest test_common_arguments.py python -m coverage run -m unittest test_extract_param_defaults.py + python -m coverage run -m unittest test_internationalization.py + python -m coverage run -m unittest test_middleware_template_overview.py python -m coverage run -m unittest test_param_pid_adjustment_update.py + python -m coverage run -m unittest version_test.py python -m coverage html diff --git a/tests/test__main__.py b/tests/test__main__.py new file mode 100755 index 0000000..c226049 --- /dev/null +++ b/tests/test__main__.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python3 + +""" +Tests for the __main__.py file. + +This file is part of Ardupilot methodic configurator. https://github.com/ArduPilot/MethodicConfigurator + +SPDX-FileCopyrightText: 2024 Amilcar do Carmo Lucas + +SPDX-License-Identifier: GPL-3.0-or-later +""" + +import argparse +import unittest +from unittest.mock import MagicMock, patch + +from ardupilot_methodic_configurator.__main__ import argument_parser, component_editor, connect_to_fc_and_read_parameters + + +class TestArgumentParser(unittest.TestCase): # pylint: disable=missing-class-docstring + @patch( + "argparse.ArgumentParser.parse_args", return_value=argparse.Namespace(conn="tcp:127.0.0.1:5760", params="params_dir") + ) + def test_argument_parser(self, mock_args) -> None: + args = argument_parser() + assert args.conn == "tcp:127.0.0.1:5760" + assert args.params == "params_dir" + + +class TestMainFunctions(unittest.TestCase): + @patch("ardupilot_methodic_configurator.__main__.FlightController") + def test_connect_to_fc_and_read_parameters(self, mock_flight_controller) -> None: + mock_fc = mock_flight_controller.return_value + mock_fc.connect.return_value = "" + mock_fc.info.vehicle_type = "quad" + mock_fc.info.flight_sw_version_and_type = "v1.0" + mock_fc.info.vendor = "vendor" + mock_fc.info.firmware_type = "type" + + args = argparse.Namespace(device="test_device", vehicle_type="", reboot_time=5) + flight_controller, vehicle_type = connect_to_fc_and_read_parameters(args) + assert flight_controller == mock_fc + assert vehicle_type == "quad" + + @patch("ardupilot_methodic_configurator.__main__.ComponentEditorWindow") + @patch("ardupilot_methodic_configurator.__main__.LocalFilesystem") + @patch("ardupilot_methodic_configurator.__main__.ProgramSettings") + def test_component_editor(self, mock_program_settings, mock_local_filesystem, mock_component_editor_window) -> None: + mock_program_settings = mock_program_settings.return_value + mock_program_settings.get_setting.return_value = True + + mock_local_filesystem = mock_local_filesystem.return_value + mock_local_filesystem.doc_dict = {} + + mock_fc = MagicMock() + mock_fc.fc_parameters = {"param1": "value1"} + mock_fc.info.flight_sw_version_and_type = "v1.0" + mock_fc.info.vendor = "vendor" + mock_fc.info.firmware_type = "type" + + args = argparse.Namespace(skip_component_editor=False) + component_editor(args, mock_fc, "quad", mock_local_filesystem, None) + mock_component_editor_window.assert_called_once() + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_ardupilot_methodic_configurator.py b/tests/test_ardupilot_methodic_configurator.py deleted file mode 100755 index a34530c..0000000 --- a/tests/test_ardupilot_methodic_configurator.py +++ /dev/null @@ -1,35 +0,0 @@ -#!/usr/bin/env python3 - -""" -Tests for the __main__.py file. - -This file is part of Ardupilot methodic configurator. https://github.com/ArduPilot/MethodicConfigurator - -SPDX-FileCopyrightText: 2024 Amilcar do Carmo Lucas - -SPDX-License-Identifier: GPL-3.0-or-later -""" - -# pylint: skip-file - -import argparse -import unittest -from unittest.mock import patch - -# from unittest.mock import MagicMock -# from unittest.mock import mock_open -from ardupilot_methodic_configurator.__main__ import argument_parser - - -class TestArgumentParser(unittest.TestCase): # pylint: disable=missing-class-docstring - @patch( - "argparse.ArgumentParser.parse_args", return_value=argparse.Namespace(conn="tcp:127.0.0.1:5760", params="params_dir") - ) - def test_argument_parser(self, mock_args) -> None: - args = argument_parser() - assert args.conn == "tcp:127.0.0.1:5760" - assert args.params == "params_dir" - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/test_argparse_check_range.py b/tests/test_argparse_check_range.py new file mode 100755 index 0000000..dafa5a7 --- /dev/null +++ b/tests/test_argparse_check_range.py @@ -0,0 +1,91 @@ +#!/usr/bin/python3 + +""" +Tests for the argparse_check_range.py file. + +This file is part of Ardupilot methodic configurator. https://github.com/ArduPilot/MethodicConfigurator + +SPDX-FileCopyrightText: 2024-2025 Amilcar do Carmo Lucas + +SPDX-License-Identifier: GPL-3.0-or-later +""" + +import unittest +from argparse import ArgumentParser + +import pytest + +from ardupilot_methodic_configurator.argparse_check_range import CheckRange + + +class TestCheckRange(unittest.TestCase): + def setUp(self) -> None: + self.parser = ArgumentParser() + + def test_init_with_both_min_and_inf(self) -> None: + with pytest.raises(ValueError, match="either min or inf, but not both"): + self.parser.add_argument("--test", action=CheckRange, min=0, inf=0) + + def test_init_with_both_max_and_sup(self) -> None: + with pytest.raises(ValueError, match="either max or sup, but not both"): + self.parser.add_argument("--test", action=CheckRange, max=10, sup=10) + + def test_interval_with_min_and_max(self) -> None: + action = CheckRange(option_strings=["--test"], dest="test", min=0, max=10) + assert action.interval() == "valid range: [0, 10]" + + def test_interval_with_inf_and_sup(self) -> None: + action = CheckRange(option_strings=["--test"], dest="test", inf=0, sup=10) + assert action.interval() == "valid range: (0, 10)" + + def test_interval_with_no_bounds(self) -> None: + action = CheckRange(option_strings=["--test"], dest="test") + assert action.interval() == "valid range: (-infinity, +infinity)" + + def test_call_with_non_number_value(self) -> None: + self.parser.add_argument("--test", action=CheckRange, min=0, max=10) + with pytest.raises(SystemExit) as excinfo: + self.parser.parse_args(["--test", "non-number"]) + assert str(excinfo.value) == "2" + + def test_call_with_value_out_of_range(self) -> None: + self.parser.add_argument("--test", action=CheckRange, min=0, max=10) + with pytest.raises(SystemExit) as excinfo: + self.parser.parse_args(["--test", "11"]) + assert str(excinfo.value) == "2" + + def test_call_with_value_in_range(self) -> None: + self.parser.add_argument("--test", action=CheckRange, min=0, max=10) + args = self.parser.parse_args(["--test", "5"]) + assert args.test == 5 + + def test_call_with_value_equal_to_min(self) -> None: + self.parser.add_argument("--test", action=CheckRange, min=0, max=10) + args = self.parser.parse_args(["--test", "0"]) + assert args.test == 0 + + def test_call_with_value_equal_to_max(self) -> None: + self.parser.add_argument("--test", action=CheckRange, min=0, max=10) + args = self.parser.parse_args(["--test", "10"]) + assert args.test == 10 + + def test_call_with_value_equal_to_inf(self) -> None: + self.parser.add_argument("--test", action=CheckRange, inf=0, sup=10) + with pytest.raises(SystemExit) as excinfo: + self.parser.parse_args(["--test", "0"]) + assert str(excinfo.value) == "2" + + def test_call_with_value_equal_to_sup(self) -> None: + self.parser.add_argument("--test", action=CheckRange, inf=0, sup=10) + with pytest.raises(SystemExit) as excinfo: + self.parser.parse_args(["--test", "10"]) + assert str(excinfo.value) == "2" + + def test_call_with_negative_value(self) -> None: + self.parser.add_argument("--test", action=CheckRange, min=-10, max=10) + args = self.parser.parse_args(["--test", "-5"]) + assert args.test == -5 + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_frontend_tkinter_template_overview.py b/tests/test_frontend_tkinter_template_overview.py new file mode 100755 index 0000000..484ef40 --- /dev/null +++ b/tests/test_frontend_tkinter_template_overview.py @@ -0,0 +1,118 @@ +#!/usr/bin/python3 + +""" +Tests for the frontend_tkinter_template_overview.py file. + +This file is part of Ardupilot methodic configurator. https://github.com/ArduPilot/MethodicConfigurator + +SPDX-FileCopyrightText: 2024-2025 Amilcar do Carmo Lucas + +SPDX-License-Identifier: GPL-3.0-or-later +""" + +import argparse +import tkinter as tk + +import pytest + +from ardupilot_methodic_configurator.backend_filesystem_program_settings import ProgramSettings +from ardupilot_methodic_configurator.frontend_tkinter_template_overview import TemplateOverviewWindow, argument_parser, main + + +@pytest.fixture +def root() -> tk.Tk: + root = tk.Tk() + yield root + try: + if root.winfo_exists(): + root.destroy() + except tk.TclError: + pass + + +def test_template_overview_window_init(root: tk.Tk) -> None: + window = TemplateOverviewWindow(root=root) + root.update_idletasks() + root.update() + assert ( + window.root.title() + == f"Amilcar Lucas's - ArduPilot methodic configurator {window.__version__} - Template Overview and selection" + ) + assert window.root.geometry() == "1200x600" + window.root.destroy() + root.destroy() # Ensure the root window is closed + + +def test_adjust_treeview_column_widths(root) -> None: + window = TemplateOverviewWindow(root=root) + root.update_idletasks() + root.update() + window._adjust_treeview_column_widths() # noqa: SLF001 + for col in window.tree["columns"]: + assert window.tree.column(col, option="width") > 0 + window.root.destroy() + root.destroy() # Ensure the root window is closed + + +def test_on_row_selection_change(root) -> None: + window = TemplateOverviewWindow(root=root) + root.update_idletasks() + root.update() + window.tree.insert("", "end", text="test_path", values=("test_value",)) + window.tree.selection_set(window.tree.get_children()[0]) + window._on_row_selection_change(None) # noqa: SLF001 + assert ProgramSettings.get_recently_used_dirs()[0] == "test_path" + window.root.destroy() + root.destroy() # Ensure the root window is closed + + +def test_on_row_double_click(root) -> None: + window = TemplateOverviewWindow(root=root) + root.update_idletasks() + root.update() + window.tree.insert("", "end", text="test_path", values=("test_value",)) + event = tk.Event() + event.y = 0 + window._on_row_double_click(event) # noqa: SLF001 + assert ProgramSettings.get_recently_used_dirs()[0] == "test_path" + window.root.destroy() + root.destroy() # Ensure the root window is closed + + +def test_sort_by_column(root) -> None: + window = TemplateOverviewWindow(root=root) + root.update_idletasks() + root.update() + window.tree.insert("", "end", text="test_path", values=("1",)) + window.tree.insert("", "end", text="test_path2", values=("2",)) + window._sort_by_column("test_value", reverse=False) # noqa: SLF001 + assert window.tree.get_children()[0] == "test_path" + window.root.destroy() + root.destroy() # Ensure the root window is closed + + +def test_display_vehicle_image(root) -> None: + window = TemplateOverviewWindow(root=root) + root.update_idletasks() + root.update() + window._display_vehicle_image("test_path") # noqa: SLF001 + assert window.image_label.cget("text") == "No 'vehicle.jpg' image file in the vehicle directory." + window.root.destroy() + root.destroy() # Ensure the root window is closed + + +def test_argument_parser(mocker) -> None: + mocker.patch("sys.exit") # Mock sys.exit to prevent the test from exiting + args = argument_parser() + assert args is not None + + +def test_main(mocker) -> None: + mock_template_overview_window = mocker.Mock() + mocker.patch("argparse.ArgumentParser.parse_args", return_value=argparse.Namespace(loglevel="DEBUG")) + mocker.patch( + "ardupilot_methodic_configurator.frontend_tkinter_template_overview.TemplateOverviewWindow", + mock_template_overview_window, + ) + main() + assert mock_template_overview_window.called diff --git a/tests/test_internationalization.py b/tests/test_internationalization.py new file mode 100755 index 0000000..8606dda --- /dev/null +++ b/tests/test_internationalization.py @@ -0,0 +1,48 @@ +#!/usr/bin/python3 + +""" +Tests for the internationalization.py file. + +This file is part of Ardupilot methodic configurator. https://github.com/ArduPilot/MethodicConfigurator + +SPDX-FileCopyrightText: 2024-2025 Amilcar do Carmo Lucas + +SPDX-License-Identifier: GPL-3.0-or-later +""" + +import unittest + +from ardupilot_methodic_configurator.internationalization import LANGUAGE_CHOICES, identity_function, load_translation + + +class TestInternationalization(unittest.TestCase): + def test_default_language_is_english(self) -> None: + assert LANGUAGE_CHOICES[0] == "en" + + def test_load_translation_default(self) -> None: + translation_function = load_translation() + assert translation_function == identity_function + + def test_identity_function(self) -> None: + test_string = "test" + assert identity_function(test_string) == test_string + + def test_load_translation_with_language(self) -> None: + translation_function = load_translation("de") + assert translation_function != identity_function + + def test_load_translation_with_invalid_language(self) -> None: + translation_function = load_translation("invalid") + assert translation_function == identity_function + + def test_load_translation_fallback(self) -> None: + translation_function = load_translation("zh_CN") + assert translation_function != identity_function + + def test_language_choices(self) -> None: + expected_languages = ["en", "zh_CN", "pt", "de"] + assert expected_languages == LANGUAGE_CHOICES + + +if __name__ == "__main__": + unittest.main()