uitests: finish delete.py coverage

Signed-off-by: Cole Robinson <crobinso@redhat.com>
This commit is contained in:
Cole Robinson 2020-08-30 13:37:34 -04:00
parent 30e021bd3d
commit 1a3a89d949
4 changed files with 354 additions and 41 deletions

View File

@ -204,10 +204,14 @@ Foo bar baz &amp; yeah boii &lt; &gt; yeahfoo
<source dir='/tmp'/>
<target dev='fdb' bus='fdc'/>
</disk>
<disk type='dir' device='floppy'>
<source dir='/dev/default-pool/test-clone-simple.img'/>
<target dev='fdc' bus='fdc'/>
</disk>
<!-- bus ide -->
<disk type='file' device='disk'>
<source file='/tmp/foobar'/>
<source file='/tmp/virt-manager-uitests/tmp1'/>
<target dev='hda' bus='ide'/>
<iotune>
<read_bytes_sec>5242880</read_bytes_sec>
@ -321,7 +325,7 @@ Foo bar baz &amp; yeah boii &lt; &gt; yeahfoo
<!-- bus xen -->
<disk type='file' device='disk'>
<driver name='tap' type='qcow' cache="writethrough"/>
<source file='/tmp/foobar4'/>
<source file='/dev/default-pool/test-clone-simple.img'/>
<target dev='xvdc' bus='xen'/>
</disk>

View File

