From 60f9fc27b4a27f57d1d567b25a50b37de611b19a Mon Sep 17 00:00:00 2001 From: Ted Lawson Date: Sun, 1 Oct 2023 01:19:39 -0700 Subject: [PATCH] Unit test improvements and coverage increase. By @bigtedde (#1787) --- src/vorta/application.py | 2 +- src/vorta/i18n/ts/vorta.de.ts | 10 +-- src/vorta/i18n/ts/vorta.es.ts | 6 +- src/vorta/i18n/ts/vorta.fi.ts | 10 +-- tests/unit/test_archives.py | 16 +++++ tests/unit/test_diff.py | 123 +++++++++++++++++----------------- tests/unit/test_misc.py | 82 ++++++++++++++++------- tests/unit/test_profile.py | 42 +++++++----- tests/unit/test_repo.py | 103 ++++++++++++++++++++++++++++ tests/unit/test_source.py | 32 ++++++++- tests/unit/test_utils.py | 32 ++++++++- 11 files changed, 338 insertions(+), 120 deletions(-) diff --git a/src/vorta/application.py b/src/vorta/application.py index 921fe02f..357eb2df 100644 --- a/src/vorta/application.py +++ b/src/vorta/application.py @@ -325,7 +325,7 @@ class VortaApp(QtSingleApplication): # No fail logger.warning('VortaApp.check_failed_response was called with returncode 0') elif returncode == 130: - # Keyboard interupt + # Keyboard interrupt pass else: # Real error # Create QMessageBox diff --git a/src/vorta/i18n/ts/vorta.de.ts b/src/vorta/i18n/ts/vorta.de.ts index 1bd2463d..aa49dddd 100644 --- a/src/vorta/i18n/ts/vorta.de.ts +++ b/src/vorta/i18n/ts/vorta.de.ts @@ -1489,11 +1489,11 @@ - Schema upgrade failure, file a bug report with the link in the Misc tab with the following error: - {0} + Schema upgrade failure, file a bug report with the link in the Misc tab with the following error: + {0} {1} - Schema-Upgrade Fehler, erstelle einen Bugreport auf dem Link um "Misc"-Tab, mit folgendem Fehler: - {0} + Schema-Upgrade Fehler, erstelle einen Bugreport auf dem Link um "Misc"-Tab, mit folgendem Fehler: + {0} {1} @@ -2267,4 +2267,4 @@ Speichere Kennwort in der Vortakonfiguration - \ No newline at end of file + diff --git a/src/vorta/i18n/ts/vorta.es.ts b/src/vorta/i18n/ts/vorta.es.ts index e7aaf64e..9d7bc65f 100644 --- a/src/vorta/i18n/ts/vorta.es.ts +++ b/src/vorta/i18n/ts/vorta.es.ts @@ -1491,8 +1491,8 @@ - Schema upgrade failure, file a bug report with the link in the Misc tab with the following error: - {0} + Schema upgrade failure, file a bug report with the link in the Misc tab with the following error: + {0} {1} Fallo al actualizar el esquema, rellene un informe de error con el enlace de la pestaña «Varios» con el siguiente error: {0} @@ -2269,4 +2269,4 @@ Guardar contraseñas con los ajustes de Vorta. - \ No newline at end of file + diff --git a/src/vorta/i18n/ts/vorta.fi.ts b/src/vorta/i18n/ts/vorta.fi.ts index 3415ff60..5ae1e50c 100644 --- a/src/vorta/i18n/ts/vorta.fi.ts +++ b/src/vorta/i18n/ts/vorta.fi.ts @@ -1491,11 +1491,11 @@ - Schema upgrade failure, file a bug report with the link in the Misc tab with the following error: - {0} + Schema upgrade failure, file a bug report with the link in the Misc tab with the following error: + {0} {1} - Skeeman päivitys epäonnistui, lähetä virheraportti Sekalaiset-välilehdellä olevasta linkistä. Liitä raporttiin seuraavat tiedot: - {0} + Skeeman päivitys epäonnistui, lähetä virheraportti Sekalaiset-välilehdellä olevasta linkistä. Liitä raporttiin seuraavat tiedot: + {0} {1} @@ -2269,4 +2269,4 @@ Tallennetaan salasana Vortan asetuksiin. - \ No newline at end of file + diff --git a/tests/unit/test_archives.py b/tests/unit/test_archives.py index d965cd02..e0a7fb1a 100644 --- a/tests/unit/test_archives.py +++ b/tests/unit/test_archives.py @@ -6,6 +6,7 @@ import vorta.borg import vorta.utils import vorta.views.archive_tab from PyQt6 import QtCore +from PyQt6.QtWidgets import QMenu from vorta.store.models import ArchiveModel, BackupProfileModel @@ -202,3 +203,18 @@ def test_inline_archive_rename(qapp, qtbot, mocker, borg_json_output, archive_en # Successful rename case qtbot.waitUntil(lambda: tab.archiveTable.model().index(0, 4).data() == new_archive_name, **pytest._wait_defaults) assert tab.archiveTable.model().index(0, 4).data() == new_archive_name + + +def test_archiveitem_contextmenu(qapp, qtbot, archive_env): + main, tab = archive_env + + pos = tab.archiveTable.visualRect(tab.archiveTable.model().index(0, 0)).center() + tab.archiveTable.customContextMenuRequested.emit(pos) + qtbot.waitUntil(lambda: tab.archiveTable.findChild(QMenu) is not None, timeout=2000) + + context_menu = tab.archiveTable.findChild(QMenu) + + assert context_menu is not None + expected_actions = ['Copy', 'Recalculate', 'Mount…', 'Extract…', 'Rename…', 'Delete', 'Diff'] + for action in expected_actions: + assert any(menu_actions.text() == action for menu_actions in context_menu.actions()) diff --git a/tests/unit/test_diff.py b/tests/unit/test_diff.py index 91c1e1cd..cc001f02 100644 --- a/tests/unit/test_diff.py +++ b/tests/unit/test_diff.py @@ -5,6 +5,7 @@ import vorta.borg import vorta.utils import vorta.views.archive_tab from PyQt6.QtCore import QDateTime, QItemSelectionModel, Qt +from PyQt6.QtWidgets import QMenu from vorta.views.diff_result import ( ChangeType, DiffData, @@ -15,12 +16,8 @@ from vorta.views.diff_result import ( ) -@pytest.mark.parametrize( - 'json_mock_file,folder_root', [('diff_archives', 'test'), ('diff_archives_dict_issue', 'Users')] -) -def test_archive_diff(qapp, qtbot, mocker, borg_json_output, json_mock_file, folder_root, archive_env): - main, tab = archive_env - +def setup_diff_result_window(qtbot, mocker, tab, borg_json_output, json_mock_file="diff_archives"): + """Sets up the diff result window.""" stdout, stderr = borg_json_output(json_mock_file) popen_result = mocker.MagicMock(stdout=stdout, stderr=stderr, returncode=0) mocker.patch.object(vorta.borg.borg_job, 'Popen', return_value=popen_result) @@ -46,14 +43,70 @@ def test_archive_diff(qapp, qtbot, mocker, borg_json_output, json_mock_file, fol tab.diff_action() qtbot.waitUntil(lambda: hasattr(tab, '_resultwindow'), **pytest._wait_defaults) + assert hasattr(tab, '_resultwindow') + + +@pytest.mark.parametrize( + 'json_mock_file, folder_root', [('diff_archives', 'test'), ('diff_archives_dict_issue', 'Users')] +) +def test_archive_diff(qapp, qtbot, mocker, borg_json_output, json_mock_file, folder_root, archive_env): + """Tests basic functionality of archive diff.""" + main, tab = archive_env + setup_diff_result_window(qtbot, mocker, tab, borg_json_output, json_mock_file) model = tab._resultwindow.treeView.model().sourceModel() assert model.root.children[0].subpath == folder_root - assert tab._resultwindow.archiveNameLabel_1.text() == 'test-archive' tab._resultwindow.accept() +def test_diff_item_copy(qapp, qtbot, mocker, borg_json_output, archive_env): + """Tests copy action by row selection and when passed an index.""" + main, tab = archive_env + setup_diff_result_window(qtbot, mocker, tab, borg_json_output) + + # mock the clipboard to ensure no changes are made to it during testing + mocker.patch.object(qapp.clipboard(), "setMimeData") + clipboard_spy = mocker.spy(qapp.clipboard(), "setMimeData") + + # test 'diff_item_copy()' by passing it an item to copy + index = tab._resultwindow.treeView.model().index(0, 0) + assert index is not None + tab._resultwindow.diff_item_copy(index) + clipboard_data = clipboard_spy.call_args[0][0] + assert clipboard_data.hasText() + assert clipboard_data.text() == "/test" + + clipboard_spy.reset_mock() + + # test 'diff_item_copy()' by selecting a row to copy + flags = QItemSelectionModel.SelectionFlag.Rows + flags |= QItemSelectionModel.SelectionFlag.Select + tab._resultwindow.treeView.selectionModel().select(tab._resultwindow.treeView.model().index(0, 0), flags) + tab._resultwindow.diff_item_copy() + clipboard_data = clipboard_spy.call_args[0][0] + assert clipboard_data.hasText() + assert clipboard_data.text() == "/test" + + +def test_treeview_context_menu(qapp, qtbot, mocker, borg_json_output, archive_env): + """Tests the diff result window context menu for expected actions.""" + main, tab = archive_env + setup_diff_result_window(qtbot, mocker, tab, borg_json_output) + + # Load the context menu at the first result in window + pos = tab._resultwindow.treeView.visualRect(tab._resultwindow.treeView.model().index(0, 0)).center() + tab._resultwindow.treeview_context_menu(pos) + qtbot.waitUntil(lambda: tab._resultwindow.findChild(QMenu) is not None, **pytest._wait_defaults) + context_menu = tab._resultwindow.findChild(QMenu) + assert context_menu is not None + + # assert the actions are available in the context menu + expected_actions = ['Copy', 'Expand recursively'] + for action in expected_actions: + assert any(menu_actions.text() == action for menu_actions in context_menu.actions()) + + @pytest.mark.parametrize( 'line, expected', [ @@ -404,59 +457,3 @@ def test_archive_diff_json_parser(line, expected): assert item.path == PurePath(expected[0]).parts assert item.data == DiffData(*expected[1:]) - - -def test_diff_item_copy(qapp, qtbot, mocker, borg_json_output): - main = qapp.main_window - tab = main.archiveTab - main.tabWidget.setCurrentIndex(3) - - tab.populate_from_profile() - qtbot.waitUntil(lambda: tab.archiveTable.rowCount() == 2) - - stdout, stderr = borg_json_output("diff_archives") - popen_result = mocker.MagicMock(stdout=stdout, stderr=stderr, returncode=0) - mocker.patch.object(vorta.borg.borg_job, 'Popen', return_value=popen_result) - - compat = vorta.utils.borg_compat - - def check(feature_name): - if feature_name == 'DIFF_JSON_LINES': - return False - return vorta.utils.BorgCompatibility.check(compat, feature_name) - - mocker.patch.object(vorta.utils.borg_compat, 'check', check) - - selection_model: QItemSelectionModel = tab.archiveTable.selectionModel() - model = tab.archiveTable.model() - - flags = QItemSelectionModel.SelectionFlag.Rows - flags |= QItemSelectionModel.SelectionFlag.Select - - selection_model.select(model.index(0, 0), flags) - selection_model.select(model.index(1, 0), flags) - - tab.diff_action() - - qtbot.waitUntil(lambda: hasattr(tab, '_resultwindow'), **pytest._wait_defaults) - - # mock the clipboard to ensure no changes are made to it during testing - mocker.patch.object(qapp.clipboard(), "setMimeData") - clipboard_spy = mocker.spy(qapp.clipboard(), "setMimeData") - - # test 'diff_item_copy()' by passing it an item to copy - index = tab._resultwindow.treeView.model().index(0, 0) - assert index is not None - tab._resultwindow.diff_item_copy(index) - clipboard_data = clipboard_spy.call_args[0][0] - assert clipboard_data.hasText() - assert clipboard_data.text() == "/test" - - clipboard_spy.reset_mock() - - # test 'diff_item_copy()' by selecting a row to copy - tab._resultwindow.treeView.selectionModel().select(tab._resultwindow.treeView.model().index(0, 0), flags) - tab._resultwindow.diff_item_copy() - clipboard_data = clipboard_spy.call_args[0][0] - assert clipboard_data.hasText() - assert clipboard_data.text() == "/test" diff --git a/tests/unit/test_misc.py b/tests/unit/test_misc.py index eb1a5ac1..e4842283 100644 --- a/tests/unit/test_misc.py +++ b/tests/unit/test_misc.py @@ -6,33 +6,74 @@ from unittest.mock import Mock import pytest import vorta.store.models from PyQt6 import QtCore -from PyQt6.QtWidgets import QCheckBox, QFormLayout +from PyQt6.QtGui import QCloseEvent +from PyQt6.QtWidgets import QCheckBox, QFormLayout, QMessageBox +from vorta.store.models import SettingsModel -def test_autostart(qapp, qtbot): - """Check if file exists only on Linux, otherwise just check it doesn't crash""" +def test_toggle_all_settings(qapp, qtbot): + """Toggle each setting twice as a basic sanity test to ensure app does crash.""" + groups = ( + SettingsModel.select(SettingsModel.group) + .distinct(True) + .where(SettingsModel.group != '') + .order_by(SettingsModel.group.asc()) + ) + + settings = [ + setting + for group in groups + for setting in SettingsModel.select().where( + SettingsModel.type == 'checkbox', SettingsModel.group == group.group + ) + ] + + for setting in settings: + for _ in range(2): + _click_toggle_setting(setting.label, qapp, qtbot) + + +@pytest.mark.skipif(sys.platform != "linux", reason="testing autostart path for Linux only") +def test_autostart_linux(qapp, qtbot): + """Checks that autostart path is added correctly on Linux when setting is enabled.""" setting = "Automatically start Vorta at login" + # ensure file is present when autostart is enabled _click_toggle_setting(setting, qapp, qtbot) + autostart_path = ( + Path(os.environ.get("XDG_CONFIG_HOME", os.path.expanduser("~") + '/.config') + "/autostart") / "vorta.desktop" + ) + qtbot.waitUntil(lambda: autostart_path.exists(), **pytest._wait_defaults) + with open(autostart_path) as desktop_file: + desktop_file_text = desktop_file.read() + assert desktop_file_text.startswith("[Desktop Entry]") - if sys.platform == 'linux': - autostart_path = ( - Path(os.environ.get("XDG_CONFIG_HOME", os.path.expanduser("~") + '/.config') + "/autostart") - / "vorta.desktop" - ) - qtbot.waitUntil(lambda: autostart_path.exists(), **pytest._wait_defaults) - - with open(autostart_path) as desktop_file: - desktop_file_text = desktop_file.read() - - assert desktop_file_text.startswith("[Desktop Entry]") - + # ensure file is removed when autostart is disabled _click_toggle_setting(setting, qapp, qtbot) - if sys.platform == 'linux': assert not os.path.exists(autostart_path) +def test_enable_background_question(qapp, monkeypatch, mocker): + """Tests that 'enable background question' correctly prompts user.""" + main = qapp.main_window + close_event = Mock(value=QCloseEvent()) + + # disable system trey and enable setting to test + monkeypatch.setattr("vorta.views.main_window.is_system_tray_available", lambda: False) + mocker.patch.object(vorta.store.models.SettingsModel, "get", return_value=Mock(value=True)) + mocker.patch.object(QMessageBox, "exec") # prevent QMessageBox from stopping test + + # Create a mock for QMessageBox and its setText method + mock_msgbox = mocker.Mock(spec=QMessageBox) + mocker.patch("vorta.views.main_window.QMessageBox", return_value=mock_msgbox) + + main.closeEvent(close_event) + + mock_msgbox.setText.assert_called_once_with("Should Vorta continue to run in the background?") + close_event.accept.assert_called_once() + + def test_enable_fixed_units(qapp, qtbot, mocker): """Tests the 'enable fixed units' setting to ensure the archive tab sizes are displayed correctly.""" tab = qapp.main_window.archiveTab @@ -61,14 +102,13 @@ def test_enable_fixed_units(qapp, qtbot, mocker): assert kwargs_list['fixed_unit'] is None # use the qt bot to click the setting and see that the refresh_archive emit works as intended. - with qtbot.waitSignal(qapp.main_window.miscTab.refresh_archive, timeout=5000): + with qtbot.waitSignal(qapp.main_window.miscTab.refresh_archive, **pytest._wait_defaults): _click_toggle_setting(setting, qapp, qtbot) @pytest.mark.skipif(sys.platform != 'darwin', reason="Full Disk Access check only on Darwin") def test_check_full_disk_access(qapp, qtbot, mocker): - """Enables/disables 'Check for Full Disk Access on startup' setting and ensures functionality""" - setting = "Check for Full Disk Access on startup" + """Tests if the full disk access warning is properly silenced with the setting enabled""" # Set mocks for setting enabled mocker.patch.object(vorta.store.models.SettingsModel, "get", return_value=Mock(value=True)) @@ -88,10 +128,6 @@ def test_check_full_disk_access(qapp, qtbot, mocker): qapp.check_darwin_permissions() mock_qmessagebox.assert_not_called() - # Checks that setting doesn't crash program when click toggled on then off""" - _click_toggle_setting(setting, qapp, qtbot) - _click_toggle_setting(setting, qapp, qtbot) - def _click_toggle_setting(setting, qapp, qtbot): """Toggle setting checkbox in the misc tab""" diff --git a/tests/unit/test_profile.py b/tests/unit/test_profile.py index f7c58bb7..04cc9782 100644 --- a/tests/unit/test_profile.py +++ b/tests/unit/test_profile.py @@ -1,37 +1,49 @@ from PyQt6 import QtCore -from PyQt6.QtWidgets import QDialogButtonBox +from PyQt6.QtWidgets import QDialogButtonBox, QMessageBox, QToolTip from vorta.store.models import BackupProfileModel -def test_profile_add(qapp, qtbot): +def test_profile_add_delete(qapp, qtbot, mocker): + """Tests adding and deleting profiles.""" main = qapp.main_window + + # add profile and ensure it is created as intended qtbot.mouseClick(main.profileAddButton, QtCore.Qt.MouseButton.LeftButton) - add_profile_window = main.window - # qtbot.addWidget(add_profile_window) - qtbot.keyClicks(add_profile_window.profileNameField, 'Test Profile') - qtbot.mouseClick( - add_profile_window.buttonBox.button(QDialogButtonBox.StandardButton.Save), QtCore.Qt.MouseButton.LeftButton - ) - + save_button = add_profile_window.buttonBox.button(QDialogButtonBox.StandardButton.Save) + qtbot.mouseClick(save_button, QtCore.Qt.MouseButton.LeftButton) assert BackupProfileModel.get_or_none(name='Test Profile') is not None assert main.profileSelector.currentText() == 'Test Profile' + # delete the new profile and ensure it is no longer available. + mocker.patch.object(QMessageBox, 'question', return_value=QMessageBox.StandardButton.Yes) + qtbot.mouseClick(main.profileDeleteButton, QtCore.Qt.MouseButton.LeftButton) + assert BackupProfileModel.get_or_none(name='Test Profile') is None + assert main.profileSelector.currentText() == 'Default' + + # attempt to delete the last remaining profile + # see that it cannot be deleted, a warning is displayed, and the profile remains + warning = mocker.patch.object(QToolTip, 'showText') + qtbot.mouseClick(main.profileDeleteButton, QtCore.Qt.MouseButton.LeftButton) + assert "Cannot delete the last profile." in warning.call_args[0][1] + assert BackupProfileModel.get_or_none(name='Default') is not None + assert main.profileSelector.currentText() == 'Default' + def test_profile_edit(qapp, qtbot): + """Tests editing/renaming a profile""" main = qapp.main_window + + # click to rename profile, clear the name field, type new profile name qtbot.mouseClick(main.profileRenameButton, QtCore.Qt.MouseButton.LeftButton) - edit_profile_window = main.window - # qtbot.addWidget(edit_profile_window) - edit_profile_window.profileNameField.setText("") qtbot.keyClicks(edit_profile_window.profileNameField, 'Test Profile') - qtbot.mouseClick( - edit_profile_window.buttonBox.button(QDialogButtonBox.StandardButton.Save), QtCore.Qt.MouseButton.LeftButton - ) + save_button = edit_profile_window.buttonBox.button(QDialogButtonBox.StandardButton.Save) + qtbot.mouseClick(save_button, QtCore.Qt.MouseButton.LeftButton) + # assert a profile by the old name no longer exists, and the newly named profile does exist and is selected. assert BackupProfileModel.get_or_none(name='Default') is None assert BackupProfileModel.get_or_none(name='Test Profile') is not None assert main.profileSelector.currentText() == 'Test Profile' diff --git a/tests/unit/test_repo.py b/tests/unit/test_repo.py index 8cbb29a3..e072119d 100644 --- a/tests/unit/test_repo.py +++ b/tests/unit/test_repo.py @@ -1,5 +1,6 @@ import os import uuid +from typing import Any, Dict import pytest import vorta.borg.borg_job @@ -187,6 +188,55 @@ def test_ssh_dialog_failure(qapp, qtbot, mocker, monkeypatch, tmpdir): assert tab.sshComboBox.count() == 1 +def test_ssh_copy_to_clipboard_action(qapp, qtbot, mocker, tmpdir): + """Testing the proper QMessageBox dialogue appears depending on the copy action circumstances.""" + tab = qapp.main_window.repoTab + + # set mocks to test assertions and prevent test interruptions + text = mocker.patch.object(QMessageBox, "setText") + mocker.patch.object(QMessageBox, "show") + mocker.patch.object(qapp.clipboard(), "setText") + + qtbot.mouseClick(tab.bAddSSHKey, QtCore.Qt.MouseButton.LeftButton) + ssh_dialog = tab._window + ssh_dialog_closed = mocker.spy(ssh_dialog, 'reject') + ssh_dir = tmpdir + key_tmpfile = ssh_dir.join("id_rsa-test") + pub_tmpfile = ssh_dir.join("id_rsa-test.pub") + key_tmpfile_full = os.path.join(key_tmpfile.dirname, key_tmpfile.basename) + ssh_dialog.outputFileTextBox.setText(key_tmpfile_full) + ssh_dialog.generate_key() + + # Ensure new key file was created + qtbot.waitUntil(lambda: ssh_dialog_closed.called, **pytest._wait_defaults) + assert len(ssh_dir.listdir()) == 2 + # populate the ssh combobox with the ssh key we created in tmpdir + mock_expanduser = mocker.patch('os.path.expanduser', return_value=str(tmpdir)) + tab.init_ssh() + assert tab.sshComboBox.count() == 2 + + # test when no ssh key is selected to copy + assert tab.sshComboBox.currentIndex() == 0 + qtbot.mouseClick(tab.sshKeyToClipboardButton, QtCore.Qt.MouseButton.LeftButton) + message = "Select a public key from the dropdown first." + text.assert_called_with(message) + + # Select a key and copy it + mock_expanduser.return_value = pub_tmpfile + tab.sshComboBox.setCurrentIndex(1) + assert tab.sshComboBox.currentIndex() == 1 + qtbot.mouseClick(tab.sshKeyToClipboardButton, QtCore.Qt.MouseButton.LeftButton) + message = "The selected public SSH key was copied to the clipboard. Use it to set up remote repo permissions." + text.assert_called_with(message) + + # handle ssh key file not found + mock_expanduser.return_value = "foobar" + assert tab.sshComboBox.currentIndex() == 1 + qtbot.mouseClick(tab.sshKeyToClipboardButton, QtCore.Qt.MouseButton.LeftButton) + message = "Could not find public key." + text.assert_called_with(message) + + def test_create(qapp, borg_json_output, mocker, qtbot): main = qapp.main_window stdout, stderr = borg_json_output('create') @@ -202,3 +252,56 @@ def test_create(qapp, borg_json_output, mocker, qtbot): assert main.createStartBtn.isEnabled() assert main.archiveTab.archiveTable.rowCount() == 3 assert main.scheduleTab.logTableWidget.rowCount() == 1 + + +@pytest.mark.parametrize( + "response", + [ + { + "return_code": 0, # no error + "error": "", + "icon": None, + "info": None, + }, + { + "return_code": 1, # warning + "error": "Borg exited with warning status (rc 1).", + "icon": QMessageBox.Icon.Warning, + "info": "", + }, + { + "return_code": 2, # critical error + "error": "Repository data check for repo test_repo_url failed. Error code 2", + "icon": QMessageBox.Icon.Critical, + "info": "Consider repairing or recreating the repository soon to avoid missing data.", + }, + { + "return_code": 135, # 128 + n = kill signal n + "error": "killed by signal 7", + "icon": QMessageBox.Icon.Critical, + "info": "The process running the check job got a kill signal. Try again.", + }, + {"return_code": 130, "error": "", "icon": None, "info": None}, # keyboard interrupt + ], +) +def test_repo_check_failed_response(qapp, qtbot, mocker, response): + """Test the processing of the signal that a repo consistency check has failed.""" + mock_result: Dict[str, Any] = { + 'params': {'repo_url': 'test_repo_url'}, + 'returncode': response["return_code"], + 'errors': [(0, 'test_error_message')] if response["return_code"] not in [0, 130] else None, + } + + mock_exec = mocker.patch.object(QMessageBox, "exec") + mock_text = mocker.patch.object(QMessageBox, "setText") + mock_info = mocker.patch.object(QMessageBox, "setInformativeText") + mock_icon = mocker.patch.object(QMessageBox, "setIcon") + + qapp.check_failed_response(mock_result) + + # return codes 0 and 130 do not provide a message + # for all other return codes, assert the message is formatted correctly + if mock_exec.call_count != 0: + mock_icon.assert_called_with(response["icon"]) + assert response["error"] in mock_text.call_args[0][0] + assert response["info"] in mock_info.call_args[0][0] diff --git a/tests/unit/test_source.py b/tests/unit/test_source.py index 10fa0ed4..70b01961 100644 --- a/tests/unit/test_source.py +++ b/tests/unit/test_source.py @@ -2,6 +2,8 @@ import pytest import vorta.views from PyQt6 import QtCore from PyQt6.QtWidgets import QMessageBox +from vorta.views.main_window import MainWindow +from vorta.views.source_tab import SourceTab @pytest.fixture() @@ -9,11 +11,11 @@ def source_env(qapp, qtbot, monkeypatch, choose_file_dialog): """ Handles common setup and teardown for unit tests involving the source tab. """ - monkeypatch.setattr(vorta.views.source_tab, "choose_file_dialog", choose_file_dialog) - main = qapp.main_window + main: MainWindow = qapp.main_window + tab: SourceTab = main.sourceTab main.tabWidget.setCurrentIndex(1) - tab = main.sourceTab qtbot.waitUntil(lambda: tab.sourceFilesWidget.rowCount() == 1, timeout=2000) + monkeypatch.setattr(vorta.views.source_tab, "choose_file_dialog", choose_file_dialog) yield main, tab @@ -100,3 +102,27 @@ def test_sources_update(qapp, qtbot, mocker, source_env): qtbot.mouseClick(tab.updateButton, QtCore.Qt.MouseButton.LeftButton) assert tab.sourceFilesWidget.rowCount() == 2 assert update_path_info_spy.call_count == 2 + + +def test_source_copy(qapp, qtbot, monkeypatch, mocker, source_env): + """ + Test source_copy() with and without an index passed. + If no index is passed, it should copy the first selected source + """ + main, tab = source_env + + mock_clipboard = mocker.patch.object(qapp.clipboard(), "setMimeData") + tab.source_add(want_folder=True) + qtbot.waitUntil(lambda: tab.sourceFilesWidget.rowCount() == 2, **pytest._wait_defaults) + + tab.sourceFilesWidget.selectRow(0) + tab.source_copy() + assert mock_clipboard.call_count == 1 + source = mock_clipboard.call_args[0][0] # retrieves the QMimeData() object used in method call + assert source.text() == "/tmp" + + index = tab.sourceFilesWidget.model().index(1, 0) + tab.source_copy(index) + assert mock_clipboard.call_count == 2 + source = mock_clipboard.call_args[0][0] # retrieves the QMimeData() object used in method call + assert source.text() == "/tmp/another" diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index cbb971b8..ea529c08 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -9,6 +9,7 @@ from vorta.utils import ( is_system_tray_available, normalize_path, pretty_bytes, + sort_sizes, ) @@ -21,6 +22,33 @@ def test_keyring(): assert keyring.get_password("vorta-repo", REPO) == UNICODE_PW +@pytest.mark.parametrize( + "input_sizes, expected_sorted", + [ + # Basic ordering + (["1.0 GB", "2.0 MB", "3.0 KB"], ["3.0 KB", "2.0 MB", "1.0 GB"]), + # Multiple same units + (["3.0 GB", "2.0 GB", "1.0 GB"], ["1.0 GB", "2.0 GB", "3.0 GB"]), + # Multiple different units + (["2.0 MB", "3.0 GB", "1.0 KB", "5.0 GB"], ["1.0 KB", "2.0 MB", "3.0 GB", "5.0 GB"]), + # Larger to smaller units + (["1.0 YB", "1.0 ZB", "1.0 EB", "1.0 PB"], ["1.0 PB", "1.0 EB", "1.0 ZB", "1.0 YB"]), + # Skipping non-numeric sizes + (["2x MB", "3.0 KB", "apple GB", "1.0 GB"], ["3.0 KB", "1.0 GB"]), + # Skipping invalid suffix + (["1.0 XX", "5.0 YY", "9.0 ZZ", "1.0 MB"], ["1.0 MB"]), + # Floats with decimals + (["2.5 GB", "2.3 GB", "1.1 MB"], ["1.1 MB", "2.3 GB", "2.5 GB"]), + # Checking the same sizes across different units + (["1.0 MB", "1000.0 KB"], ["1000.0 KB", "1.0 MB"]), + # Handle empty lists + ([], []), + ], +) +def test_sort_sizes(input_sizes, expected_sorted): + assert sort_sizes(input_sizes) == expected_sorted + + @pytest.mark.parametrize( "precision, expected_unit", [ @@ -60,7 +88,7 @@ def test_best_unit_for_sizes_nonmetric(sizes, expected_unit): ) def test_pretty_bytes_fixed_units(size, metric, precision, fixed_unit, expected_output): """ - test pretty bytes when specifying a fixed unit of measurement + Test pretty bytes when specifying a fixed unit of measurement """ output = pretty_bytes(size, metric=metric, precision=precision, fixed_unit=fixed_unit) assert output == expected_output @@ -131,7 +159,7 @@ def test_get_path_datasize(tmpdir): def test_is_system_tray_available(mocker): """ - sanity check to ensure proper behavior + Sanity check to ensure proper behavior """ mocker.patch('PyQt6.QtWidgets.QSystemTrayIcon.isSystemTrayAvailable', return_value=False) assert is_system_tray_available() is False