test_qube_manager.py 70 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744
  1. #!/usr/bin/python3
  2. #
  3. # The Qubes OS Project, https://www.qubes-os.org/
  4. #
  5. # Copyright (C) 2016 Marta Marczykowska-Górecka
  6. # <marmarta@invisiblethingslab.com>
  7. #
  8. # This program is free software; you can redistribute it and/or modify
  9. # it under the terms of the GNU General Public License as published by
  10. # the Free Software Foundation; either version 2 of the License, or
  11. # (at your option) any later version.
  12. #
  13. # This program is distributed in the hope that it will be useful,
  14. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. # GNU General Public License for more details.
  17. #
  18. # You should have received a copy of the GNU General Public License along
  19. # with this program; if not, write to the Free Software Foundation, Inc.,
  20. # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
  21. #
  22. import asyncio
  23. import contextlib
  24. import functools
  25. import logging.handlers
  26. import unittest
  27. import unittest.mock
  28. import subprocess
  29. import datetime
  30. import time
  31. from PyQt5 import QtTest, QtCore, QtWidgets
  32. from PyQt5.QtCore import (Qt, QSize)
  33. from PyQt5.QtGui import (QIcon)
  34. from qubesadmin import Qubes, events, exc
  35. import qubesmanager.qube_manager as qube_manager
  36. from qubesmanager.tests import init_qtapp
  37. icon_size = qube_manager.icon_size
  38. def listen_for_events(func):
  39. """Wrapper for a test that needs events listener to be registered all the time.
  40. Note the test still needs to yield to the event loop to actually handle events.
  41. """
  42. @functools.wraps(func)
  43. def wrapper(self, *args, **kwargs):
  44. events_listener = \
  45. asyncio.ensure_future(self.dispatcher.listen_for_events())
  46. # let it connect (run until first yield/await)
  47. self.loop.run_until_complete(asyncio.sleep(0))
  48. try:
  49. return func(self, *args, **kwargs)
  50. finally:
  51. events_listener.cancel()
  52. self.loop.call_soon(self.loop.stop)
  53. self.loop.run_forever()
  54. return wrapper
  55. class QubeManagerTest(unittest.TestCase):
  56. def setUp(self):
  57. super(QubeManagerTest, self).setUp()
  58. self.qtapp, self.loop = init_qtapp()
  59. self.mock_qprogress = unittest.mock.patch(
  60. 'PyQt5.QtWidgets.QProgressDialog')
  61. self.mock_qprogress.start()
  62. self.addCleanup(self.mock_qprogress.stop)
  63. self.qapp = Qubes()
  64. self.dispatcher = events.EventsDispatcher(self.qapp)
  65. self.dialog = qube_manager.VmManagerWindow(
  66. self.qtapp, self.qapp, self.dispatcher)
  67. def test_000_window_loads(self):
  68. self.assertTrue(self.dialog.table is not None, "Window did not load")
  69. def test_001_correct_vms_listed(self):
  70. vms_in_table = []
  71. for row in range(self.dialog.table.model().rowCount()):
  72. vm = self._get_table_vm(row)
  73. self.assertIsNotNone(vm)
  74. vms_in_table.append(vm.name)
  75. # check that name is listed correctly
  76. name_item = self._get_table_item(row, "Name")
  77. self.assertEqual(name_item, vm.name,
  78. "Incorrect VM name for {}".format(vm.name))
  79. actual_vms = [vm.name for vm in self.qapp.domains]
  80. self.assertEqual(len(vms_in_table), len(actual_vms),
  81. "Incorrect number of VMs loaded")
  82. self.assertListEqual(sorted(vms_in_table), sorted(actual_vms),
  83. "Incorrect VMs loaded")
  84. def test_002_correct_template_listed(self):
  85. for row in range(self.dialog.table.model().rowCount()):
  86. vm = self._get_table_vm(row)
  87. # check that template is listed correctly
  88. template_item = self._get_table_item(row, "Template")
  89. if getattr(vm, "template", None):
  90. self.assertEqual(vm.template,
  91. template_item,
  92. "Incorrect template for {}".format(vm.name))
  93. else:
  94. self.assertEqual(vm.klass, template_item,
  95. "Incorrect class for {}".format(vm.name))
  96. def test_003_correct_netvm_listed(self):
  97. for row in range(self.dialog.table.model().rowCount()):
  98. vm = self._get_table_vm(row)
  99. # check that netvm is listed correctly
  100. netvm_item = self._get_table_item(row, "NetVM")
  101. netvm_value = getattr(vm, "netvm", None)
  102. if not netvm_value:
  103. netvm_value = "n/a"
  104. if netvm_value and hasattr(vm, "netvm") \
  105. and vm.property_is_default("netvm"):
  106. netvm_value = "default ({})".format(netvm_value)
  107. self.assertEqual(netvm_value,
  108. netvm_item,
  109. "Incorrect netvm for {}".format(vm.name))
  110. def test_004_correct_disk_usage_listed(self):
  111. for row in range(self.dialog.table.model().rowCount()):
  112. vm = self._get_table_vm(row)
  113. size_item = self._get_table_item(row, "Disk Usage")
  114. if vm.klass == 'AdminVM':
  115. size_value = "n/a"
  116. else:
  117. size_value = round(vm.get_disk_utilization() / (1024 * 1024), 2)
  118. size_value = str(size_value) + " MiB"
  119. self.assertEqual(size_value,
  120. size_item,
  121. "Incorrect size for {}".format(vm.name))
  122. def test_005_correct_internal_listed(self):
  123. for row in range(self.dialog.table.model().rowCount()):
  124. vm = self._get_table_vm(row)
  125. internal_item = self._get_table_item(row, "Internal")
  126. internal_value = "Yes" if vm.features.get('internal', False) else ""
  127. self.assertEqual(internal_item, internal_value,
  128. "Incorrect internal value for {}".format(vm.name))
  129. def test_006_correct_ip_listed(self):
  130. for row in range(self.dialog.table.model().rowCount()):
  131. vm = self._get_table_vm(row)
  132. ip_item = self._get_table_item(row, "IP")
  133. if hasattr(vm, 'ip'):
  134. ip_value = getattr(vm, 'ip')
  135. ip_value = "" if ip_value is None else ip_value
  136. else:
  137. ip_value = "n/a"
  138. self.assertEqual(ip_value, ip_item,
  139. "Incorrect ip value for {}".format(vm.name))
  140. def test_007_incl_in_backups_listed(self):
  141. for row in range(self.dialog.table.model().rowCount()):
  142. vm = self._get_table_vm(row)
  143. incl_backups_item = self._get_table_item(row, "Backup", Qt.CheckStateRole) == Qt.Checked
  144. incl_backups_value = getattr(vm, 'include_in_backups', False)
  145. self.assertEqual(
  146. incl_backups_value, incl_backups_item,
  147. "Incorrect include in backups value for {}".format(vm.name))
  148. def test_008_last_backup_listed(self):
  149. for row in range(self.dialog.table.model().rowCount()):
  150. vm = self._get_table_vm(row)
  151. last_backup_item = self._get_table_item(row, "Last backup")
  152. last_backup_value = getattr(vm, 'backup_timestamp', None)
  153. if last_backup_value:
  154. last_backup_value = str(
  155. datetime.datetime.fromtimestamp(last_backup_value))
  156. self.assertEqual(
  157. last_backup_value, last_backup_item,
  158. "Incorrect last backup value for {}".format(vm.name))
  159. def test_009_def_dispvm_listed(self):
  160. for row in range(self.dialog.table.model().rowCount()):
  161. vm = self._get_table_vm(row)
  162. def_dispvm_item = self._get_table_item(row, "Default DispVM")
  163. if vm.property_is_default("default_dispvm"):
  164. def_dispvm_value = "default ({})".format(
  165. vm.property_get_default("default_dispvm"))
  166. else:
  167. def_dispvm_value = getattr(vm, "default_dispvm", None)
  168. self.assertEqual(
  169. def_dispvm_value, def_dispvm_item,
  170. "Incorrect default dispvm value for {}".format(vm.name))
  171. def test_010_is_dvm_template_listed(self):
  172. for row in range(self.dialog.table.model().rowCount()):
  173. vm = self._get_table_vm(row)
  174. is_dvm_template_item = self._get_table_item(row, "Is DVM Template")
  175. is_dvm_template_value = "Yes" if \
  176. getattr(vm, "template_for_dispvms", False) else ""
  177. self.assertEqual(
  178. is_dvm_template_value, is_dvm_template_item,
  179. "Incorrect is DVM template value for {}".format(vm.name))
  180. def test_011_is_label_correct(self):
  181. for row in range(self.dialog.table.model().rowCount()):
  182. vm = self._get_table_vm(row)
  183. icon = QIcon.fromTheme(getattr(vm, 'icon', 'appvm-black'))
  184. icon = icon.pixmap(icon_size)
  185. label_pixmap = self._get_table_item(row, "Label", Qt.DecorationRole)
  186. self.assertEqual(label_pixmap.toImage(), icon.toImage())
  187. def test_012_is_state_correct(self):
  188. for row in range(self.dialog.table.model().rowCount()):
  189. vm = self._get_table_vm(row)
  190. displayed_power_state = self._get_table_item(row, "State")['power']
  191. self.assertEqual(
  192. displayed_power_state, vm.get_power_state(),
  193. "Wrong power state displayed for {}".format(vm.name))
  194. def test_013_incorrect_settings_file(self):
  195. mock_settings = unittest.mock.MagicMock(spec=QtCore.QSettings)
  196. settings_result_dict = {"view/sort_column": "Cthulhu",
  197. "view/sort_order": "Fhtagn",
  198. "view/menubar_visible": "R'lyeh"
  199. }
  200. mock_settings.side_effect = (
  201. lambda x, *args, **kwargs: settings_result_dict.get(x))
  202. with unittest.mock.patch('PyQt5.QtCore.QSettings.value',
  203. mock_settings),\
  204. unittest.mock.patch('PyQt5.QtWidgets.QMessageBox.warning')\
  205. as mock_warning:
  206. self.dialog = qube_manager.VmManagerWindow(
  207. self.qtapp, self.qapp, self.dispatcher)
  208. self.assertEqual(mock_warning.call_count, 1)
  209. def test_100_sorting(self):
  210. col = self.dialog.qubes_model.columns_indices.index("Template")
  211. self.dialog.table.sortByColumn(col, QtCore.Qt.AscendingOrder)
  212. self.__check_sorting("Template")
  213. col = self.dialog.qubes_model.columns_indices.index("Name")
  214. self.dialog.table.sortByColumn(col, QtCore.Qt.AscendingOrder)
  215. self.__check_sorting("Name")
  216. @unittest.mock.patch('qubesmanager.qube_manager.QSettings.setValue')
  217. def test_101_hide_column(self, mock_settings):
  218. model = self.dialog.qubes_model
  219. action_no = model.columns_indices.index('Is DVM Template')
  220. self.dialog.menu_view.actions()[action_no].trigger()
  221. mock_settings.assert_called_with('columns/Is DVM Template', True)
  222. self.dialog.menu_view.actions()[action_no].trigger()
  223. mock_settings.assert_called_with('columns/Is DVM Template', False)
  224. @unittest.mock.patch('qubesmanager.settings.VMSettingsWindow')
  225. def test_200_vm_open_settings(self, mock_window):
  226. selected_vm = self._select_non_admin_vm()
  227. self.assertIsNotNone(selected_vm, "No valid non-admin VM found")
  228. widget = self.dialog.toolbar.widgetForAction(
  229. self.dialog.action_settings)
  230. QtTest.QTest.mouseClick(widget,
  231. QtCore.Qt.LeftButton)
  232. mock_window.assert_called_once_with(
  233. selected_vm, qapp=self.qtapp, init_page="basic", qubesapp=self.qapp)
  234. def test_201_vm_open_settings_admin(self):
  235. self._select_admin_vm()
  236. self.assertFalse(self.dialog.action_settings.isEnabled(),
  237. "Settings not disabled for admin VM")
  238. self.assertFalse(self.dialog.action_editfwrules.isEnabled(),
  239. "Editfw not disabled for admin VM")
  240. self.assertFalse(self.dialog.action_appmenus.isEnabled(),
  241. "Appmenus not disabled for admin VM")
  242. @unittest.mock.patch('qubesmanager.settings.VMSettingsWindow')
  243. def test_202_vm_open_firewall(self, mock_window):
  244. selected_vm = self._select_non_admin_vm()
  245. self.assertIsNotNone(selected_vm, "No valid non-admin VM found")
  246. widget = self.dialog.toolbar.widgetForAction(
  247. self.dialog.action_editfwrules)
  248. QtTest.QTest.mouseClick(widget,
  249. QtCore.Qt.LeftButton)
  250. mock_window.assert_called_once_with(
  251. selected_vm, qapp=self.qtapp, init_page="firewall",
  252. qubesapp=self.qapp)
  253. @unittest.mock.patch('qubesmanager.settings.VMSettingsWindow')
  254. def test_203_vm_open_apps(self, mock_window):
  255. selected_vm = self._select_non_admin_vm()
  256. self.assertIsNotNone(selected_vm, "No valid non-admin VM found")
  257. widget = self.dialog.toolbar.widgetForAction(
  258. self.dialog.action_appmenus)
  259. QtTest.QTest.mouseClick(widget,
  260. QtCore.Qt.LeftButton)
  261. mock_window.assert_called_once_with(
  262. selected_vm, qapp=self.qtapp, init_page="applications",
  263. qubesapp=self.qapp)
  264. @unittest.mock.patch('PyQt5.QtWidgets.QMessageBox.warning')
  265. def test_204_vm_keyboard(self, mock_message):
  266. selected_vm = self._select_non_admin_vm(running=True)
  267. self.assertIsNotNone(selected_vm, "No valid non-admin VM found")
  268. if 'supported-feature.keyboard-layout' not in selected_vm.features:
  269. self.skipTest("VM {!s} does not support new layout change".format(selected_vm))
  270. widget = self.dialog.toolbar.widgetForAction(
  271. self.dialog.action_set_keyboard_layout)
  272. with unittest.mock.patch.object(selected_vm, 'run') as mock_run:
  273. QtTest.QTest.mouseClick(widget,
  274. QtCore.Qt.LeftButton)
  275. mock_run.assert_called_once_with("qubes-change-keyboard-layout")
  276. self.assertEqual(mock_message.call_count, 0,
  277. "VM does not support new layout change")
  278. @unittest.mock.patch('PyQt5.QtWidgets.QMessageBox.warning')
  279. def test_205_vm_keyboard_not_running(self, mock_message):
  280. selected_vm = self._select_non_admin_vm(running=False)
  281. self.assertIsNotNone(selected_vm, "No valid non-admin VM found")
  282. widget = self.dialog.toolbar.widgetForAction(
  283. self.dialog.action_set_keyboard_layout)
  284. with unittest.mock.patch.object(selected_vm, 'run') as mock_run:
  285. QtTest.QTest.mouseClick(widget,
  286. QtCore.Qt.LeftButton)
  287. self.assertEqual(mock_run.call_count, 0,
  288. "Keyboard change called on a halted VM")
  289. def test_206_dom0_keyboard(self):
  290. self._select_admin_vm()
  291. self.assertFalse(self.dialog.action_set_keyboard_layout.isEnabled())
  292. @unittest.mock.patch("PyQt5.QtWidgets.QMessageBox.question",
  293. return_value=QtWidgets.QMessageBox.Yes)
  294. def test_207_update_vm_not_running(self, _):
  295. selected_vm = self._select_templatevm(running=False)
  296. self.assertIsNotNone(selected_vm, "No valid template VM found")
  297. widget = self.dialog.toolbar.widgetForAction(
  298. self.dialog.action_updatevm)
  299. with unittest.mock.patch('qubesmanager.qube_manager.UpdateVMThread') \
  300. as mock_update:
  301. QtTest.QTest.mouseClick(widget,
  302. QtCore.Qt.LeftButton)
  303. mock_update.assert_called_once_with(selected_vm)
  304. mock_update().start.assert_called_once_with()
  305. def test_208_update_vm_admin(self):
  306. selected_vm = self._select_admin_vm()
  307. self.assertIsNotNone(selected_vm, "No valid admin VM found")
  308. widget = self.dialog.toolbar.widgetForAction(
  309. self.dialog.action_updatevm)
  310. with unittest.mock.patch('qubesmanager.qube_manager.UpdateVMThread') \
  311. as mock_update:
  312. QtTest.QTest.mouseClick(widget,
  313. QtCore.Qt.LeftButton)
  314. mock_update.assert_called_once_with(selected_vm)
  315. mock_update().start.assert_called_once_with()
  316. @unittest.mock.patch("PyQt5.QtWidgets.QInputDialog.getText",
  317. return_value=("command to run", True))
  318. def test_209_run_command_in_vm(self, _):
  319. selected_vm = self._select_non_admin_vm()
  320. self.assertIsNotNone(selected_vm, "No valid non-admin VM found")
  321. with unittest.mock.patch('qubesmanager.qube_manager.RunCommandThread') \
  322. as mock_thread:
  323. self.dialog.action_run_command_in_vm.trigger()
  324. mock_thread.assert_called_once_with(selected_vm, "command to run")
  325. mock_thread().finished.connect.assert_called_once_with(
  326. self.dialog.clear_threads)
  327. mock_thread().start.assert_called_once_with()
  328. def test_210_run_command_in_adminvm(self):
  329. self._select_admin_vm()
  330. self.assertFalse(self.dialog.action_run_command_in_vm.isEnabled(),
  331. "Should not be able to run commands for dom0")
  332. @unittest.mock.patch("PyQt5.QtWidgets.QMessageBox.warning")
  333. def test_211_pausevm(self, mock_warn):
  334. selected_vm = self._select_non_admin_vm(running=True)
  335. self.assertTrue(self.dialog.action_pausevm.isEnabled(),
  336. "Pause not enabled for a running VM")
  337. with unittest.mock.patch.object(selected_vm, 'pause') as mock_pause:
  338. self.dialog.action_pausevm.trigger()
  339. mock_pause.assert_called_once_with()
  340. mock_pause.side_effect = exc.QubesException('Error')
  341. self.dialog.action_pausevm.trigger()
  342. self.assertEqual(mock_warn.call_count, 1)
  343. def test_212_resumevm(self):
  344. selected_vm = self._select_non_admin_vm(running=False)
  345. with unittest.mock.patch.object(selected_vm, 'get_power_state')\
  346. as mock_state, \
  347. unittest.mock.patch.object(selected_vm, 'unpause')\
  348. as mock_unpause:
  349. mock_state.return_value = 'Paused'
  350. self.dialog.action_resumevm.trigger()
  351. mock_unpause.assert_called_once_with()
  352. with unittest.mock.patch('qubesmanager.qube_manager.StartVMThread') \
  353. as mock_thread:
  354. self.dialog.action_resumevm.trigger()
  355. mock_thread.assert_called_once_with(selected_vm)
  356. mock_thread().finished.connect.assert_called_once_with(
  357. self.dialog.clear_threads)
  358. mock_thread().start.assert_called_once_with()
  359. def test_213_resume_running_vm(self):
  360. self._select_non_admin_vm(running=True)
  361. self.assertFalse(self.dialog.action_resumevm.isEnabled())
  362. @unittest.mock.patch("PyQt5.QtWidgets.QMessageBox.question",
  363. return_value=QtWidgets.QMessageBox.Yes)
  364. @unittest.mock.patch('PyQt5.QtCore.QTimer.singleShot')
  365. @unittest.mock.patch('qubesmanager.qube_manager.VmShutdownMonitor')
  366. def test_214_shutdownvm(self, mock_monitor, mock_timer, _):
  367. selected_vm = self._select_non_admin_vm(running=True)
  368. with unittest.mock.patch.object(selected_vm, 'shutdown')\
  369. as mock_shutdown:
  370. self.dialog.action_shutdownvm.trigger()
  371. mock_shutdown.assert_called_once_with(force=False)
  372. mock_monitor.assert_called_once_with(
  373. selected_vm, unittest.mock.ANY, unittest.mock.ANY,
  374. unittest.mock.ANY)
  375. mock_timer.assert_called_once_with(unittest.mock.ANY,
  376. unittest.mock.ANY)
  377. def test_215_shutdown_halted_vm(self):
  378. self._select_non_admin_vm(running=False)
  379. self.assertFalse(self.dialog.action_shutdownvm.isEnabled())
  380. @unittest.mock.patch('qubesmanager.create_new_vm.NewVmDlg')
  381. def test_216_create_vm(self, mock_new_vm):
  382. action = self.dialog.action_createvm
  383. self.assertTrue(action.isEnabled())
  384. action.trigger()
  385. self.assertEqual(mock_new_vm.call_count, 1,
  386. "Create New VM window did not appear")
  387. def test_217_remove_admin_vm(self):
  388. self._select_admin_vm()
  389. self.assertFalse(self.dialog.action_removevm.isEnabled())
  390. @unittest.mock.patch("qubesmanager.qube_manager.QMessageBox")
  391. @unittest.mock.patch('qubesadmin.utils.vm_dependencies')
  392. def test_218_remove_vm_dependencies(self, mock_dependencies, mock_msgbox):
  393. mock_vm = unittest.mock.Mock(spec=['name'],
  394. **{'name.return_value': 'test-vm'})
  395. mock_dependencies.return_value = [(mock_vm, "test_prop")]
  396. action = self.dialog.action_removevm
  397. self._select_non_admin_vm()
  398. action.trigger()
  399. mock_msgbox().show.assert_called_with()
  400. @unittest.mock.patch('PyQt5.QtWidgets.QMessageBox.warning')
  401. @unittest.mock.patch("PyQt5.QtWidgets.QInputDialog.getText")
  402. @unittest.mock.patch('qubesadmin.utils.vm_dependencies')
  403. def test_219_remove_vm_no_depencies(
  404. self, mock_dependencies, mock_input, mock_warning):
  405. action = self.dialog.action_removevm
  406. selected_vm = self._select_non_admin_vm(running=False)
  407. # test with no dependencies
  408. mock_dependencies.return_value = None
  409. with unittest.mock.patch('qubesmanager.common_threads.RemoveVMThread')\
  410. as mock_thread:
  411. mock_input.return_value = (selected_vm, False)
  412. action.trigger()
  413. self.assertEqual(mock_thread.call_count, 0,
  414. "VM removed despite user clicking 'cancel")
  415. mock_input.return_value = ("wrong_name", True)
  416. action.trigger()
  417. self.assertEqual(mock_warning.call_count, 1)
  418. self.assertEqual(mock_thread.call_count, 0,
  419. "VM removed despite user not confirming the name")
  420. mock_input.return_value = (selected_vm.name, True)
  421. action.trigger()
  422. mock_thread.assert_called_once_with(selected_vm)
  423. mock_thread().finished.connect.assert_called_once_with(
  424. self.dialog.clear_threads)
  425. mock_thread().start.assert_called_once_with()
  426. def test_220_restartvm_halted_vm(self):
  427. self._select_non_admin_vm(running=False)
  428. self.assertFalse(self.dialog.action_restartvm.isEnabled())
  429. @unittest.mock.patch('PyQt5.QtCore.QTimer.singleShot')
  430. @unittest.mock.patch('qubesmanager.qube_manager.VmShutdownMonitor')
  431. @unittest.mock.patch("PyQt5.QtWidgets.QMessageBox.question",
  432. return_value=QtWidgets.QMessageBox.Yes)
  433. def test_221_restartvm_running_vm(self, _msgbox, mock_monitor, _qtimer):
  434. selected_vm = self._select_non_admin_vm(running=True)
  435. action = self.dialog.action_restartvm
  436. # currently the VM is running
  437. with unittest.mock.patch.object(selected_vm, 'shutdown')\
  438. as mock_shutdown:
  439. action.trigger()
  440. mock_shutdown.assert_called_once_with(force=True)
  441. mock_monitor.assert_called_once_with(
  442. selected_vm, 1000, True, unittest.mock.ANY)
  443. @unittest.mock.patch('qubesmanager.qube_manager.StartVMThread')
  444. @unittest.mock.patch("PyQt5.QtWidgets.QMessageBox.question",
  445. return_value=QtWidgets.QMessageBox.Yes)
  446. def test_222_restartvm_shutdown_meantime(self, _, mock_thread):
  447. selected_vm = self._select_non_admin_vm(running=True)
  448. action = self.dialog.action_restartvm
  449. # it was shutdown in the meantime
  450. with unittest.mock.patch.object(
  451. selected_vm, 'is_running', **{'return_value': False}):
  452. action.trigger()
  453. mock_thread.assert_called_once_with(selected_vm)
  454. mock_thread().finished.connect.assert_called_once_with(
  455. self.dialog.clear_threads)
  456. mock_thread().start.assert_called_once_with()
  457. @unittest.mock.patch('qubesmanager.qube_manager.UpdateVMThread')
  458. def test_223_updatevm_running(self, mock_thread):
  459. selected_vm = self._select_non_admin_vm(running=True)
  460. self.dialog.action_updatevm.trigger()
  461. mock_thread.assert_called_once_with(selected_vm)
  462. mock_thread().finished.connect.assert_called_once_with(
  463. self.dialog.clear_threads)
  464. mock_thread().start.assert_called_once_with()
  465. @unittest.mock.patch("PyQt5.QtWidgets.QMessageBox.question",
  466. return_value=QtWidgets.QMessageBox.Yes)
  467. @unittest.mock.patch('qubesmanager.qube_manager.UpdateVMThread')
  468. def test_224_updatevm_halted(self, mock_thread, _):
  469. selected_vm = self._select_non_admin_vm(running=False)
  470. self.dialog.action_updatevm.trigger()
  471. mock_thread.assert_called_once_with(selected_vm)
  472. mock_thread().finished.connect.assert_called_once_with(
  473. self.dialog.clear_threads)
  474. mock_thread().start.assert_called_once_with()
  475. @unittest.mock.patch("PyQt5.QtWidgets.QMessageBox.question",
  476. return_value=QtWidgets.QMessageBox.Yes)
  477. def test_224_killvm(self, _):
  478. selected_vm = self._select_non_admin_vm(running=True)
  479. action = self.dialog.action_killvm
  480. with unittest.mock.patch.object(selected_vm, 'kill') as mock_kill:
  481. action.trigger()
  482. mock_kill.assert_called_once_with()
  483. @unittest.mock.patch("PyQt5.QtWidgets.QMessageBox.question",
  484. return_value=QtWidgets.QMessageBox.Cancel)
  485. def test_225_killvm_cancel(self, _):
  486. selected_vm = self._select_non_admin_vm(running=True)
  487. action = self.dialog.action_killvm
  488. with unittest.mock.patch.object(selected_vm, 'kill') as mock_kill:
  489. action.trigger()
  490. self.assertEqual(mock_kill.call_count, 0,
  491. "Ignored Cancel on kill VM")
  492. @unittest.mock.patch('qubesmanager.global_settings.GlobalSettingsWindow')
  493. def test_226_global_settings(self, mock_settings):
  494. self._select_non_admin_vm()
  495. self.dialog.action_global_settings.trigger()
  496. self.assertEqual(mock_settings.call_count, 1,
  497. "Global Settings not opened")
  498. self._select_admin_vm()
  499. self.dialog.action_global_settings.trigger()
  500. self.assertEqual(mock_settings.call_count, 2,
  501. "Global Settings not opened for the second time")
  502. @unittest.mock.patch('qubesmanager.backup.BackupVMsWindow')
  503. def test_227_backup(self, mock_backup):
  504. self.dialog.action_backup.trigger()
  505. self.assertTrue(self.dialog.action_backup.isEnabled())
  506. self.assertEqual(mock_backup.call_count, 1,
  507. "Backup window does not appear")
  508. @unittest.mock.patch('qubesmanager.restore.RestoreVMsWindow')
  509. def test_228_restore(self, mock_restore):
  510. self.dialog.action_restore.trigger()
  511. self.assertTrue(self.dialog.action_restore.isEnabled())
  512. self.assertEqual(mock_restore.call_count, 1,
  513. "Backup window does not appear")
  514. @unittest.mock.patch('qubesmanager.qube_manager.AboutDialog')
  515. def test_229_about_qubes(self, mock_about):
  516. self.assertTrue(self.dialog.action_about_qubes.isEnabled())
  517. self.dialog.action_about_qubes.trigger()
  518. self.assertEqual(
  519. mock_about.call_count, 1, "About window does not appear")
  520. def test_230_exit_action(self):
  521. self.assertTrue(self.dialog.action_exit.isEnabled())
  522. with unittest.mock.patch.object(self.dialog, 'close') as mock_close:
  523. self.dialog.action_exit.trigger()
  524. mock_close.assert_called_once_with()
  525. @unittest.mock.patch('subprocess.check_call')
  526. def test_231_template_manager(self, mock_subprocess):
  527. self.assertTrue(self.dialog.action_manage_templates.isEnabled())
  528. self.dialog.action_manage_templates.trigger()
  529. mock_subprocess.assert_called_once_with('qubes-template-manager')
  530. @unittest.mock.patch('qubesmanager.clone_vm.CloneVMDlg')
  531. def test_232_clonevm(self, mock_clone):
  532. action = self.dialog.action_clonevm
  533. self._select_admin_vm()
  534. self.assertFalse(action.isEnabled())
  535. selected_vm = self._select_non_admin_vm()
  536. self.assertTrue(action.isEnabled())
  537. action.trigger()
  538. mock_clone.assert_called_once_with(self.qtapp, self.qapp,
  539. src_vm=selected_vm)
  540. def test_233_search_action(self):
  541. self.qtapp.setActiveWindow(self.dialog.searchbox)
  542. self.dialog.action_search.trigger()
  543. self.assertTrue(self.dialog.searchbox.hasFocus())
  544. # input text
  545. self.dialog.searchbox.setText("sys")
  546. # click outside the widget
  547. QtTest.QTest.mouseClick(self.dialog.table, QtCore.Qt.LeftButton)
  548. # click the widget, check if it is correctly activated and the whole
  549. # text was selected
  550. QtTest.QTest.mouseClick(self.dialog.searchbox, QtCore.Qt.LeftButton)
  551. self.assertTrue(self.dialog.searchbox.hasFocus())
  552. self.assertEqual(self.dialog.searchbox.selectedText(), "sys")
  553. def test_234_searchbox(self):
  554. # look for sys
  555. self.dialog.searchbox.setText("sys")
  556. expected_number = \
  557. len([vm for vm in self.qapp.domains if "sys" in vm.name])
  558. actual_number = self._count_visible_table_rows()
  559. self.assertEqual(expected_number, actual_number,
  560. "Incorrect number of vms shown for 'sys'")
  561. # clear search
  562. self.dialog.searchbox.setText("")
  563. expected_number = len([vm for vm in self.qapp.domains])
  564. actual_number = self._count_visible_table_rows()
  565. self.assertEqual(expected_number, actual_number,
  566. "Incorrect number of vms shown for cleared search box")
  567. def test_235_hide_show_toolbars(self):
  568. with unittest.mock.patch('PyQt5.QtCore.QSettings.setValue')\
  569. as mock_setvalue:
  570. self.dialog.action_menubar.trigger()
  571. mock_setvalue.assert_called_with('view/menubar_visible', False)
  572. self.dialog.action_toolbar.trigger()
  573. mock_setvalue.assert_called_with('view/toolbar_visible', False)
  574. self.assertFalse(self.dialog.menubar.isVisible(),
  575. "Menubar not hidden correctly")
  576. self.assertFalse(self.dialog.toolbar.isVisible(),
  577. "Toolbar not hidden correctly")
  578. def test_236_clear_searchbox(self):
  579. self.dialog.searchbox.setText("text")
  580. self.assertEqual(self.dialog.searchbox.text(), "text")
  581. QtTest.QTest.keyPress(self.dialog, QtCore.Qt.Key_Escape)
  582. self.assertEqual(self.dialog.searchbox.text(), "",
  583. "Escape failed to clear searchbox")
  584. expected_number = len([vm for vm in self.qapp.domains])
  585. actual_number = self._count_visible_table_rows()
  586. self.assertEqual(expected_number, actual_number,
  587. "Incorrect number of vms shown for cleared search box")
  588. @unittest.mock.patch('PyQt5.QtWidgets.QMessageBox.question')
  589. @listen_for_events
  590. def test_240_network_menu_single(self, mock_question):
  591. mock_question.return_value = QtWidgets.QMessageBox.Yes
  592. target_vm_name = 'work'
  593. self._run_command_and_process_events(
  594. ['qvm-prefs', '-D', target_vm_name, 'netvm'], timeout=20)
  595. self._select_vms(['work'])
  596. selected_vm = self.qapp.domains[target_vm_name]
  597. # reset to default even in case of failure
  598. self.addCleanup(functools.partial(delattr, selected_vm, 'netvm'))
  599. # this is the method to get '==' operator working on icons...
  600. on_icon = QIcon(":/on.png").pixmap(64).toImage()
  601. off_icon = QIcon().pixmap(64).toImage()
  602. for action in self.dialog.network_menu.actions():
  603. if action.text().startswith('default '):
  604. self.assertEqual(action.icon().pixmap(64).toImage(), on_icon)
  605. break
  606. else:
  607. self.fail('default netvm not found')
  608. # change to specific value
  609. for action in self.dialog.network_menu.actions():
  610. if action.text() == 'sys-net':
  611. self.assertEqual(action.icon().pixmap(64).toImage(), off_icon)
  612. action.trigger()
  613. break
  614. else:
  615. self.fail('sys-net netvm not found')
  616. # process events
  617. self.loop.run_until_complete(asyncio.sleep(0))
  618. mock_question.assert_called()
  619. self.assertEqual(str(selected_vm.netvm), 'sys-net')
  620. mock_question.reset_mock()
  621. # change to none
  622. for action in self.dialog.network_menu.actions():
  623. if action.text() == 'None':
  624. self.assertEqual(action.icon().pixmap(64).toImage(), off_icon)
  625. action.trigger()
  626. break
  627. else:
  628. self.fail('"none" netvm not found')
  629. # process events
  630. self.loop.run_until_complete(asyncio.sleep(0))
  631. mock_question.assert_called()
  632. self.assertIsNone(selected_vm.netvm)
  633. mock_question.reset_mock()
  634. # then go back to the default
  635. for action in self.dialog.network_menu.actions():
  636. if action.text().startswith('default '):
  637. self.assertEqual(action.icon().pixmap(64).toImage(), off_icon)
  638. action.trigger()
  639. break
  640. # process events
  641. self.loop.run_until_complete(asyncio.sleep(0))
  642. mock_question.assert_called()
  643. self.assertTrue(selected_vm.property_is_default('netvm'))
  644. @unittest.mock.patch('PyQt5.QtWidgets.QMessageBox.question')
  645. @listen_for_events
  646. def test_241_network_menu_multiple(self, mock_question):
  647. mock_question.return_value = QtWidgets.QMessageBox.Yes
  648. target_vm_names = ['work', 'personal', 'vault']
  649. work = self.qapp.domains['work']
  650. personal = self.qapp.domains['personal']
  651. vault = self.qapp.domains['vault']
  652. # reset to default even in case of failure
  653. self.addCleanup(functools.partial(delattr, work, 'netvm'))
  654. self.addCleanup(functools.partial(delattr, personal, 'netvm'))
  655. self.addCleanup(functools.partial(setattr, vault, 'netvm', None))
  656. self._run_command_and_process_events(
  657. ['qvm-prefs', '-D', 'work', 'netvm'], timeout=5)
  658. self._run_command_and_process_events(
  659. ['qvm-prefs', '-D', 'personal', 'netvm'], timeout=5)
  660. self._run_command_and_process_events(
  661. ['qvm-prefs', 'vault', ''], timeout=5)
  662. self._select_vms(target_vm_names)
  663. # this is the method to get '==' operator working on icons...
  664. on_icon = QIcon(":/on.png").pixmap(64).toImage()
  665. transient_icon = QIcon(":/transient.png").pixmap(64).toImage()
  666. off_icon = QIcon().pixmap(64).toImage()
  667. for action in self.dialog.network_menu.actions():
  668. if action.text().startswith('default '):
  669. # work, personal
  670. self.assertEqual(action.icon().pixmap(64).toImage(), transient_icon)
  671. elif action.text() == 'None':
  672. # vault
  673. self.assertEqual(action.icon().pixmap(64).toImage(), transient_icon)
  674. else:
  675. self.assertEqual(action.icon().pixmap(64).toImage(), off_icon)
  676. # change to specific value
  677. for action in self.dialog.network_menu.actions():
  678. if action.text() == 'sys-net':
  679. self.assertEqual(action.icon().pixmap(64).toImage(), off_icon)
  680. action.trigger()
  681. break
  682. else:
  683. self.fail('sys-net netvm not found')
  684. # process events
  685. self.loop.run_until_complete(asyncio.sleep(0))
  686. mock_question.assert_called()
  687. self.assertEqual(str(work.netvm), 'sys-net')
  688. self.assertEqual(str(personal.netvm), 'sys-net')
  689. self.assertEqual(str(vault.netvm), 'sys-net')
  690. mock_question.reset_mock()
  691. @unittest.mock.patch('PyQt5.QtWidgets.QMessageBox.question')
  692. @listen_for_events
  693. def test_250_template_menu_single(self, mock_question):
  694. mock_question.return_value = QtWidgets.QMessageBox.Yes
  695. target_vm_name = 'work'
  696. selected_vm = self.qapp.domains[target_vm_name]
  697. if selected_vm.is_running():
  698. self.skipTest(
  699. 'VM {!s} is running, please stop it first'.format(selected_vm))
  700. current_template = selected_vm.template
  701. new_template = self._select_templatevm(
  702. different_than=[str(current_template)])
  703. self._select_vms(['work'])
  704. # reset to previous value even in case of failure
  705. self.addCleanup(functools.partial(
  706. setattr, selected_vm, 'template', str(current_template)))
  707. # this is the method to get '==' operator working on icons...
  708. on_icon = QIcon(":/on.png").pixmap(64).toImage()
  709. off_icon = QIcon().pixmap(64).toImage()
  710. found = False
  711. for action in self.dialog.template_menu.actions():
  712. if action.text() == str(current_template):
  713. self.assertEqual(action.icon().pixmap(64).toImage(), on_icon)
  714. found = True
  715. else:
  716. self.assertEqual(action.icon().pixmap(64).toImage(), off_icon)
  717. if not found:
  718. self.fail(
  719. 'current template value ({!s}) not found in the menu'.format(
  720. current_template))
  721. # change to specific value
  722. for action in self.dialog.template_menu.actions():
  723. if action.text() == str(new_template):
  724. self.assertEqual(action.icon().pixmap(64).toImage(), off_icon)
  725. action.trigger()
  726. break
  727. else:
  728. self.fail('template {!s} not found in the menu'.format(new_template))
  729. # process events
  730. self.loop.run_until_complete(asyncio.sleep(0))
  731. mock_question.assert_called()
  732. # compare str(), to have better error message on mismatch
  733. self.assertEqual(str(selected_vm.template), str(new_template))
  734. mock_question.reset_mock()
  735. @unittest.mock.patch('PyQt5.QtWidgets.QMessageBox.question')
  736. @listen_for_events
  737. def test_251_template_menu_multiple(self, mock_question):
  738. mock_question.return_value = QtWidgets.QMessageBox.Yes
  739. target_vm_names = ['work', 'personal', 'untrusted']
  740. work = self.qapp.domains['work']
  741. personal = self.qapp.domains['personal']
  742. untrusted = self.qapp.domains['untrusted']
  743. if any(vm.is_running() for vm in [work, personal, untrusted]):
  744. self.skipTest('Any of work, personal, untrusted VM is running')
  745. old_template = work.template
  746. new_template = self._select_templatevm(
  747. different_than=[str(work.template),
  748. str(personal.template),
  749. str(untrusted.template)])
  750. # reset to previous value even in case of failure
  751. self.addCleanup(functools.partial(
  752. setattr, work, 'template', str(work.template)))
  753. self.addCleanup(functools.partial(
  754. setattr, personal, 'template', str(personal.template)))
  755. self.addCleanup(functools.partial(
  756. setattr, untrusted, 'template', str(untrusted.template)))
  757. # set all to the same value
  758. self._run_command_and_process_events(
  759. ['qvm-prefs', 'personal', 'template', str(work.template)], timeout=5)
  760. self._run_command_and_process_events(
  761. ['qvm-prefs', 'untrusted', 'template', str(work.template)], timeout=5)
  762. self._select_vms(target_vm_names)
  763. # this is the method to get '==' operator working on icons...
  764. on_icon = QIcon(":/on.png").pixmap(64).toImage()
  765. transient_icon = QIcon(":/transient.png").pixmap(64).toImage()
  766. off_icon = QIcon().pixmap(64).toImage()
  767. for action in self.dialog.template_menu.actions():
  768. if action.text() == str(old_template):
  769. self.assertIn(
  770. action.icon().pixmap(64).toImage(),
  771. (on_icon, transient_icon))
  772. else:
  773. self.assertEqual(action.icon().pixmap(64).toImage(), off_icon)
  774. # make one different
  775. self._run_command_and_process_events(
  776. ['qvm-prefs', 'work', 'template', str(new_template)], timeout=5)
  777. for action in self.dialog.template_menu.actions():
  778. if action.text() == str(old_template):
  779. self.assertEqual(action.icon().pixmap(64).toImage(), transient_icon)
  780. elif action.text() == str(new_template):
  781. self.assertEqual(action.icon().pixmap(64).toImage(), transient_icon)
  782. else:
  783. self.assertEqual(action.icon().pixmap(64).toImage(), off_icon)
  784. # change all to the same value
  785. for action in self.dialog.template_menu.actions():
  786. if action.text() == str(new_template):
  787. action.trigger()
  788. break
  789. else:
  790. self.fail('{!s} template not found'.format(new_template))
  791. # process events
  792. self.loop.run_until_complete(asyncio.sleep(0))
  793. mock_question.assert_called()
  794. self.assertEqual(str(work.template), str(new_template))
  795. self.assertEqual(str(personal.template), str(new_template))
  796. self.assertEqual(str(untrusted.template), str(new_template))
  797. @unittest.mock.patch('PyQt5.QtWidgets.QMessageBox.information')
  798. @unittest.mock.patch('PyQt5.QtWidgets.QMessageBox.warning')
  799. def test_300_clear_threads(self, mock_warning, mock_info):
  800. mock_thread_finished_ok = unittest.mock.Mock(
  801. spec=['isFinished', 'msg', 'msg_is_success'],
  802. msg=None, msg_is_success=False,
  803. **{'isFinished.return_value': True})
  804. mock_thread_not_finished = unittest.mock.Mock(
  805. spec=['isFinished', 'msg', 'msg_is_success'],
  806. msg=None, msg_is_success=False,
  807. **{'isFinished.return_value': False})
  808. mock_thread_finished_error = unittest.mock.Mock(
  809. spec=['isFinished', 'msg', 'msg_is_success'],
  810. msg=("Error", "Error"), msg_is_success=False,
  811. **{'isFinished.return_value': True})
  812. mock_thread_fin_error_success = unittest.mock.Mock(
  813. spec=['isFinished', 'msg', 'msg_is_success'],
  814. msg=("Done", "Done"), msg_is_success=True,
  815. **{'isFinished.return_value': True})
  816. # single finished thread
  817. self.dialog.threads_list = [mock_thread_not_finished,
  818. mock_thread_finished_ok]
  819. self.dialog.clear_threads()
  820. self.assertEqual(mock_warning.call_count, 0)
  821. self.assertEqual(mock_info.call_count, 0)
  822. self.assertEqual(len(self.dialog.threads_list), 1)
  823. # an error thread and some in-progress ones
  824. self.dialog.threads_list = [mock_thread_not_finished,
  825. mock_thread_not_finished,
  826. mock_thread_finished_error]
  827. self.dialog.clear_threads()
  828. self.assertEqual(mock_warning.call_count, 1)
  829. self.assertEqual(mock_info.call_count, 0)
  830. self.assertEqual(len(self.dialog.threads_list), 2)
  831. # an error-success thread and some in-progress ones
  832. self.dialog.threads_list = [mock_thread_not_finished,
  833. mock_thread_not_finished,
  834. mock_thread_fin_error_success,
  835. mock_thread_finished_error]
  836. self.dialog.clear_threads()
  837. self.assertEqual(mock_warning.call_count, 1)
  838. self.assertEqual(mock_info.call_count, 1)
  839. self.assertEqual(len(self.dialog.threads_list), 3)
  840. def test_400_event_domain_added(self):
  841. number_of_vms = self.dialog.table.model().rowCount()
  842. self.addCleanup(subprocess.call, ["qvm-remove", "-f", "test-vm"])
  843. self._run_command_and_process_events(
  844. ["qvm-create", "--label", "red", "test-vm"])
  845. # a single row was added to the table
  846. self.assertEqual(self.dialog.table.model().rowCount(), number_of_vms+1)
  847. # table contains the correct vms
  848. vms_in_table = self._create_set_of_current_vms()
  849. vms_in_system = set([vm.name for vm in self.qapp.domains])
  850. self.assertEqual(vms_in_table, vms_in_system, "Table not updated "
  851. "correctly after add")
  852. # check if sorting works
  853. self.__check_sorting("Name")
  854. # try opening settings for the added vm
  855. for row in range(self.dialog.table.model().rowCount()):
  856. name = self._get_table_item(row, "Name")
  857. if name == "test-vm":
  858. index = self.dialog.table.model().index(row, 0)
  859. self.dialog.table.setCurrentIndex(index)
  860. break
  861. with unittest.mock.patch('qubesmanager.settings.VMSettingsWindow')\
  862. as mock_settings:
  863. self.dialog.action_settings.trigger()
  864. mock_settings.assert_called_once_with(
  865. self.qapp.domains["test-vm"], qapp = self.qtapp,
  866. init_page = "basic", qubesapp = self.qapp)
  867. def test_401_event_domain_removed(self):
  868. initial_vms = self._create_set_of_current_vms()
  869. self._run_command_and_process_events(
  870. ["qvm-create", "--label", "red", "test-vm"])
  871. current_vms = self._create_set_of_current_vms()
  872. self.assertEqual(len(initial_vms) + 1, len(current_vms))
  873. self._run_command_and_process_events(
  874. ["qvm-remove", "--force", "test-vm"])
  875. current_vms = self._create_set_of_current_vms()
  876. self.assertEqual(initial_vms, current_vms)
  877. # check if sorting works
  878. self.__check_sorting("Name")
  879. def test_403_event_dispvm_added(self):
  880. initial_vms = self._create_set_of_current_vms()
  881. dispvm_template = None
  882. for vm in self.qapp.domains:
  883. if getattr(vm, "template_for_dispvms", False):
  884. dispvm_template = vm.name
  885. break
  886. self.assertIsNotNone(dispvm_template,
  887. "Cannot find a template for dispVMs")
  888. # this requires very long timeout, because it takes time for the
  889. # dispvm to vanish
  890. self._run_command_and_process_events(
  891. ["qvm-run", "--dispvm", dispvm_template, "true"], timeout=60,
  892. additional_timeout=60)
  893. final_vms = self._create_set_of_current_vms()
  894. self.assertEqual(initial_vms, final_vms,
  895. "Failed handling of a created-and-removed dispvm")
  896. def test_404_crashing_dispvm(self):
  897. initial_vms = self._create_set_of_current_vms()
  898. dispvm_template = None
  899. for vm in self.qapp.domains:
  900. if getattr(vm, "template_for_dispvms", False):
  901. dispvm_template = vm.name
  902. break
  903. self.assertIsNotNone(dispvm_template,
  904. "Cannot find a template for dispVMs")
  905. current_memory = getattr(self.qapp.domains[dispvm_template], "memory")
  906. self.addCleanup(
  907. subprocess.call,
  908. ["qvm-prefs", dispvm_template, "memory", str(current_memory)])
  909. subprocess.check_call(
  910. ["qvm-prefs", dispvm_template, "memory", "600000"])
  911. self._run_command_and_process_events(
  912. ["qvm-run", "--dispvm", dispvm_template, "true"], timeout=30,
  913. additional_timeout=15)
  914. final_vms = self._create_set_of_current_vms()
  915. self.assertEqual(initial_vms, final_vms,
  916. "Failed handling of dispvm that crashed on start")
  917. def test_405_prop_change_label(self):
  918. target_vm_name = "work"
  919. vm_row = self._find_vm_row(target_vm_name)
  920. current_label = self._get_table_item(vm_row, "Label", Qt.DecorationRole)
  921. self.addCleanup(
  922. subprocess.call, ["qvm-prefs", target_vm_name, "label", "blue"])
  923. self._run_command_and_process_events(
  924. ["qvm-prefs", target_vm_name, "label", "red"], timeout=20)
  925. new_label = self._get_table_item(vm_row, "Label", Qt.DecorationRole)
  926. self.assertNotEqual(current_label.toImage(), new_label.toImage(),
  927. "Label icon did not change")
  928. icon = QIcon.fromTheme(self.qapp.domains[target_vm_name].label.icon)
  929. icon = icon.pixmap(icon_size)
  930. self.assertEqual(new_label.toImage(), icon.toImage(), "Incorrect label")
  931. def test_406_prop_change_template(self):
  932. target_vm_name = "work"
  933. vm_row = self._find_vm_row(target_vm_name)
  934. old_template = self._get_table_item(vm_row, "Template")
  935. new_template = None
  936. for vm in self.qapp.domains:
  937. if vm.klass == 'TemplateVM' and vm.name != old_template:
  938. new_template = vm.name
  939. break
  940. self.addCleanup(
  941. subprocess.call,
  942. ["qvm-prefs", target_vm_name, "template", old_template])
  943. self._run_command_and_process_events(
  944. ["qvm-prefs", target_vm_name, "template", new_template])
  945. self.assertNotEqual(old_template,
  946. self._get_table_item(vm_row, "Template"),
  947. "Template did not change")
  948. self.assertEqual(
  949. self._get_table_item(vm_row, "Template"),
  950. self.qapp.domains[target_vm_name].template.name,
  951. "Incorrect template")
  952. def test_407_prop_change_netvm(self):
  953. target_vm_name = "work"
  954. vm_row = self._find_vm_row(target_vm_name)
  955. old_netvm = self._get_table_item(vm_row, "NetVM")
  956. new_netvm = None
  957. for vm in self.qapp.domains:
  958. if getattr(vm, "provides_network", False) and vm.name != old_netvm:
  959. new_netvm = vm.name
  960. break
  961. self.addCleanup(
  962. subprocess.call, ["qvm-prefs", target_vm_name, "netvm", old_netvm])
  963. self._run_command_and_process_events(
  964. ["qvm-prefs", target_vm_name, "netvm", new_netvm])
  965. self.assertNotEqual(old_netvm,
  966. self._get_table_item(vm_row, "NetVM"),
  967. "NetVM did not change")
  968. self.assertEqual(
  969. self._get_table_item(vm_row, "NetVM"),
  970. self.qapp.domains[target_vm_name].netvm.name,
  971. "Incorrect NetVM")
  972. @unittest.expectedFailure
  973. def test_408_prop_change_internal(self):
  974. target_vm_name = "work"
  975. vm_row = self._find_vm_row(target_vm_name)
  976. self.addCleanup(subprocess.call,
  977. ["qvm-features", "--unset", "work", "interal"])
  978. self._run_command_and_process_events(
  979. ["qvm-features", "work", "interal", "1"])
  980. self.assertEqual(
  981. self._get_table_item(vm_row, "Internal"),
  982. "Yes",
  983. "Incorrect value for internal VM")
  984. self._run_command_and_process_events(
  985. ["qvm-features", "--unset", "work", "interal"])
  986. self.assertEqual(
  987. self._get_table_item(vm_row, "Internal"),
  988. "",
  989. "Incorrect value for non-internal VM")
  990. def test_409_prop_change_ip(self):
  991. target_vm_name = "work"
  992. vm_row = self._find_vm_row(target_vm_name)
  993. old_ip = self._get_table_item(vm_row, "IP")
  994. new_ip = old_ip.replace(".0.", ".5.")
  995. self.addCleanup(
  996. subprocess.call, ["qvm-prefs", target_vm_name, "ip", old_ip])
  997. self._run_command_and_process_events(
  998. ["qvm-prefs", target_vm_name, "ip", new_ip])
  999. self.assertNotEqual(old_ip,
  1000. self._get_table_item(vm_row, "IP"),
  1001. "IP did not change")
  1002. self.assertEqual(
  1003. self._get_table_item(vm_row, "IP"),
  1004. self.qapp.domains[target_vm_name].ip,
  1005. "Incorrect IP")
  1006. def test_410_prop_change_in_backups(self):
  1007. target_vm_name = "work"
  1008. vm_row = self._find_vm_row(target_vm_name)
  1009. old_value = self.qapp.domains[target_vm_name].include_in_backups
  1010. new_value = not old_value
  1011. self.addCleanup(
  1012. subprocess.call,
  1013. ["qvm-prefs", target_vm_name, "include_in_backups", str(old_value)])
  1014. self._run_command_and_process_events(
  1015. ["qvm-prefs", target_vm_name, "include_in_backups", str(new_value)])
  1016. self.assertEqual(
  1017. self._get_table_item(vm_row, "Internal"),
  1018. "Yes" if new_value else "",
  1019. "Incorrect value for include_in_backups")
  1020. def test_411_prop_change_last_backup(self):
  1021. target_vm_name = "work"
  1022. target_timestamp = "2015-01-01 17:00:00"
  1023. vm_row = self._find_vm_row(target_vm_name)
  1024. old_value = self._get_table_item(vm_row, "Last backup")
  1025. new_value = datetime.datetime.strptime(
  1026. target_timestamp, "%Y-%m-%d %H:%M:%S")
  1027. self.addCleanup(
  1028. subprocess.call,
  1029. ["qvm-prefs", '-D', target_vm_name, "backup_timestamp"])
  1030. self._run_command_and_process_events(
  1031. ["qvm-prefs", target_vm_name, "backup_timestamp",
  1032. str(int(new_value.timestamp()))])
  1033. self.assertNotEqual(old_value,
  1034. self._get_table_item(vm_row, "Last backup"),
  1035. "Last backup date did not change")
  1036. self.assertEqual(
  1037. self._get_table_item(vm_row, "Last backup"),
  1038. target_timestamp,
  1039. "Incorrect Last backup date")
  1040. def test_412_prop_change_defdispvm(self):
  1041. target_vm_name = "work"
  1042. vm_row = self._find_vm_row(target_vm_name)
  1043. old_default_dispvm =\
  1044. self._get_table_item(vm_row, "Default DispVM")
  1045. new_default_dispvm = None
  1046. for vm in self.qapp.domains:
  1047. if getattr(vm, "template_for_dispvms", False) and vm.name !=\
  1048. old_default_dispvm:
  1049. new_default_dispvm = vm.name
  1050. break
  1051. self.addCleanup(
  1052. subprocess.call,
  1053. ["qvm-prefs", target_vm_name, "default_dispvm", old_default_dispvm])
  1054. self._run_command_and_process_events(
  1055. ["qvm-prefs", target_vm_name, "default_dispvm", new_default_dispvm])
  1056. self.assertNotEqual(
  1057. old_default_dispvm,
  1058. self._get_table_item(vm_row, "Default DispVM"),
  1059. "Default DispVM did not change")
  1060. self.assertEqual(
  1061. self._get_table_item(vm_row, "Default DispVM"),
  1062. self.qapp.domains[target_vm_name].default_dispvm.name,
  1063. "Incorrect Default DispVM")
  1064. def test_413_prop_change_templ_disp(self):
  1065. target_vm_name = "work"
  1066. vm_row = self._find_vm_row(target_vm_name)
  1067. self.addCleanup(
  1068. subprocess.call,
  1069. ["qvm-prefs", "--default", target_vm_name, "template_for_dispvms"])
  1070. self._run_command_and_process_events(
  1071. ["qvm-prefs", target_vm_name, "template_for_dispvms", "True"])
  1072. self.assertEqual(
  1073. self._get_table_item(vm_row, "Is DVM Template"),
  1074. "Yes",
  1075. "Incorrect value for DVM Template")
  1076. self._run_command_and_process_events(
  1077. ["qvm-prefs", "--default", target_vm_name, "template_for_dispvms"])
  1078. self.assertEqual(
  1079. self._get_table_item(vm_row, "Is DVM Template"),
  1080. "",
  1081. "Incorrect value for not DVM Template")
  1082. def test_414_vm_state_change(self):
  1083. target_vm_name = "work"
  1084. vm_row = self._find_vm_row(target_vm_name)
  1085. self.assertFalse(self.qapp.domains[target_vm_name].is_running())
  1086. self.addCleanup(
  1087. subprocess.call,
  1088. ["qvm-shutdown", "--wait", target_vm_name])
  1089. self._run_command_and_process_events(
  1090. ["qvm-start", target_vm_name], timeout=60)
  1091. displayed_state = self._get_table_item(vm_row, "State")
  1092. self.assertEqual(displayed_state['power'], 'Running',
  1093. "Power state failed to update on start")
  1094. self._run_command_and_process_events(
  1095. ["qvm-shutdown", "--wait", target_vm_name], timeout=30)
  1096. displayed_state = self._get_table_item(vm_row, "State")
  1097. self.assertEqual(displayed_state['power'], 'Halted',
  1098. "Power state failed to update on shutdown")
  1099. def test_415_template_vm_started(self):
  1100. # check whether changing state of a template_vm causes all other
  1101. # vms depending on it to check theirs
  1102. target_vm_name = None
  1103. for vm in self.qapp.domains:
  1104. if vm.klass == 'TemplateVM':
  1105. for vm2 in self.qapp.domains:
  1106. if getattr(vm2, 'template', None) == vm.name:
  1107. target_vm_name = vm.name
  1108. break
  1109. if target_vm_name:
  1110. break
  1111. for i in range(self.dialog.table.model().rowCount()):
  1112. self._get_table_vminfo(i).update = unittest.mock.Mock()
  1113. self.addCleanup(
  1114. subprocess.call,
  1115. ["qvm-shutdown", "--wait", target_vm_name])
  1116. self._run_command_and_process_events(
  1117. ["qvm-start", target_vm_name], timeout=60)
  1118. for i in range(self.dialog.table.model().rowCount()):
  1119. call_count = self._get_table_vminfo(
  1120. i).update.call_count
  1121. if self._get_table_item(i, "Template") == target_vm_name:
  1122. self.assertGreater(call_count, 0)
  1123. elif self._get_table_item(i, "Name") == target_vm_name:
  1124. self.assertGreater(call_count, 0)
  1125. else:
  1126. self.assertEqual(call_count, 0)
  1127. @unittest.mock.patch('qubesmanager.log_dialog.LogDialog')
  1128. def test_500_logs(self, mock_log_dialog):
  1129. self._select_admin_vm()
  1130. self.dialog.action_show_logs.trigger()
  1131. mock_log_dialog.assert_called_once()
  1132. dom0_logs = mock_log_dialog.mock_calls[0][1][1]
  1133. for c in dom0_logs:
  1134. self.assertIn("hypervisor", c,
  1135. "Log for dom0 does not contain 'hypervisor'")
  1136. mock_log_dialog.reset_mock()
  1137. selected_vm = self._select_non_admin_vm(running=True).name
  1138. self.dialog.action_show_logs.trigger()
  1139. mock_log_dialog.assert_called_once()
  1140. vm_logs = mock_log_dialog.mock_calls[0][1][1]
  1141. for c in vm_logs:
  1142. self.assertIn(
  1143. selected_vm,
  1144. c,
  1145. "Log for {} does not contain its name".format(selected_vm))
  1146. self.assertNotEqual(dom0_logs, vm_logs,
  1147. "Same logs found for dom0 and non-adminVM")
  1148. def _find_vm_row(self, vm_name):
  1149. for row in range(self.dialog.table.model().rowCount()):
  1150. name = self._get_table_item(row, "Name")
  1151. if name == vm_name:
  1152. return row
  1153. return None
  1154. def _count_visible_table_rows(self):
  1155. result = 0
  1156. for i in range(self.dialog.table.model().rowCount()):
  1157. if not self.dialog.table.isRowHidden(i):
  1158. result += 1
  1159. return result
  1160. def _run_command_and_process_events(self, command, timeout=5,
  1161. additional_timeout=None):
  1162. """
  1163. helper function to run a given command and process eventsDispatcher
  1164. events
  1165. :param command: list of strings, containing the command and all its
  1166. parameters
  1167. :param timeout: default 5 seconds
  1168. :param additional_timeout: default none
  1169. :return:
  1170. """
  1171. asyncio.set_event_loop(self.loop)
  1172. future1 = asyncio.ensure_future(self.dispatcher.listen_for_events())
  1173. self.loop.run_until_complete(asyncio.sleep(0))
  1174. future2 = asyncio.create_subprocess_exec(*command,
  1175. stdout=subprocess.DEVNULL,
  1176. stderr=subprocess.DEVNULL)
  1177. future2 = self.loop.run_until_complete(future2).wait()
  1178. if additional_timeout:
  1179. (done, pending) = self.loop.run_until_complete(
  1180. asyncio.wait({future1, future2}, timeout=timeout,
  1181. return_when=asyncio.FIRST_COMPLETED))
  1182. (done, pending) = self.loop.run_until_complete(
  1183. asyncio.wait(pending, timeout=additional_timeout))
  1184. else:
  1185. (done, pending) = self.loop.run_until_complete(
  1186. asyncio.wait({future1, future2}, timeout=timeout))
  1187. for task in pending:
  1188. with contextlib.suppress(asyncio.CancelledError):
  1189. task.cancel()
  1190. self.loop.call_soon(self.loop.stop)
  1191. self.loop.run_forever()
  1192. def _create_set_of_current_vms(self):
  1193. result = set()
  1194. for i in range(self.dialog.table.model().rowCount()):
  1195. result.add(self._get_table_item(i, "Name"))
  1196. return result
  1197. def _select_admin_vm(self):
  1198. for row in range(self.dialog.table.model().rowCount()):
  1199. template = self._get_table_item(row, "Template")
  1200. if template == 'AdminVM':
  1201. index = self.dialog.table.model().index(row, 0)
  1202. self.dialog.table.setCurrentIndex(index)
  1203. return index.data(Qt.UserRole).vm
  1204. return None
  1205. def _select_non_admin_vm(self, running=None):
  1206. for row in range(self.dialog.table.model().rowCount()):
  1207. template = self._get_table_item(row, "Template")
  1208. vm = self._get_table_vm(row)
  1209. if template != 'AdminVM' and not vm.provides_network and \
  1210. (running is None
  1211. or (running and vm.is_running())
  1212. or (not running and not vm.is_running())):
  1213. index = self.dialog.table.model().index(row, 0)
  1214. self.dialog.table.setCurrentIndex(index)
  1215. return vm
  1216. return None
  1217. def _select_templatevm(self, running=None, different_than=()):
  1218. for row in range(self.dialog.table.model().rowCount()):
  1219. template = self._get_table_item(row, "Template")
  1220. vm = self._get_table_vm(row)
  1221. if template == 'TemplateVM' and \
  1222. (template not in different_than) and \
  1223. (running is None
  1224. or (bool(running) == bool(vm.is_running()))):
  1225. index = self.dialog.table.model().index(row, 0)
  1226. self.dialog.table.setCurrentIndex(index)
  1227. return vm
  1228. return None
  1229. def _select_vms(self, vms: list):
  1230. self.dialog.table.selectionModel().clear()
  1231. mode = QtCore.QItemSelectionModel.Select | QtCore.QItemSelectionModel.Rows
  1232. for row in range(self.dialog.table.model().rowCount()):
  1233. vm = self._get_table_vm(row)
  1234. if str(vm) in vms:
  1235. index = self.dialog.table.model().index(row, 0)
  1236. self.dialog.table.selectionModel().select(index, mode)
  1237. def __check_sorting(self, column_name):
  1238. last_text = None
  1239. last_vm = None
  1240. for row in range(self.dialog.table.model().rowCount()):
  1241. vm = self._get_table_item(row, "Name")
  1242. text = self._get_table_item(row, column_name)
  1243. if row == 0:
  1244. self.assertEqual(vm, "dom0", "dom0 is not sorted first")
  1245. elif last_text is None:
  1246. last_text = text
  1247. last_vm = vm
  1248. else:
  1249. if last_text == text:
  1250. self.assertGreater(
  1251. vm.lower(), last_vm.lower(),
  1252. "Incorrect sorting for {}".format(column_name))
  1253. else:
  1254. self.assertGreater(
  1255. text.lower(), last_text.lower(),
  1256. "Incorrect sorting for {}".format(column_name))
  1257. last_text = text
  1258. last_vm = vm
  1259. def _get_table_vminfo(self, row):
  1260. model = self.dialog.table.model()
  1261. return model.index(row, 0).data(Qt.UserRole)
  1262. def _get_table_vm(self, row):
  1263. model = self.dialog.table.model()
  1264. return model.index(row, 0).data(Qt.UserRole).vm
  1265. def _get_table_item(self, row, column_name, role=Qt.DisplayRole):
  1266. model = self.dialog.table.model()
  1267. column = self.dialog.qubes_model.columns_indices.index(column_name)
  1268. return model.index(row, column).data(role)
  1269. class QubeManagerThreadTest(unittest.TestCase):
  1270. def test_01_startvm_thread(self):
  1271. vm = unittest.mock.Mock(spec=['start'])
  1272. thread = qube_manager.StartVMThread(vm)
  1273. thread.run()
  1274. vm.start.assert_called_once_with()
  1275. def test_02_startvm_thread_error(self):
  1276. vm = unittest.mock.Mock(
  1277. spec=['start'],
  1278. **{'start.side_effect': exc.QubesException('Error')})
  1279. thread = qube_manager.StartVMThread(vm)
  1280. thread.run()
  1281. self.assertIsNotNone(thread.msg)
  1282. def test_10_run_command_thread(self):
  1283. vm = unittest.mock.Mock(spec=['run'])
  1284. thread = qube_manager.RunCommandThread(vm, "test_command")
  1285. thread.run()
  1286. vm.run.assert_called_once_with("test_command")
  1287. def test_11_run_command_thread_error(self):
  1288. vm = unittest.mock.Mock(spec=['run'],
  1289. **{'run.side_effect': ChildProcessError})
  1290. thread = qube_manager.RunCommandThread(vm, "test_command")
  1291. thread.run()
  1292. self.assertIsNotNone(thread.msg)
  1293. @unittest.mock.patch('subprocess.check_call')
  1294. def test_20_update_vm_thread_dom0(self, check_call):
  1295. vm = unittest.mock.Mock(spec=['klass'])
  1296. vm.klass = 'AdminVM'
  1297. thread = qube_manager.UpdateVMThread(vm)
  1298. thread.run()
  1299. check_call.assert_called_once_with(
  1300. ["/usr/bin/qubes-dom0-update", "--clean", "--gui"])
  1301. @unittest.mock.patch('builtins.open')
  1302. @unittest.mock.patch('subprocess.call')
  1303. def test_21_update_vm_thread_running(self, mock_call, mock_open):
  1304. vm = unittest.mock.Mock(
  1305. spec=['klass', 'is_running', 'run_service_for_stdio', 'run_service'],
  1306. **{'is_running.return_value': True})
  1307. vm.klass = 'AppVM'
  1308. vm.run_service_for_stdio.return_value = (b'changed=no\n', None)
  1309. thread = qube_manager.UpdateVMThread(vm)
  1310. thread.run()
  1311. mock_open.assert_called_with(
  1312. '/usr/libexec/qubes-manager/dsa-4371-update', 'rb')
  1313. vm.run_service_for_stdio.assert_called_once_with(
  1314. "qubes.VMShell", user='root', input=unittest.mock.ANY)
  1315. vm.run_service.assert_called_once_with(
  1316. "qubes.InstallUpdatesGUI", user="root", wait=False)
  1317. self.assertEqual(mock_call.call_count, 0)
  1318. @unittest.mock.patch('builtins.open')
  1319. @unittest.mock.patch('subprocess.call')
  1320. def test_22_update_vm_thread_not_running(self, mock_call, mock_open):
  1321. vm = unittest.mock.Mock(
  1322. spec=['klass', 'is_running', 'run_service_for_stdio',
  1323. 'run_service', 'start', 'name'],
  1324. **{'is_running.return_value': False})
  1325. vm.klass = 'AppVM'
  1326. vm.run_service_for_stdio.return_value = (b'changed=yes\n', None)
  1327. thread = qube_manager.UpdateVMThread(vm)
  1328. thread.run()
  1329. mock_open.assert_called_with(
  1330. '/usr/libexec/qubes-manager/dsa-4371-update', 'rb')
  1331. vm.start.assert_called_once_with()
  1332. vm.run_service_for_stdio.assert_called_once_with(
  1333. "qubes.VMShell", user='root', input=unittest.mock.ANY)
  1334. vm.run_service.assert_called_once_with(
  1335. "qubes.InstallUpdatesGUI", user="root", wait=False)
  1336. self.assertEqual(mock_call.call_count, 1)
  1337. @unittest.mock.patch('builtins.open')
  1338. @unittest.mock.patch('subprocess.check_call')
  1339. def test_23_update_vm_thread_error(self, *_args):
  1340. vm = unittest.mock.Mock(
  1341. spec=['klass', 'is_running'],
  1342. **{'is_running.side_effect': ChildProcessError})
  1343. vm.klass = 'AppVM'
  1344. thread = qube_manager.UpdateVMThread(vm)
  1345. thread.run()
  1346. self.assertIsNotNone(thread.msg)
  1347. class VMShutdownMonitorTest(unittest.TestCase):
  1348. @unittest.mock.patch('qubesmanager.qube_manager.QMessageBox')
  1349. @unittest.mock.patch('PyQt5.QtCore.QTimer')
  1350. def test_01_vm_shutdown_correct(self, mock_timer, mock_question):
  1351. mock_vm = unittest.mock.Mock()
  1352. mock_vm.is_running.return_value = False
  1353. monitor = qube_manager.VmShutdownMonitor(mock_vm)
  1354. monitor.restart_vm_if_needed = unittest.mock.Mock()
  1355. monitor.check_if_vm_has_shutdown()
  1356. self.assertEqual(mock_question.call_count, 0)
  1357. self.assertEqual(mock_timer.call_count, 0)
  1358. monitor.restart_vm_if_needed.assert_called_once_with()
  1359. @unittest.mock.patch('qubesmanager.qube_manager.QMessageBox')
  1360. @unittest.mock.patch('PyQt5.QtCore.QTimer.singleShot')
  1361. def test_02_vm_not_shutdown_wait(self, mock_timer, mock_question):
  1362. mock_question().clickedButton.return_value = 1
  1363. mock_question().addButton.return_value = 0
  1364. mock_vm = unittest.mock.Mock()
  1365. mock_vm.is_running.return_value = True
  1366. mock_vm.start_time = datetime.datetime.now().timestamp() - 3000
  1367. mock_vm.shutdown_timeout = 60
  1368. monitor = qube_manager.VmShutdownMonitor(mock_vm)
  1369. time.sleep(3)
  1370. monitor.check_if_vm_has_shutdown()
  1371. self.assertEqual(mock_timer.call_count, 1)
  1372. @unittest.mock.patch('qubesmanager.qube_manager.QMessageBox')
  1373. @unittest.mock.patch('PyQt5.QtCore.QTimer.singleShot')
  1374. def test_03_vm_kill(self, mock_timer, mock_question):
  1375. mock_question().clickedButton.return_value = 1
  1376. mock_question().addButton.return_value = 1
  1377. mock_vm = unittest.mock.Mock()
  1378. mock_vm.is_running.return_value = True
  1379. mock_vm.start_time = datetime.datetime.now().timestamp() - 3000
  1380. mock_vm.shutdown_timeout = 1
  1381. monitor = qube_manager.VmShutdownMonitor(mock_vm)
  1382. time.sleep(3)
  1383. monitor.restart_vm_if_needed = unittest.mock.Mock()
  1384. monitor.check_if_vm_has_shutdown()
  1385. self.assertEqual(mock_timer.call_count, 0)
  1386. mock_vm.kill.assert_called_once_with()
  1387. monitor.restart_vm_if_needed.assert_called_once_with()
  1388. @unittest.mock.patch('qubesmanager.qube_manager.QMessageBox')
  1389. @unittest.mock.patch('PyQt5.QtCore.QTimer.singleShot')
  1390. def test_04_check_later(self, mock_timer, mock_question):
  1391. mock_vm = unittest.mock.Mock()
  1392. mock_vm.is_running.return_value = True
  1393. mock_vm.start_time = datetime.datetime.now().timestamp() - 3000
  1394. mock_vm.shutdown_timeout = 30
  1395. monitor = qube_manager.VmShutdownMonitor(mock_vm)
  1396. time.sleep(1)
  1397. monitor.check_if_vm_has_shutdown()
  1398. self.assertEqual(mock_question.call_count, 0)
  1399. self.assertEqual(mock_timer.call_count, 1)
  1400. if __name__ == "__main__":
  1401. ha_syslog = logging.handlers.SysLogHandler('/dev/log')
  1402. ha_syslog.setFormatter(
  1403. logging.Formatter('%(name)s[%(process)d]: %(message)s'))
  1404. logging.root.addHandler(ha_syslog)
  1405. unittest.main()