@ -0,0 +1,306 @@
# This work is licensed under the GNU GPLv2 or later.
# See the COPYING file in the top-level directory.
import os
import shutil
from tests.uitests import utils as uiutils
import tests.utils
class _DeleteRow:
"""
Helper class for interacting with the delete dialog rows
"""
def __init__(self, cell1, cell2, cell3, cell4):
ignore = cell4
self.chkcell = cell1
self.path = cell2.text
self.target = cell3.text
self.undeletable = not self.chkcell.sensitive
self.default = self.chkcell.checked
self.notdefault = not self.undeletable and not self.default
def _create_testdriver_path(fn):
def wrapper(self, *args, **kwargs):
# This special path is hardcoded in test-many-devices
tmppath = "/tmp/virt-manager-uitests/tmp1"
tmpdir = os.path.dirname(tmppath)
try:
if not os.path.exists(tmpdir):
os.mkdir(tmpdir)
open(tmppath, "w").write("foo")
os.chmod(tmppath, 0o444)
return fn(self, tmppath, *args, **kwargs)
finally:
if os.path.exists(tmpdir):
os.chmod(tmpdir, 0o777)
shutil.rmtree(tmpdir)
return wrapper
class Delete(uiutils.UITestCase):
"""
UI tests for virt-manager's VM delete window
"""
def _open_storage_browser(self):
self.app.root.find("New", "push button").click()
newvm = self.app.root.find("New VM", "frame")
newvm.find_fuzzy("Local install media", "radio").click()
newvm.find_fuzzy("Forward", "button").click()
newvm.find_fuzzy("install-iso-browse", "button").click()
return self.app.root.find("vmm-storage-browser")
def _open_delete(self, vmname):
manager = self.app.topwin
cell = manager.find(vmname, "table cell")
cell.click()
cell.click(button=3)
menu = self.app.root.find("vm-action-menu")
menu.find("Delete", "menu item").click()
return self.app.root.find_fuzzy("Delete", "frame")
def _finish(self, delete, paths, expect_fail=False, click_no=False):
delete.find_fuzzy("Delete", "button").click()
if paths:
alert = self.app.root.find("vmm dialog", "alert")
alert.find_fuzzy("Are you sure")
for path in paths:
alert.find_fuzzy(path)
if click_no:
alert.find("No", "push button").click()
return
alert.find("Yes", "push button").click()
if not expect_fail:
uiutils.check(lambda: not delete.showing)
def _get_all_rows(self, delete):
slist = delete.find("storage-list")
def pred(node):
return node.roleName == "table cell"
cells = slist.findChildren(pred, isLambda=True)
idx = 0
rows = []
while idx < len(cells):
rows.append(_DeleteRow(*cells[idx:idx + 4]))
idx += 4
return rows
##############
# Test cases #
##############
def _testDeleteManyDevices(self,
nondefault_path=None, delete_nondefault=False,
skip_finish=False):
delete = self._open_delete("test-many-devices")
rows = self._get_all_rows(delete)
selected_rows = [r.path for r in rows if r.default]
undeletable_rows = [r.path for r in rows if r.undeletable]
notdefault_rows = [r.path for r in rows if r.notdefault]
defpath = "/dev/default-pool/overlay.img"
nondefault_path2 = "/dev/default-pool/sharevol.img"
assert selected_rows == [defpath]
if nondefault_path:
assert nondefault_path in notdefault_rows
assert nondefault_path2 in notdefault_rows
assert "/dev/fda" in undeletable_rows
if delete_nondefault:
# Click the selector for the nondefault path
found = [r for r in rows if r.path == nondefault_path]
assert len(found) == 1
slist = delete.find("storage-list")
slist.click()
chkcell = found[0].chkcell
chkcell.bring_on_screen()
chkcell.click()
chkcell.click()
chkcell.click()
uiutils.check(lambda: chkcell.checked)
paths = []
if defpath:
paths.append(defpath)
if delete_nondefault:
paths.append(nondefault_path)
if skip_finish:
return paths
self._finish(delete, paths)
# Confirm
browser = self._open_storage_browser()
browser.find_fuzzy("default-pool", "table cell").click()
browser.find("vol-refresh", "push button").click()
uiutils.check(lambda: "overlay.img" not in browser.fmt_nodes())
browser.find("sharevol.img", "table cell")
@_create_testdriver_path
def testDeleteManyDevices(self, tmppath):
"""
Hit a specific case of a path not selected by default
because the permissions are readonly
"""
self._testDeleteManyDevices(nondefault_path=tmppath)
@_create_testdriver_path
def testDeleteNondefaultOverride(self, tmppath):
"""
Path not selected by default, but we select it,
which will cause it to be manually unlinked
"""
self._testDeleteManyDevices(
nondefault_path=tmppath,
delete_nondefault=True)
assert not os.path.exists(tmppath)
@_create_testdriver_path
def testDeleteFailure(self, tmppath):
"""
After launching the wizard we change permissions to make
file deletion fail
"""
paths = self._testDeleteManyDevices(
nondefault_path=tmppath,
delete_nondefault=True,
skip_finish=True)
os.chmod(os.path.dirname(tmppath), 0o555)
delete = self.app.root.find_fuzzy("Delete", "frame")
self._finish(delete, paths, expect_fail=True, click_no=True)
uiutils.check(lambda: delete.active)
self._finish(delete, paths, expect_fail=True)
assert os.path.exists(tmppath)
self._click_alert_button("Errors encountered", "Close")
def testDeleteRemoteManyDevices(self):
"""
Test with a remote VM to hit a certain code path
"""
self.app.uri = tests.utils.URIs.kvm_remote
self._testDeleteManyDevices()
def testDeleteSkipStorage(self):
"""
Test VM delete with all storage skipped
"""
delete = self._open_delete("test-many-devices")
chk = delete.find("Delete associated", "check box")
slist = delete.find("storage-list")
uiutils.check(lambda: chk.checked)
chk.click()
uiutils.check(lambda: not chk.checked)
uiutils.check(lambda: not slist.showing)
self._finish(delete, None)
# Confirm nothing was deleted compare to the default selections
browser = self._open_storage_browser()
browser.find_fuzzy("default-pool", "table cell").click()
browser.find("vol-refresh", "push button").click()
self.sleep(.5)
browser.find("overlay.img", "table cell")
browser.find("sharevol.img", "table cell")
def testDeleteDeviceNoStorage(self):
"""
Verify successful device remove with storage doesn't
touch host storage
"""
details = self._open_details_window("test-many-devices",
shutdown=True)
hwlist = details.find("hw-list")
hwlist.click()
c = hwlist.find("USB Disk 1")
c.bring_on_screen()
c.click()
tab = details.find("disk-tab")
uiutils.check(lambda: tab.showing)
details.find("config-remove").click()
delete = self.app.root.find_fuzzy("Remove Disk", "frame")
chk = delete.find("Delete associated", "check box")
uiutils.check(lambda: not chk.checked)
self._finish(delete, [])
details.click()
details.keyCombo("<alt>F4")
browser = self._open_storage_browser()
browser.find_fuzzy("default-pool", "table cell").click()
browser.find("vol-refresh", "push button").click()
self.sleep(.5)
browser.find("overlay.img", "table cell")
def testDeleteDeviceWithStorage(self):
"""
Verify successful device remove deletes storage
"""
details = self._open_details_window("test-many-devices",
shutdown=True)
hwlist = details.find("hw-list")
hwlist.click()
c = hwlist.find("USB Disk 1")
c.bring_on_screen()
c.click()
tab = details.find("disk-tab")
uiutils.check(lambda: tab.showing)
details.find("config-remove").click()
delete = self.app.root.find_fuzzy("Remove Disk", "frame")
chk = delete.find("Delete associated", "check box")
uiutils.check(lambda: not chk.checked)
chk.click()
uiutils.check(lambda: chk.checked)
path = "/dev/default-pool/overlay.img"
delete.find_fuzzy(path)
self._finish(delete, [path])
details.click()
details.keyCombo("<alt>F4")
browser = self._open_storage_browser()
browser.find_fuzzy("default-pool", "table cell").click()
browser.find("vol-refresh", "push button").click()
uiutils.check(lambda: "overlay.img" not in browser.fmt_nodes())
def testDeleteDeviceFail(self):
"""
Verify failed device remove does not touch storage
"""
details = self._open_details_window("test-many-devices")
hwlist = details.find("hw-list")
hwlist.click()
c = hwlist.find("USB Disk 1")
c.bring_on_screen()
c.click()
tab = details.find("disk-tab")
uiutils.check(lambda: tab.showing)
details.find("config-remove").click()
delete = self.app.root.find_fuzzy("Remove Disk", "frame")
chk = delete.find("Delete associated", "check box")
uiutils.check(lambda: not chk.checked)
chk.click()
uiutils.check(lambda: chk.checked)
path = "/dev/default-pool/overlay.img"
delete.find_fuzzy(path)
self._finish(delete, [path], expect_fail=True)
self._click_alert_button("Storage will not be.*deleted", "OK")
details.click()
details.keyCombo("<alt>F4")
# Verify file still exists
browser = self._open_storage_browser()
browser.find_fuzzy("default-pool", "table cell").click()
browser.find("vol-refresh", "push button").click()
self.sleep(.5)
browser.find("overlay.img", "table cell")

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.22.1 -->
<!-- Generated with glade 3.36.0 -->
<interface>
<requires lib="gtk+" version="3.22"/>
<object class="GtkWindow" id="vmm-delete">
@ -10,9 +10,6 @@
<property name="window_position">center-on-parent</property>
<property name="type_hint">dialog</property>
<signal name="delete-event" handler="on_vmm_delete_delete_event" swapped="no"/>
<child>
<placeholder/>
</child>
<child>
<object class="GtkBox" id="dialog-vbox1">
<property name="visible">True</property>
@ -152,6 +149,11 @@
<child internal-child="selection">
<object class="GtkTreeSelection" id="treeview-selection"/>
</child>
<child internal-child="accessible">
<object class="AtkObject" id="delete-storage-list-atkobject">
<property name="AtkObject::accessible-name">storage-list</property>
</object>
</child>
</object>
</child>
</object>
@ -219,5 +221,8 @@
</child>
</object>
</child>
<child type="titlebar">
<placeholder/>
</child>
</object>
</interface>

