[PATCH 11/11] Add the beginnings of an automated GUI test suite.

Vratislav Podzimek vpodzime at redhat.com
Tue Apr 8 07:43:53 UTC 2014


On Mon, 2014-04-07 at 14:03 -0400, Chris Lumens wrote:
> This is enough to make sure all the basic stuff works on a standard livecd.
> The documentation throughout should be enough to explain it all, but here's
> a few quick comments:
> 
> * Everything in the inside/ directory runs inside a VM.
> 
> * Most things in the outside/ directory run outside of and control a VM.
> outside/template.py is a notable exception to this.  It is a template file of
> python code and gets run inside the VM as suite.py.
> 
> * runtest.sh runs all the outside/ tests against a livecd previously created
> with make_livecd.sh.
> ---
>  tests/gui/anaconda-autogui-testing.ks  |   8 +-
>  tests/gui/inside/__init__.py           | 182 +++++++++++++++++++++
>  tests/gui/inside/date_time.py          |  59 +++++++
>  tests/gui/inside/keyboard.py           | 127 +++++++++++++++
>  tests/gui/inside/network.py            |  49 ++++++
>  tests/gui/inside/progress.py           | 122 ++++++++++++++
>  tests/gui/inside/rootpassword.py       |  66 ++++++++
>  tests/gui/inside/storage.py            | 101 ++++++++++++
>  tests/gui/inside/summary.py            |  81 ++++++++++
>  tests/gui/inside/user.py               |  74 +++++++++
>  tests/gui/inside/welcome.py            |  64 ++++++++
>  tests/gui/outside/__init__.py          | 280 +++++++++++++++++++++++++++++++++
>  tests/gui/outside/template.py          |  75 +++++++++
>  tests/gui/outside/test_simplelivecd.py |  39 +++++
>  tests/gui/runtest.sh                   |  51 ++++++
>  15 files changed, 1376 insertions(+), 2 deletions(-)
>  create mode 100644 tests/gui/inside/__init__.py
>  create mode 100644 tests/gui/inside/date_time.py
>  create mode 100644 tests/gui/inside/keyboard.py
>  create mode 100644 tests/gui/inside/network.py
>  create mode 100644 tests/gui/inside/progress.py
>  create mode 100644 tests/gui/inside/rootpassword.py
>  create mode 100644 tests/gui/inside/storage.py
>  create mode 100644 tests/gui/inside/summary.py
>  create mode 100644 tests/gui/inside/user.py
>  create mode 100644 tests/gui/inside/welcome.py
>  create mode 100644 tests/gui/outside/__init__.py
>  create mode 100644 tests/gui/outside/template.py
>  create mode 100644 tests/gui/outside/test_simplelivecd.py
>  create mode 100755 tests/gui/runtest.sh
> 
> diff --git a/tests/gui/anaconda-autogui-testing.ks b/tests/gui/anaconda-autogui-testing.ks
> index 2e0dd06..35f96d9 100644
> --- a/tests/gui/anaconda-autogui-testing.ks
> +++ b/tests/gui/anaconda-autogui-testing.ks
> @@ -11,18 +11,22 @@ bootloader --location=mbr
>  zerombr
>  clearpart --all
>  part / --fstype="ext4" --size=4400
> -part swap --fstype="swap" --size=512
>  
>  %post
>  cat >> /etc/rc.d/init.d/livesys << EOF
> +# Mount the attached disk containing test suite information.
> +mkdir /mnt/anactest
> +mount -L ANACTEST /mnt/anactest
> +chown -R liveuser.liveuser /mnt/anactest
>  
>  if [ -f /usr/share/applications/anaconda.desktop ]; then
> -    # Make anaconda start automatically, not the welcome thing.
> +    # Make anaconda start automatically.
>      if [ -f ~liveuser/.config/autostart/fedora-welcome.desktop ]; then
>          rm ~liveuser/.config/autostart/fedora-welcome.desktop
>      fi
>  
>      cp /usr/share/applications/anaconda.desktop ~liveuser/.config/autostart/
> +    sed -i -e '/Exec=/ s|/usr/bin/liveinst|python /mnt/anactest/suite.py|' ~liveuser/.config/autostart/anaconda.desktop
>  
>      # Enable accessibility needed for testing.
>      cat >> /usr/share/glib-2.0/schemas/org.gnome.desktop.interface.gschema.override << FOE
> diff --git a/tests/gui/inside/__init__.py b/tests/gui/inside/__init__.py
> new file mode 100644
> index 0000000..1cbe3ed
> --- /dev/null
> +++ b/tests/gui/inside/__init__.py
> @@ -0,0 +1,182 @@
> +#!/usr/bin/python
> +#
> +# Copyright (C) 2014  Red Hat, Inc.
> +#
> +# This program is free software; you can redistribute it and/or modify
> +# it under the terms of the GNU Lesser General Public License as published
> +# by the Free Software Foundation; either version 2.1 of the License, or
> +# (at your option) any later version.
> +#
> +# This program is distributed in the hope that it will be useful,
> +# but WITHOUT ANY WARRANTY; without even the implied warranty of
> +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
> +# GNU Lesser General Public License for more details.
> +#
> +# You should have received a copy of the GNU Lesser General Public License
> +# along with this program.  If not, see <http://www.gnu.org/licenses/>.
> +#
> +# Author: Chris Lumens <clumens at redhat.com>
> +from dogtail.config import config
> +from dogtail.predicate import GenericPredicate
> +from dogtail.tree import SearchError, root
> +from dogtail.utils import doDelay, screenshot
> +
> +import glob
> +import os
> +import unittest
> +
> +class UITestCase(unittest.TestCase):
> +    """A UITestCase is a class that incorporates all the test functions for a
> +       single anaconda Hub or Spoke window.  Moving to the window, and dealing
> +       with what happens after we move away is left up to whatever wraps up
> +       all the test cases.  A single Hub or Spoke may have multiple instances
> +       of this class - one may test the assumption that everything works as it
> +       should, while another may test that a specific initial setup fails in
> +       an expected way, and another may attempt fuzz testing on all entries on
> +       the screen.  However, a single TestSuite will only have one instance
> +       for each anaconda window.
> +
> +       Some basic assumptions about the existence of UI elements are made in
> +       UITestCase subclasses.  If an element is required to be there (for
> +       instance, a button we are going to click) we simply grab the element
> +       with the exception-raising dogtail functions and call click on it.
> +       If the element does not exist, the exception will be propagated up and
> +       fail the test.  If we are testing for the existence of some element
> +       (for instance, that a dialog was displayed after a button was clicked)
> +       then we use UITestCase.find and the unittest.assert* functions.
> +
> +       This is kind of subtle, but look at existing test cases for examples.
> +
> +       Due to the unusual nature of running such a large program as anaconda
> +       in a test suite, combined with the special environment, tests are
> +       organized inside a UITestCase as follows:
> +
> +       * Testing one little piece of the window is handled by a check_*
> +         function.  These are explicitly not named test_* as that is special
> +         to unittest.
> +
> +       * Multiple check_* functions are called linearly in the _run method,
> +         which is used instead of runTest for error handling purposes.
> +    """
> +
> +    ###
> +    ### OVERRIDES OF THINGS FROM TestCase
> +    ###
> +
> +    def runTest(self):
> +        """A version of TestCase.runTest that attempts to take a screenshot of
> +           anaconda should a test fail.  Subclasses should not override this.
> +           See the documentation for _run.
> +        """
> +        config.load({"scratchDir": "/mnt/anactest/result/"})
> +
> +        try:
> +            self._run()
> +        except (AssertionError, SearchError):
> +            # Try to take a screenshot of whatever screen anaconda's on, so
> +            # we can attempt to figure out what went wrong.
> +            screenshot()
> +            raise
> +
> +    def _run(self):
> +        """Do all the tests for this test case.  This is like the TestCase.runTest
> +           method, but we've overridden that to do more specialized error handling.
> +           Thus, all testing should be done in this method.
> +        """
> +        pass
> +
> +    def setUp(self):
> +        self.ana = root.application("anaconda")
> +
> +    ###
> +    ### METHODS FOR FINDING WIDGETS
> +    ###
> +
> +    def find(self, name, roleName=None):
> +        """Wrap findChild, returning None if no widget is found instead of
> +           raising an exception.  This method also allows for checking if
> +           anaconda has hit a traceback and if so, fails the test
> +           immediately.
> +        """
> +        if len(glob.glob("/tmp/anaconda-tb-*")) > 0:
> +            self.fail("anaconda encountered a traceback")
> +
> +        try:
> +            return self.ana.findChild(name=name, roleName=roleName)
> +        except SearchError:
> +            return None
> +
> +    ###
> +    ### METHODS FOR CHECKING A SINGLE WIDGET
> +    ###
> +
> +    def check_window_displayed(self, name):
> +        """Verify that a window (such as a hub or spoke) given by the
> +           provided name is currently displayed on the screen.  If not,
> +           the current test case will be failed.
> +        """
> +        w = self.find(name)
> +        self.assertIsNotNone(w, msg="%s not found" % name)
> +        self.assertTrue(w.showing, msg="%s is not displayed" % name)
> +
> +    def check_dialog_displayed(self, name):
> +        """Verify that a dialog given by the provided name is currently
> +           displayed on the screen.  If not, the current test case will
> +           be failed.
> +        """
> +        w = self.find(name, "dialog")
> +        self.assertIsNotNone(w, msg="%s not found" % name)
> +        self.assertTrue(w.showing, msg="%s is not displayed" % name)
> +
> +    def check_keyboard_layout_indicator(self, layout):
> +        """Verify that the keyboard layout indicator is present and that
> +           the currently enabled layout is what we expect.  If not, the
> +           current test case will be failed.
> +        """
> +        indicator = self.find("Keyboard Layout")
> +        self.assertIsNotNone(indicator, msg="keyboard layout indicator not found")
> +        self.assertEqual(indicator.description, layout,
> +                         msg="keyboard layout indicator not set to %s" % layout)
> +
> +    def check_no_warning_bar(self):
> +        """Verify that the warning bar is not currently displayed."""
> +        self.assertIsNone(self.find("Warning"), msg="Warning bar should not be displayed")
> +
> +    def check_warning_bar(self, msg=None):
> +        """Verify that the warning bar is currently displayed.  If msg is given,
> +           verify that it is contained in whatever message the warning bar is
> +           showing.
> +        """
> +        bar = self.find("Warning")
> +        self.assertTrue(bar.showing, msg="Warning bar should be displayed")
> +        if msg:
> +            self.assertIn(msg, bar.child(roleName="label").text)
> +
> +    def click_button(self, name):
> +        """Verify that a button with the given name exists and is sensitive,
> +           and then click it.
> +        """
> +        b = self.find(name, "button")
> +        self.assertIsNotNone(b, msg="%s button not found" % name)
> +        self.assertTrue(b.sensitive, msg="%s button should be sensitive" % name)
> +        b.click()
> +
> +    def enter_spoke(self, spokeSelectorName):
> +        """Click on the spoke selector for the given spoke, then wait a moment
> +           to make sure it has appeared.
> +        """
> +        selector = self.find(spokeSelectorName, "spoke selector")
> +        self.assertIsNotNone(selector, msg="Selector %s not found" % spokeSelectorName)
> +        selector.click()
> +
> +    def exit_spoke(self, hubName="INSTALLATION SUMMARY"):
> +        """Leave a spoke by clicking the Done button in the upper left corner,
> +           then verify we have returned to the proper hub.  Since most spokes
> +           are off the summary hub, that's the default.  If we are not back
> +           on the hub, the current test case will be failed.
> +        """
> +        button = self.find("Done", "button")
> +        self.assertIsNotNone(button, msg="Done button not found")
> +        button.click()
> +        doDelay(5)
I'm afraid this may be not enough. I've definitely seen the grey screen
instead of hub for more than 5 seconds on my VMs.

