diff --git a/tests/uitests/test_details.py b/tests/uitests/test_details.py index b73c063a..0292ac69 100644 --- a/tests/uitests/test_details.py +++ b/tests/uitests/test_details.py @@ -717,3 +717,156 @@ class Details(uiutils.UITestCase): # Do standard xmleditor tests self._test_xmleditor_interactions(win, finish) + + def testDetailsConsoleChecksSSH(self): + """ + Trigger a bunch of console connection failures to hit + various details/* code paths + """ + fakeuri = "qemu+ssh://foouser@256.256.256.256:1234/system" + uri = tests.utils.URIs.test_full + ",fakeuri=%s" % fakeuri + self.app.uri = uri + self.app.open(xmleditor_enabled=True) + + self.app.topwin.find("test\n", "table cell").doubleClick() + win = self.app.root.find("test on", "frame") + conpages = win.find("console-pages") + run = win.find("Run", "push button") + shutdown = win.find("Shut Down", "push button") + conbtn = win.find("Console", "radio button") + detailsbtn = win.find("Details", "radio button") + + def _run(): + win.click_title() + run.click() + uiutils.check(lambda: not run.sensitive) + def _stop(): + shutdown.click() + uiutils.check(lambda: not shutdown.sensitive) + def _checkcon(msg): + conbtn.click() + uiutils.check(lambda: conpages.showing) + conpages.find(msg) + def _check_textconsole_menu(msg): + vmenu = win.find("^View$", "menu") + vmenu.click() + tmenu = win.find("Text Consoles", "menu") + tmenu.point() + tmenu.find(msg, "menu item") + vmenu.click() + + # Check initial state + _checkcon("Graphical console not configured") + _stop() + _check_textconsole_menu("No graphical console available") + + # Add a SDL graphics device which can't be displayed + detailsbtn.click() + win.find("add-hardware", "push button").click() + addhw = self.app.root.find("Add New Virtual Hardware", "frame") + addhw.find("Graphics", "table cell").click() + addhw.find("XML", "page tab").click() + dev = '' + addhw.find("XML editor").text = dev + addhw.find("Finish", "push button").click() + uiutils.check(lambda: not addhw.active) + uiutils.check(lambda: win.active) + _run() + _checkcon("Cannot display graphical console type") + + def _change_gfx_xml(_xml): + detailsbtn.click() + win.find("Display ", "table cell").click() + win.find("XML", "page tab").click() + win.find("XML editor").set_text(_xml) + win.find("config-apply").click() + + # Listening from some other address + _stop() + xml = '' + _change_gfx_xml(xml) + _run() + _checkcon(".*resolving.*256.256.256.256.*") + + # Listening from some other address + _stop() + xml = '' + _change_gfx_xml(xml) + _run() + _checkcon(".*resolving.*257.0.0.1.*") + + # Hit a specific error about tls only and ssh + _stop() + xml = '' + _change_gfx_xml(xml) + _run() + _checkcon(".*configured for TLS only.*") + + # Fake a socket connection + _stop() + xml = '' + _change_gfx_xml(xml) + _run() + _checkcon(".*SSH tunnel error output.*") + + # Add a listen type='none' check + _stop() + xml = '' + _change_gfx_xml(xml) + _run() + _checkcon(".*local file descriptor.*") + + # Add a local list + port check + _stop() + xml = '' + _change_gfx_xml(xml) + _run() + _checkcon(".*SSH tunnel error output.*") + + def testDetailsConsoleChecksTCP(self): + """ + Hit a specific warning when the connection has + non-SSH transport but the guest config is only listening locally + """ + fakeuri = "qemu+tcp://foouser@256.256.256.256:1234/system" + uri = tests.utils.URIs.test_full + ",fakeuri=%s" % fakeuri + self.app.uri = uri + self.app.open(xmleditor_enabled=True) + + self.app.topwin.find("test\n", "table cell").doubleClick() + win = self.app.root.find("test on", "frame") + conpages = win.find("console-pages") + run = win.find("Run", "push button") + shutdown = win.find("Shut Down", "push button") + conbtn = win.find("Console", "radio button") + detailsbtn = win.find("Details", "radio button") + + def _run(): + win.click_title() + run.click() + uiutils.check(lambda: not run.sensitive) + def _stop(): + shutdown.click() + uiutils.check(lambda: not shutdown.sensitive) + def _checkcon(msg): + conbtn.click() + uiutils.check(lambda: conpages.showing) + conpages.find(msg) + + # Check initial state + _checkcon("Graphical console not configured") + _stop() + + # Add a SDL graphics device which can't be displayed + detailsbtn.click() + win.find("add-hardware", "push button").click() + addhw = self.app.root.find("Add New Virtual Hardware", "frame") + addhw.find("Graphics", "table cell").click() + addhw.find("XML", "page tab").click() + dev = '' + addhw.find("XML editor").text = dev + addhw.find("Finish", "push button").click() + uiutils.check(lambda: not addhw.active) + uiutils.check(lambda: win.active) + _run() + _checkcon(".*configured to listen locally.*") diff --git a/tests/uitests/test_livetests.py b/tests/uitests/test_livetests.py index 8aa78b87..2461087e 100644 --- a/tests/uitests/test_livetests.py +++ b/tests/uitests/test_livetests.py @@ -28,7 +28,7 @@ def _vm_wrapper(vmname, uri="qemu:///system", opts=None): extra_opts = (opts or []) extra_opts += ["--show-domain-console", vmname] self.app.open(extra_opts=extra_opts) - fn(self, *args, **kwargs) + fn(self, dom, *args, **kwargs) finally: try: self.app.stop() @@ -55,7 +55,7 @@ class Console(uiutils.UITestCase): # Test cases # ############## - def _checkConsoleStandard(self): + def _checkConsoleStandard(self, dom): """ Shared logic for general console handling """ @@ -97,6 +97,11 @@ class Console(uiutils.UITestCase): uiutils.check(lambda: not fstb.showing, timeout=5) self.point(win.position[0] + win.size[0] / 2, 0) uiutils.check(lambda: fstb.showing) + # Move it off and have it hide again + win.point() + uiutils.check(lambda: not fstb.showing, timeout=5) + self.point(win.position[0] + win.size[0] / 2, 0) + uiutils.check(lambda: fstb.showing) # Click stuff and exit fullscreen win.find("Fullscreen Send Key").click() @@ -104,6 +109,13 @@ class Console(uiutils.UITestCase): win.find("Fullscreen Exit").click() uiutils.check(lambda: win.size == newsize) + # Trigger pointer grab, verify title was updated + win.click() + uiutils.check(lambda: "Control_L" in win.name) + # Ungrab + win.keyCombo("") + uiutils.check(lambda: "Control_L" not in win.name) + # Tweak scaling win.click_title() win.click_title() @@ -115,15 +127,20 @@ class Console(uiutils.UITestCase): scalemenu = win.find("Scale Display", "menu") scalemenu.point() scalemenu.find("Never", "radio menu item").click() - self.sleep(.5) + win.find("^View$", "menu").click() + scalemenu = win.find("Scale Display", "menu") + scalemenu.point() + scalemenu.find("Only", "radio menu item").click() + + dom.destroy() + win.find("Guest is not running.") @_vm_wrapper("uitests-vnc-standard") - def testConsoleVNCStandard(self): - return self._checkConsoleStandard() + def testConsoleVNCStandard(self, dom): + return self._checkConsoleStandard(dom) @_vm_wrapper("uitests-spice-standard") - def testConsoleSpiceStandard(self): - return self._checkConsoleStandard() - + def testConsoleSpiceStandard(self, dom): + return self._checkConsoleStandard(dom) def _checkPassword(self): """ @@ -177,12 +194,32 @@ class Console(uiutils.UITestCase): uiutils.check(lambda: not bool(passwd.text)) @_vm_wrapper("uitests-vnc-password") - def testConsoleVNCPassword(self): + def testConsoleVNCPassword(self, dom): + ignore = dom return self._checkPassword() @_vm_wrapper("uitests-spice-password") - def testConsoleSpicePassword(self): + def testConsoleSpicePassword(self, dom): + ignore = dom return self._checkPassword() + @_vm_wrapper("uitests-vnc-password", + opts=["--test-options=fake-vnc-username"]) + def testConsoleVNCPasswordUsername(self, dom): + ignore = dom + win = self.app.topwin + con = win.find("console-gfx-viewport") + uiutils.check(lambda: not con.showing) + passwd = win.find("Password:", "password text") + uiutils.check(lambda: passwd.showing) + username = win.find("Username:", "text") + uiutils.check(lambda: username.showing) + + # Since we are mocking the username, sending the credentials + # is ignored, so with the correct password this succeeds + username.text = "fakeuser" + passwd.typeText("goodp") + win.find("Login", "push button").click() + uiutils.check(lambda: con.showing) @_vm_wrapper("uitests-vnc-socket") def testConsoleVNCSocket(self, dom): @@ -205,10 +242,11 @@ class Console(uiutils.UITestCase): uiutils.check(lambda: con.showing) @_vm_wrapper("uitests-lxc-serial", uri="lxc:///") - def testConsoleLXCSerial(self): + def testConsoleLXCSerial(self, dom): """ Ensure LXC has serial open, and we can send some data """ + ignore = dom win = self.app.topwin term = win.find("Serial Terminal") uiutils.check(lambda: term.showing) @@ -246,12 +284,15 @@ class Console(uiutils.UITestCase): term = win.find("Serial Terminal") uiutils.check(lambda: term.showing) - @_vm_wrapper("uitests-spice-specific", opts=["--test-options=spice-agent"]) - def testConsoleSpiceSpecific(self): + @_vm_wrapper("uitests-spice-specific", + opts=["--test-options=spice-agent", + "--test-options=fake-console-resolution"]) + def testConsoleSpiceSpecific(self, dom): """ Spice specific behavior. Has lots of devices that will open channels, spice GL + local config, and usbredir """ + ignore = dom win = self.app.topwin con = win.find("console-gfx-viewport") uiutils.check(lambda: con.showing) @@ -337,11 +378,12 @@ class Console(uiutils.UITestCase): @_vm_wrapper("uitests-hotplug") - def testLiveHotplug(self): + def testLiveHotplug(self, dom): """ Live test for basic hotplugging and media change, as well as testing our auto-poolify magic """ + ignore = dom import tempfile tmpdir = tempfile.TemporaryDirectory(prefix="uitests-tmp") dname = tmpdir.name diff --git a/ui/vmwindow.ui b/ui/vmwindow.ui index 6c78890a..39178706 100644 --- a/ui/vmwindow.ui +++ b/ui/vmwindow.ui @@ -1,5 +1,5 @@ - + @@ -18,9 +18,6 @@ - - - True @@ -772,6 +769,11 @@ False + + + console-pages + + 1 @@ -821,5 +823,8 @@ + + + diff --git a/virtManager/details/console.py b/virtManager/details/console.py index 21fda86a..67ba90e9 100644 --- a/virtManager/details/console.py +++ b/virtManager/details/console.py @@ -350,8 +350,12 @@ class vmmConsolePages(vmmGObjectUI): def _scroll_size_allocate(self, src_ignore, req): if not self._viewer: return - if not self._viewer.console_get_desktop_resolution(): - return + + res = self._viewer.console_get_desktop_resolution() + if res is None: + if not self.config.CLITestOptions.fake_console_resolution: + return + res = (800, 600) scroll = self.widget("console-gfx-scroll") is_scale = self._viewer.console_get_scaling() @@ -362,7 +366,7 @@ class vmmConsolePages(vmmGObjectUI): align_ratio = float(req.width) / float(req.height) # pylint: disable=unpacking-non-sequence - desktop_w, desktop_h = self._viewer.console_get_desktop_resolution() + desktop_w, desktop_h = res desktop_ratio = float(desktop_w) / float(desktop_h) if is_scale: @@ -556,7 +560,7 @@ class vmmConsolePages(vmmGObjectUI): ########################## def _show_vm_status_unavailable(self): - if self.vm.is_crashed(): + if self.vm.is_crashed(): # pragma: no cover self._activate_unavailable_page(_("Guest has crashed.")) else: self._activate_unavailable_page(_("Guest is not running.")) @@ -778,7 +782,7 @@ class vmmConsolePages(vmmGObjectUI): force_accel = self.config.get_console_accels() if force_accel: - self._enable_modifiers() + self._enable_modifiers() # pragma: no cover elif self._someone_has_focus(): self._disable_modifiers() else: @@ -800,14 +804,15 @@ class vmmConsolePages(vmmGObjectUI): self._activate_auth_page(withPassword, withUsername) def _viewer_agent_connected(self, ignore): - self._refresh_resizeguest_from_settings() + self._refresh_resizeguest_from_settings() # pragma: no cover def _viewer_usb_redirect_error(self, ignore, errstr): - self.err.show_err(_("USB redirection error"), - text2=str(errstr), modal=True) + self.err.show_err( + _("USB redirection error"), + text2=str(errstr), modal=True) # pragma: no cover def _viewer_disconnected_set_page(self, errdetails, ssherr): - if self.vm.is_runable(): + if self.vm.is_runable(): # pragma: no cover # Exit was probably for legitimate reasons self._show_vm_status_unavailable() return diff --git a/virtManager/details/sshtunnels.py b/virtManager/details/sshtunnels.py index 7ed8e9b1..9afc1e13 100644 --- a/virtManager/details/sshtunnels.py +++ b/virtManager/details/sshtunnels.py @@ -148,7 +148,7 @@ class _Tunnel(object): def close(self): if self._closed: - return + return # pragma: no cover self._closed = True log.debug("Close tunnel PID=%s ERRFD=%s", @@ -168,7 +168,7 @@ class _Tunnel(object): while True: try: new = self._errfd.recv(1024) - except Exception: + except Exception: # pragma: no cover break if not new: @@ -180,11 +180,11 @@ class _Tunnel(object): def open(self, argv, sshfd): if self._closed: - return + return # pragma: no cover errfds = socket.socketpair() pid = os.fork() - if pid == 0: + if pid == 0: # pragma: no cover errfds[0].close() os.dup2(sshfd.fileno(), 0) diff --git a/virtManager/details/viewers.py b/virtManager/details/viewers.py index 6d75ce4e..569a404e 100644 --- a/virtManager/details/viewers.py +++ b/virtManager/details/viewers.py @@ -16,7 +16,7 @@ try: from gi.repository import SpiceClientGtk from gi.repository import SpiceClientGLib have_spice_gtk = True -except (ValueError, ImportError): +except (ValueError, ImportError): # pragma: no cover have_spice_gtk = False from virtinst import log @@ -131,7 +131,7 @@ class Viewer(vmmGObject): if self._ginfo.gtlsport and not self._ginfo.gport: # This makes spice loop requesting an fd. Disable until spice is # fixed: https://bugzilla.redhat.com/show_bug.cgi?id=1334071 - return None + return None # pragma: no cover if not self._vm.conn.support.domain_open_graphics(): return None @@ -334,6 +334,9 @@ class VNCViewer(Viewer): for idx in range(int(credList.n_values)): values.append(credList.get_nth(idx)) + if self.config.CLITestOptions.fake_vnc_username: + values.append(GtkVnc.DisplayCredential.USERNAME) + withUsername = False withPassword = False for cred in values: @@ -378,23 +381,23 @@ class VNCViewer(Viewer): def _refresh_grab_keys(self): if not self._display: - return + return # pragma: no cover try: keys = self.config.get_keys_combination() if not keys: - return + return # pragma: no cover try: keys = [int(k) for k in keys.split(',')] - except Exception: + except Exception: # pragma: no cover log.debug("Error in grab_keys configuration in Gsettings", exc_info=True) return seq = GtkVnc.GrabSequence.new(keys) self._display.set_grab_keys(seq) - except Exception as e: + except Exception as e: # pragma: no cover log.debug("Error when getting the grab keys combination: %s", str(e)) @@ -403,7 +406,7 @@ class VNCViewer(Viewer): def _refresh_keyboard_grab_default(self): if not self._display: - return + return # pragma: no cover self._display.set_keyboard_grab(self.config.get_keyboard_grab_default()) def _get_desktop_resolution(self): @@ -508,7 +511,7 @@ class SpiceViewer(Viewer): autoredir = self.config.get_auto_usbredir() if autoredir: gtk_session.set_property("auto-usbredir", True) - except Exception: + except Exception: # pragma: no cover self._usbdev_manager = None log.debug("Error initializing spice usb device manager", exc_info=True) @@ -559,7 +562,7 @@ class SpiceViewer(Viewer): # Can happen if we close the details window and clear self._tunnels # while initially connecting to spice and channel FD requests # are still rolling in - return + return # pragma: no cover log.debug("Requesting fd for channel: %s", channel) channel.connect_after("channel-event", self._fd_channel_event_cb) @@ -585,7 +588,7 @@ class SpiceViewer(Viewer): not self._display): channel_id = channel.get_property("channel-id") - if channel_id != 0: + if channel_id != 0: # pragma: no cover log.debug("Spice multi-head unsupported") return @@ -601,7 +604,7 @@ class SpiceViewer(Viewer): self._audio = SpiceClientGLib.Audio.get(self._spice_session, None) def _agent_connected_cb(self, src, val): - self.emit("agent-connected") + self.emit("agent-connected") # pragma: no cover ################################ @@ -626,23 +629,23 @@ class SpiceViewer(Viewer): def _refresh_grab_keys(self): if not self._display: - return + return # pragma: no cover try: keys = self.config.get_keys_combination() if not keys: - return + return # pragma: no cover try: keys = [int(k) for k in keys.split(',')] - except Exception: + except Exception: # pragma: no cover log.debug("Error in grab_keys configuration in Gsettings", exc_info=True) return seq = SpiceClientGtk.GrabSequence.new(keys) self._display.set_grab_keys(seq) - except Exception as e: + except Exception as e: # pragma: no cover log.debug("Error when getting the grab keys combination: %s", str(e)) @@ -652,7 +655,7 @@ class SpiceViewer(Viewer): def _refresh_keyboard_grab_default(self): if not self._display: - return + return # pragma: no cover self._display.set_property("grab-keyboard", self.config.get_keyboard_grab_default()) @@ -663,7 +666,7 @@ class SpiceViewer(Viewer): def _has_agent(self): if not self._main_channel: - return False + return False # pragma: no cover return (self._main_channel.get_property("agent-connected") or self.config.CLITestOptions.spice_agent) @@ -686,14 +689,14 @@ class SpiceViewer(Viewer): self._spice_session.open_fd(fd) def _set_username(self, cred): - ignore = cred + ignore = cred # pragma: no cover def _set_password(self, cred): self._spice_session.set_property("password", cred) fd = self._get_fd_for_open() if fd is not None: self._spice_session.open_fd(fd) else: - self._spice_session.connect() + self._spice_session.connect() # pragma: no cover def _get_scaling(self): if self._display: @@ -709,17 +712,17 @@ class SpiceViewer(Viewer): def _get_resizeguest(self): if self._display: return self._display.get_property("resize-guest") - return False + return False # pragma: no cover def _usbdev_redirect_error(self, spice_usbdev_widget, spice_usb_device, - errstr): + errstr): # pragma: no cover ignore = spice_usbdev_widget ignore = spice_usb_device self.emit("usb-redirect-error", errstr) def _get_usb_widget(self): if not self._spice_session: - return + return # pragma: no cover usbwidget = SpiceClientGtk.UsbDeviceWidget.new(self._spice_session, None) @@ -728,7 +731,7 @@ class SpiceViewer(Viewer): def _has_usb_redirection(self): if not self._spice_session or not self._usbdev_manager: - return False + return False # pragma: no cover for c in self._spice_session.get_channels(): if c.__class__ is SpiceClientGLib.UsbredirChannel: diff --git a/virtManager/lib/testmock.py b/virtManager/lib/testmock.py index 6b4ed0d1..928ad9a3 100644 --- a/virtManager/lib/testmock.py +++ b/virtManager/lib/testmock.py @@ -108,6 +108,10 @@ class CLITestOptionsClass: if we are doing firstrun testing * fake-systemd-success: If doing firstrun testing, fake that systemd checks for libvirtd succeeded + * fake-vnc-username: Fake VNC username auth request + * fake-console-resolution: Fake viewer console resolution response. + Spice doesn't return values here when we are just testing + against seabios in uitests, this fakes it to hit more code paths """ def __init__(self, test_options_str): optset = set() @@ -143,6 +147,8 @@ class CLITestOptionsClass: self.spice_agent = _get("spice-agent") self.firstrun_uri = _get_value("firstrun-uri") self.fake_systemd_success = _get("fake-systemd-success") + self.fake_vnc_username = _get("fake-vnc-username") + self.fake_console_resolution = _get("fake-console-resolution") if optset: # pragma: no cover raise RuntimeError("Unknown --test-options keys: %s" % optset)