mirror of
https://github.com/virt-manager/virt-manager.git
synced 2024-12-22 13:34:07 +03:00
uitests: finish delete.py coverage
Signed-off-by: Cole Robinson <crobinso@redhat.com>
This commit is contained in:
parent
30e021bd3d
commit
1a3a89d949
@ -204,10 +204,14 @@ Foo bar baz & yeah boii < > 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 & yeah boii < > 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>
|
||||
|
||||
|
306
tests/uitests/test_delete.py
Normal file
306
tests/uitests/test_delete.py
Normal 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")
|
13
ui/delete.ui
13
ui/delete.ui
@ -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>
|
||||
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user