View File

@ -198,7 +198,8 @@ class _vmmDeleteBase(vmmGObjectUI):
storage_errors = self._async_delete_paths(paths, conn, meter)
self._delete_vm(vm)
except Exception as e:
vm.conn.schedule_priority_tick(pollvm=True)
except Exception as e: # pragma: no cover
error = _("Error deleting virtual machine '%(vm)s': %(error)s") % {
"vm": vm.get_name(),
"error": str(e),
@ -214,7 +215,7 @@ class _vmmDeleteBase(vmmGObjectUI):
# We had extra storage errors. If there was another error message,
# errors to it. Otherwise, build the main error around them.
if details:
if details: # pragma: no cover
details += "\n\n"
details += _("Additionally, there were errors removing"
" certain storage devices: \n")
@ -226,7 +227,6 @@ class _vmmDeleteBase(vmmGObjectUI):
if error:
asyncjob.set_error(error, details)
vm.conn.schedule_priority_tick(pollvm=True)
def _async_delete_paths(self, paths, conn, meter):
storage_errors = []
@ -335,7 +335,7 @@ class vmmDeleteStorage(_vmmDeleteBase):
# Define the change
try:
vm.remove_device(devobj)
except Exception as e:
except Exception as e: # pragma: no cover
err.show_err(_("Error Removing Device: %s") % str(e))
return
@ -516,9 +516,16 @@ def _prepare_storage_list(storage_list):
chkbox = Gtk.CellRendererToggle()
chkbox.connect('toggled', _storage_item_toggled, storage_list)
confirmCol.pack_start(chkbox, False)
confirmCol.add_attribute(chkbox, 'active', STORAGE_ROW_CONFIRM)
confirmCol.add_attribute(chkbox, 'inconsistent',
STORAGE_ROW_CANT_DELETE)
def sensitive_cb(column, cell, model, _iter, data):
row = model[_iter]
inconsistent = row[STORAGE_ROW_CANT_DELETE]
sensitive = not inconsistent
active = row[STORAGE_ROW_CONFIRM]
chk = column.get_cells()[0]
chk.set_property('inconsistent', inconsistent)
chk.set_property('active', active)
chk.set_property('sensitive', sensitive)
confirmCol.set_cell_data_func(chkbox, sensitive_cb)
confirmCol.set_sort_column_id(STORAGE_ROW_CANT_DELETE)
path_txt = Gtk.CellRendererText()
@ -550,16 +557,15 @@ def _storage_item_toggled(src, index, storage_list):
def _can_delete(conn, vol, path):
"""Is the passed path even deleteable"""
ret = True
msg = None
if vol:
# Managed storage
pool_type = vol.get_parent_pool().get_type()
if pool_type == virtinst.StoragePool.TYPE_ISCSI:
msg = _("Cannot delete iSCSI share.")
msg = _("Cannot delete iSCSI share.") # pragma: no cover
elif pool_type == virtinst.StoragePool.TYPE_SCSI:
msg = _("Cannot delete SCSI device.")
msg = _("Cannot delete SCSI device.") # pragma: no cover
else:
if conn.is_remote():
msg = _("Cannot delete unmanaged remote storage.")
@ -567,50 +573,42 @@ def _can_delete(conn, vol, path):
msg = _("Path does not exist.")
elif not os.access(os.path.dirname(path), os.W_OK):
msg = _("No write access to parent directory.")
elif stat.S_ISBLK(os.stat(path)[stat.ST_MODE]):
elif stat.S_ISBLK(os.stat(path)[stat.ST_MODE]): # pragma: no cover
msg = _("Cannot delete unmanaged block device.")
if msg:
ret = False
return (ret, msg)
can_delete = bool(not msg)
return (can_delete, msg)
def _do_we_default(conn, vm_name, vol, diskdata):
""" Returns (do we delete by default?, info string if not)"""
info = ""
def append_str(str1, str2, delim="\n"):
if not str2:
return str1
if str1:
str1 += delim
str1 += str2
return str1
info = []
if diskdata.ro:
info = append_str(info, _("Storage is read-only."))
info.append(_("Storage is read-only."))
elif not vol and not os.access(diskdata.path, os.W_OK):
info = append_str(info, _("No write access to path."))
info.append(_("No write access to path."))
if diskdata.shared:
info = append_str(info, _("Storage is marked as shareable."))
info.append(_("Storage is marked as shareable."))
if not info and diskdata.is_media:
info = append_str(info, _("Storage is a media device."))
info.append(_("Storage is a media device."))
try:
names = virtinst.DeviceDisk.path_in_use_by(conn.get_backend(),
diskdata.path)
if len(names) > 1:
namestr = ""
names.remove(vm_name)
for name in names:
namestr = append_str(namestr, name, delim="\n- ")
info = append_str(info, _("Storage is in use by the following "
"virtual machines:\n- %s " % namestr))
except Exception as e:
namestr = "\n- ".join(names)
msg = _("Storage is in use by the following virtual machines")
msg += "\n- " + namestr
info.append(msg)
except Exception as e: # pragma: no cover
log.exception("Failed checking disk conflict: %s", str(e))
info.append(_("Failed to check disk usage conflict."))
return (not info, info)
infostr = "\n".join(info)
do_default = bool(not infostr)
return (do_default, infostr)