> +        self.check_window_displayed(hubName)
> diff --git a/tests/gui/inside/date_time.py b/tests/gui/inside/date_time.py
> new file mode 100644
> index 0000000..3b4a6ff
> --- /dev/null
> +++ b/tests/gui/inside/date_time.py
> @@ -0,0 +1,59 @@
> +# Copyright (C) 2014  Red Hat, Inc.
> +#
> +# This program is free software; you can redistribute it and/or modify
> +# it under the terms of the GNU Lesser General Public License as published
> +# by the Free Software Foundation; either version 2.1 of the License, or
> +# (at your option) any later version.
> +#
> +# This program is distributed in the hope that it will be useful,
> +# but WITHOUT ANY WARRANTY; without even the implied warranty of
> +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
> +# GNU Lesser General Public License for more details.
> +#
> +# You should have received a copy of the GNU Lesser General Public License
> +# along with this program.  If not, see <http://www.gnu.org/licenses/>.
> +#
> +# Author: Chris Lumens <clumens at redhat.com>
> +
> +from . import UITestCase
> +
> +class LiveCDDateTimeTestCase(UITestCase):
> +    def check_region_city(self):
> +        # FIXME:  This encodes default information.
> +        entry = self.find("Region", "textentry")
> +        self.assertIsNotNone(entry, "Region entry does not exist")
> +        self.assertEqual(entry.text, "Americas", msg="Region should be set to default")
> +
> +        entry = self.find("City", "textentry")
> +        self.assertIsNotNone(entry, "City entry does not exist")
> +        self.assertEqual(entry.text, "New York", msg="City should be set to default")
> +
> +    def check_ntp(self):
> +        # NTP should be enabled given that we started up with networking.
> +        # FIXME:  This encodes default information.
> +        button = self.find("Use Network Time", "button")
> +        self.assertIsNotNone(button, msg="Use Network Time button not found")
> +        self.assertTrue(button.checked, msg="NTP should be enabled")
> +
> +        button = self.find("Configure NTP", "button")
> +        self.assertIsNotNone(button, msg="Configure NTP button not found")
> +        self.assertTrue(button.sensitive, msg="Configure NTP button should be sensitive")
> +
> +        button = self.find("Set Date & Time", "button")
> +        self.assertIsNotNone(button, msg="Set Date & Time button not found")
> +        self.assertFalse(button.sensitive, msg="Date & Time region should not be sensitive")
> +
> +    def _run(self):
> +        # First, we need to click on the network spoke selector.
> +        self.enter_spoke("DATE & TIME")
> +
> +        # Now verify we are on the right screen.
> +        self.check_window_displayed("DATE & TIME")
> +
> +        # And now we can check everything else on the screen.
> +        self.check_region_city()
> +        self.check_ntp()
> +
> +        # And then we click the Done button to go back to the hub, verifying
> +        # that's where we ended up.
> +        self.exit_spoke()
> diff --git a/tests/gui/inside/keyboard.py b/tests/gui/inside/keyboard.py
> new file mode 100644
> index 0000000..a3a7a6b
> --- /dev/null
> +++ b/tests/gui/inside/keyboard.py
> @@ -0,0 +1,127 @@
> +# Copyright (C) 2014  Red Hat, Inc.
> +#
> +# This program is free software; you can redistribute it and/or modify
> +# it under the terms of the GNU Lesser General Public License as published
> +# by the Free Software Foundation; either version 2.1 of the License, or
> +# (at your option) any later version.
> +#
> +# This program is distributed in the hope that it will be useful,
> +# but WITHOUT ANY WARRANTY; without even the implied warranty of
> +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
> +# GNU Lesser General Public License for more details.
> +#
> +# You should have received a copy of the GNU Lesser General Public License
> +# along with this program.  If not, see <http://www.gnu.org/licenses/>.
> +#
> +# Author: Chris Lumens <clumens at redhat.com>
> +
> +from dogtail.predicate import GenericPredicate
> +
> +from . import UITestCase
> +
> +class BasicKeyboardTestCase(UITestCase):
> +    def _get_enabled_layouts(self, view):
> +        return [child.text for child in view.findChildren(GenericPredicate(roleName="table cell"))]
> +
> +    def check_options_dialog(self):
> +        # Click the options button.
> +        self.click_button("Layout Options")
> +        self.check_dialog_displayed("Layout Options")
> +        self.click_button("Cancel")
> +
> +    def check_num_layouts(self):
> +        # There ought to be only one layout enabled right now.
> +        # FIXME:  This encodes default information.
> +        view = self.find("Selected Layouts")
> +        self.assertIsNotNone(view, "Selected Layouts view not found")
> +        self.assertEqual(len(self._get_enabled_layouts(view)), 1, msg="An unexpected number of keyboard layouts are enabled")
> +
> +    def check_layout_buttons_initial(self):
> +        button = self.find("Add layout", "button")
> +        self.assertIsNotNone(button, msg="Add layout button not found")
> +        self.assertTrue(button.sensitive, msg="Add layout button should be sensitive")
> +
> +        # When no layouts are selected in the view, none of these buttons mean anything.
> +        button = self.find("Remove layout", "button")
> +        self.assertIsNotNone(button, msg="Remove layout button not found")
> +        self.assertFalse(button.sensitive, msg="Remove layout button should not be sensitive")
> +
> +        button = self.find("Move selected layout up", "button")
> +        self.assertIsNotNone(button, msg="Move layout up button not found")
> +        self.assertFalse(button.sensitive, msg="Move layout up button should not be sensitive")
> +
> +        button = self.find("Move selected layout down", "button")
> +        self.assertIsNotNone(button, msg="Move layout down button not found")
> +        self.assertFalse(button.sensitive, msg="Move layout down button should not be sensitive")
> +
> +        button = self.find("Preview layout", "button")
> +        self.assertIsNotNone(button, msg="Preview layout button not found")
> +        self.assertFalse(button.sensitive, msg="Preview layout button should not be sensitive")
> +
> +    def check_add_layout_dialog(self):
> +        self.click_button("Add layout")
> +        self.check_dialog_displayed("Add layout")
> +        self.click_button("Cancel")
> +
> +    def check_layout_buttons_after_click(self):
> +        # Click on the first (and only) layout shown in the view.  This
> +        # ensures buttons change sensitivity.
> +        # FIXME:  This encodes default information.
> +        view = self.find("Selected Layouts")
> +        self.assertIsNotNone(view, msg="Selected Layouts view not found")
> +        view.children[1].click()
> +
> +        button = self.find("Add layout", "button")
> +        self.assertIsNotNone(button, msg="Add layout button not found")
> +        self.assertTrue(button.sensitive, msg="Add layout button should be sensitive")
> +
> +        button = self.find("Remove layout", "button")
> +        self.assertIsNotNone(button, msg="Remove layout button not found")
> +        self.assertTrue(button.sensitive, msg="Remove layout button should be sensitive")
> +
> +        # These two should still not be sensitive - we've only got one layout.
> +        # We ensured that with check_num_layouts.
> +        # FIXME:  This encodes default information.
> +        button = self.find("Move selected layout up", "button")
> +        self.assertIsNotNone(button, msg="Move layout up button not found")
> +        self.assertFalse(button.sensitive, msg="Move layout up button should not be sensitive")
> +
> +        button = self.find("Move selected layout down", "button")
> +        self.assertIsNotNone(button, msg="Move layout down button not found")
> +        self.assertFalse(button.sensitive, msg="Move layout down button should not be sensitive")
> +
> +        button = self.find("Preview layout", "button")
> +        self.assertIsNotNone(button, msg="Preview layout button not found")
> +        self.assertTrue(button.sensitive, msg="Preview layout button should be sensitive")
> +
> +    def check_preview_dialog(self):
> +        self.click_button("Preview layout")
> +
> +        # Verify the preview dialog is displayed.  The dialog out to be titled
> +        # with the name of the current layout.  We happen to know that - it's
> +        # the default.
> +        # FIXME:  This encodes default information.
> +        self.check_dialog_displayed("English (US)")
> +
> +        self.click_button("Close")
> +
> +    def _run(self):
> +        # First, we need to click on the network spoke selector.
> +        self.enter_spoke("KEYBOARD")
> +
> +        # Now verify we are on the right screen.
> +        self.check_window_displayed("KEYBOARD")
> +
> +        # And now we can check everything else on the screen.
> +        self.check_options_dialog()
> +        self.check_num_layouts()
> +        self.check_layout_buttons_initial()
> +        self.check_add_layout_dialog()
> +
> +        # Once a layout has been selected in the view, we can test these other buttons.
> +        self.check_layout_buttons_after_click()
> +        self.check_preview_dialog()
> +
> +        # And then we click the Done button to go back to the hub, verifying
> +        # that's where we ended up.
> +        self.exit_spoke()
> diff --git a/tests/gui/inside/network.py b/tests/gui/inside/network.py
> new file mode 100644
> index 0000000..2112c94
> --- /dev/null
> +++ b/tests/gui/inside/network.py
> @@ -0,0 +1,49 @@
> +# Copyright (C) 2014  Red Hat, Inc.
> +#
> +# This program is free software; you can redistribute it and/or modify
> +# it under the terms of the GNU Lesser General Public License as published
> +# by the Free Software Foundation; either version 2.1 of the License, or
> +# (at your option) any later version.
> +#
> +# This program is distributed in the hope that it will be useful,
> +# but WITHOUT ANY WARRANTY; without even the implied warranty of
> +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
> +# GNU Lesser General Public License for more details.
> +#
> +# You should have received a copy of the GNU Lesser General Public License
> +# along with this program.  If not, see <http://www.gnu.org/licenses/>.
> +#
> +# Author: Chris Lumens <clumens at redhat.com>
> +
> +from . import UITestCase
> +
> +class LiveCDNetworkTestCase(UITestCase):
> +    def check_hostname_entry(self):
> +        # Only the live install hint and hostname box should be visible.
> +        self.assertTrue(self.ana.childLabelled("Please use the live desktop environment's tools for customizing your network configuration.  You can set the hostname here.").showing)
> +
> +        box = self.find("Network Config Box")
> +        self.assertIsNotNone(box, "Network Config box not found")
> +        self.assertFalse(box.showing, msg="Network Config box should not be displayed")
> +
> +        box = self.find("More Network Config Box")
> +        self.assertIsNotNone(box, "More Network Config box not found")
> +        self.assertFalse(box.showing, msg="More Network Config box should not be displayed")
> +
> +        entry = self.find("Hostname", "textentry")
> +        self.assertIsNotNone(entry , "Hostname entry not found")
> +        self.assertTrue(entry.showing, msg="Hostname entry should be displayed")
> +
> +    def _run(self):
> +        # First, we need to click on the network spoke selector.
> +        self.enter_spoke("NETWORK & HOSTNAME")
> +
> +        # Now verify we are on the right screen.
> +        self.check_window_displayed("NETWORK & HOSTNAME")
> +
> +        # And now we can check everything else on the screen.
> +        self.check_hostname_entry()
> +
> +        # And then we click the Done button to go back to the hub, verifying
> +        # that's where we ended up.
> +        self.exit_spoke()
> diff --git a/tests/gui/inside/progress.py b/tests/gui/inside/progress.py
> new file mode 100644
> index 0000000..8f48b4b
> --- /dev/null
> +++ b/tests/gui/inside/progress.py
> @@ -0,0 +1,122 @@
> +# Copyright (C) 2014  Red Hat, Inc.
> +#
> +# This program is free software; you can redistribute it and/or modify
> +# it under the terms of the GNU Lesser General Public License as published
> +# by the Free Software Foundation; either version 2.1 of the License, or
> +# (at your option) any later version.
> +#
> +# This program is distributed in the hope that it will be useful,
> +# but WITHOUT ANY WARRANTY; without even the implied warranty of
> +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
> +# GNU Lesser General Public License for more details.
> +#
> +# You should have received a copy of the GNU Lesser General Public License
> +# along with this program.  If not, see <http://www.gnu.org/licenses/>.
> +#
> +# Author: Chris Lumens <clumens at redhat.com>
> +
> +import signal
> +
> +from dogtail.predicate import GenericPredicate
> +from dogtail.utils import doDelay
> +from . import UITestCase
> +
> +class LiveCDProgressTestCase(UITestCase):
> +    def check_begin_installation_button(self):
> +        button = self.find("Begin Installation", "button")
> +        self.assertIsNotNone(button, msg="Begin Installation button does not exist")
> +        self.assertTrue(button.sensitive, msg="Begin Installation button should be sensitive")
> +
> +    def check_shown_spoke_selectors(self):
> +        # FIXME:  This forces English.
> +        validSelectors = ["ROOT PASSWORD", "USER CREATION"]
> +        selectors = self.ana.findChildren(GenericPredicate(roleName="spoke selector"))
> +
> +        self.assertEqual(len(selectors), len(validSelectors), msg="Incorrect number of spoke selectors shown")
> +
> +        # Validate that only the spoke selectors we expect are shown.  At the same time,
> +        # we can also validate the status of each selector.  This only validates the
> +        # initial state of everything.  Once we start clicking on spokes, things are
> +        # going to change.
> +        # FIXME:  This encodes default information.
> +        for selector in selectors:
> +            if selector.name == "ROOT PASSWORD":
> +                self.assertEqual(selector.description, "Root password is not set")
> +            elif selector.name == "USER CREATION":
> +                self.assertEqual(selector.description, "No user will be created")
> +            else:
> +                self.fail("Invalid spoke selector shown on livecd: %s" % selector.name)
> +
> +    def _timer_expired(self):
This should take signum and frame arguments.
(https://docs.python.org/2/library/signal.html#signal.signal)

> +        self.fail("anaconda did not finish in 30 minutes")
> +
> +    def _run(self):
> +        # Before doing anything, verify we are still on the summary hub.
> +        self.check_window_displayed("INSTALLATION SUMMARY")
> +        # All spokes should have been visited and satisfied now.
> +        self.check_no_warning_bar()
> +        self.check_begin_installation_button()
> +
> +        # Click the begin installation button, wait a moment, and now we should
> +        # be on the progress hub.
> +        self.click_button("Begin Installation")
> +        self.check_window_displayed("CONFIGURATION")
> +
> +        self.check_shown_spoke_selectors()
> +        self.check_warning_bar()
> +
> +        # Now we need to wait for installation to finish.  We're doing that two ways:
> +        # (1) Set a 30 minute timeout.  Should we hit that, anaconda's clearly not
> +        #     going to finish (or is hung or something) and we should fail.
> +        # (2) Poll the UI waiting for the "Complete!" message to pop up.  Once
> +        #     that happens, we know it's time to test the user settings stuff and
> +        #     the end of the UI.
> +        signal.signal(signal.SIGALRM, self._timer_expired)
> +        signal.alarm(30*60)
> +
> +        while True:
> +            label = self.find("Complete!")
> +            if label:
> +                signal.alarm(0)
> +                break
> +
> +            doDelay(20)
> +
> +        # If we got here, installation completed successfully.  Since we've not
> +        # done a password or created a user yet, we still have to do that.  The
> +        # finish configuration button should still be insensitive.
> +        button = self.find("Finish configuration", "button")
> +        self.assertIsNotNone(button, msg="Finish configuration button not found")
> +        self.assertFalse(button.sensitive, msg="Finish Configuration button should not be sensitive")
> +
> +class LiveCDFinishTestCase(UITestCase):
> +    def check_finish_config_button(self):
> +        # Click the Finish Configuration button.
> +        self.click_button("Finish configuration")
> +
> +    def _timer_expired(self):
Same here.

> +        self.fail("anaconda did not finish in 5 minutes")
> +
> +    def _run(self):
> +        # Before doing anything, verify we are still on the progress hub.
> +        self.check_window_displayed("CONFIGURATION")
> +
> +        self.check_finish_config_button()
> +        # We've completed configuration, so the warning bar should be gone.
> +        self.check_no_warning_bar()
> +
> +        # And now we wait for configuration to finish.  Then we check the
> +        # reboot button, but don't click it.  The end of the test case shuts
> +        # down the system for us.
> +        signal.signal(signal.SIGALRM, self._timer_expired)
> +        signal.alarm(5*60)
> +
> +        while True:
> +            button = self.find("Quit", "button")
> +            if button:
> +                signal.alarm(0)
> +                break
> +
> +            doDelay(20)
> +
> +        self.click_button("Quit")
> diff --git a/tests/gui/inside/rootpassword.py b/tests/gui/inside/rootpassword.py
> new file mode 100644
> index 0000000..d982ab7
> --- /dev/null
> +++ b/tests/gui/inside/rootpassword.py
> @@ -0,0 +1,66 @@
> +# Copyright (C) 2014  Red Hat, Inc.
> +#
> +# This program is free software; you can redistribute it and/or modify
> +# it under the terms of the GNU Lesser General Public License as published
> +# by the Free Software Foundation; either version 2.1 of the License, or
> +# (at your option) any later version.
> +#
> +# This program is distributed in the hope that it will be useful,
> +# but WITHOUT ANY WARRANTY; without even the implied warranty of
> +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
> +# GNU Lesser General Public License for more details.
> +#
> +# You should have received a copy of the GNU Lesser General Public License
> +# along with this program.  If not, see <http://www.gnu.org/licenses/>.
> +#
> +# Author: Chris Lumens <clumens at redhat.com>
> +
> +from dogtail.utils import doDelay
> +
> +from . import UITestCase
> +
> +class BasicRootPasswordTestCase(UITestCase):
> +    def check_enter_password(self):
> +        # The warning bar starts off telling us there's no password set.
> +        self.check_warning_bar("The password is empty")
> +
> +        entry = self.find("Password", "textentry")
> +        self.assertIsNotNone(entry, msg="Password entry should be displayed")
> +        entry.grabFocus()
> +        entry.text = "asdfasdf"
> +
> +        # That wasn't a very good password and we haven't confirmed it, so the
> +        # bar is still displayed at the bottom.
> +        doDelay(1)
> +        self.check_warning_bar("it does not contain enough DIFFERENT characters")
> +
> +        # Let's confirm that terrible password.
> +        entry = self.find("Confirm Password", "textentry")
> +        self.assertIsNotNone(entry, msg="Confirm password should be displayed")
> +        entry.grabFocus()
> +        entry.text = "asdfasdf"
> +
> +        # But of course it's still a terrible password, so the bar is still
> +        # displayed at the bottom.
> +        doDelay(1)
> +        self.check_warning_bar("it does not contain enough DIFFERENT characters")
> +
> +    def check_click_done(self):
> +        # Press the Done button once, which won't take us anywhere but will change the
> +        # warning label at the bottom.
> +        self.click_button("Done")
> +        self.check_warning_bar("Press Done again")
> +
> +        # Pressing Done again should take us back to the progress hub.
> +        self.exit_spoke(hubName="CONFIGURATION")
> +
> +    def _run(self):
> +        # First, we need to click on the spoke selector.
> +        self.enter_spoke("ROOT PASSWORD")
> +
> +        # Now, verify we are on the right screen.
> +        self.check_window_displayed("ROOT PASSWORD")
> +
> +        # And now we can check everything else on the screen.
> +        self.check_enter_password()
> +        self.check_click_done()
> diff --git a/tests/gui/inside/storage.py b/tests/gui/inside/storage.py
> new file mode 100644
> index 0000000..3b20cbf
> --- /dev/null
> +++ b/tests/gui/inside/storage.py
> @@ -0,0 +1,101 @@
> +# Copyright (C) 2014  Red Hat, Inc.
> +#
> +# This program is free software; you can redistribute it and/or modify
> +# it under the terms of the GNU Lesser General Public License as published
> +# by the Free Software Foundation; either version 2.1 of the License, or
> +# (at your option) any later version.
> +#
> +# This program is distributed in the hope that it will be useful,
> +# but WITHOUT ANY WARRANTY; without even the implied warranty of
> +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
> +# GNU Lesser General Public License for more details.
> +#
> +# You should have received a copy of the GNU Lesser General Public License
> +# along with this program.  If not, see <http://www.gnu.org/licenses/>.
> +#
> +# Author: Chris Lumens <clumens at redhat.com>
> +
> +from dogtail.predicate import GenericPredicate
> +
> +from . import UITestCase
> +
> +class BasicStorageTestCase(UITestCase):
> +    def check_select_disks(self):
> +        # FIXME:  This is a really roundabout way of determining whether a disk is
> +        # selected or not.  For some reason when a DiskOverview is selected, its icon
> +        # loses the name "Hard Disk".  For now, we can use this to check.
> +        def _selected(do):
> +            return len(do.findChildren(GenericPredicate(name="Hard Disk"))) == 0
> +
> +        # A real disk is any disk attached to the VM that's not the special 10 MB
> +        # one used for communicating in and out of the VM.  Therefore, scan all
> +        # children of the DiskOverview looking for one whose name contains that
> +        # size.
> +        def _real_disk(do):
> +            for child in do.findChildren(GenericPredicate()):
> +                if child.name.startswith("10."):
> +                    return False
> +
> +            return True
> +
> +        # There should be some disks displayed on the screen.
> +        overviews = self.ana.findChildren(GenericPredicate(roleName="disk overview"))
> +        self.assertGreater(len(overviews), 0, msg="No disks are displayed")
> +
> +        if len(overviews) == 1:
> +            # Only one disk was given to this test case, so anaconda selected it
> +            # by default.  Verify.  Not sure how this would happen with how
> +            # testing is currently done, but it's good to be prepared.
> +            self.assertTrue(_selected(overviews[0]))
> +        else:
> +            # More than one disk was provided, so anaconda did not select any.
> +            # Let's select all disks and proceed.
> +            for overview in filter(_real_disk, overviews):
> +                self.assertFalse(_selected(overview))
> +                overview.click()
> +                self.assertTrue(_selected(overview))
> +
> +    def check_shopping_cart(self):
> +        pass
> +
> +    def check_storage_options(self):
> +        button = self.find("Automatically configure partitioning.", "button")
> +        self.assertIsNotNone(button, msg="Autopart button not found")
> +        self.assertTrue(button.checked, msg="Autopart should be selected")
> +
> +        button = self.find("I would lke to make additional space available.", "button")
TYPO:                                  ^^ like

> +        self.assertIsNotNone(button, msg="Reclaim button not found")
> +        self.assertFalse(button.checked, msg="Reclaim button should not be selected")
> +
> +        button = self.find("Encrypt my data.", "button")
> +        self.assertIsNotNone(button, msg="Encrypt button not found")
> +        self.assertFalse(button.checked, msg="Encrypt button should not be selected")
> +
> +    def _run(self):
> +        # First, we need to click on the network spoke selector.
> +        self.enter_spoke("INSTALLATION DESTINATION")
> +
> +        # Now verify we are on the right screen.
> +        self.check_window_displayed("INSTALLATION DESTINATION")
> +
> +        # Given that we attach a second disk to the system (for storing the test
> +        # suite and results), anaconda will not select disks by default.  Thus,
> +        # the storage options panel should currently be insensitive.
> +        button = self.find("Storage Options", "button")
> +        self.assertIsNotNone(button, "Storage Options button not found")
> +        self.assertFalse(button.sensitive, msg="Storage options should be insensitive")
> +
> +        # Select disk overviews.  In the basic case, this means uninitialized
> +        # disks that we're going to do autopart on.
> +        self.check_select_disks()
> +
> +        # And now with disks selected, the storage options should be sensitive.
> +        self.assertTrue(button.sensitive, msg="Storage options should be sensitive")
> +
> +        self.check_shopping_cart()
> +        self.check_storage_options()
> +
> +        # And then we click the Done button which should take the user right back to
> +        # the hub.  There's no need to display any other dialogs given that this is
> +        # an install against empty disks and no other options were checked.
> +        self.exit_spoke()
> diff --git a/tests/gui/inside/summary.py b/tests/gui/inside/summary.py
> new file mode 100644
> index 0000000..98988cf
> --- /dev/null
> +++ b/tests/gui/inside/summary.py
> @@ -0,0 +1,81 @@
> +# Copyright (C) 2014  Red Hat, Inc.
> +#
> +# This program is free software; you can redistribute it and/or modify
> +# it under the terms of the GNU Lesser General Public License as published
> +# by the Free Software Foundation; either version 2.1 of the License, or
> +# (at your option) any later version.
> +#
> +# This program is distributed in the hope that it will be useful,
> +# but WITHOUT ANY WARRANTY; without even the implied warranty of
> +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
> +# GNU Lesser General Public License for more details.
> +#
> +# You should have received a copy of the GNU Lesser General Public License
> +# along with this program.  If not, see <http://www.gnu.org/licenses/>.
> +#
> +# Author: Chris Lumens <clumens at redhat.com>
> +from dogtail.predicate import GenericPredicate
> +from dogtail.utils import doDelay
> +
> +from . import UITestCase
> +
> +# This test case handles the livecd case on the summary hub where everything
> +# works as intended.  On this spoke, we are testing the following:
> +#
> +# * Clicking the Quit button brings up a dialog asking if you're sure, though
> +#   we're not going to test that confirming actually quits.
> +# * The Begin Installation button is insensitive, since no disks have yet
> +#   been selected.
> +# * Only the Date & Time, Keyboard, Installation Destination, and Network Config
> +#   spoke selectors are visible.
> +
> +class LiveCDSummaryTestCase(UITestCase):
> +    def check_quit_button(self):
> +        self.click_button("Quit")
> +        self.check_dialog_displayed("Quit")
> +        self.click_button("No")
> +
> +    def check_begin_installation_button(self):
> +        button = self.find("Begin Installation", "button")
> +        self.assertIsNotNone(button, msg="Begin Installation button not found")
> +        self.assertTrue(button.showing, msg="Begin Installation button should be displayed")
> +        self.assertFalse(button.sensitive, msg="Begin Installation button should not be sensitive")
> +
> +    def check_shown_spoke_selectors(self):
> +        # FIXME:  This forces English.
> +        validSelectors = ["DATE & TIME", "KEYBOARD", "INSTALLATION DESTINATION", "NETWORK & HOSTNAME"]
How does those ampersands work with the &amp; entities used in the
previous patches?

> +        selectors = self.ana.findChildren(GenericPredicate(roleName="spoke selector"))
> +
> +        self.assertEqual(len(selectors), len(validSelectors), msg="Incorrect number of spoke selectors shown")
> +
> +        # Validate that only the spoke selectors we expect are shown.  At the same time,
> +        # we can also validate the status of each selector.  This only validates the
> +        # initial state of everything.  Once we start clicking on spokes, things are
> +        # going to change.
> +        # FIXME:  This encodes default information.
> +        for selector in selectors:
> +            if selector.name == "DATE & TIME":
> +                self.assertEqual(selector.description, "Americas/New York timezone")
> +            elif selector.name == "KEYBOARD":
> +                self.assertEqual(selector.description, "English (US)")
> +            elif selector.name == "INSTALLATION DESTINATION":
> +                # We don't know how many disks are going to be involved - if there's
> +                # just one, anaconda selects it by default.  If there's more than
> +                # one, it selects none.
> +                self.assertIn(selector.description, ["Automatic partitioning selected",
> +                                                     "No disks selected"])
> +            elif selector.name == "NETWORK & HOSTNAME":
> +                self.assertRegexpMatches(selector.description, "Wired (.+) connected")
> +            else:
> +                self.fail("Invalid spoke selector shown on livecd: %s" % selector.name)
> +
> +    def _run(self):
> +        # Before doing anything, verify we are on the right screen.
> +        doDelay(5)
> +        self.check_window_displayed("INSTALLATION SUMMARY")
> +
> +        # And now we can check everything else on the screen.
> +        self.check_quit_button()
> +        self.check_begin_installation_button()
> +        self.check_shown_spoke_selectors()
> +        self.check_warning_bar()
> diff --git a/tests/gui/inside/user.py b/tests/gui/inside/user.py
> new file mode 100644
> index 0000000..70f2744
> --- /dev/null
> +++ b/tests/gui/inside/user.py
> @@ -0,0 +1,74 @@
> +# Copyright (C) 2014  Red Hat, Inc.
> +#
> +# This program is free software; you can redistribute it and/or modify
> +# it under the terms of the GNU Lesser General Public License as published
> +# by the Free Software Foundation; either version 2.1 of the License, or
> +# (at your option) any later version.
> +#
> +# This program is distributed in the hope that it will be useful,
> +# but WITHOUT ANY WARRANTY; without even the implied warranty of
> +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
> +# GNU Lesser General Public License for more details.
> +#
> +# You should have received a copy of the GNU Lesser General Public License
> +# along with this program.  If not, see <http://www.gnu.org/licenses/>.
> +#
> +# Author: Chris Lumens <clumens at redhat.com>
> +
> +from . import UITestCase
> +
> +class BasicUserTestCase(UITestCase):
> +    def check_enter_name(self):
> +        entry = self.find("Full Name", "textentry")
> +        self.assertIsNotNone(entry, msg="Full name entry not found")
> +        entry.grabFocus()
> +        entry.text = "Bobby Good User"
> +
> +        # We base the username on the full name entered
> +        entry = self.find("Username", "textentry")
> +        self.assertIsNotNone(entry, msg="Username entry not found")
> +        self.assertEqual(entry.text, "buser", msg="Generated username does not match expectation")
> +
> +    def check_enter_password(self):
> +        # The warning bar starts off telling us there's no password set.
> +        self.check_warning_bar("The password is empty")
> +
> +        entry = self.find("Password", "textentry")
> +        self.assertIsNotNone(entry, msg="Password entry should be displayed")
> +        entry.grabFocus()
> +        entry.text = "asdfasdf"
> +
> +        # That wasn't a very good password and we haven't confirmed it, so the
> +        # bar is still displayed at the bottom.
> +        self.check_warning_bar("it does not contain enough DIFFERENT characters")
> +
> +        # Let's confirm that terrible password.
> +        entry = self.find("Confirm Password", "textentry")
> +        self.assertIsNotNone(entry, msg="Confirm password should be displayed")
> +        entry.grabFocus()
> +        entry.text = "asdfasdf"
> +
> +        # But of course it's still a terrible password, so the bar is still
> +        # displayed at the bottom.
> +        self.check_warning_bar("it does not contain enough DIFFERENT characters")
> +
> +    def check_click_done(self):
> +        # Press the Done button once, which won't take us anywhere but will change the
> +        # warning label at the bottom.
> +        self.click_button("Done")
> +        self.check_warning_bar("Press Done again")
> +
> +        # Pressing Done again should take us back to the progress hub.
> +        self.exit_spoke(hubName="CONFIGURATION")
> +
> +    def _run(self):
> +        # First, we need to click on the spoke selector.
> +        self.enter_spoke("USER CREATION")
> +
> +        # Now, verify we are on the right screen.
> +        self.check_window_displayed("CREATE USER")
> +
> +        # And now we can check everything else on the screen.
> +        self.check_enter_name()
> +        self.check_enter_password()
> +        self.check_click_done()
> diff --git a/tests/gui/inside/welcome.py b/tests/gui/inside/welcome.py
> new file mode 100644
> index 0000000..bbe7185
> --- /dev/null
> +++ b/tests/gui/inside/welcome.py
> @@ -0,0 +1,64 @@
> +# Copyright (C) 2014  Red Hat, Inc.
> +#
> +# This program is free software; you can redistribute it and/or modify
> +# it under the terms of the GNU Lesser General Public License as published
> +# by the Free Software Foundation; either version 2.1 of the License, or
> +# (at your option) any later version.
> +#
> +# This program is distributed in the hope that it will be useful,
> +# but WITHOUT ANY WARRANTY; without even the implied warranty of
> +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
> +# GNU Lesser General Public License for more details.
> +#
> +# You should have received a copy of the GNU Lesser General Public License
> +# along with this program.  If not, see <http://www.gnu.org/licenses/>.
> +#
> +# Author: Chris Lumens <clumens at redhat.com>
> +
> +from . import UITestCase
> +
> +# This test case handles the basic case on the welcome language spoke where
> +# everything works as intended.  On this spoke, we are testing the following:
> +#
> +#   * The default language and locale are selected and displayed at the top
> +#     of their views.
> +#   * Clicking the Quit button brings up a dialog asking if you're sure, though
> +#     we're not going to test that confirming actually quits.
> +#   * Clicking the Continue button brings up the betanag dialog, though we're
> +#     not going to test the quit button there either.
> +#
> +# TODO:
> +#   * Entering text into the search box should result in narrowing down the
> +#     contents of the left hand view.
> +
> +class BasicWelcomeTestCase(UITestCase):
> +    def check_lang_locale_views(self):
> +        # FIXME:  This encodes default information.
> +        lang = "English"
> +        locale = "English (United States)"
> +
> +        return
> +#        self.assertEqual(ldtp.verifytablecell("frmWelcome", "tblLanguages", 0, 0, lang), 1,
> +#                         msg="Default language (%s) is not selected" % lang)
> +#        self.assertEqual(ldtp.verifytablecell("frmWelcome", "tblLocales", 0, 0, locale), 1,
> +#                         msg="Default locale (%s) is not selected")
> +
> +    def check_quit_button(self):
> +        self.click_button("Quit")
> +        self.check_dialog_displayed("Quit")
> +        self.click_button("No")
> +
> +    def check_continue_button(self):
> +        self.click_button("Continue")
> +        self.check_dialog_displayed("Beta Warn")
> +        self.click_button("I accept my fate.")
> +
> +    def _run(self):
> +        # Before doing anything, verify we are on the right screen.
> +        self.check_window_displayed("WELCOME")
> +
> +        # And now we can check everything else on the screen.
> +        self.check_keyboard_layout_indicator("us")
> +        self.check_lang_locale_views()
> +        self.check_quit_button()
> +        self.check_continue_button()
> diff --git a/tests/gui/outside/__init__.py b/tests/gui/outside/__init__.py
> new file mode 100644
> index 0000000..6d5265b
> --- /dev/null
> +++ b/tests/gui/outside/__init__.py
> @@ -0,0 +1,280 @@
> +#!/usr/bin/python
> +#
> +# Copyright (C) 2014  Red Hat, Inc.
> +#
> +# This program is free software; you can redistribute it and/or modify
> +# it under the terms of the GNU Lesser General Public License as published
> +# by the Free Software Foundation; either version 2.1 of the License, or
> +# (at your option) any later version.
> +#
> +# This program is distributed in the hope that it will be useful,
> +# but WITHOUT ANY WARRANTY; without even the implied warranty of
> +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
> +# GNU Lesser General Public License for more details.
> +#
> +# You should have received a copy of the GNU Lesser General Public License
> +# along with this program.  If not, see <http://www.gnu.org/licenses/>.
> +#
> +# Author: Chris Lumens <clumens at redhat.com>
> +
> +__all__ = ["Creator", "OutsideMixin"]
> +
> +# Can't import from a directory with a dash in its name.  This will have to do.
> +execfile("/usr/share/doc/python-blivet/examples/common.py")
> +
> +import blivet
> +
> +from contextlib import contextmanager
> +import os
> +import shutil
> +import subprocess
> +import tempfile
> +import unittest
> +
> +class Creator(object):
> +    """A Creator subclass defines all the parameters for making a VM to run a
> +       test against as well as handles creating and running that VM, inspecting
> +       results, and managing temporary data.
> +
> +       Most Creator subclasses will only need to define the following four
> +       attributes:
> +
> +       drives       -- A list of tuples describing disk images to create.  Each
> +                       tuple is the name and size, in GB.
> +       name         -- A unique string that names a Creator.  This name will
> +                       be used in creating the results directory (and perhaps
> +                       other places in the future) so make sure it doesn't
> +                       conflict with another object.
> +       reqMemory    -- The amount of memory this VM needs, in MB.  The default
> +                       is 2048, which seems to be the minimum required to
> +                       make things run quickly.  Redefine if you need more.
> +       tests        -- A list of tuples describing which test cases make up
> +                       this test.  Each tuple is the name of the module
> +                       containing the test case (minus the leading "inside."
> +                       and the name of the test case class.  Tests will be
> +                       run in the order provided.
> +    """
> +    drives = []
> +    name = "Creator"
> +    reqMemory = 2048
> +    tests = []
> +
> +    def __init__(self):
> +        self._drivePaths = {}
> +        self._mountpoint = None
> +        self._proc = None
> +        self._tempdir = None
> +
> +    def archive(self):
> +        """Copy all log files and other test results to a subdirectory of the
> +           given resultsdir.  If logs are no longer available, this method
> +           does nothing.  It is up to the caller to make sure logs are available
> +           beforehand and clean up afterwards.
> +        """
> +        from testconfig import config
> +
> +        if not os.path.ismount(self.mountpoint):
> +            return
> +
> +        shutil.copytree(self.mountpoint + "/result", config["resultsdir"] + "/" + self.name)
> +
> +    def cleanup(self):
> +        """Remove all disk images used during this test case and the temporary
> +           directory they were stored in.
> +        """
> +        shutil.rmtree(self.tempdir, ignore_errors=True)
> +        os.unlink(self._drivePaths["suite"])
> +
> +    def die(self):
> +        """Kill any running qemu process previously started by this test."""
> +        if self._proc:
> +            self._proc.kill()
> +            self._proc = None
> +
> +    def makeDrives(self):
> +        """Create all hard drive images associated with this test.  Images
> +           must be listed in Creator.drives and will be stored in a temporary
> +           directory this method creates.  It is up to the caller to remove
> +           everything later by calling Creator.cleanup.
> +        """
> +        for (drive, size) in self.drives:
> +            (fd, diskimage) = tempfile.mkstemp(dir=self.tempdir)
> +            os.close(fd)
> +
> +            subprocess.call(["/usr/bin/qemu-img", "create", "-f", "qcow2", diskimage, "%sG" % size],
> +                            stdout=open("/dev/null", "w"))
> +            self._drivePaths[drive] = diskimage
> +
> +    @property
> +    def template(self):
> +        with open("outside/template.py", "r") as f:
> +            return f.read()
> +
> +    def makeSuite(self):
> +        """The suite is a small disk image attached to every test VM automatically
> +           by the test framework.  It includes all the inside/ stuff, a special
> +           suite.py file that will be automatically run by the live CD (and is
> +           what actually runs the test), and a directory structure for reporting
> +           results.
> +
> +           It is mounted under Creator.mountpoint as needed.
> +
> +           This method creates the suite image and adds it to the internal list of
> +           images associated with this test.
> +
> +           Note that because this image is attached to the VM, anaconda will always
> +           see two hard drives and thus will never automatically select disks.
> +           Note also that this means tests must be careful to not select this
> +           disk.
> +        """
> +        from testconfig import config
> +
> +        # First, create a disk image and put a filesystem on it.
> +        b = blivet.Blivet()
> +
> +        # pylint: disable-msg=E0602
> +        disk1_path = create_sparse_file(b, "suite", blivet.size.Size(spec="11Mb"))
I guess it means the same in blivet, but this should probably be 11 MB^^

> +        b.config.diskImages["suite"] = disk1_path
> +
> +        b.reset()
> +
> +        try:
> +            disk1 = b.devicetree.getDeviceByName("suite")
> +            b.initializeDisk(disk1)
> +
> +            part = b.newPartition(size=blivet.size.Size(spec="10Mb"), parents=[disk1])
Same on this line.

-- 
Vratislav Podzimek

Anaconda Rider | Red Hat, Inc. | Brno - Czech Republic



More information about the anaconda-patches mailing list