Added and fixed tests for CloneVM tool

fixes QubesOS/qubes-issues#5978
This commit is contained in:
Marta Marczykowska-Górecka 2020-08-04 17:01:34 +02:00
parent 8461a307f2
commit 354578dc37
No known key found for this signature in database
GPG Key ID: 9A752C30B26FD04B
7 changed files with 291 additions and 26 deletions

1
debian/install vendored
View File

@ -65,6 +65,7 @@
/usr/lib/*/dist-packages/qubesmanager/tests/test_qube_manager.py /usr/lib/*/dist-packages/qubesmanager/tests/test_qube_manager.py
/usr/lib/*/dist-packages/qubesmanager/tests/test_create_new_vm.py /usr/lib/*/dist-packages/qubesmanager/tests/test_create_new_vm.py
/usr/lib/*/dist-packages/qubesmanager/tests/test_vm_settings.py /usr/lib/*/dist-packages/qubesmanager/tests/test_vm_settings.py
/usr/lib/*/dist-packages/qubesmanager/tests/test_clone_vm.py
/usr/lib/*/dist-packages/qubesmanager-*.egg-info/* /usr/lib/*/dist-packages/qubesmanager-*.egg-info/*

View File

@ -138,9 +138,11 @@ class CloneVMDlg(QtWidgets.QDialog, Ui_CloneVMDlg):
self.tr("Error cloning the qube!"), self.tr("Error cloning the qube!"),
self.tr("ERROR: {0}").format(self.thread.msg)) self.tr("ERROR: {0}").format(self.thread.msg))
else: else:
QtWidgets.QMessageBox.information( (title, msg) = self.thread.msg
QtWidgets.QMessageBox.warning(
self, self,
*self.thread.msg) title,
msg)
self.done(0) self.done(0)

View File

@ -0,0 +1,276 @@
#!/usr/bin/python3
#
# The Qubes OS Project, https://www.qubes-os.org/
#
# Copyright (C) 2020 Marta Marczykowska-Górecka
# <marmarta@invisiblethingslab.com>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
import logging.handlers
import unittest
import unittest.mock
from PyQt5 import QtTest, QtCore
from qubesadmin import Qubes
from qubesmanager.tests import init_qtapp
from qubesmanager import clone_vm
# TODO: test when you do give a src vm
class CloneVMTest(unittest.TestCase):
def setUp(self):
super(CloneVMTest, self).setUp()
self.qtapp, self.loop = init_qtapp()
self.qapp = Qubes()
# mock up the Create VM Thread to avoid changing system state
self.patcher_thread = unittest.mock.patch(
'qubesmanager.common_threads.CloneVMThread')
self.mock_thread = self.patcher_thread.start()
self.addCleanup(self.patcher_thread.stop)
# mock the progress dialog to speed testing up
self.patcher_progress = unittest.mock.patch(
'PyQt5.QtWidgets.QProgressDialog')
self.mock_progress = self.patcher_progress.start()
self.addCleanup(self.patcher_progress.stop)
# mock the progress dialog to speed testing up
self.patcher_warning = unittest.mock.patch(
'PyQt5.QtWidgets.QMessageBox.warning')
self.mock_warning = self.patcher_warning.start()
self.addCleanup(self.patcher_warning.stop)
self.dialog = clone_vm.CloneVMDlg(self.qtapp, self.qapp)
def test_00_window_loads(self):
self.assertGreater(self.dialog.src_vm.count(), 0,
"No source vms shown")
self.assertGreater(self.dialog.label.count(), 0, "No labels listed")
self.assertGreater(self.dialog.storage_pool.count(), 0,
"No pools listed")
self.assertTrue(self.dialog.src_vm.isEnabled(),
"source vm dialog not active")
def test_01_cancel_works(self):
self.__click_cancel()
self.assertEqual(self.mock_thread.call_count, 0,
"Attempted to create VM on cancel")
def test_02_name_correctly_updates(self):
src_name = self.dialog.src_vm.currentText()
target_name = self.dialog.name.text()
self.assertTrue(target_name.startswith(src_name),
"target name does not contain source name")
self.assertTrue('clone' in target_name,
"target name does not contain >clone<")
self.dialog.src_vm.setCurrentIndex(self.dialog.src_vm.currentIndex()+1)
src_name = self.dialog.src_vm.currentText()
target_name = self.dialog.name.text()
self.assertTrue(target_name.startswith(src_name),
"target name does not contain source name")
self.assertTrue('clone' in target_name,
"target name does not contain >clone<")
def test_03_label_correctly_updates(self):
src_label = self.dialog.src_vm.currentData().label.name
target_label = self.dialog.label.currentText()
self.assertEqual(src_label, target_label, "incorrect start label")
while self.dialog.src_vm.currentData().label.name == src_label:
self.dialog.src_vm.setCurrentIndex(
self.dialog.src_vm.currentIndex() + 1)
src_label = self.dialog.src_vm.currentData().label.name
target_label = self.dialog.label.currentText()
self.assertEqual(src_label, target_label,
"label did not change correctly")
def test_04_clone_first_vm(self):
self.dialog.name.setText("clone-test")
src_vm = self.qapp.domains[self.dialog.src_vm.currentText()]
self.__click_ok()
self.mock_thread.assert_called_once_with(
src_vm, "clone-test", pool=None, label=src_vm.label)
self.mock_thread().start.assert_called_once_with()
def test_05_clone_other_vm(self):
self.dialog.src_vm.setCurrentIndex(self.dialog.src_vm.currentIndex()+1)
src_vm = self.qapp.domains[self.dialog.src_vm.currentText()]
dst_name = self.dialog.name.text()
self.__click_ok()
self.mock_thread.assert_called_once_with(
src_vm, dst_name, pool=None, label=src_vm.label)
self.mock_thread().start.assert_called_once_with()
def test_06_clone_label(self):
src_vm = self.qapp.domains[self.dialog.src_vm.currentText()]
dst_name = self.dialog.name.text()
while self.dialog.label.currentText() != 'blue':
self.dialog.label.setCurrentIndex(
self.dialog.label.currentIndex()+1)
self.__click_ok()
self.mock_thread.assert_called_once_with(
src_vm, dst_name, pool=None, label=self.qapp.labels['blue'])
self.mock_thread().start.assert_called_once_with()
@unittest.mock.patch('subprocess.check_call')
def test_07_launch_settings(self, mock_call):
self.dialog.launch_settings.setChecked(True)
self.dialog.name.setText("clone-test")
self.__click_ok()
self.mock_thread.assert_called_once_with(
unittest.mock.ANY, "clone-test", pool=None,
label=unittest.mock.ANY)
self.mock_thread().msg = ("Success", "Success")
self.dialog.clone_finished()
mock_call.assert_called_once_with(['qubes-vm-settings', "clone-test"])
def test_08_progress_hides(self):
self.dialog.name.setText("clone-test")
self.__click_ok()
self.mock_thread.assert_called_once_with(
unittest.mock.ANY, "clone-test", pool=None,
label=unittest.mock.ANY)
# make sure the thread is not reporting an error
self.mock_thread().start.assert_called_once_with()
self.mock_thread().msg = ("Success", "Success")
self.mock_progress().show.assert_called_once_with()
self.dialog.clone_finished()
self.mock_progress().hide.assert_called_once_with()
def test_09_pool_nondefault(self):
while 'default' in self.dialog.storage_pool.currentText():
self.dialog.storage_pool.setCurrentIndex(
self.dialog.storage_pool.currentIndex()+1)
selected_pool = self.dialog.storage_pool.currentText()
self.__click_ok()
self.mock_thread.assert_called_once_with(
unittest.mock.ANY, unittest.mock.ANY,
pool=selected_pool,
label=unittest.mock.ANY)
self.mock_thread().start.assert_called_once_with()
def __click_ok(self):
okwidget = self.dialog.buttonBox.button(
self.dialog.buttonBox.Ok)
QtTest.QTest.mouseClick(okwidget, QtCore.Qt.LeftButton)
def __click_cancel(self):
cancelwidget = self.dialog.buttonBox.button(
self.dialog.buttonBox.Cancel)
QtTest.QTest.mouseClick(cancelwidget, QtCore.Qt.LeftButton)
class CloneVMTestSrcVM(unittest.TestCase):
def setUp(self):
super(CloneVMTestSrcVM, self).setUp()
self.qtapp, self.loop = init_qtapp()
self.qapp = Qubes()
# mock up the Create VM Thread to avoid changing system state
self.patcher_thread = unittest.mock.patch(
'qubesmanager.common_threads.CloneVMThread')
self.mock_thread = self.patcher_thread.start()
self.addCleanup(self.patcher_thread.stop)
# mock the progress dialog to speed testing up
self.patcher_progress = unittest.mock.patch(
'PyQt5.QtWidgets.QProgressDialog')
self.mock_progress = self.patcher_progress.start()
self.addCleanup(self.patcher_progress.stop)
# mock the progress dialog to speed testing up
self.patcher_warning = unittest.mock.patch(
'PyQt5.QtWidgets.QMessageBox.warning')
self.mock_warning = self.patcher_warning.start()
self.addCleanup(self.patcher_warning.stop)
self.src_vm = next(
domain for domain in self.qapp.domains
if domain.klass != 'AdminVM')
self.dialog = clone_vm.CloneVMDlg(self.qtapp, self.qapp,
src_vm=self.src_vm)
def test_00_window_loads(self):
self.assertEqual(self.dialog.src_vm.currentText(), self.src_vm.name)
self.assertEqual(self.dialog.src_vm.currentData(), self.src_vm)
self.assertFalse(self.dialog.src_vm.isEnabled(),
"source vm dialog active")
self.assertEqual(self.dialog.label.currentText(),
self.src_vm.label.name)
def test_01_simple_clone(self):
self.dialog.name.setText("clone-test")
self.__click_ok()
self.mock_thread.assert_called_once_with(
self.src_vm, "clone-test", pool=None, label=self.src_vm.label)
self.mock_thread().start.assert_called_once_with()
def __click_ok(self):
okwidget = self.dialog.buttonBox.button(
self.dialog.buttonBox.Ok)
QtTest.QTest.mouseClick(okwidget, QtCore.Qt.LeftButton)
if __name__ == "__main__":
ha_syslog = logging.handlers.SysLogHandler('/dev/log')
ha_syslog.setFormatter(
logging.Formatter('%(name)s[%(process)d]: %(message)s'))
logging.root.addHandler(ha_syslog)
unittest.main()

View File

@ -22,7 +22,6 @@
import logging.handlers import logging.handlers
import unittest import unittest
import unittest.mock import unittest.mock
import qubesadmin
from PyQt5 import QtTest, QtCore from PyQt5 import QtTest, QtCore
from qubesadmin import Qubes from qubesadmin import Qubes

View File

@ -637,9 +637,8 @@ class QubeManagerTest(unittest.TestCase):
self.dialog.action_manage_templates.trigger() self.dialog.action_manage_templates.trigger()
mock_subprocess.assert_called_once_with('qubes-template-manager') mock_subprocess.assert_called_once_with('qubes-template-manager')
@unittest.mock.patch('qubesmanager.common_threads.CloneVMThread') @unittest.mock.patch('qubesmanager.clone_vm.CloneVMDlg')
@unittest.mock.patch('PyQt5.QtWidgets.QInputDialog.getText') def test_232_clonevm(self, mock_clone):
def test_232_clonevm(self, mock_input, mock_thread):
action = self.dialog.action_clonevm action = self.dialog.action_clonevm
self._select_admin_vm() self._select_admin_vm()
@ -648,18 +647,9 @@ class QubeManagerTest(unittest.TestCase):
selected_vm = self._select_non_admin_vm() selected_vm = self._select_non_admin_vm()
self.assertTrue(action.isEnabled()) self.assertTrue(action.isEnabled())
mock_input.return_value = (selected_vm.name + "clone1", False)
action.trigger() action.trigger()
self.assertEqual(mock_thread.call_count, 0, mock_clone.assert_called_once_with(self.qtapp, self.qapp,
"Ignores cancelling clone VM") src_vm=selected_vm)
mock_input.return_value = (selected_vm.name + "clone1", True)
action.trigger()
mock_thread.assert_called_once_with(selected_vm,
selected_vm.name + "clone1")
mock_thread().finished.connect.assert_called_once_with(
self.dialog.clear_threads)
mock_thread().start.assert_called_once_with()
def test_233_search_action(self): def test_233_search_action(self):
self.qtapp.setActiveWindow(self.dialog.searchbox) self.qtapp.setActiveWindow(self.dialog.searchbox)

View File

@ -311,23 +311,19 @@ class VMSettingsTest(unittest.TestCase):
mock_thread.assert_called_with(self.vm, "test-vm2", unittest.mock.ANY) mock_thread.assert_called_with(self.vm, "test-vm2", unittest.mock.ANY)
mock_thread().start.assert_called_with() mock_thread().start.assert_called_with()
# TODO: thread tests for rename @unittest.mock.patch('qubesmanager.clone_vm.CloneVMDlg')
def test_12_clone_vm(self, mock_clone):
@unittest.mock.patch('PyQt5.QtWidgets.QProgressDialog')
@unittest.mock.patch('PyQt5.QtWidgets.QInputDialog.getText')
@unittest.mock.patch('qubesmanager.common_threads.CloneVMThread')
def test_12_clone_vm(self, mock_thread, mock_input, _):
self.vm = self.qapp.add_new_vm("AppVM", "test-vm", "blue") self.vm = self.qapp.add_new_vm("AppVM", "test-vm", "blue")
self.dialog = vm_settings.VMSettingsWindow( self.dialog = vm_settings.VMSettingsWindow(
self.vm, qapp=self.qtapp, qubesapp=self.qapp, init_page="basic") self.vm, qapp=self.qtapp, qubesapp=self.qapp, init_page="basic")
self.assertTrue(self.dialog.clone_vm_button.isEnabled()) self.assertTrue(self.dialog.clone_vm_button.isEnabled())
mock_input.return_value = ("test-vm2", True)
self.dialog.clone_vm_button.click() self.dialog.clone_vm_button.click()
mock_thread.assert_called_with(self.vm, "test-vm2") mock_clone.assert_called_once_with(self.qtapp, self.qapp,
mock_thread().start.assert_called_with() src_vm=self.vm)
@unittest.mock.patch('PyQt5.QtWidgets.QMessageBox.warning') @unittest.mock.patch('PyQt5.QtWidgets.QMessageBox.warning')
@unittest.mock.patch('PyQt5.QtWidgets.QProgressDialog') @unittest.mock.patch('PyQt5.QtWidgets.QProgressDialog')

View File

@ -122,6 +122,7 @@ rm -rf $RPM_BUILD_ROOT
%{python3_sitelib}/qubesmanager/tests/test_qube_manager.py %{python3_sitelib}/qubesmanager/tests/test_qube_manager.py
%{python3_sitelib}/qubesmanager/tests/test_create_new_vm.py %{python3_sitelib}/qubesmanager/tests/test_create_new_vm.py
%{python3_sitelib}/qubesmanager/tests/test_vm_settings.py %{python3_sitelib}/qubesmanager/tests/test_vm_settings.py
%{python3_sitelib}/qubesmanager/tests/test_clone_vm.py
%dir %{python3_sitelib}/qubesmanager-*.egg-info %dir %{python3_sitelib}/qubesmanager-*.egg-info
%{python3_sitelib}/qubesmanager-*.egg-info/* %{python3_sitelib}/qubesmanager-*.egg-info/*