diff --git a/.github/codecov.yml b/.github/codecov.yml index db921cb1..3dfc9be6 100644 --- a/.github/codecov.yml +++ b/.github/codecov.yml @@ -2,5 +2,5 @@ coverage: status: project: default: - target: 100% - threshold: 15% + target: 95% + threshold: 5% diff --git a/README.md b/README.md index 42d6d587..658dd9bb 100644 --- a/README.md +++ b/README.md @@ -12,8 +12,9 @@ without typing any command in terminal for [flash the firmware onto the device]( ## Installing -There are pre-built -[releases](https://github.com/selfcustody/krux-installer/releases) for: +[github releases page](https://github.com/selfcustody/krux-installer/releases) + +Available for: * Linux: * Debian-like; @@ -23,7 +24,7 @@ There are pre-built * intel processors; * arm64 processors (M1/M2/M3). -To build it from the source, please follow the steps below: +## Build from source * [System setup](/#system-setup) * [Linux](/#linux) diff --git a/e2e/test_000_base_screen.py b/e2e/test_000_base_screen.py index 04036149..3164f8c9 100644 --- a/e2e/test_000_base_screen.py +++ b/e2e/test_000_base_screen.py @@ -1,4 +1,5 @@ import os +import sys from pathlib import Path from unittest.mock import patch, call, MagicMock from kivy.base import EventLoop, EventLoopBase @@ -93,12 +94,21 @@ def test_static_open_settings(self, mock_get_ruunning_app): BaseScreen.open_settings() mock_get_ruunning_app.return_value.open_settings.assert_called_once() + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch("src.app.screens.base_screen.App.get_running_app") + def test_static_quit_app(self, mock_get_ruunning_app): + mock_get_ruunning_app.return_value = MagicMock() + mock_get_ruunning_app.return_value.stop = MagicMock() + + BaseScreen.quit_app() + mock_get_ruunning_app.return_value.stop.assert_called_once() + @patch.object(EventLoopBase, "ensure_window", lambda x: None) @patch("sys.platform", "linux") @patch( "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" ) - def test_init_linux(self, mock_get_locale): + def test_init_linux_no_frozen(self, mock_get_locale): screen = BaseScreen(wid="mock", name="Mock") self.render(screen) @@ -112,6 +122,31 @@ def test_init_linux(self, mock_get_locale): mock_get_locale.assert_called_once() + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch("sys.platform", "linux") + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + def test_init_linux_frozen(self, mock_get_locale): + with patch.dict( + sys.__dict__, {"_MEIPASS": os.path.join("mock", "path"), "frozen": True} + ): + screen = BaseScreen(wid="mock", name="Mock") + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + window = EventLoop.window + + # your asserts + self.assertEqual( + screen.logo_img, os.path.join("mock", "path", "assets", "logo.png") + ) + self.assertEqual(window.children[0], screen) + self.assertEqual(window.children[0].height, window.height) + + mock_get_locale.assert_called_once() + @patch.object(EventLoopBase, "ensure_window", lambda x: None) @patch("src.app.screens.base_screen.BaseScreen.get_locale") def test_init_win32(self, mock_get_locale): @@ -401,6 +436,40 @@ def test_update_screen_locale(self, mock_get_locale): self.assertEqual(screen.locale, "mocked") mock_get_locale.assert_called_once() + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch("src.app.screens.base_screen.BaseScreen.redirect_exception") + def test_fail_update_screen_locale(self, mock_redirect_exception, mock_get_locale): + screen = BaseScreen(wid="mock", name="Mock") + screen.make_grid(wid="mock_grid", rows=1) + screen.make_button( + row=0, + wid="mock_button", + root_widget="mock_grid", + text="Mocked button", + font_factor=32, + halign=None, + on_press=MagicMock(), + on_release=MagicMock(), + on_ref_press=MagicMock(), + ) + setattr(screen, "update", MagicMock()) + self.render(screen) + self.assertEqual(screen.locale, "en_US.UTF-8") + + screen.update_screen( + name="MockedScreen", + key="locale", + value=None, + allowed_screens=("MockedScreen",), + on_update=MagicMock(), + ) + + mock_get_locale.assert_called_once() + mock_redirect_exception.assert_called_once() + @patch.object(EventLoopBase, "ensure_window", lambda x: None) @patch("src.app.screens.base_screen.BaseScreen.get_locale") @patch("src.app.screens.base_screen.Color") diff --git a/e2e/test_001_greetings_screen.py b/e2e/test_001_greetings_screen.py index 82647de0..9c9137af 100644 --- a/e2e/test_001_greetings_screen.py +++ b/e2e/test_001_greetings_screen.py @@ -7,6 +7,9 @@ from kivy.tests.common import GraphicUnitTest from src.app.screens.greetings_screen import GreetingsScreen +# to be used in mocking grp +import src.app.screens.greetings_screen + class TestAboutScreen(GraphicUnitTest): @@ -317,40 +320,50 @@ def test_fail_get_os_dialout_group_file_not_found( mock_redirect_exception.assert_called() @mark.skipif( - sys.platform in ("win32", "darwin"), - reason="does not run on windows or macos", + sys.platform in ("win32"), + reason="does not run on windows", ) + @patch("sys.platform", "linux") # Patch platform to Linux @patch.object(EventLoopBase, "ensure_window", lambda x: None) @patch( "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" ) - @patch("src.app.screens.greetings_screen.grp") - def test_is_user_not_in_dialout(self, mock_grp, mock_get_locale): + def test_is_user_not_in_dialout(self, mock_get_locale): + # Create a mock grp module + mock_grp = MagicMock() mock_grp.getgrall.return_value = [ MagicMock(gr_name="dialout", gr_passwd="x", gr_gid=1234, gr_mem=["brltty"]) ] - # mock_grp.getgrall.return_value[0].__getitem__ = MagicMock( - # return_value=['dialout', 'x', 1234, ['brltty']] - # ) + + # Temporarily add the mock grp to greetings_screen's global namespace + setattr(src.app.screens.greetings_screen, "grp", mock_grp) + + # Initialize the screen and call the method to test screen = GreetingsScreen() is_in_dialout = screen.is_user_in_dialout_group( user="mockuser", group="dialout" ) - self.assertEqual(is_in_dialout, False) + # Assertions + self.assertEqual(is_in_dialout, False) mock_get_locale.assert_called() mock_grp.getgrall.assert_called() + # Clean up by removing the mock from the module's namespace + delattr(src.app.screens.greetings_screen, "grp") + @mark.skipif( - sys.platform in ("win32", "darwin"), - reason="does not run on windows or macos", + sys.platform in ("win32"), + reason="does not run on windows", ) + @patch("sys.platform", "linux") # Patch platform to Linux @patch.object(EventLoopBase, "ensure_window", lambda x: None) @patch( "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" ) - @patch("src.app.screens.greetings_screen.grp") - def test_is_user_in_dialout(self, mock_grp, mock_get_locale): + def test_is_user_in_dialout(self, mock_get_locale): + # Create a mock grp module + mock_grp = MagicMock() mock_grp.getgrall.return_value = [ MagicMock( gr_name="dialout", @@ -359,6 +372,11 @@ def test_is_user_in_dialout(self, mock_grp, mock_get_locale): gr_mem=["brltty", "mockuser"], ) ] + + # Temporarily add the mock grp to greetings_screen's global namespace + setattr(src.app.screens.greetings_screen, "grp", mock_grp) + + # Initialize the screen and call the method to test screen = GreetingsScreen() is_in_dialout = screen.is_user_in_dialout_group( user="mockuser", group="dialout" @@ -459,9 +477,10 @@ def test_pass_check_dialout_permission_not_linux( mock_schedule_once.assert_called() @mark.skipif( - sys.platform in ("win32", "darwin"), + sys.platform in ("win32"), reason="does not run on windows or darwin", ) + @patch("sys.platform", "linux") @patch.object(EventLoopBase, "ensure_window", lambda x: None) @patch("src.app.screens.greetings_screen.os.environ.get", return_value="mockuser") @patch( @@ -501,9 +520,10 @@ def test_check_dialout_permission_not_in_dialout( mock_in_dialout.assert_called() @mark.skipif( - sys.platform in ("win32", "darwin"), + sys.platform in ("win32"), reason="does not run on windows or darwin", ) + @patch("sys.platform", "linux") @patch.object(EventLoopBase, "ensure_window", lambda x: None) @patch("src.app.screens.greetings_screen.os.environ.get", return_value="mockuser") @patch( diff --git a/e2e/test_002_ask_permission_dialout_screen.py b/e2e/test_002_ask_permission_dialout_screen.py index 870b6bb7..525908da 100644 --- a/e2e/test_002_ask_permission_dialout_screen.py +++ b/e2e/test_002_ask_permission_dialout_screen.py @@ -11,7 +11,7 @@ # WARNING: Do not run these tests on windows # they will break because it do not have the builtin 'grp' module @mark.skipif( - sys.platform in ("win32", "darwin"), + sys.platform in ("win32"), reason="does not run on windows or macos", ) class TestAskPermissionDialoutScreen(GraphicUnitTest): @@ -29,11 +29,17 @@ def setUpClass(cls): def teardown_class(cls): EventLoop.exit() + @patch("sys.platform", "linux") @patch.object(EventLoopBase, "ensure_window", lambda x: None) @patch( "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" ) - def test_render_label(self, mock_get_locale): + @patch( + "src.app.screens.ask_permission_dialout_screen.open", + new_callable=mock_open, + read_data="ID_LIKE=debian", + ) + def test_render_label(self, open_mock, mock_get_locale): screen = AskPermissionDialoutScreen() screen.manager = MagicMock() screen.manager.get_screen = MagicMock() @@ -46,12 +52,19 @@ def test_render_label(self, mock_get_locale): # patch assertions mock_get_locale.assert_called_once() + open_mock.assert_called_once_with("/etc/os-release", mode="r", encoding="utf-8") @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch("sys.platform", "linux") @patch( "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" ) - def test_on_update_user(self, mock_get_locale): + @patch( + "src.app.screens.ask_permission_dialout_screen.open", + new_callable=mock_open, + read_data="ID_LIKE=debian", + ) + def test_on_update_user(self, open_mock, mock_get_locale): screen = AskPermissionDialoutScreen() screen.manager = MagicMock() screen.manager.get_screen = MagicMock() @@ -65,12 +78,19 @@ def test_on_update_user(self, mock_get_locale): # patch assertions mock_get_locale.assert_called_once() + open_mock.assert_called_once_with("/etc/os-release", mode="r", encoding="utf-8") + @patch("sys.platform", "linux") @patch.object(EventLoopBase, "ensure_window", lambda x: None) @patch( "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" ) - def test_on_update_distro(self, mock_get_locale): + @patch( + "src.app.screens.ask_permission_dialout_screen.open", + new_callable=mock_open, + read_data="ID_LIKE=debian", + ) + def test_on_update_distro(self, open_mock, mock_get_locale): screen = AskPermissionDialoutScreen() screen.manager = MagicMock() screen.manager.get_screen = MagicMock() @@ -84,7 +104,9 @@ def test_on_update_distro(self, mock_get_locale): # patch assertions mock_get_locale.assert_called_once() + open_mock.assert_called_once_with("/etc/os-release", mode="r", encoding="utf-8") + @patch("sys.platform", "linux") @patch.object(EventLoopBase, "ensure_window", lambda x: None) @patch( "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" @@ -92,7 +114,12 @@ def test_on_update_distro(self, mock_get_locale): @patch( "src.app.screens.ask_permission_dialout_screen.AskPermissionDialoutScreen.show_warning" ) - def test_on_update_screen(self, mock_show_warning, mock_get_locale): + @patch( + "src.app.screens.ask_permission_dialout_screen.open", + new_callable=mock_open, + read_data="ID_LIKE=debian", + ) + def test_on_update_screen(self, open_mock, mock_show_warning, mock_get_locale): screen = AskPermissionDialoutScreen() screen.manager = MagicMock() screen.manager.get_screen = MagicMock() @@ -106,12 +133,19 @@ def test_on_update_screen(self, mock_show_warning, mock_get_locale): # patch assertions mock_get_locale.assert_called_once() mock_show_warning.assert_called_once() + open_mock.assert_called_once_with("/etc/os-release", mode="r", encoding="utf-8") + @patch("sys.platform", "linux") @patch.object(EventLoopBase, "ensure_window", lambda x: None) @patch( "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" ) - def test_on_update_group(self, mock_get_locale): + @patch( + "src.app.screens.ask_permission_dialout_screen.open", + new_callable=mock_open, + read_data="ID_LIKE=debian", + ) + def test_on_update_group(self, open_mock, mock_get_locale): screen = AskPermissionDialoutScreen() screen.manager = MagicMock() screen.manager.get_screen = MagicMock() @@ -125,6 +159,7 @@ def test_on_update_group(self, mock_get_locale): # patch assertions mock_get_locale.assert_called_once() + open_mock.assert_called_once_with("/etc/os-release", mode="r", encoding="utf-8") @patch.object(EventLoopBase, "ensure_window", lambda x: None) @patch( @@ -181,8 +216,8 @@ def test_show_warning(self, mock_manager, open_mock, mock_get_locale): mock_get_locale.assert_called_once() open_mock.assert_called() - @patch.object(EventLoopBase, "ensure_window", lambda x: None) @patch("sys.platform", "linux") + @patch.object(EventLoopBase, "ensure_window", lambda x: None) @patch( "src.app.screens.ask_permission_dialout_screen.open", new_callable=mock_open, @@ -221,6 +256,53 @@ def test_press_allow_on_debian(self, mock_exec, mock_get_locale, open_mock): callback=on_permission_created, ) + @patch("sys.platform", "linux") + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.ask_permission_dialout_screen.open", + new_callable=mock_open, + read_data="ID_LIKE=debian", + ) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch( + "src.app.screens.ask_permission_dialout_screen.SudoerLinux.exec", + side_effect=Exception(), + ) + @patch("src.app.screens.base_screen.BaseScreen.redirect_exception") + def test_fail_press_allow_on_debian( + self, mock_redirect_exception, mock_exec, mock_get_locale, open_mock + ): + screen = AskPermissionDialoutScreen() + screen.manager = MagicMock() + screen.manager.get_screen = MagicMock() + screen.bin = "mock" + screen.bin_args = ["-a", "-G"] + screen.group = "mockedgroup" + screen.user = "mockeduser" + + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + action = getattr(screen, f"on_ref_press_{screen.id}_label") + action("Allow") + + # patch assertions + on_permission_created = getattr( + AskPermissionDialoutScreen, "on_permission_created" + ) + + open_mock.assert_called_once() + mock_get_locale.assert_called_once() + mock_exec.assert_called_once_with( + cmd=["/usr/sbin/usermod", "-a", "-G", "mockedgroup", "mockeduser"], + env={}, + callback=on_permission_created, + ) + mock_redirect_exception.assert_called() + @patch.object(EventLoopBase, "ensure_window", lambda x: None) @patch("sys.platform", "linux") @patch( @@ -381,13 +463,18 @@ def test_press_allow_on_alpine(self, mock_exec, mock_get_locale, open_mock): callback=on_permission_created, ) - @patch.object(EventLoopBase, "ensure_window", lambda x: None) @patch("sys.platform", "linux") + @patch.object(EventLoopBase, "ensure_window", lambda x: None) @patch( "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" ) + @patch( + "src.app.screens.ask_permission_dialout_screen.open", + new_callable=mock_open, + read_data="ID_LIKE=debian", + ) @patch("src.app.screens.base_screen.BaseScreen.quit_app") - def test_press_deny(self, mock_quit_app, mock_get_locale): + def test_press_deny(self, mock_quit_app, open_mock, mock_get_locale): screen = AskPermissionDialoutScreen() screen.manager = MagicMock() screen.manager.get_screen = MagicMock() @@ -406,13 +493,19 @@ def test_press_deny(self, mock_quit_app, mock_get_locale): mock_get_locale.assert_called_once() mock_quit_app.assert_called_once() + open_mock.assert_called_once_with("/etc/os-release", mode="r", encoding="utf-8") - @patch.object(EventLoopBase, "ensure_window", lambda x: None) @patch("sys.platform", "linux") + @patch.object(EventLoopBase, "ensure_window", lambda x: None) @patch( "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" ) - def test_on_permission_created(self, mock_get_locale): + @patch( + "src.app.screens.ask_permission_dialout_screen.open", + new_callable=mock_open, + read_data="ID_LIKE=debian", + ) + def test_on_permission_created(self, open_mock, mock_get_locale): screen = AskPermissionDialoutScreen() screen.manager = MagicMock() screen.manager.get_screen = MagicMock() @@ -443,3 +536,4 @@ def test_on_permission_created(self, mock_get_locale): # patch assertions mock_get_locale.assert_called_once() + open_mock.assert_called_once_with("/etc/os-release", mode="r", encoding="utf-8") diff --git a/e2e/test_003_main_screen.py b/e2e/test_003_main_screen.py index 465d016e..3859f0a2 100644 --- a/e2e/test_003_main_screen.py +++ b/e2e/test_003_main_screen.py @@ -220,6 +220,22 @@ def test_update_version(self, mock_get_locale): mock_get_locale.assert_has_calls(calls) + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch("src.app.screens.base_screen.BaseScreen.redirect_exception") + def test_fail_update_version(self, mock_redirect_exception, mock_get_locale): + screen = MainScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + + screen.update(name="SelectVersionScreen", key="version") + mock_redirect_exception.assert_called() + mock_get_locale.assert_called() + @patch.object(EventLoopBase, "ensure_window", lambda x: None) @patch( "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" @@ -257,6 +273,22 @@ def test_update_device(self, mock_get_locale): mock_get_locale.assert_any_call() + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch("src.app.screens.base_screen.BaseScreen.redirect_exception") + def test_fail_update_device(self, mock_redirect_exception, mock_get_locale): + screen = MainScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + + screen.update(name="SelectVersionScreen", key="device") + mock_redirect_exception.assert_called() + mock_get_locale.assert_called() + @patch.object(EventLoopBase, "ensure_window", lambda x: None) @patch( "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" @@ -527,12 +559,10 @@ def test_on_press_can_flash_or_wipe(self, mock_get_locale, mock_set_background): "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" ) @patch("src.app.screens.base_screen.BaseScreen.get_destdir_assets") - @patch("src.app.screens.main_screen.re.findall", side_effect=[True]) @patch("src.app.screens.main_screen.os.path.isfile", side_effect=[False]) def test_on_release_flash_to_download_stable_zip_screen( self, mock_isfile, - mock_findall, mock_get_destdir_assets, mock_get_locale, mock_manager, @@ -561,10 +591,43 @@ def test_on_release_flash_to_download_stable_zip_screen( mock_set_screen.assert_called_once_with( name="DownloadStableZipScreen", direction="left" ) - mock_findall.assert_called_once_with(r"^v\d+\.\d+\.\d$", "v24.03.0") pattern = re.compile(r".*v24\.03\.0\.zip") self.assertTrue(pattern.match(mock_isfile.call_args[0][0])) + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch("src.app.screens.main_screen.MainScreen.set_background") + @patch("src.app.screens.main_screen.MainScreen.manager") + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch("src.app.screens.base_screen.BaseScreen.redirect_exception") + def test_fail_on_release_flash_to_download_stable_zip_screen( + self, + mock_redirect_exception, + mock_get_locale, + mock_manager, + mock_set_background, + ): + mock_manager.get_screen = MagicMock() + + screen = MainScreen() + screen.version = "mocked" + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + window = EventLoop.window + grid = window.children[0].children[0] + button = grid.children[3] + + screen.update(name="SelectVersionScreen", key="device", value="m5stickv") + action = getattr(screen.__class__, f"on_release_{button.id}") + action(button) + + mock_get_locale.assert_any_call() + mock_set_background.assert_called_once_with(wid="main_flash", rgba=(0, 0, 0, 1)) + mock_redirect_exception.assert_called() + @patch.object(EventLoopBase, "ensure_window", lambda x: None) @patch("src.app.screens.main_screen.MainScreen.set_background") @patch("src.app.screens.main_screen.MainScreen.set_screen") @@ -573,12 +636,10 @@ def test_on_release_flash_to_download_stable_zip_screen( "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" ) @patch("src.app.screens.base_screen.BaseScreen.get_destdir_assets") - @patch("src.app.screens.main_screen.re.findall", side_effect=[True]) @patch("src.app.screens.main_screen.os.path.isfile", side_effect=[True]) def test_on_release_flash_to_warning_already_downloaded_zip_screen( self, mock_isfile, - mock_findall, mock_get_destdir_assets, mock_get_locale, mock_manager, @@ -607,7 +668,6 @@ def test_on_release_flash_to_warning_already_downloaded_zip_screen( mock_set_screen.assert_called_once_with( name="WarningAlreadyDownloadedScreen", direction="left" ) - mock_findall.assert_called_once_with(r"^v\d+\.\d+\.\d$", "v24.03.0") pattern = re.compile(r".*v24\.03\.0\.zip") self.assertTrue(pattern.match(mock_isfile.call_args[0][0])) @@ -618,10 +678,8 @@ def test_on_release_flash_to_warning_already_downloaded_zip_screen( @patch( "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" ) - @patch("src.app.screens.main_screen.re.findall", side_effect=[False, True]) def test_on_release_flash_to_download_beta_screen( self, - mock_findall, mock_get_locale, mock_manager, mock_set_screen, @@ -644,12 +702,6 @@ def test_on_release_flash_to_download_beta_screen( action(button) mock_get_locale.assert_any_call() - mock_findall.assert_has_calls( - [ - call("^v\\d+\\.\\d+\\.\\d$", "odudex/krux_binaries"), - call(r"^odudex/krux_binaries", "odudex/krux_binaries"), - ] - ) mock_set_background.assert_called_once_with(wid="main_flash", rgba=(0, 0, 0, 1)) mock_set_screen.assert_called_once_with( name="DownloadBetaScreen", direction="left" diff --git a/e2e/test_008_about_screen.py b/e2e/test_008_about_screen.py index e3393dcb..9bb3a677 100644 --- a/e2e/test_008_about_screen.py +++ b/e2e/test_008_about_screen.py @@ -43,7 +43,7 @@ def test_render_main_screen(self, mock_get_locale): text = "".join( [ - "[ref=SourceCode][b]v0.0.20-beta[/b][/ref]", + "[ref=SourceCode][b]v0.0.20[/b][/ref]", "\n", "\n", "follow us on X: ", @@ -81,7 +81,7 @@ def test_update_locale(self, mock_get_locale): text = "".join( [ - "[ref=SourceCode][b]v0.0.20-beta[/b][/ref]", + "[ref=SourceCode][b]v0.0.20[/b][/ref]", "\n", "\n", "siga-nos no X: ", diff --git a/e2e/test_016_unzip_stable_screen.py b/e2e/test_016_unzip_stable_screen.py index a96a8d60..39bd9968 100644 --- a/e2e/test_016_unzip_stable_screen.py +++ b/e2e/test_016_unzip_stable_screen.py @@ -245,24 +245,22 @@ def test_update_airgap_button(self, mock_get_locale, mock_get_destdir_assets): screen.update(name="VerifyStableZipScreen", key="version", value="v0.0.1") screen.update(name="VerifyStableZipScreen", key="airgap-button") - # p = os.path.join("mock", "krux-v0.0.1", "maixpy_mock", "firmware.bin") - # text = "".join( - # [ - # "[color=#333333]", - # "Air-gapped update with (soon)", - # "[/color]", - # "\n", - # "[color=#333333]", - # p, - # "[/color]", - # ] - # ) + p = os.path.join("mock", "krux-v0.0.1", "maixpy_mock", "firmware.bin") + text = "".join( + [ + "Air-gapped update with", + "\n", + "[color=#efcc00]", + p, + "[/color]", + ] + ) # default assertions - # button = screen.ids[f"{screen.id}_airgap_button"] - # self.assertEqual(screen.ids[f"{screen.id}_airgap_button"].text, text) - # self.assertTrue(hasattr(screen.__class__, f"on_press_{button.id}")) - # self.assertTrue(hasattr(screen.__class__, f"on_release_{button.id}")) + button = screen.ids[f"{screen.id}_airgap_button"] + self.assertEqual(screen.ids[f"{screen.id}_airgap_button"].text, text) + self.assertTrue(hasattr(screen.__class__, f"on_press_{button.id}")) + self.assertTrue(hasattr(screen.__class__, f"on_release_{button.id}")) # patch assertions mock_get_destdir_assets.assert_any_call() @@ -335,30 +333,28 @@ def test_on_press_airgap_button( screen.update(name="VerifyStableZipScreen", key="device", value="mock") screen.update(name="VerifyStableZipScreen", key="version", value="v0.0.1") screen.update(name="VerifyStableZipScreen", key="airgap-button") - # button = screen.ids[f"{screen.id}_airgap_button"] - # action = getattr(screen.__class__, f"on_press_{button.id}") - # action(button) - - # p = os.path.join("mock", "krux-v0.0.1", "maixpy_mock", "firmware.bin") - # text = "".join( - # [ - # "[color=#333333]", - # "Air-gapped update with (soon)", - # "[/color]", - # "\n", - # "[color=#333333]", - # p, - # "[/color]", - # ] - # ) + button = screen.ids[f"{screen.id}_airgap_button"] + action = getattr(screen.__class__, f"on_press_{button.id}") + action(button) + + p = os.path.join("mock", "krux-v0.0.1", "maixpy_mock", "firmware.bin") + text = "".join( + [ + "Extracting", + "\n", + "[color=#efcc00]", + p, + "[/color]", + ] + ) # default assertions - # self.assertEqual(button.text, text) + self.assertEqual(button.text, text) # patch assertions mock_get_destdir_assets.assert_called_once() mock_get_locale.assert_called() - mock_set_background.assert_not_called() + mock_set_background.assert_called() @patch.object(EventLoopBase, "ensure_window", lambda x: None) @patch( @@ -436,19 +432,19 @@ def test_on_release_flash_button( "src.app.screens.base_screen.BaseScreen.get_destdir_assets", return_value="mock" ) @patch("src.app.screens.unzip_stable_screen.UnzipStableScreen.set_background") - # @patch("src.app.screens.unzip_stable_screen.FirmwareUnzip") + @patch("src.app.screens.unzip_stable_screen.FirmwareUnzip") @patch("src.app.screens.unzip_stable_screen.UnzipStableScreen.manager") @patch("src.app.screens.unzip_stable_screen.time.sleep") def test_on_release_airgapped_button( self, mock_sleep, mock_manager, - # mock_firmware_unzip, + mock_firmware_unzip, mock_set_background, mock_get_destdir_assets, mock_get_locale, ): - # mock_firmware_unzip.load = MagicMock() + mock_firmware_unzip.load = MagicMock() mock_manager.get_screen = MagicMock() screen = UnzipStableScreen() @@ -463,13 +459,13 @@ def test_on_release_airgapped_button( screen.update(name="VerifyStableZipScreen", key="airgap-button") button = screen.ids[f"{screen.id}_airgap_button"] - # action = getattr(screen.__class__, f"on_release_{button.id}") - # action(button) + action = getattr(screen.__class__, f"on_release_{button.id}") + action(button) p = os.path.join("mock", "krux-v0.0.1", "maixpy_mock", "firmware.bin") text = "".join( [ - "Air-gapped update with", + "Extracted", "\n", "[color=#efcc00]", p, @@ -483,7 +479,7 @@ def test_on_release_airgapped_button( # patch assertions mock_get_destdir_assets.assert_called_once() mock_get_locale.assert_called() - # mock_firmware_unzip.assert_not_called() - mock_set_background.assert_not_called() - mock_manager.get_screen.assert_not_called() - mock_sleep.assert_not_called() + mock_firmware_unzip.assert_called() + mock_set_background.assert_called() + mock_manager.get_screen.assert_called() + mock_sleep.assert_called() diff --git a/e2e/test_019_config_krux_installer.py b/e2e/test_019_config_krux_installer.py index 04b243cd..0f98dd2d 100644 --- a/e2e/test_019_config_krux_installer.py +++ b/e2e/test_019_config_krux_installer.py @@ -13,6 +13,65 @@ class TestConfigKruxInstaller(GraphicUnitTest): def teardown_class(cls): EventLoop.exit() + @patch("src.app.config_krux_installer.LabelBase.register") + @patch( + "src.app.config_krux_installer.os.path.abspath", + side_effect=[ + os.path.join("mock", "path", "assets"), + os.path.join("mock", "path", "i18n"), + ], + ) + def test_init(self, mock_abspath, mock_register): + with patch.dict(sys.__dict__, {"frozen": False}): + ConfigKruxInstaller() + mock_register.assert_called_once_with( + "Roboto", + os.path.join( + "mock", "path", "assets", "NotoSansCJK_CY_JP_SC_KR_VI_Krux.ttf" + ), + ) + mock_abspath.assert_called() + + @patch("src.app.config_krux_installer.kv_resources.resource_add_path") + @patch("src.app.config_krux_installer.LabelBase.register") + def test_init_frozen_branch(self, mock_register, mock_resource_add_path): + with patch.dict( + sys.__dict__, {"_MEIPASS": os.path.join("mocked", "path"), "frozen": True} + ): + installer = ConfigKruxInstaller() + mock_resource_add_path.assert_called_with(os.path.join("mocked", "path")) + mock_register.assert_called_once_with( + "Roboto", + os.path.join( + "mocked", "path", "assets", "NotoSansCJK_CY_JP_SC_KR_VI_Krux.ttf" + ), + ) + self.assertEqual( + installer.assets_path, os.path.join("mocked", "path", "assets") + ) + self.assertEqual( + installer.i18n_path, os.path.join("mocked", "path", "src", "i18n") + ) + + @patch("sys.platform", "linux") + def test_make_lang_code_posix(self): + lang = ConfigKruxInstaller.make_lang_code(lang="en_US") + self.assertEqual(lang, "en_US.UTF-8") + + @patch("sys.platform", "win32") + def test_make_lang_code_windows(self): + lang = ConfigKruxInstaller.make_lang_code(lang="en_US") + self.assertEqual(lang, "en_US") + + @patch("sys.platform", "mockos") + def test_fail_make_lang_code(self): + with self.assertRaises(OSError) as exc: + ConfigKruxInstaller.make_lang_code(lang="en_US") + self.assertEqual( + str(exc.exception), + "Couldn't possible to setup locale: OS 'mockos' not implemented", + ) + @patch.dict(os.environ, {"LANG": "en-US.UTF-8"}, clear=True) @patch("sys.platform", "linux") def test_get_system_lang_linux(self): @@ -357,30 +416,139 @@ def test_get_application_config( ) mock_get_application_config.assert_called_once_with("mockfile") - @patch("src.app.config_krux_installer.ConfigKruxInstaller.create_app_dir") - @patch("src.app.config_krux_installer.ConfigKruxInstaller.get_system_lang") - def test_build_config(self, mock_get_system_lang, mock_create_app_dir): - mock_create_app_dir.return_value = "mockdir" + @patch("sys.platform", "linux") + @patch( + "src.app.config_krux_installer.ConfigKruxInstaller.create_app_dir", + return_value="mockdir", + ) + @patch( + "src.app.config_krux_installer.ConfigKruxInstaller.get_system_lang", + return_value="en_US.UTF-8", + ) + @patch("src.app.config_krux_installer.os.path.isfile", return_value=True) + def test_build_config_posix_no_default_lang( + self, mock_isfile, mock_get_system_lang, mock_create_app_dir + ): config = MagicMock() config.setdefaults = MagicMock() - mock_get_system_lang.return_value = "en_US.UTF-8" app = ConfigKruxInstaller() + app.i18n_path = os.path.join("mock", "i18n") app.build_config(config) # patch assertions mock_create_app_dir.assert_called_once_with(name="local") + mock_get_system_lang.assert_called_once() + mock_isfile.assert_called_once_with( + os.path.join("mock", "i18n", "en_US.UTF-8.json") + ) + config.setdefaults.assert_has_calls( + [ + call("destdir", {"assets": "mockdir"}), + call("flash", {"baudrate": 1500000}), + call("locale", {"lang": "en_US.UTF-8"}), + ] + ) + + @patch("sys.platform", "linux") + @patch( + "src.app.config_krux_installer.ConfigKruxInstaller.create_app_dir", + return_value="mockdir", + ) + @patch( + "src.app.config_krux_installer.ConfigKruxInstaller.get_system_lang", + return_value="mo_CK.UTF-8", + ) + @patch("src.app.config_krux_installer.os.path.isfile", return_value=False) + def test_build_config_posix_default_lang( + self, mock_isfile, mock_get_system_lang, mock_create_app_dir + ): + config = MagicMock() + config.setdefaults = MagicMock() - if sys.platform in ("linux", "darwin"): - lang = "en_US.UTF-8" - else: - lang = "en_US" + app = ConfigKruxInstaller() + app.i18n_path = os.path.join("mock", "i18n") + app.build_config(config) + # patch assertions + mock_create_app_dir.assert_called_once_with(name="local") + mock_get_system_lang.assert_called_once() + mock_isfile.assert_called_once_with( + os.path.join("mock", "i18n", "mo_CK.UTF-8.json") + ) + config.setdefaults.assert_has_calls( + [ + call("destdir", {"assets": "mockdir"}), + call("flash", {"baudrate": 1500000}), + call("locale", {"lang": "en_US.UTF-8"}), + ] + ) + + @patch("sys.platform", "win32") + @patch( + "src.app.config_krux_installer.ConfigKruxInstaller.create_app_dir", + return_value="mockdir", + ) + @patch( + "src.app.config_krux_installer.ConfigKruxInstaller.get_system_lang", + return_value="en_US", + ) + @patch("src.app.config_krux_installer.os.path.isfile", return_value=True) + def test_build_config_windows_default_lang( + self, mock_isfile, mock_get_system_lang, mock_create_app_dir + ): + config = MagicMock() + config.setdefaults = MagicMock() + + app = ConfigKruxInstaller() + app.i18n_path = os.path.join("mock", "i18n") + app.build_config(config) + + # patch assertions + mock_create_app_dir.assert_called_once_with(name="local") + mock_get_system_lang.assert_called_once() + mock_isfile.assert_called_once_with( + os.path.join("mock", "i18n", "en_US.UTF-8.json") + ) + config.setdefaults.assert_has_calls( + [ + call("destdir", {"assets": "mockdir"}), + call("flash", {"baudrate": 1500000}), + call("locale", {"lang": "en_US"}), + ] + ) + + @patch("sys.platform", "win32") + @patch( + "src.app.config_krux_installer.ConfigKruxInstaller.create_app_dir", + return_value="mockdir", + ) + @patch( + "src.app.config_krux_installer.ConfigKruxInstaller.get_system_lang", + return_value="mo_CK", + ) + @patch("src.app.config_krux_installer.os.path.isfile", return_value=False) + def test_build_config_windows_no_default_lang( + self, mock_isfile, mock_get_system_lang, mock_create_app_dir + ): + config = MagicMock() + config.setdefaults = MagicMock() + + app = ConfigKruxInstaller() + app.i18n_path = os.path.join("mock", "i18n") + app.build_config(config) + + # patch assertions + mock_create_app_dir.assert_called_once_with(name="local") + mock_get_system_lang.assert_called_once() + mock_isfile.assert_called_once_with( + os.path.join("mock", "i18n", "mo_CK.UTF-8.json") + ) config.setdefaults.assert_has_calls( [ call("destdir", {"assets": "mockdir"}), call("flash", {"baudrate": 1500000}), - call("locale", {"lang": lang}), + call("locale", {"lang": "en_US"}), ] ) diff --git a/e2e/test_020_app_init.py b/e2e/test_020_app_init.py index 8771fef1..179a1724 100644 --- a/e2e/test_020_app_init.py +++ b/e2e/test_020_app_init.py @@ -1,6 +1,6 @@ import os import sys -from unittest.mock import patch +from unittest.mock import patch, mock_open, MagicMock from pytest import mark from kivy.base import EventLoop, EventLoopBase from kivy.tests.common import GraphicUnitTest @@ -32,18 +32,19 @@ def test_init(self): self.assertIsInstance(app.screen_manager, ScreenManager) @mark.skipif( - sys.platform in ("win32", "darwin"), - reason="does not run on windows or macos", + sys.platform in ("win32"), + reason="does not run on windows", ) @patch.object(EventLoopBase, "ensure_window", lambda x: None) @patch("sys.platform", "linux") + @patch("builtins.open", new_callable=mock_open, read_data="ID_LIKE=debian") @patch( "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" ) @patch( "src.app.screens.base_screen.BaseScreen.get_destdir_assets", return_value="mock" ) - def test_build_linux(self, mock_get_destdir_assets, mock_get_locale): + def test_build_debian(self, mock_get_destdir_assets, mock_get_locale, open_mock): app = KruxInstallerApp() app.build() @@ -72,9 +73,307 @@ def test_build_linux(self, mock_get_destdir_assets, mock_get_locale): for screen in screens: self.assertFalse(app.screen_manager.get_screen(screen) is None) + open_mock.assert_called_once_with("/etc/os-release", mode="r", encoding="utf-8") mock_get_destdir_assets.assert_called_once() mock_get_locale.assert_called() + @mark.skipif( + sys.platform in ("win32"), + reason="does not run on windows", + ) + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch("sys.platform", "linux") + @patch("builtins.open", new_callable=mock_open, read_data="ID_LIKE=rhel") + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch( + "src.app.screens.base_screen.BaseScreen.get_destdir_assets", return_value="mock" + ) + def test_build_rhel(self, mock_get_destdir_assets, mock_get_locale, open_mock): + app = KruxInstallerApp() + app.build() + + screens = ( + "GreetingsScreen", + "AskPermissionDialoutScreen", + "MainScreen", + "SelectDeviceScreen", + "SelectVersionScreen", + "SelectOldVersionScreen", + "WarningBetaScreen", + "AboutScreen", + "DownloadStableZipScreen", + "DownloadStableZipSha256Screen", + "DownloadStableZipSigScreen", + "DownloadSelfcustodyPemScreen", + "VerifyStableZipScreen", + "UnzipStableScreen", + "DownloadBetaScreen", + "WarningAlreadyDownloadedScreen", + "WarningWipeScreen", + "FlashScreen", + "WipeScreen", + "ErrorScreen", + ) + for screen in screens: + self.assertFalse(app.screen_manager.get_screen(screen) is None) + + open_mock.assert_called_once_with("/etc/os-release", mode="r", encoding="utf-8") + mock_get_destdir_assets.assert_called_once() + mock_get_locale.assert_called() + + @mark.skipif( + sys.platform in ("win32"), + reason="does not run on windows", + ) + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch("sys.platform", "linux") + @patch("builtins.open", new_callable=mock_open, read_data="ID_LIKE=suse") + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch( + "src.app.screens.base_screen.BaseScreen.get_destdir_assets", return_value="mock" + ) + def test_build_suse(self, mock_get_destdir_assets, mock_get_locale, open_mock): + app = KruxInstallerApp() + app.build() + + screens = ( + "GreetingsScreen", + "AskPermissionDialoutScreen", + "MainScreen", + "SelectDeviceScreen", + "SelectVersionScreen", + "SelectOldVersionScreen", + "WarningBetaScreen", + "AboutScreen", + "DownloadStableZipScreen", + "DownloadStableZipSha256Screen", + "DownloadStableZipSigScreen", + "DownloadSelfcustodyPemScreen", + "VerifyStableZipScreen", + "UnzipStableScreen", + "DownloadBetaScreen", + "WarningAlreadyDownloadedScreen", + "WarningWipeScreen", + "FlashScreen", + "WipeScreen", + "ErrorScreen", + ) + for screen in screens: + self.assertFalse(app.screen_manager.get_screen(screen) is None) + + open_mock.assert_called_once_with("/etc/os-release", mode="r", encoding="utf-8") + mock_get_destdir_assets.assert_called_once() + mock_get_locale.assert_called() + + @mark.skipif( + sys.platform in ("win32"), + reason="does not run on windows", + ) + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch("sys.platform", "linux") + @patch("builtins.open", new_callable=mock_open, read_data="ID=arch") + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch( + "src.app.screens.base_screen.BaseScreen.get_destdir_assets", return_value="mock" + ) + def test_build_arch(self, mock_get_destdir_assets, mock_get_locale, open_mock): + app = KruxInstallerApp() + app.build() + + screens = ( + "GreetingsScreen", + "AskPermissionDialoutScreen", + "MainScreen", + "SelectDeviceScreen", + "SelectVersionScreen", + "SelectOldVersionScreen", + "WarningBetaScreen", + "AboutScreen", + "DownloadStableZipScreen", + "DownloadStableZipSha256Screen", + "DownloadStableZipSigScreen", + "DownloadSelfcustodyPemScreen", + "VerifyStableZipScreen", + "UnzipStableScreen", + "DownloadBetaScreen", + "WarningAlreadyDownloadedScreen", + "WarningWipeScreen", + "FlashScreen", + "WipeScreen", + "ErrorScreen", + ) + for screen in screens: + self.assertFalse(app.screen_manager.get_screen(screen) is None) + + open_mock.assert_called() + mock_get_destdir_assets.assert_called_once() + mock_get_locale.assert_called() + + @mark.skipif( + sys.platform in ("win32"), + reason="does not run on windows", + ) + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch("sys.platform", "linux") + @patch("builtins.open", new_callable=mock_open, read_data="ID=alpine") + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch( + "src.app.screens.base_screen.BaseScreen.get_destdir_assets", return_value="mock" + ) + def test_build_alpine(self, mock_get_destdir_assets, mock_get_locale, open_mock): + app = KruxInstallerApp() + app.build() + + screens = ( + "GreetingsScreen", + "AskPermissionDialoutScreen", + "MainScreen", + "SelectDeviceScreen", + "SelectVersionScreen", + "SelectOldVersionScreen", + "WarningBetaScreen", + "AboutScreen", + "DownloadStableZipScreen", + "DownloadStableZipSha256Screen", + "DownloadStableZipSigScreen", + "DownloadSelfcustodyPemScreen", + "VerifyStableZipScreen", + "UnzipStableScreen", + "DownloadBetaScreen", + "WarningAlreadyDownloadedScreen", + "WarningWipeScreen", + "FlashScreen", + "WipeScreen", + "ErrorScreen", + ) + for screen in screens: + self.assertFalse(app.screen_manager.get_screen(screen) is None) + + open_mock.assert_called() + mock_get_destdir_assets.assert_called_once() + mock_get_locale.assert_called() + + @mark.skipif( + sys.platform in ("win32"), + reason="does not run on windows", + ) + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch("sys.platform", "linux") + @patch( + "builtins.open", + new_callable=mock_open, + read_data="ID=mockos\nPRETTY_NAME=MockOS", + ) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch( + "src.app.screens.base_screen.BaseScreen.get_destdir_assets", return_value="mock" + ) + @patch("src.app.screens.base_screen.BaseScreen.redirect_exception") + def test_fail_build_unrecognized( + self, + mock_redirect_exception, + mock_get_destdir_assets, + mock_get_locale, + open_mock, + ): + app = KruxInstallerApp() + app.build() + + screens = ( + "GreetingsScreen", + "AskPermissionDialoutScreen", + "MainScreen", + "SelectDeviceScreen", + "SelectVersionScreen", + "SelectOldVersionScreen", + "WarningBetaScreen", + "AboutScreen", + "DownloadStableZipScreen", + "DownloadStableZipSha256Screen", + "DownloadStableZipSigScreen", + "DownloadSelfcustodyPemScreen", + "VerifyStableZipScreen", + "UnzipStableScreen", + "DownloadBetaScreen", + "WarningAlreadyDownloadedScreen", + "WarningWipeScreen", + "FlashScreen", + "WipeScreen", + "ErrorScreen", + ) + for screen in screens: + self.assertFalse(app.screen_manager.get_screen(screen) is None) + + open_mock.assert_called() + mock_get_destdir_assets.assert_called_once() + mock_get_locale.assert_called() + mock_redirect_exception.assert_called() + + @mark.skipif( + sys.platform in ("win32"), + reason="does not run on windows", + ) + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch("sys.platform", "linux") + @patch("builtins.open", new_callable=mock_open) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch( + "src.app.screens.base_screen.BaseScreen.get_destdir_assets", return_value="mock" + ) + @patch("src.app.screens.base_screen.BaseScreen.redirect_exception") + def test_fail_build_linux_filenotfound( + self, + mock_redirect_exception, + mock_get_destdir_assets, + mock_get_locale, + open_mock, + ): + open_mock.return_value.__enter__.side_effect = [FileNotFoundError, MagicMock()] + app = KruxInstallerApp() + app.build() + + screens = ( + "GreetingsScreen", + "AskPermissionDialoutScreen", + "MainScreen", + "SelectDeviceScreen", + "SelectVersionScreen", + "SelectOldVersionScreen", + "WarningBetaScreen", + "AboutScreen", + "DownloadStableZipScreen", + "DownloadStableZipSha256Screen", + "DownloadStableZipSigScreen", + "DownloadSelfcustodyPemScreen", + "VerifyStableZipScreen", + "UnzipStableScreen", + "DownloadBetaScreen", + "WarningAlreadyDownloadedScreen", + "WarningWipeScreen", + "FlashScreen", + "WipeScreen", + "ErrorScreen", + ) + for screen in screens: + self.assertFalse(app.screen_manager.get_screen(screen) is None) + + open_mock.assert_called() + mock_get_destdir_assets.assert_called_once() + mock_get_locale.assert_called() + mock_redirect_exception.assert_called() + @patch("sys.platform", "win32") @patch( "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" diff --git a/e2e/test_022_flash_screen.py b/e2e/test_022_flash_screen.py index 25f01523..07a12070 100644 --- a/e2e/test_022_flash_screen.py +++ b/e2e/test_022_flash_screen.py @@ -1,3 +1,4 @@ +import threading from unittest.mock import patch, MagicMock, call from kivy.base import EventLoop, EventLoopBase from kivy.tests.common import GraphicUnitTest @@ -140,6 +141,32 @@ def test_on_pre_enter(self, mock_get_locale): # patch assertions mock_get_locale.assert_called() + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + def test_greeting_fail_on_data_mock(self, mock_get_locale): + screen = FlashScreen() + screen.flasher.ktool.kill = MagicMock() + screen.flasher.ktool.checkKillExit = MagicMock() + + screen.output = [] + screen.on_pre_enter() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + + on_data = getattr(FlashScreen, "on_data") + on_data("Greeting fail: mock") + + self.assertEqual(screen.fail_msg, "Greeting fail: mock") + + # patch assertions + mock_get_locale.assert_called() + screen.flasher.ktool.kill.assert_called() + screen.flasher.ktool.checkKillExit.assert_called() + @patch.object(EventLoopBase, "ensure_window", lambda x: None) @patch( "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" @@ -389,3 +416,209 @@ def test_on_enter(self, mock_flasher, mock_thread, mock_partial, mock_get_locale any_order=True, ) mock_thread.assert_called_once_with(name=screen.name, target=mock_partial()) + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch("src.app.screens.flash_screen.partial") + @patch("src.app.screens.flash_screen.threading.Thread") + @patch("src.utils.flasher.Flasher") + @patch("src.app.screens.base_screen.BaseScreen.redirect_exception") + def test_on_enter_fail_stopiteration( + self, + mock_redirect_exception, + mock_flasher, + mock_thread, + mock_partial, + mock_get_locale, + ): + mock_flasher.__class__.print_callback = MagicMock() + + screen = FlashScreen() + screen.flasher = MagicMock() + screen.flasher.ktool = MagicMock() + screen.flasher.flash = MagicMock() + setattr(FlashScreen, "on_done", MagicMock()) + setattr(FlashScreen, "on_data", MagicMock()) + setattr(FlashScreen, "on_process", MagicMock()) + + # Define a custom excepthook + def mock_excepthook(args): + exc_type, exc_value, exc_traceback, thread = args.sequence + self.assertTrue(issubclass(exc_type, Exception)) + self.assertEqual(str(exc_value), "StopIteration mocked") + self.assertEqual(exc_traceback, None) + self.assertTrue(thread is mock_thread) + + # Patch threading.excepthook with the custom hook + with patch("threading.excepthook", mock_excepthook): + # Call the on_enter method + screen.on_enter() + + # Simulate the exception using ExceptHookArgs + exc_args = threading.ExceptHookArgs( + sequence=( + Exception, + Exception("StopIteration mocked"), + None, + mock_thread, + ) + ) + threading.excepthook(exc_args) + + # patch assertions + mock_get_locale.assert_called() + mock_partial.assert_called() + mock_thread.assert_called_once_with(name=screen.name, target=mock_partial()) + mock_redirect_exception.assert_called() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch("src.app.screens.flash_screen.partial") + @patch("src.app.screens.flash_screen.threading.Thread") + @patch("src.utils.flasher.Flasher") + @patch("src.app.screens.base_screen.BaseScreen.redirect_exception") + def test_on_enter_fail_cancel( + self, + mock_redirect_exception, + mock_flasher, + mock_thread, + mock_partial, + mock_get_locale, + ): + mock_flasher.__class__.print_callback = MagicMock() + + screen = FlashScreen() + screen.flasher = MagicMock() + screen.flasher.ktool = MagicMock() + screen.flasher.flash = MagicMock() + setattr(FlashScreen, "on_done", MagicMock()) + setattr(FlashScreen, "on_data", MagicMock()) + setattr(FlashScreen, "on_process", MagicMock()) + + # Define a custom excepthook + def mock_excepthook(args): + exc_type, exc_value, exc_traceback, thread = args.sequence + self.assertTrue(issubclass(exc_type, Exception)) + self.assertEqual(str(exc_value), "Cancel mocked") + self.assertEqual(exc_traceback, None) + self.assertTrue(thread is mock_thread) + + # Patch threading.excepthook with the custom hook + with patch("threading.excepthook", mock_excepthook): + # Call the on_enter method + screen.on_enter() + + # Simulate the exception using ExceptHookArgs + exc_args = threading.ExceptHookArgs( + sequence=(Exception, Exception("Cancel mocked"), None, mock_thread) + ) + threading.excepthook(exc_args) + + # patch assertions + mock_get_locale.assert_called() + mock_partial.assert_called() + mock_thread.assert_called_once_with(name=screen.name, target=mock_partial()) + mock_redirect_exception.assert_called() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch("src.app.screens.flash_screen.partial") + @patch("src.app.screens.flash_screen.threading.Thread") + @patch("src.utils.flasher.Flasher") + @patch("src.app.screens.base_screen.BaseScreen.redirect_exception") + def test_on_enter_fail_unknow( + self, + mock_redirect_exception, + mock_flasher, + mock_thread, + mock_partial, + mock_get_locale, + ): + mock_flasher.__class__.print_callback = MagicMock() + + screen = FlashScreen() + screen.flasher = MagicMock() + screen.flasher.ktool = MagicMock() + screen.flasher.flash = MagicMock() + setattr(FlashScreen, "on_done", MagicMock()) + setattr(FlashScreen, "on_data", MagicMock()) + setattr(FlashScreen, "on_process", MagicMock()) + + # Define a custom excepthook + def mock_excepthook(args): + exc_type, exc_value, exc_traceback, thread = args.sequence + self.assertTrue(issubclass(exc_type, Exception)) + self.assertEqual(str(exc_value), "Unknow mocked") + self.assertEqual(exc_traceback, None) + self.assertTrue(thread is mock_thread) + + # Patch threading.excepthook with the custom hook + with patch("threading.excepthook", mock_excepthook): + # Call the on_enter method + screen.on_enter() + + # Simulate the exception using ExceptHookArgs + exc_args = threading.ExceptHookArgs( + sequence=(Exception, Exception("Unknow mocked"), None, mock_thread) + ) + threading.excepthook(exc_args) + + # patch assertions + mock_get_locale.assert_called() + mock_partial.assert_called() + mock_thread.assert_called_once_with(name=screen.name, target=mock_partial()) + mock_redirect_exception.assert_called() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch("src.app.screens.base_screen.BaseScreen.set_screen") + def test_on_ref_press_back_after_done(self, mock_set_screen, mock_get_locale): + screen = FlashScreen() + screen.output = [] + screen.on_pre_enter() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + + on_done = getattr(FlashScreen, "on_done") + on_ref_press = getattr(FlashScreen, "on_ref_press_flash_screen_info") + + on_done(0) + on_ref_press(screen.ids["flash_screen_info"], "Back") + + # patch assertions + mock_get_locale.assert_any_call() + mock_set_screen.assert_called() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch("src.app.screens.base_screen.BaseScreen.quit_app") + def test_on_ref_press_quit_after_done(self, mock_quit_app, mock_get_locale): + screen = FlashScreen() + screen.output = [] + screen.on_pre_enter() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + + on_done = getattr(FlashScreen, "on_done") + on_ref_press = getattr(FlashScreen, "on_ref_press_flash_screen_info") + + on_done(0) + on_ref_press(screen.ids["flash_screen_info"], "Quit") + + # patch assertions + mock_get_locale.assert_any_call() + mock_quit_app.assert_called() diff --git a/e2e/test_023_wipe_screen.py b/e2e/test_023_wipe_screen.py index 972a174d..4619a9ee 100644 --- a/e2e/test_023_wipe_screen.py +++ b/e2e/test_023_wipe_screen.py @@ -1,4 +1,5 @@ import os +import threading from unittest.mock import patch, MagicMock, call from kivy.base import EventLoop, EventLoopBase from kivy.tests.common import GraphicUnitTest @@ -52,34 +53,23 @@ def test_init(self, mock_schedule_once, mock_partial, mock_get_locale): ) mock_schedule_once.assert_has_calls([call(mock_partial(), 0)], any_order=True) - @patch.object(EventLoopBase, "ensure_window", lambda x: None) @patch( "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" ) @patch("src.app.screens.base_screen.BaseScreen.redirect_exception") def test_fail_update_wrong_name(self, mock_redirect_exception, mock_get_locale): screen = WipeScreen() - self.render(screen) - - # get your Window instance safely - EventLoop.ensure_window() - screen.update(name="MockScreen") # patch assertions mock_get_locale.assert_called_once() mock_redirect_exception.assert_called_once() - @patch.object(EventLoopBase, "ensure_window", lambda x: None) @patch( "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" ) def test_update_locale(self, mock_get_locale): screen = WipeScreen() - self.render(screen) - - # get your Window instance safely - EventLoop.ensure_window() screen.update(name=screen.name, key="locale", value="en_US.UTF-8") self.assertEqual(screen.locale, "en_US.UTF-8") @@ -87,16 +77,11 @@ def test_update_locale(self, mock_get_locale): # patch assertions mock_get_locale.assert_called_once() - @patch.object(EventLoopBase, "ensure_window", lambda x: None) @patch( "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" ) def test_update_device(self, mock_get_locale): screen = WipeScreen() - self.render(screen) - - # get your Window instance safely - EventLoop.ensure_window() screen.update(name=screen.name, key="device", value="amigo") self.assertEqual(screen.device, "amigo") @@ -104,16 +89,13 @@ def test_update_device(self, mock_get_locale): # patch assertions mock_get_locale.asset_called_once() - @patch.object(EventLoopBase, "ensure_window", lambda x: None) @patch( "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" ) def test_update_wiper(self, mock_get_locale): screen = WipeScreen() - self.render(screen) # get your Window instance safely - EventLoop.ensure_window() screen.update(name=screen.name, key="wiper", value=1500000) self.assertEqual(screen.wiper.baudrate, 1500000) @@ -142,18 +124,34 @@ def test_on_pre_enter(self, mock_get_locale): # patch assertions mock_get_locale.assert_any_call() - @patch.object(EventLoopBase, "ensure_window", lambda x: None) @patch( "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" ) - def test_on_data(self, mock_get_locale): + def test_greeting_fail_on_data_mock(self, mock_get_locale): screen = WipeScreen() + screen.wiper = MagicMock() + screen.wiper.ktool.kill = MagicMock() + screen.wiper.ktool.checkKillExit = MagicMock() screen.output = [] screen.on_pre_enter() - self.render(screen) - # get your Window instance safely - EventLoop.ensure_window() + on_data = getattr(WipeScreen, "on_data") + on_data("Greeting fail: mock") + + self.assertEqual(screen.fail_msg, "Greeting fail: mock") + + # patch assertions + mock_get_locale.assert_called() + screen.wiper.ktool.kill.assert_called() + screen.wiper.ktool.checkKillExit.assert_called() + + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + def test_on_data(self, mock_get_locale): + screen = WipeScreen() + screen.output = [] + screen.on_pre_enter() on_data = getattr(WipeScreen, "on_data") on_data("[color=#00ff00] INFO [/color] mock") @@ -162,7 +160,6 @@ def test_on_data(self, mock_get_locale): # patch assertions mock_get_locale.assert_any_call() - @patch.object(EventLoopBase, "ensure_window", lambda x: None) @patch( "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" ) @@ -170,11 +167,6 @@ def test_on_print_callback_pop_ouput(self, mock_get_locale): screen = WipeScreen() screen.output = [] screen.on_pre_enter() - self.render(screen) - - # get your Window instance safely - EventLoop.ensure_window() - on_data = getattr(WipeScreen, "on_data") for i in range(4): @@ -185,7 +177,6 @@ def test_on_print_callback_pop_ouput(self, mock_get_locale): # patch assertions mock_get_locale.assert_any_call() - @patch.object(EventLoopBase, "ensure_window", lambda x: None) @patch( "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" ) @@ -194,10 +185,6 @@ def test_on_data_erased(self, mock_done, mock_get_locale): screen = WipeScreen() screen.output = [] screen.on_pre_enter() - self.render(screen) - - # get your Window instance safely - EventLoop.ensure_window() on_data = getattr(WipeScreen, "on_data") on_data("[color=#00ff00] INFO [/color] SPI Flash erased.") @@ -225,6 +212,8 @@ def test_on_done(self, mock_get_locale): [ "[b]DONE![/b]", "\n", + "disconnect and reconnect device before flash again", + "\n", "[color=#00FF00]", "[ref=Back][u]Back[/u][/ref]", "[/color]", @@ -243,7 +232,6 @@ def test_on_done(self, mock_get_locale): # patch assertions mock_get_locale.assert_any_call() - @patch.object(EventLoopBase, "ensure_window", lambda x: None) @patch( "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" ) @@ -277,3 +265,209 @@ def test_on_enter(self, mock_flasher, mock_thread, mock_partial, mock_get_locale any_order=True, ) mock_thread.assert_called_once_with(name=screen.name, target=mock_partial()) + + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch("src.app.screens.wipe_screen.partial") + @patch("src.app.screens.wipe_screen.threading.Thread") + @patch("src.utils.flasher.Flasher") + @patch("src.app.screens.base_screen.BaseScreen.redirect_exception") + def test_on_enter_fail_stopiteration( + self, + mock_redirect_exception, + mock_flasher, + mock_thread, + mock_partial, + mock_get_locale, + ): + mock_flasher.__class__.print_callback = MagicMock() + + screen = WipeScreen() + screen.wiper = MagicMock() + screen.wiper.ktool = MagicMock() + screen.wiper.flash = MagicMock() + setattr(WipeScreen, "on_done", MagicMock()) + setattr(WipeScreen, "on_data", MagicMock()) + setattr(WipeScreen, "on_process", MagicMock()) + + # Define a custom excepthook + def mock_excepthook(args): + exc_type, exc_value, exc_traceback, thread = args.sequence + self.assertTrue(issubclass(exc_type, Exception)) + self.assertEqual(str(exc_value), "StopIteration mocked") + self.assertEqual(exc_traceback, None) + self.assertTrue(thread is mock_thread) + + # Patch threading.excepthook with the custom hook + with patch("threading.excepthook", mock_excepthook): + # Call the on_enter method + screen.on_pre_enter() + screen.on_enter() + + # Simulate the exception using ExceptHookArgs + exc_args = threading.ExceptHookArgs( + sequence=( + Exception, + Exception("StopIteration mocked"), + None, + mock_thread, + ) + ) + threading.excepthook(exc_args) + + # patch assertions + mock_get_locale.assert_called() + mock_partial.assert_called() + mock_thread.assert_called_once_with(name=screen.name, target=mock_partial()) + mock_redirect_exception.assert_called() + + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch("src.app.screens.wipe_screen.partial") + @patch("src.app.screens.wipe_screen.threading.Thread") + @patch("src.utils.flasher.Flasher") + @patch("src.app.screens.base_screen.BaseScreen.redirect_exception") + def test_on_enter_fail_cancel( + self, + mock_redirect_exception, + mock_flasher, + mock_thread, + mock_partial, + mock_get_locale, + ): + mock_flasher.__class__.print_callback = MagicMock() + + screen = WipeScreen() + screen.wiper = MagicMock() + screen.wiper.ktool = MagicMock() + screen.wiper.flash = MagicMock() + setattr(WipeScreen, "on_done", MagicMock()) + setattr(WipeScreen, "on_data", MagicMock()) + setattr(WipeScreen, "on_process", MagicMock()) + + # Define a custom excepthook + def mock_excepthook(args): + exc_type, exc_value, exc_traceback, thread = args.sequence + self.assertTrue(issubclass(exc_type, Exception)) + self.assertEqual(str(exc_value), "Cancel mocked") + self.assertEqual(exc_traceback, None) + self.assertTrue(thread is mock_thread) + + # Patch threading.excepthook with the custom hook + with patch("threading.excepthook", mock_excepthook): + # Call the on_enter method + screen.on_pre_enter() + screen.on_enter() + + # Simulate the exception using ExceptHookArgs + exc_args = threading.ExceptHookArgs( + sequence=( + Exception, + Exception("Cancel mocked"), + None, + mock_thread, + ) + ) + threading.excepthook(exc_args) + + # patch assertions + mock_get_locale.assert_called() + mock_partial.assert_called() + mock_thread.assert_called_once_with(name=screen.name, target=mock_partial()) + mock_redirect_exception.assert_called() + + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch("src.app.screens.wipe_screen.partial") + @patch("src.app.screens.wipe_screen.threading.Thread") + @patch("src.utils.flasher.Flasher") + @patch("src.app.screens.base_screen.BaseScreen.redirect_exception") + def test_on_enter_fail_unknow( + self, + mock_redirect_exception, + mock_flasher, + mock_thread, + mock_partial, + mock_get_locale, + ): + mock_flasher.__class__.print_callback = MagicMock() + + screen = WipeScreen() + screen.wiper = MagicMock() + screen.wiper.ktool = MagicMock() + screen.wiper.flash = MagicMock() + setattr(WipeScreen, "on_done", MagicMock()) + setattr(WipeScreen, "on_data", MagicMock()) + setattr(WipeScreen, "on_process", MagicMock()) + + # Define a custom excepthook + def mock_excepthook(args): + exc_type, exc_value, exc_traceback, thread = args.sequence + self.assertTrue(issubclass(exc_type, Exception)) + self.assertEqual(str(exc_value), "Unknow mocked") + self.assertEqual(exc_traceback, None) + self.assertTrue(thread is mock_thread) + + # Patch threading.excepthook with the custom hook + with patch("threading.excepthook", mock_excepthook): + # Call the on_enter method + screen.on_pre_enter() + screen.on_enter() + + # Simulate the exception using ExceptHookArgs + exc_args = threading.ExceptHookArgs( + sequence=( + Exception, + Exception("Unknow mocked"), + None, + mock_thread, + ) + ) + threading.excepthook(exc_args) + + # patch assertions + mock_get_locale.assert_called() + mock_partial.assert_called() + mock_thread.assert_called_once_with(name=screen.name, target=mock_partial()) + mock_redirect_exception.assert_called() + + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch("src.app.screens.base_screen.BaseScreen.set_screen") + def test_on_ref_press_back_after_done(self, mock_set_screen, mock_get_locale): + screen = WipeScreen() + screen.output = [] + screen.on_pre_enter() + + on_done = getattr(WipeScreen, "on_done") + on_ref_press = getattr(WipeScreen, "on_ref_press_wipe_screen_info") + + on_done(0) + on_ref_press(screen.ids["wipe_screen_info"], "Back") + + # patch assertions + mock_get_locale.assert_any_call() + mock_set_screen.assert_called() + + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch("src.app.screens.base_screen.BaseScreen.quit_app") + def test_on_ref_press_quit_after_done(self, mock_quit_app, mock_get_locale): + screen = WipeScreen() + screen.output = [] + screen.on_pre_enter() + + on_done = getattr(WipeScreen, "on_done") + on_ref_press = getattr(WipeScreen, "on_ref_press_wipe_screen_info") + + on_done(0) + on_ref_press(screen.ids["wipe_screen_info"], "Quit") + + # patch assertions + mock_get_locale.assert_any_call() + mock_quit_app.assert_called() diff --git a/e2e/test_024_warning_wipe_screen.py b/e2e/test_024_warning_wipe_screen.py index 011b96cb..8e0d2870 100644 --- a/e2e/test_024_warning_wipe_screen.py +++ b/e2e/test_024_warning_wipe_screen.py @@ -1,6 +1,6 @@ import os from unittest.mock import patch, MagicMock, call -from kivy.base import EventLoop, EventLoopBase +from kivy.base import EventLoop from kivy.tests.common import GraphicUnitTest from kivy.core.text import LabelBase, DEFAULT_FONT from src.app.screens.warning_wipe_screen import WarningWipeScreen @@ -21,7 +21,6 @@ def setUpClass(cls): def teardown_class(cls): EventLoop.exit() - @patch.object(EventLoopBase, "ensure_window", lambda x: None) @patch( "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" ) @@ -29,20 +28,13 @@ def teardown_class(cls): @patch("src.app.screens.warning_wipe_screen.Clock.schedule_once") def test_init(self, mock_schedule_once, mock_partial, mock_get_locale): screen = WarningWipeScreen() - self.render(screen) - - # get your Window instance safely - EventLoop.ensure_window() - window = EventLoop.window - grid = window.children[0].children[0] - image = grid.children[1] - label = grid.children[0] # default assertions - self.assertEqual(grid.id, "warning_wipe_screen_grid") + grid = screen.ids[f"{screen.id}_grid"] + self.assertTrue("warning_wipe_screen_grid" in screen.ids) self.assertEqual(len(grid.children), 2) - self.assertEqual(image.id, "warning_wipe_screen_warn") - self.assertEqual(label.id, "warning_wipe_screen_label") + self.assertTrue("warning_wipe_screen_warn" in screen.ids) + self.assertTrue("warning_wipe_screen_label" in screen.ids) # patch assertions mock_get_locale.assert_called_once() @@ -51,19 +43,12 @@ def test_init(self, mock_schedule_once, mock_partial, mock_get_locale): ) mock_schedule_once.assert_has_calls([call(mock_partial(), 0)], any_order=True) - @patch.object(EventLoopBase, "ensure_window", lambda x: None) @patch( "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" ) def test_make_label_text(self, mock_get_locale): screen = WarningWipeScreen() - self.render(screen) - - # get your Window instance safely - EventLoop.ensure_window() - window = EventLoop.window - grid = window.children[0].children[0] - label = grid.children[0] + label = screen.ids[f"{screen.id}_label"] screen.update(name=screen.name, key="locale", value="en_US.UTF-8") text = "".join( @@ -93,7 +78,6 @@ def test_make_label_text(self, mock_get_locale): # patch assertions mock_get_locale.assert_called_once() - @patch.object(EventLoopBase, "ensure_window", lambda x: None) @patch( "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" ) @@ -103,10 +87,8 @@ def test_make_label_text(self, mock_get_locale): ) def test_update_locale(self, mock_make_label_text, mock_get_locale): screen = WarningWipeScreen() - self.render(screen) # get your Window instance safely - EventLoop.ensure_window() screen.update(name=screen.name, key="locale", value="en_US.UTF8") self.assertEqual(screen.locale, "en_US.UTF8") @@ -115,7 +97,6 @@ def test_update_locale(self, mock_make_label_text, mock_get_locale): mock_get_locale.assert_called_once() mock_make_label_text.assert_any_call() - @patch.object(EventLoopBase, "ensure_window", lambda x: None) @patch( "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" ) @@ -125,17 +106,12 @@ def test_update_locale(self, mock_make_label_text, mock_get_locale): ) def test_on_enter(self, mock_make_label_text, mock_get_locale): screen = WarningWipeScreen() - self.render(screen) screen.on_enter() - # get your Window instance safely - EventLoop.ensure_window() - # patch assertions mock_get_locale.assert_any_call() mock_make_label_text.assert_called_once() - @patch.object(EventLoopBase, "ensure_window", lambda x: None) @patch( "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" ) @@ -149,17 +125,10 @@ def test_on_ref_press_proceed( screen.manager = MagicMock() screen.manager.get_screen = MagicMock() - self.render(screen) - - # get your Window instance safely - EventLoop.ensure_window() - window = EventLoop.window - grid = window.children[0].children[0] - label = grid.children[0] button = screen.ids[f"{screen.id}_label"] action = getattr(screen.__class__, f"on_ref_press_{button.id}") - action(label, "WipeScreen") + action(button, "WipeScreen") mock_get_locale.assert_any_call() screen.manager.get_screen.assert_has_calls( @@ -187,7 +156,6 @@ def test_on_ref_press_proceed( [call(mock_partial(), 0), call(mock_partial(), 0)], any_order=True ) - @patch.object(EventLoopBase, "ensure_window", lambda x: None) @patch( "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" ) @@ -197,16 +165,10 @@ def test_on_ref_press_deny(self, mock_set_screen, mock_get_locale): screen.manager = MagicMock() screen.manager.get_screen = MagicMock() - self.render(screen) - # get your Window instance safely - EventLoop.ensure_window() - window = EventLoop.window - grid = window.children[0].children[0] - label = grid.children[0] button = screen.ids[f"{screen.id}_label"] action = getattr(screen.__class__, f"on_ref_press_{button.id}") - action(label, "MainScreen") + action(button, "MainScreen") mock_get_locale.assert_any_call() mock_set_screen.assert_called_once_with(name="MainScreen", direction="right") diff --git a/e2e/test_025_error_screen.py b/e2e/test_025_error_screen.py index 29a76cd8..bac00ded 100644 --- a/e2e/test_025_error_screen.py +++ b/e2e/test_025_error_screen.py @@ -1,6 +1,6 @@ import os from unittest.mock import patch, MagicMock -from kivy.base import EventLoop, EventLoopBase +from kivy.base import EventLoop from kivy.tests.common import GraphicUnitTest from kivy.core.text import LabelBase, DEFAULT_FONT from src.app.screens.error_screen import ErrorScreen @@ -21,29 +21,19 @@ def setUpClass(cls): def teardown_class(cls): EventLoop.exit() - @patch.object(EventLoopBase, "ensure_window", lambda x: None) @patch( "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" ) def test_init(self, mock_get_locale): screen = ErrorScreen() - self.render(screen) - - # get your Window instance safely - EventLoop.ensure_window() - window = EventLoop.window - grid = window.children[0].children[0] - label = grid.children[0] # default assertions - self.assertEqual(grid.id, "error_screen_grid") - self.assertEqual(len(grid.children), 1) - self.assertEqual(label.id, "error_screen_label") + self.assertTrue("error_screen_grid" in screen.ids) + self.assertTrue("error_screen_label" in screen.ids) # patch assertions mock_get_locale.assert_called_once() - @patch.object(EventLoopBase, "ensure_window", lambda x: None) @patch( "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" ) @@ -51,10 +41,8 @@ def test_make_label_text(self, mock_get_locale): screen = ErrorScreen() screen.manager = MagicMock() screen.manager.screen_names = ["KruxInstallerApp", screen.name] - self.render(screen) # get your Window instance safely - EventLoop.ensure_window() label = screen.ids[f"{screen.id}_label"] error = RuntimeError("Error: mocked error: at test") @@ -93,32 +81,22 @@ def test_make_label_text(self, mock_get_locale): # patch assertions mock_get_locale.assert_called_once() - @patch.object(EventLoopBase, "ensure_window", lambda x: None) @patch( "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" ) @patch("src.app.screens.error_screen.ErrorScreen.set_screen") def test_on_ref_press_back(self, mock_set_screen, mock_get_locale): screen = ErrorScreen() - - self.render(screen) - - # get your Window instance safely - EventLoop.ensure_window() - window = EventLoop.window - grid = window.children[0].children[0] - label = grid.children[0] button = screen.ids[f"{screen.id}_label"] action = getattr(ErrorScreen, f"on_ref_press_{button.id}") - action(label, "Back") + action(button, "Back") mock_get_locale.assert_any_call() mock_set_screen.assert_called_once_with( name="GreetingsScreen", direction="right" ) - @patch.object(EventLoopBase, "ensure_window", lambda x: None) @patch( "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" ) @@ -128,22 +106,14 @@ def test_on_ref_press_quit(self, mock_quit_app, mock_get_locale): screen.manager = MagicMock() screen.manager.get_screen = MagicMock() - self.render(screen) - - # get your Window instance safely - EventLoop.ensure_window() - window = EventLoop.window - grid = window.children[0].children[0] - label = grid.children[0] button = screen.ids[f"{screen.id}_label"] action = getattr(ErrorScreen, f"on_ref_press_{button.id}") - action(label, "Quit") + action(button, "Quit") mock_get_locale.assert_any_call() mock_quit_app.assert_called_once() - @patch.object(EventLoopBase, "ensure_window", lambda x: None) @patch( "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" ) @@ -153,17 +123,10 @@ def test_on_ref_press_report(self, mock_web_open, mock_get_locale): screen.manager = MagicMock() screen.manager.get_screen = MagicMock() - self.render(screen) - - # get your Window instance safely - EventLoop.ensure_window() - window = EventLoop.window - grid = window.children[0].children[0] - label = grid.children[0] button = screen.ids[f"{screen.id}_label"] action = getattr(ErrorScreen, f"on_ref_press_{button.id}") - action(label, "ReportIssue") + action(button, "ReportIssue") mock_get_locale.assert_any_call() mock_web_open.assert_called_once_with( diff --git a/e2e/test_026_warning_before_airgap_update_screen.py b/e2e/test_026_warning_before_airgap_update_screen.py new file mode 100644 index 00000000..61241283 --- /dev/null +++ b/e2e/test_026_warning_before_airgap_update_screen.py @@ -0,0 +1,297 @@ +import os +from unittest.mock import patch, MagicMock +from kivy.base import EventLoop, EventLoopBase +from kivy.tests.common import GraphicUnitTest +from kivy.core.text import LabelBase, DEFAULT_FONT +from src.app.screens.warning_before_airgap_update_screen import ( + WarningBeforeAirgapUpdateScreen, +) + + +class TestWarningBeforeAirgapUpdateScreen(GraphicUnitTest): + + @classmethod + def setUpClass(cls): + cwd_path = os.path.dirname(__file__) + rel_assets_path = os.path.join(cwd_path, "..", "assets") + assets_path = os.path.abspath(rel_assets_path) + font_name = "NotoSansCJK_CY_JP_SC_KR_VI_Krux.ttf" + noto_sans_path = os.path.join(assets_path, font_name) + LabelBase.register(DEFAULT_FONT, noto_sans_path) + + @classmethod + def teardown_class(cls): + EventLoop.exit() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + def test_render_main_screen(self, mock_get_locale): + screen = WarningBeforeAirgapUpdateScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + window = EventLoop.window + grid = window.children[0].children[0] + warn = grid.children[1] + button = grid.children[0] + + self.assertEqual(window.children[0], screen) + self.assertEqual(screen.name, "WarningBeforeAirgapUpdateScreen") + self.assertEqual(screen.id, "warning_before_airgap_update_screen") + self.assertEqual(grid.id, "warning_before_airgap_update_screen_grid") + self.assertEqual(warn.id, "warning_before_airgap_update_screen_warn") + self.assertEqual(button.id, "warning_before_airgap_update_screen_label") + + text = "".join( + [ + "[color=#efcc00]Before proceeding with the air-gapped update:[/color]", + "\n", + "* Insert a FAT32 formatted SDCard into your computer", + "\n", + "* On the next screen, choose the drive to copy firmware", + "\n", + "\n", + "[color=#ff0000][ref=MainScreen]Back[/ref][/color] [color=#00ff00][ref=AirgapUpdateScreen]Proceed[/ref][/color]", + ] + ) + + self.assertEqual(button.text, text) + mock_get_locale.assert_any_call() + + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch("src.app.screens.base_screen.BaseScreen.set_screen") + def test_on_ref_press_back(self, mock_set_screen, mock_get_locale): + screen = WarningBeforeAirgapUpdateScreen() + + action = getattr( + screen, "on_ref_press_warning_before_airgap_update_screen_label" + ) + action("MainScreen") + + mock_set_screen.assert_called_once_with(name="MainScreen", direction="left") + mock_get_locale.assert_any_call() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + def test_on_update(self, mock_get_locale): + screen = WarningBeforeAirgapUpdateScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + + screen.update(name=screen.name, key="locale", value="en_US.UTF-8") + + mock_get_locale.assert_any_call() + + @patch("sys.platform", "linux") + # @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch( + "src.app.screens.base_screen.BaseScreen.on_get_removable_drives_linux", + return_value=[], + ) + def test_on_ref_press_no_drives_found_linux( + self, mock_on_get_removable_drives_linux, mock_get_locale + ): + screen = WarningBeforeAirgapUpdateScreen() + screen.manager = MagicMock() + screen.manager.get_screen = MagicMock() + # self.render(screen) + + # get your Window instance safely + # EventLoop.ensure_window() + + action = getattr( + screen, "on_ref_press_warning_before_airgap_update_screen_label" + ) + action("AirgapUpdateScreen") + + mock_get_locale.assert_any_call() + mock_on_get_removable_drives_linux.assert_called_once() + + @patch("sys.platform", "darwin") + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch( + "src.app.screens.base_screen.BaseScreen.on_get_removable_drives_macos", + return_value=[], + ) + def test_on_ref_press_no_drives_found_darwin( + self, on_get_removable_drives_macos, mock_get_locale + ): + screen = WarningBeforeAirgapUpdateScreen() + screen.manager = MagicMock() + screen.manager.get_screen = MagicMock() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + + action = getattr( + screen, "on_ref_press_warning_before_airgap_update_screen_label" + ) + action("AirgapUpdateScreen") + + mock_get_locale.assert_any_call() + on_get_removable_drives_macos.assert_called_once() + + @patch("sys.platform", "win32") + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch( + "src.app.screens.base_screen.BaseScreen.on_get_removable_drives_windows", + return_value=[], + ) + def test_on_ref_press_no_drives_found_windows( + self, on_get_removable_drives_windows, mock_get_locale + ): + screen = WarningBeforeAirgapUpdateScreen() + screen.manager = MagicMock() + screen.manager.get_screen = MagicMock() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + + action = getattr( + screen, "on_ref_press_warning_before_airgap_update_screen_label" + ) + action("AirgapUpdateScreen") + + mock_get_locale.assert_any_call() + on_get_removable_drives_windows.assert_called_once() + + @patch("sys.platform", "linux") + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch( + "src.app.screens.base_screen.BaseScreen.on_get_removable_drives_linux", + return_value=["/media/mock"], + ) + @patch("src.app.screens.warning_before_airgap_update_screen.partial") + @patch("src.app.screens.warning_before_airgap_update_screen.Clock.schedule_once") + @patch("src.app.screens.base_screen.BaseScreen.set_screen") + def test_on_ref_press_drives_found_linux( + self, + mock_set_screen, + mock_schedule_once, + mock_partial, + mock_on_get_removable_drives_linux, + mock_get_locale, + ): + screen = WarningBeforeAirgapUpdateScreen() + screen.manager = MagicMock() + screen.manager.get_screen = MagicMock() + + action = getattr( + screen, "on_ref_press_warning_before_airgap_update_screen_label" + ) + action("AirgapUpdateScreen") + + mock_get_locale.assert_any_call() + mock_on_get_removable_drives_linux.assert_called_once() + screen.manager.get_screen.assert_called_once_with("AirgapUpdateScreen") + mock_partial.assert_called() + mock_schedule_once.assert_called() + mock_set_screen.assert_called_once_with( + name="AirgapUpdateScreen", direction="right" + ) + + @patch("sys.platform", "darwin") + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch( + "src.app.screens.base_screen.BaseScreen.on_get_removable_drives_macos", + return_value=["/Volumes/mock"], + ) + @patch("src.app.screens.warning_before_airgap_update_screen.partial") + @patch("src.app.screens.warning_before_airgap_update_screen.Clock.schedule_once") + @patch("src.app.screens.base_screen.BaseScreen.set_screen") + def test_on_ref_press_drives_found_macos( + self, + mock_set_screen, + mock_schedule_once, + mock_partial, + mock_on_get_removable_drives_macos, + mock_get_locale, + ): + screen = WarningBeforeAirgapUpdateScreen() + screen.manager = MagicMock() + screen.manager.get_screen = MagicMock() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + + action = getattr( + screen, "on_ref_press_warning_before_airgap_update_screen_label" + ) + action("AirgapUpdateScreen") + + mock_get_locale.assert_any_call() + mock_on_get_removable_drives_macos.assert_called_once() + screen.manager.get_screen.assert_called_once_with("AirgapUpdateScreen") + mock_partial.assert_called() + mock_schedule_once.assert_called() + mock_set_screen.assert_called_once_with( + name="AirgapUpdateScreen", direction="right" + ) + + @patch("sys.platform", "win32") + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch( + "src.app.screens.base_screen.BaseScreen.on_get_removable_drives_windows", + return_value=["D:\\"], + ) + @patch("src.app.screens.warning_before_airgap_update_screen.partial") + @patch("src.app.screens.warning_before_airgap_update_screen.Clock.schedule_once") + @patch("src.app.screens.base_screen.BaseScreen.set_screen") + def test_on_ref_press_drives_found_windows( + self, + mock_set_screen, + mock_schedule_once, + mock_partial, + mock_on_get_removable_drives_windows, + mock_get_locale, + ): + screen = WarningBeforeAirgapUpdateScreen() + screen.manager = MagicMock() + screen.manager.get_screen = MagicMock() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + + action = getattr( + screen, "on_ref_press_warning_before_airgap_update_screen_label" + ) + action("AirgapUpdateScreen") + + mock_get_locale.assert_any_call() + mock_on_get_removable_drives_windows.assert_called_once() + screen.manager.get_screen.assert_called_once_with("AirgapUpdateScreen") + mock_partial.assert_called() + mock_schedule_once.assert_called() + mock_set_screen.assert_called_once_with( + name="AirgapUpdateScreen", direction="right" + ) diff --git a/e2e/test_027_airgap_update_screen.py b/e2e/test_027_airgap_update_screen.py new file mode 100644 index 00000000..7564af60 --- /dev/null +++ b/e2e/test_027_airgap_update_screen.py @@ -0,0 +1,176 @@ +import os +from unittest.mock import patch, MagicMock, mock_open +from kivy.base import EventLoop, EventLoopBase +from kivy.tests.common import GraphicUnitTest +from kivy.core.text import LabelBase, DEFAULT_FONT +from src.app.screens.airgap_update_screen import ( + AirgapUpdateScreen, +) + +# pylint: disable=line-too-long +MOCK_SIG = b"0E\x02!\x00\xed\xfb\xb2\x99\x06\x99\x97fDQ\x0f%\xdf=\xe7^h\xd1\xb6n\x16\x9cBm\xc4\xcc\xbbb:P\xb5#\x02 f\xee\xf8\x95\xfd'sqH\x9eO\xa3x\xb6>\xdc\x83\x96\xd1\xf7\x92\xcf&W\xf4n\xc0\xd3\xc8\xfe\xd3\xfd" + + +class TestAirgapUpdateScreen(GraphicUnitTest): + + @classmethod + def setUpClass(cls): + cwd_path = os.path.dirname(__file__) + rel_assets_path = os.path.join(cwd_path, "..", "assets") + assets_path = os.path.abspath(rel_assets_path) + font_name = "NotoSansCJK_CY_JP_SC_KR_VI_Krux.ttf" + noto_sans_path = os.path.join(assets_path, font_name) + LabelBase.register(DEFAULT_FONT, noto_sans_path) + + @classmethod + def teardown_class(cls): + EventLoop.exit() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + def test_init(self, mock_get_locale): + screen = AirgapUpdateScreen() + self.render(screen) + + self.assertEqual(screen.firmware_bin, "") + self.assertEqual(screen.firmware_sig, "") + + mock_get_locale.assert_any_call() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + def test_update_firmware_bin(self, mock_get_locale): + screen = AirgapUpdateScreen() + screen.update( + name=screen.name, key="binary", value=os.path.join("mock", "firmware.bin") + ) + + self.assertEqual(screen.firmware_bin, os.path.join("mock", "firmware.bin")) + mock_get_locale.assert_any_call() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + def test_update_firmware_sig(self, mock_get_locale): + screen = AirgapUpdateScreen() + screen.update( + name=screen.name, + key="signature", + value=os.path.join("mock", "firmware.bin.sig"), + ) + + self.assertEqual(screen.firmware_sig, os.path.join("mock", "firmware.bin.sig")) + mock_get_locale.assert_any_call() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + def test_update_drives(self, mock_get_locale): + screen = AirgapUpdateScreen() + screen.update(name=screen.name, key="drives", value=[os.path.join("mock", "0")]) + + self.assertTrue(f"{screen.id}_grid" in screen.ids) + self.assertTrue(f"{screen.id}_button_0" in screen.ids) + + button_0_text = screen.ids[f"{screen.id}_button_0"].text + self.assertEqual( + button_0_text, + "".join( + [ + "Select", + "\n", + f"[color=#efcc00]{os.path.join("mock", "0")}[/color]", + "\n", + "to copy firmware", + ] + ), + ) + mock_get_locale.assert_any_call() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + def test_on_leave(self, mock_get_locale): + screen = AirgapUpdateScreen() + screen.update(name=screen.name, key="drives", value=[os.path.join("mock", "0")]) + + self.assertTrue(f"{screen.id}_grid" in screen.ids) + self.assertTrue(f"{screen.id}_button_0" in screen.ids) + + # now clean + screen.on_leave() + self.assertTrue(f"{screen.id}_grid" not in screen.ids) + + mock_get_locale.assert_any_call() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch("src.app.screens.base_screen.BaseScreen.set_background") + def test_on_press_button(self, mock_set_background, mock_get_locale): + screen = AirgapUpdateScreen() + screen.update(name=screen.name, key="drives", value=[os.path.join("mock", "0")]) + + action = getattr(AirgapUpdateScreen, f"on_press_{screen.id}_button_0") + action(screen.ids[f"{screen.id}_button_0"]) + + mock_get_locale.assert_any_call() + mock_set_background.assert_called_once_with( + wid=f"{screen.id}_button_0", rgba=(0.25, 0.25, 0.25, 1) + ) + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch("src.app.screens.airgap_update_screen.Clock.schedule_once") + @patch("src.app.screens.airgap_update_screen.partial") + @patch("src.app.screens.base_screen.BaseScreen.set_background") + @patch("src.app.screens.base_screen.BaseScreen.set_screen") + @patch("src.app.screens.airgap_update_screen.shutil.copyfile") + @patch("src.utils.verifyer.sha256_verifyer.os.path.exists", return_value=True) + @patch( + "src.utils.verifyer.sha256_verifyer.open", + new_callable=mock_open, + read_data=MOCK_SIG, + ) + def test_on_release_button( + self, + open_mock, + mock_exists, + mock_copyfile, + mock_set_screen, + mock_set_background, + mock_partial, + mock_schedule_once, + mock_get_locale, + ): + screen = AirgapUpdateScreen() + screen.manager = MagicMock() + screen.manager.get_screen = MagicMock() + + screen.update(name=screen.name, key="drives", value=[os.path.join("mock", "0")]) + + action = getattr(AirgapUpdateScreen, f"on_release_{screen.id}_button_0") + action(screen.ids[f"{screen.id}_button_0"]) + + mock_get_locale.assert_any_call() + mock_partial.assert_called() + mock_schedule_once.assert_called() + mock_set_background.assert_called_once_with( + wid=f"{screen.id}_button_0", rgba=(0, 0, 0, 1) + ) + mock_set_screen.assert_called_once_with( + name="WarningAfterAirgapUpdateScreen", direction="left" + ) + mock_copyfile.assert_called() + mock_exists.assert_called() + open_mock.assert_called() diff --git a/e2e/test_028_warning_after_airgap_update_screen.py b/e2e/test_028_warning_after_airgap_update_screen.py new file mode 100644 index 00000000..059a9388 --- /dev/null +++ b/e2e/test_028_warning_after_airgap_update_screen.py @@ -0,0 +1,125 @@ +import os +from unittest.mock import patch +from kivy.base import EventLoop, EventLoopBase +from kivy.tests.common import GraphicUnitTest +from kivy.core.text import LabelBase, DEFAULT_FONT +from src.app.screens.warning_after_airgap_update_screen import ( + WarningAfterAirgapUpdateScreen, +) + + +class TestWarningAfterAirgapUpdateScreen(GraphicUnitTest): + + @classmethod + def setUpClass(cls): + cwd_path = os.path.dirname(__file__) + rel_assets_path = os.path.join(cwd_path, "..", "assets") + assets_path = os.path.abspath(rel_assets_path) + font_name = "NotoSansCJK_CY_JP_SC_KR_VI_Krux.ttf" + noto_sans_path = os.path.join(assets_path, font_name) + LabelBase.register(DEFAULT_FONT, noto_sans_path) + + @classmethod + def teardown_class(cls): + EventLoop.exit() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + def test_render_main_screen(self, mock_get_locale): + screen = WarningAfterAirgapUpdateScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + window = EventLoop.window + + self.assertEqual(window.children[0], screen) + self.assertEqual(screen.name, "WarningAfterAirgapUpdateScreen") + self.assertEqual(screen.id, "warning_after_airgap_update_screen") + self.assertTrue("warning_after_airgap_update_screen_grid" in screen.ids) + self.assertTrue("warning_after_airgap_update_screen_subgrid" in screen.ids) + self.assertTrue("warning_after_airgap_update_screen_done" in screen.ids) + self.assertTrue("warning_after_airgap_update_screen_menu" in screen.ids) + self.assertTrue("warning_after_airgap_update_screen_label" in screen.ids) + + mock_get_locale.assert_called() + + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch( + "src.app.screens.warning_after_airgap_update_screen.WarningAfterAirgapUpdateScreen.quit_app" + ) + def test_on_ref_press_quit(self, mock_quit_app, mock_get_locale): + screen = WarningAfterAirgapUpdateScreen() + + action = getattr(screen, "on_ref_press_warning_after_airgap_update_screen_menu") + action("Quit") + + mock_quit_app.assert_called_once() + mock_get_locale.assert_any_call() + + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch("src.app.screens.base_screen.BaseScreen.set_screen") + def test_on_ref_press_back(self, mock_set_screen, mock_get_locale): + screen = WarningAfterAirgapUpdateScreen() + + action = getattr(screen, "on_ref_press_warning_after_airgap_update_screen_menu") + action("MainScreen") + + mock_set_screen.assert_called_once_with(name="MainScreen", direction="right") + mock_get_locale.assert_any_call() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + def test_on_update_locale(self, mock_get_locale): + screen = WarningAfterAirgapUpdateScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + + screen.update(name=screen.name, key="label") + screen.update(name=screen.name, key="sdcard", value=os.path.join("tmp", "mock")) + screen.update(name=screen.name, key="hash", value="abcdef01234567890a") + screen.update(name=screen.name, key="locale", value="en_US.UTF-8") + + text_menu = "".join( + [ + ".bin and .sig have been copied to", + "\n", + f"[color=#efcc00]{os.path.join('tmp', 'mock')}[/color].", + "\n", + "\n", + "[color=#ff0000]", + "[u][ref=Quit]Quit[/ref][/u]", + "[/color]", + " ", + "[color=#00ff00]", + "[u][ref=MainScreen]Back[/ref][/u]", + "[/color]", + ] + ) + + text_label = "".join( + [ + "* Insert the SDcard into your device and reboot it to update.", + "\n", + "\n", + "* You should see this computed hash on device screen:", + "\n", + "\n", + "[color=#efcc00]ab cd ef 01 23 45 67 89 0a", + "[/color]", + ] + ) + + self.assertEqual(screen.ids[f"{screen.id}_menu"].text, text_menu) + self.assertEqual(screen.ids[f"{screen.id}_label"].text, text_label) + mock_get_locale.assert_any_call() diff --git a/e2e_drives/__init__.py b/e2e_drives/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/e2e_drives/test_000_base_screen_linux_logical_drives.py b/e2e_drives/test_000_base_screen_linux_logical_drives.py new file mode 100644 index 00000000..dd08b39d --- /dev/null +++ b/e2e_drives/test_000_base_screen_linux_logical_drives.py @@ -0,0 +1,59 @@ +import sys +import importlib +import unittest +from unittest.mock import MagicMock, patch +from src.app.screens.base_screen import BaseScreen + +MOCK_DISKS_LINUX = """ +NAME="sda" TYPE="disk" RM="0" MOUNTPOINT="" +NAME="sda1" TYPE="part" RM="0" MOUNTPOINT="/boot" +NAME="sda2" TYPE="part" RM="0" MOUNTPOINT="/" +NAME="sda3" TYPE="part" RM="0" MOUNTPOINT="[SWAP]" +NAME="sdb" TYPE="disk" RM="1" MOUNTPOINT="" +NAME="sdb1" TYPE="part" RM="1" MOUNTPOINT="/media/mock/USB1" +NAME="sdc" TYPE="disk" RM="0" MOUNTPOINT="" +NAME="sdc1" TYPE="part" RM="0" MOUNTPOINT="/mnt/data" +NAME="sdd" TYPE="disk" RM="1" MOUNTPOINT="" +NAME="sdd1" TYPE="part" RM="1" MOUNTPOINT="/media/mock/USB2" +""" + + +class TestBaseScreenLinuxDrives(unittest.TestCase): + def setUp(self): + self.platform_patch = patch("sys.platform", "linux") + self.platform_patch.start() + + mock_stdout = MagicMock(stdout=MOCK_DISKS_LINUX) + + mock_run = MagicMock() + mock_run.return_value = mock_stdout + + self.mock_subprocess = MagicMock() + self.mock_subprocess.run = mock_run + + with patch.dict("sys.modules", {"subprocess": self.mock_subprocess}): + # Reload the base_screen module to apply the patch + importlib.reload(sys.modules["src.app.screens.base_screen"]) + + def tearDown(self): + self.platform_patch.stop() + + @patch("src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US") + def test_on_get_removable_drives_windows(self, mock_get_locale): + screen = BaseScreen(wid="mock", name="Mock") + screen.screen_manager = MagicMock() + screen.screen_manager.get_screen = MagicMock() + + disks = screen.on_get_removable_drives_linux() + + self.assertEqual(len(disks), 2) + self.assertEqual(disks[0], "/media/mock/USB1") + self.assertEqual(disks[1], "/media/mock/USB2") + + mock_get_locale.assert_called_once() + self.mock_subprocess.run.assert_called_once_with( + ["lsblk", "-P", "-o", "NAME,TYPE,RM,MOUNTPOINT"], + capture_output=True, + text=True, + check=True, + ) diff --git a/e2e_drives/test_001_base_screen_windows_logical_drives.py b/e2e_drives/test_001_base_screen_windows_logical_drives.py new file mode 100644 index 00000000..ab294b43 --- /dev/null +++ b/e2e_drives/test_001_base_screen_windows_logical_drives.py @@ -0,0 +1,38 @@ +import sys +import importlib +import unittest +from unittest.mock import MagicMock, patch +from src.app.screens.base_screen import BaseScreen + + +class TestBaseScreenWindowsDrives(unittest.TestCase): + def setUp(self): + # Patch `sys.platform` to simulate a Windows environment + self.platform_patch = patch("sys.platform", "win32") + self.platform_patch.start() + self.mock_win32file = MagicMock() + + # Mock bitmask for drives D and E + self.mock_win32file.GetLogicalDrives.return_value = 0b011000 + self.mock_win32file.DRIVE_REMOVABLE = 2 # Mock constant for removable drives + self.mock_win32file.GetDriveType.side_effect = lambda drive: ( + self.mock_win32file.DRIVE_REMOVABLE if drive in ["D:\\", "E:\\"] else 3 + ) + + with patch.dict("sys.modules", {"win32file": self.mock_win32file}): + # Reload the base_screen module to apply the patch + importlib.reload(sys.modules["src.app.screens.base_screen"]) + + def tearDown(self): + self.platform_patch.stop() + + @patch("src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US") + def test_on_get_removable_drives_windows(self, mock_get_locale): + screen = BaseScreen(wid="mock", name="Mock") + screen.screen_manager = MagicMock() + screen.screen_manager.get_screen = MagicMock() + screen.on_get_removable_drives_windows() + self.mock_win32file.GetLogicalDrives.assert_called_once() + self.mock_win32file.GetDriveType.assert_any_call("D:\\") + self.mock_win32file.GetDriveType.assert_any_call("E:\\") + mock_get_locale.assert_called() diff --git a/e2e_drives/test_002_base_screen_macos_logical_drives.py b/e2e_drives/test_002_base_screen_macos_logical_drives.py new file mode 100644 index 00000000..3e845f50 --- /dev/null +++ b/e2e_drives/test_002_base_screen_macos_logical_drives.py @@ -0,0 +1,111 @@ +import sys +import importlib +import unittest +from unittest.mock import MagicMock, patch +from src.app.screens.base_screen import BaseScreen + +MOCK_DISKS_MAC = """ + Device Identifier: disk0 + Device Node: /dev/disk0 + Whole: Yes + Part of Whole: disk0 + Device / Media Name: Apple SSD + Volume Name: Macintosh HD + Mounted: Yes + Mount Point: / + File System: APFS + Content (IOContent): Apple_APFS + Device Block Size: 512 Bytes + Disk Size: 500.3 GB (500279395328 Bytes) + Read-Only Media: No + Removable Media: No + Solid State: Yes + Virtual: No + Ejectable: No + + Device Identifier: disk0s1 + Device Node: /dev/disk0s1 + Whole: No + Part of Whole: disk0 + Volume Name: EFI + Mounted: No + File System: MS-DOS (FAT32) + Content (IOContent): EFI + Device Block Size: 512 Bytes + Disk Size: 209.7 MB (209715200 Bytes) + Read-Only Media: No + Removable Media: No + + Device Identifier: disk0s2 + Device Node: /dev/disk0s2 + Whole: No + Part of Whole: disk0 + Volume Name: Macintosh HD - Data + Mounted: Yes + Mount Point: /System/Volumes/Data + File System: APFS + Content (IOContent): Apple_APFS + Device Block Size: 512 Bytes + Disk Size: 500.1 GB (500000000000 Bytes) + Read-Only Media: No + Removable Media: No + + Device Identifier: disk1 + Device Node: /dev/disk1 + Device Location: External + Whole: Yes + Part of Whole: disk1 + Device / Media Name: External USB Drive + Volume Name: Backup Drive + Mounted: Yes + Mount Point: /Volumes/Backup Drive + File System: FAT32 + File System Personality MS-DOS FAT32 + Content (IOContent): Apple_HFS + Device Block Size: 4096 Bytes + Disk Size: 2.0 TB (2000000000000 Bytes) + Read-Only Media: No + Removable Media: Yes + Solid State: No + Virtual: No + Ejectable: Yes +""" + + +class TestBaseScreenLinuxDrives(unittest.TestCase): + def setUp(self): + self.platform_patch = patch("sys.platform", "darwin") + self.platform_patch.start() + + mock_stdout = MagicMock(stdout=MOCK_DISKS_MAC) + + mock_run = MagicMock() + mock_run.return_value = mock_stdout + + self.mock_subprocess = MagicMock() + self.mock_subprocess.run = mock_run + + with patch.dict("sys.modules", {"subprocess": self.mock_subprocess}): + # Reload the base_screen module to apply the patch + importlib.reload(sys.modules["src.app.screens.base_screen"]) + + def tearDown(self): + self.platform_patch.stop() + + @patch("src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US") + def test_on_get_removable_drives_mac(self, mock_get_locale): + screen = BaseScreen(wid="mock", name="Mock") + screen.screen_manager = MagicMock() + screen.screen_manager.get_screen = MagicMock() + + disks = screen.on_get_removable_drives_macos() + + self.assertEqual(len(disks), 1) + + mock_get_locale.assert_called_once() + self.mock_subprocess.run.assert_called_once_with( + ["diskutil", "info", "-all"], + capture_output=True, + text=True, + check=True, + ) diff --git a/e2e_drives/test_003_fail_base_screen_linux_logical_drives.py b/e2e_drives/test_003_fail_base_screen_linux_logical_drives.py new file mode 100644 index 00000000..79e8d9a8 --- /dev/null +++ b/e2e_drives/test_003_fail_base_screen_linux_logical_drives.py @@ -0,0 +1,64 @@ +import sys +import importlib +import unittest +from unittest.mock import MagicMock, patch +from src.app.screens.base_screen import BaseScreen + +MOCK_DISKS_LINUX = """ +NAME="sda" TYPE="disk" RM="0" MOUNTPOINT="" +NAME="sda1" TYPE="part" RM="0" MOUNTPOINT="/boot" +NAME="sda2" TYPE="part" RM="0" MOUNTPOINT="/" +NAME="sda3" TYPE="part" RM="0" MOUNTPOINT="[SWAP]" +NAME="sdb" TYPE="disk" RM="1" MOUNTPOINT="" +NAME="sdb1" TYPE="part" RM="1" MOUNTPOINT="/media/mock/USB1" +NAME="sdc" TYPE="disk" RM="0" MOUNTPOINT="" +NAME="sdc1" TYPE="part" RM="0" MOUNTPOINT="/mnt/data" +NAME="sdd" TYPE="disk" RM="1" MOUNTPOINT="" +NAME="sdd1" TYPE="part" RM="1" MOUNTPOINT="/media/mock/USB2" +""" + + +# Mock named exception for this test +class CalledProcessError(Exception): + + def __init__(self, cmd, returncode): + super().__init__(cmd) + self.returncode = returncode + + +class TestBaseScreenLinuxDrives(unittest.TestCase): + def setUp(self): + self.platform_patch = patch("sys.platform", "linux") + self.platform_patch.start() + + mock_run = MagicMock() + mock_run.side_effect = CalledProcessError(cmd="mock", returncode=1) + + self.mock_subprocess = MagicMock() + self.mock_subprocess.run = mock_run + self.mock_subprocess.CalledProcessError = CalledProcessError + + with patch.dict("sys.modules", {"subprocess": self.mock_subprocess}): + # Reload the base_screen module to apply the patch + importlib.reload(sys.modules["src.app.screens.base_screen"]) + + def tearDown(self): + self.platform_patch.stop() + + @patch("src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US") + def test_on_get_removable_drives_windows(self, mock_get_locale): + screen = BaseScreen(wid="mock", name="Mock") + screen.manager = MagicMock() + screen.manager.get_screen = MagicMock() + screen.redirect_exception = MagicMock() + + screen.on_get_removable_drives_linux() + + mock_get_locale.assert_called_once() + self.mock_subprocess.run.assert_called_once_with( + ["lsblk", "-P", "-o", "NAME,TYPE,RM,MOUNTPOINT"], + capture_output=True, + text=True, + check=True, + ) + screen.redirect_exception.assert_called() diff --git a/e2e_drives/test_004_fail_base_screen_windows_logical_drives.py b/e2e_drives/test_004_fail_base_screen_windows_logical_drives.py new file mode 100644 index 00000000..15dd9f0b --- /dev/null +++ b/e2e_drives/test_004_fail_base_screen_windows_logical_drives.py @@ -0,0 +1,32 @@ +import sys +import importlib +import unittest +from unittest.mock import MagicMock, patch +from src.app.screens.base_screen import BaseScreen + + +class TestFailBaseScreenWindowsDrives(unittest.TestCase): + + def setUp(self): + self.platform_patch = patch("sys.platform", "win32") + self.platform_patch.start() + self.mock_win32file = MagicMock() + self.mock_win32file.GetLogicalDrives.side_effect = Exception("mock") + + with patch.dict("sys.modules", {"win32file": self.mock_win32file}): + # Reload the base_screen module to apply the patch + importlib.reload(sys.modules["src.app.screens.base_screen"]) + + def tearDown(self): + self.platform_patch.stop() + + @patch("src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US") + def test_fail_on_get_removable_drives_windows(self, mock_get_locale): + screen = BaseScreen(wid="mock", name="Mock") + screen.manager = MagicMock() + screen.manager.get_screen = MagicMock() + screen.redirect_exception = MagicMock() + screen.on_get_removable_drives_windows() + self.mock_win32file.GetLogicalDrives.assert_called_once() + screen.redirect_exception.assert_called() + mock_get_locale.assert_called_once() diff --git a/e2e_drives/test_005_fail_base_screen_macos_logical_drives.py b/e2e_drives/test_005_fail_base_screen_macos_logical_drives.py new file mode 100644 index 00000000..ff31f637 --- /dev/null +++ b/e2e_drives/test_005_fail_base_screen_macos_logical_drives.py @@ -0,0 +1,51 @@ +import sys +import importlib +import unittest +from unittest.mock import MagicMock, patch +from src.app.screens.base_screen import BaseScreen + + +# Mock named exception for this test +class CalledProcessError(Exception): + + def __init__(self, cmd, returncode): + super().__init__(cmd) + self.returncode = returncode + + +class TestFailBaseScreenMacDrives(unittest.TestCase): + def setUp(self): + self.platform_patch = patch("sys.platform", "darwin") + self.platform_patch.start() + + mock_run = MagicMock() + mock_run.side_effect = CalledProcessError(cmd="mock", returncode=1) + + self.mock_subprocess = MagicMock() + self.mock_subprocess.run = mock_run + self.mock_subprocess.CalledProcessError = CalledProcessError + + with patch.dict("sys.modules", {"subprocess": self.mock_subprocess}): + # Reload the base_screen module to apply the patch + importlib.reload(sys.modules["src.app.screens.base_screen"]) + + def tearDown(self): + self.platform_patch.stop() + + @patch("src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US") + def test_on_get_removable_drives_mac(self, mock_get_locale): + screen = BaseScreen(wid="mock", name="Mock") + screen.manager = MagicMock() + screen.manager.get_screen = MagicMock() + screen.redirect_exception = MagicMock() + + screen.on_get_removable_drives_macos() + + mock_get_locale.assert_called_once() + self.mock_subprocess.run.assert_called_once_with( + ["diskutil", "info", "-all"], + capture_output=True, + text=True, + check=True, + ) + screen.redirect_exception.assert_called() diff --git a/e2e_drives/test_006_fail_unknow_base_screen_linux_logical_drives.py b/e2e_drives/test_006_fail_unknow_base_screen_linux_logical_drives.py new file mode 100644 index 00000000..3ed1356c --- /dev/null +++ b/e2e_drives/test_006_fail_unknow_base_screen_linux_logical_drives.py @@ -0,0 +1,51 @@ +import sys +import importlib +import unittest +from unittest.mock import MagicMock, patch +from src.app.screens.base_screen import BaseScreen + + +# Mock named exception for this test +class CalledProcessError(Exception): + + def __init__(self, cmd, returncode): + super().__init__(cmd) + self.returncode = returncode + + +class TestBaseScreenLinuxDrives(unittest.TestCase): + def setUp(self): + self.platform_patch = patch("sys.platform", "linux") + self.platform_patch.start() + + mock_run = MagicMock() + mock_run.side_effect = Exception("mock") + + self.mock_subprocess = MagicMock() + self.mock_subprocess.run = mock_run + self.mock_subprocess.CalledProcessError = CalledProcessError + + with patch.dict("sys.modules", {"subprocess": self.mock_subprocess}): + # Reload the base_screen module to apply the patch + importlib.reload(sys.modules["src.app.screens.base_screen"]) + + def tearDown(self): + self.platform_patch.stop() + + @patch("src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US") + def test_on_get_removable_drives_windows(self, mock_get_locale): + screen = BaseScreen(wid="mock", name="Mock") + screen.manager = MagicMock() + screen.manager.get_screen = MagicMock() + screen.redirect_exception = MagicMock() + + screen.on_get_removable_drives_linux() + + mock_get_locale.assert_called_once() + self.mock_subprocess.run.assert_called_once_with( + ["lsblk", "-P", "-o", "NAME,TYPE,RM,MOUNTPOINT"], + capture_output=True, + text=True, + check=True, + ) + screen.redirect_exception.assert_called() diff --git a/e2e_drives/test_007_fail_unknow_base_screen_macos_logical_drives.py b/e2e_drives/test_007_fail_unknow_base_screen_macos_logical_drives.py new file mode 100644 index 00000000..52763eca --- /dev/null +++ b/e2e_drives/test_007_fail_unknow_base_screen_macos_logical_drives.py @@ -0,0 +1,51 @@ +import sys +import importlib +import unittest +from unittest.mock import MagicMock, patch +from src.app.screens.base_screen import BaseScreen + + +# Mock named exception for this test +class CalledProcessError(Exception): + + def __init__(self, cmd, returncode): + super().__init__(cmd) + self.returncode = returncode + + +class TestFailBaseScreenMacDrives(unittest.TestCase): + def setUp(self): + self.platform_patch = patch("sys.platform", "darwin") + self.platform_patch.start() + + mock_run = MagicMock() + mock_run.side_effect = Exception("mock") + + self.mock_subprocess = MagicMock() + self.mock_subprocess.run = mock_run + self.mock_subprocess.CalledProcessError = CalledProcessError + + with patch.dict("sys.modules", {"subprocess": self.mock_subprocess}): + # Reload the base_screen module to apply the patch + importlib.reload(sys.modules["src.app.screens.base_screen"]) + + def tearDown(self): + self.platform_patch.stop() + + @patch("src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US") + def test_on_get_removable_drives_mac(self, mock_get_locale): + screen = BaseScreen(wid="mock", name="Mock") + screen.manager = MagicMock() + screen.manager.get_screen = MagicMock() + screen.redirect_exception = MagicMock() + + screen.on_get_removable_drives_macos() + + mock_get_locale.assert_called_once() + self.mock_subprocess.run.assert_called_once_with( + ["diskutil", "info", "-all"], + capture_output=True, + text=True, + check=True, + ) + screen.redirect_exception.assert_called() diff --git a/img/badge_github.png b/img/badge_github.png new file mode 100644 index 00000000..326d2547 Binary files /dev/null and b/img/badge_github.png differ diff --git a/poetry.lock b/poetry.lock index 6ae5f6b7..cfa18de6 100644 --- a/poetry.lock +++ b/poetry.lock @@ -710,7 +710,6 @@ files = [ {file = "kivy_deps.angle-0.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:574381d4e66f3198bc48aa10f238e7a3816ad56b80ec939f5d56fb33a378d0b1"}, {file = "kivy_deps.angle-0.4.0-cp312-cp312-win32.whl", hash = "sha256:4fa7a6366899fba13f7624baf4645787165f45731db08d14557da29c12ee48f0"}, {file = "kivy_deps.angle-0.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:668e670d4afd2551af0af2c627ceb0feac884bd799fb6a3dff78fdbfa2ea0451"}, - {file = "kivy_deps.angle-0.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:9afbf702f8bb9a993c48f39c018ca3b4d2ec381a5d3f82fe65bdaa6af0bba29b"}, {file = "kivy_deps.angle-0.4.0-cp37-cp37m-win32.whl", hash = "sha256:24cfc0076d558080a00c443c7117311b4a977c1916fe297232eff1fd6f62651e"}, {file = "kivy_deps.angle-0.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:48592ac6f7c183c5cd10d9ebe43d4148d0b2b9e400a2b0bcb5d21014cc929ce2"}, {file = "kivy_deps.angle-0.4.0-cp38-cp38-win32.whl", hash = "sha256:1bbacf20bf6bd6ee965388f95d937c8fba2c54916fb44faa166c2ba58276753c"}, @@ -732,7 +731,6 @@ files = [ {file = "kivy_deps.glew-0.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:22e155ec59ce717387f5d8804811206d200a023ba3d0bc9bbf1393ee28d0053e"}, {file = "kivy_deps.glew-0.3.1-cp312-cp312-win32.whl", hash = "sha256:b64ee4e445a04bc7c848c0261a6045fc2f0944cc05d7f953e3860b49f2703424"}, {file = "kivy_deps.glew-0.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:3acbbd30da05fc10c185b5d4bb75fbbc882a6ef2192963050c1c94d60a6e795a"}, - {file = "kivy_deps.glew-0.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:f4aa8322078359862ccd9e16e5cea61976d75fb43125d87922e20c916fa31a11"}, {file = "kivy_deps.glew-0.3.1-cp37-cp37m-win32.whl", hash = "sha256:5bf6a63fe9cc4fe7bbf280ec267ec8c47914020a1175fb22152525ff1837b436"}, {file = "kivy_deps.glew-0.3.1-cp37-cp37m-win_amd64.whl", hash = "sha256:d64a8625799fab7a7efeb3661ef8779a7f9c6d80da53eed87a956320f55530fa"}, {file = "kivy_deps.glew-0.3.1-cp38-cp38-win32.whl", hash = "sha256:00f4ae0a4682d951266458ddb639451edb24baa54a35215dce889209daf19a06"}, diff --git a/pyproject.toml b/pyproject.toml index a35ef8c1..ac8a535c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ prefer-active-python = true [tool.poetry] name = "krux-installer" -version = "0.0.20-beta" +version = "0.0.20" description = "A GUI based application to flash Krux firmware on K210 based devices" authors = [ "qlrd " @@ -40,22 +40,26 @@ cli = "python src/krux-installer.py" format-src= "black ./src" format-tests= "black ./tests" format-e2e= "black ./e2e" +format-drives= "black ./e2e_drives" format-installer = "black ./krux-installer.py" -format = ["format-src", "format-tests", "format-e2e", "format-installer"] +format = ["format-src", "format-tests", "format-e2e", "format-drives", "format-installer"] test-unit = "pytest --cache-clear --cov=src/utils/constants --cov=src/utils/info --cov=src/utils/selector --cov=src/utils/downloader --cov=src/utils/trigger --cov=src/utils/flasher --cov=src/utils/unzip --cov=src/utils/signer --cov=src/utils/verifyer --cov=src/i18n --cov-branch --cov-report html ./tests" test-e2e = "pytest --cov-append --cov=src/app --cov-branch --cov-report html ./e2e" -test = ["test-unit", "test-e2e"] +test-drives = "pytest --cov-append --cov=src/app --cov-branch --cov-report html ./e2e_drives" +test = ["test-unit", "test-e2e", "test-drives"] coverage-unit = "pytest --cache-clear --cov=src/utils/constants --cov=src/utils/info --cov=src/utils/selector --cov=src/utils/downloader --cov=src/utils/trigger --cov=src/utils/flasher --cov=src/utils/unzip --cov=src/utils/signer --cov=src/utils/verifyer --cov=src/i18n --cov-branch --cov-report xml ./tests" coverage-e2e = "pytest --cov-append --cov=src/app --cov-branch --cov-report xml ./e2e" -coverage = ["coverage-unit", "coverage-e2e"] +coverage-drives = "pytest --cov-append --cov=src/app --cov-branch --cov-report xml ./e2e_drives" +coverage = ["coverage-unit", "coverage-e2e", "coverage-drives"] lint.sequence = [ { cmd = "jsonlint src/i18n/*.json"}, { cmd = "pylint --rcfile=.pylint/src ./src" }, { cmd = "pylint --rcfile=.pylint/tests ./tests"}, - { cmd = "pylint --rcfile=.pylint/tests ./e2e"} + { cmd = "pylint --rcfile=.pylint/tests ./e2e"}, + { cmd = "pylint --rcfile=.pylint/tests ./e2e_drives"} ] build-linux.sequence = [ diff --git a/src/app/config_krux_installer.py b/src/app/config_krux_installer.py index 66695d33..9d44cf3b 100644 --- a/src/app/config_krux_installer.py +++ b/src/app/config_krux_installer.py @@ -77,7 +77,7 @@ def make_lang_code(lang: str) -> str: return lang raise OSError( - f"Couldn 't possible to setup locale: OS '{sys.platform}' not implemented" + f"Couldn't possible to setup locale: OS '{sys.platform}' not implemented" ) @staticmethod diff --git a/src/app/screens/airgap_update_screen.py b/src/app/screens/airgap_update_screen.py index 0c4a9aca..16e86bc5 100644 --- a/src/app/screens/airgap_update_screen.py +++ b/src/app/screens/airgap_update_screen.py @@ -47,40 +47,55 @@ def on_press(instance): self.debug(f"Calling {instance.id}::on_press") self.set_background(wid=instance.id, rgba=(0.25, 0.25, 0.25, 1)) + setattr(AirgapUpdateScreen, f"on_press_{self.id}_button_{row}", on_press) + def on_release(instance): - new_firmware_bin = os.path.join(drive, "firmware.bin") - new_firmware_sig = os.path.join(drive, "firmware.bin.sig") - shutil.copyfile(self.firmware_bin, new_firmware_bin) - shutil.copyfile(self.firmware_sig, new_firmware_sig) - - # After copy, make sha256 hash to show - sha256 = Sha256Verifyer(filename=new_firmware_bin) - sha256.load() - - # Now update the next screen - warn_screen = self.manager.get_screen("WarningAfterAirgapUpdateScreen") - - fns = [ - partial( - warn_screen.update, - name=self.name, - key="sdcard", - value=drive, - ), - partial( - warn_screen.update, - name=self.name, - key="hash", - value=sha256.data.split(" ", maxsplit=1)[0], - ), - partial(warn_screen.update, name=self.name, key="label"), - ] - - for fn in fns: - Clock.schedule_once(fn, 0) - - self.set_background(wid=instance.id, rgba=(0, 0, 0, 1)) - self.set_screen(name="WarningAfterAirgapUpdateScreen", direction="left") + new_firmware_bin = os.path.normpath(os.path.join(drive, "firmware.bin")) + new_firmware_sig = os.path.normpath(os.path.join(drive, "firmware.bin.sig")) + try: + shutil.copyfile(self.firmware_bin, new_firmware_bin) + shutil.copyfile(self.firmware_sig, new_firmware_sig) + + # After copy, make sha256 hash to show + sha256 = Sha256Verifyer(filename=new_firmware_bin) + sha256.load() + + # Now update the next screen + warn_screen = self.manager.get_screen("WarningAfterAirgapUpdateScreen") + + fns = [ + partial( + warn_screen.update, + name=self.name, + key="sdcard", + value=drive, + ), + partial( + warn_screen.update, + name=self.name, + key="hash", + value=sha256.data.split(" ", maxsplit=1)[0], + ), + partial(warn_screen.update, name=self.name, key="label"), + ] + + for fn in fns: + Clock.schedule_once(fn, 0) + + self.set_background(wid=instance.id, rgba=(0, 0, 0, 1)) + self.set_screen(name="WarningAfterAirgapUpdateScreen", direction="left") + + except shutil.Error as err_exc: + self.redirect_exception(exception=err_exc) + + except shutil.ExecError as exec_exc: + self.redirect_exception(exception=exec_exc) + + # pylint: disable=broad-exception-caught + except Exception as exc: + self.redirect_exception(exception=exc) + + setattr(AirgapUpdateScreen, f"on_release_{self.id}_button_{row}", on_release) select = self.translate("Select") to_copy = self.translate("to copy firmware") diff --git a/src/app/screens/ask_permission_dialout_screen.py b/src/app/screens/ask_permission_dialout_screen.py index d5e1a4c1..b79c9be1 100644 --- a/src/app/screens/ask_permission_dialout_screen.py +++ b/src/app/screens/ask_permission_dialout_screen.py @@ -86,7 +86,6 @@ def on_ref_press(*args): # pylint: disable=broad-exception-caught except Exception as err: - self.error(str(err)) self.redirect_exception(exception=err) if args[1] == "Deny": @@ -137,6 +136,11 @@ def detect_usermod_bin(self): elif "ID_LIKE" in os_data and "suse" in os_data["ID_LIKE"]: bin_path = "/usr/sbin/usermod" + # Check for Fedora, to fix issue #115 + # see https://github.com/selfcustody/krux-installer/issues/115 + elif "ID" in os_data and "fedora" in os_data["ID"]: + bin_path = "/usr/sbin/usermod" + # Arch, Manjaro, Slackware, Gentoo elif os_data.get("ID") in ("arch", "manjaro", "slackware", "gentoo"): bin_path = "/usr/bin/usermod" diff --git a/src/app/screens/base_flash_screen.py b/src/app/screens/base_flash_screen.py index 86c21187..2f1f0c7e 100644 --- a/src/app/screens/base_flash_screen.py +++ b/src/app/screens/base_flash_screen.py @@ -140,10 +140,22 @@ def on_done(dt): back = self.translate("Back") _quit = self.translate("Quit") + # If the done step occurs in FlashScreen, + # just put a line break + # It it occurs in WipeScreen, + # add a message to remove device and re-plug + # it again before flash + below_done = "\n" + if self.name == "WipeScreen": + below_done += self.translate( + "disconnect and reconnect device before flash again" + ) + below_done += "\n" + self.ids[f"{self.id}_progress"].text = "".join( [ f"[b]{done}![/b]", - "\n", + below_done, "[color=#00FF00]", f"[ref=Back][u]{back}[/u][/ref]", "[/color]", diff --git a/src/app/screens/base_screen.py b/src/app/screens/base_screen.py index 06de292e..343c7d4e 100644 --- a/src/app/screens/base_screen.py +++ b/src/app/screens/base_screen.py @@ -294,6 +294,11 @@ def on_get_removable_drives_linux(self) -> typing.List[str]: exc = RuntimeError(f"Error detecting removable drives:\n{e}") self.redirect_exception(exception=exc) + # pylint: disable=broad-exception-caught + except Exception as e: + exc = RuntimeError(f"Unknow error while detecting removable drives:\n{e}") + self.redirect_exception(exception=exc) + return drive_list def on_get_removable_drives_macos(self) -> typing.List[str]: @@ -351,6 +356,11 @@ def on_get_removable_drives_macos(self) -> typing.List[str]: exc = RuntimeError(f"Error detecting removable drives:\n{e}") self.redirect_exception(exception=exc) + # pylint: disable=broad-exception-caught + except Exception as e: + exc = RuntimeError(f"Unknow error while detecting removable drives:\n{e}") + self.redirect_exception(exception=exc) + return drive_list def on_get_removable_drives_windows(self) -> typing.List[str]: @@ -391,6 +401,7 @@ def on_get_removable_drives_windows(self) -> typing.List[str]: def redirect_exception(self, exception: Exception): """Get an exception and prepare a ErrorScreen rendering""" + print(exception) screen = self.manager.get_screen("ErrorScreen") fns = [ partial(screen.update, name=self.name, key="canvas"), diff --git a/src/app/screens/flash_screen.py b/src/app/screens/flash_screen.py index 24fb2bb1..a2450951 100644 --- a/src/app/screens/flash_screen.py +++ b/src/app/screens/flash_screen.py @@ -24,7 +24,6 @@ import threading import traceback from functools import partial -from kivy.app import App from kivy.clock import Clock from src.app.screens.base_flash_screen import BaseFlashScreen from src.utils.flasher import Flasher @@ -39,6 +38,7 @@ def __init__(self, **kwargs): self.flashing_msg = self.translate("Flashing") self.at_msg = self.translate("at") self.flasher = Flasher() + self.fail_msg = "" fn = partial(self.update, name=self.name, key="canvas") Clock.schedule_once(fn, 0) @@ -71,6 +71,11 @@ def on_data(*args, **kwargs): self.output.append("*") self.output.append("") + elif "Greeting fail" in text: + self.fail_msg = text + self.flasher.ktool.kill() + self.flasher.ktool.checkKillExit() + if len(self.output) > 10: del self.output[:1] @@ -118,16 +123,16 @@ def on_pre_enter(self, *args): self.build_on_process() self.build_on_done() + wid = f"{self.id}_info" + def on_ref_press(*args): if args[1] == "Back": self.set_screen(name="MainScreen", direction="right") - elif args[1] == "Quit": - App.get_running_app().stop() + if args[1] == "Quit": + self.quit_app() - else: - exc = RuntimeError(f"Invalid ref: {args[1]}") - self.redirect_exception(exception=exc) + setattr(FlashScreen, f"on_ref_press_{wid}", on_ref_press) self.make_subgrid( wid=f"{self.id}_subgrid", rows=2, root_widget=f"{self.id}_grid" @@ -157,7 +162,7 @@ def on_ref_press(*args): self.make_button( row=2, - wid=f"{self.id}_info", + wid=wid, text="", font_factor=72, root_widget=f"{self.id}_grid", @@ -186,11 +191,33 @@ def hook(err): err.exc_type, err.exc_value, err.exc_traceback ) msg = "".join(trace[-2:]) - self.error(msg) - self.redirect_exception(exception=RuntimeError(f"Flash failed: {msg}")) + general_msg = "".join( + [ + "Ensure that you have selected the correct device ", + "and that your computer has successfully detected it.", + ] + ) + + if "StopIteration" in msg: + self.fail_msg = msg + self.fail_msg += f"\n\n{general_msg}" + not_conn_fail = RuntimeError(f"Flash failed:\n{self.fail_msg}\n") + self.redirect_exception(exception=not_conn_fail) + + elif "Cancel" in msg: + self.fail_msg = f"{self.fail_msg}\n\n{general_msg}" + greeting_fail = RuntimeError(f"Flash failed:\n{self.fail_msg}\n") + self.redirect_exception(exception=greeting_fail) + + else: + self.fail_msg = msg + any_fail = RuntimeError(f"Flash failed:\n{self.fail_msg}\n") + self.redirect_exception(exception=any_fail) + + setattr(FlashScreen, "on_except_hook", hook) # hook what happened - threading.excepthook = hook + threading.excepthook = getattr(FlashScreen, "on_except_hook") # start thread self.thread.start() diff --git a/src/app/screens/greetings_screen.py b/src/app/screens/greetings_screen.py index 890af219..2c0aaeca 100644 --- a/src/app/screens/greetings_screen.py +++ b/src/app/screens/greetings_screen.py @@ -112,7 +112,7 @@ def get_os_dialout_group(self): "dialout", ) # Pop!_OS will fall under this - # Check for Red Hat-based systems (Fedora, CentOS, Rocky Linux, etc.) + # Check for Red Hat-based systems (CentOS, Rocky Linux, etc.) if "ID_LIKE" in os_data and "rhel" in os_data["ID_LIKE"]: detected = ( os_data["ID_LIKE"], @@ -126,6 +126,14 @@ def get_os_dialout_group(self): "dialout", ) # SUSE systems also often use `dialout` + # Check for Fedora, to fix issue #115 + # see https://github.com/selfcustody/krux-installer/issues/115 + elif "ID" in os_data and "fedora" in os_data["ID"]: + detected = ( + os_data["ID"], + "dialout", + ) # FEDORA systems also often use `dialout` + # Arch, Manjaro, Slackware, Gentoo if os_data.get("ID") in ("arch", "manjaro", "slackware", "gentoo"): detected = (os_data["ID"], "uucp") diff --git a/src/app/screens/main_screen.py b/src/app/screens/main_screen.py index c0b16403..b24431cd 100644 --- a/src/app/screens/main_screen.py +++ b/src/app/screens/main_screen.py @@ -23,6 +23,7 @@ """ import os import re +import typing from functools import partial from kivy.clock import Clock from src.utils.selector import VALID_DEVICES @@ -190,6 +191,71 @@ def on_release_select_device(instance): ) self.ids[wid].size_hint = (1, 1) + def on_check_any_official_release( + self, partial_list: typing.List[typing.Callable] + ) -> str: + """Check if any official release file exists""" + resources = MainScreen.get_destdir_assets() + zipfile = os.path.join(resources, f"krux-{self.version}.zip") + to_screen = None + + if os.path.isfile(zipfile): + to_screen = "WarningAlreadyDownloadedScreen" + else: + to_screen = "DownloadStableZipScreen" + + screen = self.manager.get_screen(to_screen) + partial_list.append( + partial( + screen.update, + name=self.name, + key="canvas", + ) + ) + partial_list.append( + partial( + screen.update, + name=self.name, + key="version", + value=self.version, + ) + ) + + return to_screen + + def on_check_any_beta_release( + self, partial_list: typing.List[typing.Callable] + ) -> str: + """Check if release is beta""" + to_screen = "DownloadBetaScreen" + screen = self.manager.get_screen(to_screen) + partial_list.append( + partial( + screen.update, + name=self.name, + key="canvas", + ) + ) + partial_list.append( + partial( + screen.update, + name=self.name, + key="firmware", + value="kboot.kfpkg", + ) + ) + partial_list.append( + partial( + screen.update, + name=self.name, + key="device", + value=self.device, + ) + ) + + partial_list.append(partial(screen.update, name=self.name, key="downloader")) + return to_screen + def build_flash_button(self): """Create staticmethods using instance variables to control the flash button""" wid = "main_flash" @@ -211,75 +277,33 @@ def on_release_flash(instance): # partials are functions that call `update` # method in screen before go to them - partials = [] - - # Check if any official release file exists - if re.findall(r"^v\d+\.\d+\.\d$", self.version): - resources = MainScreen.get_destdir_assets() - zipfile = os.path.join(resources, f"krux-{self.version}.zip") - - if os.path.isfile(zipfile): - to_screen = "WarningAlreadyDownloadedScreen" - else: - to_screen = "DownloadStableZipScreen" - - screen = self.manager.get_screen(to_screen) - partials.append( - partial( - screen.update, - name=self.name, - key="canvas", - ) - ) - partials.append( - partial( - screen.update, - name=self.name, - key="version", - value=self.version, - ) - ) + partial_list = [] + err = None - # check if release is beta - elif re.findall("^odudex/krux_binaries", self.version): - to_screen = "DownloadBetaScreen" - screen = self.manager.get_screen(to_screen) - partials.append( - partial( - screen.update, - name=self.name, - key="canvas", - ) - ) - partials.append( - partial( - screen.update, - name=self.name, - key="firmware", - value="kboot.kfpkg", - ) - ) - partials.append( - partial( - screen.update, - name=self.name, - key="device", - value=self.device, - ) + if re.match(r"^v\d+\.\d+\.\d$", self.version): + to_screen = self.on_check_any_official_release( + partial_list=partial_list ) - partials.append( - partial(screen.update, name=self.name, key="downloader") + + elif re.match("^odudex/krux_binaries", self.version): + to_screen = self.on_check_any_beta_release( + partial_list=partial_list ) + else: + err = RuntimeError(f"version '{self.version}' not supported") + self.redirect_exception(exception=err) + return + # Execute the partials - for fn in partials: + for fn in partial_list: Clock.schedule_once(fn, 0) # Goto the selected screen self.set_screen(name=to_screen, direction="left") - setattr(MainScreen, "on_press_flash", on_press_flash) - setattr(MainScreen, "on_release_flash", on_release_flash) + setattr(MainScreen, f"on_press_{wid}", on_press_flash) + setattr(MainScreen, f"on_release_{wid}", on_release_flash) flash_msg = self.translate("Flash") self.make_button( @@ -289,8 +313,8 @@ def on_release_flash(instance): text=f"[color=#333333]{flash_msg}[/color]", font_factor=28, halign=None, - on_press=getattr(MainScreen, "on_press_flash"), - on_release=getattr(MainScreen, "on_release_flash"), + on_press=getattr(MainScreen, f"on_press_{wid}"), + on_release=getattr(MainScreen, f"on_release_{wid}"), on_ref_press=None, ) @@ -450,7 +474,6 @@ def on_update(): self.update_version(value) else: error = RuntimeError(f"Invalid value for key '{key}': {value}") - self.error(str(error)) self.redirect_exception(exception=error) if key == "device": @@ -458,7 +481,6 @@ def on_update(): self.update_device(value) else: error = RuntimeError(f"Invalid value for key '{key}': {value}") - self.error(str(error)) self.redirect_exception(exception=error) if key == "flash": diff --git a/src/app/screens/unzip_stable_screen.py b/src/app/screens/unzip_stable_screen.py index 293d076c..52972647 100644 --- a/src/app/screens/unzip_stable_screen.py +++ b/src/app/screens/unzip_stable_screen.py @@ -177,6 +177,8 @@ def build_extract_to_airgap_button(self): extract_msg = self.translate("Unziping") extracted_msg = self.translate("Unziped") + wid = f"{self.id}_airgap_button" + def on_press(instance): self.debug(f"Calling Button::{instance.id}::on_press") file_path = os.path.join(rel_path, "firmware.bin") @@ -185,6 +187,8 @@ def on_press(instance): ) self.set_background(wid=instance.id, rgba=(0.25, 0.25, 0.25, 1)) + setattr(UnzipStableScreen, f"on_press_{wid}", on_press) + def on_release(instance): self.debug(f"Calling Button::{instance.id}::on_release") bin_path = os.path.join(base_path, "firmware.bin") @@ -223,10 +227,12 @@ def on_release(instance): time.sleep(2.1) self.set_screen(name="WarningBeforeAirgapUpdateScreen", direction="left") + setattr(UnzipStableScreen, f"on_release_{wid}", on_release) + p = os.path.join(rel_path, "firmware.bin") self.make_button( row=1, - wid=f"{self.id}_airgap_button", + wid=wid, root_widget=f"{self.id}_grid", text="".join([airgap_msg, "\n", "[color=#efcc00]", p, "[/color]"]), font_factor=42, diff --git a/src/app/screens/warning_after_airgap_update_screen.py b/src/app/screens/warning_after_airgap_update_screen.py index 09877e9d..62c89240 100644 --- a/src/app/screens/warning_after_airgap_update_screen.py +++ b/src/app/screens/warning_after_airgap_update_screen.py @@ -22,7 +22,6 @@ about_screen.py """ from functools import partial -from kivy.app import App from kivy.clock import Clock from src.app.screens.base_screen import BaseScreen @@ -60,7 +59,11 @@ def on_ref_press(*args): self.set_screen(name="MainScreen", direction="right") if args[1] == "Quit": - App.get_running_app().stop() + self.quit_app() + + setattr( + WarningAfterAirgapUpdateScreen, f"on_ref_press_{self.id}_menu", on_ref_press + ) self.make_image( wid=f"{self.id}_done", diff --git a/src/app/screens/warning_before_airgap_update_screen.py b/src/app/screens/warning_before_airgap_update_screen.py index 559f281a..bdff28e3 100644 --- a/src/app/screens/warning_before_airgap_update_screen.py +++ b/src/app/screens/warning_before_airgap_update_screen.py @@ -70,12 +70,23 @@ def on_ref_press(*args): if sys.platform == "win32": drive_list = self.on_get_removable_drives_windows() - screen = self.manager.get_screen("AirgapUpdateScreen") - fn = partial( - screen.update, name=self.name, key="drives", value=drive_list - ) - Clock.schedule_once(fn, 0) - self.set_screen(name="AirgapUpdateScreen", direction="right") + if len(drive_list) == 0: + exc = RuntimeError("No removable drives found") + self.redirect_exception(exception=exc) + + else: + screen = self.manager.get_screen("AirgapUpdateScreen") + fn = partial( + screen.update, name=self.name, key="drives", value=drive_list + ) + Clock.schedule_once(fn, 0) + self.set_screen(name="AirgapUpdateScreen", direction="right") + + setattr( + WarningBeforeAirgapUpdateScreen, + f"on_ref_press_{self.id}_label", + on_ref_press, + ) self.make_button( row=0, @@ -86,7 +97,9 @@ def on_ref_press(*args): root_widget=f"{self.id}_grid", on_press=None, on_release=None, - on_ref_press=on_ref_press, + on_ref_press=getattr( + WarningBeforeAirgapUpdateScreen, f"on_ref_press_{self.id}_label" + ), ) self.ids[f"{self.id}_label"].halign = "justify" diff --git a/src/app/screens/warning_wipe_screen.py b/src/app/screens/warning_wipe_screen.py index 7ac0c54a..165c14e6 100644 --- a/src/app/screens/warning_wipe_screen.py +++ b/src/app/screens/warning_wipe_screen.py @@ -68,7 +68,6 @@ def on_ref_press(*args): ) for fn in partials: - print(fn) Clock.schedule_once(fn, 0) self.set_screen(name=args[1], direction="left") diff --git a/src/app/screens/wipe_screen.py b/src/app/screens/wipe_screen.py index 9bffd270..dca1ab77 100644 --- a/src/app/screens/wipe_screen.py +++ b/src/app/screens/wipe_screen.py @@ -25,7 +25,6 @@ import traceback from functools import partial from kivy.clock import Clock -from kivy.app import App from src.utils.flasher.wiper import Wiper from src.app.screens.base_flash_screen import BaseFlashScreen @@ -40,6 +39,7 @@ def __init__(self, **kwargs): self.success = False self.progress = "" self.device = None + self.fail_msg = "" fn = partial(self.update, name=self.name, key="canvas") Clock.schedule_once(fn, 0) @@ -74,6 +74,11 @@ def on_data(*args, **kwargs): if len(self.output) > 10: del self.output[:1] + if "Greeting fail" in text: + self.fail_msg = text + self.wiper.ktool.kill() + self.wiper.ktool.checkKillExit() + if "SPI Flash erased." in text: self.is_done = True # pylint: disable=not-callable @@ -90,18 +95,16 @@ def on_pre_enter(self, *args): self.build_on_data() self.build_on_done() + wid = f"{self.id}_info" + def on_ref_press(*args): if args[1] == "Back": self.set_screen(name="MainScreen", direction="right") - elif args[1] == "Quit": - App.get_running_app().stop() + if args[1] == "Quit": + self.quit_app() - else: - msg = f"Invalid ref: {args[1]}" - exc = RuntimeError(msg) - self.error(msg) - self.redirect_exception(exception=exc) + setattr(WipeScreen, f"on_ref_press_{wid}", on_ref_press) self.make_subgrid( wid=f"{self.id}_subgrid", rows=3, root_widget=f"{self.id}_grid" @@ -158,11 +161,34 @@ def hook(err): err.exc_type, err.exc_value, err.exc_traceback ) msg = "".join(trace[-2:]) + general_msg = "".join( + [ + "Ensure that you have selected the correct device ", + "and that your computer has successfully detected it.", + ] + ) + self.error(msg) - self.redirect_exception(exception=RuntimeError(f"Wipe failed: {msg}")) + if "StopIteration" in msg: + self.fail_msg = msg + self.fail_msg += f"\n\n{general_msg}" + not_conn_fail = RuntimeError(f"Wipe failed:\n{self.fail_msg}\n") + self.redirect_exception(exception=not_conn_fail) + + elif "Cancel" in msg: + self.fail_msg = f"{self.fail_msg}\n\n{general_msg}" + greeting_fail = RuntimeError(f"Wipe failed:\n{self.fail_msg}\n") + self.redirect_exception(exception=greeting_fail) + + else: + self.fail_msg = msg + any_fail = RuntimeError(f"Wipe failed:\n{self.fail_msg}\n") + self.redirect_exception(exception=any_fail) + + setattr(WipeScreen, "on_except_hook", hook) # hook what happened - threading.excepthook = hook + threading.excepthook = getattr(WipeScreen, "on_except_hook") self.thread.start() def update(self, *args, **kwargs): diff --git a/src/i18n/af_ZA.UTF-8.json b/src/i18n/af_ZA.UTF-8.json index 6ec9b199..d7dba786 100644 --- a/src/i18n/af_ZA.UTF-8.json +++ b/src/i18n/af_ZA.UTF-8.json @@ -117,7 +117,8 @@ "PLEASE DO NOT UNPLUG YOUR DEVICE": "MOET ASSEBLIEF NIE U TOESTEL ONTKOPPEL NIE", "DONE": "GEDOEN", "Back": "Terug", - "Quit": "Sluit" + "Quit": "Sluit", + "disconnect and reconnect device before flash again": "ontkoppel en koppel die toestel weer voordat jy weer flits" }, "warning_wipe_screen": { "You are about to initiate a FULL WIPE of this device": "U is op die punt om 'n VOLLEDIGE WIPE van hierdie toestel te begin", diff --git a/src/i18n/de_DE.UTF-8.json b/src/i18n/de_DE.UTF-8.json index fb553e5d..49663c1d 100644 --- a/src/i18n/de_DE.UTF-8.json +++ b/src/i18n/de_DE.UTF-8.json @@ -114,7 +114,8 @@ "PLEASE DO NOT UNPLUG YOUR DEVICE": "BITTE NICHT DAS GERÄT TRENNEN", "DONE": "FERTIG", "Back": "Zurück", - "Quit": "Beenden" + "Quit": "Beenden", + "disconnect and reconnect device before flash again": "Gerät trennen und erneut verbinden, bevor erneut geflasht wird" }, "warning_wipe_screen": { "You are about to initiate a FULL WIPE of this device": "Sie sind dabei, dieses Gerät KOMPLETT ZU LÖSCHEN", diff --git a/src/i18n/en_US.UTF-8.json b/src/i18n/en_US.UTF-8.json index 35b9fab6..032bf853 100644 --- a/src/i18n/en_US.UTF-8.json +++ b/src/i18n/en_US.UTF-8.json @@ -114,7 +114,8 @@ "PLEASE DO NOT UNPLUG YOUR DEVICE": "PLEASE DO NOT UNPLUG YOUR DEVICE", "DONE": "DONE", "Back": "Back", - "Quit": "Quit" + "Quit": "Quit", + "disconnect and reconnect device before flash again": "disconnect and reconnect device before flash again" }, "warning_wipe_screen": { "You are about to initiate a FULL WIPE of this device": "You are about to initiate a FULL WIPE of this device", diff --git a/src/i18n/es_ES.UTF-8.json b/src/i18n/es_ES.UTF-8.json index c30d57c3..d32777ca 100644 --- a/src/i18n/es_ES.UTF-8.json +++ b/src/i18n/es_ES.UTF-8.json @@ -118,7 +118,8 @@ "PLEASE DO NOT UNPLUG YOUR DEVICE": "POR FAVOR, NO DESCONECTE SU DISPOSITIVO", "DONE": "HECHO", "Back": "Volve", - "Quit": "Salir" + "Quit": "Salir", + "disconnect and reconnect device before flash again": "desconecta y vuelve a conectar el dispositivo antes de volver a flashear" }, "warning_wipe_screen": { "You are about to initiate a FULL WIPE of this device": "Está a punto de iniciar un BORRADO COMPLETO de este dispositivo", diff --git a/src/i18n/fr_FR.UTF-8.json b/src/i18n/fr_FR.UTF-8.json index 8aed1130..5cb89cbb 100644 --- a/src/i18n/fr_FR.UTF-8.json +++ b/src/i18n/fr_FR.UTF-8.json @@ -118,7 +118,8 @@ "PLEASE DO NOT UNPLUG YOUR DEVICE": "VEUILLEZ NE PAS DÉBRANCHER VOTRE APPAREIL", "DONE": "FAIT", "Back": "Retour", - "Quit": "Fermer" + "Quit": "Fermer", + "disconnect and reconnect device before flash again": "déconnectez et reconnectez l'appareil avant de flasher à nouveau" }, "warning_wipe_screen": { "You are about to initiate a FULL WIPE of this device": "Vous êtes sur le point de lancer un EFFACEMENT COMPLET de cet appareil", diff --git a/src/i18n/it_IT.UTF-8.json b/src/i18n/it_IT.UTF-8.json index 88440484..2de0b354 100644 --- a/src/i18n/it_IT.UTF-8.json +++ b/src/i18n/it_IT.UTF-8.json @@ -118,7 +118,8 @@ "PLEASE DO NOT UNPLUG YOUR DEVICE": "SI PREGA DI NON SCOLLEGARE IL DISPOSITIVO", "DONE": "FATTO", "Back": "Indietro", - "Quit": "Dimettersi" + "Quit": "Dimettersi", + "disconnect and reconnect device before flash again": "disconnetti e riconnetti il dispositivo prima di flashare di nuovo" }, "warning_wipe_screen": { "You are about to initiate a FULL WIPE of this device": "Stai per avviare una CANCELLAZIONE COMPLETA di questo dispositivo", diff --git a/src/i18n/ja_JP.UTF-8.json b/src/i18n/ja_JP.UTF-8.json index e001f912..829aef62 100644 --- a/src/i18n/ja_JP.UTF-8.json +++ b/src/i18n/ja_JP.UTF-8.json @@ -118,7 +118,8 @@ "PLEASE DO NOT UNPLUG YOUR DEVICE": "デバイスの電源を切らないでください。", "DONE": "完了", "Back": "戻る", - "Quit": "終了" + "Quit": "終了", + "disconnect and reconnect device before flash again": "再フラッシュする前にデバイスを切断して再接続してください" }, "warning_wipe_screen": { "You are about to initiate a FULL WIPE of this device": "このデバイスの完全消去を開始しようとしています。", diff --git a/src/i18n/ko_KR.UTF-8.json b/src/i18n/ko_KR.UTF-8.json index f51fcb80..d6659d86 100644 --- a/src/i18n/ko_KR.UTF-8.json +++ b/src/i18n/ko_KR.UTF-8.json @@ -118,7 +118,8 @@ "PLEASE DO NOT UNPLUG YOUR DEVICE": "장치의 플러그를 뽑지 마십시오.", "DONE": "수행", "Back": "뒤로", - "Quit": "사임하다" + "Quit": "사임하다", + "disconnect and reconnect device before flash again": "다시 플래시하기 전에 장치를 연결 해제하고 다시 연결하세요" }, "warning_wipe_screen": { "You are about to initiate a FULL WIPE of this device": "이 장치의 전체 지우기를 시작하려고 합니다.", diff --git a/src/i18n/nl_NL.UTF-8.json b/src/i18n/nl_NL.UTF-8.json index c3d6e65f..3b5ce88b 100644 --- a/src/i18n/nl_NL.UTF-8.json +++ b/src/i18n/nl_NL.UTF-8.json @@ -116,7 +116,8 @@ "PLEASE DO NOT UNPLUG YOUR DEVICE": "KOPPEL HET APPARAAT NIET LOS", "DONE": "KLAAR", "Back": "Terug", - "Quit": "Stoppen" + "Quit": "Stoppen", + "disconnect and reconnect device before flash again": "koppel het apparaat los en sluit het\nopnieuw aan voordat je opnieuw flasht" }, "warning_wipe_screen": { "You are about to initiate a FULL WIPE of this device": "Je staat op het punt om te starten met het VOLLEDIGE WISSEN van dit apparaat", diff --git a/src/i18n/pt_BR.UTF-8.json b/src/i18n/pt_BR.UTF-8.json index aebe3db9..30d9a9f8 100644 --- a/src/i18n/pt_BR.UTF-8.json +++ b/src/i18n/pt_BR.UTF-8.json @@ -118,7 +118,8 @@ "PLEASE DO NOT UNPLUG YOUR DEVICE": "POR FAVOR, NÃO DESCONECTE SEU DISPOSITIVO", "DONE": "FEITO", "Back": "Voltar", - "Quit": "Sair" + "Quit": "Sair", + "disconnect and reconnect device before flash again": "desconecte e reconecte o dispositivo antes de realizar o flash novamente" }, "warning_wipe_screen": { "You are about to initiate a FULL WIPE of this device": "Você está prestes a iniciar uma limpeza completa deste dispositivo", diff --git a/src/i18n/ru_RU.UTF-8.json b/src/i18n/ru_RU.UTF-8.json index fe611206..78f3939e 100644 --- a/src/i18n/ru_RU.UTF-8.json +++ b/src/i18n/ru_RU.UTF-8.json @@ -118,7 +118,8 @@ "PLEASE DO NOT UNPLUG YOUR DEVICE": "ПОЖАЛУЙСТА, НЕ ОТКЛЮЧАЙТЕ УСТРОЙСТВО ОТ СЕТИ", "DONE": "ДОГОВОРИЛИСЬ", "Back": "Назад", - "Quit": "Покидать" + "Quit": "Покидать", + "disconnect and reconnect device before flash again": "отсоедините и снова подключите\nустройство перед повторной прошивкой" }, "warning_wipe_screen": { "You are about to initiate a FULL WIPE of this device": "Вы собираетесь инициировать ПОЛНУЮ ОЧИСТКУ этого устройства", diff --git a/src/i18n/zh_CN.UTF-8.json b/src/i18n/zh_CN.UTF-8.json index 20b66ddd..527cb177 100644 --- a/src/i18n/zh_CN.UTF-8.json +++ b/src/i18n/zh_CN.UTF-8.json @@ -118,7 +118,8 @@ "PLEASE DO NOT UNPLUG YOUR DEVICE": "请不要拔下您的设备", "DONE": "完成", "Back": "返回", - "Quit": "退出" + "Quit": "退出", + "disconnect and reconnect device before flash again": "在重新刷机之前,请断开并重新连接设备" }, "warning_wipe_screen": { "You are about to initiate a FULL WIPE of this device": "您的操作将会完全擦除此设备", diff --git a/src/utils/constants/__init__.py b/src/utils/constants/__init__.py index 4a0d4cac..4169c22d 100644 --- a/src/utils/constants/__init__.py +++ b/src/utils/constants/__init__.py @@ -33,6 +33,7 @@ ROOT_DIRNAME = os.path.abspath(os.path.dirname(__file__)) VALID_DEVICES_VERSIONS = { + "v24.11.0": ["m5stickv", "amigo", "dock", "bit", "yahboom", "cube", "wonder_mv"], "v24.09.1": ["m5stickv", "amigo", "dock", "bit", "yahboom", "cube", "wonder_mv"], "v24.09.0": ["m5stickv", "amigo", "dock", "bit", "yahboom", "cube", "wonder_mv"], "v24.07.0": ["m5stickv", "amigo", "dock", "bit", "yahboom", "cube"], @@ -62,7 +63,7 @@ def _open_pyproject() -> dict[str, Any]: like name, version and description """ if sys.version_info.minor <= 10: - # pylint: disable=import-outside-toplevel + # pylint: disable=import-outside-toplevel,import-error from tomli import loads as load_toml if sys.version_info.minor > 10: # pylint: disable=import-outside-toplevel,import-error diff --git a/src/utils/flasher/base_flasher.py b/src/utils/flasher/base_flasher.py index e53c96be..fa6ebdc7 100644 --- a/src/utils/flasher/base_flasher.py +++ b/src/utils/flasher/base_flasher.py @@ -55,6 +55,7 @@ class BaseFlasher(Trigger): def __init__(self): super().__init__() self.ktool = KTool() + self.stop_thread = False @property def firmware(self) -> str: diff --git a/src/utils/flasher/flasher.py b/src/utils/flasher/flasher.py index 1d6ab1f3..96507c34 100644 --- a/src/utils/flasher/flasher.py +++ b/src/utils/flasher/flasher.py @@ -69,11 +69,11 @@ def flash(self, callback: typing.Callable): callback=callback, ) + except StopIteration as stop_exc: + self.ktool.__class__.log(str(stop_exc)) + # pylint: disable=broad-exception-caught except Exception as exc: - self.ktool.__class__.log(f"{str(exc)} for {self.port}") - self.ktool.__class__.log("") - try: newport = next(self._available_ports_generator) if self.is_port_working(newport.device): @@ -93,6 +93,9 @@ def flash(self, callback: typing.Callable): except StopIteration as stop_exc: self.ktool.__class__.log(str(stop_exc)) + except Exception as gen_exc: + self.ktool.__class__.log(str(gen_exc)) + else: exc = RuntimeError(f"Port {self.port} not working") self.ktool.__class__.log(str(exc)) diff --git a/src/utils/flasher/wiper.py b/src/utils/flasher/wiper.py index 4fd84532..f6df3c24 100644 --- a/src/utils/flasher/wiper.py +++ b/src/utils/flasher/wiper.py @@ -47,11 +47,11 @@ def wipe(self, device: str): self.ktool.process() + except StopIteration as stop_exc: + self.ktool.__class__.log(str(stop_exc)) + # pylint: disable=broad-exception-caught except Exception as exc: - self.ktool.__class__.log(f"{str(exc)} for {self.port}") - self.ktool.__class__.log("") - try: newport = next(self._available_ports_generator) if self.is_port_working(newport.device): @@ -73,6 +73,9 @@ def wipe(self, device: str): except StopIteration as stop_exc: self.ktool.__class__.log(str(stop_exc)) + except Exception as gen_exc: + self.ktool.__class__.log(str(gen_exc)) + else: exc = RuntimeError(f"Port {self.port} not working") self.ktool.__class__.log(str(exc)) diff --git a/tests/test_000_constants.py b/tests/test_000_constants.py index de93c93a..6acb3ec8 100644 --- a/tests/test_000_constants.py +++ b/tests/test_000_constants.py @@ -1,14 +1,20 @@ import os + +# import sys from unittest import TestCase -from unittest.mock import mock_open, patch +from unittest.mock import mock_open, patch, MagicMock from src.utils.constants import _open_pyproject, get_name, get_version, get_description -PYPROJECT_STR = """ -[tool.poetry] +PYPROJECT_STR = """[tool.poetry] name = "test" version = "0.0.1" -description = "Hello World!" -""" +description = \"Hello World!\"""" + +MOCK_TOML_DATA = { + "tool": { + "poetry": {"name": "test", "version": "0.0.1", "description": "Hello World!"} + } +} class TestConstants(TestCase): @@ -19,26 +25,19 @@ def test_open_pyproject_with_py_minor_version_10( self, open_mock, mock_version_info ): mock_version_info.minor = 9 - - rootdirname = os.path.abspath(os.path.dirname(__file__)) - pyproject_filename = os.path.abspath( - os.path.join(rootdirname, "..", "pyproject.toml") - ) - - data = _open_pyproject() - open_mock.assert_called_once_with(pyproject_filename, "r", encoding="utf8") - self.assertEqual( - data, - { - "tool": { - "poetry": { - "name": "test", - "version": "0.0.1", - "description": "Hello World!", - } - } - }, - ) + mock_tomli = MagicMock() + mock_tomli.loads.return_value = MOCK_TOML_DATA + + with patch.dict("sys.modules", {"tomli": mock_tomli}): + rootdirname = os.path.abspath(os.path.dirname(__file__)) + pyproject_filename = os.path.abspath( + os.path.join(rootdirname, "..", "pyproject.toml") + ) + + data = _open_pyproject() + open_mock.assert_called_once_with(pyproject_filename, "r", encoding="utf8") + mock_tomli.loads.assert_called_once_with(PYPROJECT_STR.strip()) + self.assertEqual(data, MOCK_TOML_DATA) @patch("builtins.open", new_callable=mock_open, read_data=PYPROJECT_STR) def test_open_pyproject(self, open_mock): diff --git a/tests/test_025_flasher.py b/tests/test_025_flasher.py index 7334ef02..b3319ca4 100644 --- a/tests/test_025_flasher.py +++ b/tests/test_025_flasher.py @@ -199,13 +199,7 @@ def test_fail_flash_after_first_greeting_fail_port_not_working( ), ] ) - mock_ktool_log.assert_has_calls( - [ - call("Greeting fail: mock test for mocked"), - call(""), - call("Port mocked_next not working"), - ] - ) + mock_ktool_log.assert_has_calls([call("Port mocked_next not working")]) @patch("os.path.exists", return_value=True) @patch("src.utils.flasher.base_flasher.list_ports", new_callable=MockListPortsGrep) @@ -228,7 +222,9 @@ def test_fail_flash_after_first_greeting_fail_stop_iteration( mock_process.side_effect = [mock_exception, True] mock_next.side_effect = [MagicMock(device="mocked")] - mock_list_ports.grep.return_value.__next__.side_effect = [StopIteration()] + mock_list_ports.grep.return_value.__next__.side_effect = [ + StopIteration("mocked stop") + ] callback = MagicMock() f = Flasher() @@ -257,6 +253,4 @@ def test_fail_flash_after_first_greeting_fail_stop_iteration( ), ] ) - mock_ktool_log.assert_has_calls( - [call("Greeting fail: mock test for mocked"), call("")] - ) + mock_ktool_log.assert_has_calls([call("mocked stop")]) diff --git a/tests/test_026_wiper.py b/tests/test_026_wiper.py index 992e8578..23184b9e 100644 --- a/tests/test_026_wiper.py +++ b/tests/test_026_wiper.py @@ -141,13 +141,7 @@ def test_fail_wipe_after_first_greeting_fail_port_not_working( ] ) mock_process.assert_called_once() - mock_ktool_log.assert_has_calls( - [ - call("Greeting fail: mock test for mocked"), - call(""), - call("Port mocked_next not working"), - ] - ) + mock_ktool_log.assert_has_calls([call("Port mocked_next not working")]) @patch("src.utils.flasher.base_flasher.list_ports", new_callable=MockListPortsGrep) @patch("src.utils.flasher.base_flasher.next") @@ -165,7 +159,9 @@ def test_fail_wipe_after_first_greeting_fail_stop_iteration( mock_exception = Exception("Greeting fail: mock test") mock_process.side_effect = [mock_exception, True] mock_next.side_effect = [MagicMock(device="mocked")] - mock_list_ports.grep.return_value.__next__.side_effect = [StopIteration()] + mock_list_ports.grep.return_value.__next__.side_effect = [ + StopIteration("mocked stop") + ] f = Wiper() f.baudrate = 1500000 @@ -180,6 +176,4 @@ def test_fail_wipe_after_first_greeting_fail_stop_iteration( ] ) # mock_process.assert_called_once() - mock_ktool_log.assert_has_calls( - [call("Greeting fail: mock test for mocked"), call("")] - ) + mock_ktool_log.assert_has_calls([call("mocked stop")])