./ 0000755 0000041 0000041 00000000000 13061552435 011247 5 ustar www-data www-data ./autopilot/ 0000755 0000041 0000041 00000000000 13061552435 013267 5 ustar www-data www-data ./autopilot/display/ 0000755 0000041 0000041 00000000000 13061552443 014733 5 ustar www-data www-data ./autopilot/display/_screenshot.py 0000644 0000041 0000041 00000011355 13061552435 017627 0 ustar www-data www-data # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Autopilot Functional Test Tool
# Copyright (C) 2014 Canonical
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
"""This module contains support for capturing screenshots."""
import logging
import os
import subprocess
import time
import tempfile
from io import BytesIO
from PIL import Image
import autopilot._glib
logger = logging.getLogger(__name__)
def get_screenshot_data(display_type):
"""Return a BytesIO object of the png data for the screenshot image.
*display_type* is the display server type. supported values are:
- "X11"
- "MIR"
:raises RuntimeError: If attempting to capture an image on an unsupported
display server.
:raises RuntimeError: If saving image data to file-object fails.
"""
if display_type == "MIR":
return _get_screenshot_mir()
elif display_type == "X11":
return _get_screenshot_x11()
else:
raise RuntimeError(
"Don't know how to take screen shot for this display server: {}"
.format(display_type)
)
def _get_screenshot_x11():
"""Capture screenshot from an X11 display.
:raises RuntimeError: If saving pixbuf to fileobject fails.
"""
pixbuf_data = _get_x11_pixbuf_data()
return _save_gdk_pixbuf_to_fileobject(pixbuf_data)
def _get_x11_pixbuf_data():
Gdk = autopilot._glib._import_gdk()
window = Gdk.get_default_root_window()
x, y, width, height = window.get_geometry()
return Gdk.pixbuf_get_from_window(window, x, y, width, height)
def _save_gdk_pixbuf_to_fileobject(pixbuf):
image_data = pixbuf.save_to_bufferv("png", [], [])
if image_data[0] is True:
image_datafile = BytesIO()
image_datafile.write(image_data[1])
image_datafile.seek(0)
return image_datafile
logger.error("Unable to write image data.")
raise RuntimeError("Failed to save image data to file object.")
def _get_screenshot_mir():
"""Capture screenshot from Mir display.
:raises FileNotFoundError: If the mirscreencast utility is not found.
:raises CalledProcessError: If the mirscreencast utility errors while
taking a screenshot.
:raises ValueError: If the PNG conversion step fails.
"""
from autopilot.display import Display
display_resolution = Display.create().get_screen_geometry(0)[2:]
screenshot_filepath = _take_mirscreencast_screenshot()
try:
png_data_file = _get_png_from_rgba_file(
screenshot_filepath,
display_resolution
)
finally:
os.remove(screenshot_filepath)
return png_data_file
def _take_mirscreencast_screenshot():
"""Takes a single frame capture of the screen using mirscreencast.
Return the path to the resulting rgba file.
:raises FileNotFoundError: If the mirscreencast utility is not found.
:raises CalledProcessError: If the mirscreencast utility errors while
taking a screenshot.
"""
timestamp = int(time.time())
filename = "ap-screenshot-data-{ts}.rgba".format(ts=timestamp)
filepath = os.path.join(tempfile.gettempdir(), filename)
try:
subprocess.check_call([
"mirscreencast",
"-m", "/run/mir_socket",
"-n", "1",
"-f", filepath
])
except FileNotFoundError as e:
e.args += ("The utility 'mirscreencast' is not available.", )
raise
except subprocess.CalledProcessError as e:
e.args += ("Failed to take screenshot.", )
raise
return filepath
# This currently uses PIL but I'm investigating using something
# quicker/lighter-weight.
def _get_png_from_rgba_file(filepath, image_size):
"""Convert an rgba file to a png file stored in a filelike object.
Returns a BytesIO object containing the png data.
"""
image_data = _image_data_from_file(filepath, image_size)
bio = BytesIO()
image_data.save(bio, format="png")
bio.seek(0)
return bio
def _image_data_from_file(filepath, image_size):
with open(filepath, "rb") as f:
image_data = Image.frombuffer(
"RGBA", image_size, f.read(), "raw", "RGBA", 0, 1
)
return image_data
./autopilot/display/_upa.py 0000644 0000041 0000041 00000006250 13061552435 016235 0 ustar www-data www-data # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Autopilot Functional Test Tool
# Copyright (C) 2012-2013 Canonical
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
import os
import subprocess
from autopilot.display import Display as DisplayBase
from autopilot.platform import get_display_server, image_codename
DISPLAY_SERVER_X11 = 'X11'
DISPLAY_SERVER_MIR = 'MIR'
ENV_MIR_SOCKET = 'MIR_SERVER_HOST_SOCKET'
def query_resolution():
display_server = get_display_server()
if display_server == DISPLAY_SERVER_X11:
return _get_resolution_from_xrandr()
elif display_server == DISPLAY_SERVER_MIR:
return _get_resolution_from_mirout()
else:
return _get_hardcoded_resolution()
def _get_hardcoded_resolution():
name = image_codename()
resolutions = {
"Aquaris_M10_HD": (800, 1280),
"Desktop": (1920, 1080)
}
if name not in resolutions:
raise NotImplementedError(
'Device "{}" is not supported by Autopilot.'.format(name))
return resolutions[name]
def _get_stdout_for_command(command, *args):
full_command = [command]
full_command.extend(args)
return subprocess.check_output(
full_command,
universal_newlines=True,
stderr=subprocess.DEVNULL,
).split('\n')
def _get_resolution(server_output):
relevant_line = list(filter(lambda line: '*' in line, server_output))[0]
if relevant_line:
return tuple([int(i) for i in relevant_line.split()[0].split('x')])
raise ValueError(
'Failed to get display resolution, is a display connected?'
)
def _get_resolution_from_xrandr():
return _get_resolution(_get_stdout_for_command('xrandr', '--current'))
def _get_resolution_from_mirout():
return _get_resolution(
_get_stdout_for_command('mirout', os.environ.get(ENV_MIR_SOCKET))
)
class Display(DisplayBase):
"""The base class/inteface for the display devices"""
def __init__(self):
super(Display, self).__init__()
self._X, self._Y = query_resolution()
def get_num_screens(self):
"""Get the number of screens attached to the PC."""
return 1
def get_primary_screen(self):
"""Returns an integer of which screen is considered the primary"""
return 0
def get_screen_width(self):
return self._X
def get_screen_height(self):
return self._Y
def get_screen_geometry(self, screen_number):
"""Get the geometry for a particular screen.
:return: Tuple containing (x, y, width, height).
"""
return 0, 0, self._X, self._Y
./autopilot/display/__init__.py 0000644 0000041 0000041 00000011227 13061552443 017047 0 ustar www-data www-data # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Autopilot Functional Test Tool
# Copyright (C) 2012-2013 Canonical
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
"""The display module contaions support for getting screen information."""
from collections import OrderedDict
from autopilot.utilities import _pick_backend
from autopilot.input import Mouse
from autopilot.display._screenshot import get_screenshot_data
__all__ = [
"Display",
"get_screenshot_data",
"is_rect_on_screen",
"is_point_on_screen",
"is_point_on_any_screen",
"move_mouse_to_screen",
]
def is_rect_on_screen(screen_number, rect):
"""Return True if *rect* is **entirely** on the specified screen, with no
overlap."""
x, y, w, h = rect
mx, my, mw, mh = Display.create().get_screen_geometry(screen_number)
return x >= mx and x + w <= mx + mw and y >= my and y + h <= my + mh
def is_point_on_screen(screen_number, point):
"""Return True if *point* is on the specified screen.
*point* must be an iterable type with two elements: (x, y)
"""
x, y = point
mx, my, mw, mh = Display.create().get_screen_geometry(screen_number)
return mx <= x < mx + mw and my <= y < my + mh
def is_point_on_any_screen(point):
"""Return true if *point* is on any currently configured screen."""
return any([is_point_on_screen(m, point) for m in
range(Display.create().get_num_screens())])
def move_mouse_to_screen(screen_number):
"""Move the mouse to the center of the specified screen."""
geo = Display.create().get_screen_geometry(screen_number)
x = geo[0] + (geo[2] / 2)
y = geo[1] + (geo[3] / 2)
# dont animate this or it might not get there due to barriers
Mouse.create().move(x, y, False)
class Display(object):
"""The base class/inteface for the display devices."""
@staticmethod
def create(preferred_backend=''):
"""Get an instance of the Display class.
For more infomration on picking specific backends, see
:ref:`tut-picking-backends`
:param preferred_backend: A string containing a hint as to which
backend you would like.
possible backends are:
* ``X11`` - Get display information from X11.
* ``UPA`` - Get display information from the ubuntu platform API.
:raises: RuntimeError if autopilot cannot instantate any of the
possible backends.
:raises: RuntimeError if the preferred_backend is specified and is not
one of the possible backends for this device class.
:raises: :class:`~autopilot.BackendException` if the preferred_backend
is set, but that backend could not be instantiated.
:returns: Instance of Display with appropriate backend.
"""
def get_x11_display():
from autopilot.display._X11 import Display
return Display()
def get_upa_display():
from autopilot.display._upa import Display
return Display()
backends = OrderedDict()
backends['X11'] = get_x11_display
backends['UPA'] = get_upa_display
return _pick_backend(backends, preferred_backend)
class BlacklistedDriverError(RuntimeError):
"""Cannot set primary monitor when running drivers listed in the
driver blacklist."""
def get_num_screens(self):
"""Get the number of screens attached to the PC."""
raise NotImplementedError("You cannot use this class directly.")
def get_primary_screen(self):
raise NotImplementedError("You cannot use this class directly.")
def get_screen_width(self, screen_number=0):
raise NotImplementedError("You cannot use this class directly.")
def get_screen_height(self, screen_number=0):
raise NotImplementedError("You cannot use this class directly.")
def get_screen_geometry(self, monitor_number):
"""Get the geometry for a particular monitor.
:return: Tuple containing (x, y, width, height).
"""
raise NotImplementedError("You cannot use this class directly.")
./autopilot/display/_X11.py 0000644 0000041 0000041 00000005006 13061552435 016017 0 ustar www-data www-data # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Autopilot Functional Test Tool
# Copyright (C) 2012-2013 Canonical
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
import autopilot._glib
from autopilot.display import Display as DisplayBase
class Display(DisplayBase):
def __init__(self):
# Note: MUST import these here, rather than at the top of the file.
# Why? Because sphinx imports these modules to build the API
# documentation, which in turn tries to import Gdk, which in turn
# fails because there's no DISPLAY environment set in the package
# builder.
Gdk = autopilot._glib._import_gdk()
self._default_screen = Gdk.Screen.get_default()
if self._default_screen is None:
raise RuntimeError(
"Unable to determine default screen information")
self._blacklisted_drivers = ["NVIDIA"]
def get_num_screens(self):
"""Get the number of screens attached to the PC.
:returns: int indicating number of screens attached.
"""
return self._default_screen.get_n_monitors()
def get_primary_screen(self):
"""Return an integer of which screen is considered the primary."""
return self._default_screen.get_primary_monitor()
def get_screen_width(self, screen_number=0):
return self.get_screen_geometry(screen_number)[2]
def get_screen_height(self, screen_number=0):
return self.get_screen_geometry(screen_number)[3]
def get_screen_geometry(self, screen_number):
"""Get the geometry for a particular screen.
:return: Tuple containing (x, y, width, height).
"""
if screen_number < 0 or screen_number >= self.get_num_screens():
raise ValueError('Specified screen number is out of range.')
rect = self._default_screen.get_monitor_geometry(screen_number)
return (rect.x, rect.y, rect.width, rect.height)
./autopilot/_timeout.py 0000644 0000041 0000041 00000006660 13061552435 015476 0 ustar www-data www-data # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Autopilot Functional Test Tool
# Copyright (C) 2014 Canonical
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
"""Autopilot timeout functions.
Autopilot frequently needs to repeatedly run code within a certain timeout
period. Historically this was done with a simple ``for`` loop and a ``sleep``
statement, like this::
for i in range(10):
# do some polling code here
time.sleep(1)
The problem with this approach is that we hard-code both the absolute timeout
value (10 seconds in this case), as well as the time to sleep after each poll.
When Autopilot runs on certain platforms, we need to globally increase the
timeout period. We'd also like to be able to avoid the common pitfall of
forgetting to call ``time.sleep``.
Finally, we support mocking out the ``sleep`` call, so autopilot tests can
run quickly and verify the polling behavior of low-level function calls.
"""
from autopilot.utilities import sleep
from autopilot.globals import (
get_default_timeout_period,
get_long_timeout_period,
)
class Timeout(object):
"""Class for starting different timeout loops.
This class contains two static methods. Each method is a generator, and
provides a timeout for a different period of time. For example, to
generate a short polling timeout, the code would look like this::
for elapsed_time in Timeout.default():
# polling code goes here
the ``elapsed_time`` variable will contain the amount of time elapsed, in
seconds, since the beginning of the loop, although this is not guaranteed
to be accurate.
"""
@staticmethod
def default():
"""Start a polling loop with the default timeout.
This is the polling loop that should be used by default (hence the
name) unless the operation is known to take a very long time,
especially on slow or virtualised hardware.
"""
timeout = float(get_default_timeout_period())
# Once we only support py3.3, replace this with
# yield from _do_timeout(timeout)
for i in _do_timeout(timeout):
yield i
@staticmethod
def long():
"""Start a polling loop with a long timeout.
This is the polling loop that should be used for operations that are
known to take extra long on slow, or virtualised hardware.
"""
timeout = float(get_long_timeout_period())
# Once we only support py3.3, replace this with
# yield from _do_timeout(timeout)
for i in _do_timeout(timeout):
yield i
def _do_timeout(timeout):
time_elapsed = 0.0
while timeout - time_elapsed > 0.0:
yield time_elapsed
time_to_sleep = min(timeout - time_elapsed, 1.0)
sleep(time_to_sleep)
time_elapsed += time_to_sleep
yield time_elapsed
./autopilot/exceptions.py 0000644 0000041 0000041 00000006753 13061552435 016035 0 ustar www-data www-data # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Autopilot Functional Test Tool
# Copyright (C) 2013 Canonical
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
"""Autopilot Exceptions.
This module contains exceptions that autopilot may raise in various conditions.
Each exception is documented with when it is raised: a generic description in
this module, as well as a detailed description in the function or method that
raises it.
"""
class BackendException(RuntimeError):
"""An error occured while trying to initialise an autopilot backend."""
def __init__(self, original_exception):
super(BackendException, self).__init__(
"Error while initialising backend. Original exception was: " +
str(original_exception))
self.original_exception = original_exception
class ProcessSearchError(RuntimeError):
"""Object introspection error occured."""
pass
class StateNotFoundError(RuntimeError):
"""Raised when a piece of state information is not found.
This exception is commonly raised when the application has destroyed (or
not yet created) the object you are trying to access in autopilot. This
typically happens for a number of possible reasons:
* The UI widget you are trying to access with
:py:meth:`~autopilot.introspection.ProxyBase.select_single` or
:py:meth:`~autopilot.introspection.ProxyBase.wait_select_single` or
:py:meth:`~autopilot.introspection.ProxyBase.select_many` does not exist
yet.
* The UI widget you are trying to access has been destroyed by the
application.
"""
def __init__(self, class_name=None, **filters):
"""Construct a StateNotFoundError.
:raises ValueError: if neither the class name not keyword arguments
are specified.
"""
if class_name is None and not filters:
raise ValueError("Must specify either class name or filters.")
if class_name is None:
self._message = \
"Object not found with properties {}.".format(
repr(filters)
)
elif not filters:
self._message = "Object not found with name '{}'.".format(
class_name
)
else:
self._message = \
"Object not found with name '{}' and properties {}.".format(
class_name,
repr(filters)
)
_troubleshoot_url_message = (
'Tips on minimizing the occurrence of this failure '
'are available here: '
'https://developer.ubuntu.com/api/autopilot/python/1.6.0/'
'faq-troubleshooting/'
)
def __str__(self):
return '{}\n\n{}'.format(
self._message,
self._troubleshoot_url_message
)
class InvalidXPathQuery(ValueError):
"""Raised when an XPathselect query is invalid or unsupported."""
./autopilot/run.py 0000644 0000041 0000041 00000062615 13061552435 014457 0 ustar www-data www-data # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Autopilot Functional Test Tool
# Copyright (C) 2012-2014 Canonical
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
from argparse import ArgumentParser, Action, REMAINDER
from codecs import open
from collections import OrderedDict
import cProfile
from datetime import datetime
from imp import find_module
import logging
import os
import os.path
from platform import node
from random import shuffle
import subprocess
import sys
from unittest import TestLoader, TestSuite
from testtools import iterate_tests
from autopilot import get_version_string, have_vis
import autopilot.globals
from autopilot import _config as test_config
from autopilot._debug import (
get_all_debug_profiles,
get_default_debug_profile,
)
from autopilot import _video
from autopilot.testresult import get_default_format, get_output_formats
from autopilot.utilities import DebugLogFilter, LogFormatter
from autopilot.application._launcher import (
_get_app_env_from_string_hint,
get_application_launcher_wrapper,
launch_process,
)
def _get_parser():
"""Return a parser object for handling command line arguments."""
common_arguments = ArgumentParser(add_help=False)
common_arguments.add_argument(
'--enable-profile', required=False, default=False,
action="store_true", help="Enable collection of profile data for "
"autopilot itself. If enabled, profile data will be stored in "
"'autopilot_.profile' in the current working directory."
)
parser = ArgumentParser(
description="Autopilot test tool.",
epilog="Each command (run, list, launch etc.) has additional help that"
" can be viewed by passing the '-h' flag to the command. For "
"example: 'autopilot run -h' displays further help for the "
"'run' command."
)
parser.add_argument('-v', '--version', action='version',
version=get_version_string(),
help="Display autopilot version and exit.")
subparsers = parser.add_subparsers(help='Run modes', dest="mode")
parser_run = subparsers.add_parser(
'run', help="Run autopilot tests", parents=[common_arguments]
)
parser_run.add_argument('-o', "--output", required=False,
help='Write test result report to file.\
Defaults to stdout.\
If given a directory instead of a file will \
write to a file in that directory named: \
_.log')
available_formats = get_output_formats().keys()
parser_run.add_argument('-f', "--format", choices=available_formats,
default=get_default_format(),
required=False,
help='Specify desired output format. \
Default is "text".')
parser_run.add_argument("-ff", "--failfast", action='store_true',
required=False, default=False,
help="Stop the test run on the first error \
or failure.")
parser_run.add_argument('-r', '--record', action='store_true',
default=False, required=False,
help="Record failing tests. Required \
'recordmydesktop' app to be installed.\
Videos are stored in /tmp/autopilot if not \
specified with -rd option.")
parser_run.add_argument("-rd", "--record-directory", required=False,
type=str, help="Directory to put recorded tests")
parser_run.add_argument("--record-options", required=False,
type=str, help="Comma separated list of options \
to pass to recordmydesktop")
parser_run.add_argument("-ro", "--random-order", action='store_true',
required=False, default=False,
help="Run the tests in random order")
parser_run.add_argument(
'-v', '--verbose', default=False, required=False, action='count',
help="If set, autopilot will output test log data to stderr during a "
"test run. Set twice (i.e. -vv) to also log debug level messages. "
"(This can be useful for debugging autopilot itself.)")
parser_run.add_argument(
"--debug-profile",
choices=[p.name for p in get_all_debug_profiles()],
default=get_default_debug_profile().name,
help="Select a profile for what additional debugging information "
"should be attached to failed test results."
)
parser_run.add_argument(
"--timeout-profile",
choices=['normal', 'long'],
default='normal',
help="Alter the timeout values Autopilot uses. Selecting 'long' will "
"make autopilot use longer timeouts for various polling loops. This "
"can be useful if autopilot is running on very slow hardware"
)
parser_run.add_argument(
"-c", "--config", default="", help="If set, specifies configuration "
"for the test suite being run. Value should be a comma-separated list "
"of values, where each value is either of the form 'key', or "
"'key=value'.", dest="test_config"
)
parser_run.add_argument(
"--test-timeout", default=0, type=int, help="If set, autopilot will "
"attempt to abort tests that have run longer than "
"seconds. This is not guaranteed to succeed - several scenarios exist "
"which make it impossible to abort a test case. Tests aborted will "
"raise a 'TimeoutException' error."
)
parser_run.add_argument("suite", nargs="+",
help="Specify test suite(s) to run.")
parser_list = subparsers.add_parser(
'list', help="List autopilot tests", parents=[common_arguments]
)
parser_list.add_argument(
"-ro", "--run-order", required=False, default=False,
action="store_true",
help="List tests in run order, rather than alphabetical order (the "
"default).")
parser_list.add_argument(
"--suites", required=False, action='store_true',
help="Lists only available suites, not tests contained within the "
"suite.")
parser_list.add_argument("suite", nargs="+",
help="Specify test suite(s) to run.")
if have_vis():
parser_vis = subparsers.add_parser(
'vis', help="Open the Autopilot visualiser tool",
parents=[common_arguments]
)
parser_vis.add_argument(
'-v', '--verbose', required=False, default=False, action='count',
help="Show autopilot log messages. Set twice to also log data "
"useful for debugging autopilot itself.")
parser_vis.add_argument(
'-testability', required=False, default=False,
action='store_true', help="Start the vis tool in testability "
"mode. Used for self-tests only."
)
parser_launch = subparsers.add_parser(
'launch', help="Launch an application with introspection enabled",
parents=[common_arguments]
)
parser_launch.add_argument(
'-i', '--interface', choices=('Gtk', 'Qt', 'Auto'), default='Auto',
help="Specify which introspection interface to load. The default"
"('Auto') uses ldd to try and detect which interface to load.")
parser_launch.add_argument(
'-v', '--verbose', required=False, default=False, action='count',
help="Show autopilot log messages. Set twice to also log data useful "
"for debugging autopilot itself.")
parser_launch.add_argument(
'application', action=_OneOrMoreArgumentStoreAction, type=str,
nargs=REMAINDER,
help="The application to launch. Can be a full path, or just an "
"application name (in which case Autopilot will search for it in "
"$PATH).")
return parser
def _parse_arguments(argv=None):
"""Parse command-line arguments, and return an argparse arguments
object.
"""
parser = _get_parser()
args = parser.parse_args(args=argv)
# TR - 2013-11-27 - a bug in python3.3 means argparse doesn't fail
# correctly when no commands are specified.
# http://bugs.python.org/issue16308
if args.mode is None:
parser.error("too few arguments")
if 'suite' in args:
args.suite = [suite.rstrip('/') for suite in args.suite]
return args
class _OneOrMoreArgumentStoreAction(Action):
def __call__(self, parser, namespace, values, option_string=None):
if len(values) == 0:
parser.error(
"Must specify at least one argument to the 'launch' command")
setattr(namespace, self.dest, values)
def setup_logging(verbose):
"""Configure the root logger and verbose logging to stderr."""
root_logger = get_root_logger()
root_logger.setLevel(logging.INFO)
if verbose == 0:
set_null_log_handler(root_logger)
if verbose >= 1:
autopilot.globals.set_log_verbose(True)
set_stderr_stream_handler(root_logger)
if verbose >= 2:
root_logger.setLevel(logging.DEBUG)
enable_debug_log_messages()
def log_autopilot_version():
root_logger = get_root_logger()
root_logger.info(get_version_string())
def get_root_logger():
return logging.getLogger()
def set_null_log_handler(root_logger):
root_logger.addHandler(logging.NullHandler())
def set_stderr_stream_handler(root_logger):
formatter = LogFormatter()
stderr_handler = logging.StreamHandler(stream=sys.stderr)
stderr_handler.setFormatter(formatter)
root_logger.addHandler(stderr_handler)
def enable_debug_log_messages():
DebugLogFilter.debug_log_enabled = True
def construct_test_result(args):
formats = get_output_formats()
return formats[args.format](
stream=get_output_stream(args.format, args.output),
failfast=args.failfast,
)
def get_output_stream(format, path):
"""Get an output stream pointing to 'path' that's appropriate for format
'format'.
:param format: A string that describes one of the output formats supported
by autopilot.
:param path: A path to the file you wish to write, or None to write to
stdout.
"""
if path:
log_file = _get_log_file_path(path)
if format == 'xml':
return _get_text_mode_file_stream(log_file)
elif format == 'text':
return _get_binary_mode_file_stream(log_file)
else:
return _get_raw_binary_mode_file_stream(log_file)
else:
return sys.stdout
def _get_text_mode_file_stream(log_file):
return open(
log_file,
'w',
encoding='utf-8',
)
def _get_binary_mode_file_stream(log_file):
return open(
log_file,
'w',
encoding='utf-8',
errors='backslashreplace'
)
def _get_raw_binary_mode_file_stream(log_file):
return open(
log_file,
'wb'
)
def _get_log_file_path(requested_path):
dirname = os.path.dirname(requested_path)
if dirname != '' and not os.path.exists(dirname):
os.makedirs(dirname)
if os.path.isdir(requested_path):
return _get_default_log_filename(requested_path)
return requested_path
def _get_default_log_filename(target_directory):
"""Return a filename that's likely to be unique to this test run."""
default_log_filename = "%s_%s.log" % (
node(),
datetime.now().strftime("%d.%m.%y-%H%M%S")
)
_print_default_log_path(default_log_filename)
log_file = os.path.join(target_directory, default_log_filename)
return log_file
def _print_default_log_path(default_log_filename):
print("Using default log filename: %s" % default_log_filename)
def get_package_location(import_name):
"""Get the on-disk location of a package from a test id name.
:raises ImportError: if the name could not be found.
:returns: path as a string
"""
top_level_pkg = import_name.split('.')[0]
_, path, _ = find_module(top_level_pkg, [os.getcwd()] + sys.path)
return os.path.abspath(
os.path.join(
path,
'..'
)
)
def _is_testing_autopilot_module(test_names):
return (
os.path.basename(sys.argv[0]) == 'autopilot'
and any(t.startswith('autopilot') for t in test_names)
)
def _reexecute_autopilot_using_module():
autopilot_command = [sys.executable, '-m', 'autopilot.run'] + sys.argv[1:]
try:
subprocess.check_call(autopilot_command)
except subprocess.CalledProcessError as e:
return e.returncode
return 0
def _discover_test(test_name):
"""Return tuple of (TestSuite of found test, top_level_dir of test).
:raises ImportError: if test_name isn't a valid module or test name
"""
loader = TestLoader()
top_level_dir = get_package_location(test_name)
# no easy way to figure out if test_name is a module or a test, so we
# try to do the discovery first=...
try:
test = loader.discover(
start_dir=test_name,
top_level_dir=top_level_dir
)
except ImportError:
# and if that fails, we try it as a test id.
test = loader.loadTestsFromName(test_name)
return (test, top_level_dir)
def _discover_requested_tests(test_names):
"""Return a collection of tests that are under test_names.
returns a tuple containig a TestSuite of tests found and a boolean
depicting wherether any difficulties were encountered while searching
(namely un-importable module names).
"""
all_tests = []
test_package_locations = []
error_occured = False
for name in test_names:
try:
test, top_level_dir = _discover_test(name)
all_tests.append(test)
test_package_locations.append(top_level_dir)
except ImportError as e:
_handle_discovery_error(name, e)
error_occured = True
_show_test_locations(test_package_locations)
return (TestSuite(all_tests), error_occured)
def _handle_discovery_error(test_name, exception):
print("could not import package %s: %s" % (test_name, str(exception)))
def _filter_tests(all_tests, test_names):
"""Filter a given TestSuite for tests starting with any name contained
within test_names.
"""
requested_tests = {}
for test in iterate_tests(all_tests):
# The test loader returns tests that start with 'unittest.loader' if
# for whatever reason the test failed to load. We run the tests without
# the built-in exception catching turned on, so we can get at the
# raised exception, which we print, so the user knows that something in
# their tests is broken.
if test.id().startswith('unittest.loader'):
test_id = test._testMethodName
try:
test.debug()
except Exception as e:
print(e)
else:
test_id = test.id()
if any([test_id.startswith(name) for name in test_names]):
requested_tests[test_id] = test
return requested_tests
def load_test_suite_from_name(test_names):
"""Return a test suite object given a dotted test names.
Returns a tuple containing the TestSuite and a boolean indicating wherever
any issues where encountered during the loading process.
"""
# The 'autopilot' program cannot be used to run the autopilot test suite,
# since setuptools needs to import 'autopilot.run', and that grabs the
# system autopilot package. After that point, the module is loaded and
# cached in sys.modules, and there's no way to unload a module in python
# once it's been loaded.
#
# The solution is to detect whether we've been started with the 'autopilot'
# application, *and* whether we're running the autopilot test suite itself,
# and ≡ that's the case, we re-call autopilot using the standard
# autopilot.run entry method, and exit with the sub-process' return code.
if _is_testing_autopilot_module(test_names):
exit(_reexecute_autopilot_using_module())
if isinstance(test_names, str):
test_names = [test_names]
elif not isinstance(test_names, list):
raise TypeError("test_names must be either a string or list, not %r"
% (type(test_names)))
all_tests, error_occured = _discover_requested_tests(test_names)
filtered_tests = _filter_tests(all_tests, test_names)
return (TestSuite(filtered_tests.values()), error_occured)
def _show_test_locations(test_directories):
"""Print the test directories tests have been loaded from."""
print("Loading tests from: %s\n" % ",".join(sorted(test_directories)))
def _configure_debug_profile(args):
for fixture_class in get_all_debug_profiles():
if args.debug_profile == fixture_class.name:
autopilot.globals.set_debug_profile_fixture(fixture_class)
break
def _configure_timeout_profile(args):
if args.timeout_profile == 'long':
autopilot.globals.set_default_timeout_period(20.0)
autopilot.globals.set_long_timeout_period(30.0)
def _configure_test_timeout(args):
autopilot.globals.set_test_timeout(args.test_timeout)
def _prepare_application_for_launch(application, interface):
app_path, app_arguments = _get_application_path_and_arguments(application)
return _prepare_launcher_environment(
interface,
app_path,
app_arguments
)
def _get_application_path_and_arguments(application):
app_name, app_arguments = _get_app_name_and_args(application)
try:
app_path = _get_applications_full_path(app_name)
except ValueError as e:
raise RuntimeError(str(e))
return app_path, app_arguments
def _get_app_name_and_args(argument_list):
"""Return a tuple of (app_name, [app_args])."""
return argument_list[0], argument_list[1:]
def _get_applications_full_path(app_name):
if not _application_name_is_full_path(app_name):
try:
app_name = subprocess.check_output(
["which", app_name],
universal_newlines=True
).strip()
except subprocess.CalledProcessError:
raise ValueError(
"Cannot find application '%s'" % (app_name)
)
return app_name
def _application_name_is_full_path(app_name):
return os.path.isabs(app_name) or os.path.exists(app_name)
def _prepare_launcher_environment(interface, app_path, app_arguments):
launcher_env = _get_application_launcher_env(interface, app_path)
_raise_if_launcher_is_none(launcher_env, app_path)
return launcher_env.prepare_environment(app_path, app_arguments)
def _raise_if_launcher_is_none(launcher_env, app_path):
if launcher_env is None:
raise RuntimeError(
"Could not determine introspection type to use for "
"application '{app_path}'.\n"
"(Perhaps use the '-i' argument to specify an interface.)".format(
app_path=app_path
)
)
def _get_application_launcher_env(interface, application_path):
launcher_env = None
if interface == 'Auto':
launcher_env = _try_determine_launcher_env_or_raise(application_path)
else:
launcher_env = _get_app_env_from_string_hint(interface)
return launcher_env
def _try_determine_launcher_env_or_raise(app_name):
try:
return get_application_launcher_wrapper(app_name)
except RuntimeError as e:
# Re-format the runtime error to be more user friendly.
raise RuntimeError(
"Error detecting launcher: {err}\n"
"(Perhaps use the '-i' argument to specify an interface.)".format(
err=str(e)
)
)
def _print_message_and_exit_error(msg):
print(msg)
exit(1)
def _run_with_profiling(callable, output_file=None):
if output_file is None:
output_file = 'autopilot_%d.profile' % (os.getpid())
cProfile.runctx(
'callable()',
globals(),
locals(),
filename=output_file,
)
class TestProgram(object):
def __init__(self, defined_args=None):
"""Create a new TestProgram instance.
:param defined_args: If specified, must be an object representing
command line arguments, as returned by the ``_parse_arguments``
function. Passing in arguments prevents argparse from parsing
sys.argv. Used in testing.
"""
self.args = defined_args or _parse_arguments()
def run(self):
setup_logging(getattr(self.args, 'verbose', False))
log_autopilot_version()
action = None
if self.args.mode == 'list':
action = self.list_tests
elif self.args.mode == 'run':
action = self.run_tests
elif self.args.mode == 'vis':
action = self.run_vis
elif self.args.mode == 'launch':
action = self.launch_app
if action is not None:
if getattr(self.args, 'enable_profile', False):
_run_with_profiling(action)
else:
action()
def run_vis(self):
# importing this requires that DISPLAY is set. Since we don't always
# want that requirement, do the import here:
from autopilot.vis import vis_main
# XXX - in quantal, overlay scrollbars make this process consume 100%
# of the CPU. It's a known bug:
#
# bugs.launchpad.net/ubuntu/quantal/+source/qt4-x11/+bug/1005677
#
# Once that's been fixed we can remove the following line:
#
os.putenv('LIBOVERLAY_SCROLLBAR', '0')
args = ['-testability'] if self.args.testability else []
vis_main(args)
def launch_app(self):
"""Launch an application, with introspection support."""
try:
app_path, app_arguments = _prepare_application_for_launch(
self.args.application,
self.args.interface
)
launch_process(
app_path,
app_arguments,
capture_output=False
)
except RuntimeError as e:
_print_message_and_exit_error("Error: " + str(e))
def run_tests(self):
"""Run tests, using input from `args`."""
_configure_debug_profile(self.args)
_configure_timeout_profile(self.args)
_configure_test_timeout(self.args)
try:
_video.configure_video_recording(self.args)
except RuntimeError as e:
print("Error: %s" % str(e))
exit(1)
test_config.set_configuration_string(self.args.test_config)
test_suite, error_encountered = load_test_suite_from_name(
self.args.suite
)
if not test_suite.countTestCases():
raise RuntimeError('Did not find any tests')
if self.args.random_order:
shuffle(test_suite._tests)
print("Running tests in random order")
result = construct_test_result(self.args)
result.startTestRun()
try:
test_result = test_suite.run(result)
finally:
result.stopTestRun()
if not test_result.wasSuccessful() or error_encountered:
exit(1)
def list_tests(self):
"""Print a list of tests we find inside autopilot.tests."""
num_tests = 0
total_title = "tests"
test_suite, error_encountered = load_test_suite_from_name(
self.args.suite
)
if self.args.run_order:
test_list_fn = lambda: iterate_tests(test_suite)
else:
test_list_fn = lambda: sorted(iterate_tests(test_suite), key=id)
# only show test suites, not test cases. TODO: Check if this is still
# a requirement.
if self.args.suites:
suite_names = ["%s.%s" % (t.__module__, t.__class__.__name__)
for t in test_list_fn()]
unique_suite_names = list(OrderedDict.fromkeys(suite_names).keys())
num_tests = len(unique_suite_names)
total_title = "suites"
print(" %s" % ("\n ".join(unique_suite_names)))
else:
for test in test_list_fn():
has_scenarios = (hasattr(test, "scenarios")
and type(test.scenarios) is list)
if has_scenarios:
num_tests += len(test.scenarios)
print(" *%d %s" % (len(test.scenarios), test.id()))
else:
num_tests += 1
print(" " + test.id())
print("\n\n %d total %s." % (num_tests, total_title))
if error_encountered:
exit(1)
def main():
test_app = TestProgram()
try:
test_app.run()
except RuntimeError as e:
print(e)
exit(1)
if __name__ == "__main__":
main()
./autopilot/matchers/ 0000755 0000041 0000041 00000000000 13061552435 015075 5 ustar www-data www-data ./autopilot/matchers/__init__.py 0000644 0000041 0000041 00000012164 13061552435 017212 0 ustar www-data www-data # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Autopilot Functional Test Tool
# Copyright (C) 2012-2013 Canonical
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
"""Autopilot-specific testtools matchers."""
from functools import partial
from testtools.matchers import Matcher, Mismatch
from autopilot.utilities import sleep
class Eventually(Matcher):
"""Asserts that a value will eventually equal a given Matcher object.
This matcher wraps another testtools matcher object. It makes that other
matcher work with a timeout. This is necessary for several reasons:
1. Since most actions in a GUI applicaton take some time to complete, the
test may need to wait for the application to enter the expected state.
2. Since the test is running in a separate process to the application under
test, test authors cannot make any assumptions about when the
application under test will recieve CPU time to update to the expected
state.
There are two main ways of using the Eventually matcher:
**Attributes from the application**::
self.assertThat(window.maximized, Eventually(Equals(True)))
Here, ``window`` is an object generated by autopilot from the applications
state. This pattern of usage will cover 90% (or more) of the assertions in
an autopilot test. Note that any matcher can be used - either from
testtools or any custom matcher that implements the matcher API::
self.assertThat(window.height, Eventually(GreaterThan(200)))
**Callable Objects**::
self.assertThat(
autopilot.platform.model, Eventually(Equals("Galaxy Nexus")))
In this example we're using the :func:`autopilot.platform.model` function
as a callable. In this form, Eventually matches against the return value
of the callable.
This can also be used to use a regular python property inside an Eventually
matcher::
self.assertThat(lambda: self.mouse.x, Eventually(LessThan(10)))
.. note:: Using this form generally makes your tests less readable, and
should be used with great care. It also relies the test author to have
knowledge about the implementation of the object being matched against.
In this example, if ``self.mouse.x`` were ever to change to be a
regular python attribute, this test would likely break.
**Timeout**
By default timeout period is ten seconds. This can be altered by passing
the timeout keyword::
self.assertThat(foo.bar, Eventually(Equals(123), timeout=30))
.. warning:: The Eventually matcher does not work with any other matcher
that expects a callable argument (such as testtools' 'Raises' matcher)
"""
def __init__(self, matcher, **kwargs):
super(Eventually, self).__init__()
self.timeout = kwargs.pop('timeout', 10)
if kwargs:
raise ValueError(
"Unknown keyword arguments: %s" % ', '.join(kwargs.keys()))
match_fun = getattr(matcher, 'match', None)
if match_fun is None or not callable(match_fun):
raise TypeError(
"Eventually must be called with a testtools matcher argument.")
self.matcher = matcher
def match(self, value):
if callable(value):
wait_fun = partial(_callable_wait_for, value)
else:
wait_fun = getattr(value, 'wait_for', None)
if wait_fun is None or not callable(wait_fun):
raise TypeError(
"Eventually is only usable with attributes that have a "
"wait_for function or callable objects.")
try:
wait_fun(self.matcher, self.timeout)
except AssertionError as e:
return Mismatch(str(e))
return None
def __str__(self):
return "Eventually " + str(self.matcher)
def _callable_wait_for(refresh_fn, matcher, timeout):
"""Like the patched :meth:`wait_for method`, but for callable objects
instead of patched variables.
"""
time_left = timeout
while True:
new_value = refresh_fn()
mismatch = matcher.match(new_value)
if mismatch:
failure_msg = mismatch.describe()
else:
return
if time_left >= 1:
sleep(1)
time_left -= 1
else:
sleep(time_left)
break
# can't give a very descriptive message here, especially as refresh_fn
# is likely to be a lambda.
raise AssertionError(
"After %.1f seconds test failed: %s" % (timeout, failure_msg))
./autopilot/application/ 0000755 0000041 0000041 00000000000 13061552435 015572 5 ustar www-data www-data ./autopilot/application/__init__.py 0000644 0000041 0000041 00000002243 13061552435 017704 0 ustar www-data www-data # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Autopilot Functional Test Tool
# Copyright (C) 2013 Canonical
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
"""Base package for application launching and environment management."""
from autopilot.application._launcher import (
ClickApplicationLauncher,
get_application_launcher_wrapper,
NormalApplicationLauncher,
UpstartApplicationLauncher,
)
__all__ = [
'ClickApplicationLauncher',
'NormalApplicationLauncher',
'UpstartApplicationLauncher',
'get_application_launcher_wrapper',
]
./autopilot/application/_launcher.py 0000644 0000041 0000041 00000052160 13061552435 020110 0 ustar www-data www-data # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Autopilot Functional Test Tool
# Copyright (C) 2013,2017 Canonical
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
"""Base module for application launchers."""
import fixtures
from gi import require_version
try:
require_version('UbuntuAppLaunch', '3')
except ValueError:
require_version('UbuntuAppLaunch', '2')
from gi.repository import GLib, UbuntuAppLaunch
import json
import logging
import os
import psutil
import subprocess
import signal
from systemd import journal
from autopilot.utilities import safe_text_content
from autopilot._timeout import Timeout
from autopilot._fixtures import FixtureWithDirectAddDetail
from autopilot.application._environment import (
GtkApplicationEnvironment,
QtApplicationEnvironment,
)
from autopilot.introspection import (
get_proxy_object_for_existing_process,
)
_logger = logging.getLogger(__name__)
class ApplicationLauncher(FixtureWithDirectAddDetail):
"""A class that knows how to launch an application with a certain type of
introspection enabled.
:keyword case_addDetail: addDetail method to use.
:keyword proxy_base: custom proxy base class to use, defaults to None
:keyword dbus_bus: dbus bus to use, if set to something other than the
default ('session') the environment will be patched
"""
def __init__(self, case_addDetail=None, emulator_base=None,
dbus_bus='session'):
super().__init__(case_addDetail)
self.proxy_base = emulator_base
self.dbus_bus = dbus_bus
def setUp(self):
super().setUp()
if self.dbus_bus != 'session':
self.useFixture(
fixtures.EnvironmentVariable(
"DBUS_SESSION_BUS_ADDRESS",
self.dbus_bus
)
)
def launch(self, *arguments):
raise NotImplementedError("Sub-classes must implement this method.")
class UpstartApplicationLauncher(ApplicationLauncher):
"""A launcher class that launches applications with UpstartAppLaunch."""
__doc__ += ApplicationLauncher.__doc__
Timeout = object()
Failed = object()
Started = object()
Stopped = object()
def launch(self, app_id, app_uris=[]):
"""Launch an application with upstart.
This method launches an application via the ``upstart-app-launch``
library, on platforms that support it.
Usage is similar to NormalApplicationLauncher::
from autopilot.application import UpstartApplicationLauncher
launcher = UpstartApplicationLauncher()
launcher.setUp()
app_proxy = launcher.launch('gallery-app')
:param app_id: name of the application to launch
:param app_uris: list of separate application uris to launch
:raises RuntimeError: If the specified application cannot be launched.
:returns: proxy object for the launched package application
"""
if isinstance(app_uris, str):
app_uris = [app_uris]
if isinstance(app_uris, bytes):
app_uris = [app_uris.decode()]
_logger.info(
"Attempting to launch application '%s' with URIs '%s' via "
"upstart-app-launch",
app_id,
','.join(app_uris)
)
state = {}
state['loop'] = self._get_glib_loop()
state['expected_app_id'] = app_id
state['message'] = ''
UbuntuAppLaunch.observer_add_app_failed(self._on_failed, state)
UbuntuAppLaunch.observer_add_app_started(self._on_started, state)
UbuntuAppLaunch.observer_add_app_focus(self._on_started, state)
GLib.timeout_add_seconds(10.0, self._on_timeout, state)
self._launch_app(app_id, app_uris)
state['loop'].run()
UbuntuAppLaunch.observer_delete_app_failed(self._on_failed)
UbuntuAppLaunch.observer_delete_app_started(self._on_started)
UbuntuAppLaunch.observer_delete_app_focus(self._on_started)
self._maybe_add_application_cleanups(state)
self._check_status_error(
state.get('status', None),
state.get('message', '')
)
pid = self._get_pid_for_launched_app(app_id)
return self._get_proxy_object(pid)
def _get_proxy_object(self, pid):
return get_proxy_object_for_existing_process(
dbus_bus=self.dbus_bus,
emulator_base=self.proxy_base,
pid=pid
)
@staticmethod
def _on_failed(launched_app_id, failure_type, state):
if launched_app_id == state['expected_app_id']:
if failure_type == UbuntuAppLaunch.AppFailed.CRASH:
state['message'] = 'Application crashed.'
elif failure_type == UbuntuAppLaunch.AppFailed.START_FAILURE:
state['message'] = 'Application failed to start.'
state['status'] = UpstartApplicationLauncher.Failed
state['loop'].quit()
@staticmethod
def _on_started(launched_app_id, state):
if launched_app_id == state['expected_app_id']:
state['status'] = UpstartApplicationLauncher.Started
state['loop'].quit()
@staticmethod
def _on_stopped(stopped_app_id, state):
if stopped_app_id == state['expected_app_id']:
state['status'] = UpstartApplicationLauncher.Stopped
state['loop'].quit()
@staticmethod
def _on_timeout(state):
state['status'] = UpstartApplicationLauncher.Timeout
state['loop'].quit()
def _maybe_add_application_cleanups(self, state):
if state.get('status', None) == UpstartApplicationLauncher.Started:
app_id = state['expected_app_id']
self.addCleanup(self._stop_application, app_id)
self.addCleanup(self._attach_application_log, app_id)
@staticmethod
def _get_user_unit_match(app_id):
return 'ubuntu-app-launch-*-%s-*.service' % app_id
def _attach_application_log(self, app_id):
j = journal.Reader()
j.log_level(journal.LOG_INFO)
j.add_match(_SYSTEMD_USER_UNIT=self._get_user_unit_match(app_id))
log_data = ''
for i in j:
log_data += str(i) + '\n'
if len(log_data) > 0:
self.caseAddDetail('Application Log (%s)' % app_id,
safe_text_content(log_data))
def _stop_application(self, app_id):
state = {}
state['loop'] = self._get_glib_loop()
state['expected_app_id'] = app_id
UbuntuAppLaunch.observer_add_app_stop(self._on_stopped, state)
GLib.timeout_add_seconds(10.0, self._on_timeout, state)
UbuntuAppLaunch.stop_application(app_id)
state['loop'].run()
UbuntuAppLaunch.observer_delete_app_stop(self._on_stopped)
if state.get('status', None) == UpstartApplicationLauncher.Timeout:
_logger.error(
"Timed out waiting for Application with app_id '%s' to stop.",
app_id
)
@staticmethod
def _get_glib_loop():
return GLib.MainLoop()
@staticmethod
def _get_pid_for_launched_app(app_id):
return UbuntuAppLaunch.get_primary_pid(app_id)
@staticmethod
def _launch_app(app_name, app_uris):
UbuntuAppLaunch.start_application_test(app_name, app_uris)
@staticmethod
def _check_status_error(status, extra_message=''):
message_parts = []
if status == UpstartApplicationLauncher.Timeout:
message_parts.append(
"Timed out while waiting for application to launch"
)
elif status == UpstartApplicationLauncher.Failed:
message_parts.append("Application Launch Failed")
if message_parts and extra_message:
message_parts.append(extra_message)
if message_parts:
raise RuntimeError(': '.join(message_parts))
class AlreadyLaunchedUpstartLauncher(UpstartApplicationLauncher):
"""Launcher that doesn't wait for a proxy object.
This is useful when you are 're-launching' an already running application
and it's state has changed to suspended.
"""
def _get_proxy_object(self, pid):
# Don't wait for a proxy object
return None
class ClickApplicationLauncher(UpstartApplicationLauncher):
"""Fixture to manage launching a Click application."""
__doc__ += ApplicationLauncher.__doc__
def launch(self, package_id, app_name=None, app_uris=[]):
"""Launch a click package application with introspection enabled.
This method takes care of launching a click package with introspection
exabled. You probably want to use this method if your application is
packaged in a click application, or is started via upstart.
Usage is similar to NormalApplicationLauncher.launch::
from autopilot.application import ClickApplicationLauncher
launcher = ClickApplicationLauncher()
launcher.setUp()
app_proxy = launcher.launch('com.ubuntu.dropping-letters')
:param package_id: The Click package name you want to launch. For
example: ``com.ubuntu.dropping-letters``
:param app_name: Currently, only one application can be packaged in a
click package, and this parameter can be left at None. If
specified, it should be the application name you wish to launch.
:param app_uris: Parameters used to launch the click package. This
parameter will be left empty if not used.
:raises RuntimeError: If the specified package_id cannot be found in
the click package manifest.
:raises RuntimeError: If the specified app_name cannot be found within
the specified click package.
:returns: proxy object for the launched package application
"""
if isinstance(app_uris, str):
app_uris = [app_uris]
if isinstance(app_uris, bytes):
app_uris = [app_uris.decode()]
_logger.info(
"Attempting to launch click application '%s' from click package "
" '%s' and URIs '%s'",
app_name if app_name is not None else "(default)",
package_id,
','.join(app_uris)
)
app_id = _get_click_app_id(package_id, app_name)
return super().launch(app_id, app_uris)
class NormalApplicationLauncher(ApplicationLauncher):
"""Fixture to manage launching an application."""
__doc__ += ApplicationLauncher.__doc__
def launch(self, application, arguments=[], app_type=None, launch_dir=None,
capture_output=True):
"""Launch an application and return a proxy object.
Use this method to launch an application and start testing it. The
arguments passed in ``arguments`` are used as arguments to the
application to launch. Additional keyword arguments are used to control
the manner in which the application is launched.
This fixture is designed to be flexible enough to launch all supported
types of applications. Autopilot can automatically determine how to
enable introspection support for dynamically linked binary
applications. For example, to launch a binary Gtk application, a test
might start with::
from autopilot.application import NormalApplicationLauncher
launcher = NormalApplicationLauncher()
launcher.setUp()
app_proxy = launcher.launch('gedit')
For use within a testcase, use useFixture:
from autopilot.application import NormalApplicationLauncher
launcher = self.useFixture(NormalApplicationLauncher())
app_proxy = launcher.launch('gedit')
Applications can be given command line arguments by supplying an
``arguments`` argument to this method. For example, if we want to
launch ``gedit`` with a certain document loaded, we might do this::
app_proxy = launcher.launch(
'gedit', arguments=['/tmp/test-document.txt'])
... a Qt5 Qml application is launched in a similar fashion::
app_proxy = launcher.launch(
'qmlscene', arguments=['my_scene.qml'])
If you wish to launch an application that is not a dynamically linked
binary, you must specify the application type. For example, a Qt4
python application might be launched like this::
app_proxy = launcher.launch(
'my_qt_app.py', app_type='qt')
Similarly, a python/Gtk application is launched like so::
app_proxy = launcher.launch(
'my_gtk_app.py', app_type='gtk')
:param application: The application to launch. The application can be
specified as:
* A full, absolute path to an executable file.
(``/usr/bin/gedit``)
* A relative path to an executable file.
(``./build/my_app``)
* An app name, which will be searched for in $PATH (``my_app``)
:keyword arguments: If set, the list of arguments is passed to the
launched app.
:keyword app_type: If set, provides a hint to autopilot as to which
kind of introspection to enable. This is needed when the
application you wish to launch is *not* a dynamically linked
binary. Valid values are 'gtk' or 'qt'. These strings are case
insensitive.
:keyword launch_dir: If set to a directory that exists the process
will be launched from that directory.
:keyword capture_output: If set to True (the default), the process
output will be captured and attached to the test as test detail.
:return: A proxy object that represents the application. Introspection
data is retrievable via this object.
"""
_logger.info(
"Attempting to launch application '%s' with arguments '%s' as a "
"normal process",
application,
' '.join(arguments)
)
app_path = _get_application_path(application)
app_path, arguments = self._setup_environment(
app_path, app_type, arguments)
process = self._launch_application_process(
app_path, capture_output, launch_dir, arguments)
proxy_object = get_proxy_object_for_existing_process(
dbus_bus=self.dbus_bus,
emulator_base=self.proxy_base,
process=process,
pid=process.pid
)
proxy_object.set_process(process)
return proxy_object
def _setup_environment(self, app_path, app_type, arguments):
app_env = self.useFixture(
_get_application_environment(app_type, app_path)
)
return app_env.prepare_environment(
app_path,
list(arguments),
)
def _launch_application_process(self, app_path, capture_output, cwd,
arguments):
process = launch_process(
app_path,
arguments,
capture_output,
cwd=cwd,
)
self.addCleanup(self._kill_process_and_attach_logs, process, app_path)
return process
def _kill_process_and_attach_logs(self, process, app_path):
stdout, stderr, return_code = _kill_process(process)
self.caseAddDetail(
'process-return-code (%s)' % app_path,
safe_text_content(str(return_code))
)
self.caseAddDetail(
'process-stdout (%s)' % app_path,
safe_text_content(stdout)
)
self.caseAddDetail(
'process-stderr (%s)' % app_path,
safe_text_content(stderr)
)
def launch_process(application, args, capture_output=False, **kwargs):
"""Launch an autopilot-enabled process and return the process object."""
commandline = [application]
commandline.extend(args)
_logger.info("Launching process: %r", commandline)
cap_mode = None
if capture_output:
cap_mode = subprocess.PIPE
process = subprocess.Popen(
commandline,
stdin=subprocess.PIPE,
stdout=cap_mode,
stderr=cap_mode,
close_fds=True,
preexec_fn=os.setsid,
universal_newlines=True,
**kwargs
)
return process
def _get_click_app_id(package_id, app_name=None):
for pkg in _get_click_manifest():
if pkg['name'] == package_id:
if app_name is None:
# py3 dict.keys isn't indexable.
app_name = list(pkg['hooks'].keys())[0]
elif app_name not in pkg['hooks']:
raise RuntimeError(
"Application '{}' is not present within the click "
"package '{}'.".format(app_name, package_id))
return "{0}_{1}_{2}".format(package_id, app_name, pkg['version'])
raise RuntimeError(
"Unable to find package '{}' in the click manifest."
.format(package_id)
)
def _get_click_manifest():
"""Return the click package manifest as a python list."""
# get the whole click package manifest every time - it seems fast enough
# but this is a potential optimisation point for the future:
click_manifest_str = subprocess.check_output(
["click", "list", "--manifest"],
universal_newlines=True
)
return json.loads(click_manifest_str)
def _get_application_environment(app_type=None, app_path=None):
if app_type is None and app_path is None:
raise ValueError("Must specify either app_type or app_path.")
try:
if app_type is not None:
return _get_app_env_from_string_hint(app_type)
else:
return get_application_launcher_wrapper(app_path)
except (RuntimeError, ValueError) as e:
_logger.error(str(e))
raise RuntimeError(
"Autopilot could not determine the correct introspection type "
"to use. You can specify this by providing app_type."
)
def get_application_launcher_wrapper(app_path):
"""Return an instance of :class:`ApplicationLauncher` that knows how to
launch the application at 'app_path'.
"""
# TODO: this is a teeny bit hacky - we call ldd to check whether this
# application links to certain library. We're assuming that linking to
# libQt* or libGtk* means the application is introspectable. This excludes
# any non-dynamically linked executables, which we may need to fix further
# down the line.
try:
ldd_output = subprocess.check_output(
["ldd", app_path],
universal_newlines=True
).strip().lower()
except subprocess.CalledProcessError as e:
raise RuntimeError(str(e))
if 'libqtcore' in ldd_output or 'libqt5core' in ldd_output:
return QtApplicationEnvironment()
elif 'libgtk' in ldd_output:
return GtkApplicationEnvironment()
return None
def _get_application_path(application):
try:
return subprocess.check_output(
['which', application],
universal_newlines=True
).strip()
except subprocess.CalledProcessError as e:
raise ValueError(
"Unable to find path for application {app}: {reason}"
.format(app=application, reason=str(e))
)
def _get_app_env_from_string_hint(hint):
lower_hint = hint.lower()
if lower_hint == 'qt':
return QtApplicationEnvironment()
elif lower_hint == 'gtk':
return GtkApplicationEnvironment()
raise ValueError("Unknown hint string: {hint}".format(hint=hint))
def _kill_process(process):
"""Kill the process, and return the stdout, stderr and return code."""
stdout_parts = []
stderr_parts = []
_logger.info("waiting for process to exit.")
_attempt_kill_pid(process.pid)
for _ in Timeout.default():
tmp_out, tmp_err = process.communicate()
if isinstance(tmp_out, bytes):
tmp_out = tmp_out.decode('utf-8', errors='replace')
if isinstance(tmp_err, bytes):
tmp_err = tmp_err.decode('utf-8', errors='replace')
stdout_parts.append(tmp_out)
stderr_parts.append(tmp_err)
if not _is_process_running(process.pid):
break
else:
_logger.info(
"Killing process group, since it hasn't exited after "
"10 seconds."
)
_attempt_kill_pid(process.pid, signal.SIGKILL)
return ''.join(stdout_parts), ''.join(stderr_parts), process.returncode
def _attempt_kill_pid(pid, sig=signal.SIGTERM):
try:
_logger.info("Killing process %d", pid)
os.killpg(pid, sig)
except OSError:
_logger.info("Appears process has already exited.")
def _is_process_running(pid):
return psutil.pid_exists(pid)
./autopilot/application/_environment.py 0000644 0000041 0000041 00000004524 13061552435 020654 0 ustar www-data www-data # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Autopilot Functional Test Tool
# Copyright (C) 2013 Canonical
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
"""Base module or application environment setup."""
import fixtures
import os
class ApplicationEnvironment(fixtures.Fixture):
def prepare_environment(self, app_path, arguments):
"""Prepare the application, or environment to launch with
autopilot-support.
The method *must* return a tuple of (*app_path*, *arguments*). Either
of these can be altered by this method.
"""
raise NotImplementedError("Sub-classes must implement this method.")
class GtkApplicationEnvironment(ApplicationEnvironment):
def prepare_environment(self, app_path, arguments):
"""Prepare the application, or environment to launch with
autopilot-support.
:returns: unmodified app_path and arguments
"""
modules = os.getenv('GTK_MODULES', '').split(':')
if 'autopilot' not in modules:
modules.append('autopilot')
os.putenv('GTK_MODULES', ':'.join(modules))
return app_path, arguments
class QtApplicationEnvironment(ApplicationEnvironment):
def prepare_environment(self, app_path, arguments):
"""Prepare the application, or environment to launch with
autopilot-support.
:returns: unmodified app_path and arguments
"""
if '-testability' not in arguments:
insert_pos = 0
for pos, argument in enumerate(arguments):
if argument.startswith("-qt="):
insert_pos = pos + 1
break
arguments.insert(insert_pos, '-testability')
return app_path, arguments
./autopilot/keybindings.py 0000644 0000041 0000041 00000026431 13061552435 016155 0 ustar www-data www-data # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Autopilot Functional Test Tool
# Copyright (C) 2012-2013 Canonical
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
"""Utility functions to get shortcut keybindings for various parts of Unity.
Inside Autopilot we deal with keybindings by naming them with unique names. For
example, instead of hard-coding the fact that 'Alt+F2' opens the command lens,
we might call:
>>> keybindings.get('lens_reveal/command')
'Alt+F2'
Keybindings come from two different places:
1) Keybindings from compiz. We can get these if we have the plugin name and
setting name.
2) Elsewhere. Right now we're hard-coding these in a separate dictionary.
"""
import logging
import re
from autopilot.input import Keyboard
from autopilot.utilities import Silence
_logger = logging.getLogger(__name__)
#
# Fill this dictionary with keybindings we want to store.
#
# If keybindings are from compizconfig, the value should be a 2-value tuple
# containging (plugin_name, setting_name).
#
# If keybindings are elsewhere, just store the keybinding string.
_keys = {
# Launcher:
"launcher/reveal": ('unityshell', 'show_launcher'),
"launcher/keynav": ('unityshell', 'keyboard_focus'),
"launcher/keynav/next": "Down",
"launcher/keynav/prev": "Up",
"launcher/keynav/activate": "Enter",
"launcher/keynav/exit": "Escape",
"launcher/keynav/open-quicklist": "Right",
"launcher/keynav/close-quicklist": "Left",
"launcher/switcher": ('unityshell', 'launcher_switcher_forward'),
"launcher/switcher/exit": "Escape",
"launcher/switcher/next": "Tab",
"launcher/switcher/prev": "Shift+Tab",
"launcher/switcher/down": "Down",
"launcher/switcher/up": "Up",
# Quicklist:
"quicklist/keynav/first": "Home",
"quicklist/keynav/last": "End",
"quicklist/keynav/next": "Down",
"quicklist/keynav/prev": "Up",
"quicklist/keynav/activate": "Enter",
"quicklist/keynav/exit": "Escape",
# Panel:
"panel/show_menus": "Alt",
"panel/open_first_menu": ('unityshell', 'panel_first_menu'),
"panel/next_indicator": "Right",
"panel/prev_indicator": "Left",
# Dash:
"dash/reveal": "Super",
"dash/lens/next": "Ctrl+Tab",
"dash/lens/prev": "Ctrl+Shift+Tab",
# Lenses:
"lens_reveal/command": ("unityshell", "execute_command"),
"lens_reveal/apps": "Super+a",
"lens_reveal/files": "Super+f",
"lens_reveal/music": "Super+m",
"lens_reveal/video": "Super+v",
# Hud:
"hud/reveal": ("unityshell", "show_hud"),
# Switcher:
"switcher/reveal_normal": ("unityshell", "alt_tab_forward"),
"switcher/reveal_impropper": "Alt+Right",
"switcher/reveal_details": "Alt+`",
"switcher/reveal_all": ("unityshell", "alt_tab_forward_all"),
"switcher/cancel": "Escape",
# Shortcut Hint:
"shortcuthint/reveal": ('unityshell', 'show_launcher'),
"shortcuthint/cancel": "Escape",
# These are in compiz as 'Alt+Right' and 'Alt+Left', but the fact that it
# lists the Alt key won't work for us, so I'm defining them manually.
"switcher/next": "Tab",
"switcher/prev": "Shift+Tab",
"switcher/right": "Right",
"switcher/left": "Left",
"switcher/detail_start": "Down",
"switcher/detail_stop": "Up",
"switcher/detail_next": "`",
"switcher/detail_prev": "`",
# Workspace switcher (wall):
"workspace/move_left": ("wall", "left_key"),
"workspace/move_right": ("wall", "right_key"),
"workspace/move_up": ("wall", "up_key"),
"workspace/move_down": ("wall", "down_key"),
# Window management:
"window/show_desktop": ("unityshell", "show_desktop_key"),
"window/minimize": ("core", "minimize_window_key"),
"window/maximize": ("core", "maximize_window_key"),
"window/left_maximize": ("unityshell", "window_left_maximize"),
"window/right_maximize": ("unityshell", "window_right_maximize"),
"window/restore": ("core", "unmaximize_or_minimize_window_key"),
"window/close": ("core", "close_window_key"),
# expo plugin:
"expo/start": ("expo", "expo_key"),
"expo/cancel": "Escape",
# spread (scale) plugin:
"spread/start": ("scale", "initiate_key"),
"spread/cancel": "Escape",
}
def get(binding_name):
"""Get a keybinding, given its well-known name.
:param string binding_name:
:raises TypeError: if binding_name is not a string
:raises ValueError: if binding_name cannot be found in the bindings
dictionaries.
:returns: string for keybinding
"""
if not isinstance(binding_name, str):
raise TypeError("binding_name must be a string.")
if binding_name not in _keys:
raise ValueError("Unknown binding name '%s'." % (binding_name))
v = _keys[binding_name]
if isinstance(v, str):
return v
else:
return _get_compiz_keybinding(v)
def get_hold_part(binding_name):
"""Return the part of a keybinding that must be held permanently.
Use this function to split bindings like "Alt+Tab" into the part that must
be held down. See :meth:`get_tap_part` for the part that must be tapped.
:raises ValueError: if the binding specified does not have multiple
parts.
"""
binding = get(binding_name)
parts = binding.split('+')
if len(parts) == 1:
_logger.warning(
"Key binding '%s' does not have a hold part.", binding_name)
return parts[0]
return '+'.join(parts[:-1])
def get_tap_part(binding_name):
"""Return the part of a keybinding that must be tapped.
Use this function to split bindings like "Alt+Tab" into the part that must
be held tapped. See :meth:`get_hold_part` for the part that must be held
down.
:raises ValueError: if the binding specified does not have multiple
parts.
"""
binding = get(binding_name)
parts = binding.split('+')
if len(parts) == 1:
_logger.warning(
"Key binding '%s' does not have a tap part.", binding_name)
return parts[0]
return parts[-1]
def _get_compiz_keybinding(compiz_tuple):
"""Given a keybinding name, get the keybinding string from the compiz
option.
:raises ValueError: if the compiz setting described does not hold a
keybinding.
:raises RuntimeError: if the compiz keybinding has been disabled.
:returns: compiz keybinding
"""
plugin_name, setting_name = compiz_tuple
plugin = _get_compiz_plugin(plugin_name)
setting = _get_compiz_setting(plugin_name, setting_name)
if setting.Type != 'Key':
raise ValueError(
"Key binding maps to a compiz option that does not hold a "
"keybinding.")
if not plugin.Enabled:
_logger.warning(
"Returning keybinding for '%s' which is in un-enabled plugin '%s'",
setting.ShortDesc,
plugin.ShortDesc)
if setting.Value == "Disabled":
raise RuntimeError(
"Keybinding '%s' in compiz plugin '%s' has been disabled." %
(setting.ShortDesc, plugin.ShortDesc))
return _translate_compiz_keystroke_string(setting.Value)
def _translate_compiz_keystroke_string(keystroke_string):
"""Get a string representing the keystroke stored in *keystroke_string*.
The returned value is suitable for passing into the Keyboard emulator.
:param string keystroke_string: A compizconfig-style keystroke string.
"""
if not isinstance(keystroke_string, str):
raise TypeError("keystroke string must be a string.")
translations = {
'Control': 'Ctrl',
'Primary': 'Ctrl',
}
regex = re.compile('[<>]')
parts = regex.split(keystroke_string)
result = []
for part in parts:
part = part.strip()
if part != "" and not part.isspace():
translated = translations.get(part, part)
if translated not in result:
result.append(translated)
return '+'.join(result)
class KeybindingsHelper(object):
"""A helper class that makes it easier to use Unity keybindings."""
@property
def _keyboard(self):
return Keyboard.create()
def keybinding(self, binding_name, delay=None):
"""Press and release the keybinding with the given name.
If set, the delay parameter will override the default delay set by the
keyboard emulator.
"""
if delay is not None and type(delay) != float:
raise TypeError(
"delay parameter must be a float if it is defined.")
if delay:
self._keyboard.press_and_release(get(binding_name), delay)
else:
self._keyboard.press_and_release(get(binding_name))
def keybinding_hold(self, binding_name):
"""Hold down the hold-part of a keybinding."""
self._keyboard.press(get_hold_part(binding_name))
def keybinding_release(self, binding_name):
"""Release the hold-part of a keybinding."""
self._keyboard.release(get_hold_part(binding_name))
def keybinding_tap(self, binding_name):
"""Tap the tap-part of a keybinding."""
self._keyboard.press_and_release(get_tap_part(binding_name))
def keybinding_hold_part_then_tap(self, binding_name):
self.keybinding_hold(binding_name)
self.keybinding_tap(binding_name)
# Functions that wrap compizconfig to avoid some unpleasantness in that module.
# Local to the this keybindings for now until their removal in the very near
# future.
_global_compiz_context = None
def _get_global_compiz_context():
"""Get the compizconfig global context object.
:returns: global compiz context, either already defined or from compiz
config
"""
global _global_compiz_context
if _global_compiz_context is None:
with Silence():
from compizconfig import Context
_global_compiz_context = Context()
return _global_compiz_context
def _get_compiz_plugin(plugin_name):
"""Get a compizconfig plugin with the specified name.
:raises KeyError: if the plugin named does not exist.
:returns: compizconfig plugin
"""
ctx = _get_global_compiz_context()
with Silence():
try:
return ctx.Plugins[plugin_name]
except KeyError:
raise KeyError(
"Compiz plugin '%s' does not exist." % (plugin_name))
def _get_compiz_setting(plugin_name, setting_name):
"""Get a compizconfig setting object, given a plugin name and setting name.
:raises KeyError: if the plugin or setting is not found.
:returns: compiz setting object
"""
plugin = _get_compiz_plugin(plugin_name)
with Silence():
try:
return plugin.Screen[setting_name]
except KeyError:
raise KeyError(
"Compiz setting '%s' does not exist in plugin '%s'." %
(setting_name, plugin_name))
./autopilot/_logging.py 0000644 0000041 0000041 00000004024 13061552435 015426 0 ustar www-data www-data # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Autopilot Functional Test Tool
# Copyright (C) 2014 Canonical
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
"""Autopilot's logging code."""
import logging
from io import StringIO
from autopilot._fixtures import FixtureWithDirectAddDetail
from autopilot.utilities import (
LogFormatter,
safe_text_content,
)
class TestCaseLoggingFixture(FixtureWithDirectAddDetail):
"""A fixture that adds the log to the test case as a detail object."""
def __init__(self, test_id, *args, **kwargs):
super().__init__(*args, **kwargs)
self._test_id = test_id
def setUp(self):
super().setUp()
logging.info("*" * 60)
logging.info("Starting test %s", self._test_id)
self._log_buffer = StringIO()
root_logger = logging.getLogger()
formatter = LogFormatter()
self._log_handler = logging.StreamHandler(stream=self._log_buffer)
self._log_handler.setFormatter(formatter)
root_logger.addHandler(self._log_handler)
self.addCleanup(self._tearDownLogging)
def _tearDownLogging(self):
root_logger = logging.getLogger()
self._log_handler.flush()
self._log_buffer.seek(0)
self.caseAddDetail(
'test-log',
safe_text_content(self._log_buffer.getvalue())
)
root_logger.removeHandler(self._log_handler)
self._log_buffer = None
./autopilot/_config.py 0000644 0000041 0000041 00000006156 13061552435 015255 0 ustar www-data www-data # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Autopilot Functional Test Tool
# Copyright (C) 2014 Canonical
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
import collections.abc
_test_config_string = ""
def set_configuration_string(config_string):
"""Set the test configuration string.
This must be a text string that specifies the test configuration. The
string is a comma separated list of 'key=value' or 'key' tokens.
"""
global _test_config_string
_test_config_string = config_string
def get_test_configuration():
"""Get the test configuration dictionary.
Tests can be configured from the command line when the ``autopilot`` tool
is invoked. Typical use cases involve configuring the test suite to use
a particular binary (perhaps a locally built binary or one installed to
the system), or configuring which external services are faked.
This dictionary is populated from the ``--config`` option to the
``autopilot run`` command. For example:
``autopilot run --config use_local some.test.id``
Will result in a dictionary where the key ``use_local`` is present, and
evaluates to true, e.g.-::
from autopilot import get_test_configuration
if get_test_configuration['use_local']: print("Using local binary")
Values can also be specified. The following command:
``autopilot run --config fake_services=login some.test.id``
...will result in the key 'fake_services' having the value 'login'.
Autopilot itself does nothing with the conents of this dictionary. It is
entirely up to test authors to populate it, and to use the values as they
see fit.
"""
return ConfigDict(_test_config_string)
class ConfigDict(collections.abc.Mapping):
def __init__(self, config_string):
self._data = {}
config_items = (item for item in config_string.split(',') if item)
for item in config_items:
parts = item.split('=', 1)
safe_key = parts[0].lstrip()
if len(parts) == 1 and safe_key != '':
self._data[safe_key] = '1'
elif len(parts) == 2 and safe_key != '':
self._data[safe_key] = parts[1]
else:
raise ValueError(
"Invalid configuration string '{}'".format(config_string)
)
def __getitem__(self, key):
return self._data.__getitem__(key)
def __iter__(self):
return self._data.__iter__()
def __len__(self):
return self._data.__len__()
./autopilot/process/ 0000755 0000041 0000041 00000000000 13061552435 014745 5 ustar www-data www-data ./autopilot/process/__init__.py 0000644 0000041 0000041 00000035403 13061552435 017063 0 ustar www-data www-data # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Autopilot Functional Test Tool
# Copyright (C) 2012, 2013, 2015 Canonical
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
from collections import OrderedDict
from autopilot.utilities import _pick_backend
class ProcessManager(object):
"""A simple process manager class.
The process manager is used to handle processes, windows and applications.
This class should not be instantiated directly however. To get an instance
of the keyboard class, call :py:meth:`create` instead.
"""
KNOWN_APPS = {
'Character Map': {
'desktop-file': 'gucharmap.desktop',
'process-name': 'gucharmap',
},
'Calculator': {
'desktop-file': 'gcalctool.desktop',
'process-name': 'gnome-calculator',
},
'Mahjongg': {
'desktop-file': 'gnome-mahjongg.desktop',
'process-name': 'gnome-mahjongg',
},
'Remmina': {
'desktop-file': 'remmina.desktop',
'process-name': 'remmina',
},
'System Settings': {
'desktop-file': 'unity-control-center.desktop',
'process-name': 'unity-control-center',
},
'Text Editor': {
'desktop-file': 'gedit.desktop',
'process-name': 'gedit',
},
'Terminal': {
'desktop-file': 'gnome-terminal.desktop',
'process-name': 'gnome-terminal',
},
}
@staticmethod
def create(preferred_backend=""):
"""Get an instance of the :py:class:`ProcessManager` class.
For more infomration on picking specific backends, see
:ref:`tut-picking-backends`
:param preferred_backend: A string containing a hint as to which
backend you would like. Possible backends are:
* ``BAMF`` - Get process information using the BAMF Application
Matching Framework.
:raises: RuntimeError if autopilot cannot instantate any of the
possible backends.
:raises: RuntimeError if the preferred_backend is specified and is not
one of the possible backends for this device class.
:raises: :class:`~autopilot.BackendException` if the preferred_backend
is set, but that backend could not be instantiated.
"""
def get_bamf_pm():
from autopilot.process._bamf import ProcessManager
return ProcessManager()
backends = OrderedDict()
backends['BAMF'] = get_bamf_pm
return _pick_backend(backends, preferred_backend)
@classmethod
def register_known_application(cls, name, desktop_file, process_name):
"""Register an application with autopilot.
After calling this method, you may call :meth:`start_app` or
:meth:`start_app_window` with the `name` parameter to start this
application.
You need only call this once within a test run - the application will
remain registerred until the test run ends.
:param name: The name to be used when launching the application.
:param desktop_file: The filename (without path component) of the
desktop file used to launch the application.
:param process_name: The name of the executable process that gets run.
:raises: **KeyError** if application has been registered already
"""
if name in cls.KNOWN_APPS:
raise KeyError("Application has been registered already")
else:
cls.KNOWN_APPS[name] = {
"desktop-file": desktop_file,
"process-name": process_name
}
@classmethod
def unregister_known_application(cls, name):
"""Unregister an application with the known_apps dictionary.
:param name: The name to be used when launching the application.
:raises: **KeyError** if the application has not been registered.
"""
if name in cls.KNOWN_APPS:
del cls.KNOWN_APPS[name]
else:
raise KeyError("Application has not been registered")
def start_app(self, app_name, files=[], locale=None):
"""Start one of the known applications, and kill it on tear down.
.. warning:: This method will clear all instances of this application
on tearDown, not just the one opened by this method! We recommend that
you use the :meth:`start_app_window` method instead, as it is
generally safer.
:param app_name: The application name. *This name must either already
be registered as one of the built-in applications that are supported
by autopilot, or must have been registered using*
:meth:`register_known_application` *beforehand.*
:param files: (Optional) A list of paths to open with the
given application. *Not all applications support opening files in this
way.*
:param locale: (Optional) The locale will to set when the application
is launched. *If you want to launch an application without any
localisation being applied, set this parameter to 'C'.*
:returns: A :class:`~autopilot.process.Application` instance.
"""
raise NotImplementedError("You cannot use this class directly.")
def start_app_window(self, app_name, files=[], locale=None):
"""Open a single window for one of the known applications, and close it
at the end of the test.
:param app_name: The application name. *This name must either already
be registered as one of the built-in applications that are supported
by autopilot, or must have been registered with*
:meth:`register_known_application` *beforehand.*
:param files: (Optional) Should be a list of paths to open with the
given application. *Not all applications support opening files in this
way.*
:param locale: (Optional) The locale will to set when the application
is launched. *If you want to launch an application without any
localisation being applied, set this parameter to 'C'.*
:raises: **AssertionError** if no window was opened, or more than one
window was opened.
:returns: A :class:`~autopilot.process.Window` instance.
"""
raise NotImplementedError("You cannot use this class directly.")
def get_open_windows_by_application(self, app_name):
"""Get a list of ~autopilot.process.Window` instances
for the given application name.
:param app_name: The name of one of the well-known applications.
:returns: A list of :class:`~autopilot.process.Window`
instances.
"""
raise NotImplementedError("You cannot use this class directly.")
def close_all_app(self, app_name):
raise NotImplementedError("You cannot use this class directly.")
def get_app_instances(self, app_name):
raise NotImplementedError("You cannot use this class directly.")
def app_is_running(self, app_name):
raise NotImplementedError("You cannot use this class directly.")
def get_running_applications(self, user_visible_only=True):
"""Get a list of the currently running applications.
If user_visible_only is True (the default), only applications
visible to the user in the switcher will be returned.
"""
raise NotImplementedError("You cannot use this class directly.")
def get_running_applications_by_desktop_file(self, desktop_file):
"""Return a list of applications with the desktop file *desktop_file*.
This method will return an empty list if no applications
are found with the specified desktop file.
"""
raise NotImplementedError("You cannot use this class directly.")
def get_open_windows(self, user_visible_only=True):
"""Get a list of currently open windows.
If *user_visible_only* is True (the default), only applications visible
to the user in the switcher will be returned.
The result is sorted to be in stacking order.
"""
raise NotImplementedError("You cannot use this class directly.")
def wait_until_application_is_running(self, desktop_file, timeout):
"""Wait until a given application is running.
:param string desktop_file: The name of the application desktop file.
:param integer timeout: The maximum time to wait, in seconds. *If set
to something less than 0, this method will wait forever.*
:return: true once the application is found, or false if the
application was not found until the timeout was reached.
"""
raise NotImplementedError("You cannot use this class directly.")
def launch_application(self, desktop_file, files=[], wait=True):
"""Launch an application by specifying a desktop file.
:param files: List of files to pass to the application. *Not all
apps support this.*
:type files: List of strings
.. note:: If `wait` is True, this method will wait up to 10 seconds for
the application to appear.
:raises: **TypeError** on invalid *files* parameter.
:return: The Gobject process object.
"""
raise NotImplementedError("You cannot use this class directly.")
class Application(object):
@property
def desktop_file(self):
"""Get the application desktop file.
This returns just the filename, not the full path.
If the application no longer exists, this returns an empty string.
"""
raise NotImplementedError("You cannot use this class directly.")
@property
def name(self):
"""Get the application name.
.. note:: This may change according to the current locale. If you want
a unique string to match applications against, use desktop_file
instead.
"""
raise NotImplementedError("You cannot use this class directly.")
@property
def icon(self):
"""Get the application icon.
:return: The name of the icon.
"""
raise NotImplementedError("You cannot use this class directly.")
@property
def is_active(self):
"""Is the application active (i.e. has keyboard focus)?"""
raise NotImplementedError("You cannot use this class directly.")
@property
def is_urgent(self):
"""Is the application currently signalling urgency?"""
raise NotImplementedError("You cannot use this class directly.")
@property
def user_visible(self):
"""Is this application visible to the user?
.. note:: Some applications (such as the panel) are hidden to the user
but may still be returned.
"""
raise NotImplementedError("You cannot use this class directly.")
def get_windows(self):
"""Get a list of the application windows."""
raise NotImplementedError("You cannot use this class directly.")
class Window(object):
@property
def x_id(self):
"""Get the X11 Window Id."""
raise NotImplementedError("You cannot use this class directly.")
@property
def x_win(self):
"""Get the X11 window object of the underlying window."""
raise NotImplementedError("You cannot use this class directly.")
@property
def get_wm_state(self):
"""Get the state of the underlying window."""
raise NotImplementedError("You cannot use this class directly.")
@property
def name(self):
"""Get the window name.
.. note:: This may change according to the current locale. If you want
a unique string to match windows against, use the x_id instead.
"""
raise NotImplementedError("You cannot use this class directly.")
@property
def title(self):
"""Get the window title.
This may be different from the application name.
.. note:: This may change depending on the current locale.
"""
raise NotImplementedError("You cannot use this class directly.")
@property
def geometry(self):
"""Get the geometry for this window.
:return: Tuple containing (x, y, width, height).
"""
raise NotImplementedError("You cannot use this class directly.")
@property
def is_maximized(self):
"""Is the window maximized?
Maximized in this case means both maximized vertically and
horizontally. If a window is only maximized in one direction it is not
considered maximized.
"""
raise NotImplementedError("You cannot use this class directly.")
@property
def application(self):
"""Get the application that owns this window.
This method may return None if the window does not have an associated
application. The 'desktop' window is one such example.
"""
raise NotImplementedError("You cannot use this class directly.")
@property
def user_visible(self):
"""Is this window visible to the user in the switcher?"""
raise NotImplementedError("You cannot use this class directly.")
@property
def is_hidden(self):
"""Is this window hidden?
Windows are hidden when the 'Show Desktop' mode is activated.
"""
raise NotImplementedError("You cannot use this class directly.")
@property
def is_focused(self):
"""Is this window focused?"""
raise NotImplementedError("You cannot use this class directly.")
@property
def is_valid(self):
"""Is this window object valid?
Invalid windows are caused by windows closing during the construction
of this object instance.
"""
raise NotImplementedError("You cannot use this class directly.")
@property
def monitor(self):
"""Returns the monitor to which the windows belongs to"""
raise NotImplementedError("You cannot use this class directly.")
@property
def closed(self):
"""Returns True if the window has been closed"""
raise NotImplementedError("You cannot use this class directly.")
def close(self):
"""Close the window."""
raise NotImplementedError("You cannot use this class directly.")
def set_focus(self):
raise NotImplementedError("You cannot use this class directly.")
./autopilot/process/_bamf.py 0000644 0000041 0000041 00000053765 13061552435 016403 0 ustar www-data www-data # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Autopilot Functional Test Tool
# Copyright (C) 2012-2014 Canonical
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
"""BAMF implementation of the Process Management"""
import dbus
import dbus.glib
from gi.repository import Gio
from gi.repository import GLib
import logging
import os
from Xlib import display, X, protocol
from subprocess import check_output, CalledProcessError, call
import autopilot._glib
from autopilot._timeout import Timeout
from autopilot.dbus_handler import get_session_bus
from autopilot.process import (
ProcessManager as ProcessManagerBase,
Application as ApplicationBase,
Window as WindowBase
)
from autopilot.utilities import (
addCleanup,
Silence,
)
_BAMF_BUS_NAME = 'org.ayatana.bamf'
_X_DISPLAY = None
_logger = logging.getLogger(__name__)
def get_display():
"""Create an Xlib display object (silently) and return it."""
global _X_DISPLAY
if _X_DISPLAY is None:
with Silence():
_X_DISPLAY = display.Display()
return _X_DISPLAY
def _filter_user_visible(win):
"""Filter out non-user-visible objects.
In some cases the DBus method we need to call hasn't been registered yet,
in which case we do the safe thing and return False.
"""
try:
return win.user_visible
except dbus.DBusException:
return False
class ProcessManager(ProcessManagerBase):
"""High-level class for interacting with Bamf from within a test.
Use this class to inspect the state of running applications and open
windows.
"""
def __init__(self):
matcher_path = '/org/ayatana/bamf/matcher'
self.matcher_interface_name = 'org.ayatana.bamf.matcher'
self.matcher_proxy = get_session_bus().get_object(
_BAMF_BUS_NAME, matcher_path)
self.matcher_interface = dbus.Interface(
self.matcher_proxy, self.matcher_interface_name)
def start_app(self, app_name, files=[], locale=None):
"""Start one of the known applications, and kill it on tear down.
.. warning:: This method will clear all instances of this application
on tearDown, not just the one opened by this method! We recommend that
you use the :meth:`start_app_window` method instead, as it is
generally safer.
:param app_name: The application name. *This name must either already
be registered as one of the built-in applications that are supported
by autopilot, or must have been registered using*
:meth:`register_known_application` *beforehand.*
:param files: (Optional) A list of paths to open with the
given application. *Not all applications support opening files in this
way.*
:param locale: (Optional) The locale will to set when the application
is launched. *If you want to launch an application without any
localisation being applied, set this parameter to 'C'.*
:returns: A :class:`~autopilot.process.Application` instance.
"""
window = self._open_window(app_name, files, locale)
if window:
addCleanup(self.close_all_app, app_name)
return window.application
raise AssertionError("No new application window was opened.")
def start_app_window(self, app_name, files=[], locale=None):
"""Open a single window for one of the known applications, and close it
at the end of the test.
:param app_name: The application name. *This name must either already
be registered as one of the built-in applications that are supported
by autopilot, or must have been registered with*
:meth:`register_known_application` *beforehand.*
:param files: (Optional) Should be a list of paths to open with the
given application. *Not all applications support opening files in this
way.*
:param locale: (Optional) The locale will to set when the application
is launched. *If you want to launch an application without any
localisation being applied, set this parameter to 'C'.*
:raises: **AssertionError** if no window was opened, or more than one
window was opened.
:returns: A :class:`~autopilot.process.Window` instance.
"""
window = self._open_window(app_name, files, locale)
if window:
addCleanup(window.close)
return window
raise AssertionError("No window was opened.")
def _open_window(self, app_name, files, locale):
"""Open a new 'app_name' window, returning the window instance or None.
Raises an AssertionError if this creates more than one window.
"""
existing_windows = self.get_open_windows_by_application(app_name)
if locale:
os.putenv("LC_ALL", locale)
addCleanup(os.unsetenv, "LC_ALL")
_logger.info(
"Starting application '%s' with files %r in locale %s",
app_name, files, locale)
else:
_logger.info(
"Starting application '%s' with files %r", app_name, files)
app = self.KNOWN_APPS[app_name]
self._launch_application(app['desktop-file'], files)
apps = self.get_running_applications_by_desktop_file(
app['desktop-file'])
for _ in Timeout.default():
try:
new_windows = []
[new_windows.extend(a.get_windows()) for a in apps]
filter_fn = lambda w: w.x_id not in [
c.x_id for c in existing_windows]
new_wins = list(filter(filter_fn, new_windows))
if new_wins:
assert len(new_wins) == 1
return new_wins[0]
except dbus.DBusException:
pass
return None
def get_open_windows_by_application(self, app_name):
"""Get a list of ~autopilot.process.Window` instances
for the given application name.
:param app_name: The name of one of the well-known applications.
:returns: A list of :class:`~autopilot.process.Window`
instances.
"""
existing_windows = []
[existing_windows.extend(a.get_windows()) for a in
self.get_app_instances(app_name)]
return existing_windows
def close_all_app(self, app_name):
"""Close all instances of the application 'app_name'."""
app = self.KNOWN_APPS[app_name]
try:
pids = check_output(["pidof", app['process-name']]).split()
if len(pids):
call(["kill"] + pids)
except CalledProcessError:
_logger.warning(
"Tried to close applicaton '%s' but it wasn't running.",
app_name)
def get_app_instances(self, app_name):
"""Get `~autopilot.process.Application` instances for app_name."""
desktop_file = self.KNOWN_APPS[app_name]['desktop-file']
return self.get_running_applications_by_desktop_file(desktop_file)
def app_is_running(self, app_name):
"""Return true if an instance of the application is running."""
apps = self.get_app_instances(app_name)
return len(apps) > 0
def get_running_applications(self, user_visible_only=True):
"""Get a list of the currently running applications.
If user_visible_only is True (the default), only applications
visible to the user in the switcher will be returned.
"""
apps = [Application(p) for p in
self.matcher_interface.RunningApplications()]
if user_visible_only:
return list(filter(_filter_user_visible, apps))
return apps
def get_running_applications_by_desktop_file(self, desktop_file):
"""Return a list of applications with the desktop file *desktop_file*.
This method will return an empty list if no applications are found
with the specified desktop file.
"""
apps = []
for a in self.get_running_applications():
try:
if a.desktop_file == desktop_file:
apps.append(a)
except dbus.DBusException:
pass
return apps
def get_open_windows(self, user_visible_only=True):
"""Get a list of currently open windows.
If *user_visible_only* is True (the default), only applications visible
to the user in the switcher will be returned.
The result is sorted to be in stacking order.
"""
windows = [Window(w) for w in
self.matcher_interface.WindowStackForMonitor(-1)]
if user_visible_only:
windows = list(filter(_filter_user_visible, windows))
# Now sort on stacking order.
# We explicitly convert to a list from an iterator since tests
# frequently try and use len() on return values from these methods.
return list(reversed(windows))
def wait_until_application_is_running(self, desktop_file, timeout):
"""Wait until a given application is running.
:param string desktop_file: The name of the application desktop file.
:param integer timeout: The maximum time to wait, in seconds. *If set
to something less than 0, this method will wait forever.*
:return: true once the application is found, or false if the
application was not found until the timeout was reached.
"""
desktop_file = os.path.split(desktop_file)[1]
# python workaround since you can't assign to variables in the
# enclosing scope: see on_timeout_reached below...
found_app = [True]
# maybe the app is running already?
running_applications = self.get_running_applications_by_desktop_file(
desktop_file)
if len(running_applications) == 0:
wait_forever = timeout < 0
gobject_loop = GLib.MainLoop()
# No, so define a callback to watch the ViewOpened signal:
def on_view_added(bamf_path, name):
if bamf_path.split('/')[-2].startswith('application'):
app = Application(bamf_path)
if desktop_file == os.path.split(app.desktop_file)[1]:
gobject_loop.quit()
# ...and one for when the user-defined timeout has been reached:
def on_timeout_reached():
gobject_loop.quit()
found_app[0] = False
return False
# need a timeout? if so, connect it:
if not wait_forever:
GLib.timeout_add(timeout * 1000, on_timeout_reached)
# connect signal handler:
get_session_bus().add_signal_receiver(on_view_added, 'ViewOpened')
# pump the gobject main loop until either the correct signal is
# emitted, or the timeout happens.
gobject_loop.run()
return found_app[0]
def _launch_application(self, desktop_file, files=[], wait=True):
"""Launch an application by specifying a desktop file.
:param files: List of files to pass to the application. *Not all
apps support this.*
:type files: List of strings
.. note:: If `wait` is True, this method will wait up to 10 seconds for
the application to appear in the BAMF model.
:raises: **TypeError** on invalid *files* parameter.
:return: The Gobject process object.
"""
if type(files) is not list:
raise TypeError("files must be a list.")
proc = _launch_application(desktop_file, files)
if wait:
self.wait_until_application_is_running(desktop_file, 10)
return proc
def _launch_application(desktop_file, files):
proc = Gio.DesktopAppInfo.new(desktop_file)
# simple launch_uris() uses GLib.SpawnFlags.SEARCH_PATH by default only,
# but this inherits stdout; we don't want that as it hangs when tee'ing
# autopilot output into a file.
# Instead of depending on a newer version of gir/glib attempt to use the
# newer verison (i.e. launch_uris_as_manager works) and fall back on using
# the simple launch_uris
try:
proc.launch_uris_as_manager(
files, None,
GLib.SpawnFlags.SEARCH_PATH | GLib.SpawnFlags.STDOUT_TO_DEV_NULL,
None, None, None, None)
except TypeError:
proc.launch_uris(files, None)
class Application(ApplicationBase):
"""Represents an application, with information as returned by Bamf.
.. important:: Don't instantiate this class yourself. instead, use the
methods as provided by the Bamf class.
:raises: **dbus.DBusException** in the case of a DBus error.
"""
def __init__(self, bamf_app_path):
self.bamf_app_path = bamf_app_path
try:
self._app_proxy = get_session_bus().get_object(
_BAMF_BUS_NAME, bamf_app_path)
self._view_iface = dbus.Interface(
self._app_proxy, 'org.ayatana.bamf.view')
self._app_iface = dbus.Interface(
self._app_proxy, 'org.ayatana.bamf.application')
except dbus.DBusException as e:
e.args += ('bamf_app_path=%r' % (bamf_app_path),)
raise
@property
def desktop_file(self):
"""Get the application desktop file.
This just returns the filename, not the full path.
If the application no longer exists, this returns an empty string.
"""
try:
return os.path.split(self._app_iface.DesktopFile())[1]
except dbus.DBusException:
return ""
@property
def name(self):
"""Get the application name.
.. note:: This may change according to the current locale. If you want
a unique string to match applications against, use the desktop_file
instead.
"""
return self._view_iface.Name()
@property
def icon(self):
"""Get the application icon.
:return: The name of the icon.
"""
return self._view_iface.Icon()
@property
def is_active(self):
"""Is the application active (i.e.- has keyboard focus)?"""
return self._view_iface.IsActive()
@property
def is_urgent(self):
"""Is the application currently signalling urgency?"""
return self._view_iface.IsUrgent()
@property
def user_visible(self):
"""Is this application visible to the user?
.. note:: Some applications (such as the panel) are hidden to the user
but will still be returned by bamf.
"""
return self._view_iface.UserVisible()
def get_windows(self):
"""Get a list of the application windows."""
return [Window(w) for w in self._view_iface.Children()]
def __repr__(self):
return "" % (self.name)
def __eq__(self, other):
if other is None:
return False
return self.desktop_file == other.desktop_file
class Window(WindowBase):
"""Represents an application window, as returned by Bamf.
.. important:: Don't instantiate this class yourself. Instead, use the
appropriate methods in Application.
"""
def __init__(self, window_path):
self._bamf_win_path = window_path
self._app_proxy = get_session_bus().get_object(
_BAMF_BUS_NAME, window_path)
self._window_iface = dbus.Interface(
self._app_proxy, 'org.ayatana.bamf.window')
self._view_iface = dbus.Interface(
self._app_proxy, 'org.ayatana.bamf.view')
self._xid = int(self._window_iface.GetXid())
self._x_root_win = get_display().screen().root
self._x_win = get_display().create_resource_object(
'window', self._xid)
@property
def x_id(self):
"""Get the X11 Window Id."""
return self._xid
@property
def x_win(self):
"""Get the X11 window object of the underlying window."""
return self._x_win
@property
def name(self):
"""Get the window name.
.. note:: This may change according to the current locale. If you want
a unique string to match windows against, use the x_id instead.
"""
return self._view_iface.Name()
@property
def title(self):
"""Get the window title.
This may be different from the application name.
.. note:: This may change depending on the current locale.
"""
return self._getProperty('_NET_WM_NAME')
@property
def geometry(self):
"""Get the geometry for this window.
:return: Tuple containing (x, y, width, height).
"""
# Note: MUST import these here, rather than at the top of the file.
# Why? Because sphinx imports these modules to build the API
# documentation, which in turn tries to import Gdk, which in turn
# fails because there's no DISPlAY environment set in the package
# builder.
Gdk = autopilot._glib._import_gdk()
GdkX11 = autopilot._glib._import_gdkx11()
# FIXME: We need to use the gdk window here to get the real coordinates
geometry = self._x_win.get_geometry()
origin = GdkX11.X11Window.foreign_new_for_display(
Gdk.Display().get_default(), self._xid).get_origin()
return (origin[1], origin[2], geometry.width, geometry.height)
@property
def is_maximized(self):
"""Is the window maximized?
Maximized in this case means both maximized vertically and
horizontally. If a window is only maximized in one direction it is not
considered maximized.
"""
win_state = self._get_window_states()
return '_NET_WM_STATE_MAXIMIZED_VERT' in win_state and \
'_NET_WM_STATE_MAXIMIZED_HORZ' in win_state
@property
def application(self):
"""Get the application that owns this window.
This method may return None if the window does not have an associated
application. The 'desktop' window is one such example.
"""
# BAMF returns a list of parents since some windows don't have an
# associated application. For these windows we return none.
parents = self._view_iface.Parents()
if parents:
return Application(parents[0])
else:
return None
@property
def user_visible(self):
"""Is this window visible to the user in the switcher?"""
return self._view_iface.UserVisible()
@property
def is_hidden(self):
"""Is this window hidden?
Windows are hidden when the 'Show Desktop' mode is activated.
"""
win_state = self._get_window_states()
return '_NET_WM_STATE_HIDDEN' in win_state
@property
def is_focused(self):
"""Is this window focused?"""
win_state = self._get_window_states()
return '_NET_WM_STATE_FOCUSED' in win_state
@property
def is_valid(self):
"""Is this window object valid?
Invalid windows are caused by windows closing during the construction
of this object instance.
"""
return self._x_win is not None
@property
def monitor(self):
"""Returns the monitor to which the windows belongs to"""
return self._window_iface.Monitor()
@property
def closed(self):
"""Returns True if the window has been closed"""
# This will return False when the window is closed and then removed
# from BUS.
try:
return (self._window_iface.GetXid() != self.x_id)
except:
return True
def close(self):
"""Close the window."""
self._setProperty('_NET_CLOSE_WINDOW', [0, 0])
def set_focus(self):
self._x_win.set_input_focus(X.RevertToParent, X.CurrentTime)
self._x_win.configure(stack_mode=X.Above)
def __repr__(self):
return "" % (
self.title if self._x_win else '', self.x_id)
def _getProperty(self, _type):
"""Get an X11 property.
_type is a string naming the property type. win is the X11 window
object.
"""
atom = self._x_win.get_full_property(
get_display().get_atom(_type), X.AnyPropertyType)
if atom:
return atom.value
def _setProperty(self, _type, data, mask=None):
if type(data) is str:
dataSize = 8
else:
# data length must be 5 - pad with 0's if it's short, truncate
# otherwise.
data = (data + [0] * (5 - len(data)))[:5]
dataSize = 32
ev = protocol.event.ClientMessage(
window=self._x_win, client_type=get_display().get_atom(_type),
data=(dataSize, data))
if not mask:
mask = (X.SubstructureRedirectMask | X.SubstructureNotifyMask)
self._x_root_win.send_event(ev, event_mask=mask)
get_display().sync()
def _get_window_states(self):
"""Return a list of strings representing the current window state."""
get_display().sync()
return [get_display().get_atom_name(p)
for p in self._getProperty('_NET_WM_STATE')]
def resize(self, width, height):
"""Resize the window.
:param width: The new width for the window.
:param height: The new height for the window.
"""
self.x_win.configure(width=width, height=height)
self.x_win.change_attributes(
win_gravity=X.NorthWestGravity, bit_gravity=X.StaticGravity)
# A call to get the window geometry commits the changes.
self.geometry
./autopilot/logging.py 0000644 0000041 0000041 00000003326 13061552435 015273 0 ustar www-data www-data # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Autopilot Functional Test Tool
# Copyright (C) 2013 Canonical
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
"""Logging helpers for Autopilot tests."""
import pprint
from functools import wraps
def log_action(log_func):
"""Decorator to log the call of an action method."""
def middle(f):
@wraps(f)
def inner(instance, *args, **kwargs):
class_name = str(instance.__class__.__name__)
docstring = f.__doc__
if docstring:
docstring = docstring.split('\n')[0].strip()
else:
docstring = f.__name__
# Strip the ending periods of the docstring, if present, so only
# one will remain after using the log line format.
docstring = docstring.rstrip('.')
log_line = '%s: %s. Arguments %s. Keyword arguments: %s.'
log_func(
log_line, class_name, docstring, pprint.pformat(args),
pprint.pformat(kwargs))
return f(instance, *args, **kwargs)
return inner
return middle
./autopilot/_video.py 0000644 0000041 0000041 00000013034 13061552435 015107 0 ustar www-data www-data # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Autopilot Functional Test Tool
# Copyright (C) 2014 Canonical
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
import fixtures
import glob
from functools import partial
import logging
import os
import signal
import subprocess
from testtools.matchers import NotEquals
from autopilot.matchers import Eventually
from autopilot.utilities import safe_text_content
logger = logging.getLogger(__name__)
class RMDVideoLogFixture(fixtures.Fixture):
"""Video capture autopilot tests, saving the results if the test failed."""
_recording_app = '/usr/bin/recordmydesktop'
_recording_opts = ['--no-sound', '--no-frame', '-o']
def __init__(self, recording_directory, test_instance):
super().__init__()
self.recording_directory = recording_directory
self.test_instance = test_instance
def setUp(self):
super().setUp()
self._test_passed = True
if not self._have_recording_app():
logger.warning(
"Disabling video capture since '%s' is not present",
self._recording_app)
self.test_instance.addOnException(self._on_test_failed)
self.test_instance.addCleanup(
self._stop_video_capture,
self.test_instance
)
self._start_video_capture(self.test_instance.shortDescription())
def _have_recording_app(self):
return os.path.exists(self._recording_app)
def _start_video_capture(self, test_id):
args = self.get_capture_command_line()
self._capture_file = os.path.join(
self.recording_directory,
'%s.ogv' % (test_id)
)
_ensure_directory_exists_but_not_file(self._capture_file)
args.append(self._capture_file)
video_session_pattern = '/tmp/rMD-session*'
orig_sessions = glob.glob(video_session_pattern)
logger.debug("Starting: %r", args)
self._capture_process = subprocess.Popen(
args,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
universal_newlines=True
)
# wait until rmd session directory is created
Eventually(NotEquals(orig_sessions)).match(
lambda: glob.glob(video_session_pattern)
)
def _stop_video_capture(self, test_instance):
"""Stop the video capture. If the test failed, save the resulting
file."""
if self._test_passed:
# SIGABRT terminates the program and removes
# the specified output file.
self._capture_process.send_signal(signal.SIGABRT)
self._capture_process.wait()
else:
self._capture_process.terminate()
self._capture_process.wait()
if self._capture_process.returncode != 0:
test_instance.addDetail(
'video capture log',
safe_text_content(self._capture_process.stdout.read()))
self._capture_process = None
self._currently_recording_description = None
def _on_test_failed(self, ex_info):
"""Called when a test fails."""
from unittest.case import SkipTest
failure_class_type = ex_info[0]
if failure_class_type is not SkipTest:
self._test_passed = False
def get_capture_command_line(self):
return [self._recording_app] + self._recording_opts
def set_recording_dir(self, dir):
self.recording_directory = dir
def _have_video_recording_facilities():
call_ret_code = subprocess.call(
['which', 'recordmydesktop'],
stdout=subprocess.PIPE
)
return call_ret_code == 0
def _ensure_directory_exists_but_not_file(file_path):
dirpath = os.path.dirname(file_path)
if not os.path.exists(dirpath):
os.makedirs(dirpath)
elif os.path.exists(file_path):
logger.warning(
"Video capture file '%s' already exists, deleting.", file_path)
os.remove(file_path)
class DoNothingFixture(fixtures.Fixture):
def __init__(self, arg):
pass
VideoLogFixture = DoNothingFixture
def configure_video_recording(args):
"""Configure video recording based on contents of ``args``.
:raises RuntimeError: If the user asked for video recording, but the
system does not support video recording.
"""
global VideoLogFixture
if args.record_directory:
args.record = True
if not args.record:
# blank fixture when recording is not enabled
VideoLogFixture = DoNothingFixture
else:
if not args.record_directory:
args.record_directory = '/tmp/autopilot'
if not _have_video_recording_facilities():
raise RuntimeError(
"The application 'recordmydesktop' needs to be installed to "
"record failing jobs."
)
VideoLogFixture = partial(RMDVideoLogFixture, args.record_directory)
def get_video_recording_fixture():
return VideoLogFixture
./autopilot/platform.py 0000644 0000041 0000041 00000014020 13061552435 015462 0 ustar www-data www-data # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Autopilot Functional Test Tool
# Copyright (C) 2012-2013 Canonical
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
import os
"""
Platform identification utilities for Autopilot.
================================================
This module provides functions that give test authors hints as to which
platform their tests are currently running on. This is useful when a test
needs to test slight different behavior depending on the system it's running
on. For example::
from autopilot import platform
...
def test_something(self):
if platform.model() == "Galaxy Nexus":
# do something
elif platform.model() == "Desktop":
# do something else
def test_something_else(self):
if platform.is_tablet():
# run a tablet test
else:
# run a non-tablet test
Skipping tests based on Platform
++++++++++++++++++++++++++++++++
Sometimes you want a test to not run on certain platforms, or only run on
certain platforms. This can be easily achieved with a combination of the
functions in this module and the ``skipIf`` and ``skipUnless`` decorators. For
example, to define a test that only runs on the galaxy nexus device, write
this::
from testtools import skipUnless
...
@skipUnless(
platform.model() == 'Galaxy Nexus',
"Test is only for Galaxy Nexus"
)
def test_something(self):
# test things!
The inverse is possible as well. To define a test that will run on every device
except the Galaxy Nexus, write this::
from testtools import skipIf
...
@skipIf(
platform.model() == 'Galaxy Nexus',
"Test not available for Galaxy Nexus"
)
def test_something(self):
# test things!
Tuples of values can be used as well, to select more than one platform. For
example::
@skipIf(
platform.model() in ('Model One', 'Model Two'),
"Test not available for Models One and Two"
)
def test_something(self):
# test things!
"""
def model():
"""Get the model name of the current platform.
For desktop / laptop installations, this will return "Desktop".
Otherwise, the current hardware model will be returned. For example::
platform.model()
... "Galaxy Nexus"
"""
return _PlatformDetector.create().model
def image_codename():
"""Get the image codename.
For desktop / laptop installations this will return "Desktop".
Otherwise, the codename of the image that was installed will be
returned. For example:
platform.image_codename()
... "maguro"
"""
return _PlatformDetector.create().image_codename
def is_tablet():
"""Indicate whether system is a tablet.
The 'ro.build.characteristics' property is checked for 'tablet'.
For example:
platform.tablet()
... True
:returns: boolean indicating whether this is a tablet
"""
return _PlatformDetector.create().is_tablet
def get_display_server():
"""Returns display server type.
:returns: string indicating display server type.
"""
return os.environ.get('XDG_SESSION_TYPE', 'UNKNOWN').upper()
# Different vers. of psutil across Trusty and Utopic have name as either a
# string or a method.
def _get_process_name(proc):
if callable(proc):
return proc()
elif isinstance(proc, str):
return proc
else:
raise ValueError("Unknown process name format.")
class _PlatformDetector(object):
_cached_detector = None
@staticmethod
def create():
"""Create a platform detector object, or return one we baked
earlier."""
if _PlatformDetector._cached_detector is None:
_PlatformDetector._cached_detector = _PlatformDetector()
return _PlatformDetector._cached_detector
def __init__(self):
self.model = "Desktop"
self.image_codename = "Desktop"
self.is_tablet = False
property_file = _get_property_file()
if property_file is not None:
self.update_values_from_build_file(property_file)
def update_values_from_build_file(self, property_file):
"""Read build.prop file and parse it."""
properties = _parse_build_properties_file(property_file)
self.model = properties.get('ro.product.model', "Desktop")
self.image_codename = properties.get('ro.product.name', "Desktop")
self.is_tablet = ('ro.build.characteristics' in properties and
'tablet' in properties['ro.build.characteristics'])
def _get_property_file_path():
return '/system/build.prop'
def _get_property_file():
"""Return a file-like object that contains the contents of the build
properties file, if it exists, or None.
"""
path = _get_property_file_path()
try:
return open(path)
except IOError:
return None
def _parse_build_properties_file(property_file):
"""Parse 'property_file', which must be a file-like object containing the
system build properties.
Returns a dictionary of key,value pairs.
"""
properties = {}
for line in property_file:
line = line.strip()
if not line or line.startswith('#'):
continue
split_location = line.find('=')
if split_location == -1:
continue
key = line[:split_location]
value = line[split_location + 1:]
properties[key] = value
return properties
./autopilot/_info.py 0000644 0000041 0000041 00000004754 13061552435 014745 0 ustar www-data www-data # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Autopilot Functional Test Tool
# Copyright (C) 2012-2013 Canonical
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
"""Provide version and package availibility information for autopilot."""
import subprocess
__all__ = [
'get_version_string',
'have_vis',
'version',
]
version = '1.6.0'
def have_vis():
"""Return true if the vis package is installed."""
try:
from autopilot.vis import vis_main # flake8: noqa
return True
except ImportError:
return False
def get_version_string():
"""Return the autopilot source and package versions."""
version_string = "Autopilot Source Version: " + _get_source_version()
pkg_version = _get_package_version()
if pkg_version:
version_string += "\nAutopilot Package Version: " + pkg_version
return version_string
def _get_source_version():
return version
def _get_package_version():
"""Get the version of the currently installed package version, or None.
Only returns the package version if the package is installed, *and* we seem
to be running the system-wide installed code.
"""
if _running_in_system():
return _get_package_installed_version()
return None
def _running_in_system():
"""Return True if we're running autopilot from the system installation
dir."""
return __file__.startswith('/usr/')
def _get_package_installed_version():
"""Get the version string of the system-wide installed package, or None if
it is not installed.
"""
try:
return subprocess.check_output(
[
"dpkg-query",
"--showformat",
"${Version}",
"--show",
"python3-autopilot",
],
universal_newlines=True
).strip()
except subprocess.CalledProcessError:
return None
./autopilot/utilities.py 0000644 0000041 0000041 00000046421 13061552435 015663 0 ustar www-data www-data # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Autopilot Functional Test Tool
# Copyright (C) 2012-2013 Canonical
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
"""Various utility classes and functions that are useful when running tests."""
from contextlib import contextmanager
from decorator import decorator
import functools
import inspect
import logging
import os
import psutil
import time
import timeit
from testtools.content import text_content
from unittest.mock import Mock
from functools import wraps
from autopilot.exceptions import BackendException
logger = logging.getLogger(__name__)
def safe_text_content(text):
"""Return testtools.content.Content object.
Safe in the sense that it will catch any attempt to attach NoneType
objects.
:raises ValueError: If `text` is not a text-type object.
"""
if not isinstance(text, str):
raise TypeError(
'text argument must be string not {}'.format(
type(text).__name__
)
)
return text_content(text)
def _pick_backend(backends, preferred_backend):
"""Pick a backend and return an instance of it."""
possible_backends = list(backends.keys())
get_debug_logger().debug(
"Possible backends: %s", ','.join(possible_backends))
if preferred_backend:
if preferred_backend in possible_backends:
# make preferred_backend the first list item
possible_backends.remove(preferred_backend)
possible_backends.insert(0, preferred_backend)
else:
raise RuntimeError("Unknown backend '%s'" % (preferred_backend))
failure_reasons = []
for be in possible_backends:
try:
return backends[be]()
except Exception as e:
get_debug_logger().warning("Can't create backend %s: %r", be, e)
failure_reasons.append('%s: %r' % (be, e))
if preferred_backend != '':
raise BackendException(e)
raise RuntimeError(
"Unable to instantiate any backends\n%s" % '\n'.join(failure_reasons))
# Taken from http://ur1.ca/eqapv
# licensed under the MIT license.
class Silence(object):
"""Context manager which uses low-level file descriptors to suppress
output to stdout/stderr, optionally redirecting to the named file(s).
Example::
with Silence():
# do something that prints to stdout or stderr
"""
def __init__(self, stdout=os.devnull, stderr=os.devnull, mode='wb'):
self.outfiles = stdout, stderr
self.combine = (stdout == stderr)
self.mode = mode
def __enter__(self):
import sys
self.sys = sys
# save previous stdout/stderr
self.saved_streams = saved_streams = sys.__stdout__, sys.__stderr__
self.fds = fds = [s.fileno() for s in saved_streams]
self.saved_fds = map(os.dup, fds)
# flush any pending output
for s in saved_streams:
s.flush()
# open surrogate files
if self.combine:
null_streams = [open(self.outfiles[0], self.mode, 0)] * 2
if self.outfiles[0] != os.devnull:
# disable buffering so output is merged immediately
sys.stdout, sys.stderr = map(os.fdopen, fds, ['w']*2, [0]*2)
else:
null_streams = [open(f, self.mode, 0) for f in self.outfiles]
self.null_fds = null_fds = [s.fileno() for s in null_streams]
self.null_streams = null_streams
# overwrite file objects and low-level file descriptors
map(os.dup2, null_fds, fds)
def __exit__(self, *args):
sys = self.sys
# flush any pending output
for s in self.saved_streams:
s.flush()
# restore original streams and file descriptors
map(os.dup2, self.saved_fds, self.fds)
sys.stdout, sys.stderr = self.saved_streams
# clean up
for s in self.null_streams:
s.close()
for fd in self.saved_fds:
os.close(fd)
return False
class LogFormatter(logging.Formatter):
# this is the default format to use for logging
log_format = (
"%(asctime)s %(levelname)s %(module)s:%(lineno)d - %(message)s")
def __init__(self):
super(LogFormatter, self).__init__(self.log_format)
def formatTime(self, record, datefmt=None):
ct = self.converter(record.created)
if datefmt:
s = time.strftime(datefmt, ct)
else:
t = time.strftime("%H:%M:%S", ct)
s = "%s.%03d" % (t, record.msecs)
return s
class Timer(object):
"""A context-manager that times a block of code, writing the results to
the log."""
def __init__(self, code_name, log_level=logging.DEBUG):
self.code_name = code_name
self.log_level = log_level
self.start = 0
self.logger = get_debug_logger()
def __enter__(self):
self.start = timeit.default_timer()
def __exit__(self, *args):
self.end = timeit.default_timer()
elapsed = self.end - self.start
self.logger.log(
self.log_level, "'%s' took %.3fs", self.code_name, elapsed)
class StagnantStateDetector(object):
"""Detect when the state of something doesn't change over many iterations.
Example of use::
state_check = StagnantStateDetector(threshold=5)
x, y = get_current_position()
while not at_position(target_x, target_y):
move_toward_position(target_x, target_y)
x, y = get_current_position()
try:
# this will raise an exception after the current position
# hasn't changed on the 6th time the check is performed.
loop_detector.check_state(x, y)
except StagnantStateDetector.StagnantState as e:
e.args = ("Position has not moved.", )
raise
"""
class StagnantState(Exception):
pass
def __init__(self, threshold):
"""
:param threshold: Amount of times the updated state can fail to
differ consecutively before raising an exception.
:raises ValueError: if *threshold* isn't a positive integer.
"""
if type(threshold) is not int or threshold <= 0:
raise ValueError("Threshold must be a positive integer.")
self._threshold = threshold
self._stagnant_count = 0
self._previous_state_hash = -1
def check_state(self, *state):
"""Check if there is a difference between the previous state and
state.
:param state: Hashable state argument to compare against the previous
iteration
:raises TypeError: when state is unhashable
"""
state_hash = hash(state)
if state_hash == self._previous_state_hash:
self._stagnant_count += 1
if self._stagnant_count >= self._threshold:
raise StagnantStateDetector.StagnantState(
"State has been the same for %d iterations"
% self._threshold
)
else:
self._stagnant_count = 0
self._previous_state_hash = state_hash
def get_debug_logger():
"""Get a logging object to be used as a debug logger only.
:returns: logger object from logging module
"""
logger = logging.getLogger("autopilot.debug")
logger.addFilter(DebugLogFilter())
return logger
class DebugLogFilter(object):
"""A filter class for the logging framework that allows us to turn off the
debug log.
"""
debug_log_enabled = False
def filter(self, record):
return int(self.debug_log_enabled)
def deprecated(alternative):
"""Write a deprecation warning to the logging framework."""
def fdec(fn):
@wraps(fn)
def wrapped(*args, **kwargs):
outerframe_details = inspect.getouterframes(
inspect.currentframe())[1]
filename, line_number, function_name = outerframe_details[1:4]
logger.warning(
"WARNING: in file \"{0}\", line {1} in {2}\n"
"This function is deprecated. Please use '{3}' instead.\n"
.format(filename, line_number, function_name, alternative)
)
return fn(*args, **kwargs)
return wrapped
return fdec
class _CleanupWrapper(object):
"""Support for calling 'addCleanup' outside the test case."""
def __init__(self):
self._test_instance = None
def __call__(self, callable, *args, **kwargs):
if self._test_instance is None:
raise RuntimeError(
"Out-of-test addCleanup can only be called while an autopilot "
"test case is running!")
self._test_instance.addCleanup(callable, *args, **kwargs)
def set_test_instance(self, test_instance):
self._test_instance = test_instance
test_instance.addCleanup(self._on_test_ended)
def _on_test_ended(self):
self._test_instance = None
addCleanup = _CleanupWrapper()
_cleanup_objects = []
class _TestCleanupMeta(type):
"""Metaclass to inject the object into on test start/end functionality."""
def __new__(cls, classname, bases, classdict):
class EmptyStaticMethod(object):
"""Class used to give us 'default classmethods' for those that
don't provide them.
"""
def __get__(self, obj, klass=None):
if klass is None:
klass = type(obj)
def place_holder_method(*args):
pass
return place_holder_method
default_methods = {
'on_test_start': EmptyStaticMethod(),
'on_test_end': EmptyStaticMethod(),
}
default_methods.update(classdict)
class_object = type.__new__(cls, classname, bases, default_methods)
_cleanup_objects.append(class_object)
return class_object
CleanupRegistered = _TestCleanupMeta('CleanupRegistered', (object,), {})
def action_on_test_start(test_instance):
import sys
for obj in _cleanup_objects:
try:
obj.on_test_start(test_instance)
except KeyboardInterrupt:
raise
except:
test_instance._report_traceback(sys.exc_info())
def action_on_test_end(test_instance):
import sys
for obj in _cleanup_objects:
try:
obj.on_test_end(test_instance)
except KeyboardInterrupt:
raise
except:
test_instance._report_traceback(sys.exc_info())
def on_test_started(test_case_instance):
test_case_instance.addCleanup(action_on_test_end, test_case_instance)
action_on_test_start(test_case_instance)
addCleanup.set_test_instance(test_case_instance)
class MockableSleep(object):
"""Delay execution for a certain number of seconds.
Functionally identical to `time.sleep`, except we can replace it during
unit tests.
To delay execution for 10 seconds, use it like this::
from autopilot.utilities import sleep
sleep(10)
To mock out all calls to sleep, one might do this instead::
from autopilot.utilities import sleep
with sleep.mocked() as mock_sleep:
sleep(10) # actually does nothing!
self.assertEqual(mock_sleep.total_time_slept(), 10.0)
"""
def __init__(self):
self._mock_count = 0.0
self._mocked = False
def __call__(self, t):
if not self._mocked:
time.sleep(t)
else:
self._mock_count += t
@contextmanager
def mocked(self):
self.enable_mock()
try:
yield self
finally:
self.disable_mock()
def enable_mock(self):
self._mocked = True
self._mock_count = 0.0
def disable_mock(self):
self._mocked = False
def total_time_slept(self):
return self._mock_count
sleep = MockableSleep()
@decorator
def compatible_repr(f, *args, **kwargs):
result = f(*args, **kwargs)
if not isinstance(result, str):
return result.decode('utf-8')
return result
def _raise_on_unknown_kwargs(kwargs):
"""Raise ValueError on unknown keyword arguments.
The standard use case is to warn callers that they've passed an unknown
keyword argument. For example::
def my_function(**kwargs):
known_option = kwargs.pop('known_option')
_raise_on_unknown_kwargs(kwargs)
Given the code above, this will not raise any exceptions::
my_function(known_option=123)
...but this code *will* raise a ValueError::
my_function(known_option=123, other_option=456)
"""
if kwargs:
arglist = [repr(k) for k in kwargs.keys()]
arglist.sort()
raise ValueError(
"Unknown keyword arguments: %s." % (', '.join(arglist))
)
class cached_result(object):
"""A simple caching decorator.
This class is deliberately simple. It does not handle unhashable types,
keyword arguments, and has no built-in size control.
"""
def __init__(self, f):
functools.update_wrapper(self, f)
self.f = f
self._cache = {}
def __call__(self, *args):
try:
return self._cache[args]
except KeyError:
result = self.f(*args)
self._cache[args] = result
return result
except TypeError:
raise TypeError(
"The '%r' function can only be called with hashable arguments."
)
def reset_cache(self):
self._cache.clear()
class EventDelay(object):
"""Delay execution of a subsequent event for a certain period
of time.
To delay the execution of a subsequent event for two seconds
use it like this::
from autopilot.utilities import EventDelay
event_delayer = EventDelay()
def print_something():
event_delayer.delay(2)
print("Hi! I am an event.")
print_something()
# It will take 2 seconds for second print()
# to happen.
print_something()
"""
def __init__(self):
self._last_event = 0.0
@contextmanager
def mocked(self):
"""Enable mocking for the EventDelay class
Also mocks all calls to autopilot.utilities.sleep.
One my use it like::
from autopilot.utilities import EventDelay
event_delayer = EventDelay()
with event_delayer.mocked() as mocked_delay:
event_delayer.delay(3)
# This call will return instantly as the sleep
# is mocked, just updating the _last_event variable.
event_delayer.delay(10)
self.assertThat(mocked_delay._last_event, GreaterThan(0.0))
"""
sleep.enable_mock()
try:
yield self
finally:
sleep.disable_mock()
def last_event_time(self):
"""return the time when delay() was last called."""
return self._last_event
def delay(self, duration, current_time=time.monotonic):
"""Delay the next event for a given amount of time.
To humanize events, so that if a certain action is repeated
continuously, there is a delay between each subsequent action.
:param duration: Time interval between events.
:param current_time: Specify the block of time to use as relative
time. It is a float, representing time with precision of
microseconds. Only for testing purpose. Default value is the
monotonic time. 0.1 is the tenth part of a second.
:raises ValueError: If the time stopped or went back since last
event.
"""
monotime = current_time()
_raise_if_time_delta_not_sane(monotime, self._last_event)
time_slept = 0.0
if monotime < (self._last_event + duration):
time_slept = _sleep_for_calculated_delta(
monotime,
self._last_event,
duration
)
self._last_event = monotime + time_slept
def _raise_if_time_delta_not_sane(current_time, last_event_time):
"""Will raise a ValueError exception if current_time is before
the last event or equal to it.
"""
if current_time == last_event_time:
raise ValueError(
'current_time must be more than the last event time.'
)
elif current_time < last_event_time:
raise ValueError(
'current_time must not be behind the last event time.'
)
def _sleep_for_calculated_delta(current_time, last_event_time, gap_duration):
"""Sleep for the remaining time between the last event time
and duration.
Given a duration in fractional seconds, ensure that at least
that given amount of time occurs since the last event time.
e.g. If 4 seconds have elapsed since the last event and the
requested gap duration was 10 seconds, sleep for 6 seconds.
:param float current_timestamp: Current monotonic time,
in fractional seconds, used to calculate the time delta
since last event.
:param float last_event_timestamp: The last timestamp that
the previous delay occured.
:param float gap_duration: Maximum time, in fractional seconds,
to be slept between two events.
:return: Time, in fractional seconds, for which sleep happened.
:raises ValueError: If last_event_time equals current_time or
is ahead of current_time.
"""
_raise_if_time_delta_not_sane(current_time, last_event_time)
time_delta = (last_event_time + gap_duration) - current_time
if time_delta > 0.0:
sleep(time_delta)
return time_delta
else:
return 0.0
class MockableProcessIter:
def __init__(self):
self._mocked = False
self._fake_processes = []
def __call__(self):
if not self._mocked:
return psutil.process_iter()
else:
return self.mocked_process_iter()
@contextmanager
def mocked(self, fake_processes):
self.enable_mock(fake_processes)
try:
yield self
finally:
self.disable_mock()
def enable_mock(self, fake_processes):
self._mocked = True
self._fake_processes = fake_processes
def disable_mock(self):
self._mocked = False
self._fake_processes = []
def create_mock_process(self, name, pid):
mock_process = Mock()
mock_process.name = lambda: name
mock_process.pid = pid
return mock_process
def mocked_process_iter(self):
for process in self._fake_processes:
yield self.create_mock_process(
process.get('name'),
process.get('pid')
)
process_iter = MockableProcessIter()
./autopilot/__init__.py 0000644 0000041 0000041 00000001755 13061552435 015410 0 ustar www-data www-data # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Autopilot Functional Test Tool
# Copyright (C) 2012-2013 Canonical
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
from autopilot._info import get_version_string, have_vis, version
from autopilot._config import get_test_configuration
__all__ = [
'get_test_configuration',
'get_version_string',
'have_vis',
'version',
]
./autopilot/testresult.py 0000644 0000041 0000041 00000011535 13061552435 016064 0 ustar www-data www-data # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Autopilot Functional Test Tool
# Copyright (C) 2012-2013 Canonical
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
"""Autopilot test result classes"""
import logging
from testtools import (
ExtendedToOriginalDecorator,
ExtendedToStreamDecorator,
TestResultDecorator,
TextTestResult,
try_import,
)
from autopilot.globals import get_log_verbose
from autopilot.utilities import _raise_on_unknown_kwargs
class LoggedTestResultDecorator(TestResultDecorator):
"""A decorator that logs messages to python's logging system."""
def _log(self, level, message):
"""Perform the actual message logging."""
if get_log_verbose():
logging.getLogger().log(level, message)
def _log_details(self, level, test):
"""Log the relavent test details."""
if hasattr(test, "getDetails"):
details = test.getDetails()
for detail in details:
# Skip the test-log as it was logged while the test executed
if detail == "test-log":
continue
detail_content = details[detail]
if detail_content.content_type.type == "text":
text = "%s: {{{\n%s}}}" % (
detail,
detail_content.as_text()
)
else:
text = "Binary attachment: \"%s\" (%s)" % (
detail,
detail_content.content_type
)
self._log(level, text)
def addSuccess(self, test, details=None):
self._log(logging.INFO, "OK: %s" % (test.id()))
return super().addSuccess(test, details)
def addError(self, test, err=None, details=None):
self._log(logging.ERROR, "ERROR: %s" % (test.id()))
self._log_details(logging.ERROR, test)
return super().addError(test, err, details)
def addFailure(self, test, err=None, details=None):
"""Called for a test which failed an assert."""
self._log(logging.ERROR, "FAIL: %s" % (test.id()))
self._log_details(logging.ERROR, test)
return super().addFailure(test, err, details)
def addSkip(self, test, reason=None, details=None):
self._log(logging.INFO, "SKIP: %s" % test.id())
return super().addSkip(test, reason, details)
def addUnexpectedSuccess(self, test, details=None):
self._log(logging.ERROR, "UNEXPECTED SUCCESS: %s" % test.id())
self._log_details(logging.ERROR, test)
return super().addUnexpectedSuccess(test, details)
def addExpectedFailure(self, test, err=None, details=None):
self._log(logging.INFO, "EXPECTED FAILURE: %s" % test.id())
return super().addExpectedFailure(test, err, details)
def get_output_formats():
"""Get information regarding the different output formats supported.
:returns: dict of supported formats and appropriate construct functions
"""
supported_formats = {}
supported_formats['text'] = _construct_text
if try_import('junitxml'):
supported_formats['xml'] = _construct_xml
if try_import('subunit'):
supported_formats['subunit'] = _construct_subunit
return supported_formats
def get_default_format():
return 'text'
def _construct_xml(**kwargs):
from junitxml import JUnitXmlResult
stream = kwargs.pop('stream')
failfast = kwargs.pop('failfast')
_raise_on_unknown_kwargs(kwargs)
result_object = LoggedTestResultDecorator(
ExtendedToOriginalDecorator(
JUnitXmlResult(stream)
)
)
result_object.failfast = failfast
return result_object
def _construct_text(**kwargs):
stream = kwargs.pop('stream')
failfast = kwargs.pop('failfast')
_raise_on_unknown_kwargs(kwargs)
return LoggedTestResultDecorator(TextTestResult(stream, failfast))
def _construct_subunit(**kwargs):
from subunit import StreamResultToBytes
stream = kwargs.pop('stream')
failfast = kwargs.pop('failfast')
_raise_on_unknown_kwargs(kwargs)
result_object = LoggedTestResultDecorator(
ExtendedToStreamDecorator(
StreamResultToBytes(stream)
)
)
result_object.failfast = failfast
return result_object
./autopilot/dbus_handler.py 0000644 0000041 0000041 00000003475 13061552435 016304 0 ustar www-data www-data # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Autopilot Functional Test Tool
# Copyright (C) 2012-2013 Canonical
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
"""Initialise dbus once using glib mainloop."""
from dbus._dbus import BusConnection
import dbus
from dbus.mainloop.glib import DBusGMainLoop
_glib_loop_set = False
# DBus has an annoying bug where we need to initialise it with the gobject main
# loop *before* it's initialised anywhere else. This module exists so we can
# initialise the dbus module once, and once only.
def _ensure_glib_loop_set():
global _glib_loop_set
if not _glib_loop_set:
DBusGMainLoop(set_as_default=True)
_glib_loop_set = True
def get_session_bus():
"""Return a session bus that has had the DBus GLib main loop
initialised.
"""
_ensure_glib_loop_set()
return dbus.SessionBus()
def get_system_bus():
"""Return a system bus that has had the DBus GLib main loop
initialised.
"""
_ensure_glib_loop_set()
return dbus.SystemBus()
def get_custom_bus(bus_address):
"""Return a custom bus that has had the DBus GLib main loop
initialised.
"""
_ensure_glib_loop_set()
return BusConnection(bus_address)
./autopilot/gestures.py 0000644 0000041 0000041 00000005221 13061552435 015502 0 ustar www-data www-data # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Autopilot Functional Test Tool
# Copyright (C) 2012-2013 Canonical
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
"""Gestural support for autopilot.
This module contains functions that can generate touch and multi-touch gestures
for you. This is a convenience for the test author - there is nothing to
prevent you from generating your own gestures!
"""
from autopilot.input import Touch
from autopilot.utilities import sleep
def pinch(center, vector_start, vector_end):
"""Perform a two finger pinch (zoom) gesture.
:param center: The coordinates (x,y) of the center of the pinch gesture.
:param vector_start: The (x,y) values to move away from the center for the
start.
:param vector_end: The (x,y) values to move away from the center for the
end.
The fingers will move in 100 steps between the start and the end points.
If start is smaller than end, the gesture will zoom in, otherwise it
will zoom out.
"""
finger_1_start = [center[0] - vector_start[0], center[1] - vector_start[1]]
finger_2_start = [center[0] + vector_start[0], center[1] + vector_start[1]]
finger_1_end = [center[0] - vector_end[0], center[1] - vector_end[1]]
finger_2_end = [center[0] + vector_end[0], center[1] + vector_end[1]]
dx = 1.0 * (finger_1_end[0] - finger_1_start[0]) / 100
dy = 1.0 * (finger_1_end[1] - finger_1_start[1]) / 100
finger_1 = Touch.create()
finger_2 = Touch.create()
finger_1.press(*finger_1_start)
finger_2.press(*finger_2_start)
finger_1_cur = [finger_1_start[0] + dx, finger_1_start[1] + dy]
finger_2_cur = [finger_2_start[0] - dx, finger_2_start[1] - dy]
for i in range(0, 100):
finger_1.move(*finger_1_cur)
finger_2.move(*finger_2_cur)
sleep(0.005)
finger_1_cur = [finger_1_cur[0] + dx, finger_1_cur[1] + dy]
finger_2_cur = [finger_2_cur[0] - dx, finger_2_cur[1] - dy]
finger_1.move(*finger_1_end)
finger_2.move(*finger_2_end)
finger_1.release()
finger_2.release()
./autopilot/globals.py 0000644 0000041 0000041 00000004366 13061552435 015275 0 ustar www-data www-data # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Autopilot Functional Test Tool
# Copyright (C) 2012-2014 Canonical
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
from autopilot._debug import DebugProfile
import logging
logger = logging.getLogger(__name__)
_log_verbose = False
def get_log_verbose():
"""Return true if the user asked for verbose logging."""
global _log_verbose
return _log_verbose
def set_log_verbose(verbose):
"""Set whether or not we should log verbosely."""
global _log_verbose
if type(verbose) is not bool:
raise TypeError("Verbose flag must be a boolean.")
_log_verbose = verbose
_debug_profile_fixture = DebugProfile
def set_debug_profile_fixture(fixture_class):
global _debug_profile_fixture
_debug_profile_fixture = fixture_class
def get_debug_profile_fixture():
global _debug_profile_fixture
return _debug_profile_fixture
_default_timeout_value = 10
def set_default_timeout_period(new_timeout):
global _default_timeout_value
_default_timeout_value = new_timeout
def get_default_timeout_period():
global _default_timeout_value
return _default_timeout_value
_long_timeout_value = 30
def set_long_timeout_period(new_timeout):
global _long_timeout_value
_long_timeout_value = new_timeout
def get_long_timeout_period():
global _long_timeout_value
return _long_timeout_value
# The timeout to apply to each test. 0 means no timeout. Value is in seconds.
_test_timeout = 0
def set_test_timeout(new_timeout):
global _test_timeout
_test_timeout = new_timeout
def get_test_timeout():
global _test_timeout
return _test_timeout
./autopilot/_debug.py 0000644 0000041 0000041 00000007752 13061552435 015101 0 ustar www-data www-data # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Autopilot Functional Test Tool
# Copyright (C) 2014 Canonical
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
"""Support for debug profiles.
Debug profiles are used to attach various items of debug information to
a test result. Profiles are named, but the names are for human
consumption only, and have no other significance.
Each piece of debug information is also a fixture, so debug profiles are
fixtures of fixtures!
"""
from autopilot.content import follow_file
from autopilot._fixtures import FixtureWithDirectAddDetail
class CaseAddDetailToNormalAddDetailDecorator(object):
"""A decorator object to turn a FixtureWithDirectAddDetail object into
an object that supports addDetail.
"""
def __init__(self, decorated):
self.decorated = decorated
def addDetail(self, name, content):
return self.decorated.caseAddDetail(name, content)
def __repr__(self):
return '<%s %r>' % (self.__class__.__name__, self.decorated)
def __getattr__(self, name):
return getattr(self.decorated, name)
class DebugProfile(FixtureWithDirectAddDetail):
"""A debug profile that contains manny debug objects."""
name = ""
def __init__(self, caseAddDetail, debug_fixtures=[]):
"""Create a debug profile.
:param caseAddDetail: A closure over the testcase's addDetail
method, or a similar substitution method.
:param debug_fixtures: a list of fixture class objects, each one will
be set up when this debug profile is used.
"""
super(DebugProfile, self).__init__(caseAddDetail)
self.debug_fixtures = debug_fixtures
def setUp(self):
super(DebugProfile, self).setUp()
for FixtureClass in self.debug_fixtures:
self.useFixture(FixtureClass(self.caseAddDetail))
class NormalDebugProfile(DebugProfile):
name = "normal"
def __init__(self, caseAddDetail):
super(NormalDebugProfile, self).__init__(
caseAddDetail,
[
SyslogDebugObject,
],
)
class VerboseDebugProfile(DebugProfile):
name = "verbose"
def __init__(self, caseAddDetail):
super(VerboseDebugProfile, self).__init__(
caseAddDetail,
[
SyslogDebugObject,
],
)
def get_default_debug_profile():
return NormalDebugProfile
def get_all_debug_profiles():
return {
NormalDebugProfile,
VerboseDebugProfile,
}
class DebugObject(FixtureWithDirectAddDetail):
"""A single piece of debugging information."""
class LogFileDebugObject(DebugObject):
"""Monitors a log file on disk."""
def __init__(self, caseAddDetail, log_path):
"""Create a debug object that will monitor the contents of a log
file on disk.
:param caseAddDetail: A closure over the testcase's addDetail
method, or a similar substitution method.
:param log_path: The path to monitor.
"""
super(LogFileDebugObject, self).__init__(caseAddDetail)
self.log_path = log_path
def setUp(self):
super(LogFileDebugObject, self).setUp()
follow_file(
self.log_path,
CaseAddDetailToNormalAddDetailDecorator(self)
)
def SyslogDebugObject(caseAddDetail):
return LogFileDebugObject(caseAddDetail, "/var/log/syslog")
./autopilot/vis/ 0000755 0000041 0000041 00000000000 13061552435 014070 5 ustar www-data www-data ./autopilot/vis/objectproperties.py 0000644 0000041 0000041 00000016441 13061552435 020033 0 ustar www-data www-data # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Autopilot Functional Test Tool
# Copyright (C) 2012-2013 Canonical
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
"""Code for introspection tree object properties."""
from PyQt4 import QtGui, QtCore
from autopilot.vis.resources import get_qt_icon, dbus_string_rep
from autopilot.introspection.qt import QtObjectProxyMixin
__all__ = ['TreeNodeDetailWidget']
class TreeNodeDetailWidget(QtGui.QTabWidget):
"""A widget that shows tree node details."""
def __init__(self, parent):
super(TreeNodeDetailWidget, self).__init__(parent)
self.views = []
for view_class in ALL_VIEWS:
view = view_class()
self.views.append(view)
def tree_node_changed(self, new_node):
"""Call when the selected tree node has changed.
This method will update the available tabs to reflect those suitable
for the new tree node selected.
"""
for view in self.views:
view_tab_idx = self.indexOf(view)
if view_tab_idx == -1:
# view is not currently shown.
if view.is_relevant(new_node):
self.addTab(view, view.icon(), view.name())
else:
# view is in tab bar already.
if not view.is_relevant(new_node):
self.removeTab(view_tab_idx)
for i in range(self.count()):
self.widget(i).new_node_selected(new_node)
class AbstractView(QtGui.QWidget):
"""An abstract class that outlines the methods required to be used in the
details view widget.
"""
def name(self):
"""Return the name of the view."""
raise NotImplementedError(
"This method must be implemented in subclasses!")
def icon(self):
"""Return the icon for the view (optionsla)."""
return QtGui.QIcon()
def is_relevant(self, node):
"""Return true if the view can display data about this tree node."""
raise NotImplementedError(
"This method must be implemented in subclasses!")
def new_node_selected(self, node):
raise NotImplementedError(
"This method must be implemented in subclasses!")
class PropertyView(AbstractView):
"""A view that displays the basic exported object properties in a table."""
def __init__(self, *args, **kwargs):
super(PropertyView, self).__init__(*args, **kwargs)
header_titles = ["Name", "Value"]
self.details_layout = QtGui.QVBoxLayout(self)
self.table_view = QtGui.QTableWidget()
self.table_view.setColumnCount(2)
self.table_view.verticalHeader().setVisible(False)
self.table_view.setAlternatingRowColors(True)
self.table_view.setHorizontalHeaderLabels(header_titles)
self.table_view.setEditTriggers(QtGui.QAbstractItemView.NoEditTriggers)
self.details_layout.addWidget(self.table_view)
def name(self):
return "Properties"
def is_relevant(self, node):
return node is not None
def new_node_selected(self, node):
self.table_view.setSortingEnabled(False)
self.table_view.clearContents()
object_details = node.get_properties()
# remove the Children property - we don't care about it:
object_details.pop("Children", None)
self.table_view.setRowCount(len(object_details))
for i, key in enumerate(object_details):
details_string = dbus_string_rep(object_details[key])
item_name = QtGui.QTableWidgetItem(key)
item_details = QtGui.QTableWidgetItem(
details_string)
self.table_view.setItem(i, 0, item_name)
self.table_view.setItem(i, 1, item_details)
self.table_view.setSortingEnabled(True)
self.table_view.sortByColumn(0, QtCore.Qt.AscendingOrder)
self.table_view.resizeColumnsToContents()
class SignalView(AbstractView):
"""A view that exposes signals available on an object."""
def __init__(self, *args, **kwargs):
super(SignalView, self).__init__(*args, **kwargs)
self.details_layout = QtGui.QVBoxLayout(self)
self.signals_table = QtGui.QTableWidget()
self.signals_table.setColumnCount(1)
self.signals_table.verticalHeader().setVisible(False)
self.signals_table.setAlternatingRowColors(True)
self.signals_table.setHorizontalHeaderLabels(["Signal Signature"])
self.signals_table.setEditTriggers(
QtGui.QAbstractItemView.NoEditTriggers)
self.details_layout.addWidget(self.signals_table)
def name(self):
return "Signals"
def icon(self):
return get_qt_icon()
def is_relevant(self, node):
return node is not None and isinstance(node, QtObjectProxyMixin)
def new_node_selected(self, node):
self.signals_table.setSortingEnabled(False)
self.signals_table.clearContents()
signals = node.get_signals()
self.signals_table.setRowCount(len(signals))
for i, signal in enumerate(signals):
self.signals_table.setItem(
i, 0, QtGui.QTableWidgetItem(str(signal)))
self.signals_table.setSortingEnabled(True)
self.signals_table.sortByColumn(0, QtCore.Qt.AscendingOrder)
self.signals_table.resizeColumnsToContents()
class SlotView(AbstractView):
"""A View that exposes slots on an object."""
def __init__(self, *args, **kwargs):
super(SlotView, self).__init__(*args, **kwargs)
self.details_layout = QtGui.QVBoxLayout(self)
self.slots_table = QtGui.QTableWidget()
self.slots_table.setColumnCount(1)
self.slots_table.verticalHeader().setVisible(False)
self.slots_table.setAlternatingRowColors(True)
self.slots_table.setHorizontalHeaderLabels(["Slot Signature"])
self.slots_table.setEditTriggers(
QtGui.QAbstractItemView.NoEditTriggers)
self.details_layout.addWidget(self.slots_table)
def name(self):
return "Slots"
def icon(self):
return get_qt_icon()
def is_relevant(self, node):
return node is not None and isinstance(node, QtObjectProxyMixin)
def new_node_selected(self, node):
self.slots_table.setSortingEnabled(False)
self.slots_table.clearContents()
signals = node.get_slots()
self.slots_table.setRowCount(len(signals))
for i, signal in enumerate(signals):
self.slots_table.setItem(i, 0, QtGui.QTableWidgetItem(str(signal)))
self.slots_table.setSortingEnabled(True)
self.slots_table.sortByColumn(0, QtCore.Qt.AscendingOrder)
self.slots_table.resizeColumnsToContents()
ALL_VIEWS = [
PropertyView,
SignalView,
SlotView,
]
./autopilot/vis/dbus_search.py 0000644 0000041 0000041 00000010317 13061552435 016726 0 ustar www-data www-data # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Autopilot Functional Test Tool
# Copyright (C) 2013 Canonical
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
import logging
import os
from os.path import join
try:
import lxml.etree as ET
except ImportError:
from xml.etree import ElementTree as ET
_logger = logging.getLogger(__name__)
class DBusInspector(object):
def __init__(self, bus):
self._bus = bus
self._xml_processor = None
self.p_dbus = self._bus.get_object('org.freedesktop.DBus', '/')
def set_xml_processor(self, processor):
self._xml_processor = processor
def __call__(self, conn_name, obj_name='/'):
"""Introspects and applies the reply_handler to the dbus object
constructed from the provided bus, connection and object name.
"""
handler = lambda xml: self._xml_processor(
conn_name,
obj_name,
xml
)
error_handler = lambda *args: _logger.error("Error occured: %r" % args)
obj = self._bus.get_object(conn_name, obj_name)
# avoid introspecting our own PID, as that locks up with libdbus
try:
obj_pid = self.p_dbus.GetConnectionUnixProcessID(
conn_name,
dbus_interface='org.freedesktop.DBus'
)
if obj_pid == os.getpid():
return
except:
# can't get D-BUS daemon's own pid, ignore
pass
obj.Introspect(
dbus_interface='org.freedesktop.DBus.Introspectable',
reply_handler=handler,
error_handler=error_handler
)
class XmlProcessor(object):
def __init__(self):
self._dbus_inspector = None
self._success_callback = None
def set_dbus_inspector(self, inspector):
self._dbus_inspector = inspector
def set_success_callback(self, callback):
"""Must be a callable etc."""
self._success_callback = callback
def __call__(self, conn_name, obj_name, xml):
try:
root = ET.fromstring(xml)
for child in root.getchildren():
try:
child_name = join(obj_name, child.attrib['name'])
except KeyError:
continue
# If we found another node, make sure we get called again with
# a new XML block.
if child.tag == 'node':
self._dbus_inspector(conn_name, child_name)
# If we found an interface, call our success function with the
# interface name
elif child.tag == 'interface':
iface_name = child_name.split('/')[-1]
self._success_callback(conn_name, obj_name, iface_name)
except ET.ParseError:
_logger.warning(
"Unable to parse XML response for %s (%s)"
% (conn_name, obj_name)
)
def _start_trawl(bus, connection_name, on_success_cb):
"""Start searching *connection_name* on *bus* for interfaces under
org.freedesktop.DBus.Introspectable.
on_success_cb gets called when an interface is found.
"""
if connection_name is None:
raise ValueError("Connection name is required.")
if not callable(on_success_cb):
raise ValueError("on_success_cb needs to be callable.")
dbus_inspector = DBusInspector(bus)
xml_processor = XmlProcessor()
dbus_inspector.set_xml_processor(xml_processor)
xml_processor.set_dbus_inspector(dbus_inspector)
xml_processor.set_success_callback(on_success_cb)
dbus_inspector(connection_name)
./autopilot/vis/resources.py 0000644 0000041 0000041 00000004453 13061552435 016462 0 ustar www-data www-data # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Autopilot Functional Test Tool
# Copyright (C) 2012-2013 Canonical
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
import os.path
import dbus
from PyQt4 import QtGui
def get_qt_icon():
return QtGui.QIcon(":/trolltech/qmessagebox/images/qtlogo-64.png")
def get_filter_icon():
return QtGui.QIcon("/usr/share/icons/gnome/32x32/actions/search.png")
def get_overlay_icon():
name = "autopilot-toggle-overlay.svg"
possible_locations = [
"/usr/share/icons/hicolor/scalable/actions/",
os.path.join(os.path.dirname(__file__), '../../icons'),
"icons",
]
for location in possible_locations:
path = os.path.join(location, name)
if os.path.exists(path):
return QtGui.QIcon(path)
return QtGui.QIcon()
def dbus_string_rep(dbus_type):
"""Get a string representation of various dbus types."""
if isinstance(dbus_type, dbus.Boolean):
return repr(bool(dbus_type))
if isinstance(dbus_type, dbus.String):
return dbus_type.encode('utf-8', errors='ignore').decode('utf-8')
if (isinstance(dbus_type, dbus.Int16) or
isinstance(dbus_type, dbus.UInt16) or
isinstance(dbus_type, dbus.Int32) or
isinstance(dbus_type, dbus.UInt32) or
isinstance(dbus_type, dbus.Int64) or
isinstance(dbus_type, dbus.UInt64)):
return repr(int(dbus_type))
if isinstance(dbus_type, dbus.Double):
return repr(float(dbus_type))
if (isinstance(dbus_type, dbus.Array) or
isinstance(dbus_type, dbus.Struct)):
return ', '.join([dbus_string_rep(i) for i in dbus_type])
else:
return repr(dbus_type)
./autopilot/vis/main_window.py 0000644 0000041 0000041 00000047361 13061552435 016770 0 ustar www-data www-data # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Autopilot Functional Test Tool
# Copyright (C) 2012-2013 Canonical
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
import dbus
import logging
from PyQt4 import QtGui, QtCore
from autopilot.exceptions import StateNotFoundError
from autopilot.introspection._search import (
_get_dbus_address_object,
_make_proxy_object_async
)
from autopilot.introspection.constants import AP_INTROSPECTION_IFACE
from autopilot.introspection.qt import QtObjectProxyMixin
from autopilot.vis.objectproperties import TreeNodeDetailWidget
from autopilot.vis.resources import (
get_filter_icon,
get_overlay_icon,
get_qt_icon,
)
_logger = logging.getLogger(__name__)
class MainWindow(QtGui.QMainWindow):
def __init__(self, dbus_bus):
super(MainWindow, self).__init__()
self.selectable_interfaces = {}
self.initUI()
self.readSettings()
self._dbus_bus = dbus_bus
self.proxy_object = None
def readSettings(self):
settings = QtCore.QSettings()
geometry = settings.value("geometry")
if geometry is not None:
self.restoreGeometry(geometry.data())
window_state = settings.value("windowState")
if window_state is not None:
self.restoreState(window_state.data())
try:
self.toggle_overlay_action.setChecked(
settings.value("overlayChecked", type="bool")
)
except TypeError:
pass
# raised when returned QVariant is invalid - probably on
# first boot
def closeEvent(self, event):
settings = QtCore.QSettings()
settings.setValue("geometry", self.saveGeometry())
settings.setValue("windowState", self.saveState())
settings.setValue(
"overlayChecked",
self.toggle_overlay_action.isChecked()
)
self.visual_indicator.close()
def initUI(self):
self.setWindowTitle("Autopilot Vis")
self.statusBar().showMessage('Waiting for first valid dbus connection')
self.splitter = QtGui.QSplitter(self)
self.splitter.setChildrenCollapsible(False)
self.tree_view = ProxyObjectTreeViewWidget(self.splitter)
self.detail_widget = TreeNodeDetailWidget(self.splitter)
self.splitter.setStretchFactor(0, 0)
self.splitter.setStretchFactor(1, 100)
self.setCentralWidget(self.splitter)
self.connection_list = ConnectionList()
self.connection_list.currentIndexChanged.connect(
self.conn_list_activated
)
self.toolbar = self.addToolBar('Connection')
self.toolbar.setObjectName('Connection Toolbar')
self.toolbar.addWidget(self.connection_list)
self.toolbar.addSeparator()
self.filter_widget = FilterPane()
self.filter_widget.apply_filter.connect(self.on_filter)
self.filter_widget.reset_filter.connect(self.on_reset_filter)
self.filter_widget.set_enabled(False)
self.addDockWidget(QtCore.Qt.RightDockWidgetArea, self.filter_widget)
self.toggle_dock_widget_action = self.filter_widget.toggleViewAction()
self.toggle_dock_widget_action.setText('Show/Hide Filter Pane')
self.toggle_dock_widget_action.setIcon(get_filter_icon())
self.toolbar.addAction(self.toggle_dock_widget_action)
self.visual_indicator = VisualComponentPositionIndicator()
self.toggle_overlay_action = self.toolbar.addAction(
get_overlay_icon(),
"Enable/Disable component overlay"
)
self.toggle_overlay_action.setCheckable(True)
self.toggle_overlay_action.toggled.connect(
self.visual_indicator.setEnabled
)
# our model object gets created later.
self.tree_model = None
def on_filter(self, node_name, filters):
node_name = str(node_name)
if self.proxy_object:
p = self.proxy_object.select_many(node_name)
self.tree_model.set_tree_roots(p)
self.tree_view.set_filtered(True)
# applying the filter will always invalidate the current overlay
self.visual_indicator.tree_node_changed(None)
def on_reset_filter(self):
self.tree_model.set_tree_roots([self.proxy_object])
self.tree_view.set_filtered(False)
# resetting the filter will always invalidate the current overlay
self.visual_indicator.tree_node_changed(None)
def on_interface_found(self, conn, obj, iface):
if iface == AP_INTROSPECTION_IFACE:
self.statusBar().showMessage('Updating connection list')
try:
dbus_address_instance = _get_dbus_address_object(
str(conn), str(obj), self._dbus_bus)
_make_proxy_object_async(
dbus_address_instance,
None,
self.on_proxy_object_built,
self.on_dbus_error
)
except (dbus.DBusException, RuntimeError) as e:
_logger.warning("Invalid introspection interface: %s" % str(e))
if self.connection_list.count() == 0:
self.statusBar().showMessage('No valid connections exist.')
def on_proxy_object_built(self, proxy_object):
cls_name = proxy_object.__class__.__name__
if cls_name not in self.selectable_interfaces:
self.selectable_interfaces[cls_name] = proxy_object
self.update_selectable_interfaces()
self.statusBar().clearMessage()
def on_dbus_error(*args):
print(args)
def update_selectable_interfaces(self):
selected_text = self.connection_list.currentText()
self.connection_list.clear()
self.connection_list.addItem("Please select a connection", None)
for name, proxy_obj in self.selectable_interfaces.items():
if isinstance(proxy_obj, QtObjectProxyMixin):
self.connection_list.addItem(
get_qt_icon(),
name,
proxy_obj
)
else:
self.connection_list.addItem(name, proxy_obj)
prev_selected = self.connection_list.findText(selected_text,
QtCore.Qt.MatchExactly)
if prev_selected == -1:
prev_selected = 0
self.connection_list.setCurrentIndex(prev_selected)
def conn_list_activated(self, index):
proxy_object = self.connection_list.itemData(index)
self.proxy_object = proxy_object
if self.proxy_object:
self.filter_widget.set_enabled(True)
self.tree_model = VisTreeModel(self.proxy_object)
self.tree_view.set_model(self.tree_model)
self.tree_view.selection_changed.connect(self.tree_item_changed)
else:
self.filter_widget.set_enabled(False)
self.tree_view.set_model(None)
self.visual_indicator.tree_node_changed(None)
self.detail_widget.tree_node_changed(None)
def tree_item_changed(self, current, previous):
tree_node = current.internalPointer()
proxy = tree_node.dbus_object if tree_node is not None else None
self.detail_widget.tree_node_changed(proxy)
self.visual_indicator.tree_node_changed(proxy)
class VisualComponentPositionIndicator(QtGui.QWidget):
def __init__(self, parent=None):
super(VisualComponentPositionIndicator, self).__init__(None)
self.setWindowFlags(
QtCore.Qt.Window |
QtCore.Qt.X11BypassWindowManagerHint |
QtCore.Qt.FramelessWindowHint |
QtCore.Qt.WindowStaysOnTopHint
)
self.setAttribute(QtCore.Qt.WA_TranslucentBackground)
self.setStyleSheet(
"""\
QWidget {
background-color: rgba(253, 255, 225, 128);
}
"""
)
self.enabled = False
self.proxy = None
def tree_node_changed(self, proxy):
self.proxy = proxy
self._maybe_update()
def paintEvent(self, paint_evt):
opt = QtGui.QStyleOption()
opt.init(self)
p = QtGui.QPainter(self)
self.style().drawPrimitive(
QtGui.QStyle.PE_Widget,
opt,
p,
self
)
def setEnabled(self, enabled):
self.enabled = enabled
self._maybe_update()
def _maybe_update(self):
"""Maybe update the visual overlay.
Several things need to be taken into account:
1. The state of the UI toggle button, which determines whether the
user expects us to be visible or not. Stored in 'self.enabled'
2. The current proxy object set, and whether it has a 'globalRect'
attribute (stored in self.proxy) - the proxy object may be None as
well.
"""
position = getattr(self.proxy, 'globalRect', None)
should_be_visible = self.enabled and (position is not None)
if should_be_visible:
self.setGeometry(*position)
if should_be_visible != self.isVisible():
self.setVisible(should_be_visible)
class ProxyObjectTreeView(QtGui.QTreeView):
"""A subclass of QTreeView with a few customisations."""
def __init__(self, parent=None):
super(ProxyObjectTreeView, self).__init__(parent)
self.setSelectionMode(QtGui.QAbstractItemView.SingleSelection)
self.header().setResizeMode(QtGui.QHeaderView.ResizeToContents)
self.header().setStretchLastSection(False)
def scrollTo(self, index, hint=QtGui.QAbstractItemView.EnsureVisible):
"""Scroll the view to make the node at index visible.
Overriden to stop autoScroll from horizontally jumping when selecting
nodes, and to make arrow navigation work correctly when scrolling off
the bottom of the viewport.
:param index: The node to be made visible.
:param hint: Where the visible item should be - this is ignored.
"""
# calculate the visual rect of the item we're scrolling to in viewport
# coordinates. The default implementation gives us a rect that ends
# in the RHS of the viewport, which isn't what we want. We use a
# QFontMetrics instance to calculate the probably width of the text
# beign rendered. This may not be totally accurate, but it seems good
# enough.
visual_rect = self.visualRect(index)
fm = self.fontMetrics()
text_width = fm.width(index.data())
visual_rect.setRight(visual_rect.left() + text_width)
# horizontal scrolling is done per-pixel, with the scrollbar value
# being the number of pixels past the RHS of the VP. For some reason
# one needs to add 8 pixels - possibly this is for the tree expansion
# widget?
hbar = self.horizontalScrollBar()
if visual_rect.right() + 8 > self.viewport().width():
offset = (visual_rect.right() -
self.viewport().width() +
hbar.value() + 8)
hbar.setValue(offset)
if visual_rect.left() < 0:
offset = hbar.value() + visual_rect.left() - 8
hbar.setValue(offset)
# Vertical scrollbar scrolls in steps equal to the height of each item
vbar = self.verticalScrollBar()
if visual_rect.bottom() > self.viewport().height():
offset_pixels = (visual_rect.bottom() -
self.viewport().height() +
vbar.value())
new_position = max(
offset_pixels / visual_rect.height(),
1
) + vbar.value()
vbar.setValue(new_position)
if visual_rect.top() < 0:
new_position = min(visual_rect.top() / visual_rect.height(), -1)
vbar.setValue(vbar.value() + new_position)
class ProxyObjectTreeViewWidget(QtGui.QWidget):
"""A Widget that contains a tree view and a few other things."""
selection_changed = QtCore.pyqtSignal('QModelIndex', 'QModelIndex')
def __init__(self, parent=None):
super(ProxyObjectTreeViewWidget, self).__init__(parent)
layout = QtGui.QVBoxLayout(self)
self.tree_view = ProxyObjectTreeView()
layout.addWidget(self.tree_view)
self.status_label = QtGui.QLabel("Showing Filtered Results ONLY")
self.status_label.hide()
layout.addWidget(self.status_label)
self.setLayout(layout)
def set_model(self, model):
self.tree_view.setModel(model)
self.tree_view.selectionModel().currentChanged.connect(
self.selection_changed
)
self.set_filtered(False)
def set_filtered(self, is_filtered):
if is_filtered:
self.status_label.show()
self.tree_view.setStyleSheet("""\
QTreeView {
background-color: #fdffe1;
}
""")
else:
self.status_label.hide()
self.tree_view.setStyleSheet("")
class ConnectionList(QtGui.QComboBox):
"""Used to show a list of applications we can connect to."""
def __init__(self):
super(ConnectionList, self).__init__()
self.setObjectName("ConnectionList")
self.setSizeAdjustPolicy(QtGui.QComboBox.AdjustToContents)
@QtCore.pyqtSlot(str)
def trySetSelectedItem(self, desired_text):
index = self.findText(desired_text)
if index != -1:
self.setCurrentIndex(index)
class TreeNode(object):
"""Used to represent the tree data structure that is the backend of the
treeview.
Lazy loads a nodes children instead of waiting to load and store a static
snapshot of the apps whole state.
"""
def __init__(self, parent=None, dbus_object=None):
self.parent = parent
self.name = dbus_object.__class__.__name__
self.dbus_object = dbus_object
self._children = None
@property
def children(self):
if self._children is None:
self._children = []
try:
for child in self.dbus_object.get_children():
self._children.append(TreeNode(self, child))
except StateNotFoundError:
pass
return self._children
@property
def num_children(self):
"""An optimisation that allows us to get the number of children without
actually retrieving them all. This is useful since Qt needs to know if
there are children (to draw the drop-down triangle thingie), but
doesn't need to know about the details.
"""
# Thomi - 2014-04-09 - the code below is subtly broken because
# libautopilot-qt returns items in the Children property that it never
# exports. I'm reverting this optimisation and doing the simple thing
# until that gets fixed.
# https://bugs.launchpad.net/autopilot-qt/+bug/1286985
return len(self.children)
# old code - re-enable once above bug has been fixed.
num_children = 0
with self.dbus_object.no_automatic_refreshing():
if hasattr(self.dbus_object, 'Children'):
num_children = len(self.dbus_object.Children)
return num_children
class VisTreeModel(QtCore.QAbstractItemModel):
def __init__(self, proxy_object):
"""Create a new proxy object tree model.
:param proxy_object: A DBus proxy object representing the root of the
tree to show.
"""
super(VisTreeModel, self).__init__()
self.tree_roots = [
TreeNode(dbus_object=proxy_object),
]
def set_tree_roots(self, new_tree_roots):
"""Call this method to change the root nodes the model shows.
:param new_tree_roots: An iterable of dbus proxy objects, each one will
be a root node in the model after calling this method.
"""
self.beginResetModel()
self.tree_roots = [TreeNode(dbus_object=r) for r in new_tree_roots]
self.endResetModel()
def index(self, row, col, parent):
if not self.hasIndex(row, col, parent):
return QtCore.QModelIndex()
# If there's no parent, return the root of our tree:
if not parent.isValid():
if row < len(self.tree_roots):
return self.createIndex(row, col, self.tree_roots[row])
else:
return QtCore.QModelIndex()
else:
parentItem = parent.internalPointer()
try:
childItem = parentItem.children[row]
return self.createIndex(row, col, childItem)
except IndexError:
return QtCore.QModelIndex()
def parent(self, index):
if not index.isValid():
return QtCore.QModelIndex()
childItem = index.internalPointer()
if not childItem:
return QtCore.QModelIndex()
parentItem = childItem.parent
if parentItem is None:
return QtCore.QModelIndex()
row = parentItem.children.index(childItem)
return self.createIndex(row, 0, parentItem)
def rowCount(self, parent):
if not parent.isValid():
return len(self.tree_roots)
else:
return parent.internalPointer().num_children
def columnCount(self, parent):
return 1
def data(self, index, role):
if not index.isValid():
return None
if role == QtCore.Qt.DisplayRole:
return index.internalPointer().name
def headerData(self, column, orientation, role):
if (orientation == QtCore.Qt.Horizontal and
role == QtCore.Qt.DisplayRole):
return "Tree Node"
return None
class FilterPane(QtGui.QDockWidget):
"""A widget that provides a filter UI."""
apply_filter = QtCore.pyqtSignal(str, list)
reset_filter = QtCore.pyqtSignal()
class ControlWidget(QtGui.QWidget):
def __init__(self, parent=None):
super(FilterPane.ControlWidget, self).__init__(parent)
self._layout = QtGui.QFormLayout(self)
self.node_name_edit = QtGui.QLineEdit()
self._layout.addRow(
QtGui.QLabel("Node Name:"),
self.node_name_edit
)
btn_box = QtGui.QDialogButtonBox()
self.apply_btn = btn_box.addButton(QtGui.QDialogButtonBox.Apply)
self.apply_btn.setDefault(True)
self.reset_btn = btn_box.addButton(QtGui.QDialogButtonBox.Reset)
self._layout.addRow(btn_box)
self.setLayout(self._layout)
def __init__(self, parent=None):
super(FilterPane, self).__init__("Filter Tree", parent)
self.setObjectName("FilterTreePane")
self.control_widget = FilterPane.ControlWidget(self)
self.control_widget.node_name_edit.returnPressed.connect(
self.on_apply_clicked
)
self.control_widget.apply_btn.clicked.connect(self.on_apply_clicked)
self.control_widget.reset_btn.clicked.connect(self.reset_filter)
self.setWidget(self.control_widget)
def on_apply_clicked(self):
node_name = self.control_widget.node_name_edit.text()
self.apply_filter.emit(node_name, [])
def set_enabled(self, enabled):
self.control_widget.setEnabled(enabled)
./autopilot/vis/bus_enumerator.py 0000644 0000041 0000041 00000005476 13061552435 017510 0 ustar www-data www-data # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Autopilot Functional Test Tool
# Copyright (C) 2013 Canonical
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
from collections import defaultdict
from autopilot.vis.dbus_search import _start_trawl
from PyQt4.QtCore import (
pyqtSignal,
QObject,
)
class BusEnumerator(QObject):
"""A simple utility class to support enumeration of all DBus connections,
objects, and interfaces.
Create an instance of ths class, and connect to the new_interface_found
signal.
"""
new_interface_found = pyqtSignal(str, str, str)
def __init__(self, bus):
super(BusEnumerator, self).__init__()
self._bus = bus
self._data = defaultdict(lambda: defaultdict(list))
def get_found_connections(self):
"""Get a list of found connection names. This may not be up to date."""
return list(self._data.keys())
def get_found_objects(self, connection_string):
"""Get a list of found objects for a particular connection name.
This may be out of date.
"""
if connection_string not in self._data:
raise KeyError("%s not in results" % connection_string)
return list(self._data[connection_string].keys())
def get_found_interfaces(self, connection_string, object_path):
"""Get a list of found interfaces for a particular connection name and
object path.
This may be out of date.
"""
if connection_string not in self._data:
raise KeyError("connection %s not in results" % connection_string)
if object_path not in self._data[connection_string]:
raise KeyError(
"object %s not in results for connection %s" %
(object_path, connection_string))
return self._data[connection_string][object_path]
def start_trawl(self):
"""Start trawling the bus for interfaces."""
for connection in self._bus.list_names():
_start_trawl(self._bus, connection, self._add_hit)
def _add_hit(self, conn_name, obj_name, interface_name):
self.new_interface_found.emit(conn_name, obj_name, interface_name)
self._data[conn_name][obj_name].append(interface_name)
./autopilot/vis/__init__.py 0000644 0000041 0000041 00000003034 13061552435 016201 0 ustar www-data www-data # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Autopilot Functional Test Tool
# Copyright (C) 2012-2013 Canonical
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
import dbus
import sys
def vis_main(extra_flags):
# To aid in testing only import when we are launching the GUI component
from dbus.mainloop.qt import DBusQtMainLoop
from PyQt4 import QtGui
from autopilot.vis.main_window import MainWindow
from autopilot.vis.bus_enumerator import BusEnumerator
app = QtGui.QApplication(sys.argv + extra_flags)
app.setApplicationName("Autopilot")
app.setOrganizationName("Canonical")
dbus_loop = DBusQtMainLoop()
session_bus = dbus.SessionBus(mainloop=dbus_loop)
window = MainWindow(session_bus)
bus_enumerator = BusEnumerator(session_bus)
bus_enumerator.new_interface_found.connect(window.on_interface_found)
bus_enumerator.start_trawl()
window.show()
sys.exit(app.exec_())
./autopilot/testcase.py 0000644 0000041 0000041 00000047701 13061552435 015465 0 ustar www-data www-data # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Autopilot Functional Test Tool
# Copyright (C) 2012-2014 Canonical
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
"""
Quick Start
===========
The :class:`AutopilotTestCase` is the main class test authors will be
interacting with. Every autopilot test case should derive from this class.
:class:`AutopilotTestCase` derives from :class:`testtools.TestCase`, so test
authors can use all the methods defined in that class as well.
**Writing tests**
Tests must be named: ``test_``, where ** is the name of the
test. Test runners (including autopilot itself) look for methods with this
naming convention. It is recommended that you make your test names descriptive
of what each test is testing. For example, possible test names include::
test_ctrl_p_opens_print_dialog
test_dash_remembers_maximized_state
**Launching the Application Under Test**
If you are writing a test for an application, you need to use the
:meth:`~AutopilotTestCase.launch_test_application` method. This will launch the
application, enable introspection, and return a proxy object representing the
root of the application introspection tree.
"""
import logging
import fixtures
from testscenarios import TestWithScenarios
from testtools import TestCase, RunTest
from testtools.content import ContentType, content_from_stream
from testtools.matchers import Equals
from testtools.testcase import _ExpectedFailure
from unittest.case import SkipTest
from autopilot.application import (
ClickApplicationLauncher,
NormalApplicationLauncher,
UpstartApplicationLauncher,
)
from autopilot.display import Display, get_screenshot_data
from autopilot.globals import get_debug_profile_fixture, get_test_timeout
from autopilot.input import Keyboard, Mouse
from autopilot.keybindings import KeybindingsHelper
from autopilot.matchers import Eventually
from autopilot.platform import get_display_server
from autopilot.process import ProcessManager
from autopilot.utilities import deprecated, on_test_started
from autopilot._fixtures import OSKAlwaysEnabled
from autopilot._timeout import Timeout
from autopilot._logging import TestCaseLoggingFixture
from autopilot._video import get_video_recording_fixture
try:
from autopilot import tracepoint as tp
HAVE_TRACEPOINT = True
except ImportError:
HAVE_TRACEPOINT = False
_logger = logging.getLogger(__name__)
try:
from testscenarios.scenarios import multiply_scenarios
except ImportError:
from itertools import product
def multiply_scenarios(*scenarios):
"""Multiply two or more iterables of scenarios.
It is safe to pass scenario generators or iterators.
:returns: A list of compound scenarios: the cross-product of all
scenarios, with the names concatenated and the parameters
merged together.
"""
result = []
scenario_lists = map(list, scenarios)
for combination in product(*scenario_lists):
names, parameters = zip(*combination)
scenario_name = ','.join(names)
scenario_parameters = {}
for parameter in parameters:
scenario_parameters.update(parameter)
result.append((scenario_name, scenario_parameters))
return result
def _lttng_trace_test_started(test_id):
if HAVE_TRACEPOINT:
tp.emit_test_started(test_id)
else:
_logger.warning(
"No tracing available - install the python-autopilot-trace "
"package!")
def _lttng_trace_test_ended(test_id):
if HAVE_TRACEPOINT:
tp.emit_test_ended(test_id)
class _TimedRunTest(RunTest):
def run(self, *args, **kwargs):
timeout = get_test_timeout()
if timeout > 0:
with fixtures.Timeout(timeout, True):
return super().run(*args, **kwargs)
else:
return super().run(*args, **kwargs)
class AutopilotTestCase(TestWithScenarios, TestCase, KeybindingsHelper):
"""Wrapper around testtools.TestCase that adds significant functionality.
This class should be the base class for all autopilot test case classes.
Not using this class as the base class disables several important
convenience methods, and also prevents the use of the failed-test
recording tools.
"""
run_tests_with = _TimedRunTest
def setUp(self):
super(AutopilotTestCase, self).setUp()
on_test_started(self)
self.useFixture(
TestCaseLoggingFixture(
self.shortDescription(),
self.addDetailUniqueName,
)
)
self.useFixture(get_debug_profile_fixture()(self.addDetailUniqueName))
self.useFixture(get_video_recording_fixture()(self))
_lttng_trace_test_started(self.id())
self.addCleanup(_lttng_trace_test_ended, self.id())
self._process_manager = None
self._mouse = None
self._display = None
self._kb = Keyboard.create()
# Instatiate this after keyboard creation to ensure it doesn't get
# overwritten
# Workaround for bug lp:1474444
self.useFixture(OSKAlwaysEnabled())
# Work around for bug lp:1297592.
_ensure_uinput_device_created()
if get_display_server() == 'X11':
try:
self._app_snapshot = _get_process_snapshot()
self.addCleanup(self._compare_system_with_app_snapshot)
except RuntimeError:
_logger.warning(
"Process manager backend unavailable, application "
"snapshot support disabled.")
self.addOnException(self._take_screenshot_on_failure)
@property
def process_manager(self):
if self._process_manager is None:
self._process_manager = ProcessManager.create()
return self._process_manager
@property
def keyboard(self):
return self._kb
@property
def mouse(self):
if self._mouse is None:
self._mouse = Mouse.create()
return self._mouse
@property
def display(self):
if self._display is None:
self._display = Display.create()
return self._display
def launch_test_application(self, application, *arguments, **kwargs):
"""Launch ``application`` and return a proxy object for the
application.
Use this method to launch an application and start testing it. The
positional arguments are used as arguments to the application to lanch.
Keyword arguments are used to control the manner in which the
application is launched.
This method is designed to be flexible enough to launch all supported
types of applications. Autopilot can automatically determine how to
enable introspection support for dynamically linked binary
applications. For example, to launch a binary Gtk application, a test
might start with::
app_proxy = self.launch_test_application('gedit')
Applications can be given command line arguments by supplying
positional arguments to this method. For example, if we want to launch
``gedit`` with a certain document loaded, we might do this::
app_proxy = self.launch_test_application(
'gedit', '/tmp/test-document.txt')
... a Qt5 Qml application is launched in a similar fashion::
app_proxy = self.launch_test_application(
'qmlscene', 'my_scene.qml')
If you wish to launch an application that is not a dynamically linked
binary, you must specify the application type. For example, a Qt4
python application might be launched like this::
app_proxy = self.launch_test_application(
'my_qt_app.py', app_type='qt')
Similarly, a python/Gtk application is launched like so::
app_proxy = self.launch_test_application(
'my_gtk_app.py', app_type='gtk')
:param application: The application to launch. The application can be
specified as:
* A full, absolute path to an executable file.
(``/usr/bin/gedit``)
* A relative path to an executable file.
(``./build/my_app``)
* An app name, which will be searched for in $PATH (``my_app``)
:keyword app_type: If set, provides a hint to autopilot as to which
kind of introspection to enable. This is needed when the
application you wish to launch is *not* a dynamically linked
binary. Valid values are 'gtk' or 'qt'. These strings are case
insensitive.
:keyword launch_dir: If set to a directory that exists the process
will be launched from that directory.
:keyword capture_output: If set to True (the default), the process
output will be captured and attached to the test as test detail.
:keyword emulator_base: If set, specifies the base class to be used for
all emulators for this loaded application.
:return: A proxy object that represents the application. Introspection
data is retrievable via this object.
"""
launch_args = _get_application_launch_args(kwargs)
launcher = self.useFixture(
NormalApplicationLauncher(
case_addDetail=self.addDetailUniqueName,
**kwargs
)
)
return launcher.launch(application, arguments, **launch_args)
def launch_click_package(self, package_id, app_name=None, app_uris=[],
**kwargs):
"""Launch a click package application with introspection enabled.
This method takes care of launching a click package with introspection
exabled. You probably want to use this method if your application is
packaged in a click application, or is started via upstart.
Usage is similar to the
:py:meth:`AutopilotTestCase.launch_test_application`::
app_proxy = self.launch_click_package(
"com.ubuntu.dropping-letters"
)
:param package_id: The Click package name you want to launch. For
example: ``com.ubuntu.dropping-letters``
:param app_name: Currently, only one application can be packaged in a
click package, and this parameter can be left at None. If
specified, it should be the application name you wish to launch.
:param app_uris: Parameters used to launch the click package. This
parameter will be left empty if not used.
:keyword emulator_base: If set, specifies the base class to be used for
all emulators for this loaded application.
:raises RuntimeError: If the specified package_id cannot be found in
the click package manifest.
:raises RuntimeError: If the specified app_name cannot be found within
the specified click package.
:returns: proxy object for the launched package application
"""
launcher = self.useFixture(
ClickApplicationLauncher(
case_addDetail=self.addDetailUniqueName,
**kwargs
)
)
return launcher.launch(package_id, app_name, app_uris)
def launch_upstart_application(self, application_name, uris=[],
launcher_class=UpstartApplicationLauncher,
**kwargs):
"""Launch an application with upstart.
This method launched an application via the ``ubuntu-app-launch``
library, on platforms that support it.
Usage is similar to the
:py:meth:`AutopilotTestCase.launch_test_application`::
app_proxy = self.launch_upstart_application("gallery-app")
:param application_name: The name of the application to launch.
:param launcher_class: The application launcher class to use. Useful if
you need to overwrite the default to do something custom (i.e. using
AlreadyLaunchedUpstartLauncher)
:keyword emulator_base: If set, specifies the base class to be used for
all emulators for this loaded application.
:raises RuntimeError: If the specified application cannot be launched.
"""
launcher = self.useFixture(
launcher_class(
case_addDetail=self.addDetailUniqueName,
**kwargs
)
)
return launcher.launch(application_name, uris)
def _compare_system_with_app_snapshot(self):
"""Compare the currently running application with the last snapshot.
This method will raise an AssertionError if there are any new
applications currently running that were not running when the snapshot
was taken.
"""
try:
_compare_system_with_process_snapshot(
_get_process_snapshot,
self._app_snapshot
)
finally:
self._app_snapshot = None
def take_screenshot(self, attachment_name):
"""Take a screenshot of the current screen and adds it to the test as a
detail named *attachment_name*.
If *attachment_name* already exists as a detail the name will be
modified to remove the naming conflict
(i.e. using TestCase.addDetailUniqueName).
Returns True if the screenshot was taken and attached successfully,
False otherwise.
"""
try:
image_content = content_from_stream(
get_screenshot_data(get_display_server()),
content_type=ContentType('image', 'png'),
buffer_now=True
)
self.addDetailUniqueName(attachment_name, image_content)
return True
except Exception as e:
logging.error(
"Taking screenshot failed: {exception}".format(exception=e)
)
return False
def _take_screenshot_on_failure(self, ex_info):
failure_class_type = ex_info[0]
if _considered_failing_test(failure_class_type):
self.take_screenshot("FailedTestScreenshot")
@deprecated('fixtures.EnvironmentVariable')
def patch_environment(self, key, value):
"""Patch environment using fixture.
This function is deprecated and planned for removal in autopilot 1.6.
New implementations should use EnvironmenVariable from the fixtures
module::
from fixtures import EnvironmentVariable
def my_test(AutopilotTestCase):
my_patch = EnvironmentVariable('key', 'value')
self.useFixture(my_patch)
'key' will be set to 'value'. During tearDown, it will be reset to a
previous value, if one is found, or unset if not.
"""
self.useFixture(fixtures.EnvironmentVariable(key, value))
def assertVisibleWindowStack(self, stack_start):
"""Check that the visible window stack starts with the windows passed
in.
.. note:: Minimised windows are skipped.
:param stack_start: An iterable of
:class:`~autopilot.process.Window` instances.
:raises AssertionError: if the top of the window stack does not
match the contents of the stack_start parameter.
"""
stack = [
win for win in
self.process_manager.get_open_windows() if not win.is_hidden]
for pos, win in enumerate(stack_start):
self.assertThat(
stack[pos].x_id, Equals(win.x_id),
"%r at %d does not equal %r" % (stack[pos], pos, win))
def assertProperty(self, obj, **kwargs):
"""Assert that *obj* has properties equal to the key/value pairs in
kwargs.
This method is intended to be used on objects whose attributes do not
have the :meth:`wait_for` method (i.e.- objects that do not come from
the autopilot DBus interface).
For example, from within a test, to assert certain properties on a
`~autopilot.process.Window` instance::
self.assertProperty(my_window, is_maximized=True)
.. note:: assertProperties is a synonym for this method.
:param obj: The object to test.
:param kwargs: One or more keyword arguments to match against the
attributes of the *obj* parameter.
:raises ValueError: if no keyword arguments were given.
:raises ValueError: if a named attribute is a callable object.
:raises AssertionError: if any of the attribute/value pairs in
kwargs do not match the attributes on the object passed in.
"""
if not kwargs:
raise ValueError("At least one keyword argument must be present.")
for prop_name, desired_value in kwargs.items():
none_val = object()
attr = getattr(obj, prop_name, none_val)
if attr == none_val:
raise AssertionError(
"Object %r does not have an attribute named '%s'"
% (obj, prop_name))
if callable(attr):
raise ValueError(
"Object %r's '%s' attribute is a callable. It must be a "
"property." % (obj, prop_name))
self.assertThat(
lambda: getattr(obj, prop_name),
Eventually(Equals(desired_value)))
assertProperties = assertProperty
def _get_application_launch_args(kwargs):
"""Returns a dict containing relevant args and values for launching an
application.
Removes used arguments from kwargs parameter.
"""
launch_args = {}
launch_arg_list = ['app_type', 'launch_dir', 'capture_output']
for arg in launch_arg_list:
if arg in kwargs:
launch_args[arg] = kwargs.pop(arg)
return launch_args
def _get_process_snapshot():
"""Return a snapshot of running processes on the system.
:returns: a list of running processes.
:raises RuntimeError: if the process manager is unsavailble on this
platform.
"""
return ProcessManager.create().get_running_applications()
def _compare_system_with_process_snapshot(snapshot_fn, old_snapshot):
"""Compare an existing process snapshot with current running processes.
:param snapshot_fn: A callable that returns the current running process
list.
:param old_snapshot: A list of processes to compare against.
:raises AssertionError: If, after 10 seconds, there are still running
processes that were not present in ``old_snapshot``.
"""
new_apps = []
for _ in Timeout.default():
current_apps = snapshot_fn()
new_apps = [app for app in current_apps if app not in old_snapshot]
if not new_apps:
return
raise AssertionError(
"The following apps were started during the test and not closed: "
"%r" % new_apps)
def _ensure_uinput_device_created():
# This exists for a work around for bug lp:1297592. Need to create
# an input device before an application launch.
try:
from autopilot.input._uinput import Touch, _UInputTouchDevice
if _UInputTouchDevice._device is None:
Touch.create()
except Exception as e:
_logger.warning(
"Failed to create Touch device for bug lp:1297595 workaround: "
"%s" % str(e)
)
def _considered_failing_test(failure_class_type):
return (
not issubclass(failure_class_type, SkipTest)
and not issubclass(failure_class_type, _ExpectedFailure)
)
./autopilot/_glib.py 0000644 0000041 0000041 00000004416 13061552435 014722 0 ustar www-data www-data # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Autopilot Functional Test Tool
# Copyright (C) 2014 Canonical
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
"""Private module for wrapping Gdk/GdkX11 import workarounds.
The Gtk/Gdk/GdkX11 gi.repository bindings have an issue where importing Gdk,
running a Gdk method (specifically get_default_root_window) and then importing
GdkX11 will cause a different API to be loaded than if one had imported both
Gdk and GdkX11 at the same time.
This is captured in this bug:
https://bugs.launchpad.net/ubuntu/+source/gtk+3.0/+bug/1343069
To work around this we ensure that all the modules are loaded once at the same
time. This ensures that the call to _import_gdk will still import GdkX11 before
any Gdk calls are made.
"""
from autopilot.utilities import Silence
_Gtk = None
_Gdk = None
_GdkX11 = None
# Need to make sure that both modules are imported at the same time to stop any
# API changes happening under the covers.
def _import_gdk_modules():
global _Gtk
global _Gdk
global _GdkX11
version = '3.0'
with Silence():
from gi import require_version
require_version('Gdk', version)
require_version('GdkX11', version)
require_version('Gtk', version)
from gi.repository import Gtk, Gdk, GdkX11
_Gtk = Gtk
_Gdk = Gdk
_GdkX11 = GdkX11
def _import_gtk():
global _Gtk
if _Gtk is None:
_import_gdk_modules()
return _Gtk
def _import_gdk():
global _Gdk
if _Gdk is None:
_import_gdk_modules()
return _Gdk
def _import_gdkx11():
global _GdkX11
if _GdkX11 is None:
_import_gdk_modules()
return _GdkX11
./autopilot/tests/ 0000755 0000041 0000041 00000000000 13061552435 014431 5 ustar www-data www-data ./autopilot/tests/README 0000644 0000041 0000041 00000000571 13061552435 015314 0 ustar www-data www-data This is the root directory for autopilot tests. The "unit" folder contains unit tests that are run when the autopilot packae builds. They must not have any external dependancies (especially not to, for example X11). The "functional" folder contains larger tests that may depend on external components.
Both these folders are packages in the 'python-autopilot-tests' package.
./autopilot/tests/functional/ 0000755 0000041 0000041 00000000000 13061552435 016573 5 ustar www-data www-data ./autopilot/tests/functional/test_application_mixin.py 0000644 0000041 0000041 00000003407 13061552435 023717 0 ustar www-data www-data # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Autopilot Functional Test Tool
# Copyright (C) 2012-2013 Canonical
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
from autopilot.testcase import AutopilotTestCase
from testtools.matchers import raises
class ApplicationSupportTests(AutopilotTestCase):
def test_launch_with_bad_types_raises_typeerror(self):
"""Calling launch_test_application with something other than a string
must raise a TypeError"""
self.assertThat(
lambda: self.launch_test_application(1), raises(TypeError))
self.assertThat(
lambda: self.launch_test_application(True), raises(TypeError))
self.assertThat(
lambda: self.launch_test_application(1.0), raises(TypeError))
self.assertThat(
lambda: self.launch_test_application(object()), raises(TypeError))
self.assertThat(
lambda: self.launch_test_application(None), raises(TypeError))
self.assertThat(
lambda: self.launch_test_application([]), raises(TypeError))
self.assertThat(
lambda: self.launch_test_application((None,)), raises(TypeError))
./autopilot/tests/functional/test_process_emulator.py 0000644 0000041 0000041 00000016133 13061552435 023576 0 ustar www-data www-data # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Autopilot Functional Test Tool
# Copyright (C) 2012, 2013, 2014, 2015 Canonical
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
import os
import sys
from subprocess import Popen, call
from textwrap import dedent
from threading import Thread
from time import sleep
from timeit import default_timer
from testtools import skipIf
from testtools.matchers import Equals, GreaterThan, NotEquals, LessThan
from autopilot.exceptions import BackendException
from autopilot.matchers import Eventually
from autopilot.platform import model
from autopilot.process import ProcessManager
from autopilot.testcase import AutopilotTestCase
from autopilot.tests.functional.fixtures import ExecutableScript
@skipIf(model() != "Desktop", "Not suitable for device (ProcManager)")
class ProcessEmulatorTests(AutopilotTestCase):
def ensure_gedit_not_running(self):
"""Close any open gedit applications."""
apps = self.process_manager.get_running_applications_by_desktop_file(
'gedit.desktop')
if apps:
# this is a bit brutal, but easier in this context than the
# alternative.
call(['killall', 'gedit'])
def test_wait_for_app_running_works(self):
"""Make sure we can wait for an application to start."""
def start_gedit():
sleep(5)
Popen(['gedit'])
self.addCleanup(self.ensure_gedit_not_running)
start = default_timer()
t = Thread(target=start_gedit())
t.start()
ret = self.process_manager.wait_until_application_is_running(
'gedit.desktop', 10)
end = default_timer()
t.join()
self.assertThat(ret, Equals(True))
self.assertThat(end - start, GreaterThan(5))
def test_wait_for_app_running_times_out_correctly(self):
"""Make sure the bamf emulator times out correctly if no app is
started."""
self.ensure_gedit_not_running()
start = default_timer()
ret = self.process_manager.wait_until_application_is_running(
'gedit.desktop', 5)
end = default_timer()
self.assertThat(abs(end - start - 5.0), LessThan(1))
self.assertThat(ret, Equals(False))
def test_start_app(self):
"""Ensure we can start an Application."""
app = self.process_manager.start_app('Calculator')
self.assertThat(app, NotEquals(None))
# locale='C' does not work here as this goes through bamf, so we can't
# assert the precise name
self.assertThat(app.name, NotEquals(''))
self.assertThat(app.desktop_file, Equals('gcalctool.desktop'))
def test_start_app_window(self):
"""Ensure we can start an Application Window."""
app = self.process_manager.start_app_window('Calculator', locale='C')
self.assertThat(app, NotEquals(None))
self.assertThat(app.name, Equals('Calculator'))
@skipIf(model() != 'Desktop', "Bamf only available on desktop (Qt4)")
def test_bamf_geometry_gives_reliable_results(self):
script = dedent("""\
#!%s
from PyQt4.QtGui import QMainWindow, QApplication
from sys import argv
app = QApplication(argv)
win = QMainWindow()
win.show()
app.exec_()
""" % sys.executable)
path = self.useFixture(ExecutableScript(script)).path
app_proxy = self.launch_test_application(path, app_type='qt')
proxy_window = app_proxy.select_single('QMainWindow')
pm = ProcessManager.create()
window = [
w for w in pm.get_open_windows()
if w.name == os.path.basename(path)
][0]
self.assertThat(list(window.geometry), Equals(proxy_window.geometry))
@skipIf(model() != "Desktop", "Not suitable for device (ProcManager)")
class StartKnowAppsTests(AutopilotTestCase):
scenarios = [
(app_name, {
'app_name': app_name,
'desktop_file': (
ProcessManager.KNOWN_APPS[app_name]['desktop-file'])
})
for app_name in ProcessManager.KNOWN_APPS
]
def test_start_app_window(self):
"""Ensure we can start all the known applications."""
app = self.process_manager.start_app_window(self.app_name, locale='C')
self.assertThat(app, NotEquals(None))
class ProcessManagerApplicationNoCleanupTests(AutopilotTestCase):
"""Testing the process manager without the automated cleanup that running
within as an AutopilotTestCase provides.
"""
def test_can_close_all_app(self):
"""Ensure that closing an app actually closes all app instances."""
try:
process_manager = ProcessManager.create(preferred_backend="BAMF")
except BackendException as e:
self.skip("Test is only for BAMF backend ({}).".format(str(e)))
process_manager.start_app('Calculator')
process_manager.close_all_app('Calculator')
# ps/pgrep lists gnome-calculator as gnome-calculato (see lp
# bug:1174911)
ret_code = call(["pgrep", "-c", "gnome-calculato"])
self.assertThat(ret_code, Equals(1), "Application is still running")
@skipIf(model() != "Desktop", "Not suitable for device (X11)")
class BAMFResizeWindowTestCase(AutopilotTestCase):
"""Tests for the BAMF window helpers."""
scenarios = [
('increase size', dict(delta_width=40, delta_height=40)),
('decrease size', dict(delta_width=-40, delta_height=-40))
]
def start_mock_window(self):
self.launch_test_application(
'window-mocker',
app_type='qt'
)
try:
process_manager = ProcessManager.create(preferred_backend='BAMF')
except BackendException as e:
self.skip('Test is only for BAMF backend ({}).'.format(str(e)))
window = [
w for w in process_manager.get_open_windows()
if w.name == 'Default Window Title'
][0]
return window
def test_resize_window_must_update_width_and_height_geometry(self):
window = self.start_mock_window()
def get_size():
_, _, width, height = window.geometry
return width, height
initial_width, initial_height = get_size()
expected_width = initial_width + self.delta_width
expected_height = initial_height + self.delta_height
window.resize(width=expected_width, height=expected_height)
self.assertThat(
get_size, Eventually(Equals((expected_width, expected_height))))
./autopilot/tests/functional/test_mouse_emulator.py 0000644 0000041 0000041 00000003224 13061552435 023245 0 ustar www-data www-data # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Autopilot Functional Test Tool
# Copyright (C) 2012-2013 Canonical
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
from testtools import skipIf, TestCase
from testtools.matchers import Equals
from unittest.mock import patch
from autopilot import platform
from autopilot.input import Pointer, Mouse
@skipIf(platform.model() != "Desktop", "Not suitable for device (X11)")
class MouseEmulatorTests(TestCase):
"""Tests for the autopilot mouse emulator."""
def setUp(self):
super(MouseEmulatorTests, self).setUp()
self.mouse = Pointer(Mouse.create())
def tearDown(self):
super(MouseEmulatorTests, self).tearDown()
self.mouse = None
def test_x_y_properties(self):
"""x and y properties must simply return values from the position()
method."""
with patch.object(
self.mouse._device, 'position', return_value=(42, 37)):
self.assertThat(self.mouse.x, Equals(42))
self.assertThat(self.mouse.y, Equals(37))
./autopilot/tests/functional/test_input_stack.py 0000644 0000041 0000041 00000046661 13061552435 022545 0 ustar www-data www-data
# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Autopilot Functional Test Tool
# Copyright (C) 2012, 2013, 2014, 2015 Canonical
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
import json
import logging
import os
from tempfile import mktemp
from testtools import TestCase, skipIf
from testtools.matchers import (
IsInstance,
Equals,
raises,
GreaterThan
)
from testscenarios import TestWithScenarios
from textwrap import dedent
from time import sleep
import timeit
from unittest import SkipTest
from unittest.mock import patch
from autopilot import (
platform,
tests
)
from autopilot.display import Display
from autopilot.gestures import pinch
from autopilot.input import Keyboard, Mouse, Pointer, Touch, get_center_point
from autopilot.matchers import Eventually
from autopilot.testcase import AutopilotTestCase, multiply_scenarios
from autopilot.tests.functional import QmlScriptRunnerMixin
from autopilot.utilities import on_test_started
class ElapsedTimeCounter(object):
"""A simple utility to count the amount of real time that passes."""
def __enter__(self):
self._start_time = timeit.default_timer()
return self
def __exit__(self, *args):
pass
@property
def elapsed_time(self):
return timeit.default_timer() - self._start_time
class InputStackKeyboardBase(AutopilotTestCase):
scenarios = [
('UInput', dict(backend='UInput')),
]
if platform.model() == "Desktop":
scenarios.append(('X11', dict(backend='X11')))
def setUp(self):
super(InputStackKeyboardBase, self).setUp()
if self.backend == 'UInput' and not os.access('/dev/uinput', os.W_OK):
raise SkipTest(
"UInput backend currently requires write access to "
"/dev/uinput")
class InputStackKeyboardCreationTests(InputStackKeyboardBase):
def test_can_create_backend(self):
keyboard = Keyboard.create(self.backend)
self.assertThat(keyboard, IsInstance(Keyboard))
@skipIf(platform.model() != "Desktop", "Only suitable on Desktop (WinMocker)")
class InputStackKeyboardTypingTests(InputStackKeyboardBase):
scenarios = multiply_scenarios(
InputStackKeyboardBase.scenarios,
[
('lower_alpha', dict(input='abcdefghijklmnopqrstuvwxyz')),
('upper_alpha', dict(input='ABCDEFGHIJKLMNOPQRSTUVWXYZ')),
('numeric', dict(input='0123456789')),
('punctuation', dict(input='`~!@#$%^&*()_-+={}[]|\\:;"\'<>,.?/')),
('whitespace', dict(input='\t\n'))
]
)
def start_mock_app(self):
window_spec_file = mktemp(suffix='.json')
window_spec = {"Contents": "TextEdit"}
json.dump(
window_spec,
open(window_spec_file, 'w')
)
self.addCleanup(os.remove, window_spec_file)
return self.launch_test_application(
'window-mocker',
window_spec_file,
app_type='qt'
)
def test_text_typing(self):
"""Typing text must produce the correct characters in the target
app.
"""
app_proxy = self.start_mock_app()
text_edit = app_proxy.select_single('QTextEdit')
# make sure the text edit has keyboard focus:
self.mouse.click_object(text_edit)
self.assertThat(text_edit.focus, Eventually(Equals(True)))
# even though we ensured the textedit has focus, it occasionally
# does not yet accept keyboard input, causing this test to fail
# intermittently. to remedy this, we just add a sleep.
sleep(2)
# create keyboard and type the text.
keyboard = Keyboard.create(self.backend)
keyboard.type(self.input, 0.01)
self.assertThat(text_edit.plainText,
Eventually(Equals(self.input)),
"app shows: " + text_edit.plainText
)
def test_typing_with_contextmanager(self):
"""Typing text must produce the correct characters in the target
app.
"""
app_proxy = self.start_mock_app()
text_edit = app_proxy.select_single('QTextEdit')
keyboard = Keyboard.create(self.backend)
with keyboard.focused_type(text_edit) as kb:
kb.type(self.input, 0.01)
self.assertThat(
text_edit.plainText,
Eventually(Equals(self.input)),
"app shows: " + text_edit.plainText
)
def test_keyboard_keys_are_released(self):
"""Typing characters must not leave keys pressed."""
app_proxy = self.start_mock_app()
text_edit = app_proxy.select_single('QTextEdit')
# make sure the text edit has keyboard focus:
self.mouse.click_object(text_edit)
keyboard = Keyboard.create(self.backend)
for character in self.input:
self.assertThat(self._get_pressed_keys_list(), Equals([]))
keyboard.type(character, 0.01)
self.assertThat(self._get_pressed_keys_list(), Equals([]))
def _get_pressed_keys_list(self):
"""Get a list of keys pressed, but not released from the backend we're
using.
"""
if self.backend == 'X11':
from autopilot.input._X11 import _PRESSED_KEYS
return _PRESSED_KEYS
elif self.backend == 'UInput':
from autopilot.input import _uinput
return _uinput.Keyboard._device._pressed_keys_ecodes
else:
self.fail(
"Don't know how to get pressed keys list for {}".format(
self.backend
)
)
@skipIf(platform.model() != "Desktop", "Only suitable on Desktop (WinMocker)")
class InputStackKeyboardBackspaceTests(InputStackKeyboardBase):
def start_mock_app(self):
window_spec_file = mktemp(suffix='.json')
window_spec = {"Contents": "TextEdit"}
json.dump(
window_spec,
open(window_spec_file, 'w')
)
self.addCleanup(os.remove, window_spec_file)
return self.launch_test_application(
'window-mocker',
window_spec_file,
app_type='qt'
)
def test_backspace_works(self):
app_proxy = self.start_mock_app()
text_edit = app_proxy.select_single('QTextEdit')
self.mouse.click_object(text_edit)
keyboard = Keyboard.create(self.backend)
keyboard.type("Hello1")
keyboard.press_and_release("Backspace")
self.assertThat(text_edit.plainText,
Eventually(Equals("Hello")),
"app shows: " + text_edit.plainText
)
def test_inline_backspace_works(self):
app_proxy = self.start_mock_app()
text_edit = app_proxy.select_single('QTextEdit')
self.mouse.click_object(text_edit)
keyboard = Keyboard.create(self.backend)
keyboard.type("Hello1\b")
self.assertThat(text_edit.plainText,
Eventually(Equals("Hello")),
"app shows: " + text_edit.plainText
)
def osk_backend_available():
try:
from autopilot.input._osk import Keyboard # NOQA
return True
except ImportError:
return False
class OSKBackendTests(AutopilotTestCase, QmlScriptRunnerMixin):
"""Testing the Onscreen Keyboard (Ubuntu Keyboard) backend specifically.
There are limitations (i.e. on device only, window-mocker doesn't work on
the device, can't type all the characters that X11/UInput can.) that
necessitate this split into it's own test class.
"""
scenarios = [
('lower_alpha', dict(input='abcdefghijklmnopqrstuvwxyz')),
('upper_alpha', dict(input='ABCDEFGHIJKLMNOPQRSTUVWXYZ')),
('numeric', dict(input='0123456789')),
# Remove " due to bug: lp:1243501
('punctuation', dict(input='`~!@#$%^&*()_-+={}[]|\\:;\'<>,.?/')),
]
def launch_test_input_area(self):
self.app = self._launch_simple_input()
text_area = self.app.select_single("QQuickTextInput")
return text_area
def _launch_simple_input(self):
simple_script = dedent("""
import QtQuick 2.0
import Ubuntu.Components 0.1
Rectangle {
id: window
objectName: "windowRectangle"
color: "lightgrey"
Text {
id: inputLabel
text: "OSK Tests"
font.pixelSize: units.gu(3)
anchors {
left: input.left
top: parent.top
topMargin: 25
bottomMargin: 25
}
}
TextField {
id: input;
objectName: "input"
anchors {
top: inputLabel.bottom
horizontalCenter: parent.horizontalCenter
topMargin: 10
}
inputMethodHints: Qt.ImhNoPredictiveText
}
}
""")
return self.start_qml_script(simple_script)
@skipIf(platform.model() == 'Desktop', "Only suitable on a device")
@skipIf(not osk_backend_available(), "Test requires OSK Backend installed")
def test_can_type_string(self):
"""Typing text must produce the expected characters in the input
field.
"""
text_area = self.launch_test_input_area()
keyboard = Keyboard.create('OSK')
pointer = Pointer(Touch.create())
pointer.click_object(text_area)
keyboard._keyboard.wait_for_keyboard_ready()
keyboard.type(self.input)
self.assertThat(text_area.text, Eventually(Equals(self.input)))
@skipIf(platform.model() == 'Desktop', "Only suitable on a device")
@skipIf(not osk_backend_available(), "Test requires OSK Backend installed")
def test_focused_typing_contextmanager(self):
"""Typing text using the 'focused_typing' context manager must not only
produce the expected characters in the input field but also cleanup the
OSK afterwards too.
"""
text_area = self.launch_test_input_area()
keyboard = Keyboard.create('OSK')
with keyboard.focused_type(text_area) as kb:
kb.type(self.input)
self.assertThat(
text_area.text,
Eventually(Equals(self.input))
)
self.assertThat(
keyboard._keyboard.is_available,
Eventually(Equals(False))
)
class MockAppMouseTestBase(AutopilotTestCase):
def start_mock_app(self):
window_spec_file = mktemp(suffix='.json')
window_spec = {"Contents": "MouseTest"}
json.dump(
window_spec,
open(window_spec_file, 'w')
)
self.addCleanup(os.remove, window_spec_file)
return self.launch_test_application(
'window-mocker', window_spec_file, app_type='qt')
class MouseTestCase(AutopilotTestCase, tests.LogHandlerTestCase):
@skipIf(platform.model() != "Desktop", "Only suitable on Desktop (Mouse)")
def test_move_to_nonint_point(self):
"""Test mouse does not get stuck when we move to a non-integer point.
LP bug #1195499.
"""
device = Mouse.create()
screen_geometry = Display.create().get_screen_geometry(0)
target_x = screen_geometry[0] + 10
target_y = screen_geometry[1] + 10.6
device.move(target_x, target_y)
self.assertEqual(device.position(), (target_x, int(target_y)))
@patch('autopilot.platform.model', new=lambda *args: "Not Desktop", )
def test_mouse_creation_on_device_raises_useful_error(self):
"""Trying to create a mouse device on the phablet devices must raise an
explicit exception.
"""
expected_exception = RuntimeError(
"Cannot create a Mouse on devices where X11 is not available."
)
self.assertThat(lambda: Mouse.create(),
raises(expected_exception))
@skipIf(platform.model() != "Desktop", "Only suitable on Desktop (Mouse)")
def test_mouse_move_must_log_final_position_at_debug_level(self):
self.root_logger.setLevel(logging.DEBUG)
mouse = Mouse.create()
mouse.move(10, 10)
self.assertLogLevelContains(
'DEBUG',
"The mouse is now at position 10,10."
)
@skipIf(platform.model() != "Desktop", "Only suitable on Desktop (WinMocker)")
class TouchTests(MockAppMouseTestBase):
def setUp(self):
super(TouchTests, self).setUp()
self.device = Touch.create()
self.app = self.start_mock_app()
self.widget = self.app.select_single('MouseTestWidget')
self.button_status = self.app.select_single(
'QLabel', objectName='button_status')
def test_tap(self):
x, y = get_center_point(self.widget)
self.device.tap(x, y)
self.assertThat(
self.button_status.text, Eventually(Equals("Touch Release")))
def test_press_and_release(self):
x, y = get_center_point(self.widget)
self.device.press(x, y)
self.assertThat(
self.button_status.text, Eventually(Equals("Touch Press")))
self.device.release()
self.assertThat(
self.button_status.text, Eventually(Equals("Touch Release")))
class TouchGesturesTests(AutopilotTestCase, QmlScriptRunnerMixin):
def test_pinch_gesture(self):
"""Ensure that the pinch gesture pinches as expected."""
test_qml = dedent("""\
import QtQuick 2.0
Rectangle {
id: colorBox
width: 250
height: 250
color: "#00FF00"
PinchArea {
anchors.fill: parent
onPinchFinished: {
colorBox.color = "#0000FF"
}
}
}
""")
# Returned results include the alpha (RGBA)
start_green_bg = [0, 255, 0, 255]
end_blue_bg = [0, 0, 255, 255]
app = self.start_qml_script(test_qml)
pinch_widget = app.select_single("QQuickRectangle")
widget_bg_colour = lambda: pinch_widget.color
self.assertThat(widget_bg_colour, Eventually(Equals(start_green_bg)))
x, y = get_center_point(pinch_widget)
pinch((x, y), (10, 0), (100, 0))
self.assertThat(widget_bg_colour, Eventually(Equals(end_blue_bg)))
class PointerWrapperTests(AutopilotTestCase):
def test_can_move_touch_wrapper(self):
device = Pointer(Touch.create())
device.move(34, 56)
self.assertThat(device.x, Equals(34))
self.assertThat(device.y, Equals(56))
def test_touch_drag_updates_coordinates(self):
"""The Pointer wrapper must update it's x and y properties when
wrapping a touch object and performing a drag operation.
"""
class FakeTouch(Touch):
def __init__(self):
self.x = 0
self.y = 0
def drag(self, x1, y1, x2, y2, rate='dummy',
time_between_events='dummy'):
self.x = x2
self.y = y2
p = Pointer(FakeTouch())
p.drag(0, 0, 100, 123)
self.assertThat(p.x, Equals(100))
self.assertThat(p.y, Equals(123))
@skipIf(platform.model() != "Desktop", "Window mocker only available on X11")
class InputEventDelayTests(MockAppMouseTestBase, TestWithScenarios):
scenarios = [
('Touch', dict(input_class=Touch)),
('Mouse', dict(input_class=Mouse)),
]
def setUp(self):
super(InputEventDelayTests, self).setUp()
self.device = Pointer(self.input_class.create())
self.widget = self.get_mock_app_main_widget()
def get_mock_app_main_widget(self):
self.app = self.start_mock_app()
return self.app.select_single('MouseTestWidget')
def test_subsequent_events_delay(self):
with ElapsedTimeCounter() as time_counter:
for i in range(3):
self.device.click_object(self.widget, time_between_events=0.6)
self.assertThat(time_counter.elapsed_time, GreaterThan(1.0))
def test_subsequent_events_default_delay(self):
with ElapsedTimeCounter() as time_counter:
for i in range(10):
self.device.click_object(self.widget)
self.assertThat(time_counter.elapsed_time, GreaterThan(0.9))
class InputStackCleanupTests(TestCase):
def test_cleanup_called(self):
"""Derived classes cleanup method must be called when interface cleanup
method is called.
"""
class FakeKeyboard(Keyboard):
cleanup_called = False
@classmethod
def on_test_end(cls, test_instance):
FakeKeyboard.cleanup_called = True
class FakeTestCase(TestCase):
def test_foo(self):
on_test_started(self)
FakeKeyboard()
FakeTestCase("test_foo").run()
self.assertThat(FakeKeyboard.cleanup_called, Equals(True))
class InputStackCleanup(AutopilotTestCase):
@skipIf(platform.model() != "Desktop", "Only suitable on Desktop (X11)")
def test_keyboard_keys_released_X11(self):
"""Cleanup must release any keys that an X11 keyboard has had
pressed."""
class FakeTestCase(AutopilotTestCase):
def test_press_key(self):
kb = Keyboard.create('X11')
kb.press('Shift')
test_result = FakeTestCase("test_press_key").run()
self.assertThat(test_result.wasSuccessful(), Equals(True))
from autopilot.input._X11 import _PRESSED_KEYS
self.assertThat(_PRESSED_KEYS, Equals([]))
def test_keyboard_keys_released_UInput(self):
"""Cleanup must release any keys that an UInput keyboard has had
pressed."""
class FakeTestCase(AutopilotTestCase):
def test_press_key(self):
kb = Keyboard.create('UInput')
kb.press('Shift')
test_result = FakeTestCase("test_press_key").run()
self.assertThat(test_result.wasSuccessful(), Equals(True))
from autopilot.input import _uinput
self.assertThat(
_uinput.Keyboard._device._pressed_keys_ecodes, Equals([]))
@skipIf(platform.model() != "Desktop", "Not suitable for device (X11)")
@patch('autopilot.input._X11.fake_input', new=lambda *args: None, )
def test_mouse_button_released(self):
"""Cleanup must release any mouse buttons that have been pressed."""
class FakeTestCase(AutopilotTestCase):
def test_press_button(self):
mouse = Mouse.create('X11')
mouse.press()
test_result = FakeTestCase("test_press_button").run()
from autopilot.input._X11 import _PRESSED_MOUSE_BUTTONS
self.assertThat(test_result.wasSuccessful(), Equals(True))
self.assertThat(_PRESSED_MOUSE_BUTTONS, Equals([]))
./autopilot/tests/functional/test_dbus_query.py 0000644 0000041 0000041 00000033741 13061552435 022376 0 ustar www-data www-data # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Autopilot Functional Test Tool
# Copyright (C) 2012-2013 Canonical
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
import json
import os
import subprocess
import signal
from timeit import default_timer
from tempfile import mktemp
from testtools import skipIf
from testtools.matchers import Equals, NotEquals, raises, LessThan, GreaterThan
from autopilot import platform
from autopilot.testcase import AutopilotTestCase
from autopilot.exceptions import StateNotFoundError
@skipIf(platform.model() != "Desktop", "Only suitable on Desktop (WinMocker)")
class DbusQueryTests(AutopilotTestCase):
"""A collection of dbus query tests for autopilot."""
def start_fully_featured_app(self):
"""Create an application that includes menus and other nested
elements.
"""
window_spec = {
"Menu": [
{
"Title": "File",
"Menu": [
"Open",
"Save",
"Save As",
"Quit"
]
},
{
"Title": "Help",
"Menu": [
"Help 1",
"Help 2",
"Help 3",
"Help 4"
]
}
],
"Contents": "TextEdit"
}
file_path = mktemp()
json.dump(window_spec, open(file_path, 'w'))
self.addCleanup(os.remove, file_path)
return self.launch_test_application(
'window-mocker', file_path, app_type="qt")
def test_select_single_selects_only_available_object(self):
"""Must be able to select a single unique object."""
app = self.start_fully_featured_app()
main_window = app.select_single('QMainWindow')
self.assertThat(main_window, NotEquals(None))
def test_can_select_parent_of_root(self):
"""Must be able to select the parent of the root object."""
root = self.start_fully_featured_app()
root_parent = root.get_parent()
self.assertThat(root.id, Equals(root_parent.id))
def test_can_select_parent_of_normal_node(self):
root = self.start_fully_featured_app()
main_window = root.select_single('QMainWindow')
window_parent = main_window.get_parent()
self.assertThat(window_parent.id, Equals(root.id))
def test_can_select_specific_parent(self):
root = self.start_fully_featured_app()
action_item = root.select_single('QAction', text='Save')
window_parent = action_item.get_parent('window-mocker')
self.assertThat(window_parent.id, Equals(root.id))
def test_select_parent_raises_if_node_not_parent(self):
root = self.start_fully_featured_app()
action_item = root.select_single('QAction', text='Save')
match_fn = lambda: action_item.get_parent('QMadeUpType')
self.assertThat(match_fn, raises(StateNotFoundError('QMadeUpType')))
def test_select_parent_with_property_only(self):
root = self.start_fully_featured_app()
action_item = root.select_single('QAction', text='Save')
# The ID of parent of a tree is always 1.
window_parent = action_item.get_parent(id=1)
self.assertThat(window_parent.id, Equals(root.id))
def test_select_parent_raises_if_property_not_match(self):
root = self.start_fully_featured_app()
action_item = root.select_single('QAction', text='Save')
self.assertIsNotNone(action_item.get_parent('QMenu'))
match_fn = lambda: action_item.get_parent('QMenu', visible=True)
self.assertThat(
match_fn,
raises(StateNotFoundError('QMenu', visible=True))
)
def test_single_select_on_object(self):
"""Must be able to select a single unique child of an object."""
app = self.start_fully_featured_app()
main_win = app.select_single('QMainWindow')
menu_bar = main_win.select_single('QMenuBar')
self.assertThat(menu_bar, NotEquals(None))
def test_select_multiple_on_object_returns_all(self):
"""Must be able to select all child objects."""
app = self.start_fully_featured_app()
main_win = app.select_single('QMainWindow')
menu_bar = main_win.select_single('QMenuBar')
menus = menu_bar.select_many('QMenu')
self.assertThat(len(menus), Equals(2))
def test_select_multiple_on_object_with_parameter(self):
"""Must be able to select a specific object determined by a
parameter.
"""
app = self.start_fully_featured_app()
main_win = app.select_single('QMainWindow')
menu_bar = main_win.select_single('QMenuBar')
help_menu = menu_bar.select_many('QMenu', title='Help')
self.assertThat(len(help_menu), Equals(1))
self.assertThat(help_menu[0].title, Equals('Help'))
def test_select_single_on_object_with_param(self):
"""Must only select a single unique object using a parameter."""
app = self.start_fully_featured_app()
main_win = app.select_single('QMainWindow')
menu_bar = main_win.select_single('QMenuBar')
help_menu = menu_bar.select_single('QMenu', title='Help')
self.assertThat(help_menu, NotEquals(None))
self.assertThat(help_menu.title, Equals('Help'))
def test_select_many_uses_unique_object(self):
"""Given 2 objects of the same type with childen, selection on one will
only get its children.
"""
app = self.start_fully_featured_app()
main_win = app.select_single('QMainWindow')
menu_bar = main_win.select_single('QMenuBar')
help_menu = menu_bar.select_single('QMenu', title='Help')
actions = help_menu.select_many('QAction')
self.assertThat(len(actions), Equals(5))
def test_select_single_no_name_no_parameter_raises_exception(self):
app = self.start_fully_featured_app()
fn = lambda: app.select_single()
self.assertRaises(ValueError, fn)
def test_select_single_no_match_raises_exception(self):
app = self.start_fully_featured_app()
match_fn = lambda: app.select_single("QMadeupType")
self.assertThat(match_fn, raises(StateNotFoundError('QMadeupType')))
def test_exception_raised_when_operating_on_dead_app(self):
app = self.start_fully_featured_app()
main_window = app.select_single('QMainWindow')
app.kill_application()
self.assertRaises(RuntimeError, main_window.get_parent)
def test_exception_message_when_operating_on_dead_app(self):
app = self.start_fully_featured_app()
app.kill_application()
try:
app.select_single('QMainWindow')
except RuntimeError as e:
msg = ("Lost dbus backend communication. It appears the "
"application under test exited before the test "
"finished!")
self.assertEqual(str(e), msg)
def test_select_single_parameters_only(self):
app = self.start_fully_featured_app()
main_win = app.select_single('QMainWindow')
titled_help = main_win.select_single(title='Help')
self.assertThat(titled_help, NotEquals(None))
self.assertThat(titled_help.title, Equals('Help'))
def test_select_single_parameters_no_match_raises_exception(self):
app = self.start_fully_featured_app()
match_fn = lambda: app.select_single(title="Non-existant object")
self.assertThat(
match_fn,
raises(StateNotFoundError('*', title="Non-existant object"))
)
def test_select_single_returning_multiple_raises(self):
app = self.start_fully_featured_app()
fn = lambda: app.select_single('QMenu')
self.assertRaises(ValueError, fn)
def test_select_many_no_name_no_parameter_raises_exception(self):
app = self.start_fully_featured_app()
fn = lambda: app.select_single()
self.assertRaises(ValueError, fn)
def test_select_many_only_using_parameters(self):
app = self.start_fully_featured_app()
many_help_menus = app.select_many(title='Help')
self.assertThat(len(many_help_menus), Equals(1))
def test_select_many_with_no_parameter_matches_returns_empty_list(self):
app = self.start_fully_featured_app()
failed_match = app.select_many('QMenu', title='qwerty')
self.assertThat(failed_match, Equals([]))
def test_select_many_sorted_result(self):
app = self.start_fully_featured_app()
un_sorted_texts = [item.text for item in app.select_many('QAction')]
sorted_texts = [
item.text for item in app.select_many(
'QAction',
ap_result_sort_keys=['text']
)
]
self.assertNotEqual(un_sorted_texts, sorted_texts)
self.assertEqual(sorted_texts, sorted(un_sorted_texts))
def test_wait_select_single_succeeds_quickly(self):
app = self.start_fully_featured_app()
start_time = default_timer()
main_window = app.wait_select_single('QMainWindow')
end_time = default_timer()
self.assertThat(main_window, NotEquals(None))
self.assertThat(abs(end_time - start_time), LessThan(1))
def test_wait_select_single_timeout_less_than_ten_seconds(self):
app = self.start_fully_featured_app()
match_fn = lambda: app.wait_select_single(
'QMadeupType',
ap_query_timeout=3
)
start_time = default_timer()
self.assertThat(match_fn, raises(StateNotFoundError('QMadeupType')))
end_time = default_timer()
self.assertThat(abs(end_time - start_time), GreaterThan(2))
self.assertThat(abs(end_time - start_time), LessThan(4))
def test_wait_select_single_timeout_more_than_ten_seconds(self):
app = self.start_fully_featured_app()
match_fn = lambda: app.wait_select_single(
'QMadeupType',
ap_query_timeout=12
)
start_time = default_timer()
self.assertThat(match_fn, raises(StateNotFoundError('QMadeupType')))
end_time = default_timer()
self.assertThat(abs(end_time - start_time), GreaterThan(11))
self.assertThat(abs(end_time - start_time), LessThan(13))
def test_wait_select_many_requested_elements_count_not_match_raises(self):
app = self.start_fully_featured_app()
fn = lambda: app.wait_select_many(
'QMadeupType',
ap_query_timeout=4,
ap_result_count=2
)
start_time = default_timer()
self.assertRaises(ValueError, fn)
end_time = default_timer()
self.assertThat(abs(end_time - start_time), GreaterThan(3))
self.assertThat(abs(end_time - start_time), LessThan(5))
def test_wait_select_many_requested_elements_count_matches(self):
app = self.start_fully_featured_app()
start_time = default_timer()
menus = app.wait_select_many(
'QMenu',
ap_query_timeout=4,
ap_result_count=3
)
end_time = default_timer()
self.assertThat(len(menus), GreaterThan(2))
self.assertThat(abs(end_time - start_time), LessThan(5))
def test_wait_select_many_sorted_result(self):
app = self.start_fully_featured_app()
start_time = default_timer()
sorted_action_items = app.wait_select_many(
'QAction',
ap_query_timeout=4,
ap_result_count=10,
ap_result_sort_keys=['text']
)
end_time = default_timer()
self.assertThat(len(sorted_action_items), GreaterThan(9))
self.assertThat(abs(end_time - start_time), LessThan(5))
unsorted_action_items = app.wait_select_many(
'QAction',
ap_query_timeout=4,
ap_result_count=10
)
sorted_texts = [item.text for item in sorted_action_items]
un_sorted_texts = [item.text for item in unsorted_action_items]
self.assertNotEqual(un_sorted_texts, sorted_texts)
self.assertEqual(sorted_texts, sorted(un_sorted_texts))
@skipIf(platform.model() != "Desktop", "Only suitable on Desktop (WinMocker)")
class DbusCustomBusTests(AutopilotTestCase):
"""Test the ability to use custom dbus buses during a test."""
def setUp(self):
self.dbus_bus_addr = self._enable_custom_dbus_bus()
super(DbusCustomBusTests, self).setUp()
def _enable_custom_dbus_bus(self):
p = subprocess.Popen(['dbus-launch'], stdout=subprocess.PIPE,
universal_newlines=True)
output = p.communicate()
results = output[0].split("\n")
dbus_pid = int(results[1].split("=")[1])
dbus_address = results[0].split("=", 1)[1]
kill_dbus = lambda pid: os.killpg(pid, signal.SIGTERM)
self.addCleanup(kill_dbus, dbus_pid)
return dbus_address
def _start_mock_app(self, dbus_bus):
window_spec = {
"Contents": "TextEdit"
}
file_path = mktemp()
json.dump(window_spec, open(file_path, 'w'))
self.addCleanup(os.remove, file_path)
return self.launch_test_application(
'window-mocker',
file_path,
app_type="qt",
dbus_bus=dbus_bus,
)
def test_can_use_custom_dbus_bus(self):
app = self._start_mock_app(self.dbus_bus_addr)
self.assertThat(app, NotEquals(None))
./autopilot/tests/functional/test_testcase.py 0000644 0000041 0000041 00000005671 13061552435 022030 0 ustar www-data www-data # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Autopilot Functional Test Tool
# Copyright (C) 2014 Canonical
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
from testtools import TestCase
from testtools.matchers import Contains
from testtools.content_type import ContentType
from unittest.mock import patch
import time
from autopilot import testcase
class AutopilotTestCaseScreenshotTests(TestCase):
def test_screenshot_taken_when_test_fails(self):
class InnerTest(testcase.AutopilotTestCase):
def test_foo(self):
self.fail()
test = InnerTest('test_foo')
test_run = test.run()
self.assertFalse(test_run.wasSuccessful())
screenshot_content = test.getDetails()['FailedTestScreenshot']
self.assertEqual(
screenshot_content.content_type,
ContentType("image", "png")
)
def test_take_screenshot(self):
screenshot_name = self.getUniqueString()
class InnerTest(testcase.AutopilotTestCase):
def test_foo(self):
self.take_screenshot(screenshot_name)
test = InnerTest('test_foo')
test_run = test.run()
self.assertTrue(test_run.wasSuccessful())
screenshot_content = test.getDetails()[screenshot_name]
self.assertEqual(
screenshot_content.content_type,
ContentType("image", "png")
)
class TimedRunTestTests(TestCase):
@patch.object(testcase, 'get_test_timeout', new=lambda: 5)
def test_timed_run_test_times_out(self):
class TimedTest(testcase.AutopilotTestCase):
def test_will_timeout(self):
time.sleep(10) # should timeout after 5 seconds
test = TimedTest('test_will_timeout')
result = test.run()
self.assertFalse(result.wasSuccessful())
self.assertEqual(1, len(result.errors))
self.assertThat(
result.errors[0][1],
Contains('raise TimeoutException()')
)
@patch.object(testcase, 'get_test_timeout', new=lambda: 0)
def test_untimed_run_test_does_not_time_out(self):
class TimedTest(testcase.AutopilotTestCase):
def test_wont_timeout(self):
time.sleep(10)
test = TimedTest('test_wont_timeout')
result = test.run()
self.assertTrue(result.wasSuccessful())
./autopilot/tests/functional/__init__.py 0000644 0000041 0000041 00000013607 13061552435 020713 0 ustar www-data www-data # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Autopilot Functional Test Tool
# Copyright (C) 2012-2013 Canonical
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
from codecs import open
import os
import os.path
import sys
import logging
from shutil import rmtree
import subprocess
from tempfile import mkdtemp, mktemp
from testtools.content import text_content
from autopilot import platform
from autopilot.tests.functional.fixtures import TempDesktopFile
from autopilot.testcase import AutopilotTestCase
def remove_if_exists(path):
if os.path.exists(path):
if os.path.isdir(path):
rmtree(path)
else:
os.remove(path)
logger = logging.getLogger(__name__)
class AutopilotRunTestBase(AutopilotTestCase):
"""The base class for the autopilot functional tests."""
def setUp(self):
super(AutopilotRunTestBase, self).setUp()
self.base_path = self.create_empty_test_module()
def create_empty_test_module(self):
"""Create an empty temp directory, with an empty test directory inside
it.
This method handles cleaning up the directory once the test completes.
Returns the full path to the temp directory.
"""
# create the base directory:
base_path = mkdtemp()
self.addDetail('base path', text_content(base_path))
self.addCleanup(rmtree, base_path)
# create the tests directory:
os.mkdir(
os.path.join(base_path, 'tests')
)
# make tests importable:
open(
os.path.join(
base_path,
'tests',
'__init__.py'),
'w').write('# Auto-generated file.')
return base_path
def run_autopilot(self, arguments, pythonpath="", use_script=False):
environment_patch = _get_environment_patch(pythonpath)
if use_script:
arg = [sys.executable, self._write_setup_tools_script()]
else:
arg = [sys.executable, '-m', 'autopilot.run']
environ = os.environ.copy()
environ.update(environment_patch)
logger.info("Starting autopilot command with:")
logger.info("Autopilot command = %s", ' '.join(arg))
logger.info("Arguments = %s", arguments)
logger.info("CWD = %r", self.base_path)
arg.extend(arguments)
process = subprocess.Popen(
arg,
cwd=self.base_path,
env=environ,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
universal_newlines=True,
)
stdout, stderr = process.communicate()
retcode = process.poll()
self.addDetail('retcode', text_content(str(retcode)))
self.addDetail(
'stdout',
text_content(stdout)
)
self.addDetail(
'stderr',
text_content(stderr)
)
return (retcode, stdout, stderr)
def create_test_file(self, name, contents):
"""Create a test file with the given name and contents.
'name' must end in '.py' if it is to be importable.
'contents' must be valid python code.
"""
open(
os.path.join(
self.base_path,
'tests',
name),
'w',
encoding='utf8').write(contents)
def _write_setup_tools_script(self):
"""Creates a python script that contains the setup entry point."""
base_path = mkdtemp()
self.addCleanup(rmtree, base_path)
script_file = os.path.join(base_path, 'autopilot')
open(script_file, 'w').write(load_entry_point_script)
return script_file
def _get_environment_patch(pythonpath):
environment_patch = dict(DISPLAY=':0')
ap_base_path = os.path.abspath(
os.path.join(
os.path.dirname(__file__),
'..',
'..',
'..'
)
)
pythonpath_additions = []
if pythonpath:
pythonpath_additions.append(pythonpath)
if not ap_base_path.startswith('/usr/'):
pythonpath_additions.append(ap_base_path)
environment_patch['PYTHONPATH'] = ":".join(pythonpath_additions)
return environment_patch
load_entry_point_script = """\
#!/usr/bin/python
__requires__ = 'autopilot==1.6.0'
import sys
from pkg_resources import load_entry_point
if __name__ == '__main__':
sys.exit(
load_entry_point('autopilot==1.6.0', 'console_scripts', 'autopilot3')()
)
"""
class QmlScriptRunnerMixin(object):
"""A Mixin class that knows how to get a proxy object for a qml script."""
def start_qml_script(self, script_contents):
"""Launch a qml script."""
qml_path = mktemp(suffix='.qml')
open(qml_path, 'w').write(script_contents)
self.addCleanup(os.remove, qml_path)
extra_args = ''
if platform.model() != "Desktop":
# We need to add the desktop-file-hint
desktop_file = self.useFixture(
TempDesktopFile()
).get_desktop_file_path()
extra_args = '--desktop_file_hint={hint_file}'.format(
hint_file=desktop_file
)
return self.launch_test_application(
"qmlscene",
"-qt=qt5",
qml_path,
extra_args,
app_type='qt',
)
./autopilot/tests/functional/test_autopilot_performance.py 0000644 0000041 0000041 00000005162 13061552435 024611 0 ustar www-data www-data # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Autopilot Functional Test Tool
# Copyright (C) 2012-2013 Canonical
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
from contextlib import contextmanager
from timeit import default_timer
from textwrap import dedent
from testtools import skipIf
from testtools.matchers import Equals
import logging
from autopilot import platform
from autopilot.tests.functional import AutopilotRunTestBase
logger = logging.getLogger(__name__)
@contextmanager
def maximum_runtime(max_time):
start_time = default_timer()
yield
total_time = abs(default_timer() - start_time)
if total_time >= max_time:
raise AssertionError(
"Runtime of %f was not within defined "
"limit of %f" % (total_time, max_time)
)
else:
logger.info(
"Test completed in %f seconds, which is below the "
" threshold of %f.",
total_time,
max_time
)
@skipIf(platform.model() != "Desktop", "Only suitable on Desktop (WinMocker)")
class AutopilotPerformanceTests(AutopilotRunTestBase):
"""A suite of functional tests that will fail if autopilot performance
regresses below certain strictly defined limits.
Each test must be named after the feature we are benchmarking, and should
use the maximum_runtime contextmanager defined above.
"""
def test_autopilot_launch_test_app(self):
self.create_test_file(
'test_something.py',
dedent("""
from autopilot.testcase import AutopilotTestCase
class LaunchTestAppTests(AutopilotTestCase):
def test_launch_test_app(self):
app_proxy = self.launch_test_application(
'window-mocker',
app_type='qt'
)
""")
)
with maximum_runtime(5.0):
rc, out, err = self.run_autopilot(['run', 'tests'])
self.assertThat(rc, Equals(0))
./autopilot/tests/functional/test_introspection_features.py 0000644 0000041 0000041 00000034546 13061552435 025016 0 ustar www-data www-data # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Autopilot Functional Test Tool
# Copyright (C) 2013 Canonical
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
import json
import logging
import os
import re
import subprocess
import tempfile
from tempfile import mktemp
from testtools import skipIf
from testtools.matchers import (
Contains,
Equals,
GreaterThan,
IsInstance,
LessThan,
MatchesRegex,
Not,
StartsWith,
)
from textwrap import dedent
from unittest.mock import patch
from io import StringIO
from autopilot import platform
from autopilot.matchers import Eventually
from autopilot.testcase import AutopilotTestCase
from autopilot.tests.functional import QmlScriptRunnerMixin
from autopilot.tests.functional.fixtures import TempDesktopFile
from autopilot.introspection import CustomEmulatorBase
from autopilot.introspection import _object_registry as object_registry
from autopilot.introspection import _search
from autopilot.introspection.qt import QtObjectProxyMixin
from autopilot.display import Display
logger = logging.getLogger(__name__)
class EmulatorBase(CustomEmulatorBase):
pass
@skipIf(platform.model() != "Desktop", "Only suitable on Desktop (WinMocker)")
class IntrospectionFeatureTests(AutopilotTestCase):
"""Test various features of the introspection code."""
def start_mock_app(self, emulator_base):
window_spec_file = mktemp(suffix='.json')
window_spec = {"Contents": "MouseTest"}
json.dump(
window_spec,
open(window_spec_file, 'w')
)
self.addCleanup(os.remove, window_spec_file)
return self.launch_test_application(
'window-mocker',
window_spec_file,
app_type='qt',
emulator_base=emulator_base,
)
def test_can_get_custom_proxy_for_app_root(self):
"""Test two things:
1) We can get a custom proxy object for the root object in the object
tree.
2) We can get a custom proxy object for an object in the tree which
contains characters which are usually disallowed in python class
names.
"""
class WindowMockerApp(EmulatorBase):
@classmethod
def validate_dbus_object(cls, path, _state):
return path == b'/window-mocker'
# verify that the initial proxy object we get back is the correct type:
app = self.start_mock_app(EmulatorBase)
self.assertThat(type(app), Equals(WindowMockerApp))
# verify that we get the correct type from get_root_instance:
self.assertThat(
type(app.get_root_instance()),
Equals(WindowMockerApp)
)
def test_customised_proxy_classes_have_extension_classes(self):
class WindowMockerApp(EmulatorBase):
@classmethod
def validate_dbus_object(cls, path, _state):
return path == b'/window-mocker'
app = self.start_mock_app(EmulatorBase)
self.assertThat(app.__class__.__bases__, Contains(QtObjectProxyMixin))
def test_customised_proxy_classes_have_multiple_extension_classes(self):
with object_registry.patch_registry({}):
class SecondEmulatorBase(CustomEmulatorBase):
pass
class WindowMockerApp(EmulatorBase, SecondEmulatorBase):
@classmethod
def validate_dbus_object(cls, path, _state):
return path == b'/window-mocker'
app = self.start_mock_app(EmulatorBase)
self.assertThat(app.__class__.__bases__, Contains(EmulatorBase))
self.assertThat(
app.__class__.__bases__,
Contains(SecondEmulatorBase)
)
def test_handles_using_app_cpo_base_class(self):
# This test replicates an issue found in an application test suite
# where using the App CPO caused an exception.
with object_registry.patch_registry({}):
class WindowMockerApp(CustomEmulatorBase):
@classmethod
def validate_dbus_object(cls, path, _state):
return path == b'/window-mocker'
self.start_mock_app(WindowMockerApp)
def test_warns_when_using_incorrect_cpo_base_class(self):
# Ensure the warning method is called when launching a proxy.
with object_registry.patch_registry({}):
class TestCPO(CustomEmulatorBase):
pass
class WindowMockerApp(TestCPO):
@classmethod
def validate_dbus_object(cls, path, _state):
return path == b'/window-mocker'
with patch.object(_search, 'logger') as p_logger:
self.start_mock_app(WindowMockerApp)
self.assertTrue(p_logger.warning.called)
def test_can_select_custom_emulators_by_name(self):
"""Must be able to select a custom emulator type by name."""
class MouseTestWidget(EmulatorBase):
pass
app = self.start_mock_app(EmulatorBase)
test_widget = app.select_single('MouseTestWidget')
self.assertThat(type(test_widget), Equals(MouseTestWidget))
def test_can_select_custom_emulators_by_type(self):
"""Must be able to select a custom emulator type by type."""
class MouseTestWidget(EmulatorBase):
pass
app = self.start_mock_app(EmulatorBase)
test_widget = app.select_single(MouseTestWidget)
self.assertThat(type(test_widget), Equals(MouseTestWidget))
def test_can_access_custom_emulator_properties(self):
"""Must be able to access properties of a custom emulator."""
class MouseTestWidget(EmulatorBase):
pass
app = self.start_mock_app(EmulatorBase)
test_widget = app.select_single(MouseTestWidget)
self.assertThat(test_widget.visible, Eventually(Equals(True)))
def test_selecting_generic_from_custom_is_not_inherited_from_custom(self):
"""Selecting a generic proxy object from a custom proxy object must not
return an object derived of the custom object type.
"""
class MouseTestWidget(EmulatorBase):
pass
app = self.start_mock_app(EmulatorBase)
mouse_widget = app.select_single(MouseTestWidget)
child_label = mouse_widget.select_many("QLabel")[0]
self.assertThat(child_label, Not(IsInstance(MouseTestWidget)))
def test_selecting_custom_from_generic_is_not_inherited_from_generic(self):
"""Selecting a custom proxy object from a generic proxy object must
return an object that is of the custom type.
"""
class MouseTestWidget(EmulatorBase):
pass
app = self.start_mock_app(EmulatorBase)
generic_window = app.select_single("QMainWindow")
mouse_widget = generic_window.select_single(MouseTestWidget)
self.assertThat(
mouse_widget,
Not(IsInstance(type(generic_window)))
)
def test_print_tree_full(self):
"""Print tree of full application"""
app = self.start_mock_app(EmulatorBase)
win = app.select_single("QMainWindow")
stream = StringIO()
win.print_tree(stream)
out = stream.getvalue()
# starts with root node
self.assertThat(
out,
StartsWith("== /window-mocker/QMainWindow ==\nChildren:")
)
# has root node properties
self.assertThat(
out,
MatchesRegex(
".*windowTitle: [u]?'Default Window Title'.*",
re.DOTALL
)
)
# has level-1 widgets with expected indent
self.assertThat(
out,
Contains(" == /window-mocker/QMainWindow/QRubberBand ==\n")
)
self.assertThat(
out,
MatchesRegex(".* objectName: [u]?'qt_rubberband'\n", re.DOTALL)
)
# has level-2 widgets with expected indent
self.assertThat(
out,
Contains(
" == /window-mocker/QMainWindow/QMenuBar/QToolButton =="
)
)
self.assertThat(
out,
MatchesRegex(
".* objectName: [u]?'qt_menubar_ext_button'.*",
re.DOTALL
)
)
def test_print_tree_depth_limit(self):
"""Print depth-limited tree for a widget"""
app = self.start_mock_app(EmulatorBase)
win = app.select_single("QMainWindow")
stream = StringIO()
win.print_tree(stream, 1)
out = stream.getvalue()
# has level-0 (root) node
self.assertThat(out, Contains("== /window-mocker/QMainWindow =="))
# has level-1 widgets
self.assertThat(out, Contains("/window-mocker/QMainWindow/QMenuBar"))
# no level-2 widgets
self.assertThat(out, Not(Contains(
"/window-mocker/QMainWindow/QMenuBar/QToolButton")))
def test_window_geometry(self):
"""Window.geometry property
Check that all Window geometry properties work and have a plausible
range.
"""
# ensure we have at least one open app window
self.start_mock_app(EmulatorBase)
display = Display.create()
top = left = right = bottom = None
# for multi-monitor setups, make sure we examine the full desktop
# space:
for monitor in range(display.get_num_screens()):
sx, sy, swidth, sheight = Display.create().get_screen_geometry(
monitor
)
logger.info(
"Monitor %d geometry is (%d, %d, %d, %d)",
monitor,
sx,
sy,
swidth,
sheight,
)
if left is None or sx < left:
left = sx
if top is None or sy < top:
top = sy
if right is None or sx + swidth >= right:
right = sx + swidth
if bottom is None or sy + sheight >= bottom:
bottom = sy + sheight
logger.info(
"Total desktop geometry is (%d, %d), (%d, %d)",
left,
top,
right,
bottom,
)
for win in self.process_manager.get_open_windows():
logger.info("Win '%r' geometry is %r", win, win.geometry)
geom = win.geometry
self.assertThat(len(geom), Equals(4))
self.assertThat(geom[0], GreaterThan(left - 1)) # no GreaterEquals
self.assertThat(geom[1], GreaterThan(top - 1))
self.assertThat(geom[2], LessThan(right))
self.assertThat(geom[3], LessThan(bottom))
class QMLCustomEmulatorTestCase(AutopilotTestCase):
"""Test the introspection of a QML application with a custom emulator."""
def test_can_access_custom_emulator_properties_twice(self):
"""Must be able to run more than one test with a custom emulator."""
class InnerTestCase(AutopilotTestCase):
class QQuickView(EmulatorBase):
pass
test_qml = dedent("""\
import QtQuick 2.0
Rectangle {
}
""")
def launch_test_qml(self):
arch = subprocess.check_output(
["dpkg-architecture", "-qDEB_HOST_MULTIARCH"],
universal_newlines=True).strip()
qml_path = tempfile.mktemp(suffix='.qml')
open(qml_path, 'w').write(self.test_qml)
self.addCleanup(os.remove, qml_path)
extra_args = ''
if platform.model() != "Desktop":
# We need to add the desktop-file-hint
desktop_file = self.useFixture(
TempDesktopFile()
).get_desktop_file_path()
extra_args = '--desktop_file_hint={hint_file}'.format(
hint_file=desktop_file
)
return self.launch_test_application(
"/usr/lib/" + arch + "/qt5/bin/qmlscene",
qml_path,
extra_args,
emulator_base=EmulatorBase)
def test_custom_emulator(self):
app = self.launch_test_qml()
test_widget = app.select_single(InnerTestCase.QQuickView)
self.assertThat(test_widget.visible, Eventually(Equals(True)))
result1 = InnerTestCase('test_custom_emulator').run()
self.assertThat(
result1.wasSuccessful(),
Equals(True),
'\n\n'.join(
[e[1] for e in result1.decorated.errors]
)
)
result2 = InnerTestCase('test_custom_emulator').run()
self.assertThat(
result2.wasSuccessful(),
Equals(True),
'\n\n'.join(
[e[1] for e in result2.decorated.errors]
)
)
class CustomCPOTest(AutopilotTestCase, QmlScriptRunnerMixin):
def launch_simple_qml_script(self):
simple_script = dedent("""
import QtQuick 2.0
Rectangle {
objectName: "ExampleRectangle"
}
""")
return self.start_qml_script(simple_script)
def test_cpo_can_be_named_different_to_underlying_type(self):
"""A CPO with the correct name match method must be matched if the
class name is different to the Type name.
"""
with object_registry.patch_registry({}):
class RandomNamedCPORectangle(CustomEmulatorBase):
@classmethod
def get_type_query_name(cls):
return 'QQuickRectangle'
app = self.launch_simple_qml_script()
rectangle = app.select_single(RandomNamedCPORectangle)
self.assertThat(rectangle.objectName, Equals('ExampleRectangle'))
./autopilot/tests/functional/test_open_window.py 0000644 0000041 0000041 00000004536 13061552435 022544 0 ustar www-data www-data # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Autopilot Functional Test Tool
# Copyright (C) 2012-2013 Canonical
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
import os.path
from testtools import skipIf
from testtools.matchers import Equals
from autopilot import platform
from autopilot.testcase import AutopilotTestCase
from autopilot.process import ProcessManager
import logging
logger = logging.getLogger(__name__)
@skipIf(platform.model() != "Desktop", "Not suitable for device (ProcManager)")
class OpenWindowTests(AutopilotTestCase):
scenarios = [
(
k,
{
'app_name': k,
'app_details': v,
}
) for k, v in ProcessManager.KNOWN_APPS.items()
]
def test_open_window(self):
"""self.start_app_window must open a new window of the given app."""
if not os.path.exists(
os.path.join(
'/usr/share/applications',
self.app_details['desktop-file']
)
):
self.skip("Application '%s' is not installed" % self.app_name)
existing_apps = self.process_manager.get_app_instances(self.app_name)
# if we opened the app, ensure that we close it again to avoid leaking
# processes like remmina
if not existing_apps:
self.addCleanup(self.process_manager.close_all_app, self.app_name)
old_wins = []
for app in existing_apps:
old_wins.extend(app.get_windows())
logger.debug("Old windows: %r", old_wins)
win = self.process_manager.start_app_window(self.app_name)
logger.debug("New window: %r", win)
is_new = win.x_id not in [w.x_id for w in old_wins]
self.assertThat(is_new, Equals(True))
./autopilot/tests/functional/test_autopilot_functional.py 0000644 0000041 0000041 00000105644 13061552435 024460 0 ustar www-data www-data # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Autopilot Functional Test Tool
# Copyright (C) 2012-2014 Canonical
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
from fixtures import TempDir
import glob
import os
import os.path
import re
from tempfile import mktemp
from testtools import skipIf
from testtools.matchers import Contains, Equals, MatchesRegex, Not, NotEquals
from textwrap import dedent
from unittest.mock import Mock
from autopilot import platform
from autopilot.matchers import Eventually
from autopilot.tests.functional import AutopilotRunTestBase, remove_if_exists
from autopilot._video import RMDVideoLogFixture
class AutopilotFunctionalTestsBase(AutopilotRunTestBase):
"""A collection of functional tests for autopilot."""
def run_autopilot_list(self, list_spec='tests', extra_args=[]):
"""Run 'autopilot list' in the specified base path.
This patches the environment to ensure that it's *this* version of
autopilot that's run.
returns a tuple containing: (exit_code, stdout, stderr)
"""
args_list = ["list"] + extra_args + [list_spec]
return self.run_autopilot(args_list)
def assertTestsInOutput(self, tests, output, total_title='tests'):
"""Asserts that 'tests' are all present in 'output'.
This assertion is intelligent enough to know that tests are not always
printed in alphabetical order.
'tests' can either be a list of test ids, or a list of tuples
containing (scenario_count, test_id), in the case of scenarios.
"""
if type(tests) is not list:
raise TypeError("tests must be a list, not %r" % type(tests))
if not isinstance(output, str):
raise TypeError("output must be a string, not %r" % type(output))
expected_heading = 'Loading tests from: %s\n\n' % self.base_path
expected_tests = []
expected_total = 0
for test in tests:
if type(test) == tuple:
expected_tests.append(' *%d %s' % test)
expected_total += test[0]
else:
expected_tests.append(' %s' % test)
expected_total += 1
expected_footer = ' %d total %s.' % (expected_total, total_title)
parts = output.split('\n')
observed_heading = '\n'.join(parts[:2]) + '\n'
observed_test_list = parts[2:-4]
observed_footer = parts[-2]
self.assertThat(expected_heading, Equals(observed_heading))
self.assertThat(
sorted(expected_tests),
Equals(sorted(observed_test_list))
)
self.assertThat(expected_footer, Equals(observed_footer))
class FunctionalTestMain(AutopilotFunctionalTestsBase):
def test_config_available_in_decorator(self):
"""Any commandline config values must be available for decorators."""
unique_config_value = self.getUniqueString()
self.create_test_file(
'test_config_skip.py', dedent("""\
from testtools import skipIf
import autopilot
from autopilot.testcase import AutopilotTestCase
class ConfigTest(AutopilotTestCase):
@skipIf(
autopilot.get_test_configuration().get('skipme', None)
== '{unique_config_value}',
'Skipping Test')
def test_config(self):
self.fail('Should not run.')
""".format(unique_config_value=unique_config_value))
)
config_string = 'skipme={}'.format(unique_config_value)
code, output, error = self.run_autopilot(
['run', 'tests', '--config', config_string])
self.assertThat(code, Equals(0))
def test_can_list_empty_test_dir(self):
"""Autopilot list must report 0 tests found with an empty test
module."""
code, output, error = self.run_autopilot_list()
self.assertThat(code, Equals(0))
self.assertThat(error, Equals(''))
self.assertTestsInOutput([], output)
def test_can_list_tests(self):
"""Autopilot must find tests in a file."""
self.create_test_file(
'test_simple.py', dedent("""\
from autopilot.testcase import AutopilotTestCase
class SimpleTest(AutopilotTestCase):
def test_simple(self):
pass
""")
)
# ideally these would be different tests, but I'm lazy:
valid_test_specs = [
'tests',
'tests.test_simple',
'tests.test_simple.SimpleTest',
'tests.test_simple.SimpleTest.test_simple',
]
for test_spec in valid_test_specs:
code, output, error = self.run_autopilot_list(test_spec)
self.assertThat(code, Equals(0))
self.assertThat(error, Equals(''))
self.assertTestsInOutput(
['tests.test_simple.SimpleTest.test_simple'], output)
def test_list_tests_with_import_error(self):
self.create_test_file(
'test_simple.py', dedent("""\
from autopilot.testcase import AutopilotTestCase
# create an import error:
import asdjkhdfjgsdhfjhsd
class SimpleTest(AutopilotTestCase):
def test_simple(self):
pass
""")
)
code, output, error = self.run_autopilot_list()
self.assertThat(code, Equals(0))
self.assertThat(error, Equals(''))
self.assertThat(
output,
MatchesRegex(
".*ImportError: No module named [']?asdjkhdfjgsdhfjhsd[']?.*",
re.DOTALL
)
)
def test_list_tests_with_syntax_error(self):
self.create_test_file(
'test_simple.py', dedent("""\
from autopilot.testcase import AutopilotTestCase
# create a syntax error:
..
class SimpleTest(AutopilotTestCase):
def test_simple(self):
pass
""")
)
code, output, error = self.run_autopilot_list()
expected_error = 'SyntaxError: invalid syntax'
self.assertThat(code, Equals(0))
self.assertThat(error, Equals(''))
self.assertThat(output, Contains(expected_error))
def test_list_nonexistent_test_returns_nonzero(self):
code, output, error = self.run_autopilot_list(list_spec='1234')
expected_msg = "could not import package 1234: No module"
expected_result = "0 total tests"
self.assertThat(code, Equals(1))
self.assertThat(output, Contains(expected_msg))
self.assertThat(output, Contains(expected_result))
def test_can_list_scenariod_tests(self):
"""Autopilot must show scenario counts next to tests that have
scenarios."""
self.create_test_file(
'test_simple.py', dedent("""\
from autopilot.testcase import AutopilotTestCase
class SimpleTest(AutopilotTestCase):
scenarios = [
('scenario one', {'key': 'value'}),
]
def test_simple(self):
pass
""")
)
expected_output = '''\
Loading tests from: %s
*1 tests.test_simple.SimpleTest.test_simple
1 total tests.
''' % self.base_path
code, output, error = self.run_autopilot_list()
self.assertThat(code, Equals(0))
self.assertThat(error, Equals(''))
self.assertThat(output, Equals(expected_output))
def test_can_list_scenariod_tests_with_multiple_scenarios(self):
"""Autopilot must show scenario counts next to tests that have
scenarios.
Tests multiple scenarios on a single test suite with multiple test
cases.
"""
self.create_test_file(
'test_simple.py', dedent("""\
from autopilot.testcase import AutopilotTestCase
class SimpleTest(AutopilotTestCase):
scenarios = [
('scenario one', {'key': 'value'}),
('scenario two', {'key': 'value2'}),
]
def test_simple(self):
pass
def test_simple_two(self):
pass
""")
)
code, output, error = self.run_autopilot_list()
self.assertThat(code, Equals(0))
self.assertThat(error, Equals(''))
self.assertTestsInOutput(
[
(2, 'tests.test_simple.SimpleTest.test_simple'),
(2, 'tests.test_simple.SimpleTest.test_simple_two'),
],
output
)
def test_can_list_invalid_scenarios(self):
"""Autopilot must ignore scenarios that are not lists."""
self.create_test_file(
'test_simple.py', dedent("""\
from autopilot.testcase import AutopilotTestCase
class SimpleTest(AutopilotTestCase):
scenarios = None
def test_simple(self):
pass
""")
)
code, output, error = self.run_autopilot_list()
self.assertThat(code, Equals(0))
self.assertThat(error, Equals(''))
self.assertTestsInOutput(
['tests.test_simple.SimpleTest.test_simple'], output)
def test_local_module_loaded_and_not_system_module(self):
module_path1 = self.create_empty_test_module()
module_path2 = self.create_empty_test_module()
self.base_path = module_path2
retcode, stdout, stderr = self.run_autopilot(
["run", "tests"],
pythonpath=module_path1,
use_script=True
)
self.assertThat(stdout, Contains(module_path2))
def test_can_list_just_suites(self):
"""Must only list available suites, not the contained tests."""
self.create_test_file(
'test_simple_suites.py', dedent("""\
from autopilot.testcase import AutopilotTestCase
class SimpleTest(AutopilotTestCase):
def test_simple(self):
pass
class AnotherSimpleTest(AutopilotTestCase):
def test_another_simple(self):
pass
def test_yet_another_simple(self):
pass
""")
)
code, output, error = self.run_autopilot_list(extra_args=['--suites'])
self.assertThat(code, Equals(0))
self.assertThat(error, Equals(''))
self.assertTestsInOutput(
['tests.test_simple_suites.SimpleTest',
'tests.test_simple_suites.AnotherSimpleTest'],
output, total_title='suites')
@skipIf(platform.model() != "Desktop", "Only suitable on Desktop (VidRec)")
def test_record_flag_works(self):
"""Must be able to record videos when the -r flag is present."""
video_dir = mktemp()
ap_dir = '/tmp/autopilot'
video_session_pattern = '/tmp/rMD-session*'
self.addCleanup(remove_if_exists, video_dir)
self.addCleanup(
remove_if_exists,
'%s/Dummy_Description.ogv' % (ap_dir)
)
self.addCleanup(remove_if_exists, ap_dir)
mock_test_case = Mock()
mock_test_case.shortDescription.return_value = 'Dummy_Description'
orig_sessions = glob.glob(video_session_pattern)
video_logger = RMDVideoLogFixture(video_dir, mock_test_case)
video_logger.setUp()
video_logger._test_passed = False
# We use Eventually() to avoid the case where recordmydesktop does not
# create a file because it gets stopped before it's even started
# capturing anything.
self.assertThat(
lambda: glob.glob(video_session_pattern),
Eventually(NotEquals(orig_sessions))
)
video_logger._stop_video_capture(mock_test_case)
self.assertTrue(os.path.exists(video_dir))
self.assertTrue(os.path.exists(
'%s/Dummy_Description.ogv' % (video_dir)))
self.assertFalse(os.path.exists(
'%s/Dummy_Description.ogv' % (ap_dir)))
@skipIf(platform.model() != "Desktop", "Only suitable on Desktop (VidRec)")
def test_record_dir_option_and_record_works(self):
"""Must be able to specify record directory flag and record."""
# The sleep is to avoid the case where recordmydesktop does not create
# a file because it gets stopped before it's even started capturing
# anything.
self.create_test_file(
"test_simple.py", dedent("""\
from autopilot.testcase import AutopilotTestCase
from time import sleep
class SimpleTest(AutopilotTestCase):
def test_simple(self):
sleep(1)
self.fail()
""")
)
video_dir = mktemp()
ap_dir = '/tmp/autopilot'
self.addCleanup(remove_if_exists, video_dir)
should_delete = not os.path.exists(ap_dir)
if should_delete:
self.addCleanup(remove_if_exists, ap_dir)
else:
self.addCleanup(
remove_if_exists,
'%s/tests.test_simple.SimpleTest.test_simple.ogv' % (ap_dir))
code, output, error = self.run_autopilot(
["run", "-r", "-rd", video_dir, "tests"])
self.assertThat(code, Equals(1))
self.assertTrue(os.path.exists(video_dir))
self.assertTrue(os.path.exists(
'%s/tests.test_simple.SimpleTest.test_simple.ogv' % (video_dir)))
self.assertFalse(
os.path.exists(
'%s/tests.test_simple.SimpleTest.test_simple.ogv' % (ap_dir)))
@skipIf(platform.model() != "Desktop", "Only suitable on Desktop (VidRec)")
def test_record_dir_option_works(self):
"""Must be able to specify record directory flag."""
# The sleep is to avoid the case where recordmydesktop does not create
# a file because it gets stopped before it's even started capturing
# anything.
self.create_test_file(
"test_simple.py", dedent("""\
from autopilot.testcase import AutopilotTestCase
from time import sleep
class SimpleTest(AutopilotTestCase):
def test_simple(self):
sleep(1)
self.fail()
""")
)
video_dir = mktemp()
self.addCleanup(remove_if_exists, video_dir)
code, output, error = self.run_autopilot(
["run", "-rd", video_dir, "tests"])
self.assertThat(code, Equals(1))
self.assertTrue(os.path.exists(video_dir))
self.assertTrue(
os.path.exists(
'%s/tests.test_simple.SimpleTest.test_simple.ogv' %
(video_dir)))
@skipIf(platform.model() != "Desktop", "Only suitable on Desktop (VidRec)")
def test_no_videos_saved_when_record_option_is_not_present(self):
"""Videos must not be saved if the '-r' option is not specified."""
self.create_test_file(
"test_simple.py", dedent("""\
from autopilot.testcase import AutopilotTestCase
from time import sleep
class SimpleTest(AutopilotTestCase):
def test_simple(self):
sleep(1)
self.fail()
""")
)
self.addCleanup(
remove_if_exists,
'/tmp/autopilot/tests.test_simple.SimpleTest.test_simple.ogv')
code, output, error = self.run_autopilot(["run", "tests"])
self.assertThat(code, Equals(1))
self.assertFalse(os.path.exists(
'/tmp/autopilot/tests.test_simple.SimpleTest.test_simple.ogv'))
@skipIf(platform.model() != "Desktop", "Only suitable on Desktop (VidRec)")
def test_no_videos_saved_for_skipped_test(self):
"""Videos must not be saved if the test has been skipped (not
failed).
"""
self.create_test_file(
"test_simple.py", dedent("""\
from autopilot.testcase import AutopilotTestCase
from time import sleep
class SimpleTest(AutopilotTestCase):
def test_simple(self):
sleep(1)
self.skip("Skipping Test")
""")
)
video_file_path = (
'/tmp/autopilot/tests.test_simple.SimpleTest.test_simple.ogv')
self.addCleanup(remove_if_exists, video_file_path)
code, output, error = self.run_autopilot(["run", "-r", "tests"])
self.assertThat(code, Equals(0))
self.assertThat(os.path.exists(video_file_path), Equals(False))
@skipIf(platform.model() != "Desktop", "Only suitable on Desktop (VidRec)")
def test_no_video_session_dir_saved_for_passed_test(self):
"""RecordMyDesktop should clean up its session files in tmp dir."""
with TempDir() as tmp_dir_fixture:
dir_pattern = os.path.join(tmp_dir_fixture.path, 'rMD-session*')
original_session_dirs = set(glob.glob(dir_pattern))
get_new_sessions = lambda: \
set(glob.glob(dir_pattern)) - original_session_dirs
mock_test_case = Mock()
mock_test_case.shortDescription.return_value = "Dummy_Description"
logger = RMDVideoLogFixture(tmp_dir_fixture.path, mock_test_case)
logger.set_recording_dir(tmp_dir_fixture.path)
logger._recording_opts = ['--workdir', tmp_dir_fixture.path] \
+ logger._recording_opts
logger.setUp()
self.assertThat(get_new_sessions, Eventually(NotEquals(set())))
logger._stop_video_capture(mock_test_case)
self.assertThat(get_new_sessions, Eventually(Equals(set())))
@skipIf(platform.model() != "Desktop", "Only suitable on Desktop (VidRec)")
def test_no_video_for_nested_testcase_when_parent_and_child_fail(self):
"""Test recording must not create a new recording for nested testcases
where both the parent and the child testcase fail.
"""
self.create_test_file(
"test_simple.py", dedent("""\
from autopilot.testcase import AutopilotTestCase
import os
class OuterTestCase(AutopilotTestCase):
def test_nested_classes(self):
class InnerTestCase(AutopilotTestCase):
def test_will_fail(self):
self.assertTrue(False)
InnerTestCase("test_will_fail").run()
self.assertTrue(False)
""")
)
expected_video_file = (
'/tmp/autopilot/tests.test_simple.OuterTestCase.'
'test_nested_classes.ogv')
erroneous_video_file = (
'/tmp/autopilot/tests.test_simple.OuterTestCase.'
'test_nested_classes.InnerTestCase.test_will_fail.ogv')
self.addCleanup(remove_if_exists, expected_video_file)
self.addCleanup(remove_if_exists, erroneous_video_file)
code, output, error = self.run_autopilot(["run", "-v", "-r", "tests"])
self.assertThat(code, Equals(1))
self.assertThat(os.path.exists(expected_video_file), Equals(True))
self.assertThat(os.path.exists(erroneous_video_file), Equals(False))
def test_runs_with_import_errors_fail(self):
"""Import errors inside a test must be considered a test failure."""
self.create_test_file(
'test_simple.py', dedent("""\
from autopilot.testcase import AutopilotTestCase
# create an import error:
import asdjkhdfjgsdhfjhsd
class SimpleTest(AutopilotTestCase):
def test_simple(self):
pass
""")
)
code, output, error = self.run_autopilot(["run", "tests"])
self.assertThat(code, Equals(1))
self.assertThat(error, Equals(''))
self.assertThat(
output,
MatchesRegex(
".*ImportError: No module named [']?asdjkhdfjgsdhfjhsd[']?.*",
re.DOTALL
)
)
self.assertThat(output, Contains("FAILED (failures=1)"))
def test_runs_with_syntax_errors_fail(self):
"""Import errors inside a test must be considered a test failure."""
self.create_test_file(
'test_simple.py', dedent("""\
from autopilot.testcase import AutopilotTestCase
# create a syntax error:
..
class SimpleTest(AutopilotTestCase):
def test_simple(self):
pass
""")
)
code, output, error = self.run_autopilot(["run", "tests"])
expected_error = '''\
tests/test_simple.py", line 4
..
^
SyntaxError: invalid syntax
'''
self.assertThat(code, Equals(1))
self.assertThat(error, Equals(''))
self.assertThat(output, Contains(expected_error))
self.assertThat(output, Contains("FAILED (failures=1)"))
def test_can_create_subunit_result_file(self):
self.create_test_file(
"test_simple.py", dedent("""\
from autopilot.testcase import AutopilotTestCase
class SimpleTest(AutopilotTestCase):
def test_simple(self):
pass
""")
)
output_file_path = mktemp()
self.addCleanup(remove_if_exists, output_file_path)
code, output, error = self.run_autopilot([
"run",
"-o", output_file_path,
"-f", "subunit",
"tests"])
self.assertThat(code, Equals(0))
self.assertTrue(os.path.exists(output_file_path))
def test_launch_needs_arguments(self):
"""Autopilot launch must complain if not given an application to
launch."""
rc, _, _ = self.run_autopilot(["launch"])
self.assertThat(rc, Equals(2))
def test_complains_on_unknown_introspection_type(self):
"""Launching a binary that does not support an introspection type we
are familiar with must result in a nice error message.
"""
rc, stdout, _ = self.run_autopilot(["launch", "yes"])
self.assertThat(rc, Equals(1))
self.assertThat(
stdout,
Contains(
"Error: Could not determine introspection type to use for "
"application '/usr/bin/yes'"))
def test_complains_on_missing_file(self):
"""Must give a nice error message if we try and launch a binary that's
missing."""
rc, stdout, _ = self.run_autopilot(["launch", "DoEsNotExist"])
self.assertThat(rc, Equals(1))
self.assertThat(
stdout, Contains("Error: Cannot find application 'DoEsNotExist'"))
def test_complains_on_non_dynamic_binary(self):
"""Must give a nice error message when passing in a non-dynamic
binary."""
# tzselect is a bash script, and is in the base system, so should
# always exist.
rc, stdout, _ = self.run_autopilot(["launch", "tzselect"])
self.assertThat(rc, Equals(1))
self.assertThat(
stdout, Contains(
"Error detecting launcher: Command '['ldd', "
"'/usr/bin/tzselect']' returned non-zero exit status 1\n"
"(Perhaps use the '-i' argument to specify an interface.)\n")
)
def test_run_random_order_flag_works(self):
"""Must run tests in random order when -ro is used"""
self.create_test_file(
"test_simple.py", dedent("""\
from autopilot.testcase import AutopilotTestCase
from time import sleep
class SimpleTest(AutopilotTestCase):
def test_simple_one(self):
pass
def test_simple_two(self):
pass
""")
)
code, output, error = self.run_autopilot(["run", "-ro", "tests"])
self.assertThat(code, Equals(0))
self.assertThat(output, Contains('Running tests in random order'))
def test_run_random_flag_not_used(self):
"""Must not run tests in random order when -ro is not used"""
self.create_test_file(
"test_simple.py", dedent("""\
from autopilot.testcase import AutopilotTestCase
from time import sleep
class SimpleTest(AutopilotTestCase):
def test_simple_one(self):
pass
def test_simple_two(self):
pass
""")
)
code, output, error = self.run_autopilot(["run", "tests"])
self.assertThat(code, Equals(0))
self.assertThat(output, Not(Contains('Running tests in random order')))
def test_get_test_configuration_from_command_line(self):
self.create_test_file(
'test_config.py', dedent("""\
from autopilot import get_test_configuration
from autopilot.testcase import AutopilotTestCase
class Tests(AutopilotTestCase):
def test_foo(self):
c = get_test_configuration()
print(c['foo'])
""")
)
code, output, error = self.run_autopilot(
["run", "--config", "foo=This is a test", "tests"]
)
self.assertThat(code, Equals(0))
self.assertIn("This is a test", output)
class AutopilotVerboseFunctionalTests(AutopilotFunctionalTestsBase):
"""Scenarioed functional tests for autopilot's verbose logging."""
scenarios = [
('text_format', dict(output_format='text')),
('xml_format', dict(output_format='xml'))
]
def test_verbose_flag_works(self):
"""Verbose flag must log to stderr."""
self.create_test_file(
"test_simple.py", dedent("""\
from autopilot.testcase import AutopilotTestCase
class SimpleTest(AutopilotTestCase):
def test_simple(self):
pass
""")
)
code, output, error = self.run_autopilot(["run",
"-f", self.output_format,
"-v", "tests"])
self.assertThat(code, Equals(0))
self.assertThat(
error, Contains(
"Starting test tests.test_simple.SimpleTest.test_simple"))
def test_verbose_flag_shows_timestamps(self):
"""Verbose log must include timestamps."""
self.create_test_file(
"test_simple.py", dedent("""\
from autopilot.testcase import AutopilotTestCase
class SimpleTest(AutopilotTestCase):
def test_simple(self):
pass
""")
)
code, output, error = self.run_autopilot(["run",
"-f", self.output_format,
"-v", "tests"])
self.assertThat(error, MatchesRegex("^\d\d:\d\d:\d\d\.\d\d\d"))
def test_verbose_flag_shows_success(self):
"""Verbose log must indicate successful tests (text format)."""
self.create_test_file(
"test_simple.py", dedent("""\
from autopilot.testcase import AutopilotTestCase
class SimpleTest(AutopilotTestCase):
def test_simple(self):
pass
""")
)
code, output, error = self.run_autopilot(["run",
"-f", self.output_format,
"-v", "tests"])
self.assertThat(
error, Contains("OK: tests.test_simple.SimpleTest.test_simple"))
def test_verbose_flag_shows_error(self):
"""Verbose log must indicate test error with a traceback."""
self.create_test_file(
"test_simple.py", dedent("""\
from autopilot.testcase import AutopilotTestCase
class SimpleTest(AutopilotTestCase):
def test_simple(self):
raise RuntimeError("Intentionally fail test.")
""")
)
code, output, error = self.run_autopilot(["run",
"-f", self.output_format,
"-v", "tests"])
self.assertThat(
error, Contains("ERROR: tests.test_simple.SimpleTest.test_simple"))
self.assertThat(error, Contains("traceback:"))
self.assertThat(
error,
Contains("RuntimeError: Intentionally fail test.")
)
def test_verbose_flag_shows_failure(self):
"""Verbose log must indicate a test failure with a traceback (xml
format)."""
self.create_test_file(
"test_simple.py", dedent("""\
from autopilot.testcase import AutopilotTestCase
class SimpleTest(AutopilotTestCase):
def test_simple(self):
self.assertTrue(False)
""")
)
code, output, error = self.run_autopilot(["run",
"-f", self.output_format,
"-v", "tests"])
self.assertIn("FAIL: tests.test_simple.SimpleTest.test_simple", error)
self.assertIn("traceback:", error)
self.assertIn("AssertionError: False is not true", error)
def test_verbose_flag_captures_nested_autopilottestcase_classes(self):
"""Verbose log must contain the log details of both the nested and
parent testcase."""
self.create_test_file(
"test_simple.py", dedent("""\
from autopilot.testcase import AutopilotTestCase
import os
class OuterTestCase(AutopilotTestCase):
def test_nested_classes(self):
class InnerTestCase(AutopilotTestCase):
def test_produce_log_output(self):
self.assertTrue(True)
InnerTestCase("test_produce_log_output").run()
self.assertTrue(True)
""")
)
code, output, error = self.run_autopilot(["run",
"-f", self.output_format,
"-v", "tests"])
self.assertThat(code, Equals(0))
self.assertThat(
error,
Contains(
"Starting test tests.test_simple.OuterTestCase."
"test_nested_classes"
)
)
self.assertThat(
error,
Contains(
"Starting test tests.test_simple.InnerTestCase."
"test_produce_log_output"
)
)
def test_can_enable_debug_output(self):
"""Verbose log must show debug messages if we specify '-vv'."""
self.create_test_file(
"test_simple.py", dedent("""\
from autopilot.testcase import AutopilotTestCase
from autopilot.utilities import get_debug_logger
class SimpleTest(AutopilotTestCase):
def test_simple(self):
get_debug_logger().debug("Hello World")
""")
)
code, output, error = self.run_autopilot(["run",
"-f", self.output_format,
"-vv", "tests"])
self.assertThat(error, Contains("Hello World"))
def test_debug_output_not_shown_by_default(self):
"""Verbose log must not show debug messages unless we specify '-vv'."""
self.create_test_file(
"test_simple.py", dedent("""\
from autopilot.testcase import AutopilotTestCase
from autopilot.utilities import get_debug_logger
class SimpleTest(AutopilotTestCase):
def test_simple(self):
get_debug_logger().debug("Hello World")
""")
)
code, output, error = self.run_autopilot(["run",
"-f", self.output_format,
"-v", "tests"])
self.assertThat(error, Not(Contains("Hello World")))
def test_debug_output_not_shown_by_default_normal_logger(self):
"""Verbose log must not show logger.debug level details with -v."""
debug_string = self.getUniqueString()
self.create_test_file(
"test_simple.py", dedent("""\
import logging
from autopilot.testcase import AutopilotTestCase
logger = logging.getLogger(__name__)
class SimpleTest(AutopilotTestCase):
def test_simple(self):
logger.debug('{debug_string}')
""".format(debug_string=debug_string))
)
code, output, error = self.run_autopilot(["run",
"-f", self.output_format,
"-v", "tests"])
self.assertThat(error, Not(Contains(debug_string)))
def test_verbose_flag_shows_autopilot_version(self):
from autopilot import get_version_string
"""Verbose log must indicate successful tests (text format)."""
self.create_test_file(
"test_simple.py", dedent("""\
from autopilot.testcase import AutopilotTestCase
class SimpleTest(AutopilotTestCase):
def test_simple(self):
pass
""")
)
code, output, error = self.run_autopilot(["run",
"-f", self.output_format,
"-v", "tests"])
self.assertThat(
error, Contains(get_version_string()))
def test_failfast(self):
"""Run stops after first error encountered."""
self.create_test_file(
'test_failfast.py', dedent("""\
from autopilot.testcase import AutopilotTestCase
class SimpleTest(AutopilotTestCase):
def test_one(self):
raise Exception
def test_two(self):
raise Exception
""")
)
code, output, error = self.run_autopilot(["run",
"--failfast",
"tests"])
self.assertThat(code, Equals(1))
self.assertIn("Ran 1 test", output)
self.assertIn("FAILED (failures=1)", output)
./autopilot/tests/functional/test_ap_apps.py 0000644 0000041 0000041 00000035375 13061552435 021644 0 ustar www-data www-data # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Autopilot Functional Test Tool
# Copyright (C) 2012-2013 Canonical
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
import datetime
import os
import subprocess
import logging
import sys
from testtools import skipIf
from testtools.matchers import (
Equals,
LessThan,
Not,
Raises,
raises,
)
from textwrap import dedent
from fixtures import EnvironmentVariable
from autopilot.application import (
NormalApplicationLauncher,
UpstartApplicationLauncher,
)
from autopilot.exceptions import ProcessSearchError
from autopilot.process import ProcessManager
from autopilot.platform import model
from autopilot.testcase import AutopilotTestCase
from autopilot.tests.functional.fixtures import (
ExecutableScript,
TempDesktopFile,
)
from autopilot.introspection import get_proxy_object_for_existing_process
from autopilot.introspection.utilities import _pid_is_running
from autopilot.utilities import sleep
logger = logging.getLogger(__name__)
def locale_is_supported():
"""Check if our currently set locale supports writing unicode to stdout."""
try:
encoding = sys.stdout.encoding or sys.getfilesystemencoding()
'\u2026'.encode(encoding)
return True
except UnicodeEncodeError:
return False
def _get_unused_pid():
"""Returns a Process ID number that isn't currently running.
:raises: **RuntimeError** if unable to produce a number that doesn't
correspond to a currently running process.
"""
for i in range(10000, 20000):
if not _pid_is_running(i):
return i
raise RuntimeError("Unable to find test PID.")
class ApplicationTests(AutopilotTestCase):
"""A base class for application mixin tests."""
def write_script(self, content, extension=".py"):
"""Write a script to a temporary file, make it executable,
and return the path to the script file.
"""
return self.useFixture(ExecutableScript(content, extension)).path
class ApplicationLaunchTests(ApplicationTests):
def test_unknown_app_exception(self):
"""launch_test_app must raise a RuntimeError when asked to launch an
application that has an unknown introspection type.
"""
path = self.write_script("")
expected_error_message = (
"Autopilot could not determine the correct "
"introspection type to use. You can specify this by providing "
"app_type."
)
self.assertThat(
lambda: self.launch_test_application(path),
raises(RuntimeError(expected_error_message)))
def test_creating_app_proxy_for_running_app_not_on_dbus_fails(self):
"""Creating app proxy object for an application that isn't connected to
the dbus session must raise a ProcessSearchError exception.
"""
path = self.write_script(dedent("""\
#!%s
from time import sleep
while True:
print("Still running")
sleep(1)
""" % sys.executable))
self.assertThat(
lambda: self.launch_test_application(path, app_type='qt'),
raises(ProcessSearchError)
)
def test_creating_app_for_non_running_app_fails(self):
"""Attempting to create an application proxy object for a process
(using a PID) that isn't running must raise an exception.
"""
pid = _get_unused_pid()
self.assertThat(
lambda: get_proxy_object_for_existing_process(pid=pid),
raises(ProcessSearchError("PID %d could not be found" % pid))
)
def test_creating_proxy_for_segfaulted_app_failed(self):
"""Creating a proxy object for an application that has died since
launching must throw ProcessSearchError exception.
"""
path = self.write_script(dedent("""\
#!%s
from time import sleep
import sys
sleep(5)
sys.exit(1)
""" % sys.executable))
expected_error = "Process exited with exit code: 1"
self.assertThat(
lambda: self.launch_test_application(path, app_type='qt'),
raises(ProcessSearchError(expected_error))
)
def test_creating_proxy_for_segfaulted_app_fails_quicker(self):
"""Searching for a process that has died since launching, the search
must fail before the 10 second timeout.
"""
path = self.write_script(dedent("""\
#!%s
from time import sleep
import sys
sleep(1)
sys.exit(1)
""" % sys.executable))
start = datetime.datetime.now()
try:
self.launch_test_application(path, app_type='qt')
except ProcessSearchError:
end = datetime.datetime.now()
else:
self.fail(
"launch_test_application didn't raise expected exception"
)
difference = end - start
self.assertThat(difference.total_seconds(), LessThan(5))
@skipIf(model() != "Desktop", "Not suitable for device (Qt4)")
def test_closing_app_produces_good_error(self):
"""Testing an application that closes before the test ends must
produce a good error message when calling refresh_state() on the
application proxy object.
"""
path = self.write_script(dedent("""\
#!%s
from PyQt4.QtGui import QMainWindow, QApplication
from PyQt4.QtCore import QTimer
from sys import argv
app = QApplication(argv)
win = QMainWindow()
win.show()
QTimer.singleShot(8000, app.exit)
app.exec_()
""" % sys.executable))
app_proxy = self.launch_test_application(path, app_type='qt')
self.assertTrue(app_proxy is not None)
def crashing_fn():
for i in range(10):
logger.debug("%d %r", i, app_proxy.refresh_state())
sleep(1)
self.assertThat(
crashing_fn,
raises(
RuntimeError(
"Application under test exited before the test finished!"
)
)
)
class QmlTestMixin(object):
def get_qml_viewer_app_path(self):
try:
qtversions = subprocess.check_output(
['qtchooser', '-list-versions'],
universal_newlines=True
).split('\n')
check_func = self._find_qt_binary_chooser
except OSError:
# This means no qtchooser is installed, so let's check for
# qmlviewer and qmlscene manually, the old way
qtversions = ['qt4', 'qt5']
check_func = self._find_qt_binary_old
not_found = True
if 'qt4' in qtversions:
path = check_func('qt4', 'qmlviewer')
if path:
not_found = False
self.qml_viewer_app_path = path
self.useFixture(EnvironmentVariable("QT_SELECT", "qt4"))
if 'qt5' in qtversions:
path = check_func('qt5', 'qmlscene')
if path:
not_found = False
self.qml_viewer_app_path = path
self.useFixture(EnvironmentVariable("QT_SELECT", "qt5"))
if not_found:
self.skip("Neither qmlviewer nor qmlscene is installed")
return self.qml_viewer_app_path
def _find_qt_binary_chooser(self, version, name):
# Check for existence of the binary when qtchooser is installed
# We cannot use 'which', as qtchooser installs wrappers - we need to
# check in the actual library paths
env = subprocess.check_output(
['qtchooser', '-qt=' + version, '-print-env'],
universal_newlines=True).split('\n')
for i in env:
if i.find('QTTOOLDIR') >= 0:
path = i.lstrip("QTTOOLDIR=").strip('"') + "/" + name
if os.path.exists(path):
return path
return None
return None
def _find_qt_binary_old(self, version, name):
# Check for the existence of the binary the old way
try:
path = subprocess.check_output(['which', 'qmlviewer'],
universal_newlines=True).strip()
except subprocess.CalledProcessError:
path = None
return path
class QtTests(ApplicationTests, QmlTestMixin):
def test_can_launch_normal_app(self):
path = self.get_qml_viewer_app_path()
fixture = self.useFixture(TempDesktopFile(exec_=path,))
launcher = self.useFixture(NormalApplicationLauncher())
app_proxy = launcher.launch(
path,
['--desktop_file_hint=%s' % fixture.get_desktop_file_path()],
app_type='qt'
)
self.assertTrue(app_proxy is not None)
def test_can_launch_upstart_app(self):
path = self.get_qml_viewer_app_path()
fixture = self.useFixture(TempDesktopFile(exec_=path,))
launcher = self.useFixture(UpstartApplicationLauncher())
launcher.launch(fixture.get_desktop_file_id())
@skipIf(model() != "Desktop", "Only suitable on Desktop (Qt4)")
def test_can_launch_normal_qt_script(self):
path = self.write_script(dedent("""\
#!%s
from PyQt4.QtGui import QMainWindow, QApplication
from sys import argv
app = QApplication(argv)
win = QMainWindow()
win.show()
app.exec_()
""" % sys.executable))
app_proxy = self.launch_test_application(path, app_type='qt')
self.assertTrue(app_proxy is not None)
# TODO: move this into a test module that tests bamf.
@skipIf(model() != 'Desktop', "Bamf only available on desktop (Qt4)")
def test_bamf_geometry_gives_reliable_results(self):
path = self.write_script(dedent("""\
#!%s
from PyQt4.QtGui import QMainWindow, QApplication
from sys import argv
app = QApplication(argv)
win = QMainWindow()
win.show()
app.exec_()
""" % sys.executable))
app_proxy = self.launch_test_application(path, app_type='qt')
proxy_window = app_proxy.select_single('QMainWindow')
pm = ProcessManager.create()
window = [
w for w in pm.get_open_windows()
if w.name == os.path.basename(path)
][0]
self.assertThat(list(window.geometry), Equals(proxy_window.geometry))
def test_can_launch_qt_script_that_aborts(self):
path = self.write_script(dedent("""\
#!/usr/bin/python
import os
import time
time.sleep(1)
os.abort()
"""))
launch_fn = lambda: self.launch_test_application(path, app_type='qt')
self.assertThat(launch_fn, raises(ProcessSearchError))
@skipIf(model() != "Desktop", "Only suitable on Desktop (Qt4)")
def test_can_launch_wrapper_script(self):
path = self.write_script(dedent("""\
#!%s
from PyQt4.QtGui import QMainWindow, QApplication
from sys import argv
app = QApplication(argv)
win = QMainWindow()
win.show()
app.exec_()
""" % sys.executable))
wrapper_path = self.write_script(dedent("""\
#!/bin/sh
echo "Launching %s"
%s $*
""" % (path, path)),
extension=".sh")
app_proxy = self.launch_test_application(wrapper_path, app_type='qt')
self.assertTrue(app_proxy is not None)
@skipIf(
model() != "Desktop" or not locale_is_supported(),
"Current locale is not supported or not on desktop (Qt4)"
)
def test_can_handle_non_unicode_stdout_and_stderr(self):
path = self.write_script(dedent("""\
#!%s
# -*- coding: utf-8 -*-
from PyQt4.QtGui import QMainWindow, QApplication
from sys import argv, stdout, stderr
app = QApplication(argv)
win = QMainWindow()
win.show()
stdout.write('Hello\x88stdout')
stdout.flush()
stderr.write('Hello\x88stderr')
stderr.flush()
app.exec_()
""" % sys.executable))
self.launch_test_application(path, app_type='qt')
details_dict = self.getDetails()
for name, content_obj in details_dict.items():
self.assertThat(
lambda: content_obj.as_text(),
Not(Raises())
)
@skipIf(model() != "Desktop", "Only suitable on Desktop (Gtk)")
class GtkTests(ApplicationTests):
def _get_mahjongg_path(self):
try:
return subprocess.check_output(
['which', 'gnome-mahjongg'], universal_newlines=True).strip()
except:
return
def test_can_launch_gtk_app(self):
mahjongg_path = self._get_mahjongg_path()
if not mahjongg_path:
self.skip("gnome-mahjongg not found.")
app_proxy = self.launch_test_application(mahjongg_path)
self.assertTrue(app_proxy is not None)
def test_can_launch_gtk_script(self):
path = self.write_script(dedent("""\
#!%s
from gi.repository import Gtk
win = Gtk.Window()
win.connect("delete-event", Gtk.main_quit)
win.show_all()
Gtk.main()
""" % sys.executable))
app_proxy = self.launch_test_application(path, app_type='gtk')
self.assertTrue(app_proxy is not None)
def test_can_launch_wrapper_script(self):
path = self.write_script(dedent("""\
#!%s
from gi.repository import Gtk
win = Gtk.Window()
win.connect("delete-event", Gtk.main_quit)
win.show_all()
Gtk.main()
""" % sys.executable))
wrapper_path = self.write_script(dedent("""\
#!/bin/sh
echo "Launching %s"
%s
""" % (path, path)),
extension=".sh")
app_proxy = self.launch_test_application(wrapper_path, app_type='gtk')
self.assertTrue(app_proxy is not None)
./autopilot/tests/functional/fixtures.py 0000644 0000041 0000041 00000013533 13061552435 021023 0 ustar www-data www-data # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Autopilot Functional Test Tool
# Copyright (C) 2014 Canonical
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
"""Fixtures for the autopilot functional test suite."""
import logging
import os
import stat
from shutil import rmtree
import tempfile
from textwrap import dedent
import time
from fixtures import EnvironmentVariable, Fixture
logger = logging.getLogger(__name__)
class ExecutableScript(Fixture):
"""Write some text to a file on disk and make it executable."""
def __init__(self, script, extension=".py"):
"""Initialise the fixture.
:param script: The contents of the script file.
:param extension: The desired extension on the script file.
"""
super(ExecutableScript, self).__init__()
self._script = script
self._extension = extension
def setUp(self):
super(ExecutableScript, self).setUp()
with tempfile.NamedTemporaryFile(
suffix=self._extension,
mode='w',
delete=False
) as f:
f.write(self._script)
self.path = f.name
self.addCleanup(os.unlink, self.path)
os.chmod(self.path, os.stat(self.path).st_mode | stat.S_IXUSR)
class TempDesktopFile(Fixture):
def __init__(self, type=None, exec_=None, name=None, icon=None):
"""Create a TempDesktopFile instance.
Parameters control the contents of the created desktop file. Default
values will create a desktop file with bogus contents.
:param type: The type field in the created file. Defaults to
'Application'.
:param exec_: The path to the file to execute.
:param name: The name of the application being launched. Defaults to
"Test App".
"""
super(TempDesktopFile, self).__init__()
type_line = type if type is not None else "Application"
exec_line = exec_ if exec_ is not None else "Not Important"
name_line = name if name is not None else "Test App"
icon_line = icon if icon is not None else "Not Important"
self._file_contents = dedent(
"""\
[Desktop Entry]
Type={}
Exec={}
Name={}
Icon={}
""".format(type_line, exec_line, name_line, icon_line)
)
def setUp(self):
super(TempDesktopFile, self).setUp()
path_created = TempDesktopFile._ensure_desktop_dir_exists()
self._desktop_file_path = self._create_desktop_file(
self._file_contents,
)
self.addCleanup(
TempDesktopFile._remove_desktop_file_components,
path_created,
self._desktop_file_path,
)
def get_desktop_file_path(self):
return self._desktop_file_path
def get_desktop_file_id(self):
return os.path.splitext(os.path.basename(self._desktop_file_path))[0]
@staticmethod
def _ensure_desktop_dir_exists():
desktop_file_dir = TempDesktopFile._desktop_file_dir()
if not os.path.exists(desktop_file_dir):
return TempDesktopFile._create_desktop_file_dir(desktop_file_dir)
return ''
@staticmethod
def _desktop_file_dir():
return os.path.join(
os.getenv('HOME'),
'.local',
'share',
'applications'
)
@staticmethod
def _create_desktop_file_dir(desktop_file_dir):
"""Create the directory specified.
Returns the component of the path that did not exist, or the empty
string if the entire path already existed.
"""
# We might be creating more than just the leaf directory, so we need to
# keep track of what doesn't already exist and remove it when we're
# done. Defaults to removing the full path
path_to_delete = ""
if not os.path.exists(desktop_file_dir):
path_to_delete = desktop_file_dir
full_path, leaf = os.path.split(desktop_file_dir)
while leaf != "":
if not os.path.exists(full_path):
path_to_delete = full_path
full_path, leaf = os.path.split(full_path)
try:
os.makedirs(desktop_file_dir)
except OSError:
logger.warning("Directory already exists: %s" % desktop_file_dir)
return path_to_delete
@staticmethod
def _remove_desktop_file_components(created_path, created_file):
if created_path != "":
rmtree(created_path)
else:
os.remove(created_file)
@staticmethod
def _create_desktop_file(file_contents):
_, tmp_file_path = tempfile.mkstemp(
suffix='.desktop',
dir=TempDesktopFile._desktop_file_dir()
)
with open(tmp_file_path, 'w') as desktop_file:
desktop_file.write(file_contents)
return tmp_file_path
class Timezone(Fixture):
def __init__(self, timezone):
self._timezone = timezone
def setUp(self):
super().setUp()
# These steps need to happen in the right order otherwise they won't
# get cleaned up properly and we'll be left in an incorrect timezone.
self.addCleanup(time.tzset)
self.useFixture(EnvironmentVariable('TZ', self._timezone))
time.tzset()
./autopilot/tests/functional/test_platform.py 0000644 0000041 0000041 00000002363 13061552435 022034 0 ustar www-data www-data # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Autopilot Functional Test Tool
# Copyright (C) 2014 Canonical
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
from testtools import TestCase, skipIf
import autopilot.platform as platform
class PublicAPITests(TestCase):
@skipIf(platform.model() != "Desktop", "Only available on desktop.")
def test_get_display_server_returns_x11(self):
self.assertEqual(platform.get_display_server(), "X11")
@skipIf(platform.model() == "Desktop", "Only available on device.")
def test_get_display_server_returns_mir(self):
self.assertEqual(platform.get_display_server(), "MIR")
./autopilot/tests/functional/test_types.py 0000644 0000041 0000041 00000007231 13061552435 021353 0 ustar www-data www-data # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Autopilot Functional Test Tool
# Copyright (C) 2013-2014 Canonical
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
from datetime import datetime
from autopilot.testcase import AutopilotTestCase
from autopilot.tests.functional import QmlScriptRunnerMixin
from autopilot.tests.functional.fixtures import Timezone
from textwrap import dedent
class DateTimeTests(AutopilotTestCase, QmlScriptRunnerMixin):
scenarios = [
('UTC', dict(
TZ='UTC',
)),
('NZ', dict(
TZ='Pacific/Auckland',
)),
('US Central', dict(
TZ='US/Central',
)),
('US Eastern', dict(
TZ='US/Eastern',
)),
('Hongkong', dict(
TZ='Hongkong'
)),
('CET', dict(
TZ='Europe/Copenhagen',
)),
# QML timezone database is incorrect/out-of-date for
# Europe/Moscow. Given the timestamp of 1411992000 (UTC: 2014:9:29
# 12:00) for this date offset should be +0400
# (http://en.wikipedia.org/wiki/Time_in_Russia#Daylight_saving_time)
# QML app gives: 2014:9:29 14:00 where it should be 2014:9:29 16:00
# ('MSK', dict(
# TZ='Europe/Moscow',
# )),
]
def get_test_qml_string(self, date_string):
return dedent("""
import QtQuick 2.0
import QtQml 2.2
Rectangle {
property date testingTime: new Date(%s);
Text {
text: testingTime;
}
}""" % date_string)
def test_qml_applies_timezone_to_timestamp(self):
"""Test that when given a timestamp the datetime displayed has the
timezone applied to it.
QML will apply a timezone calculation to a timestamp (but not a
timestring).
"""
self.useFixture(Timezone(self.TZ))
timestamp = 1411992000
timestamp_ms = 1411992000 * 1000
qml_script = self.get_test_qml_string(timestamp_ms)
expected_string = datetime.fromtimestamp(timestamp).strftime('%FT%T')
proxy = self.start_qml_script(qml_script)
self.assertEqual(
proxy.select_single('QQuickText').text,
expected_string
)
def test_timezone_not_applied_to_timestring(self):
"""Test that, in all timezones, the literal representation we get in
the proxy object matches the one in the Qml script.
"""
self.useFixture(Timezone(self.TZ))
qml_script = self.get_test_qml_string("'2014-01-15 12:34:52'")
proxy = self.start_qml_script(qml_script)
date_object = proxy.select_single("QQuickRectangle").testingTime
self.assertEqual(date_object.year, 2014)
self.assertEqual(date_object.month, 1)
self.assertEqual(date_object.day, 15)
self.assertEqual(date_object.hour, 12)
self.assertEqual(date_object.minute, 34)
self.assertEqual(date_object.second, 52)
self.assertEqual(datetime(2014, 1, 15, 12, 34, 52), date_object)
./autopilot/tests/functional/test_custom_assertions.py 0000644 0000041 0000041 00000010010 13061552435 023760 0 ustar www-data www-data # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Autopilot Functional Test Tool
# Copyright (C) 2012-2013 Canonical
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
from autopilot.testcase import AutopilotTestCase
from testtools.matchers import Equals, raises, Not
import logging
logger = logging.getLogger(__name__)
class TestObject(object):
test_property = 123
another_property = "foobar"
none_prop = None
def test_method(self):
return 456
class AssertionTests(AutopilotTestCase):
test_object = TestObject()
def test_assertProperty_raises_valueerror_on_empty_test(self):
"""assertProperty must raise ValueError if called without any
kwargs."""
self.assertThat(
lambda: self.assertProperty(self.test_object), raises(ValueError))
def test_assertProperty_raises_valueerror_on_callable(self):
"""assertProperty must raise ValueError when called with a callable
property name.
"""
self.assertThat(
lambda: self.assertProperty(self.test_object, test_method=456),
raises(ValueError))
def test_assertProperty_raises_assert_with_single_property(self):
"""assertProperty must raise an AssertionError when called with a
single property.
"""
self.assertThat(
lambda: self.assertProperty(self.test_object, test_property=234),
raises(AssertionError))
def test_assertProperty_doesnt_raise(self):
"""assertProperty must not raise an exception if called with correct
parameters.
"""
self.assertThat(
lambda: self.assertProperty(self.test_object, test_property=123),
Not(raises(AssertionError)))
def test_assertProperty_doesnt_raise_multiples(self):
"""assertProperty must not raise an exception if called with correct
parameters.
"""
self.assertThat(
lambda: self.assertProperty(
self.test_object, test_property=123,
another_property="foobar"),
Not(raises(AssertionError)))
def test_assertProperty_raises_assert_with_double_properties(self):
"""assertProperty must raise an AssertionError when called with
multiple properties.
"""
self.assertThat(
lambda: self.assertProperty(
self.test_object, test_property=234, another_property=123),
raises(AssertionError))
def test_assertProperties_works(self):
"""Asserts that the assertProperties method is a synonym for
assertProperty."""
self.assertThat(callable(self.assertProperties), Equals(True))
self.assertThat(
lambda: self.assertProperties(
self.test_object, test_property=123,
another_property="foobar"),
Not(raises(AssertionError)))
def test_assertProperty_raises_assertionerror_on_no_such_property(self):
"""AssertProperty must rise an AssertionError if the property is not
found."""
self.assertThat(
lambda: self.assertProperty(self.test_object, foo="bar"),
raises(AssertionError))
def test_assertProperty_works_for_None_properties(self):
"""Must be able to match properties whose values are None."""
self.assertThat(
lambda: self.assertProperties(self.test_object, none_prop=None),
Not(raises(AssertionError)))
./autopilot/tests/functional/test_application_launcher.py 0000644 0000041 0000041 00000006640 13061552435 024376 0 ustar www-data www-data # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Autopilot Functional Test Tool
# Copyright (C) 2014 Canonical
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
from testtools import TestCase
from unittest.mock import patch
from autopilot.testcase import AutopilotTestCase
from autopilot.application import _launcher
class AutopilotTestCaseClassTests(TestCase):
"""Test functions of the AutopilotTestCase class."""
@patch('autopilot.testcase.NormalApplicationLauncher')
def test_launch_test_application(self, nal):
class LauncherTest(AutopilotTestCase):
"""Test launchers."""
def test_anything(self):
pass
test_case = LauncherTest('test_anything')
with patch.object(test_case, 'useFixture') as uf:
result = test_case.launch_test_application('a', 'b', 'c')
uf.assert_called_once_with(nal.return_value)
uf.return_value.launch.assert_called_once_with('a', ('b', 'c'))
self.assertEqual(result, uf.return_value.launch.return_value)
@patch('autopilot.testcase.ClickApplicationLauncher')
def test_launch_click_package(self, cal):
class LauncherTest(AutopilotTestCase):
"""Test launchers."""
def test_anything(self):
pass
test_case = LauncherTest('test_anything')
with patch.object(test_case, 'useFixture') as uf:
result = test_case.launch_click_package('a', 'b', ['c', 'd'])
uf.assert_called_once_with(cal.return_value)
uf.return_value.launch.assert_called_once_with(
'a', 'b', ['c', 'd']
)
self.assertEqual(result, uf.return_value.launch.return_value)
@patch('autopilot.testcase.UpstartApplicationLauncher')
def test_launch_upstart_application_defaults(self, ual):
class LauncherTest(AutopilotTestCase):
"""Test launchers."""
def test_anything(self):
pass
test_case = LauncherTest('test_anything')
with patch.object(test_case, 'useFixture') as uf:
result = test_case.launch_upstart_application(
'a', ['b'], launcher_class=ual
)
uf.assert_called_once_with(ual.return_value)
uf.return_value.launch.assert_called_once_with('a', ['b'])
self.assertEqual(result, uf.return_value.launch.return_value)
def test_launch_upstart_application_custom_launcher(self):
class LauncherTest(AutopilotTestCase):
"""Test launchers."""
def test_anything(self):
pass
test_case = LauncherTest('test_anything')
self.assertRaises(
NotImplementedError,
test_case.launch_upstart_application,
'a', ['b'], launcher_class=_launcher.ApplicationLauncher
)
./autopilot/tests/__init__.py 0000644 0000041 0000041 00000005414 13061552435 016546 0 ustar www-data www-data # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Autopilot Functional Test Tool
# Copyright (C) 2012-2014 Canonical
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
import logging
import testtools
class LogHandlerTestCase(testtools.TestCase):
"""A mixin that adds a memento loghandler for testing logging."""
class MementoHandler(logging.Handler):
"""A handler class which stores logging records in a list."""
def __init__(self, *args, **kwargs):
"""Create the instance, and add a records attribute."""
super().__init__(*args, **kwargs)
self.records = []
def emit(self, record):
"""Just add the record to self.records."""
self.records.append(record)
def check(self, level, msg, check_traceback=False):
"""Check that something is logged."""
result = False
for rec in self.records:
if rec.levelname == level:
result = str(msg) in rec.getMessage()
if not result and check_traceback:
result = str(msg) in rec.exc_text
if result:
break
return result
def setUp(self):
"""Add the memento handler to the root logger."""
super().setUp()
self.memento_handler = self.MementoHandler()
self.root_logger = logging.getLogger()
self.root_logger.addHandler(self.memento_handler)
def tearDown(self):
"""Remove the memento handler from the root logger."""
self.root_logger.removeHandler(self.memento_handler)
super().tearDown()
def assertLogLevelContains(self, level, message, check_traceback=False):
check = self.memento_handler.check(
level, message, check_traceback=check_traceback)
msg = ('Expected logging message/s could not be found:\n%s\n'
'Current logging records are:\n%s')
expected = '\t%s: %s' % (level, message)
records = ['\t%s: %s' % (r.levelname, r.getMessage())
for r in self.memento_handler.records]
self.assertTrue(check, msg % (expected, '\n'.join(records)))
./autopilot/tests/acceptance/ 0000755 0000041 0000041 00000000000 13061552435 016517 5 ustar www-data www-data ./autopilot/tests/acceptance/__init__.py 0000644 0000041 0000041 00000001477 13061552435 020641 0 ustar www-data www-data # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Autopilot Functional Test Tool
# Copyright (C) 2014 Canonical
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
"""Acceptance tests for the autopilot vis tool."""
./autopilot/tests/acceptance/test_vis_main.py 0000644 0000041 0000041 00000003542 13061552435 021741 0 ustar www-data www-data # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Autopilot Functional Test Tool
# Copyright (C) 2014 Canonical
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
"""Acceptance tests for the autopilot vis tool."""
import sys
from testtools import skipIf
from testtools.matchers import Equals
from autopilot.introspection.dbus import CustomEmulatorBase
from autopilot.matchers import Eventually
from autopilot.platform import model
from autopilot.testcase import AutopilotTestCase
class VisToolEmulatorBase(CustomEmulatorBase):
pass
class VisAcceptanceTests(AutopilotTestCase):
def launch_windowmocker(self):
return self.launch_test_application("window-mocker", app_type="qt")
@skipIf(model() != "Desktop", "Vis not usable on device.")
def test_can_select_windowmocker(self):
wm = self.launch_windowmocker()
vis = self.launch_test_application(
sys.executable,
'-m', 'autopilot.run', 'vis', '-testability',
app_type='qt',
)
connection_list = vis.select_single('ConnectionList')
connection_list.slots.trySetSelectedItem(wm.applicationName)
self.assertThat(
connection_list.currentText,
Eventually(Equals(wm.applicationName))
)
./autopilot/tests/unit/ 0000755 0000041 0000041 00000000000 13061552443 015407 5 ustar www-data www-data ./autopilot/tests/unit/test_introspection_search.py 0000644 0000041 0000041 00000066036 13061552435 023261 0 ustar www-data www-data # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Autopilot Functional Test Tool
# Copyright (C) 2014 Canonical
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
from dbus import DBusException
import os
from unittest.mock import call, patch, Mock
from testtools import TestCase
from testtools.matchers import (
Contains,
Equals,
MatchesAll,
MatchesListwise,
MatchesSetwise,
Not,
raises,
)
from autopilot.exceptions import ProcessSearchError
from autopilot.utilities import sleep
from autopilot.introspection import _search as _s
from autopilot.introspection import CustomEmulatorBase
from autopilot.introspection.constants import AUTOPILOT_PATH
def ListContainsOnly(value_list):
"""Returns a MatchesAll matcher for comparing a list."""
return MatchesSetwise(*map(Equals, value_list))
class PassingFilter(object):
@classmethod
def matches(cls, dbus_tuple, params):
return True
class FailingFilter(object):
@classmethod
def matches(cls, dbus_tuple, params):
return False
class LowPriorityFilter(object):
@classmethod
def priority(cls):
return 0
class HighPriorityFilter(object):
@classmethod
def priority(cls):
return 10
class MatcherCallableTests(TestCase):
def test_can_provide_list_of_filters(self):
_s._filter_runner([PassingFilter], None, None)
def test_passing_empty_filter_list_raises(self):
self.assertThat(
lambda: _s._filter_runner([], None, None),
raises(ValueError("Filter list must not be empty"))
)
def test_matches_returns_True_with_PassingFilter(self):
self.assertTrue(_s._filter_runner([PassingFilter], None, None))
def test_matches_returns_False_with_FailingFilter(self):
self.assertFalse(_s._filter_runner([FailingFilter], None, None))
def test_fails_when_first_filter_fails(self):
self.assertFalse(
_s._filter_runner([FailingFilter, PassingFilter], None, None)
)
def test_fails_when_second_filter_fails(self):
self.assertFalse(
_s._filter_runner([PassingFilter, FailingFilter], None, None)
)
def test_passes_when_two_filters_pass(self):
self.assertTrue(
_s._filter_runner([PassingFilter, PassingFilter], None, None)
)
def test_fails_when_two_filters_fail(self):
self.assertFalse(
_s._filter_runner([FailingFilter, FailingFilter], None, None)
)
def test_filter_returning_False_results_in_failure(self):
class FalseFilter(object):
@classmethod
def matches(cls, dbus_tuple, params):
return False
_s._filter_runner([FalseFilter], None, None)
self.assertFalse(
_s._filter_runner([FalseFilter], None, None)
)
def test_runner_matches_passes_dbus_tuple_to_filter(self):
DBusConnectionFilter = Mock()
dbus_tuple = ("bus", "connection_name")
_s._filter_runner([DBusConnectionFilter], {}, dbus_tuple)
DBusConnectionFilter.matches.assert_called_once_with(
dbus_tuple, {}
)
class FilterFunctionGeneratorTests(TestCase):
"""Tests to ensure the correctness of the
_filter_function_from_search_params function.
"""
def test_uses_priority_sorted_filter_list(self):
unsorted_filters = [LowPriorityFilter, HighPriorityFilter]
matcher = _s._filter_function_with_sorted_filters(unsorted_filters, {})
self.assertThat(
matcher.args[0],
Equals([HighPriorityFilter, LowPriorityFilter])
)
class FiltersFromSearchParametersTests(TestCase):
def test_raises_with_unknown_search_parameter(self):
search_parameters = dict(unexpected_key=True)
placeholder_lookup = dict(noop_lookup=True)
self.assertThat(
lambda: _s._filters_from_search_parameters(
search_parameters,
placeholder_lookup
),
raises(
KeyError(
"Search parameter 'unexpected_key' doesn't have a "
"corresponding filter in %r"
% placeholder_lookup
)
)
)
def test_returns_only_required_filters(self):
search_parameters = dict(high=True, low=True)
filter_lookup = dict(
high=HighPriorityFilter,
low=LowPriorityFilter,
passing=PassingFilter,
)
self.assertThat(
_s._filters_from_search_parameters(
search_parameters,
filter_lookup
),
ListContainsOnly([HighPriorityFilter, LowPriorityFilter])
)
def test_creates_unique_list_of_filters(self):
search_parameters = dict(pid=True, process=True)
filter_lookup = dict(
pid=HighPriorityFilter,
process=HighPriorityFilter
)
self.assertThat(
_s._filters_from_search_parameters(
search_parameters,
filter_lookup
),
ListContainsOnly([HighPriorityFilter])
)
def test_doesnt_modify_search_parameters(self):
search_parameters = dict(high=True)
filter_lookup = dict(high=HighPriorityFilter)
_s._filters_from_search_parameters(
search_parameters,
filter_lookup
)
self.assertThat(search_parameters.get('high', None), Not(Equals(None)))
class MandatoryFiltersTests(TestCase):
def test_returns_list_containing_mandatory_filters(self):
self.assertThat(
_s._mandatory_filters(),
ListContainsOnly([
_s.ConnectionIsNotOurConnection,
_s.ConnectionIsNotOrgFreedesktopDBus
])
)
class PrioritySortFiltersTests(TestCase):
def test_sorts_filters_based_on_priority(self):
self.assertThat(
_s._priority_sort_filters(
[LowPriorityFilter, HighPriorityFilter]
),
Equals([HighPriorityFilter, LowPriorityFilter])
)
def test_sorts_single_filter_based_on_priority(self):
self.assertThat(
_s._priority_sort_filters(
[LowPriorityFilter]
),
Equals([LowPriorityFilter])
)
class FilterFunctionFromFiltersTests(TestCase):
def test_returns_a_callable(self):
self.assertTrue(
callable(_s._filter_function_with_sorted_filters([], {}))
)
def test_uses_sorted_filter_list(self):
matcher = _s._filter_function_with_sorted_filters(
[HighPriorityFilter, LowPriorityFilter],
{}
)
self.assertThat(
matcher.args[0], Equals([HighPriorityFilter, LowPriorityFilter])
)
class ConnectionHasNameTests(TestCase):
"""Tests specific to the ConnectionHasName filter."""
def test_raises_KeyError_when_missing_connection_name_param(self):
dbus_tuple = ("bus", "name")
self.assertThat(
lambda: _s.ConnectionHasName.matches(dbus_tuple, {}),
raises(KeyError('connection_name'))
)
def test_returns_True_when_connection_name_matches(self):
dbus_tuple = ("bus", "connection_name")
search_params = dict(connection_name="connection_name")
self.assertTrue(
_s.ConnectionHasName.matches(dbus_tuple, search_params)
)
def test_returns_False_when_connection_name_matches(self):
dbus_tuple = ("bus", "connection_name")
search_params = dict(connection_name="not_connection_name")
self.assertFalse(
_s.ConnectionHasName.matches(dbus_tuple, search_params)
)
class ConnectionIsNotOurConnectionTests(TestCase):
@patch.object(_s, '_get_bus_connections_pid')
def test_doesnt_raise_exception_with_no_parameters(self, get_bus_pid):
dbus_tuple = ("bus", "name")
_s.ConnectionIsNotOurConnection.matches(dbus_tuple, {})
@patch.object(_s, '_get_bus_connections_pid', return_value=0)
def test_returns_True_when_pid_isnt_our_connection(self, get_bus_pid):
dbus_tuple = ("bus", "name")
self.assertTrue(
_s.ConnectionIsNotOurConnection.matches(
dbus_tuple,
{}
)
)
@patch.object(_s, '_get_bus_connections_pid', return_value=os.getpid())
def test_returns_False_when_pid_is_our_connection(self, get_bus_pid):
dbus_tuple = ("bus", "name")
self.assertFalse(
_s.ConnectionIsNotOurConnection.matches(
dbus_tuple,
{}
)
)
@patch.object(_s, '_get_bus_connections_pid', side_effect=DBusException())
def test_returns_False_exception_raised(self, get_bus_pid):
dbus_tuple = ("bus", "name")
self.assertFalse(
_s.ConnectionIsNotOurConnection.matches(
dbus_tuple,
{}
)
)
class ConnectionHasPathWithAPInterfaceTests(TestCase):
"""Tests specific to the ConnectionHasPathWithAPInterface filter."""
def test_raises_KeyError_when_missing_object_path_param(self):
dbus_tuple = ("bus", "name")
self.assertThat(
lambda: _s.ConnectionHasPathWithAPInterface.matches(
dbus_tuple,
{}
),
raises(KeyError('object_path'))
)
@patch.object(_s.dbus, "Interface")
def test_returns_True_on_success(self, Interface):
bus_obj = Mock()
connection_name = "name"
path = "path"
dbus_tuple = (bus_obj, connection_name)
self.assertTrue(
_s.ConnectionHasPathWithAPInterface.matches(
dbus_tuple,
dict(object_path=path)
)
)
bus_obj.get_object.assert_called_once_with("name", path)
@patch.object(_s.dbus, "Interface")
def test_returns_False_on_dbus_exception(self, Interface):
bus_obj = Mock()
connection_name = "name"
path = "path"
dbus_tuple = (bus_obj, connection_name)
Interface.side_effect = DBusException()
self.assertFalse(
_s.ConnectionHasPathWithAPInterface.matches(
dbus_tuple,
dict(object_path=path)
)
)
bus_obj.get_object.assert_called_once_with("name", path)
class ConnectionHasPidTests(TestCase):
"""Tests specific to the ConnectionHasPid filter."""
def test_raises_when_missing_param(self):
self.assertThat(
lambda: _s.ConnectionHasPid.matches(None, {}),
raises(KeyError('pid'))
)
def test_returns_True_when_bus_pid_matches(self):
connection_pid = self.getUniqueInteger()
dbus_tuple = ("bus", "org.freedesktop.DBus")
params = dict(pid=connection_pid)
with patch.object(
_s,
'_get_bus_connections_pid',
return_value=connection_pid
):
self.assertTrue(
_s.ConnectionHasPid.matches(dbus_tuple, params)
)
def test_returns_False_with_DBusException(self):
connection_pid = self.getUniqueInteger()
dbus_tuple = ("bus", "org.freedesktop.DBus")
params = dict(pid=connection_pid)
with patch.object(
_s,
'_get_bus_connections_pid',
side_effect=DBusException()
):
self.assertFalse(
_s.ConnectionHasPid.matches(dbus_tuple, params)
)
class ConnectionIsNotOrgFreedesktopDBusTests(TestCase):
"""Tests specific to the ConnectionIsNotOrgFreedesktopDBus filter."""
def test_returns_True_when_connection_name_isnt_DBus(self):
dbus_tuple = ("bus", "connection.name")
self.assertTrue(
_s.ConnectionIsNotOrgFreedesktopDBus.matches(dbus_tuple, {})
)
def test_returns_False_when_connection_name_is_DBus(self):
dbus_tuple = ("bus", "org.freedesktop.DBus")
self.assertFalse(
_s.ConnectionIsNotOrgFreedesktopDBus.matches(dbus_tuple, {})
)
class ConnectionHasAppNameTests(TestCase):
"""Tests specific to the ConnectionHasAppName filter."""
def test_raises_when_missing_app_name_param(self):
self.assertThat(
lambda: _s.ConnectionHasAppName.matches(None, {}),
raises(KeyError('application_name'))
)
@patch.object(_s.ConnectionHasAppName, '_get_application_name')
def test_uses_default_object_name_when_not_provided(self, app_name):
dbus_tuple = ("bus", "connection_name")
search_params = dict(application_name="application_name")
_s.ConnectionHasAppName.matches(dbus_tuple, search_params)
app_name.assert_called_once_with(
"bus",
"connection_name",
AUTOPILOT_PATH
)
@patch.object(_s.ConnectionHasAppName, '_get_application_name')
def test_uses_provided_object_name(self, app_name):
object_name = self.getUniqueString()
dbus_tuple = ("bus", "connection_name")
search_params = dict(
application_name="application_name",
object_path=object_name
)
_s.ConnectionHasAppName.matches(dbus_tuple, search_params)
app_name.assert_called_once_with(
"bus",
"connection_name",
object_name
)
def get_mock_dbus_address_with_app_name(slf, app_name):
mock_dbus_address = Mock()
mock_dbus_address.introspection_iface.GetState.return_value = (
('/' + app_name, {}),
)
return mock_dbus_address
def test_get_application_name_returns_application_name(self):
with patch.object(_s, '_get_dbus_address_object') as gdbao:
gdbao.return_value = self.get_mock_dbus_address_with_app_name(
"SomeAppName"
)
self.assertEqual(
_s.ConnectionHasAppName._get_application_name("", "", ""),
"SomeAppName"
)
class FilterHelpersTests(TestCase):
"""Tests for helpers around the Filters themselves."""
def test_param_to_filter_includes_all(self):
"""Ensure all filters are used in the matcher when requested."""
search_parameters = {
f: True
for f
in _s._filter_lookup_map().keys()
}
self.assertThat(
_s._filters_from_search_parameters(search_parameters),
ListContainsOnly(_s._filter_lookup_map().values())
)
def test_filter_priority_order_is_correct(self):
"""Ensure all filters are used in the matcher when requested."""
search_parameters = {
f: True
for f
in _s._filter_lookup_map().keys()
}
search_filters = _s._filters_from_search_parameters(search_parameters)
mandatory_filters = _s._mandatory_filters()
sorted_filters = _s._priority_sort_filters(
search_filters + mandatory_filters
)
expected_filter_order = [
_s.ConnectionIsNotOrgFreedesktopDBus,
_s.ConnectionIsNotOurConnection,
_s.ConnectionHasName,
_s.ConnectionHasPid,
_s.ConnectionHasPathWithAPInterface,
_s.ConnectionHasAppName,
]
self.assertThat(
sorted_filters,
MatchesListwise(list(map(Equals, expected_filter_order)))
)
class ProcessAndPidErrorCheckingTests(TestCase):
def test_raises_ProcessSearchError_when_process_is_not_running(self):
with patch.object(_s, '_pid_is_running') as pir:
pir.return_value = False
self.assertThat(
lambda: _s._check_process_and_pid_details(pid=123),
raises(ProcessSearchError("PID 123 could not be found"))
)
def test_raises_RuntimeError_when_pid_and_process_disagree(self):
mock_process = Mock()
mock_process.pid = 1
self.assertThat(
lambda: _s._check_process_and_pid_details(mock_process, 2),
raises(RuntimeError("Supplied PID and process.pid do not match."))
)
def test_returns_pid_when_specified(self):
expected = self.getUniqueInteger()
with patch.object(_s, '_pid_is_running') as pir:
pir.return_value = True
observed = _s._check_process_and_pid_details(pid=expected)
self.assertEqual(expected, observed)
def test_returns_process_pid_attr_when_specified(self):
fake_process = Mock()
fake_process.pid = self.getUniqueInteger()
with patch.object(_s, '_pid_is_running') as pir:
pir.return_value = True
observed = _s._check_process_and_pid_details(fake_process)
self.assertEqual(fake_process.pid, observed)
def test_returns_None_when_neither_parameters_present(self):
self.assertEqual(
None,
_s._check_process_and_pid_details()
)
def test_returns_pid_when_both_specified(self):
fake_process = Mock()
fake_process.pid = self.getUniqueInteger()
with patch.object(_s, '_pid_is_running') as pir:
pir.return_value = True
observed = _s._check_process_and_pid_details(
fake_process,
fake_process.pid
)
self.assertEqual(fake_process.pid, observed)
class FilterParentPidsFromChildrenTests(TestCase):
def test_returns_all_connections_with_no_parent_match(self):
search_pid = 123
connections = ['1:0', '1:3']
dbus_bus = Mock()
pid_mapping = Mock(side_effect=[111, 222])
self.assertThat(
_s._filter_parent_pids_from_children(
search_pid,
connections,
dbus_bus,
_connection_pid_fn=pid_mapping
),
Equals(connections)
)
def test_calls_connection_pid_fn_in_order(self):
search_pid = 123
connections = ['1:3', '1:0']
dbus_bus = Mock()
pid_mapping = Mock(side_effect=[222, 123])
_s._filter_parent_pids_from_children(
search_pid,
connections,
dbus_bus,
_connection_pid_fn=pid_mapping
)
self.assertTrue(
pid_mapping.call_args_list == [
call('1:3', dbus_bus),
call('1:0', dbus_bus)
]
)
def test_returns_just_parent_connection_with_pid_match(self):
search_pid = 123
# connection '1.0' has pid 123.
connections = ['1:3', '1:0']
dbus_bus = Mock()
# Mapping returns parent pid on second call.
pid_mapping = Mock(side_effect=[222, 123])
self.assertThat(
_s._filter_parent_pids_from_children(
search_pid,
connections,
dbus_bus,
_connection_pid_fn=pid_mapping
),
Equals(['1:0'])
)
self.assertTrue(
pid_mapping.call_args_list == [
call('1:3', dbus_bus),
call('1:0', dbus_bus)
]
)
def test_returns_all_connections_with_no_pids_returned_in_search(self):
search_pid = 123
connections = ['1:3', '1:0']
dbus_bus = Mock()
pid_mapping = Mock(side_effect=[None, None])
self.assertThat(
_s._filter_parent_pids_from_children(
search_pid,
connections,
dbus_bus,
_connection_pid_fn=pid_mapping
),
Equals(connections)
)
class ProcessSearchErrorStringRepTests(TestCase):
"""Various tests for the _get_search_criteria_string_representation
function.
"""
def test_get_string_rep_defaults_to_empty_string(self):
observed = _s._get_search_criteria_string_representation()
self.assertEqual("", observed)
def test_pid(self):
self.assertEqual(
'pid = 123',
_s._get_search_criteria_string_representation(pid=123)
)
def test_dbus_bus(self):
self.assertEqual(
"dbus bus = 'foo'",
_s._get_search_criteria_string_representation(dbus_bus='foo')
)
def test_connection_name(self):
self.assertEqual(
"connection name = 'foo'",
_s._get_search_criteria_string_representation(
connection_name='foo'
)
)
def test_object_path(self):
self.assertEqual(
"object path = 'foo'",
_s._get_search_criteria_string_representation(object_path='foo')
)
def test_application_name(self):
self.assertEqual(
"application name = 'foo'",
_s._get_search_criteria_string_representation(
application_name='foo'
)
)
def test_process_object(self):
class FakeProcess(object):
def __repr__(self):
return 'foo'
process = FakeProcess()
self.assertEqual(
"process object = 'foo'",
_s._get_search_criteria_string_representation(process=process)
)
def test_all_parameters_combined(self):
class FakeProcess(object):
def __repr__(self):
return 'foo'
process = FakeProcess()
observed = _s._get_search_criteria_string_representation(
pid=123,
dbus_bus='session_bus',
connection_name='com.Canonical.Unity',
object_path='/com/Canonical/Autopilot',
application_name='MyApp',
process=process
)
expected_strings = [
"pid = 123",
"dbus bus = 'session_bus'",
"connection name = 'com.Canonical.Unity'",
"object path = '/com/Canonical/Autopilot'",
"application name = 'MyApp'",
"process object = 'foo'",
]
self.assertThat(
observed,
MatchesAll(*map(Contains, expected_strings))
)
class ProxyObjectTests(TestCase):
def test_raise_if_not_single_result_raises_on_0_connections(self):
criteria_string = self.getUniqueString()
self.assertThat(
lambda: _s._raise_if_not_single_result(
[],
criteria_string
),
raises(
ProcessSearchError(
"Search criteria (%s) returned no results"
% criteria_string
)
)
)
def test_raise_if_not_single_result_raises_on_many_connections(self):
criteria_string = self.getUniqueString()
self.assertThat(
lambda: _s._raise_if_not_single_result(
[1, 2, 3, 4, 5],
criteria_string
),
raises(
RuntimeError(
"Search criteria (%s) returned multiple results"
% criteria_string
)
)
)
def test_raise_if_not_single_result_doesnt_raise_with_single_result(self):
_s._raise_if_not_single_result([1], "")
class FMCTest:
def list_names(self):
return ["conn1"]
def test_find_matching_connections_calls_connection_matcher(self):
bus = ProxyObjectTests.FMCTest()
connection_matcher = Mock(return_value=False)
with sleep.mocked():
_s._find_matching_connections(bus, connection_matcher)
connection_matcher.assert_called_with((bus, "conn1"))
def test_find_matching_connections_attempts_multiple_times(self):
bus = ProxyObjectTests.FMCTest()
connection_matcher = Mock(return_value=False)
with sleep.mocked():
_s._find_matching_connections(bus, connection_matcher)
connection_matcher.assert_called_with((bus, "conn1"))
self.assertEqual(connection_matcher.call_count, 11)
def test_find_matching_connections_dedupes_results_on_pid(self):
bus = ProxyObjectTests.FMCTest()
with patch.object(_s, '_dedupe_connections_on_pid') as dedupe:
with sleep.mocked():
_s._find_matching_connections(bus, lambda *args: True)
dedupe.assert_called_once_with(["conn1"], bus)
class ActualBaseClassTests(TestCase):
def test_dont_raise_passed_base_when_is_only_base(self):
class ActualBase(CustomEmulatorBase):
pass
try:
_s._raise_if_base_class_not_actually_base(ActualBase)
except ValueError:
self.fail('Unexpected ValueError exception')
def test_raises_if_passed_incorrect_base_class(self):
class ActualBase(CustomEmulatorBase):
pass
class InheritedCPO(ActualBase):
pass
self.assertRaises(
ValueError,
_s._raise_if_base_class_not_actually_base,
InheritedCPO
)
def test_raises_parent_with_simple_non_ap_multi_inheritance(self):
"""When mixing in non-customproxy classes must return the base."""
class ActualBase(CustomEmulatorBase):
pass
class InheritedCPO(ActualBase):
pass
class TrickyOne(object):
pass
class FinalForm(InheritedCPO, TrickyOne):
pass
self.assertRaises(
ValueError,
_s._raise_if_base_class_not_actually_base,
FinalForm
)
def test_raises_parent_with_non_ap_multi_inheritance(self):
class ActualBase(CustomEmulatorBase):
pass
class InheritedCPO(ActualBase):
pass
class TrickyOne(object):
pass
class FinalForm(TrickyOne, InheritedCPO):
pass
self.assertRaises(
ValueError,
_s._raise_if_base_class_not_actually_base,
FinalForm
)
def test_dont_raise_when_using_default_emulator_base(self):
# _make_proxy_object potentially creates a default base.
DefaultBase = _s._make_default_emulator_base()
try:
_s._raise_if_base_class_not_actually_base(DefaultBase)
except ValueError:
self.fail('Unexpected ValueError exception')
def test_exception_message_contains_useful_information(self):
class ActualBase(CustomEmulatorBase):
pass
class InheritedCPO(ActualBase):
pass
try:
_s._raise_if_base_class_not_actually_base(InheritedCPO)
except ValueError as err:
self.assertEqual(
str(err),
_s.WRONG_CPO_CLASS_MSG.format(
passed=InheritedCPO,
actual=ActualBase
)
)
./autopilot/tests/unit/test_pick_backend.py 0000644 0000041 0000041 00000011650 13061552435 021421 0 ustar www-data www-data # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Autopilot Functional Test Tool
# Copyright (C) 2012-2013 Canonical
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
from collections import OrderedDict
from testtools import TestCase
from testtools.matchers import raises, Equals, IsInstance
from textwrap import dedent
from autopilot.exceptions import BackendException
from autopilot.utilities import _pick_backend
class PickBackendTests(TestCase):
def test_raises_runtime_error_on_empty_backends(self):
"""Must raise a RuntimeError when we pass no backends."""
fn = lambda: _pick_backend({}, '')
self.assertThat(
fn, raises(RuntimeError("Unable to instantiate any backends\n")))
def test_single_backend(self):
"""Must return a backend when called with a single backend."""
class Backend(object):
pass
_create_backend = lambda: Backend()
backend = _pick_backend(dict(foo=_create_backend), '')
self.assertThat(backend, IsInstance(Backend))
def test_first_backend(self):
"""Must return the first backend when called with a two backends."""
class Backend1(object):
pass
class Backend2(object):
pass
backend_dict = OrderedDict()
backend_dict['be1'] = lambda: Backend1()
backend_dict['be2'] = lambda: Backend2()
backend = _pick_backend(backend_dict, '')
self.assertThat(backend, IsInstance(Backend1))
def test_preferred_backend(self):
"""Must return the preferred backend when called with a two
backends."""
class Backend1(object):
pass
class Backend2(object):
pass
backend_dict = OrderedDict()
backend_dict['be1'] = lambda: Backend1()
backend_dict['be2'] = lambda: Backend2()
backend = _pick_backend(backend_dict, 'be2')
self.assertThat(backend, IsInstance(Backend2))
def test_raises_backend_exception_on_preferred_backend(self):
"""Must raise a BackendException when the preferred backendcannot be
created."""
class Backend1(object):
pass
class Backend2(object):
def __init__(self):
raise ValueError("Foo")
backend_dict = OrderedDict()
backend_dict['be1'] = lambda: Backend1()
backend_dict['be2'] = lambda: Backend2()
fn = lambda: _pick_backend(backend_dict, 'be2')
self.assertThat(fn, raises(BackendException))
def test_raises_RuntimeError_on_invalid_preferred_backend(self):
"""Must raise RuntimeError when we pass a backend that's not there"""
class Backend(object):
pass
_create_backend = lambda: Backend()
fn = lambda: _pick_backend(dict(foo=_create_backend), 'bar')
self.assertThat(
fn,
raises(RuntimeError("Unknown backend 'bar'"))
)
def test_backend_exception_wraps_original_exception(self):
"""Raised backend Exception must wrap exception from backend."""
class Backend1(object):
pass
class Backend2(object):
def __init__(self):
raise ValueError("Foo")
backend_dict = OrderedDict()
backend_dict['be1'] = lambda: Backend1()
backend_dict['be2'] = lambda: Backend2()
raised = False
try:
_pick_backend(backend_dict, 'be2')
except BackendException as e:
raised = True
self.assertTrue(hasattr(e, 'original_exception'))
self.assertThat(e.original_exception, IsInstance(ValueError))
self.assertThat(str(e.original_exception), Equals("Foo"))
self.assertTrue(raised)
def test_failure_of_all_backends(self):
"""When we cannot create any backends, must raise RuntimeError."""
class BadBackend(object):
def __init__(self):
raise ValueError("Foo")
backend_dict = OrderedDict()
backend_dict['be1'] = lambda: BadBackend()
backend_dict['be2'] = lambda: BadBackend()
fn = lambda: _pick_backend(backend_dict, '')
expected_exception = RuntimeError(dedent("""\
Unable to instantiate any backends
be1: ValueError('Foo',)
be2: ValueError('Foo',)"""))
self.assertThat(fn, raises(expected_exception))
./autopilot/tests/unit/test_introspection_backends.py 0000644 0000041 0000041 00000027773 13061552435 023573 0 ustar www-data www-data # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Autopilot Functional Test Tool
# Copyright (C) 2012-2013 Canonical
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
from dbus import String, DBusException
from unittest.mock import patch, MagicMock, Mock
from testtools import TestCase
from testtools.matchers import Equals, Not, NotEquals, IsInstance
from autopilot.introspection import (
_xpathselect as xpathselect,
backends,
dbus,
)
class DBusAddressTests(TestCase):
def test_can_construct(self):
fake_bus = object()
backends.DBusAddress(fake_bus, "conn", "path")
def test_can_store_address_in_dictionary(self):
fake_bus = object()
backends.DBusAddress(fake_bus, "conn", "path")
dict(addr=object())
def test_equality_operator(self):
fake_bus = object()
addr1 = backends.DBusAddress(fake_bus, "conn", "path")
self.assertThat(
addr1,
Equals(backends.DBusAddress(fake_bus, "conn", "path"))
)
self.assertThat(
addr1,
NotEquals(backends.DBusAddress(fake_bus, "conn", "new_path"))
)
self.assertThat(
addr1,
NotEquals(backends.DBusAddress(fake_bus, "conn2", "path"))
)
self.assertThat(
addr1,
NotEquals(backends.DBusAddress(object(), "conn", "path"))
)
def test_inequality_operator(self):
fake_bus = object()
addr1 = backends.DBusAddress(fake_bus, "conn", "path")
self.assertThat(
addr1,
Not(NotEquals(backends.DBusAddress(fake_bus, "conn", "path")))
)
self.assertThat(
addr1,
NotEquals(backends.DBusAddress(fake_bus, "conn", "new_path"))
)
self.assertThat(
addr1,
NotEquals(backends.DBusAddress(fake_bus, "conn2", "path"))
)
self.assertThat(
addr1,
NotEquals(backends.DBusAddress(object(), "conn", "path"))
)
def test_session_bus_construction(self):
connection = self.getUniqueString()
object_path = self.getUniqueString()
with patch.object(backends, 'get_session_bus') as patch_sb:
addr = backends.DBusAddress.SessionBus(connection, object_path)
self.assertThat(
addr._addr_tuple,
Equals(
backends.DBusAddress.AddrTuple(
patch_sb.return_value,
connection,
object_path
)
)
)
def test_system_bus_construction(self):
connection = self.getUniqueString()
object_path = self.getUniqueString()
with patch.object(backends, 'get_system_bus') as patch_sb:
addr = backends.DBusAddress.SystemBus(connection, object_path)
self.assertThat(
addr._addr_tuple,
Equals(
backends.DBusAddress.AddrTuple(
patch_sb.return_value,
connection,
object_path
)
)
)
def test_custom_bus_construction(self):
connection = self.getUniqueString()
object_path = self.getUniqueString()
bus_path = self.getUniqueString()
with patch.object(backends, 'get_custom_bus') as patch_cb:
addr = backends.DBusAddress.CustomBus(
bus_path,
connection,
object_path
)
self.assertThat(
addr._addr_tuple,
Equals(
backends.DBusAddress.AddrTuple(
patch_cb.return_value,
connection,
object_path
)
)
)
patch_cb.assert_called_once_with(bus_path)
class ClientSideFilteringTests(TestCase):
def get_empty_fake_object(self):
return type(
'EmptyObject',
(object,),
{'no_automatic_refreshing': MagicMock()}
)
def test_object_passes_filters_disables_refreshing(self):
obj = self.get_empty_fake_object()
backends._object_passes_filters(obj)
obj.no_automatic_refreshing.assert_called_once_with()
self.assertTrue(
obj.no_automatic_refreshing.return_value.__enter__.called
)
def test_object_passes_filters_works_with_no_filters(self):
obj = self.get_empty_fake_object()
self.assertTrue(backends._object_passes_filters(obj))
def test_object_passes_filters_fails_when_attr_missing(self):
obj = self.get_empty_fake_object()
self.assertFalse(backends._object_passes_filters(obj, foo=123))
def test_object_passes_filters_fails_when_attr_has_wrong_value(self):
obj = self.get_empty_fake_object()
obj.foo = 456
self.assertFalse(backends._object_passes_filters(obj, foo=123))
def test_object_passes_filters_succeeds_with_one_correct_parameter(self):
obj = self.get_empty_fake_object()
obj.foo = 123
self.assertTrue(backends._object_passes_filters(obj, foo=123))
class BackendTests(TestCase):
@patch('autopilot.introspection.backends._logger')
def test_large_query_returns_log_warnings(self, mock_logger):
"""Queries that return large numbers of items must cause a log warning.
'large' is defined as more than 15.
"""
query = xpathselect.Query.root('foo')
fake_dbus_address = Mock()
fake_dbus_address.introspection_iface.GetState.return_value = \
[(b'/root/path', {}) for i in range(16)]
backend = backends.Backend(fake_dbus_address)
backend.execute_query_get_data(
query,
)
mock_logger.warning.assert_called_once_with(
"Your query '%r' returned a lot of data (%d items). This "
"is likely to be slow. You may want to consider optimising"
" your query to return fewer items.",
query,
16)
@patch('autopilot.introspection.backends._logger')
def test_small_query_returns_dont_log_warnings(self, mock_logger):
"""Queries that return small numbers of items must not log a warning.
'small' is defined as 15 or fewer.
"""
query = xpathselect.Query.root('foo')
fake_dbus_address = Mock()
fake_dbus_address.introspection_iface.GetState.return_value = \
[(b'/root/path', {}) for i in range(15)]
backend = backends.Backend(fake_dbus_address)
backend.execute_query_get_data(
query,
)
self.assertThat(mock_logger.warning.called, Equals(False))
@patch.object(backends, 'make_introspection_object', return_value=None)
def test_proxy_instances_returns_list(self, mio):
query = xpathselect.Query.root('foo')
fake_dbus_address = Mock()
fake_dbus_address.introspection_iface.GetState.return_value = [
(b'/root/path', {}) for i in range(1)
]
backend = backends.Backend(fake_dbus_address)
self.assertThat(
backend.execute_query_get_proxy_instances(query, 0),
Equals([None])
)
@patch.object(backends, 'make_introspection_object', return_value=None)
def test_proxy_instances_with_clientside_filtering_returns_list(self, mio):
query = xpathselect.Query.root('foo')
query.needs_client_side_filtering = Mock(return_value=True)
fake_dbus_address = Mock()
fake_dbus_address.introspection_iface.GetState.return_value = [
(b'/root/path', {}) for i in range(1)
]
backend = backends.Backend(fake_dbus_address)
with patch.object(
backends, '_object_passes_filters', return_value=True):
self.assertThat(
backend.execute_query_get_proxy_instances(query, 0),
Equals([None])
)
def test_proxy_instance_catches_unknown_service_exception(self):
query = xpathselect.Query.root('foo')
e = DBusException(
name='org.freedesktop.DBus.Error.ServiceUnknown'
)
fake_dbus_address = Mock()
fake_dbus_address.introspection_iface.GetState.side_effect = e
backend = backends.Backend(fake_dbus_address)
self.assertRaises(RuntimeError, backend.execute_query_get_data, query)
def test_unknown_service_exception_gives_correct_msg(self):
query = xpathselect.Query.root('foo')
e = DBusException(
name='org.freedesktop.DBus.Error.ServiceUnknown'
)
fake_dbus_address = Mock()
fake_dbus_address.introspection_iface.GetState.side_effect = e
backend = backends.Backend(fake_dbus_address)
try:
backend.execute_query_get_data(query)
except RuntimeError as e:
msg = ("Lost dbus backend communication. It appears the "
"application under test exited before the test "
"finished!")
self.assertEqual(str(e), msg)
def test_proxy_instance_raises_uncaught_dbus_exceptions(self):
query = xpathselect.Query.root('foo')
e = DBusException()
fake_dbus_address = Mock()
fake_dbus_address.introspection_iface.GetState.side_effect = e
backend = backends.Backend(fake_dbus_address)
self.assertRaises(DBusException, backend.execute_query_get_data, query)
def test_proxy_instance_raises_uncaught_exceptions(self):
query = xpathselect.Query.root('foo')
e = Exception()
fake_dbus_address = Mock()
fake_dbus_address.introspection_iface.GetState.side_effect = e
backend = backends.Backend(fake_dbus_address)
self.assertRaises(Exception, backend.execute_query_get_data, query)
class MakeIntrospectionObjectTests(TestCase):
"""Test selection of custom proxy object class."""
class DefaultSelector(dbus.CustomEmulatorBase):
pass
class AlwaysSelected(dbus.CustomEmulatorBase):
@classmethod
def validate_dbus_object(cls, path, state):
"""Validate always.
:returns: True
"""
return True
class NeverSelected(dbus.CustomEmulatorBase):
@classmethod
def validate_dbus_object(cls, path, state):
"""Validate never.
:returns: False
"""
return False
def test_class_has_validation_method(self):
"""Verify that a class has a validation method by default."""
self.assertTrue(callable(self.DefaultSelector.validate_dbus_object))
@patch.object(backends, '_get_proxy_object_class')
def test_make_introspection_object(self, gpoc):
"""Verify that make_introspection_object makes the right call."""
gpoc.return_value = self.DefaultSelector
fake_id = Mock()
new_fake = backends.make_introspection_object(
(String('/Object'), {'id': [0, 42]}),
None,
fake_id,
)
self.assertThat(new_fake, IsInstance(self.DefaultSelector))
gpoc.assert_called_once_with(
fake_id,
b'/Object',
{'id': [0, 42]}
)
def test_validate_dbus_object_matches_on_class_name(self):
"""Validate_dbus_object must match class name."""
selected = self.DefaultSelector.validate_dbus_object(
'/DefaultSelector', {})
self.assertTrue(selected)
./autopilot/tests/unit/test_exceptions.py 0000644 0000041 0000041 00000006142 13061552435 021205 0 ustar www-data www-data # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Autopilot Functional Test Tool
# Copyright (C) 2013 Canonical
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
from testtools import TestCase
from testtools.matchers import raises, Equals, EndsWith
from autopilot.exceptions import StateNotFoundError
class StateNotFoundTests(TestCase):
def test_requires_class_name_to_construct(self):
"""You must pass a class name in to the StateNotFoundError exception
class initialiser in order to construct it.
"""
self.assertThat(
StateNotFoundError,
raises(ValueError("Must specify either class name or filters."))
)
def test_can_be_constructed_with_class_name_only(self):
"""Must be able to construct error class with a class name only."""
err = StateNotFoundError("MyClass")
self.assertThat(
str(err),
Equals("Object not found with name 'MyClass'.\n\n{}".format(
StateNotFoundError._troubleshoot_url_message
))
)
def test_can_be_constructed_with_filters_only(self):
"""Must be able to construct exception with filters only."""
err = StateNotFoundError(foo="bar")
self.assertThat(
str(err),
Equals(
"Object not found with properties {}."
"\n\n{}".format(
"{'foo': 'bar'}",
StateNotFoundError._troubleshoot_url_message
))
)
def test_can_be_constructed_with_class_name_and_filters(self):
"""Must be able to construct with both class name and filters."""
err = StateNotFoundError('MyClass', foo="bar")
self.assertThat(
str(err),
Equals("Object not found with name 'MyClass'"
" and properties {}.\n\n{}".format(
"{'foo': 'bar'}",
StateNotFoundError._troubleshoot_url_message
))
)
def test_StateNotFoundError_endswith_troubleshoot_url_message_text(self):
"""The assertion raised must end with a link to troubleshooting url."""
err = StateNotFoundError('MyClass', foo="bar")
self.assertThat(
str(err),
EndsWith(
'Tips on minimizing the occurrence of this failure '
'are available here: '
'https://developer.ubuntu.com/api/autopilot/python/1.6.0/'
'faq-troubleshooting/'
)
)
./autopilot/tests/unit/test_debug.py 0000644 0000041 0000041 00000015415 13061552435 020115 0 ustar www-data www-data # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Autopilot Functional Test Tool
# Copyright (C) 2014 Canonical
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
from autopilot import _debug as d
from unittest.mock import Mock, patch
from tempfile import NamedTemporaryFile
from testtools import TestCase
from testtools.matchers import (
Equals,
Not,
Raises,
)
class CaseAddDetailToNormalAddDetailDecoratorTests(TestCase):
def test_sets_decorated_ivar(self):
fake_detailed = Mock()
decorated = d.CaseAddDetailToNormalAddDetailDecorator(fake_detailed)
self.assertThat(decorated.decorated, Equals(fake_detailed))
def test_addDetail_calls_caseAddDetail(self):
fake_detailed = Mock()
decorated = d.CaseAddDetailToNormalAddDetailDecorator(fake_detailed)
content_name = self.getUniqueString()
content_object = object()
decorated.addDetail(content_name, content_object)
fake_detailed.caseAddDetail.assert_called_once_with(
content_name,
content_object
)
def test_all_other_attrs_are_passed_through(self):
fake_detailed = Mock()
decorated = d.CaseAddDetailToNormalAddDetailDecorator(fake_detailed)
decorated.some_method()
fake_detailed.some_method.assert_called_once_with()
def test_repr(self):
fake_detailed = Mock()
decorated = d.CaseAddDetailToNormalAddDetailDecorator(fake_detailed)
self.assertThat(
repr(decorated),
Equals(
''.format(
repr(fake_detailed)
)
)
)
class DebugProfileTests(TestCase):
def setUp(self):
super(DebugProfileTests, self).setUp()
self.fake_caseAddDetail = Mock()
def test_can_construct_debug_profile(self):
d.DebugProfile(self.fake_caseAddDetail)
def test_debug_profile_sets_caseAddDetail(self):
profile = d.DebugProfile(self.fake_caseAddDetail)
self.assertThat(
profile.caseAddDetail,
Equals(self.fake_caseAddDetail)
)
def test_default_debug_profile_is_normal(self):
self.assertThat(
d.get_default_debug_profile().name,
Equals("normal")
)
def test_normal_profile_name(self):
self.assertThat(
d.NormalDebugProfile(self.fake_caseAddDetail).name,
Equals("normal")
)
def test_verbose_profile_name(self):
self.assertThat(
d.VerboseDebugProfile(self.fake_caseAddDetail).name,
Equals("verbose")
)
def test_all_profiles(self):
self.assertThat(
d.get_all_debug_profiles(),
Equals({d.VerboseDebugProfile, d.NormalDebugProfile})
)
def test_debug_profile_uses_fixtures_in_setup(self):
class DebugObjectDouble(d.DebugObject):
init_called = False
setup_called = False
def __init__(self, *args, **kwargs):
super(DebugObjectDouble, self).__init__(*args, **kwargs)
DebugObjectDouble.init_called = True
def setUp(self, *args, **kwargs):
super(DebugObjectDouble, self).setUp(*args, **kwargs)
DebugObjectDouble.setup_called = True
class TestDebugProfile(d.DebugProfile):
name = "test"
def __init__(self, caseAddDetail):
super(TestDebugProfile, self).__init__(
caseAddDetail,
[DebugObjectDouble]
)
profile = TestDebugProfile(Mock())
profile.setUp()
self.assertTrue(DebugObjectDouble.init_called)
self.assertTrue(DebugObjectDouble.setup_called)
class LogFollowerTests(TestCase):
def setUp(self):
super(LogFollowerTests, self).setUp()
self.fake_caseAddDetail = Mock()
def test_can_construct_log_debug_object(self):
path = self.getUniqueString()
log_debug_object = d.LogFileDebugObject(
self.fake_caseAddDetail,
path
)
self.assertThat(log_debug_object.log_path, Equals(path))
def test_calls_follow_file_with_correct_parameters(self):
path = self.getUniqueString()
with patch.object(d, 'follow_file') as patched_follow_file:
log_debug_object = d.LogFileDebugObject(
self.fake_caseAddDetail,
path
)
log_debug_object.setUp()
self.assertThat(patched_follow_file.call_count, Equals(1))
args, _ = patched_follow_file.call_args
self.assertThat(args[0], Equals(path))
self.assertTrue(callable(getattr(args[1], 'addDetail', None)))
self.assertTrue(callable(getattr(args[1], 'addCleanup', None)))
def test_reads_new_file_lines(self):
open_args = dict(buffering=0)
with NamedTemporaryFile(**open_args) as temp_file:
temp_file.write("Hello\n".encode())
log_debug_object = d.LogFileDebugObject(
self.fake_caseAddDetail,
temp_file.name
)
log_debug_object.setUp()
temp_file.write("World\n".encode())
log_debug_object.cleanUp()
self.assertThat(self.fake_caseAddDetail.call_count, Equals(1))
args, _ = self.fake_caseAddDetail.call_args
self.assertThat(args[0], Equals(temp_file.name))
self.assertThat(args[1].as_text(), Equals("World\n"))
def test_can_follow_file_with_binary_content(self):
open_args = dict(buffering=0)
with NamedTemporaryFile(**open_args) as temp_file:
log_debug_object = d.LogFileDebugObject(
self.fake_caseAddDetail,
temp_file.name
)
log_debug_object.setUp()
temp_file.write("Hello\x88World".encode())
log_debug_object.cleanUp()
args, _ = self.fake_caseAddDetail.call_args
self.assertThat(args[1].as_text, Not(Raises()))
def test_can_create_syslog_follower(self):
debug_obj = d.SyslogDebugObject(Mock())
self.assertThat(debug_obj.log_path, Equals("/var/log/syslog"))
./autopilot/tests/unit/test_query_resolution.py 0000644 0000041 0000041 00000001413 13061552435 022450 0 ustar www-data www-data # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Autopilot Functional Test Tool
# Copyright (C) 2014 Canonical
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
./autopilot/tests/unit/test_utilities.py 0000644 0000041 0000041 00000030313 13061552435 021034 0 ustar www-data www-data # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Autopilot Functional Test Tool
# Copyright (C) 2013 Canonical
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
from unittest.mock import Mock, patch
import re
from testtools import TestCase
from testtools.content import Content
from testtools.matchers import (
Equals,
IsInstance,
LessThan,
MatchesRegex,
Not,
raises,
Raises,
)
import timeit
from autopilot.utilities import (
_raise_if_time_delta_not_sane,
_raise_on_unknown_kwargs,
_sleep_for_calculated_delta,
cached_result,
compatible_repr,
deprecated,
EventDelay,
safe_text_content,
sleep,
)
class ElapsedTimeCounter(object):
"""A simple utility to count the amount of real time that passes."""
def __enter__(self):
self._start_time = timeit.default_timer()
return self
def __exit__(self, *args):
pass
@property
def elapsed_time(self):
return timeit.default_timer() - self._start_time
class MockableSleepTests(TestCase):
def test_mocked_sleep_contextmanager(self):
with ElapsedTimeCounter() as time_counter:
with sleep.mocked():
sleep(10)
self.assertThat(time_counter.elapsed_time, LessThan(2))
def test_mocked_sleep_methods(self):
with ElapsedTimeCounter() as time_counter:
sleep.enable_mock()
self.addCleanup(sleep.disable_mock)
sleep(10)
self.assertThat(time_counter.elapsed_time, LessThan(2))
def test_total_time_slept_starts_at_zero(self):
with sleep.mocked() as sleep_counter:
self.assertThat(sleep_counter.total_time_slept(), Equals(0.0))
def test_total_time_slept_accumulates(self):
with sleep.mocked() as sleep_counter:
sleep(1)
self.assertThat(sleep_counter.total_time_slept(), Equals(1.0))
sleep(0.5)
self.assertThat(sleep_counter.total_time_slept(), Equals(1.5))
sleep(0.5)
self.assertThat(sleep_counter.total_time_slept(), Equals(2.0))
def test_unmocked_sleep_calls_real_time_sleep_function(self):
with patch('autopilot.utilities.time') as patched_time:
sleep(1.0)
patched_time.sleep.assert_called_once_with(1.0)
class EventDelayTests(TestCase):
def test_mocked_event_delayer_contextmanager(self):
event_delayer = EventDelay()
with event_delayer.mocked():
# The first call of delay() only stores the last time
# stamp, it is only the second call where the delay
# actually happens. So we call delay() twice here to
# ensure mocking is working as expected.
event_delayer.delay(duration=0)
event_delayer.delay(duration=3)
self.assertAlmostEqual(sleep.total_time_slept(), 3, places=1)
def test_last_event_start_at_zero(self):
event_delayer = EventDelay()
self.assertThat(event_delayer.last_event_time(), Equals(0.0))
def test_last_event_delay_counter_updates_on_first_call(self):
event_delayer = EventDelay()
event_delayer.delay(duration=1.0, current_time=lambda: 10)
self.assertThat(event_delayer._last_event, Equals(10.0))
def test_first_call_to_delay_causes_no_sleep(self):
event_delayer = EventDelay()
with sleep.mocked() as mocked_sleep:
event_delayer.delay(duration=0.0)
self.assertThat(mocked_sleep.total_time_slept(), Equals(0.0))
def test_second_call_to_delay_causes_sleep(self):
event_delayer = EventDelay()
with sleep.mocked() as mocked_sleep:
event_delayer.delay(duration=0, current_time=lambda: 100)
event_delayer.delay(duration=10, current_time=lambda: 105)
self.assertThat(mocked_sleep.total_time_slept(), Equals(5.0))
def test_no_delay_if_time_jumps_since_last_event(self):
event_delayer = EventDelay()
with sleep.mocked() as mocked_sleep:
event_delayer.delay(duration=2, current_time=lambda: 100)
event_delayer.delay(duration=2, current_time=lambda: 110)
self.assertThat(mocked_sleep.total_time_slept(), Equals(0.0))
def test_no_delay_if_given_delay_time_negative(self):
event_delayer = EventDelay()
with sleep.mocked() as mocked_sleep:
event_delayer.delay(duration=-2, current_time=lambda: 100)
event_delayer.delay(duration=-2, current_time=lambda: 101)
self.assertThat(mocked_sleep.total_time_slept(), Equals(0.0))
def test_sleep_delta_calculator_returns_zero_if_time_delta_negative(self):
result = _sleep_for_calculated_delta(100, 97, 2)
self.assertThat(result, Equals(0.0))
def test_sleep_delta_calculator_doesnt_sleep_if_time_delta_negative(self):
with sleep.mocked() as mocked_sleep:
_sleep_for_calculated_delta(100, 97, 2)
self.assertThat(mocked_sleep.total_time_slept(), Equals(0.0))
def test_sleep_delta_calculator_returns_zero_if_time_delta_zero(self):
result = _sleep_for_calculated_delta(100, 98, 2)
self.assertThat(result, Equals(0.0))
def test_sleep_delta_calculator_doesnt_sleep_if_time_delta_zero(self):
with sleep.mocked() as mocked_sleep:
_sleep_for_calculated_delta(100, 98, 2)
self.assertThat(mocked_sleep.total_time_slept(), Equals(0.0))
def test_sleep_delta_calculator_returns_non_zero_if_delta_not_zero(self):
with sleep.mocked():
result = _sleep_for_calculated_delta(101, 100, 2)
self.assertThat(result, Equals(1.0))
def test_sleep_delta_calc_returns_zero_if_gap_duration_negative(self):
result = _sleep_for_calculated_delta(100, 99, -2)
self.assertEquals(result, 0.0)
def test_sleep_delta_calc_raises_if_last_event_ahead_current_time(self):
self.assertRaises(
ValueError,
_sleep_for_calculated_delta,
current_time=100,
last_event_time=110,
gap_duration=2
)
def test_sleep_delta_calc_raises_if_last_event_equals_current_time(self):
self.assertRaises(
ValueError,
_sleep_for_calculated_delta,
current_time=100,
last_event_time=100,
gap_duration=2
)
def test_sleep_delta_calc_raises_if_current_time_negative(self):
self.assertRaises(
ValueError,
_sleep_for_calculated_delta,
current_time=-100,
last_event_time=10,
gap_duration=10
)
def test_time_sanity_checker_raises_if_time_smaller_than_last_event(self):
self.assertRaises(
ValueError,
_raise_if_time_delta_not_sane,
current_time=90,
last_event_time=100
)
def test_time_sanity_checker_raises_if_time_equal_last_event_time(self):
self.assertRaises(
ValueError,
_raise_if_time_delta_not_sane,
current_time=100,
last_event_time=100
)
def test_time_sanity_checker_raises_if_time_negative_last_event_not(self):
self.assertRaises(
ValueError,
_raise_if_time_delta_not_sane,
current_time=-100,
last_event_time=100
)
class CompatibleReprTests(TestCase):
def test_py3_unicode_is_untouched(self):
repr_fn = compatible_repr(lambda: "unicode")
result = repr_fn()
self.assertThat(result, IsInstance(str))
self.assertThat(result, Equals('unicode'))
def test_py3_bytes_are_returned_as_unicode(self):
repr_fn = compatible_repr(lambda: b"bytes")
result = repr_fn()
self.assertThat(result, IsInstance(str))
self.assertThat(result, Equals('bytes'))
class UnknownKWArgsTests(TestCase):
def test_raise_if_not_empty_raises_on_nonempty_dict(self):
populated_dict = dict(testing=True)
self.assertThat(
lambda: _raise_on_unknown_kwargs(populated_dict),
raises(ValueError("Unknown keyword arguments: 'testing'."))
)
def test_raise_if_not_empty_does_not_raise_on_empty(self):
empty_dict = dict()
self.assertThat(
lambda: _raise_on_unknown_kwargs(empty_dict),
Not(Raises())
)
class DeprecatedDecoratorTests(TestCase):
def test_deprecated_logs_warning(self):
@deprecated('Testing')
def not_testing():
pass
with patch('autopilot.utilities.logger') as patched_log:
not_testing()
self.assertThat(
patched_log.warning.call_args[0][0],
MatchesRegex(
"WARNING: in file \".*.py\", line \d+ in "
"test_deprecated_logs_warning\nThis "
"function is deprecated. Please use 'Testing' instead.\n",
re.DOTALL
)
)
class CachedResultTests(TestCase):
def get_wrapped_mock_pair(self):
inner = Mock()
# Mock() under python 2 does not support __name__. When we drop py2
# support we can obviously delete this hack:
return inner, cached_result(inner)
def test_can_be_used_as_decorator(self):
@cached_result
def foo():
pass
def test_adds_reset_cache_callable_to_function(self):
@cached_result
def foo():
pass
self.assertTrue(hasattr(foo, 'reset_cache'))
def test_retains_docstring(self):
@cached_result
def foo():
"""xxXX super docstring XXxx"""
pass
self.assertThat(foo.__doc__, Equals("xxXX super docstring XXxx"))
def test_call_passes_through_once(self):
inner, wrapped = self.get_wrapped_mock_pair()
wrapped()
inner.assert_called_once_with()
def test_call_passes_through_only_once(self):
inner, wrapped = self.get_wrapped_mock_pair()
wrapped()
wrapped()
inner.assert_called_once_with()
def test_first_call_returns_actual_result(self):
inner, wrapped = self.get_wrapped_mock_pair()
self.assertThat(
wrapped(),
Equals(inner.return_value)
)
def test_subsequent_calls_return_actual_results(self):
inner, wrapped = self.get_wrapped_mock_pair()
wrapped()
self.assertThat(
wrapped(),
Equals(inner.return_value)
)
def test_can_pass_hashable_arguments(self):
inner, wrapped = self.get_wrapped_mock_pair()
wrapped(1, True, 2.0, "Hello", tuple(), )
inner.assert_called_once_with(1, True, 2.0, "Hello", tuple())
def test_passing_kwargs_raises_TypeError(self):
inner, wrapped = self.get_wrapped_mock_pair()
self.assertThat(
lambda: wrapped(foo='bar'),
raises(TypeError)
)
def test_passing_unhashable_args_raises_TypeError(self):
inner, wrapped = self.get_wrapped_mock_pair()
self.assertThat(
lambda: wrapped([]),
raises(TypeError)
)
def test_resetting_cache_works(self):
inner, wrapped = self.get_wrapped_mock_pair()
wrapped()
wrapped.reset_cache()
wrapped()
self.assertThat(inner.call_count, Equals(2))
class SafeTextContentTests(TestCase):
def test_raises_TypeError_on_non_texttype(self):
self.assertThat(
lambda: safe_text_content(None),
raises(TypeError)
)
def test_returns_text_content_object(self):
example_string = self.getUniqueString()
content_obj = safe_text_content(example_string)
self.assertTrue(isinstance(content_obj, Content))
./autopilot/tests/unit/test_out_of_test_addcleanup.py 0000644 0000041 0000041 00000003027 13061552435 023535 0 ustar www-data www-data # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Autopilot Functional Test Tool
# Copyright (C) 2012-2013 Canonical
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
from testtools import TestCase
from testtools.matchers import Equals
from autopilot.utilities import addCleanup, on_test_started
log = ''
class AddCleanupTests(TestCase):
def test_addCleanup_called_with_args_and_kwargs(self):
"""Test that out-of-test addClenaup works as expected, and is passed
both args and kwargs.
"""
class InnerTest(TestCase):
def write_to_log(self, *args, **kwargs):
global log
log = "Hello %r %r" % (args, kwargs)
def test_foo(self):
on_test_started(self)
addCleanup(self.write_to_log, "arg1", 2, foo='bar')
InnerTest('test_foo').run()
self.assertThat(log, Equals("Hello ('arg1', 2) {'foo': 'bar'}"))
./autopilot/tests/unit/test_timeout.py 0000644 0000041 0000041 00000006137 13061552435 020516 0 ustar www-data www-data # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Autopilot Functional Test Tool
# Copyright (C) 2014 Canonical
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
from testtools import TestCase
from testtools.matchers import Equals, GreaterThan
from autopilot.globals import (
get_default_timeout_period,
get_long_timeout_period,
set_default_timeout_period,
set_long_timeout_period,
)
from autopilot._timeout import Timeout
from autopilot.utilities import sleep
class TimeoutClassTests(TestCase):
def setUp(self):
super(TimeoutClassTests, self).setUp()
# We need to ignore the settings the autopilot runner may set,
# otherwise we cannot make any meaningful assertions in these tests.
self.addCleanup(
set_default_timeout_period,
get_default_timeout_period()
)
set_default_timeout_period(10.0)
self.addCleanup(
set_long_timeout_period,
get_long_timeout_period()
)
set_long_timeout_period(30.0)
def test_medium_sleeps_for_correct_time(self):
with sleep.mocked() as mocked_sleep:
for _ in Timeout.default():
pass
self.assertEqual(10.0, mocked_sleep.total_time_slept())
def test_long_sleeps_for_correct_time(self):
with sleep.mocked() as mocked_sleep:
for _ in Timeout.long():
pass
self.assertEqual(30.0, mocked_sleep.total_time_slept())
def test_medium_elapsed_time_increases(self):
with sleep.mocked():
last_elapsed = None
for elapsed in Timeout.default():
if last_elapsed is not None:
self.assertThat(elapsed, GreaterThan(last_elapsed))
else:
self.assertEqual(elapsed, 0.0)
last_elapsed = elapsed
def test_long_elapsed_time_increases(self):
with sleep.mocked():
last_elapsed = None
for elapsed in Timeout.long():
if last_elapsed is not None:
self.assertThat(elapsed, GreaterThan(last_elapsed))
else:
self.assertEqual(elapsed, 0.0)
last_elapsed = elapsed
def test_medium_timeout_final_call(self):
set_default_timeout_period(0.0)
self.assertThat(len(list(Timeout.default())), Equals(1))
def test_long_timeout_final_call(self):
set_long_timeout_period(0.0)
self.assertThat(len(list(Timeout.long())), Equals(1))
./autopilot/tests/unit/test_testcase.py 0000644 0000041 0000041 00000011625 13061552435 020641 0 ustar www-data www-data # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Autopilot Functional Test Tool
# Copyright (C) 2013-2014 Canonical
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
from unittest.mock import Mock
from testtools import TestCase
from testtools.matchers import raises
from autopilot.testcase import (
_compare_system_with_process_snapshot,
_considered_failing_test,
_get_application_launch_args,
)
from autopilot.utilities import sleep
class ProcessSnapshotTests(TestCase):
def test_snapshot_returns_when_no_apps_running(self):
with sleep.mocked() as mock_sleep:
_compare_system_with_process_snapshot(lambda: [], [])
self.assertEqual(0.0, mock_sleep.total_time_slept())
def test_snapshot_raises_AssertionError_with_new_apps_opened(self):
with sleep.mocked():
fn = lambda: _compare_system_with_process_snapshot(
lambda: ['foo'],
[]
)
self.assertThat(fn, raises(AssertionError(
"The following apps were started during the test and "
"not closed: ['foo']"
)))
def test_bad_snapshot_waits_10_seconds(self):
with sleep.mocked() as mock_sleep:
try:
_compare_system_with_process_snapshot(
lambda: ['foo'],
[]
)
except:
pass
finally:
self.assertEqual(10.0, mock_sleep.total_time_slept())
def test_snapshot_does_not_raise_on_closed_old_app(self):
_compare_system_with_process_snapshot(lambda: [], ['foo'])
def test_snapshot_exits_after_first_success(self):
get_snapshot = Mock()
get_snapshot.side_effect = [['foo'], []]
with sleep.mocked() as mock_sleep:
_compare_system_with_process_snapshot(
get_snapshot,
[]
)
self.assertEqual(1.0, mock_sleep.total_time_slept())
class AutopilotTestCaseSupportFunctionTests(TestCase):
def test_considered_failing_test_returns_true_for_failing(self):
self.assertTrue(_considered_failing_test(AssertionError))
def test_considered_failing_test_returns_true_for_unexpected_success(self):
from unittest.case import _UnexpectedSuccess
self.assertTrue(_considered_failing_test(_UnexpectedSuccess))
def test_considered_failing_test_returns_false_for_skip(self):
from unittest.case import SkipTest
self.assertFalse(_considered_failing_test(SkipTest))
def test_considered_failing_test_returns_false_for_inherited_skip(self):
from unittest.case import SkipTest
class CustomSkip(SkipTest):
pass
self.assertFalse(_considered_failing_test(CustomSkip))
def test_considered_failing_test_returns_false_for_expected_fail(self):
from testtools.testcase import _ExpectedFailure
self.assertFalse(_considered_failing_test(_ExpectedFailure))
def test_considered_failing_test_returns_false_for_inherited_expected_fail(self): # NOQA
from testtools.testcase import _ExpectedFailure
class CustomExpected(_ExpectedFailure):
pass
self.assertFalse(_considered_failing_test(CustomExpected))
class AutopilotGetApplicationLaunchArgsTests(TestCase):
def test_when_no_args_returns_empty_dict(self):
self.assertEqual(_get_application_launch_args(dict()), dict())
def test_ignores_unknown_args(self):
self.assertEqual(_get_application_launch_args(dict(unknown="")), {})
def test_gets_argument_values(self):
app_type_value = self.getUniqueString()
self.assertEqual(
_get_application_launch_args(dict(app_type=app_type_value)),
dict(app_type=app_type_value)
)
def test_gets_argument_values_ignores_unknown_values(self):
app_type_value = self.getUniqueString()
kwargs = dict(app_type=app_type_value, unknown=self.getUniqueString())
self.assertEqual(
_get_application_launch_args(kwargs),
dict(app_type=app_type_value)
)
def test_removes_used_arguments_from_parameter(self):
app_type_value = self.getUniqueString()
kwargs = dict(app_type=app_type_value)
_get_application_launch_args(kwargs)
self.assertEqual(kwargs, dict())
./autopilot/tests/unit/__init__.py 0000644 0000041 0000041 00000001420 13061552435 017516 0 ustar www-data www-data # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Autopilot Functional Test Tool
# Copyright (C) 2012-2013 Canonical
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
./autopilot/tests/unit/test_introspection_utilities.py 0000644 0000041 0000041 00000014037 13061552435 024021 0 ustar www-data www-data # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Autopilot Functional Test Tool
# Copyright (C) 2016 Canonical
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
from testtools import TestCase
from autopilot.introspection.dbus import raises
from autopilot.introspection.utilities import process_util, sort_by_keys
from autopilot.tests.unit.introspection_base import (
get_mock_object,
get_global_rect,
)
PROCESS_NAME = 'dummy_process'
PROCESS_WITH_SINGLE_INSTANCE = [{'name': PROCESS_NAME, 'pid': -80}]
PROCESS_WITH_MULTIPLE_INSTANCES = [
PROCESS_WITH_SINGLE_INSTANCE[0],
{'name': PROCESS_NAME, 'pid': -81}
]
# a list of dummy co-ordinates
X_COORDS = [0, 0, 0, 0]
Y_COORDS = [7, 9, 18, 14]
class ProcessUtilitiesTestCase(TestCase):
def test_passing_non_running_process_raises(self):
self.assertRaises(
ValueError,
process_util._query_pids_for_process,
PROCESS_NAME
)
def test_passing_running_process_not_raises(self):
with process_util.mocked(PROCESS_WITH_SINGLE_INSTANCE):
self.assertFalse(
raises(
ValueError,
process_util._query_pids_for_process,
PROCESS_NAME
)
)
def test_passing_integer_raises(self):
self.assertRaises(
ValueError,
process_util._query_pids_for_process,
911
)
def test_pid_for_process_is_int(self):
with process_util.mocked(PROCESS_WITH_SINGLE_INSTANCE):
self.assertIsInstance(
process_util.get_pid_for_process(PROCESS_NAME),
int
)
def test_pids_for_process_is_list(self):
with process_util.mocked(PROCESS_WITH_MULTIPLE_INSTANCES):
self.assertIsInstance(
process_util.get_pids_for_process(PROCESS_NAME),
list
)
def test_passing_process_with_multiple_pids_raises(self):
with process_util.mocked(PROCESS_WITH_MULTIPLE_INSTANCES):
self.assertRaises(
ValueError,
process_util.get_pid_for_process,
PROCESS_NAME
)
class SortByKeysTests(TestCase):
def _get_root_property_from_object_list(self, objects, prop):
return [getattr(obj, prop) for obj in objects]
def _get_child_property_from_object_list(self, objects, child, prop):
return [getattr(getattr(obj, child), prop) for obj in objects]
def test_sort_by_single_property(self):
objects = [get_mock_object(y=y) for y in Y_COORDS]
sorted_objects = sort_by_keys(objects, ['y'])
self.assertEqual(len(sorted_objects), len(objects))
self.assertEqual(
self._get_root_property_from_object_list(sorted_objects, 'y'),
sorted(Y_COORDS)
)
def test_sort_by_multiple_properties(self):
objects = [
get_mock_object(x=x, y=y) for x, y in zip(X_COORDS, Y_COORDS)
]
sorted_objects = sort_by_keys(objects, ['x', 'y'])
self.assertEqual(len(sorted_objects), len(objects))
self.assertEqual(
self._get_root_property_from_object_list(sorted_objects, 'x'),
sorted(X_COORDS)
)
self.assertEqual(
self._get_root_property_from_object_list(sorted_objects, 'y'),
sorted(Y_COORDS)
)
def test_sort_by_single_nested_property(self):
objects = [
get_mock_object(globalRect=get_global_rect(y=y)) for y in Y_COORDS
]
sorted_objects = sort_by_keys(objects, ['globalRect.y'])
self.assertEqual(len(sorted_objects), len(objects))
self.assertEqual(
self._get_child_property_from_object_list(
sorted_objects,
child='globalRect',
prop='y'
),
sorted(Y_COORDS)
)
def test_sort_by_multiple_nested_properties(self):
objects = [
get_mock_object(globalRect=get_global_rect(x=x, y=y))
for x, y in zip(X_COORDS, Y_COORDS)
]
sorted_objects = sort_by_keys(
objects,
['globalRect.x', 'globalRect.y']
)
self.assertEqual(len(sorted_objects), len(objects))
self.assertEqual(
self._get_child_property_from_object_list(
sorted_objects,
child='globalRect',
prop='x'
),
sorted(X_COORDS)
)
self.assertEqual(
self._get_child_property_from_object_list(
sorted_objects,
child='globalRect',
prop='y'
),
sorted(Y_COORDS)
)
def test_sort_three_levels_nested_property(self):
objects = [
get_mock_object(
fake_property=get_global_rect(
y=get_global_rect(y=y)
)
) for y in Y_COORDS
]
sorted_objects = sort_by_keys(objects, ['fake_property.y.y'])
self.assertEqual(len(sorted_objects), len(objects))
sorted_ys = [i.fake_property.y.y for i in sorted_objects]
self.assertEqual(sorted_ys, sorted(Y_COORDS))
def test_raises_if_sort_keys_not_list(self):
self.assertRaises(ValueError, sort_by_keys, None, 'y')
def test_returns_unchanged_if_one_object(self):
obj = [get_mock_object()]
output = sort_by_keys(obj, ['x'])
self.assertEqual(output, obj)
./autopilot/tests/unit/test_globals.py 0000644 0000041 0000041 00000005512 13061552435 020447 0 ustar www-data www-data # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Autopilot Functional Test Tool
# Copyright (C) 2014 Canonical
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
from testtools import TestCase
from testtools.matchers import Equals
import autopilot.globals as _g
def restore_value(cleanup_enabled, object, attr_name):
"""Ensure that, at the end of the current test, object.attr_name is
restored to it's current state.
"""
original_value = getattr(object, attr_name)
cleanup_enabled.addCleanup(
lambda: setattr(object, attr_name, original_value)
)
class DebugProfileFunctionTests(TestCase):
def setUp(self):
super(DebugProfileFunctionTests, self).setUp()
# since we're modifying a global in our tests, make sure we restore
# the original value after each test has run:
restore_value(self, _g, '_debug_profile_fixture')
def test_can_set_and_get_fixture(self):
fake_fixture = object()
_g.set_debug_profile_fixture(fake_fixture)
self.assertThat(_g.get_debug_profile_fixture(), Equals(fake_fixture))
class TimeoutFunctionTests(TestCase):
def setUp(self):
super(TimeoutFunctionTests, self).setUp()
# since we're modifying a global in our tests, make sure we restore
# the original value after each test has run:
restore_value(self, _g, '_default_timeout_value')
restore_value(self, _g, '_long_timeout_value')
restore_value(self, _g, '_test_timeout')
def test_default_timeout_values(self):
self.assertEqual(10.0, _g.get_default_timeout_period())
self.assertEqual(30.0, _g.get_long_timeout_period())
def test_can_set_default_timeout_value(self):
new_value = self.getUniqueInteger()
_g.set_default_timeout_period(new_value)
self.assertEqual(new_value, _g.get_default_timeout_period())
def test_can_set_long_timeout_value(self):
new_value = self.getUniqueInteger()
_g.set_long_timeout_period(new_value)
self.assertEqual(new_value, _g.get_long_timeout_period())
def test_can_set_test_timeout(self):
new_value = self.getUniqueInteger()
_g.set_test_timeout(new_value)
self.assertEqual(new_value, _g.get_test_timeout())
./autopilot/tests/unit/test_stagnate_state.py 0000644 0000041 0000041 00000004455 13061552435 022037 0 ustar www-data www-data # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Autopilot Functional Test Tool
# Copyright (C) 2013 Canonical
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
from testtools import TestCase
from testtools.matchers import Equals, raises
from autopilot.utilities import StagnantStateDetector
class StagnantCheckTests(TestCase):
def test_state_change_resets_counter(self):
state_check = StagnantStateDetector(threshold=5)
x, y = (1, 1)
for i in range(4):
state_check.check_state(x, y)
self.assertThat(state_check._stagnant_count, Equals(3))
state_check.check_state(10, 10)
self.assertThat(state_check._stagnant_count, Equals(0))
def test_raises_exception_when_threshold_hit(self):
state_check = StagnantStateDetector(threshold=1)
x, y = (1, 1)
state_check.check_state(x, y)
fn = lambda: state_check.check_state(x, y)
self.assertThat(
fn,
raises(
StagnantStateDetector.StagnantState(
"State has been the same for 1 iterations"
)
)
)
def test_raises_exception_when_thresold_is_zero(self):
fn = lambda: StagnantStateDetector(threshold=0)
self.assertThat(
fn,
raises(ValueError("Threshold must be a positive integer."))
)
def test_passing_nonhashable_data_raises_exception(self):
class UnHashable(object):
__hash__ = None
no_hash = UnHashable()
state_check = StagnantStateDetector(threshold=5)
fn = lambda: state_check.check_state(no_hash)
self.assertThat(fn, raises(TypeError("unhashable type: 'UnHashable'")))
./autopilot/tests/unit/test_test_loader.py 0000644 0000041 0000041 00000017560 13061552435 021337 0 ustar www-data www-data # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Autopilot Functional Test Tool
# Copyright (C) 2013 Canonical
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
import os
import os.path
import random
import string
from testtools import TestCase
from testtools.matchers import Not, Raises
from contextlib import contextmanager
from unittest.mock import patch
import shutil
import tempfile
from autopilot.run import (
_discover_test,
get_package_location,
load_test_suite_from_name
)
@contextmanager
def working_dir(directory):
original_directory = os.getcwd()
os.chdir(directory)
try:
yield
finally:
os.chdir(original_directory)
class TestLoaderTests(TestCase):
_previous_module_names = []
def setUp(self):
super(TestLoaderTests, self).setUp()
self.sandbox_dir = tempfile.mkdtemp()
self.addCleanup(shutil.rmtree, self.sandbox_dir)
self.test_module_name = self._unique_module_name()
def _unique_module_name(self):
generator = lambda: ''.join(
random.choice(string.ascii_letters) for letter in range(8)
)
name = generator()
while name in self._previous_module_names:
name = generator()
self._previous_module_names.append(name)
return name
def create_empty_package_file(self, filename):
full_filename = os.path.join(self.test_module_name, filename)
with self.open_sandbox_file(full_filename) as f:
f.write('')
def create_package_file_with_contents(self, filename, contents):
full_filename = os.path.join(self.test_module_name, filename)
with self.open_sandbox_file(full_filename) as f:
f.write(contents)
@contextmanager
def open_sandbox_file(self, relative_path):
full_path = os.path.join(self.sandbox_dir, relative_path)
dirname = os.path.dirname(full_path)
if not os.path.exists(dirname):
os.makedirs(dirname)
with open(full_path, 'w') as f:
yield f
@contextmanager
def simple_file_setup(self):
with self.open_sandbox_file('test_foo.py') as f:
f.write('')
with working_dir(self.sandbox_dir):
yield
def test_get_package_location_can_import_file(self):
with self.simple_file_setup():
self.assertThat(
lambda: get_package_location('test_foo'),
Not(Raises())
)
def test_get_package_location_returns_correct_directory(self):
with self.simple_file_setup():
actual = get_package_location('test_foo')
self.assertEqual(self.sandbox_dir, actual)
def test_get_package_location_can_import_package(self):
self.create_empty_package_file('__init__.py')
with working_dir(self.sandbox_dir):
self.assertThat(
lambda: get_package_location(self.test_module_name),
Not(Raises()),
verbose=True
)
def test_get_package_location_returns_correct_directory_for_package(self):
self.create_empty_package_file('__init__.py')
with working_dir(self.sandbox_dir):
actual = get_package_location(self.test_module_name)
self.assertEqual(self.sandbox_dir, actual)
def test_get_package_location_can_import_nested_module(self):
self.create_empty_package_file('__init__.py')
self.create_empty_package_file('foo.py')
with working_dir(self.sandbox_dir):
self.assertThat(
lambda: get_package_location('%s.foo' % self.test_module_name),
Not(Raises()),
verbose=True
)
def test_get_package_location_returns_correct_directory_for_nested_module(self): # noqa
self.create_empty_package_file('__init__.py')
self.create_empty_package_file('foo.py')
with working_dir(self.sandbox_dir):
actual = get_package_location('%s.foo' % self.test_module_name)
self.assertEqual(self.sandbox_dir, actual)
@patch('autopilot.run._show_test_locations', new=lambda a: True)
def test_load_test_suite_from_name_can_load_file(self):
with self.open_sandbox_file('test_foo.py') as f:
f.write(SIMPLE_TESTCASE)
with working_dir(self.sandbox_dir):
suite, _ = load_test_suite_from_name('test_foo')
self.assertEqual(1, len(suite._tests))
@patch('autopilot.run._show_test_locations', new=lambda a: True)
def test_load_test_suite_from_name_can_load_nested_module(self):
self.create_empty_package_file('__init__.py')
self.create_package_file_with_contents('test_foo.py', SIMPLE_TESTCASE)
with working_dir(self.sandbox_dir):
suite, _ = load_test_suite_from_name(
'%s.test_foo' % self.test_module_name
)
self.assertEqual(1, suite.countTestCases())
@patch('autopilot.run._show_test_locations', new=lambda a: True)
def test_load_test_suite_from_name_only_loads_requested_suite(self):
self.create_empty_package_file('__init__.py')
self.create_package_file_with_contents('test_foo.py', SIMPLE_TESTCASE)
self.create_package_file_with_contents('test_bar.py', SIMPLE_TESTCASE)
with working_dir(self.sandbox_dir):
suite, _ = load_test_suite_from_name(
'%s.test_bar' % self.test_module_name
)
self.assertEqual(1, suite.countTestCases())
@patch('autopilot.run._show_test_locations', new=lambda a: True)
def test_load_test_suite_from_name_loads_requested_test_from_suite(self):
self.create_empty_package_file('__init__.py')
self.create_package_file_with_contents('test_foo.py', SAMPLE_TESTCASES)
self.create_package_file_with_contents('test_bar.py', SAMPLE_TESTCASES)
with working_dir(self.sandbox_dir):
suite, _ = load_test_suite_from_name(
'%s.test_bar.SampleTests.test_passes_again'
% self.test_module_name
)
self.assertEqual(1, suite.countTestCases())
@patch('autopilot.run._handle_discovery_error')
@patch('autopilot.run._show_test_locations', new=lambda a: True)
def test_loading_nonexistent_test_suite_doesnt_error(self, err_handler):
self.assertThat(
lambda: load_test_suite_from_name('nonexistent'),
Not(Raises())
)
def test_loading_nonexistent_test_suite_indicates_error(self):
self.assertRaises(
ImportError,
lambda: _discover_test('nonexistent')
)
@patch('autopilot.run._reexecute_autopilot_using_module')
@patch('autopilot.run._is_testing_autopilot_module', new=lambda *a: True)
def test_testing_autopilot_is_redirected(self, patched_executor):
patched_executor.return_value = 0
self.assertRaises(
SystemExit,
lambda: load_test_suite_from_name('autopilot')
)
self.assertTrue(patched_executor.called)
SIMPLE_TESTCASE = """\
from unittest import TestCase
class SimpleTests(TestCase):
def test_passes(self):
self.assertEqual(1, 1)
"""
SAMPLE_TESTCASES = """\
from unittest import TestCase
class SampleTests(TestCase):
def test_passes(self):
self.assertEqual(1, 1)
def test_passes_again(self):
self.assertEqual(1, 1)
"""
./autopilot/tests/unit/test_command_line_args.py 0000644 0000041 0000041 00000035110 13061552435 022462 0 ustar www-data www-data #!/usr/bin/env python
# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Autopilot Functional Test Tool
# Copyright (C) 2012-2013 Canonical
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
"Unit tests for the command line parser in autopilot."
from unittest.mock import patch
from io import StringIO
from testscenarios import WithScenarios
from testtools import TestCase
from testtools.matchers import Equals
from autopilot.run import _parse_arguments
class InvalidArguments(Exception):
pass
def parse_args(args):
if isinstance(args, str):
args = args.split()
try:
return _parse_arguments(args)
except SystemExit as e:
raise InvalidArguments("%s" % e)
class CommandLineArgsTests(TestCase):
def test_launch_command_accepts_application(self):
args = parse_args("launch app")
self.assertThat(args.mode, Equals("launch"))
def test_launch_command_has_correct_default_interface(self):
args = parse_args("launch app")
self.assertThat(args.interface, Equals("Auto"))
def test_launch_command_can_specify_Qt_interface(self):
args = parse_args("launch -i Qt app")
self.assertThat(args.interface, Equals("Qt"))
def test_launch_command_can_specify_Gtk_interface(self):
args = parse_args("launch -i Gtk app")
self.assertThat(args.interface, Equals("Gtk"))
@patch('sys.stderr', new=StringIO())
def test_launch_command_fails_on_unknown_interface(self):
self.assertRaises(
InvalidArguments, parse_args, "launch -i unknown app")
def test_launch_command_has_correct_default_verbosity(self):
args = parse_args("launch app")
self.assertThat(args.verbose, Equals(False))
def test_launch_command_can_specify_verbosity(self):
args = parse_args("launch -v app")
self.assertThat(args.verbose, Equals(1))
def test_launch_command_can_specify_extra_verbosity(self):
args = parse_args("launch -vv app")
self.assertThat(args.verbose, Equals(2))
args = parse_args("launch -v -v app")
self.assertThat(args.verbose, Equals(2))
def test_launch_command_stores_application(self):
args = parse_args("launch app")
self.assertThat(args.application, Equals(["app"]))
def test_launch_command_stores_application_with_args(self):
args = parse_args("launch app arg1 arg2")
self.assertThat(args.application, Equals(["app", "arg1", "arg2"]))
def test_launch_command_accepts_different_app_arg_formats(self):
args = parse_args("launch app -s --long --key=val arg1 arg2")
self.assertThat(
args.application,
Equals(["app", "-s", "--long", "--key=val", "arg1", "arg2"]))
@patch('sys.stderr', new=StringIO())
def test_launch_command_must_specify_app(self):
self.assertRaises(InvalidArguments, parse_args, "launch")
@patch('autopilot.run.have_vis', new=lambda: True)
def test_vis_present_when_vis_module_installed(self):
args = parse_args('vis')
self.assertThat(args.mode, Equals("vis"))
@patch('autopilot.run.have_vis', new=lambda: False)
@patch('sys.stderr', new=StringIO())
def test_vis_not_present_when_vis_module_not_installed(self):
self.assertRaises(InvalidArguments, parse_args, 'vis')
@patch('autopilot.run.have_vis', new=lambda: True)
def test_vis_default_verbosity(self):
args = parse_args('vis')
self.assertThat(args.verbose, Equals(False))
@patch('autopilot.run.have_vis', new=lambda: True)
def test_vis_single_verbosity(self):
args = parse_args('vis -v')
self.assertThat(args.verbose, Equals(1))
@patch('autopilot.run.have_vis', new=lambda: True)
def test_vis_double_verbosity(self):
args = parse_args('vis -vv')
self.assertThat(args.verbose, Equals(2))
args = parse_args('vis -v -v')
self.assertThat(args.verbose, Equals(2))
@patch('autopilot.run.have_vis', new=lambda: True)
def test_vis_default_testability_flag(self):
args = parse_args('vis')
self.assertThat(args.testability, Equals(False))
@patch('autopilot.run.have_vis', new=lambda: True)
def test_vis_can_set_testability_flag(self):
args = parse_args('vis -testability')
self.assertThat(args.testability, Equals(True))
@patch('autopilot.run.have_vis', new=lambda: True)
def test_vis_default_profile_flag(self):
args = parse_args('vis')
self.assertThat(args.enable_profile, Equals(False))
@patch('autopilot.run.have_vis', new=lambda: True)
def test_vis_can_enable_profiling(self):
args = parse_args('vis --enable-profile')
self.assertThat(args.enable_profile, Equals(True))
def test_list_mode(self):
args = parse_args('list foo')
self.assertThat(args.mode, Equals("list"))
def test_list_mode_accepts_suite_name(self):
args = parse_args('list foo')
self.assertThat(args.suite, Equals(["foo"]))
def test_list_mode_accepts_many_suite_names(self):
args = parse_args('list foo bar baz')
self.assertThat(args.suite, Equals(["foo", "bar", "baz"]))
def test_list_run_order_long_option(self):
args = parse_args('list --run-order foo')
self.assertThat(args.run_order, Equals(True))
def test_list_run_order_short_option(self):
args = parse_args('list -ro foo')
self.assertThat(args.run_order, Equals(True))
def test_list_no_run_order(self):
args = parse_args('list foo')
self.assertThat(args.run_order, Equals(False))
def test_list_suites_option(self):
args = parse_args('list --suites foo')
self.assertThat(args.suites, Equals(True))
def test_list_not_suites_option(self):
args = parse_args('list foo')
self.assertThat(args.suites, Equals(False))
def test_run_mode(self):
args = parse_args('run foo')
self.assertThat(args.mode, Equals("run"))
def test_run_mode_accepts_suite_name(self):
args = parse_args('run foo')
self.assertThat(args.suite, Equals(["foo"]))
def test_run_mode_accepts_many_suite_names(self):
args = parse_args('run foo bar baz')
self.assertThat(args.suite, Equals(["foo", "bar", "baz"]))
def test_run_command_default_output(self):
args = parse_args('run foo')
self.assertThat(args.output, Equals(None))
def test_run_command_path_output_short(self):
args = parse_args('run -o /path/to/file foo')
self.assertThat(args.output, Equals("/path/to/file"))
def test_run_command_path_output_long(self):
args = parse_args('run --output ../file foo')
self.assertThat(args.output, Equals("../file"))
def test_run_command_default_format(self):
args = parse_args('run foo')
self.assertThat(args.format, Equals("text"))
def test_run_command_text_format_short_version(self):
args = parse_args('run -f text foo')
self.assertThat(args.format, Equals("text"))
def test_run_command_text_format_long_version(self):
args = parse_args('run --format text foo')
self.assertThat(args.format, Equals("text"))
def test_run_command_xml_format_short_version(self):
args = parse_args('run -f xml foo')
self.assertThat(args.format, Equals("xml"))
def test_run_command_xml_format_long_version(self):
args = parse_args('run --format xml foo')
self.assertThat(args.format, Equals("xml"))
def test_run_command_default_failfast_off(self):
args = parse_args('run foo')
self.assertThat(args.failfast, Equals(False))
def test_run_command_accepts_failfast_short(self):
args = parse_args('run -ff foo')
self.assertThat(args.failfast, Equals(True))
def test_run_command_accepts_failfast_long(self):
args = parse_args('run --failfast foo')
self.assertThat(args.failfast, Equals(True))
@patch('sys.stderr', new=StringIO())
def test_run_command_unknown_format_short_version(self):
self.assertRaises(
InvalidArguments, parse_args, 'run -f unknown foo')
@patch('sys.stderr', new=StringIO())
def test_run_command_unknown_format_long_version(self):
self.assertRaises(
InvalidArguments, parse_args, 'run --format unknown foo')
def test_run_command_record_flag_default(self):
args = parse_args("run foo")
self.assertThat(args.record, Equals(False))
def test_run_command_record_flag_short(self):
args = parse_args("run -r foo")
self.assertThat(args.record, Equals(True))
def test_run_command_record_flag_long(self):
args = parse_args("run --record foo")
self.assertThat(args.record, Equals(True))
def test_run_command_record_dir_flag_short(self):
args = parse_args("run -rd /path/to/dir foo")
self.assertThat(args.record_directory, Equals("/path/to/dir"))
def test_run_command_record_dir_flag_long(self):
args = parse_args("run --record-directory /path/to/dir foo")
self.assertThat(args.record_directory, Equals("/path/to/dir"))
def test_run_command_record_options_flag_long(self):
args = parse_args(
"run --record-options=--fps=6,--no-wm-check foo")
self.assertThat(args.record_options, Equals("--fps=6,--no-wm-check"))
def test_run_command_random_order_flag_short(self):
args = parse_args("run -ro foo")
self.assertThat(args.random_order, Equals(True))
def test_run_command_random_order_flag_long(self):
args = parse_args("run --random-order foo")
self.assertThat(args.random_order, Equals(True))
def test_run_command_random_order_flag_default(self):
args = parse_args("run foo")
self.assertThat(args.random_order, Equals(False))
def test_run_default_verbosity(self):
args = parse_args('run foo')
self.assertThat(args.verbose, Equals(False))
def test_run_single_verbosity(self):
args = parse_args('run -v foo')
self.assertThat(args.verbose, Equals(1))
def test_run_double_verbosity(self):
args = parse_args('run -vv foo')
self.assertThat(args.verbose, Equals(2))
args = parse_args('run -v -v foo')
self.assertThat(args.verbose, Equals(2))
def test_fails_with_no_arguments_supplied(self):
with patch('sys.stderr', new=StringIO()) as patched_err:
try:
_parse_arguments([])
except SystemExit as e:
self.assertThat(e.code, Equals(2))
stderr_lines = patched_err.getvalue().split('\n')
self.assertTrue(
stderr_lines[-2].endswith("error: too few arguments")
)
self.assertThat(stderr_lines[-1], Equals(""))
else:
self.fail("Argument parser unexpectedly passed")
def test_default_debug_profile_is_normal(self):
args = parse_args('run foo')
self.assertThat(args.debug_profile, Equals('normal'))
def test_can_select_normal_profile(self):
args = parse_args('run --debug-profile normal foo')
self.assertThat(args.debug_profile, Equals('normal'))
def test_can_select_verbose_profile(self):
args = parse_args('run --debug-profile verbose foo')
self.assertThat(args.debug_profile, Equals('verbose'))
@patch('sys.stderr', new=StringIO())
def test_cannot_select_other_debug_profile(self):
self.assertRaises(
InvalidArguments,
parse_args,
'run --debug-profile nonexistant foo'
)
def test_default_timeout_profile_is_normal(self):
args = parse_args('run foo')
self.assertThat(args.timeout_profile, Equals('normal'))
def test_can_select_long_timeout_profile(self):
args = parse_args('run --timeout-profile long foo')
self.assertThat(args.timeout_profile, Equals('long'))
@patch('sys.stderr', new=StringIO())
def test_cannot_select_other_timeout_profile(self):
self.assertRaises(
InvalidArguments,
parse_args,
'run --timeout-profile nonexistant foo'
)
def test_list_mode_strips_single_suite_slash(self):
args = parse_args('list foo/')
self.assertThat(args.suite, Equals(["foo"]))
def test_list_mode_strips_multiple_suite_slash(self):
args = parse_args('list foo/ bar/')
self.assertThat(args.suite, Equals(["foo", "bar"]))
def test_run_mode_strips_single_suite_slash(self):
args = parse_args('run foo/')
self.assertThat(args.suite, Equals(["foo"]))
def test_run_mode_strips_multiple_suite_slash(self):
args = parse_args('run foo/ bar/')
self.assertThat(args.suite, Equals(["foo", "bar"]))
def test_accepts_config_string(self):
args = parse_args('run --config foo test_id')
self.assertThat(args.test_config, Equals('foo'))
def test_accepts_long_config_string(self):
args = parse_args('run --config bar=foo,baz test_id')
self.assertThat(args.test_config, Equals('bar=foo,baz'))
def test_default_config_string(self):
args = parse_args('run foo')
self.assertThat(args.test_config, Equals(""))
def test_default_test_timeout(self):
args = parse_args('run foo')
self.assertThat(args.test_timeout, Equals(0))
def test_can_set_test_timeout(self):
args = parse_args('run --test-timeout 42 foo')
self.assertThat(args.test_timeout, Equals(42))
class GlobalProfileOptionTests(WithScenarios, TestCase):
scenarios = [
('run', dict(command='run', args='foo')),
('list', dict(command='list', args='foo')),
('launch', dict(command='launch', args='foo')),
('vis', dict(command='vis', args='')),
]
@patch('autopilot.run.have_vis', new=lambda: True)
def test_all_commands_support_profile_option(self):
command_parts = [self.command, '--enable-profile']
if self.args:
command_parts.append(self.args)
args = parse_args(' '.join(command_parts))
self.assertThat(args.enable_profile, Equals(True))
./autopilot/tests/unit/test_fixtures.py 0000644 0000041 0000041 00000006170 13061552435 020676 0 ustar www-data www-data # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Autopilot Functional Test Tool
# Copyright (C) 2014 Canonical
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
from testtools import TestCase
from testtools.matchers import raises
from unittest.mock import patch, Mock
import autopilot._fixtures as ap_fixtures
class FixtureWithDirectAddDetailTests(TestCase):
def test_sets_caseAddDetail_method(self):
fixture = ap_fixtures.FixtureWithDirectAddDetail(self.addDetail)
self.assertEqual(fixture.caseAddDetail, self.addDetail)
def test_can_construct_without_arguments(self):
fixture = ap_fixtures.FixtureWithDirectAddDetail()
self.assertEqual(fixture.caseAddDetail, fixture.addDetail)
class GSettingsAccessTests(TestCase):
def test_incorrect_schema_raises_exception(self):
self.assertThat(
lambda: ap_fixtures._gsetting_get_setting('com.foo.', 'baz'),
raises(ValueError)
)
def test_incorrect_key_raises_exception(self):
self.assertThat(
lambda: ap_fixtures._gsetting_get_setting(
'org.gnome.system.locale',
'baz'
),
raises(ValueError)
)
def test_get_value_returns_expected_value(self):
with patch.object(ap_fixtures, '_gsetting_get_setting') as get_setting:
setting = Mock()
setting.get_boolean.return_value = True
get_setting.return_value = setting
self.assertEqual(
ap_fixtures.get_bool_gsettings_value('foo', 'bar'),
True
)
class OSKAlwaysEnabledTests(TestCase):
@patch.object(ap_fixtures, '_gsetting_get_setting')
def test_sets_stayhidden_to_False(self, gs):
with patch.object(ap_fixtures, 'set_bool_gsettings_value') as set_gs:
with ap_fixtures.OSKAlwaysEnabled():
set_gs.assert_called_once_with(
'com.canonical.keyboard.maliit',
'stay-hidden',
False
)
def test_resets_value_to_original(self):
with patch.object(ap_fixtures, 'set_bool_gsettings_value') as set_gs:
with patch.object(ap_fixtures, 'get_bool_gsettings_value') as get_gs: # NOQA
get_gs.return_value = 'foo'
with ap_fixtures.OSKAlwaysEnabled():
pass
set_gs.assert_called_with(
'com.canonical.keyboard.maliit',
'stay-hidden',
'foo'
)
./autopilot/tests/unit/test_display_screenshot.py 0000644 0000041 0000041 00000012767 13061552435 022740 0 ustar www-data www-data # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Autopilot Functional Test Tool
# Copyright (C) 2014 Canonical
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
"""Unit tests for the display screenshot functionality."""
import subprocess
import tempfile
import os
from contextlib import contextmanager
from tempfile import NamedTemporaryFile
from testtools import TestCase, skipIf
from testtools.matchers import (
Equals,
FileExists,
MatchesRegex,
Not,
StartsWith,
raises,
)
from unittest.mock import Mock, patch
import autopilot.display._screenshot as _ss
from autopilot import platform
class ScreenShotTests(TestCase):
def test_get_screenshot_data_raises_RuntimeError_on_unknown_display(self):
self.assertRaises(RuntimeError, lambda: _ss.get_screenshot_data(""))
class X11ScreenShotTests(TestCase):
def get_pixbuf_that_mocks_saving(self, success, data):
pixbuf_obj = Mock()
pixbuf_obj.save_to_bufferv.return_value = (success, data)
return pixbuf_obj
@skipIf(platform.model() != "Desktop", "Only available on desktop.")
def test_save_gdk_pixbuf_to_fileobject_raises_error_if_save_failed(self):
pixbuf_obj = self.get_pixbuf_that_mocks_saving(False, None)
with patch.object(_ss, 'logger') as p_log:
self.assertRaises(
RuntimeError,
lambda: _ss._save_gdk_pixbuf_to_fileobject(pixbuf_obj)
)
p_log.error.assert_called_once_with("Unable to write image data.")
def test_save_gdk_pixbuf_to_fileobject_returns_data_object(self):
expected_data = b"Tests Rock"
pixbuf_obj = self.get_pixbuf_that_mocks_saving(True, expected_data)
data_object = _ss._save_gdk_pixbuf_to_fileobject(pixbuf_obj)
self.assertThat(data_object, Not(Equals(None)))
self.assertEqual(data_object.tell(), 0)
self.assertEqual(data_object.getvalue(), expected_data)
class MirScreenShotTests(TestCase):
def test_take_screenshot_raises_when_binary_not_available(self):
with patch.object(_ss.subprocess, 'check_call') as check_call:
check_call.side_effect = FileNotFoundError()
self.assertThat(
_ss._take_mirscreencast_screenshot,
raises(
FileNotFoundError(
"The utility 'mirscreencast' is not available."
)
)
)
def test_take_screenshot_raises_when_screenshot_fails(self):
with patch.object(_ss.subprocess, 'check_call') as check_call:
check_call.side_effect = subprocess.CalledProcessError(None, None)
self.assertThat(
_ss._take_mirscreencast_screenshot,
raises(
subprocess.CalledProcessError(
None, None, "Failed to take screenshot."
)
)
)
def test_take_screenshot_returns_resulting_filename(self):
with patch.object(_ss.subprocess, 'check_call'):
self.assertThat(
_ss._take_mirscreencast_screenshot(),
MatchesRegex(".*ap-screenshot-data-\d+.rgba")
)
def test_take_screenshot_filepath_is_in_tmp_dir(self):
with patch.object(_ss.subprocess, 'check_call'):
self.assertThat(
_ss._take_mirscreencast_screenshot(),
StartsWith(tempfile.gettempdir())
)
def test_image_data_from_file_returns_PIL_Image(self):
with _single_pixel_rgba_data_file() as filepath:
image_data = _ss._image_data_from_file(filepath, (1, 1))
self.assertEqual(image_data.mode, "RGBA")
self.assertEqual(image_data.size, (1, 1))
def test_get_png_from_rgba_file_returns_png_file(self):
with _single_pixel_rgba_data_file() as filepath:
png_image_data = _ss._get_png_from_rgba_file(filepath, (1, 1))
self.assertEqual(0, png_image_data.tell())
self.assertThat(png_image_data.read(), StartsWith(b'\x89PNG\r\n'))
@skipIf(platform.model() == "Desktop", "Only available on device.")
def test_raw_data_file_cleaned_up_on_failure(self):
"""Creation of image will fail with a nonsense filepath."""
with _simulate_bad_rgba_image_file() as image_file_path:
self.assertRaises(ValueError, _ss._get_screenshot_mir)
self.assertThat(image_file_path, Not(FileExists()))
@contextmanager
def _simulate_bad_rgba_image_file():
try:
with NamedTemporaryFile(delete=False) as f:
with patch.object(_ss, "_take_mirscreencast_screenshot") as mir_ss:
mir_ss.return_value = f.name
yield f.name
finally:
if os.path.exists(f.name):
os.remove(f.name)
@contextmanager
def _single_pixel_rgba_data_file():
with NamedTemporaryFile() as f:
f.write(b'<\x1e#\xff')
f.seek(0)
yield f.name
./autopilot/tests/unit/test_introspection_xpathselect.py 0000644 0000041 0000041 00000034132 13061552435 024330 0 ustar www-data www-data # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Autopilot Functional Test Tool
# Copyright (C) 2014 Canonical
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
from testscenarios import TestWithScenarios
from testtools import TestCase
from testtools.matchers import raises
from autopilot.introspection import _xpathselect as xpathselect
from autopilot.exceptions import InvalidXPathQuery
class XPathSelectQueryTests(TestCase):
def test_query_raises_TypeError_on_non_bytes_query(self):
fn = lambda: xpathselect.Query(
None,
xpathselect.Query.Operation.CHILD,
'asd'
)
self.assertThat(
fn,
raises(
TypeError(
"'query' parameter must be bytes, not %s"
% type('').__name__
)
)
)
def test_can_create_root_query(self):
q = xpathselect.Query.root(b'Foo')
self.assertEqual(b"/Foo", q.server_query_bytes())
def test_can_create_app_name_from_ascii_string(self):
q = xpathselect.Query.root('Foo')
self.assertEqual(b"/Foo", q.server_query_bytes())
def test_creating_root_query_with_unicode_app_name_raises(self):
self.assertThat(
lambda: xpathselect.Query.root("\u2026"),
raises(
InvalidXPathQuery(
"Type name '%s', must be ASCII encodable" % ('\u2026')
)
)
)
def test_repr_with_path(self):
path = b"/some/path"
q = xpathselect.Query.root('some').select_child('path')
self.assertEqual("Query(%r)" % path, repr(q))
def test_repr_with_path_and_filters(self):
expected = b"/some/path[bar=456,foo=123]"
filters = dict(foo=123, bar=456)
q = xpathselect.Query.root('some').select_child('path', filters)
self.assertEqual("Query(%r)" % expected, repr(q))
def test_select_child(self):
q = xpathselect.Query.root("Foo").select_child("Bar")
self.assertEqual(q.server_query_bytes(), b"/Foo/Bar")
def test_select_child_with_filters(self):
q = xpathselect.Query.root("Foo")\
.select_child("Bar", dict(visible=True))
self.assertEqual(q.server_query_bytes(), b"/Foo/Bar[visible=True]")
def test_many_select_children(self):
q = xpathselect.Query.root("Foo") \
.select_child("Bar") \
.select_child("Baz")
self.assertEqual(b"/Foo/Bar/Baz", q.server_query_bytes())
def test_many_select_children_with_filters(self):
q = xpathselect.Query.root("Foo") \
.select_child("Bar", dict(visible=True)) \
.select_child("Baz", dict(id=123))
self.assertEqual(
b"/Foo/Bar[visible=True]/Baz[id=123]",
q.server_query_bytes()
)
def test_select_descendant(self):
q = xpathselect.Query.root("Foo") \
.select_descendant("Bar")
self.assertEqual(b"/Foo//Bar", q.server_query_bytes())
def test_select_descendant_with_filters(self):
q = xpathselect.Query.root("Foo") \
.select_descendant("Bar", dict(name="Hello"))
self.assertEqual(b'/Foo//Bar[name="Hello"]', q.server_query_bytes())
def test_many_select_descendants(self):
q = xpathselect.Query.root("Foo") \
.select_descendant("Bar") \
.select_descendant("Baz")
self.assertEqual(b"/Foo//Bar//Baz", q.server_query_bytes())
def test_many_select_descendants_with_filters(self):
q = xpathselect.Query.root("Foo") \
.select_descendant("Bar", dict(visible=True)) \
.select_descendant("Baz", dict(id=123))
self.assertEqual(
b"/Foo//Bar[visible=True]//Baz[id=123]",
q.server_query_bytes()
)
def test_full_server_side_filter(self):
q = xpathselect.Query.root("Foo") \
.select_descendant("Bar", dict(visible=True)) \
.select_descendant("Baz", dict(id=123))
self.assertFalse(q.needs_client_side_filtering())
def test_client_side_filter(self):
q = xpathselect.Query.root("Foo") \
.select_descendant("Bar", dict(visible=True)) \
.select_descendant("Baz", dict(name="\u2026"))
self.assertTrue(q.needs_client_side_filtering())
def test_client_side_filter_all_query_bytes(self):
q = xpathselect.Query.root("Foo") \
.select_descendant("Bar", dict(visible=True)) \
.select_descendant("Baz", dict(name="\u2026"))
self.assertEqual(
b'/Foo//Bar[visible=True]//Baz',
q.server_query_bytes()
)
def test_deriving_from_client_side_filtered_query_raises_ValueError(self):
q = xpathselect.Query.root("Foo") \
.select_descendant("Baz", dict(name="\u2026"))
fn = lambda: q.select_child("Foo")
self.assertThat(
fn,
raises(InvalidXPathQuery(
"Cannot create a new query from a parent that requires "
"client-side filter processing."
))
)
def test_init_raises_TypeError_on_invalid_operation_type(self):
fn = lambda: xpathselect.Query(None, '/', b'sdf')
self.assertThat(
fn,
raises(TypeError(
"'operation' parameter must be bytes, not '%s'"
% type('').__name__
))
)
def test_init_raises_ValueError_on_invalid_operation(self):
fn = lambda: xpathselect.Query(None, b'foo', b'sdf')
self.assertThat(
fn,
raises(InvalidXPathQuery("Invalid operation 'foo'."))
)
def test_init_raises_ValueError_on_invalid_descendant_search(self):
fn = lambda: xpathselect.Query(None, b'//', b'*')
self.assertThat(
fn,
raises(InvalidXPathQuery(
"Must provide at least one server-side filter when searching "
"for descendants and using a wildcard node."
))
)
def test_new_from_path_and_id_raises_TypeError_on_unicode_path(self):
fn = lambda: xpathselect.Query.new_from_path_and_id('bad_path', 42)
self.assertThat(
fn,
raises(TypeError(
"'path' attribute must be bytes, not '%s'" % type('').__name__
))
)
def test_new_from_path_and_id_raises_ValueError_on_invalid_path(self):
fn = lambda: xpathselect.Query.new_from_path_and_id(b'bad_path', 42)
self.assertThat(
fn,
raises(InvalidXPathQuery("Invalid path 'bad_path'."))
)
def test_new_from_path_and_id_raises_ValueError_on_invalid_path2(self):
fn = lambda: xpathselect.Query.new_from_path_and_id(b'/', 42)
self.assertThat(
fn,
raises(InvalidXPathQuery("Invalid path '/'."))
)
def test_new_from_path_and_id_works_for_root_node(self):
q = xpathselect.Query.new_from_path_and_id(b'/root', 42)
self.assertEqual(b'/root', q.server_query_bytes())
def test_new_from_path_and_id_works_for_small_tree(self):
q = xpathselect.Query.new_from_path_and_id(b'/root/child', 42)
self.assertEqual(b'/root/child[id=42]', q.server_query_bytes())
def test_new_from_path_and_id_works_for_larger_tree(self):
q = xpathselect.Query.new_from_path_and_id(b'/root/child/leaf', 42)
self.assertEqual(b'/root/child/leaf[id=42]', q.server_query_bytes())
def test_get_client_side_filters_returns_client_side_filters(self):
q = xpathselect.Query.root('app') \
.select_child('leaf', dict(name='\u2026'))
self.assertEqual(dict(name='\u2026'), q.get_client_side_filters())
def test_get_parent_on_root_node_returns_the_same_query(self):
q = xpathselect.Query.root('app')
q2 = q.select_parent()
self.assertEqual(b'/app/..', q2.server_query_bytes())
def test_get_parent_on_node_returns_parent_query(self):
q = xpathselect.Query.new_from_path_and_id(b'/root/child', 42)
q2 = q.select_parent()
self.assertEqual(b'/root/child[id=42]/..', q2.server_query_bytes())
def test_init_raises_ValueError_when_passing_filters_and_parent(self):
fn = lambda: xpathselect.Query(None, b'/', b'..', dict(foo=123))
self.assertThat(
fn,
raises(InvalidXPathQuery(
"Cannot specify filters while selecting a parent"
))
)
def test_init_raises_ValueError_when_passing_bad_op_and_parent(self):
fn = lambda: xpathselect.Query(None, b'//', b'..')
self.assertThat(
fn,
raises(InvalidXPathQuery(
"Operation must be CHILD while selecting a parent"
))
)
def test_select_tree_root_returns_correct_query(self):
q = xpathselect.Query.pseudo_tree_root()
self.assertEqual(b'/', q.server_query_bytes())
def test_cannot_select_child_on_pseudo_tree_root(self):
fn = lambda: xpathselect.Query.pseudo_tree_root().select_child('foo')
self.assertThat(
fn,
raises(InvalidXPathQuery(
"Cannot select children from a pseudo-tree-root query."
))
)
def test_whole_tree_search_returns_correct_query(self):
q = xpathselect.Query.whole_tree_search('Foo')
self.assertEqual(b'//Foo', q.server_query_bytes())
def test_whole_tree_search_with_filters_returns_correct_query(self):
q = xpathselect.Query.whole_tree_search('Foo', dict(foo='bar'))
self.assertEqual(b'//Foo[foo="bar"]', q.server_query_bytes())
class ParameterFilterStringScenariodTests(TestWithScenarios, TestCase):
scenarios = [
('bool true', dict(k='visible', v=True, r=b"visible=True")),
('bool false', dict(k='visible', v=False, r=b"visible=False")),
('int +ve', dict(k='size', v=123, r=b"size=123")),
('int -ve', dict(k='prio', v=-12, r=b"prio=-12")),
('simple string', dict(k='Name', v="btn1", r=b"Name=\"btn1\"")),
('simple bytes', dict(k='Name', v=b"btn1", r=b"Name=\"btn1\"")),
('string space', dict(k='Name', v="a b c ", r=b"Name=\"a b c \"")),
('bytes space', dict(k='Name', v=b"a b c ", r=b"Name=\"a b c \"")),
('string escapes', dict(
k='a',
v="\a\b\f\n\r\t\v\\",
r=br'a="\x07\x08\x0c\n\r\t\x0b\\"')),
('byte escapes', dict(
k='a',
v=b"\a\b\f\n\r\t\v\\",
r=br'a="\x07\x08\x0c\n\r\t\x0b\\"')),
('escape quotes (str)', dict(k='b', v="'", r=b'b="\\' + b"'" + b'"')),
(
'escape quotes (bytes)',
dict(k='b', v=b"'", r=b'b="\\' + b"'" + b'"')
),
]
def test_query_string(self):
s = xpathselect._get_filter_string_for_key_value_pair(self.k, self.v)
self.assertEqual(s, self.r)
class ParameterFilterStringTests(TestWithScenarios, TestCase):
def test_raises_ValueError_on_unknown_type(self):
fn = lambda: xpathselect._get_filter_string_for_key_value_pair(
'k',
object()
)
self.assertThat(
fn,
raises(
ValueError("Unsupported value type: object")
)
)
class ServerSideParamMatchingTests(TestWithScenarios, TestCase):
"""Tests for the server side matching decision function."""
scenarios = [
('should work', dict(key='keyname', value='value', result=True)),
('invalid key', dict(key='k e', value='value', result=False)),
('string value', dict(key='key', value='v e', result=True)),
('string value2', dict(key='key', value='v?e', result=True)),
('string value3', dict(key='key', value='1/2."!@#*&^%', result=True)),
('bool value', dict(key='key', value=False, result=True)),
('int value', dict(key='key', value=123, result=True)),
('int value2', dict(key='key', value=-123, result=True)),
('float value', dict(key='key', value=1.0, result=False)),
('dict value', dict(key='key', value={}, result=False)),
('obj value', dict(key='key', value=TestCase, result=False)),
('int overflow 1', dict(key='key', value=-2147483648, result=True)),
('int overflow 2', dict(key='key', value=-2147483649, result=False)),
('int overflow 3', dict(key='key', value=2147483647, result=True)),
('int overflow 4', dict(key='key', value=2147483648, result=False)),
('unicode string', dict(key='key', value='H\u2026i', result=False)),
]
def test_valid_server_side_param(self):
self.assertEqual(
xpathselect._is_valid_server_side_filter_param(
self.key,
self.value
),
self.result
)
class GetClassnameFromPathTests(TestCase):
def test_single_element(self):
self.assertEqual("Foo", xpathselect.get_classname_from_path("Foo"))
def test_single_element_with_path(self):
self.assertEqual("Foo", xpathselect.get_classname_from_path("/Foo"))
def test_multiple_elements(self):
self.assertEqual(
"Baz",
xpathselect.get_classname_from_path("/Foo/Bar/Baz")
)
class GetPathRootTests(TestCase):
def test_get_root_path_on_string_path(self):
self.assertEqual("Foo", xpathselect.get_path_root("/Foo/Bar/Baz"))
def test_get_root_path_on_bytes_literal_path(self):
self.assertEqual(b"Foo", xpathselect.get_path_root(b"/Foo/Bar/Baz"))
def test_get_root_path_on_garbage_path_raises(self):
self.assertRaises(IndexError, xpathselect.get_path_root, "asdfgh")
./autopilot/tests/unit/test_application_environment.py 0000644 0000041 0000041 00000007054 13061552435 023756 0 ustar www-data www-data # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Autopilot Functional Test Tool
# Copyright (C) 2013 Canonical
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
from unittest.mock import patch
from testtools import TestCase
from testtools.matchers import raises
from autopilot.application._environment import (
ApplicationEnvironment,
GtkApplicationEnvironment,
QtApplicationEnvironment,
)
class ApplicationEnvironmentTests(TestCase):
def test_raises_notimplementederror(self):
self.assertThat(
lambda: ApplicationEnvironment().prepare_environment(None, None),
raises(
NotImplementedError("Sub-classes must implement this method.")
)
)
class GtkApplicationEnvironmentTests(TestCase):
def setUp(self):
super(GtkApplicationEnvironmentTests, self).setUp()
self.app_environment = GtkApplicationEnvironment()
def test_does_not_alter_app(self):
fake_app = self.getUniqueString()
app, args = self.app_environment.prepare_environment(fake_app, [])
self.assertEqual(fake_app, app)
@patch("autopilot.application._environment.os")
def test_modules_patched(self, patched_os):
patched_os.getenv.return_value = ""
fake_app = self.getUniqueString()
app, args = self.app_environment.prepare_environment(fake_app, [])
patched_os.putenv.assert_called_once_with('GTK_MODULES', ':autopilot')
@patch("autopilot.application._environment.os")
def test_modules_not_patched_twice(self, patched_os):
patched_os.getenv.return_value = "autopilot"
fake_app = self.getUniqueString()
app, args = self.app_environment.prepare_environment(fake_app, [])
self.assertFalse(patched_os.putenv.called)
class QtApplicationEnvironmentTests(TestCase):
def setUp(self):
super(QtApplicationEnvironmentTests, self).setUp()
self.app_environment = QtApplicationEnvironment()
def test_does_not_alter_app(self):
fake_app = self.getUniqueString()
app, args = self.app_environment.prepare_environment(fake_app, [])
self.assertEqual(fake_app, app)
def test_inserts_testability_with_no_args(self):
app, args = self.app_environment.prepare_environment('some_app', [])
self.assertEqual(['-testability'], args)
def test_inserts_testability_before_normal_argument(self):
app, args = self.app_environment.prepare_environment('app', ['-l'])
self.assertEqual(['-testability', '-l'], args)
def test_inserts_testability_after_qt_version_arg(self):
app, args = self.app_environment.prepare_environment(
'app',
['-qt=qt5']
)
self.assertEqual(['-qt=qt5', '-testability'], args)
def test_does_not_insert_testability_if_already_present(self):
app, args = self.app_environment.prepare_environment(
'app', ['-testability']
)
self.assertEqual(['-testability'], args)
./autopilot/tests/unit/test_config.py 0000644 0000041 0000041 00000007337 13061552435 020300 0 ustar www-data www-data # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Autopilot Functional Test Tool
# Copyright (C) 2014 Canonical
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
from unittest.mock import patch
from testtools import TestCase
from autopilot import _config as config
class TestConfigurationTests(TestCase):
def test_can_set_test_config_string(self):
token = self.getUniqueString()
config.set_configuration_string(token)
self.assertEqual(config._test_config_string, token)
def test_can_create_config_dictionary_from_empty_string(self):
d = config.ConfigDict('')
self.assertEqual(0, len(d))
def test_cannot_write_to_config_dict(self):
def set_item():
d['sdf'] = 123
d = config.ConfigDict('')
self.assertRaises(
TypeError,
set_item,
)
def test_simple_key_present(self):
d = config.ConfigDict('foo')
self.assertTrue('foo' in d)
def test_simple_key_value(self):
d = config.ConfigDict('foo')
self.assertEqual(d['foo'], '1')
def test_single_value_containing_equals_symbol(self):
d = config.ConfigDict('foo=b=a')
self.assertEqual(d['foo'], 'b=a')
def test_multiple_simple_keys(self):
d = config.ConfigDict('foo,bar')
self.assertTrue('foo' in d)
self.assertTrue('bar' in d)
self.assertEqual(2, len(d))
def test_ignores_empty_simple_keys_at_end(self):
d = config.ConfigDict('foo,,')
self.assertEqual(1, len(d))
def test_ignores_empty_simple_keys_at_start(self):
d = config.ConfigDict(',,foo')
self.assertEqual(1, len(d))
def test_ignores_empty_simple_keys_in_middle(self):
d = config.ConfigDict('foo,,bar')
self.assertEqual(2, len(d))
def test_strips_leading_whitespace_for_simple_keys(self):
d = config.ConfigDict(' foo, bar')
self.assertEqual(set(d.keys()), {'foo', 'bar'})
def test_complex_key_single(self):
d = config.ConfigDict('foo=bar')
self.assertEqual(1, len(d))
self.assertEqual(d['foo'], 'bar')
def test_complex_key_multiple(self):
d = config.ConfigDict('foo=bar,baz=foo')
self.assertEqual(d['foo'], 'bar')
self.assertEqual(d['baz'], 'foo')
def test_complex_keys_strip_leading_whitespace(self):
d = config.ConfigDict(' foo=bar, bar=baz')
self.assertEqual(set(d.keys()), {'foo', 'bar'})
def test_raises_ValueError_on_blank_key(self):
self.assertRaises(ValueError, lambda: config.ConfigDict('=,'))
def test_raises_ValueError_on_space_key(self):
self.assertRaises(ValueError, lambda: config.ConfigDict(' =,'))
def test_raises_ValueError_on_invalid_string(self):
self.assertRaises(ValueError, lambda: config.ConfigDict('f,='))
def test_iter(self):
k = config.ConfigDict('foo').keys()
self.assertEqual({'foo'}, k)
def test_get_test_configuration_uses_global_test_config_string(self):
with patch.object(config, '_test_config_string', new='foo'):
d = config.get_test_configuration()
self.assertTrue('foo' in d)
./autopilot/tests/unit/test_input.py 0000644 0000041 0000041 00000117025 13061552443 020165 0 ustar www-data www-data # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Autopilot Functional Test Tool
# Copyright (C) 2013, 2014, 2015 Canonical
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
import logging
import unittest
import testscenarios
from evdev import ecodes, uinput
from unittest.mock import ANY, call, Mock, patch
from testtools import TestCase
from testtools.matchers import Contains, raises
import autopilot.input
from autopilot import (
tests,
utilities
)
from autopilot.input import _uinput, get_center_point, Keyboard
class Empty(object):
def __repr__(self):
return ""
def make_fake_object(globalRect=False, center=False, xywh=False):
obj = Empty()
if globalRect:
obj.globalRect = (0, 0, 100, 100)
if center:
obj.center_x = 123
obj.center_y = 345
if xywh:
obj.x, obj.y, obj.w, obj.h = (100, 100, 20, 40)
return obj
class InputCenterPointTests(TestCase):
"""Tests for the input get_center_point utility."""
def test_get_center_point_raises_ValueError_on_empty_object(self):
obj = make_fake_object()
fn = lambda: get_center_point(obj)
expected_exception = ValueError(
"Object '%r' does not have any recognised position attributes" %
obj)
self.assertThat(fn, raises(expected_exception))
def test_get_center_point_works_with_globalRect(self):
obj = make_fake_object(globalRect=True)
x, y = get_center_point(obj)
self.assertEqual(50, x)
self.assertEqual(50, y)
def test_raises_ValueError_on_uniterable_globalRect(self):
obj = Empty()
obj.globalRect = 123
expected_exception = ValueError(
"Object '' has globalRect attribute, but it is not of the "
"correct type"
)
self.assertThat(
lambda: get_center_point(obj),
raises(expected_exception)
)
def test_raises_ValueError_on_too_small_globalRect(self):
obj = Empty()
obj.globalRect = (1, 2, 3)
expected_exception = ValueError(
"Object '' has globalRect attribute, but it is not of the "
"correct type"
)
self.assertThat(
lambda: get_center_point(obj),
raises(expected_exception)
)
@patch('autopilot.input._common._logger')
def test_get_center_point_logs_with_globalRect(self, mock_logger):
obj = make_fake_object(globalRect=True)
x, y = get_center_point(obj)
mock_logger.debug.assert_called_once_with(
"Moving to object's globalRect coordinates."
)
def test_get_center_point_works_with_center_points(self):
obj = make_fake_object(center=True)
x, y = get_center_point(obj)
self.assertEqual(123, x)
self.assertEqual(345, y)
@patch('autopilot.input._common._logger')
def test_get_center_point_logs_with_center_points(self, mock_logger):
obj = make_fake_object(center=True)
x, y = get_center_point(obj)
mock_logger.debug.assert_called_once_with(
"Moving to object's center_x, center_y coordinates."
)
def test_get_center_point_works_with_xywh(self):
obj = make_fake_object(xywh=True)
x, y = get_center_point(obj)
self.assertEqual(110, x)
self.assertEqual(120, y)
@patch('autopilot.input._common._logger')
def test_get_center_point_logs_with_xywh(self, mock_logger):
obj = make_fake_object(xywh=True)
x, y = get_center_point(obj)
mock_logger.debug.assert_called_once_with(
"Moving to object's center point calculated from x,y,w,h "
"attributes."
)
def test_get_center_point_raises_valueError_on_non_numerics(self):
obj = Empty()
obj.x, obj.y, obj.w, obj.h = 1, None, True, "oof"
expected_exception = ValueError(
"Object '' has x,y attribute, but they are not of the "
"correct type"
)
self.assertThat(
lambda: get_center_point(obj),
raises(expected_exception)
)
def test_get_center_point_prefers_globalRect(self):
obj = make_fake_object(globalRect=True, center=True, xywh=True)
x, y = get_center_point(obj)
self.assertEqual(50, x)
self.assertEqual(50, y)
def test_get_center_point_prefers_center_points(self):
obj = make_fake_object(globalRect=False, center=True, xywh=True)
x, y = get_center_point(obj)
self.assertEqual(123, x)
self.assertEqual(345, y)
class UInputTestCase(TestCase):
"""Tests for the global methods of the uinput module."""
def test_create_touch_device_must_print_deprecation_message(self):
with patch('autopilot.utilities.logger') as patched_log:
with patch('autopilot.input._uinput.UInput'):
_uinput.create_touch_device('dummy', 'dummy')
self.assertThat(
patched_log.warning.call_args[0][0],
Contains(
"This function is deprecated. Please use 'the Touch class to "
"instantiate a device object' instead."
)
)
class UInputKeyboardDeviceTestCase(TestCase):
"""Test the integration with evdev.UInput for the keyboard."""
_PRESS_VALUE = 1
_RELEASE_VALUE = 0
def get_keyboard_with_mocked_backend(self):
keyboard = _uinput._UInputKeyboardDevice(device_class=Mock)
keyboard._device.mock_add_spec(uinput.UInput, spec_set=True)
return keyboard
def assert_key_press_emitted_write_and_syn(self, keyboard, key):
self.assert_emitted_write_and_syn(keyboard, key, self._PRESS_VALUE)
def assert_key_release_emitted_write_and_syn(self, keyboard, key):
self.assert_emitted_write_and_syn(keyboard, key, self._RELEASE_VALUE)
def assert_emitted_write_and_syn(self, keyboard, key, value):
key_ecode = ecodes.ecodes.get(key)
expected_calls = [
call.write(ecodes.EV_KEY, key_ecode, value),
call.syn()
]
self.assertEqual(expected_calls, keyboard._device.mock_calls)
def press_key_and_reset_mock(self, keyboard, key):
keyboard.press(key)
keyboard._device.reset_mock()
def test_press_key_must_emit_write_and_syn(self):
keyboard = self.get_keyboard_with_mocked_backend()
keyboard.press('KEY_A')
self.assert_key_press_emitted_write_and_syn(keyboard, 'KEY_A')
def test_press_key_must_append_leading_string(self):
keyboard = self.get_keyboard_with_mocked_backend()
keyboard.press('A')
self.assert_key_press_emitted_write_and_syn(keyboard, 'KEY_A')
def test_press_key_must_ignore_case(self):
keyboard = self.get_keyboard_with_mocked_backend()
keyboard.press('a')
self.assert_key_press_emitted_write_and_syn(keyboard, 'KEY_A')
def test_press_unexisting_key_must_raise_error(self):
keyboard = self.get_keyboard_with_mocked_backend()
error = self.assertRaises(
ValueError, keyboard.press, 'unexisting')
self.assertEqual('Unknown key name: unexisting.', str(error))
def test_release_not_pressed_key_must_raise_error(self):
keyboard = self.get_keyboard_with_mocked_backend()
error = self.assertRaises(
ValueError, keyboard.release, 'A')
self.assertEqual("Key 'A' not pressed.", str(error))
def test_release_key_must_emit_write_and_syn(self):
keyboard = self.get_keyboard_with_mocked_backend()
self.press_key_and_reset_mock(keyboard, 'KEY_A')
keyboard.release('KEY_A')
self.assert_key_release_emitted_write_and_syn(keyboard, 'KEY_A')
def test_release_key_must_append_leading_string(self):
keyboard = self.get_keyboard_with_mocked_backend()
self.press_key_and_reset_mock(keyboard, 'KEY_A')
keyboard.release('A')
self.assert_key_release_emitted_write_and_syn(keyboard, 'KEY_A')
def test_release_key_must_ignore_case(self):
keyboard = self.get_keyboard_with_mocked_backend()
self.press_key_and_reset_mock(keyboard, 'KEY_A')
keyboard.release('a')
self.assert_key_release_emitted_write_and_syn(keyboard, 'KEY_A')
def test_release_unexisting_key_must_raise_error(self):
keyboard = self.get_keyboard_with_mocked_backend()
error = self.assertRaises(
ValueError, keyboard.release, 'unexisting')
self.assertEqual('Unknown key name: unexisting.', str(error))
def test_release_pressed_keys_without_pressed_keys_must_do_nothing(self):
keyboard = self.get_keyboard_with_mocked_backend()
keyboard.release_pressed_keys()
self.assertEqual([], keyboard._device.mock_calls)
def test_release_pressed_keys_with_pressed_keys(self):
expected_calls = [
call.write(
ecodes.EV_KEY, ecodes.ecodes.get('KEY_A'),
self._RELEASE_VALUE),
call.syn(),
call.write(
ecodes.EV_KEY, ecodes.ecodes.get('KEY_B'),
self._RELEASE_VALUE),
call.syn()
]
keyboard = self.get_keyboard_with_mocked_backend()
self.press_key_and_reset_mock(keyboard, 'KEY_A')
self.press_key_and_reset_mock(keyboard, 'KEY_B')
keyboard.release_pressed_keys()
self.assertEqual(expected_calls, keyboard._device.mock_calls)
def test_release_pressed_keys_already_released(self):
expected_calls = []
keyboard = self.get_keyboard_with_mocked_backend()
keyboard.press('KEY_A')
keyboard.release_pressed_keys()
keyboard._device.reset_mock()
keyboard.release_pressed_keys()
self.assertEqual(expected_calls, keyboard._device.mock_calls)
class UInputKeyboardTestCase(testscenarios.TestWithScenarios, TestCase):
"""Test UInput Keyboard helper for autopilot tests."""
scenarios = [
('single key', dict(keys='a', expected_calls_args=['a'])),
('upper-case letter', dict(
keys='A', expected_calls_args=['KEY_LEFTSHIFT', 'A'])),
('key combination', dict(
keys='a+b', expected_calls_args=['a', 'b']))
]
def setUp(self):
super(UInputKeyboardTestCase, self).setUp()
# Return to the original device after the test.
self.addCleanup(self.set_keyboard_device, _uinput.Keyboard._device)
# Mock the sleeps so we don't have to spend time actually sleeping.
self.addCleanup(utilities.sleep.disable_mock)
utilities.sleep.enable_mock()
def set_keyboard_device(self, device):
_uinput.Keyboard._device = device
def get_keyboard_with_mocked_backend(self):
_uinput.Keyboard._device = None
keyboard = _uinput.Keyboard(device_class=Mock)
keyboard._device.mock_add_spec(
_uinput._UInputKeyboardDevice, spec_set=True)
return keyboard
def test_press_must_put_press_device_keys(self):
expected_calls = [
call.press(arg) for arg in self.expected_calls_args]
keyboard = self.get_keyboard_with_mocked_backend()
keyboard.press(self.keys)
self.assertEqual(expected_calls, keyboard._device.mock_calls)
def test_release_must_release_device_keys(self):
keyboard = self.get_keyboard_with_mocked_backend()
keyboard.press(self.keys)
keyboard._device.reset_mock()
expected_calls = [
call.release(arg) for arg in
reversed(self.expected_calls_args)]
keyboard.release(self.keys)
self.assertEqual(
expected_calls, keyboard._device.mock_calls)
def test_press_and_release_must_press_device_keys(self):
expected_press_calls = [
call.press(arg) for arg in self.expected_calls_args]
ignored_calls = [
ANY for arg in self.expected_calls_args]
keyboard = self.get_keyboard_with_mocked_backend()
keyboard.press_and_release(self.keys)
self.assertEqual(
expected_press_calls + ignored_calls,
keyboard._device.mock_calls)
def test_press_and_release_must_release_device_keys_in_reverse_order(
self):
ignored_calls = [
ANY for arg in self.expected_calls_args]
expected_release_calls = [
call.release(arg) for arg in
reversed(self.expected_calls_args)]
keyboard = self.get_keyboard_with_mocked_backend()
keyboard.press_and_release(self.keys)
self.assertEqual(
ignored_calls + expected_release_calls,
keyboard._device.mock_calls)
def test_on_test_end_without_device_must_do_nothing(self):
_uinput.Keyboard._device = None
# This will fail if it calls anything from the device, as it's None.
_uinput.Keyboard.on_test_end(self)
def test_on_test_end_with_device_must_release_pressed_keys(self):
keyboard = self.get_keyboard_with_mocked_backend()
_uinput.Keyboard.on_test_end(self)
self.assertEqual(
[call.release_pressed_keys()], keyboard._device.mock_calls)
class TouchEventsTestCase(TestCase):
def assert_expected_ev_abs(self, res_x, res_y, actual_ev_abs):
expected_ev_abs = [
(ecodes.ABS_X, (0, res_x, 0, 0)),
(ecodes.ABS_Y, (0, res_y, 0, 0)),
(ecodes.ABS_PRESSURE, (0, 65535, 0, 0)),
(ecodes.ABS_MT_POSITION_X, (0, res_x, 0, 0)),
(ecodes.ABS_MT_POSITION_Y, (0, res_y, 0, 0)),
(ecodes.ABS_MT_TOUCH_MAJOR, (0, 30, 0, 0)),
(ecodes.ABS_MT_TRACKING_ID, (0, 65535, 0, 0)),
(ecodes.ABS_MT_PRESSURE, (0, 255, 0, 0)),
(ecodes.ABS_MT_SLOT, (0, 9, 0, 0))
]
self.assertEqual(expected_ev_abs, actual_ev_abs)
def test_get_touch_events_without_args_must_use_system_resolution(self):
with patch.object(
_uinput, '_get_system_resolution', spec_set=True,
autospec=True) as mock_system_resolution:
mock_system_resolution.return_value = (
'system_res_x', 'system_res_y')
events = _uinput._get_touch_events()
ev_abs = events.get(ecodes.EV_ABS)
self.assert_expected_ev_abs('system_res_x', 'system_res_y', ev_abs)
def test_get_touch_events_with_args_must_use_given_resulution(self):
events = _uinput._get_touch_events('given_res_x', 'given_res_y')
ev_abs = events.get(ecodes.EV_ABS)
self.assert_expected_ev_abs('given_res_x', 'given_res_y', ev_abs)
class UInputTouchDeviceTestCase(tests.LogHandlerTestCase):
"""Test the integration with evdev.UInput for the touch device."""
def setUp(self):
super(UInputTouchDeviceTestCase, self).setUp()
self._number_of_slots = 9
# Return to the original device after the test.
self.addCleanup(
self.set_mouse_device,
_uinput._UInputTouchDevice._device,
_uinput._UInputTouchDevice._touch_fingers_in_use,
_uinput._UInputTouchDevice._last_tracking_id)
# Always start the tests without fingers in use.
_uinput._UInputTouchDevice._touch_fingers_in_use = []
_uinput._UInputTouchDevice._last_tracking_id = 0
def set_mouse_device(
self, device, touch_fingers_in_use, last_tracking_id):
_uinput._UInputTouchDevice._device = device
_uinput._UInputTouchDevice._touch_fingers_in_use = touch_fingers_in_use
_uinput._UInputTouchDevice._last_tracking_id = last_tracking_id
def get_touch_with_mocked_backend(self):
dummy_x_resolution = 100
dummy_y_resolution = 100
_uinput._UInputTouchDevice._device = None
touch = _uinput._UInputTouchDevice(
res_x=dummy_x_resolution, res_y=dummy_y_resolution,
device_class=Mock)
touch._device.mock_add_spec(uinput.UInput, spec_set=True)
return touch
def assert_finger_down_emitted_write_and_syn(
self, touch, slot, tracking_id, x, y):
press_value = 1
expected_calls = [
call.write(ecodes.EV_ABS, ecodes.ABS_MT_SLOT, slot),
call.write(
ecodes.EV_ABS, ecodes.ABS_MT_TRACKING_ID, tracking_id),
call.write(
ecodes.EV_KEY, ecodes.BTN_TOUCH, press_value),
call.write(ecodes.EV_ABS, ecodes.ABS_MT_POSITION_X, x),
call.write(ecodes.EV_ABS, ecodes.ABS_MT_POSITION_Y, y),
call.write(ecodes.EV_ABS, ecodes.ABS_MT_PRESSURE, 400),
call.syn()
]
self.assertEqual(expected_calls, touch._device.mock_calls)
def assert_finger_move_emitted_write_and_syn(self, touch, slot, x, y):
expected_calls = [
call.write(ecodes.EV_ABS, ecodes.ABS_MT_SLOT, slot),
call.write(ecodes.EV_ABS, ecodes.ABS_MT_POSITION_X, x),
call.write(ecodes.EV_ABS, ecodes.ABS_MT_POSITION_Y, y),
call.syn()
]
self.assertEqual(expected_calls, touch._device.mock_calls)
def assert_finger_up_emitted_write_and_syn(self, touch, slot):
lift_tracking_id = -1
release_value = 0
expected_calls = [
call.write(ecodes.EV_ABS, ecodes.ABS_MT_SLOT, slot),
call.write(
ecodes.EV_ABS, ecodes.ABS_MT_TRACKING_ID, lift_tracking_id),
call.write(
ecodes.EV_KEY, ecodes.BTN_TOUCH, release_value),
call.syn()
]
self.assertEqual(expected_calls, touch._device.mock_calls)
def test_finger_down_must_use_free_slot(self):
for slot in range(self._number_of_slots):
touch = self.get_touch_with_mocked_backend()
touch.finger_down(0, 0)
self.assert_finger_down_emitted_write_and_syn(
touch, slot=slot, tracking_id=ANY, x=0, y=0)
def test_finger_down_without_free_slots_must_raise_error(self):
# Claim all the available slots.
for slot in range(self._number_of_slots):
touch = self.get_touch_with_mocked_backend()
touch.finger_down(0, 0)
touch = self.get_touch_with_mocked_backend()
# Try to use one more.
error = self.assertRaises(RuntimeError, touch.finger_down, 11, 11)
self.assertEqual(
'All available fingers have been used already.', str(error))
def test_finger_down_must_use_unique_tracking_id(self):
for number in range(self._number_of_slots):
touch = self.get_touch_with_mocked_backend()
touch.finger_down(0, 0)
self.assert_finger_down_emitted_write_and_syn(
touch, slot=ANY, tracking_id=number + 1, x=0, y=0)
def test_finger_down_must_not_reuse_tracking_ids(self):
# Claim and release all the available slots once.
for number in range(self._number_of_slots):
touch = self.get_touch_with_mocked_backend()
touch.finger_down(0, 0)
touch.finger_up()
touch = self.get_touch_with_mocked_backend()
touch.finger_down(12, 12)
self.assert_finger_down_emitted_write_and_syn(
touch, slot=ANY, tracking_id=number + 2, x=12, y=12)
def test_finger_down_with_finger_pressed_must_raise_error(self):
touch = self.get_touch_with_mocked_backend()
touch.finger_down(0, 0)
error = self.assertRaises(RuntimeError, touch.finger_down, 0, 0)
self.assertEqual(
"Cannot press finger: it's already pressed.", str(error))
def test_finger_move_without_finger_pressed_must_raise_error(self):
touch = self.get_touch_with_mocked_backend()
error = self.assertRaises(RuntimeError, touch.finger_move, 10, 10)
self.assertEqual(
'Attempting to move without finger being down.', str(error))
def test_finger_move_must_use_assigned_slot(self):
for slot in range(self._number_of_slots):
touch = self.get_touch_with_mocked_backend()
touch.finger_down(0, 0)
touch._device.reset_mock()
touch.finger_move(10, 10)
self.assert_finger_move_emitted_write_and_syn(
touch, slot=slot, x=10, y=10)
def test_finger_move_must_reuse_assigned_slot(self):
first_slot = 0
touch = self.get_touch_with_mocked_backend()
touch.finger_down(1, 1)
touch._device.reset_mock()
touch.finger_move(13, 13)
self.assert_finger_move_emitted_write_and_syn(
touch, slot=first_slot, x=13, y=13)
touch._device.reset_mock()
touch.finger_move(14, 14)
self.assert_finger_move_emitted_write_and_syn(
touch, slot=first_slot, x=14, y=14)
def test_finger_move_must_log_position_at_debug_level(self):
self.root_logger.setLevel(logging.DEBUG)
touch = self.get_touch_with_mocked_backend()
touch.finger_down(0, 0)
touch.finger_move(10, 10)
self.assertLogLevelContains(
'DEBUG',
"Moving pointing 'finger' to position 10,10."
)
self.assertLogLevelContains(
'DEBUG',
"The pointing 'finger' is now at position 10,10."
)
def test_finger_up_without_finger_pressed_must_raise_error(self):
touch = self.get_touch_with_mocked_backend()
error = self.assertRaises(RuntimeError, touch.finger_up)
self.assertEqual(
"Cannot release finger: it's not pressed.", str(error))
def test_finger_up_must_use_assigned_slot(self):
fingers = []
for slot in range(self._number_of_slots):
touch = self.get_touch_with_mocked_backend()
touch.finger_down(0, 0)
touch._device.reset_mock()
fingers.append(touch)
for slot, touch in enumerate(fingers):
touch.finger_up()
self.assert_finger_up_emitted_write_and_syn(touch, slot=slot)
touch._device.reset_mock()
def test_finger_up_must_release_slot(self):
fingers = []
# Claim all the available slots.
for slot in range(self._number_of_slots):
touch = self.get_touch_with_mocked_backend()
touch.finger_down(0, 0)
fingers.append(touch)
slot_to_reuse = 3
fingers[slot_to_reuse].finger_up()
touch = self.get_touch_with_mocked_backend()
# Try to use one more.
touch.finger_down(15, 15)
self.assert_finger_down_emitted_write_and_syn(
touch, slot=slot_to_reuse, tracking_id=ANY, x=15, y=15)
def test_device_with_finger_down_must_be_pressed(self):
touch = self.get_touch_with_mocked_backend()
touch.finger_down(0, 0)
self.assertTrue(touch.pressed)
def test_device_without_finger_down_must_not_be_pressed(self):
touch = self.get_touch_with_mocked_backend()
self.assertFalse(touch.pressed)
def test_device_after_finger_up_must_not_be_pressed(self):
touch = self.get_touch_with_mocked_backend()
touch.finger_down(0, 0)
touch.finger_up()
self.assertFalse(touch.pressed)
def test_press_other_device_must_not_press_all_of_them(self):
other_touch = self.get_touch_with_mocked_backend()
other_touch.finger_down(0, 0)
touch = self.get_touch_with_mocked_backend()
self.assertFalse(touch.pressed)
class UInputTouchBaseTestCase(TestCase):
def setUp(self):
super(UInputTouchBaseTestCase, self).setUp()
# Mock the sleeps so we don't have to spend time actually sleeping.
self.addCleanup(utilities.sleep.disable_mock)
utilities.sleep.enable_mock()
def get_touch_with_mocked_backend(self):
touch = _uinput.Touch(device_class=Mock)
touch._device.mock_add_spec(_uinput._UInputTouchDevice)
return touch
class UInputTouchFingerCoordinatesTestCase(
testscenarios.TestWithScenarios, UInputTouchBaseTestCase):
TEST_X_DESTINATION = 10
TEST_Y_DESTINATION = 10
scenarios = [
('tap', {
'method': 'tap',
'args': (TEST_X_DESTINATION, TEST_Y_DESTINATION)
}),
('press', {
'method': 'press',
'args': (TEST_X_DESTINATION, TEST_Y_DESTINATION)
}),
('move', {
'method': 'move',
'args': (TEST_X_DESTINATION, TEST_Y_DESTINATION)
}),
('drag', {
'method': 'drag',
'args': (0, 0, TEST_X_DESTINATION, TEST_Y_DESTINATION)
})
]
def call_scenario_method(self, object_, method, *args):
getattr(object_, method)(*self.args)
def test_method_must_update_finger_coordinates(self):
touch = self.get_touch_with_mocked_backend()
self.call_scenario_method(touch, self.method, *self.args)
self.assertEqual(touch.x, self.TEST_X_DESTINATION)
self.assertEqual(touch.y, self.TEST_Y_DESTINATION)
class UInputTouchTestCase(UInputTouchBaseTestCase):
"""Test UInput Touch helper for autopilot tests."""
def test_initial_coordinates_must_be_zero(self):
touch = self.get_touch_with_mocked_backend()
self.assertEqual(touch.x, 0)
self.assertEqual(touch.y, 0)
def test_tap_must_put_finger_down_then_sleep_and_then_put_finger_up(self):
expected_calls = [
call.finger_down(0, 0),
call.sleep(ANY),
call.finger_up()
]
touch = self.get_touch_with_mocked_backend()
with patch('autopilot.input._uinput.sleep') as mock_sleep:
touch._device.attach_mock(mock_sleep, 'sleep')
touch.tap(0, 0)
self.assertEqual(expected_calls, touch._device.mock_calls)
def test_tap_object_must_put_finger_down_and_then_up_on_the_center(self):
object_ = make_fake_object(center=True)
expected_calls = [
call.finger_down(object_.center_x, object_.center_y),
call.finger_up()
]
touch = self.get_touch_with_mocked_backend()
touch.tap_object(object_)
self.assertEqual(expected_calls, touch._device.mock_calls)
def test_press_must_put_finger_down(self):
expected_calls = [call.finger_down(0, 0)]
touch = self.get_touch_with_mocked_backend()
touch.press(0, 0)
self.assertEqual(expected_calls, touch._device.mock_calls)
def test_release_must_put_finger_up(self):
expected_calls = [call.finger_up()]
touch = self.get_touch_with_mocked_backend()
touch.release()
self.assertEqual(expected_calls, touch._device.mock_calls)
def test_move_must_move_finger(self):
expected_calls = [call.finger_move(10, 10)]
touch = self.get_touch_with_mocked_backend()
touch.move(10, 10)
self.assertEqual(expected_calls, touch._device.mock_calls)
def test_move_must_move_with_specified_rate(self):
expected_calls = [
call.finger_move(5, 5),
call.finger_move(10, 10),
call.finger_move(15, 15),
]
touch = self.get_touch_with_mocked_backend()
touch.move(15, 15, rate=5)
self.assertEqual(
expected_calls, touch._device.mock_calls)
def test_move_without_rate_must_use_default(self):
expected_calls = [
call.finger_move(10, 10),
call.finger_move(20, 20),
]
touch = self.get_touch_with_mocked_backend()
touch.move(20, 20)
self.assertEqual(
expected_calls, touch._device.mock_calls)
def test_move_to_same_place_must_not_move(self):
expected_calls = []
touch = self.get_touch_with_mocked_backend()
touch.move(0, 0)
self.assertEqual(expected_calls, touch._device.mock_calls)
def test_drag_must_call_finger_down_move_and_up(self):
expected_calls = [
call.finger_down(0, 0),
call.finger_move(10, 10),
call.finger_up()
]
touch = self.get_touch_with_mocked_backend()
touch.drag(0, 0, 10, 10)
self.assertEqual(expected_calls, touch._device.mock_calls)
def test_tap_without_press_duration_must_sleep_default_time(self):
touch = self.get_touch_with_mocked_backend()
touch.tap(0, 0)
self.assertEqual(utilities.sleep.total_time_slept(), 0.1)
def test_tap_with_press_duration_must_sleep_specified_time(self):
touch = self.get_touch_with_mocked_backend()
touch.tap(0, 0, press_duration=10)
self.assertEqual(utilities.sleep.total_time_slept(), 10)
def test_tap_object_without_duration_must_call_tap_with_default_time(self):
object_ = make_fake_object(center=True)
touch = self.get_touch_with_mocked_backend()
with patch.object(touch, 'tap') as mock_tap:
touch.tap_object(object_)
mock_tap.assert_called_once_with(
object_.center_x,
object_.center_y,
press_duration=0.1,
time_between_events=0.1
)
def test_tap_object_with_duration_must_call_tap_with_specified_time(self):
object_ = make_fake_object(center=True)
touch = self.get_touch_with_mocked_backend()
with patch.object(touch, 'tap') as mock_tap:
touch.tap_object(
object_,
press_duration=10
)
mock_tap.assert_called_once_with(
object_.center_x,
object_.center_y,
press_duration=10,
time_between_events=0.1
)
class MultipleUInputTouchBackend(_uinput._UInputTouchDevice):
def __init__(self, res_x=100, res_y=100, device_class=Mock):
super(MultipleUInputTouchBackend, self).__init__(
res_x, res_y, device_class)
class MultipleUInputTouchTestCase(TestCase):
def setUp(self):
super(MultipleUInputTouchTestCase, self).setUp()
# Return to the original device after the test.
self.addCleanup(
self.set_mouse_device,
_uinput._UInputTouchDevice._device,
_uinput._UInputTouchDevice._touch_fingers_in_use,
_uinput._UInputTouchDevice._last_tracking_id)
def set_mouse_device(
self, device, touch_fingers_in_use, last_tracking_id):
_uinput._UInputTouchDevice._device = device
_uinput._UInputTouchDevice._touch_fingers_in_use = touch_fingers_in_use
_uinput._UInputTouchDevice._last_tracking_id = last_tracking_id
def test_press_other_device_must_not_press_all_of_them(self):
finger1 = _uinput.Touch(device_class=MultipleUInputTouchBackend)
finger2 = _uinput.Touch(device_class=MultipleUInputTouchBackend)
finger1.press(0, 0)
self.addCleanup(finger1.release)
self.assertFalse(finger2.pressed)
class MoveWithAnimationUInputTouchTestCase(
testscenarios.TestWithScenarios, TestCase):
scenarios = [
('move to top', dict(
start_x=50, start_y=50, stop_x=50, stop_y=30,
expected_moves=[call.finger_move(50, 40),
call.finger_move(50, 30)])),
('move to bottom', dict(
start_x=50, start_y=50, stop_x=50, stop_y=70,
expected_moves=[call.finger_move(50, 60),
call.finger_move(50, 70)])),
('move to left', dict(
start_x=50, start_y=50, stop_x=30, stop_y=50,
expected_moves=[call.finger_move(40, 50),
call.finger_move(30, 50)])),
('move to right', dict(
start_x=50, start_y=50, stop_x=70, stop_y=50,
expected_moves=[call.finger_move(60, 50),
call.finger_move(70, 50)])),
('move to top-left', dict(
start_x=50, start_y=50, stop_x=30, stop_y=30,
expected_moves=[call.finger_move(40, 40),
call.finger_move(30, 30)])),
('move to top-right', dict(
start_x=50, start_y=50, stop_x=70, stop_y=30,
expected_moves=[call.finger_move(60, 40),
call.finger_move(70, 30)])),
('move to bottom-left', dict(
start_x=50, start_y=50, stop_x=30, stop_y=70,
expected_moves=[call.finger_move(40, 60),
call.finger_move(30, 70)])),
('move to bottom-right', dict(
start_x=50, start_y=50, stop_x=70, stop_y=70,
expected_moves=[call.finger_move(60, 60),
call.finger_move(70, 70)])),
('move less than rate', dict(
start_x=50, start_y=50, stop_x=55, stop_y=55,
expected_moves=[call.finger_move(55, 55)])),
('move with last move less than rate', dict(
start_x=50, start_y=50, stop_x=65, stop_y=65,
expected_moves=[call.finger_move(60, 60),
call.finger_move(65, 65)])),
]
def setUp(self):
super(MoveWithAnimationUInputTouchTestCase, self).setUp()
# Mock the sleeps so we don't have to spend time actually sleeping.
self.addCleanup(utilities.sleep.disable_mock)
utilities.sleep.enable_mock()
def get_touch_with_mocked_backend(self):
touch = _uinput.Touch(device_class=Mock)
touch._device.mock_add_spec(
_uinput._UInputTouchDevice, spec_set=True)
return touch
def test_drag_moves(self):
touch = self.get_touch_with_mocked_backend()
touch.press(self.start_x, self.start_y)
touch.move(self.stop_x, self.stop_y)
expected_calls = (
[call.finger_down(self.start_x, self.start_y)] +
self.expected_moves)
self.assertEqual(
expected_calls, touch._device.mock_calls)
class PointerWithTouchBackendTestCase(TestCase):
def get_pointer_with_touch_backend_with_mock_device(self):
touch = _uinput.Touch(device_class=Mock)
touch._device.mock_add_spec(
_uinput._UInputTouchDevice, spec_set=True)
pointer = autopilot.input.Pointer(touch)
return pointer
def test_initial_coordinates_must_be_zero(self):
pointer = self.get_pointer_with_touch_backend_with_mock_device()
self.assertEqual(pointer.x, 0)
self.assertEqual(pointer.y, 0)
def test_drag_must_call_move_with_animation(self):
test_rate = 2
test_time_between_events = 1
test_destination_x = 20
test_destination_y = 20
pointer = self.get_pointer_with_touch_backend_with_mock_device()
with patch.object(pointer._device, 'move') as mock_move:
pointer.drag(
0, 0,
test_destination_x, test_destination_y,
rate=test_rate, time_between_events=test_time_between_events)
mock_move.assert_called_once_with(
test_destination_x, test_destination_y,
animate=True,
rate=test_rate, time_between_events=test_time_between_events)
def test_drag_with_rate(self):
pointer = self.get_pointer_with_touch_backend_with_mock_device()
with patch.object(pointer._device, 'drag') as mock_drag:
pointer.drag(0, 0, 20, 20, rate='test')
mock_drag.assert_called_once_with(
0, 0, 20, 20, rate='test', time_between_events=0.01)
def test_drag_with_time_between_events(self):
pointer = self.get_pointer_with_touch_backend_with_mock_device()
with patch.object(pointer._device, 'drag') as mock_drag:
pointer.drag(0, 0, 20, 20, time_between_events='test')
mock_drag.assert_called_once_with(
0, 0, 20, 20, rate=10, time_between_events='test')
def test_drag_with_default_parameters(self):
pointer = self.get_pointer_with_touch_backend_with_mock_device()
with patch.object(pointer._device, 'drag') as mock_drag:
pointer.drag(0, 0, 20, 20)
mock_drag.assert_called_once_with(
0, 0, 20, 20, rate=10, time_between_events=0.01)
def test_click_with_default_press_duration(self):
pointer = self.get_pointer_with_touch_backend_with_mock_device()
with patch.object(pointer._device, 'tap') as mock_tap:
pointer.click(1)
mock_tap.assert_called_once_with(
0, 0, press_duration=0.1, time_between_events=0.1)
def test_press_with_specified_press_duration(self):
pointer = self.get_pointer_with_touch_backend_with_mock_device()
with patch.object(pointer._device, 'tap') as mock_tap:
pointer.click(1, press_duration=10)
mock_tap.assert_called_once_with(
0, 0, press_duration=10, time_between_events=0.1)
def test_not_pressed_move_must_not_move_pointing_figer(self):
"""Test for moving the finger when it is not pressed.
The move method on the pointer class must update the finger coordinates
but it must not execute a move on the device.
"""
test_x_destination = 20
test_y_destination = 20
pointer = self.get_pointer_with_touch_backend_with_mock_device()
pointer.move(10, 10)
pointer._device._device.pressed = False
with patch.object(pointer._device._device, 'finger_move') as mock_move:
pointer.move(test_x_destination, test_y_destination)
self.assertFalse(mock_move.called)
self.assertEqual(pointer.x, test_x_destination)
self.assertEqual(pointer.y, test_y_destination)
def test_pressed_move_must_move_pointing_finger(self):
test_x_destination = 20
test_y_destination = 20
pointer = self.get_pointer_with_touch_backend_with_mock_device()
pointer.move(10, 10)
pointer._device._device.pressed = True
with patch.object(pointer._device._device, 'finger_move') as mock_move:
pointer.move(test_x_destination, test_y_destination)
mock_move.assert_called_once_with(20, 20)
self.assertEqual(pointer.x, test_x_destination)
self.assertEqual(pointer.y, test_y_destination)
def test_press_must_put_finger_down_at_last_move_position(self):
pointer = self.get_pointer_with_touch_backend_with_mock_device()
pointer.move(10, 10)
pointer.press()
pointer._device._device.finger_down.assert_called_once_with(10, 10)
class UInputPowerButtonTestCase(TestCase):
def get_mock_hardware_keys_device(self):
power_button = _uinput.UInputHardwareKeysDevice(device_class=Mock)
power_button._device.mock_add_spec(uinput.UInput, spec_set=True)
return power_button
def assert_power_button_press_release_emitted_write_and_sync(self, calls):
expected_calls = [
call.write(ecodes.EV_KEY, ecodes.KEY_POWER, 1),
call.write(ecodes.EV_KEY, ecodes.KEY_POWER, 0),
call.syn(),
]
self.assertEquals(expected_calls, calls)
def test_power_button_press_release_emitted_write_and_sync(self):
device = self.get_mock_hardware_keys_device()
device.press_and_release_power_button()
self.assert_power_button_press_release_emitted_write_and_sync(
device._device.mock_calls
)
class KeyboardTestCase(unittest.TestCase):
@patch('autopilot.input._pick_backend')
def test_input_backends_default_order(self, pick_backend):
k = Keyboard()
k.create()
backends = list(pick_backend.call_args[0][0].items())
self.assertTrue(backends[0][0] == 'X11')
self.assertTrue(backends[1][0] == 'OSK')
self.assertTrue(backends[2][0] == 'UInput')
./autopilot/tests/unit/test_custom_exceptions.py 0000644 0000041 0000041 00000003441 13061552435 022576 0 ustar www-data www-data # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Autopilot Functional Test Tool
# Copyright (C) 2012-2013 Canonical
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
from testtools import TestCase
from testtools.matchers import Equals, IsInstance
from autopilot.exceptions import BackendException
class BackendExceptionTests(TestCase):
def test_must_wrap_exception(self):
"""BackendException must be able to wrap another exception instance."""
err = BackendException(RuntimeError("Hello World"))
self.assertThat(err.original_exception, IsInstance(RuntimeError))
self.assertThat(str(err.original_exception), Equals("Hello World"))
def test_dunder_str(self):
err = BackendException(RuntimeError("Hello World"))
self.assertThat(
str(err), Equals(
"Error while initialising backend. Original exception was: "
"Hello World"))
def test_dunder_repr(self):
err = BackendException(RuntimeError("Hello World"))
self.assertThat(
repr(err), Equals(
"BackendException('Error while initialising backend. Original "
"exception was: Hello World',)"))
./autopilot/tests/unit/test_process.py 0000644 0000041 0000041 00000005610 13061552435 020501 0 ustar www-data www-data # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Autopilot Functional Test Tool
# Copyright (C) 2013 Canonical
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
from gi.repository import GLib
from unittest.mock import Mock, patch
from testtools import TestCase, skipIf
from testtools.matchers import (
Not,
Raises,
)
from autopilot.platform import model
if model() == "Desktop":
import autopilot.process._bamf as _b
from autopilot.process._bamf import _launch_application
@skipIf(model() != "Desktop", "Requires BAMF framework")
class ProcessBamfTests(TestCase):
def test_launch_application_attempts_launch_uris_as_manager_first(self):
"""_launch_application must attempt to use launch_uris_as_manager
before trying to use launch_uris.
"""
with patch.object(_b.Gio.DesktopAppInfo, 'new') as process:
process.launch_uris_as_manager.called_once_with(
[],
None,
GLib.SpawnFlags.SEARCH_PATH
| GLib.SpawnFlags.STDOUT_TO_DEV_NULL,
None,
None,
None,
None
)
self.assertFalse(process.launch_uris.called)
def test_launch_application_falls_back_to_earlier_ver_uri_call(self):
"""_launch_application must fallback to using launch_uris if the call
to launch_uris_as_manager fails due to being an older version.
"""
test_desktop_file = self.getUniqueString()
process = Mock()
process.launch_uris_as_manager.side_effect = TypeError(
"Argument 2 does not allow None as a value"
)
with patch.object(_b.Gio.DesktopAppInfo, 'new', return_value=process):
_launch_application(test_desktop_file, [])
process.launch_uris.called_once_with([], None)
def test_launch_application_doesnt_raise(self):
test_desktop_file = self.getUniqueString()
process = Mock()
process.launch_uris_as_manager.side_effect = TypeError(
"Argument 2 does not allow None as a value"
)
with patch.object(_b.Gio.DesktopAppInfo, 'new', return_value=process):
self.assertThat(
lambda: _launch_application(test_desktop_file, []),
Not(Raises())
)
./autopilot/tests/unit/test_introspection.py 0000644 0000041 0000041 00000014003 13061552435 021717 0 ustar www-data www-data # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Autopilot Functional Test Tool
# Copyright (C) 2013 Canonical
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
from dbus import String
from unittest.mock import Mock
from testtools import TestCase
from testtools.matchers import (
Equals,
IsInstance,
raises,
)
import autopilot.introspection._search as _s
from autopilot.introspection.qt import QtObjectProxyMixin
import autopilot.introspection as _i
class GetDetailsFromStateDataTests(TestCase):
fake_state_data = (String('/some/path'), dict(foo=123))
def test_returns_classname(self):
class_name, _, _ = _s._get_details_from_state_data(
self.fake_state_data
)
self.assertThat(class_name, Equals('path'))
def test_returns_path(self):
_, path, _ = _s._get_details_from_state_data(self.fake_state_data)
self.assertThat(path, Equals(b'/some/path'))
def test_returned_path_is_bytestring(self):
_, path, _ = _s._get_details_from_state_data(self.fake_state_data)
self.assertThat(path, IsInstance(type(b'')))
def test_returns_state_dict(self):
_, _, state = _s._get_details_from_state_data(self.fake_state_data)
self.assertThat(state, Equals(dict(foo=123)))
class FooTests(TestCase):
fake_data_with_ap_interface = """
"""
fake_data_with_ap_and_qt_interfaces = """
"""
def test_raises_RuntimeError_when_no_interface_is_found(self):
self.assertThat(
lambda: _s._get_proxy_bases_from_introspection_xml(""),
raises(RuntimeError("Could not find Autopilot interface."))
)
def test_returns_ApplicationProxyObject_claws_for_base_interface(self):
self.assertThat(
_s._get_proxy_bases_from_introspection_xml(
self.fake_data_with_ap_interface
),
Equals(())
)
def test_returns_both_base_and_qt_interface(self):
self.assertThat(
_s._get_proxy_bases_from_introspection_xml(
self.fake_data_with_ap_and_qt_interfaces
),
Equals((QtObjectProxyMixin,))
)
class ExtendProxyBasesWithEmulatorBaseTests(TestCase):
def test_default_emulator_base_name(self):
bases = _s._extend_proxy_bases_with_emulator_base(tuple(), None)
self.assertThat(len(bases), Equals(1))
self.assertThat(bases[0].__name__, Equals("DefaultEmulatorBase"))
self.assertThat(bases[0].__bases__[0], Equals(_i.CustomEmulatorBase))
def test_appends_custom_emulator_base(self):
existing_bases = ('token',)
custom_emulator_base = Mock()
new_bases = _s._extend_proxy_bases_with_emulator_base(
existing_bases,
custom_emulator_base
)
self.assertThat(
new_bases,
Equals(existing_bases + (custom_emulator_base,))
)
./autopilot/tests/unit/introspection_base.py 0000644 0000041 0000041 00000002461 13061552435 021657 0 ustar www-data www-data # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Autopilot Functional Test Tool
# Copyright (C) 2016 Canonical
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
from unittest.mock import Mock
from collections import namedtuple
X_DEFAULT = 0
Y_DEFAULT = 0
W_DEFAULT = 0
H_DEFAULT = 0
global_rect = namedtuple('globalRect', ['x', 'y', 'w', 'h'])
class MockObject(Mock):
def __init__(self, *args, **kwargs):
super().__init__()
for k, v in kwargs.items():
setattr(self, k, v)
def get_properties(self):
return self.__dict__
get_mock_object = MockObject
def get_global_rect(x=X_DEFAULT, y=Y_DEFAULT, w=W_DEFAULT, h=H_DEFAULT):
return global_rect(x, y, w, h)
./autopilot/tests/unit/test_introspection_dbus.py 0000644 0000041 0000041 00000026652 13061552435 022751 0 ustar www-data www-data # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Autopilot Functional Test Tool
# Copyright (C) 2013 Canonical
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
import sys
import tempfile
import shutil
import os.path
from unittest.mock import patch, Mock
from io import StringIO
from textwrap import dedent
from testtools import TestCase
from testtools.matchers import (
Equals,
Not,
NotEquals,
Raises,
raises,
)
from autopilot.exceptions import StateNotFoundError
from autopilot.introspection import (
CustomEmulatorBase,
dbus,
is_element,
)
from autopilot.introspection.dbus import (
_MockableDbusObject,
_validate_object_properties,
)
from autopilot.utilities import sleep
from autopilot.tests.unit.introspection_base import (
W_DEFAULT,
X_DEFAULT,
Y_DEFAULT,
get_mock_object,
get_global_rect,
)
class IntrospectionFeatureTests(TestCase):
def test_custom_emulator_base_does_not_have_id(self):
self.assertThat(hasattr(CustomEmulatorBase, '_id'), Equals(False))
def test_derived_emulator_bases_do_have_id(self):
class MyEmulatorBase(CustomEmulatorBase):
pass
self.assertThat(hasattr(MyEmulatorBase, '_id'), Equals(True))
def test_derived_children_have_same_id(self):
class MyEmulatorBase(CustomEmulatorBase):
pass
class MyEmulator(MyEmulatorBase):
pass
class MyEmulator2(MyEmulatorBase):
pass
self.assertThat(MyEmulatorBase._id, Equals(MyEmulator._id))
self.assertThat(MyEmulatorBase._id, Equals(MyEmulator2._id))
def test_children_have_different_ids(self):
class MyEmulatorBase(CustomEmulatorBase):
pass
class MyEmulatorBase2(CustomEmulatorBase):
pass
self.assertThat(MyEmulatorBase._id, NotEquals(MyEmulatorBase2._id))
class DBusIntrospectionObjectTests(TestCase):
def test_can_access_path_attribute(self):
fake_object = dbus.DBusIntrospectionObject(
dict(id=[0, 123], path=[0, '/some/path']),
b'/root',
Mock()
)
with fake_object.no_automatic_refreshing():
self.assertThat(fake_object.path, Equals('/some/path'))
def test_wait_until_destroyed_works(self):
"""wait_until_destroyed must return if no new state is found."""
fake_object = dbus.DBusIntrospectionObject(
dict(id=[0, 123]),
b'/root',
Mock()
)
fake_object._backend.execute_query_get_data.return_value = []
fake_object.wait_until_destroyed()
self.assertThat(fake_object.wait_until_destroyed, Not(Raises()))
def test_wait_until_destroyed_raises_RuntimeError(self):
"""wait_until_destroyed must raise RuntimeError if the object
persists.
"""
fake_state = dict(id=[0, 123])
fake_object = dbus.DBusIntrospectionObject(
fake_state,
b'/root',
Mock()
)
fake_object._backend.execute_query_get_data.return_value = \
[fake_state]
with sleep.mocked():
self.assertThat(
lambda: fake_object.wait_until_destroyed(timeout=1),
raises(
RuntimeError("Object was not destroyed after 1 seconds")
),
)
def test_base_class_provides_correct_query_name(self):
self.assertThat(
dbus.DBusIntrospectionObject.get_type_query_name(),
Equals('ProxyBase')
)
def test_inherited_uses_default_get_node_name(self):
class TestCPO(dbus.DBusIntrospectionObject):
pass
self.assertThat(
TestCPO.get_type_query_name(),
Equals('TestCPO')
)
def test_inherited_overwrites_node_name_is_correct(self):
class TestCPO(dbus.DBusIntrospectionObject):
@classmethod
def get_type_query_name(cls):
return "TestCPO"
self.assertThat(TestCPO.get_type_query_name(), Equals("TestCPO"))
class ProxyObjectPrintTreeTests(TestCase):
def _print_test_fake_object(self):
"""common fake object for print_tree tests"""
fake_object = dbus.DBusIntrospectionObject(
dict(id=[0, 123], path=[0, '/some/path'], text=[0, 'Hello']),
b'/some/path',
Mock()
)
# get_properties() always refreshes state, so can't use
# no_automatic_refreshing()
fake_object.refresh_state = lambda: None
fake_object._execute_query = lambda q: []
return fake_object
def test_print_tree_stdout(self):
"""print_tree with default output (stdout)"""
fake_object = self._print_test_fake_object()
orig_sys_stdout = sys.stdout
sys.stdout = StringIO()
try:
fake_object.print_tree()
result = sys.stdout.getvalue()
finally:
sys.stdout = orig_sys_stdout
self.assertEqual(result, dedent("""\
== /some/path ==
id: 123
path: '/some/path'
text: 'Hello'
"""))
def test_print_tree_exception(self):
"""print_tree with StateNotFound exception"""
fake_object = self._print_test_fake_object()
child = Mock()
child.print_tree.side_effect = StateNotFoundError('child')
with patch.object(fake_object, 'get_children', return_value=[child]):
out = StringIO()
print_func = lambda: fake_object.print_tree(out)
self.assertThat(print_func, Not(Raises(StateNotFoundError)))
self.assertEqual(out.getvalue(), dedent("""\
== /some/path ==
id: 123
path: '/some/path'
text: 'Hello'
Error: Object not found with name 'child'.
{}
""".format(StateNotFoundError._troubleshoot_url_message)))
def test_print_tree_fileobj(self):
"""print_tree with file object output"""
fake_object = self._print_test_fake_object()
out = StringIO()
fake_object.print_tree(out)
self.assertEqual(out.getvalue(), dedent("""\
== /some/path ==
id: 123
path: '/some/path'
text: 'Hello'
"""))
def test_print_tree_path(self):
"""print_tree with file path output"""
fake_object = self._print_test_fake_object()
workdir = tempfile.mkdtemp()
self.addCleanup(shutil.rmtree, workdir)
outfile = os.path.join(workdir, 'widgets.txt')
fake_object.print_tree(outfile)
with open(outfile) as f:
result = f.read()
self.assertEqual(result, dedent("""\
== /some/path ==
id: 123
path: '/some/path'
text: 'Hello'
"""))
class GetTypeNameTests(TestCase):
"""Tests for the autopilot.introspection.dbus.get_type_name function."""
def test_returns_string(self):
token = self.getUniqueString()
self.assertEqual(token, dbus.get_type_name(token))
def test_returns_class_name(self):
class FooBarBaz(object):
pass
self.assertEqual("FooBarBaz", dbus.get_type_name(FooBarBaz))
def test_get_type_name_returns_classname(self):
class CustomCPO(dbus.DBusIntrospectionObject):
pass
type_name = dbus.get_type_name(CustomEmulatorBase)
self.assertThat(type_name, Equals('ProxyBase'))
def test_get_type_name_returns_custom_node_name(self):
class CustomCPO(dbus.DBusIntrospectionObject):
@classmethod
def get_type_query_name(cls):
return 'TestingCPO'
type_name = dbus.get_type_name(CustomCPO)
self.assertThat(type_name, Equals('TestingCPO'))
def test_get_type_name_returns_classname_of_non_proxybase_classes(self):
class Foo(object):
pass
self.assertEqual('Foo', dbus.get_type_name(Foo))
class IsElementTestCase(TestCase):
def raise_state_not_found(self, should_raise=True):
if should_raise:
raise StateNotFoundError('Just throw the exception')
def test_returns_false_if_not_element(self):
self.assertFalse(is_element(self.raise_state_not_found))
def test_returns_true_if_element(self):
self.assertTrue(
is_element(
self.raise_state_not_found,
should_raise=False
)
)
class IsElementMovingTestCase(TestCase):
def setUp(self):
super().setUp()
self.dbus_object = _MockableDbusObject(
get_mock_object(globalRect=get_global_rect())
)
def test_returns_true_if_x_changed(self):
mock_object = get_mock_object(
globalRect=get_global_rect(x=X_DEFAULT + 1)
)
with self.dbus_object.mocked(mock_object) as mocked_dbus_object:
self.assertTrue(mocked_dbus_object.is_moving())
def test_returns_true_if_y_changed(self):
mock_object = get_mock_object(
globalRect=get_global_rect(y=Y_DEFAULT + 1)
)
with self.dbus_object.mocked(mock_object) as mocked_dbus_object:
self.assertTrue(mocked_dbus_object.is_moving())
def test_returns_true_if_x_and_y_changed(self):
mock_object = get_mock_object(
globalRect=get_global_rect(x=X_DEFAULT + 1, y=Y_DEFAULT + 1)
)
with self.dbus_object.mocked(mock_object) as mocked_dbus_object:
self.assertTrue(mocked_dbus_object.is_moving())
def test_returns_false_if_x_and_y_not_changed(self):
mock_object = get_mock_object(globalRect=get_global_rect())
with self.dbus_object.mocked(mock_object) as mocked_dbus_object:
self.assertFalse(mocked_dbus_object.is_moving())
class ValidateObjectPropertiesTestCase(TestCase):
def test_return_true_if_property_match(self):
mock_object = get_mock_object(x=X_DEFAULT)
self.assertTrue(_validate_object_properties(mock_object, x=X_DEFAULT))
def test_returns_true_if_all_properties_match(self):
mock_object = get_mock_object(x=X_DEFAULT, y=Y_DEFAULT)
self.assertTrue(
_validate_object_properties(mock_object, x=X_DEFAULT, y=Y_DEFAULT)
)
def test_return_false_if_property_not_match(self):
mock_object = get_mock_object(x=X_DEFAULT + 1)
self.assertFalse(_validate_object_properties(mock_object, x=X_DEFAULT))
def test_returns_false_if_property_invalid(self):
mock_object = get_mock_object()
self.assertFalse(_validate_object_properties(mock_object, x=X_DEFAULT))
def test_returns_false_if_any_property_not_match(self):
mock_object = get_mock_object(x=X_DEFAULT, y=Y_DEFAULT, w=W_DEFAULT)
self.assertFalse(
_validate_object_properties(
mock_object,
x=X_DEFAULT,
y=Y_DEFAULT,
w=W_DEFAULT + 1
)
)
./autopilot/tests/unit/test_matchers.py 0000644 0000041 0000041 00000020573 13061552435 020636 0 ustar www-data www-data # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Autopilot Functional Test Tool
# Copyright (C) 2012-2013 Canonical
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
from contextlib import contextmanager
import dbus
from testscenarios import TestWithScenarios
from testtools import TestCase
from testtools.matchers import (
Contains,
Equals,
Is,
IsInstance,
Mismatch,
raises,
)
from autopilot.introspection import backends
from autopilot.introspection.dbus import DBusIntrospectionObject
from autopilot.introspection.types import Color, ValueType
from autopilot.matchers import Eventually
from autopilot.utilities import sleep
def make_fake_attribute_with_result(result, attribute_type='wait_for',
typeid=None):
"""Make a fake attribute with the given result.
This will either return a callable, or an attribute patched with a
wait_for method, according to the current test scenario.
"""
class FakeObject(DBusIntrospectionObject):
def __init__(self, props):
super(FakeObject, self).__init__(
props,
b"/FakeObject",
backends.FakeBackend(
[(dbus.String('/FakeObject'), props)]
)
)
if attribute_type == 'callable':
return lambda: result
elif attribute_type == 'wait_for':
if isinstance(result, str):
obj = FakeObject(dict(id=[0, 123], attr=[0, dbus.String(result)]))
return obj.attr
elif isinstance(result, bytes):
obj = FakeObject(
dict(id=[0, 123], attr=[0, dbus.UTF8String(result)])
)
return obj.attr
elif typeid is not None:
obj = FakeObject(dict(id=[0, 123], attr=[typeid] + result))
return obj.attr
else:
obj = FakeObject(dict(id=[0, 123], attr=[0, dbus.Boolean(result)]))
return obj.attr
class ObjectPatchingMatcherTests(TestCase):
"""Ensure the core functionality the matchers use is correct."""
def test_default_wait_for_args(self):
"""Ensure we can call wait_for with the correct arg."""
intro_obj = make_fake_attribute_with_result(False)
intro_obj.wait_for(False)
class MockedSleepTests(TestCase):
def setUp(self):
super(MockedSleepTests, self).setUp()
sleep.enable_mock()
self.addCleanup(sleep.disable_mock)
@contextmanager
def expected_runtime(self, tmin, tmax):
try:
yield
finally:
elapsed_time = sleep.total_time_slept()
if not tmin <= elapsed_time <= tmax:
raise AssertionError(
"Runtime of %f is not between %f and %f"
% (elapsed_time, tmin, tmax))
class EventuallyMatcherTests(TestWithScenarios, MockedSleepTests):
scenarios = [
('callable', dict(attribute_type='callable')),
('wait_for', dict(attribute_type='wait_for')),
]
def test_eventually_matcher_returns_mismatch(self):
"""Eventually matcher must return a Mismatch."""
attr = make_fake_attribute_with_result(False, self.attribute_type)
e = Eventually(Equals(True)).match(attr)
self.assertThat(e, IsInstance(Mismatch))
def test_eventually_default_timeout(self):
"""Eventually matcher must default to 10 second timeout."""
attr = make_fake_attribute_with_result(False, self.attribute_type)
with self.expected_runtime(9.5, 11.0):
Eventually(Equals(True)).match(attr)
def test_eventually_passes_immeadiately(self):
"""Eventually matcher must not wait if the assertion passes
initially."""
attr = make_fake_attribute_with_result(True, self.attribute_type)
with self.expected_runtime(0.0, 1.0):
Eventually(Equals(True)).match(attr)
def test_eventually_matcher_allows_non_default_timeout(self):
"""Eventually matcher must allow a non-default timeout value."""
attr = make_fake_attribute_with_result(False, self.attribute_type)
with self.expected_runtime(4.5, 6.0):
Eventually(Equals(True), timeout=5).match(attr)
def test_mismatch_message_has_correct_timeout_value(self):
"""The mismatch value must have the correct timeout value in it."""
attr = make_fake_attribute_with_result(False, self.attribute_type)
mismatch = Eventually(Equals(True), timeout=1).match(attr)
self.assertThat(
mismatch.describe(), Contains("After 1.0 seconds test"))
def test_eventually_matcher_works_with_list_type(self):
attr = make_fake_attribute_with_result(
Color(
dbus.Int32(1),
dbus.Int32(2),
dbus.Int32(3),
dbus.Int32(4)
),
self.attribute_type,
typeid=ValueType.COLOR,
)
mismatch = Eventually(Equals([1, 2, 3, 4])).match(attr)
self.assertThat(mismatch, Is(None))
class EventuallyNonScenariodTests(MockedSleepTests):
def test_eventually_matcher_raises_ValueError_on_unknown_kwargs(self):
self.assertThat(
lambda: Eventually(Equals(True), foo=123),
raises(ValueError("Unknown keyword arguments: foo"))
)
def test_eventually_matcher_raises_TypeError_on_non_matcher_argument(self):
self.assertThat(
lambda: Eventually(None),
raises(
TypeError(
"Eventually must be called with a testtools "
"matcher argument."
)
)
)
def test_match_raises_TypeError_when_called_with_plain_attribute(self):
eventually = Eventually(Equals(True))
self.assertThat(
lambda: eventually.match(False),
raises(
TypeError(
"Eventually is only usable with attributes that "
"have a wait_for function or callable objects."
)
)
)
def test_repr(self):
eventually = Eventually(Equals(True))
self.assertEqual("Eventually Equals(True)", str(eventually))
def test_match_with_expected_value_unicode(self):
"""The expected unicode value matches new value string."""
attr = make_fake_attribute_with_result(
'\u963f\u5e03\u4ece', 'wait_for')
with self.expected_runtime(0.0, 1.0):
Eventually(Equals("阿布从")).match(attr)
def test_match_with_new_value_unicode(self):
"""new value with unicode must match expected value string."""
attr = make_fake_attribute_with_result(str("阿布从"), 'wait_for')
with self.expected_runtime(0.0, 1.0):
Eventually(Equals('\u963f\u5e03\u4ece')).match(attr)
def test_mismatch_with_bool(self):
"""The mismatch value must fail boolean values."""
attr = make_fake_attribute_with_result(False, 'wait_for')
mismatch = Eventually(Equals(True), timeout=1).match(attr)
self.assertThat(
mismatch.describe(), Contains("failed"))
def test_mismatch_with_unicode(self):
"""The mismatch value must fail with str and unicode mix."""
attr = make_fake_attribute_with_result(str("阿布从1"), 'wait_for')
mismatch = Eventually(Equals(
'\u963f\u5e03\u4ece'), timeout=.5).match(attr)
self.assertThat(
mismatch.describe(), Contains('failed'))
def test_mismatch_output_utf8(self):
"""The mismatch has utf output."""
self.skip("mismatch Contains returns ascii error")
attr = make_fake_attribute_with_result(str("阿布从1"), 'wait_for')
mismatch = Eventually(Equals(
'\u963f\u5e03\u4ece'), timeout=.5).match(attr)
self.assertThat(
mismatch.describe(), Contains("阿布从11"))
./autopilot/tests/unit/test_vis_bus_enumerator.py 0000644 0000041 0000041 00000005433 13061552435 022741 0 ustar www-data www-data # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Autopilot Functional Test Tool
# Copyright (C) 2013 Canonical
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
from unittest.mock import patch, Mock
from testtools import TestCase, skipUnless
from textwrap import dedent
from autopilot import have_vis
if have_vis():
from autopilot.vis.dbus_search import XmlProcessor
@skipUnless(have_vis(), "Tests require vis module to be installed")
class BusEnumeratorXmlProcessorTest(TestCase):
_example_connection_name = "com.autopilot.test"
def test_invalid_xml_doesnt_raise_exception(self):
xml = ""
xml_processor = XmlProcessor()
xml_processor(self._example_connection_name, "/", xml)
@patch('autopilot.vis.dbus_search._logger')
def test_invalid_xml_logs_details(self, logger_meth):
xml = ""
xml_processor = XmlProcessor()
xml_processor(self._example_connection_name, "/", xml)
logger_meth.warning.assert_called_once_with(
'Unable to parse XML response for com.autopilot.test (/)'
)
def test_on_success_event_called(self):
xml = dedent(
''
''
''
)
success_callback = Mock()
xml_processor = XmlProcessor()
xml_processor.set_success_callback(success_callback)
xml_processor(self._example_connection_name, "/", xml)
success_callback.assert_called_with(
self._example_connection_name,
"/",
"org.autopilot.DBus.example",
)
def test_nodes_are_recursively_searched(self):
xml = dedent(
''
''
''
''
''
)
dbus_inspector = Mock()
xml_processor = XmlProcessor()
xml_processor.set_dbus_inspector(dbus_inspector)
xml_processor(self._example_connection_name, "/", xml)
dbus_inspector.assert_called_with(
self._example_connection_name,
"/example"
)
./autopilot/tests/unit/fixtures.py 0000644 0000041 0000041 00000002547 13061552435 017643 0 ustar www-data www-data # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Autopilot Functional Test Tool
# Copyright (C) 2015 Canonical
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
"""Fixtures to be used in autopilot's unit test suite."""
from fixtures import Fixture
from autopilot import globals as _g
class AutopilotVerboseLogging(Fixture):
"""Set the autopilot verbose log flag."""
def __init__(self, verbose_logging=True):
super().__init__()
self._desired_state = verbose_logging
def setUp(self):
super().setUp()
if _g.get_log_verbose() != self._desired_state:
self.addCleanup(
_g.set_log_verbose,
_g.get_log_verbose()
)
_g.set_log_verbose(self._desired_state)
./autopilot/tests/unit/test_run.py 0000644 0000041 0000041 00000111044 13061552435 017626 0 ustar www-data www-data # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Autopilot Functional Test Tool
# Copyright (C) 2014 Canonical
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
from argparse import Namespace
from unittest.mock import Mock, patch
import logging
import os.path
from shutil import rmtree
import subprocess
import tempfile
from testtools import TestCase, skipUnless
from testtools.matchers import (
Contains,
DirExists,
Equals,
FileExists,
IsInstance,
Not,
raises,
Raises,
StartsWith,
)
from contextlib import ExitStack
from io import StringIO
from autopilot import get_version_string, have_vis, run, _video
class RunUtilityFunctionTests(TestCase):
@patch('autopilot.run.autopilot.globals.set_debug_profile_fixture')
def test_sets_when_correct_profile_found(self, patched_set_fixture):
mock_profile = Mock()
mock_profile.name = "verbose"
parsed_args = Namespace(debug_profile="verbose")
with patch.object(
run, 'get_all_debug_profiles', lambda: {mock_profile}):
run._configure_debug_profile(parsed_args)
patched_set_fixture.assert_called_once_with(mock_profile)
@patch('autopilot.run.autopilot.globals.set_debug_profile_fixture')
def test_does_nothing_when_no_profile_found(self, patched_set_fixture):
mock_profile = Mock()
mock_profile.name = "verbose"
parsed_args = Namespace(debug_profile="normal")
with patch.object(
run, 'get_all_debug_profiles', lambda: {mock_profile}):
run._configure_debug_profile(parsed_args)
self.assertFalse(patched_set_fixture.called)
@patch('autopilot.run.autopilot.globals')
def test_timeout_values_set_with_long_profile(self, patched_globals):
args = Namespace(timeout_profile='long')
run._configure_timeout_profile(args)
patched_globals.set_default_timeout_period.assert_called_once_with(
20.0
)
patched_globals.set_long_timeout_period.assert_called_once_with(30.0)
@patch('autopilot.run.autopilot.globals')
def test_timeout_value_not_set_with_normal_profile(self, patched_globals):
args = Namespace(timeout_profile='normal')
run._configure_timeout_profile(args)
self.assertFalse(patched_globals.set_default_timeout_period.called)
self.assertFalse(patched_globals.set_long_timeout_period.called)
@patch('autopilot.run.autopilot.globals')
def test_test_tiemout_value_set_in_globals(self, patched_globals):
args = Namespace(test_timeout=42)
run._configure_test_timeout(args)
patched_globals.set_test_timeout.assert_called_once_with(42)
@patch.object(_video, '_have_video_recording_facilities', new=lambda: True)
def test_correct_video_record_fixture_is_called_with_record_on(self):
args = Namespace(record_directory='', record=True)
_video.configure_video_recording(args)
FixtureClass = _video.get_video_recording_fixture()
fixture = FixtureClass(args)
self.assertEqual(fixture.__class__.__name__, 'RMDVideoLogFixture')
@patch.object(_video, '_have_video_recording_facilities', new=lambda: True)
def test_correct_video_record_fixture_is_called_with_record_off(self):
args = Namespace(record_directory='', record=False)
_video.configure_video_recording(args)
FixtureClass = _video.get_video_recording_fixture()
fixture = FixtureClass(args)
self.assertEqual(fixture.__class__.__name__, 'DoNothingFixture')
@patch.object(_video, '_have_video_recording_facilities', new=lambda: True)
def test_configure_video_record_directory_implies_record(self):
token = self.getUniqueString()
args = Namespace(record_directory=token, record=False)
_video.configure_video_recording(args)
FixtureClass = _video.get_video_recording_fixture()
fixture = FixtureClass(args)
self.assertEqual(fixture.__class__.__name__, 'RMDVideoLogFixture')
@patch.object(_video, '_have_video_recording_facilities', new=lambda: True)
def test_configure_video_recording_sets_default_dir(self):
args = Namespace(record_directory='', record=True)
_video.configure_video_recording(args)
PartialFixture = _video.get_video_recording_fixture()
partial_fixture = PartialFixture(None)
self.assertEqual(partial_fixture.recording_directory, '/tmp/autopilot')
@patch.object(
_video,
'_have_video_recording_facilities',
new=lambda: False
)
def test_configure_video_recording_raises_RuntimeError(self):
args = Namespace(record_directory='', record=True)
self.assertThat(
lambda: _video.configure_video_recording(args),
raises(
RuntimeError(
"The application 'recordmydesktop' needs to be installed "
"to record failing jobs."
)
)
)
def test_video_record_check_calls_subprocess_with_correct_args(self):
with patch.object(_video.subprocess, 'call') as patched_call:
_video._have_video_recording_facilities()
patched_call.assert_called_once_with(
['which', 'recordmydesktop'],
stdout=run.subprocess.PIPE
)
def test_video_record_check_returns_true_on_zero_return_code(self):
with patch.object(_video.subprocess, 'call') as patched_call:
patched_call.return_value = 0
self.assertTrue(_video._have_video_recording_facilities())
def test_video_record_check_returns_false_on_nonzero_return_code(self):
with patch.object(_video.subprocess, 'call') as patched_call:
patched_call.return_value = 1
self.assertFalse(_video._have_video_recording_facilities())
def test_run_with_profiling_creates_profile_data_file(self):
output_path = tempfile.mktemp()
self.addCleanup(os.unlink, output_path)
def empty_callable():
pass
run._run_with_profiling(empty_callable, output_path)
self.assertThat(output_path, FileExists())
def test_run_with_profiling_runs_callable(self):
output_path = tempfile.mktemp()
self.addCleanup(os.unlink, output_path)
empty_callable = Mock()
run._run_with_profiling(empty_callable, output_path)
empty_callable.assert_called_once_with()
class TestRunLaunchApp(TestCase):
@patch.object(run, 'launch_process')
def test_launch_app_launches_app_with_arguments(self, patched_launch_proc):
app_name = self.getUniqueString()
app_arguments = self.getUniqueString()
fake_args = Namespace(
mode='launch',
application=[app_name, app_arguments],
interface=None
)
with patch.object(
run,
'_prepare_application_for_launch',
return_value=(app_name, app_arguments)
):
program = run.TestProgram(fake_args)
program.run()
patched_launch_proc.assert_called_once_with(
app_name,
app_arguments,
capture_output=False
)
def test_launch_app_exits_using_print_message_and_exit_error(self):
app_name = self.getUniqueString()
app_arguments = self.getUniqueString()
error_message = "Cannot find application 'blah'"
fake_args = Namespace(
mode='launch',
application=[app_name, app_arguments],
interface=None
)
with patch.object(
run,
'_prepare_application_for_launch',
side_effect=RuntimeError(error_message)
):
with patch.object(
run, '_print_message_and_exit_error'
) as print_and_exit:
run.TestProgram(fake_args).run()
print_and_exit.assert_called_once_with(
"Error: %s" % error_message
)
@patch.object(run, 'launch_process')
def test_launch_app_exits_with_message_on_failure(self, patched_launch_proc): # NOQA
app_name = self.getUniqueString()
app_arguments = self.getUniqueString()
fake_args = Namespace(
mode='launch',
application=[app_name, app_arguments],
interface=None
)
with patch.object(
run,
'_prepare_application_for_launch',
return_value=(app_name, app_arguments)
):
with patch('sys.stdout', new=StringIO()) as stdout:
patched_launch_proc.side_effect = RuntimeError(
"Failure Message"
)
program = run.TestProgram(fake_args)
self.assertThat(lambda: program.run(), raises(SystemExit(1)))
self.assertThat(
stdout.getvalue(),
Contains("Error: Failure Message")
)
@skipUnless(have_vis(), "Requires vis module.")
@patch('autopilot.vis.vis_main')
def test_passes_testability_to_vis_main(self, patched_vis_main):
args = Namespace(
mode='vis',
testability=True,
enable_profile=False,
)
program = run.TestProgram(args)
program.run()
patched_vis_main.assert_called_once_with(['-testability'])
@skipUnless(have_vis(), "Requires vis module.")
@patch('autopilot.vis.vis_main')
def test_passes_empty_list_without_testability_set(self, patched_vis_main):
args = Namespace(
mode='vis',
testability=False,
enable_profile=False,
)
program = run.TestProgram(args)
program.run()
patched_vis_main.assert_called_once_with([])
class TestRunLaunchAppHelpers(TestCase):
"""Tests for the 'autopilot launch' command"""
def test_get_app_name_and_args_returns_app_name_passed_app_name(self):
app_name = self.getUniqueString()
launch_args = [app_name]
self.assertThat(
run._get_app_name_and_args(launch_args),
Equals((app_name, []))
)
def test_get_app_name_and_args_returns_app_name_passed_arg_and_name(self):
app_name = self.getUniqueString()
app_arg = [self.getUniqueString()]
launch_args = [app_name] + app_arg
self.assertThat(
run._get_app_name_and_args(launch_args),
Equals((app_name, app_arg))
)
def test_get_app_name_and_args_returns_app_name_passed_args_and_name(self):
app_name = self.getUniqueString()
app_args = [self.getUniqueString(), self.getUniqueString()]
launch_args = [app_name] + app_args
self.assertThat(
run._get_app_name_and_args(launch_args),
Equals((app_name, app_args))
)
def test_application_name_is_full_path_True_when_is_abs_path(self):
with patch.object(run.os.path, 'isabs', return_value=True):
self.assertTrue(run._application_name_is_full_path(""))
def test_application_name_is_full_path_True_when_path_exists(self):
with patch.object(run.os.path, 'exists', return_value=True):
self.assertTrue(run._application_name_is_full_path(""))
def test_application_name_is_full_path_False_neither_abs_or_exists(self):
with patch.object(run.os.path, 'exists', return_value=False):
with patch.object(run.os.path, 'isabs', return_value=False):
self.assertFalse(run._application_name_is_full_path(""))
def test_get_applications_full_path_returns_same_when_full_path(self):
app_name = self.getUniqueString()
with patch.object(
run,
'_application_name_is_full_path',
return_value=True
):
self.assertThat(
run._get_applications_full_path(app_name),
Equals(app_name)
)
def test_get_applications_full_path_calls_which_command_on_app_name(self):
app_name = self.getUniqueString()
full_path = "/usr/bin/%s" % app_name
with patch.object(
run.subprocess,
'check_output',
return_value=full_path
):
self.assertThat(
run._get_applications_full_path(app_name),
Equals(full_path)
)
def test_get_applications_full_path_raises_valueerror_when_not_found(self):
app_name = self.getUniqueString()
expected_error = "Cannot find application '%s'" % (app_name)
with patch.object(
run.subprocess,
'check_output',
side_effect=subprocess.CalledProcessError(1, "")
):
self.assertThat(
lambda: run._get_applications_full_path(app_name),
raises(ValueError(expected_error))
)
def test_get_application_path_and_arguments_raises_for_unknown_app(self):
app_name = self.getUniqueString()
expected_error = "Cannot find application '{app_name}'".format(
app_name=app_name
)
self.assertThat(
lambda: run._get_application_path_and_arguments([app_name]),
raises(RuntimeError(expected_error))
)
def test_get_application_path_and_arguments_returns_app_and_args(self):
app_name = self.getUniqueString()
with patch.object(
run,
'_get_applications_full_path',
side_effect=lambda arg: arg
):
self.assertThat(
run._get_application_path_and_arguments([app_name]),
Equals((app_name, []))
)
def test_get_application_launcher_env_attempts_auto_selection(self):
interface = "Auto"
app_path = self.getUniqueString()
test_launcher_env = self.getUniqueString()
with patch.object(
run,
'_try_determine_launcher_env_or_raise',
return_value=test_launcher_env
) as get_launcher:
self.assertThat(
run._get_application_launcher_env(interface, app_path),
Equals(test_launcher_env)
)
get_launcher.assert_called_once_with(app_path)
def test_get_application_launcher_env_uses_string_hint_to_determine(self):
interface = None
app_path = self.getUniqueString()
test_launcher_env = self.getUniqueString()
with patch.object(
run,
'_get_app_env_from_string_hint',
return_value=test_launcher_env
) as get_launcher:
self.assertThat(
run._get_application_launcher_env(interface, app_path),
Equals(test_launcher_env)
)
get_launcher.assert_called_once_with(interface)
def test_get_application_launcher_env_returns_None_on_failure(self):
interface = None
app_path = self.getUniqueString()
with patch.object(
run,
'_get_app_env_from_string_hint',
return_value=None
):
self.assertThat(
run._get_application_launcher_env(interface, app_path),
Equals(None)
)
def test_try_determine_launcher_env_or_raise_raises_on_failure(self):
app_name = self.getUniqueString()
err_msg = self.getUniqueString()
with patch.object(
run,
'get_application_launcher_wrapper',
side_effect=RuntimeError(err_msg)
):
self.assertThat(
lambda: run._try_determine_launcher_env_or_raise(app_name),
raises(
RuntimeError(
"Error detecting launcher: {err}\n"
"(Perhaps use the '-i' argument to specify an "
"interface.)"
.format(err=err_msg)
)
)
)
def test_try_determine_launcher_env_or_raise_returns_launcher_wrapper_result(self): # NOQA
app_name = self.getUniqueString()
launcher_env = self.getUniqueString()
with patch.object(
run,
'get_application_launcher_wrapper',
return_value=launcher_env
):
self.assertThat(
run._try_determine_launcher_env_or_raise(app_name),
Equals(launcher_env)
)
def test_raise_if_launcher_is_none_raises_on_none(self):
app_name = self.getUniqueString()
self.assertThat(
lambda: run._raise_if_launcher_is_none(None, app_name),
raises(
RuntimeError(
"Could not determine introspection type to use for "
"application '{app_name}'.\n"
"(Perhaps use the '-i' argument to specify an interface.)"
.format(app_name=app_name)
)
)
)
def test_raise_if_launcher_is_none_does_not_raise_on_none(self):
launcher_env = self.getUniqueString()
app_name = self.getUniqueString()
run._raise_if_launcher_is_none(launcher_env, app_name)
def test_prepare_launcher_environment_creates_launcher_env(self):
interface = self.getUniqueString()
app_name = self.getUniqueString()
app_arguments = self.getUniqueString()
with patch.object(
run,
'_get_application_launcher_env',
) as get_launcher:
get_launcher.return_value.prepare_environment = lambda *args: args
self.assertThat(
run._prepare_launcher_environment(
interface,
app_name,
app_arguments
),
Equals((app_name, app_arguments))
)
def test_prepare_launcher_environment_checks_launcher_isnt_None(self):
interface = self.getUniqueString()
app_name = self.getUniqueString()
app_arguments = self.getUniqueString()
with patch.object(
run,
'_get_application_launcher_env',
) as get_launcher:
get_launcher.return_value.prepare_environment = lambda *args: args
with patch.object(
run,
'_raise_if_launcher_is_none'
) as launcher_check:
run._prepare_launcher_environment(
interface,
app_name,
app_arguments
)
launcher_check.assert_called_once_with(
get_launcher.return_value,
app_name
)
def test_print_message_and_exit_error_prints_message(self):
err_msg = self.getUniqueString()
with patch('sys.stdout', new=StringIO()) as stdout:
try:
run._print_message_and_exit_error(err_msg)
except SystemExit:
pass
self.assertThat(stdout.getvalue(), Contains(err_msg))
def test_print_message_and_exit_error_exits_non_zero(self):
self.assertThat(
lambda: run._print_message_and_exit_error(""),
raises(SystemExit(1))
)
def test_prepare_application_for_launch_returns_prepared_details(self):
interface = self.getUniqueString()
application = self.getUniqueString()
app_name = self.getUniqueString()
app_arguments = self.getUniqueString()
with ExitStack() as stack:
stack.enter_context(
patch.object(
run,
'_get_application_path_and_arguments',
return_value=(app_name, app_arguments)
)
)
prepare_launcher = stack.enter_context(
patch.object(
run,
'_prepare_launcher_environment',
return_value=(app_name, app_arguments)
)
)
self.assertThat(
run._prepare_application_for_launch(application, interface),
Equals((app_name, app_arguments))
)
prepare_launcher.assert_called_once_with(
interface,
app_name,
app_arguments
)
class LoggingSetupTests(TestCase):
verbose_level_none = 0
verbose_level_v = 1
verbose_level_vv = 2
def test_run_logs_autopilot_version(self):
with patch.object(run, 'log_autopilot_version') as log_version:
fake_args = Namespace(mode=None)
program = run.TestProgram(fake_args)
program.run()
log_version.assert_called_once_with()
def test_log_autopilot_version_logs_current_version(self):
current_version = get_version_string()
with patch.object(run, 'get_root_logger') as fake_get_root_logger:
run.log_autopilot_version()
fake_get_root_logger.return_value.info.assert_called_once_with(
current_version
)
def test_get_root_logger_returns_logging_instance(self):
logger = run.get_root_logger()
self.assertThat(logger, IsInstance(logging.RootLogger))
def test_setup_logging_calls_get_root_logger(self):
with patch.object(run, 'get_root_logger') as fake_get_root_logger:
run.setup_logging(self.verbose_level_none)
fake_get_root_logger.assert_called_once_with()
def test_setup_logging_defaults_to_info_level(self):
with patch.object(run, 'get_root_logger') as fake_get_logger:
run.setup_logging(self.verbose_level_none)
fake_get_logger.return_value.setLevel.assert_called_once_with(
logging.INFO
)
def test_setup_logging_keeps_root_logging_level_at_info_for_v(self):
with patch.object(run, 'get_root_logger') as fake_get_logger:
run.setup_logging(self.verbose_level_v)
fake_get_logger.return_value.setLevel.assert_called_once_with(
logging.INFO
)
def test_setup_logging_sets_root_logging_level_to_debug_with_vv(self):
with patch.object(run, 'get_root_logger') as fake_get_logger:
run.setup_logging(self.verbose_level_vv)
fake_get_logger.return_value.setLevel.assert_called_with(
logging.DEBUG
)
@patch.object(run.autopilot.globals, 'set_log_verbose')
def test_setup_logging_calls_set_log_verbose_for_v(self, patch_set_log):
with patch.object(run, 'get_root_logger'):
run.setup_logging(self.verbose_level_v)
patch_set_log.assert_called_once_with(True)
@patch.object(run.autopilot.globals, 'set_log_verbose')
def test_setup_logging_calls_set_log_verbose_for_vv(self, patch_set_log):
with patch.object(run, 'get_root_logger'):
run.setup_logging(self.verbose_level_vv)
patch_set_log.assert_called_once_with(True)
def test_set_null_log_handler(self):
mock_root_logger = Mock()
run.set_null_log_handler(mock_root_logger)
self.assertThat(
mock_root_logger.addHandler.call_args[0][0],
IsInstance(logging.NullHandler)
)
@patch.object(run, 'get_root_logger')
def test_verbse_level_zero_sets_null_handler(self, fake_get_logger):
with patch.object(run, 'set_null_log_handler') as fake_set_null:
run.setup_logging(0)
fake_set_null.assert_called_once_with(
fake_get_logger.return_value
)
def test_stderr_handler_sets_stream_handler_with_custom_formatter(self):
mock_root_logger = Mock()
run.set_stderr_stream_handler(mock_root_logger)
self.assertThat(mock_root_logger.addHandler.call_count, Equals(1))
created_handler = mock_root_logger.addHandler.call_args[0][0]
self.assertThat(
created_handler,
IsInstance(logging.StreamHandler)
)
self.assertThat(
created_handler.formatter,
IsInstance(run.LogFormatter)
)
@patch.object(run, 'get_root_logger')
def test_verbose_level_one_sets_stream_handler(self, fake_get_logger):
with patch.object(run, 'set_stderr_stream_handler') as stderr_handler:
run.setup_logging(1)
stderr_handler.assert_called_once_with(
fake_get_logger.return_value
)
def test_enable_debug_log_messages_sets_debugFilter_attr(self):
with patch.object(run, 'DebugLogFilter') as patched_filter:
patched_filter.debug_log_enabled = False
run.enable_debug_log_messages()
self.assertThat(
patched_filter.debug_log_enabled,
Equals(True)
)
@patch.object(run, 'get_root_logger')
def test_verbose_level_two_enables_debug_messages(self, fake_get_logger):
with patch.object(run, 'enable_debug_log_messages') as enable_debug:
run.setup_logging(2)
enable_debug.assert_called_once_with()
class OutputStreamTests(TestCase):
def remove_tree_if_exists(self, path):
if os.path.exists(path):
rmtree(path)
def test_get_log_file_path_returns_file_path(self):
requested_path = tempfile.mktemp()
result = run._get_log_file_path(requested_path)
self.assertThat(result, Equals(requested_path))
def test_get_log_file_path_creates_nonexisting_directories(self):
temp_dir = tempfile.mkdtemp()
self.addCleanup(self.remove_tree_if_exists, temp_dir)
dir_to_store_logs = os.path.join(temp_dir, 'some_directory')
requested_path = os.path.join(dir_to_store_logs, 'my_log.txt')
run._get_log_file_path(requested_path)
self.assertThat(dir_to_store_logs, DirExists())
def test_returns_default_filename_when_passed_directory(self):
temp_dir = tempfile.mkdtemp()
self.addCleanup(self.remove_tree_if_exists, temp_dir)
with patch.object(run, '_get_default_log_filename') as _get_default:
result = run._get_log_file_path(temp_dir)
_get_default.assert_called_once_with(temp_dir)
self.assertThat(result, Equals(_get_default.return_value))
def test_get_default_log_filename_calls_print_fn(self):
with patch.object(run, '_print_default_log_path') as patch_print:
run._get_default_log_filename('/some/path')
self.assertThat(
patch_print.call_count,
Equals(1)
)
call_arg = patch_print.call_args[0][0]
# shouldn't print the directory, since the user already explicitly
# specified that.
self.assertThat(call_arg, Not(StartsWith('/some/path')))
@patch.object(run, '_print_default_log_path')
def test_get_default_filename_returns_sane_string(self, patched_print):
with patch.object(run, 'node', return_value='hostname'):
with patch.object(run, 'datetime') as mock_dt:
mock_dt.now.return_value.strftime.return_value = 'date-part'
self.assertThat(
run._get_default_log_filename('/some/path'),
Equals('/some/path/hostname_date-part.log')
)
def test_get_output_stream_gets_stdout_with_no_logfile_specified(self):
output_stream = run.get_output_stream('text', None)
self.assertThat(output_stream.name, Equals(''))
def test_get_output_stream_opens_correct_file(self):
format = 'xml'
output = tempfile.mktemp()
self.addCleanup(os.unlink, output)
output_stream = run.get_output_stream(format, output)
self.assertThat(output_stream.name, Equals(output))
def test_text_mode_file_stream_opens_in_text_mode(self):
path = tempfile.mktemp()
self.addCleanup(os.unlink, path)
stream = run._get_text_mode_file_stream(path)
self.assertThat(stream.mode, Equals('wb'))
def test_text_mode_file_stream_accepts_text_type_only(self):
path = tempfile.mktemp()
self.addCleanup(os.unlink, path)
stream = run._get_text_mode_file_stream(path)
self.assertThat(
lambda: stream.write('Text!'),
Not(Raises())
)
self.assertThat(
lambda: stream.write(b'Bytes'),
raises(TypeError)
)
def test_binary_mode_file_stream_opens_in_binary_mode(self):
path = tempfile.mktemp()
self.addCleanup(os.unlink, path)
stream = run._get_binary_mode_file_stream(path)
self.assertThat(stream.mode, Equals('wb'))
def test_binary_mode_file_stream_accepts_text_only(self):
path = tempfile.mktemp()
self.addCleanup(os.unlink, path)
stream = run._get_binary_mode_file_stream(path)
self.assertThat(
lambda: stream.write('Text!'),
Not(Raises())
)
self.assertThat(
lambda: stream.write(b'Bytes'),
raises(TypeError)
)
def test_raw_binary_mode_file_stream_opens_in_binary_mode(self):
path = tempfile.mktemp()
self.addCleanup(os.unlink, path)
stream = run._get_raw_binary_mode_file_stream(path)
self.assertThat(stream.mode, Equals('wb'))
def test_raw_binary_mode_file_stream_accepts_bytes_only(self):
path = tempfile.mktemp()
self.addCleanup(os.unlink, path)
stream = run._get_raw_binary_mode_file_stream(path)
self.assertThat(
lambda: stream.write(b'Bytes'),
Not(Raises())
)
self.assertThat(
lambda: stream.write('Text'),
raises(TypeError)
)
def test_xml_format_opens_text_mode_stream(self):
output = tempfile.mktemp()
format = 'xml'
with patch.object(run, '_get_text_mode_file_stream') as pgts:
run.get_output_stream(format, output)
pgts.assert_called_once_with(output)
def test_txt_format_opens_binary_mode_stream(self):
output = tempfile.mktemp()
format = 'text'
with patch.object(run, '_get_binary_mode_file_stream') as pgbs:
run.get_output_stream(format, output)
pgbs.assert_called_once_with(output)
def test_subunit_format_opens_raw_binary_mode_stream(self):
output = tempfile.mktemp()
format = 'subunit'
with patch.object(run, '_get_raw_binary_mode_file_stream') as pgrbs:
run.get_output_stream(format, output)
pgrbs.assert_called_once_with(output)
def test_print_log_file_location_prints_correct_message(self):
path = self.getUniqueString()
with patch('sys.stdout', new=StringIO()) as patched_stdout:
run._print_default_log_path(path)
output = patched_stdout.getvalue()
expected = "Using default log filename: %s\n" % path
self.assertThat(expected, Equals(output))
class TestProgramTests(TestCase):
"""Tests for the TestProgram class.
These tests are a little ugly at the moment, and will continue to be so
until we refactor the run module to make it more testable.
"""
def test_can_provide_args(self):
fake_args = Namespace()
program = run.TestProgram(fake_args)
self.assertThat(program.args, Equals(fake_args))
def test_calls_parse_args_by_default(self):
fake_args = Namespace()
with patch.object(run, '_parse_arguments') as fake_parse_args:
fake_parse_args.return_value = fake_args
program = run.TestProgram()
fake_parse_args.assert_called_once_with()
self.assertThat(program.args, Equals(fake_args))
def test_run_calls_setup_logging_with_verbose_arg(self):
fake_args = Namespace(verbose=1, mode='')
program = run.TestProgram(fake_args)
with patch.object(run, 'setup_logging') as patched_setup_logging:
program.run()
patched_setup_logging.assert_called_once_with(True)
def test_list_command_calls_list_tests_method(self):
fake_args = Namespace(mode='list')
program = run.TestProgram(fake_args)
with patch.object(program, 'list_tests') as patched_list_tests:
program.run()
patched_list_tests.assert_called_once_with()
def test_run_command_calls_run_tests_method(self):
fake_args = Namespace(mode='run')
program = run.TestProgram(fake_args)
with patch.object(program, 'run_tests') as patched_run_tests:
program.run()
patched_run_tests.assert_called_once_with()
def test_vis_command_calls_run_vis_method(self):
fake_args = Namespace(mode='vis')
program = run.TestProgram(fake_args)
with patch.object(program, 'run_vis') as patched_run_vis:
program.run()
patched_run_vis.assert_called_once_with()
def test_vis_command_runs_under_profiling_if_profiling_is_enabled(self):
fake_args = Namespace(
mode='vis',
enable_profile=True,
testability=False,
)
program = run.TestProgram(fake_args)
with patch.object(run, '_run_with_profiling') as patched_run_profile:
program.run()
self.assertThat(
patched_run_profile.call_count,
Equals(1),
)
def test_launch_command_calls_launch_app_method(self):
fake_args = Namespace(mode='launch')
program = run.TestProgram(fake_args)
with patch.object(program, 'launch_app') as patched_launch_app:
program.run()
patched_launch_app.assert_called_once_with()
def test_run_tests_calls_utility_functions(self):
"""The run_tests method must call all the utility functions.
This test is somewhat ugly, and relies on a lot of mocks. This will be
cleaned up once run has been completely refactored.
"""
fake_args = create_default_run_args()
program = run.TestProgram(fake_args)
mock_test_result = Mock()
mock_test_result.wasSuccessful.return_value = True
mock_test_suite = Mock()
mock_test_suite.run.return_value = mock_test_result
mock_construct_test_result = Mock()
with ExitStack() as stack:
load_tests = stack.enter_context(
patch.object(run, 'load_test_suite_from_name')
)
fake_construct = stack.enter_context(
patch.object(run, 'construct_test_result')
)
configure_debug = stack.enter_context(
patch.object(run, '_configure_debug_profile')
)
config_timeout = stack.enter_context(
patch.object(run, '_configure_timeout_profile')
)
config_test_timeout = stack.enter_context(
patch.object(run, '_configure_test_timeout')
)
load_tests.return_value = (mock_test_suite, False)
fake_construct.return_value = mock_construct_test_result
program.run()
config_timeout.assert_called_once_with(fake_args)
configure_debug.assert_called_once_with(fake_args)
config_test_timeout.assert_called_once_with(fake_args)
fake_construct.assert_called_once_with(fake_args)
load_tests.assert_called_once_with(fake_args.suite)
def test_dont_run_when_zero_tests_loaded(self):
fake_args = create_default_run_args()
program = run.TestProgram(fake_args)
with patch('sys.stdout', new=StringIO()):
self.assertRaisesRegexp(RuntimeError,
'Did not find any tests',
program.run)
def create_default_run_args(**kwargs):
"""Create a an argparse.Namespace object containing arguments required
to make autopilot.run.TestProgram run a suite of tests.
Every feature that can be turned off will be. Individual arguments can be
specified with keyword arguments to this function.
"""
defaults = dict(
random_order=False,
debug_profile='normal',
timeout_profile='normal',
record_directory='',
record=False,
record_options='',
verbose=False,
mode='run',
suite='foo',
test_config='',
test_timeout=0,
)
defaults.update(kwargs)
return Namespace(**defaults)
./autopilot/tests/unit/test_version_utility_fns.py 0000644 0000041 0000041 00000006613 13061552435 023145 0 ustar www-data www-data # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Autopilot Functional Test Tool
# Copyright (C) 2012-2013 Canonical
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
from unittest.mock import patch
import subprocess
from testtools import TestCase
from testtools.matchers import Equals
from autopilot._info import (
_get_package_installed_version,
_get_package_version,
get_version_string,
)
class VersionFnTests(TestCase):
def test_package_version_returns_none_when_running_from_source(self):
"""_get_package_version must return None if we're not running in the
system.
"""
with patch('autopilot._info._running_in_system', new=lambda: False):
self.assertThat(_get_package_version(), Equals(None))
def test_get_package_installed_version_returns_None_on_error(self):
"""The _get_package_installed_version function must return None when
subprocess raises an error while calling dpkg-query.
"""
def raise_error(*args, **kwargs):
raise subprocess.CalledProcessError(1, "dpkg-query")
with patch('subprocess.check_output', new=raise_error):
self.assertThat(_get_package_installed_version(), Equals(None))
def test_get_package_installed_version_strips_command_output(self):
"""The _get_package_installed_version function must strip the output of
the dpkg-query function.
"""
with patch('subprocess.check_output',
new=lambda *a, **kwargs: "1.3daily13.05.22\n"):
self.assertThat(
_get_package_installed_version(), Equals("1.3daily13.05.22"))
def test_get_version_string_shows_source_version(self):
"""The get_version_string function must only show the source version if
the system version returns None.
"""
with patch('autopilot._info._get_package_version', new=lambda: None):
with patch('autopilot._info._get_source_version',
new=lambda: "1.3.1"):
version_string = get_version_string()
self.assertThat(
version_string, Equals("Autopilot Source Version: 1.3.1"))
def test_get_version_string_shows_both_versions(self):
"""The get_version_string function must show both source and package
versions, when the package version is avaialble.capitalize
"""
with patch('autopilot._info._get_package_version',
new=lambda: "1.3.1daily13.05.22"):
with patch('autopilot._info._get_source_version',
new=lambda: "1.3.1"):
version_string = get_version_string()
self.assertThat(
version_string,
Equals("Autopilot Source Version: 1.3.1\nAutopilot Package "
"Version: 1.3.1daily13.05.22"))
./autopilot/tests/unit/test_display.py 0000644 0000041 0000041 00000002305 13061552443 020465 0 ustar www-data www-data # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Autopilot Functional Test Tool
# Copyright (C) 2016 Canonical
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
import unittest
from unittest.mock import patch
from autopilot.display import Display
class DisplayTestCase(unittest.TestCase):
@patch('autopilot.display._pick_backend')
def test_input_backends_default_order(self, pick_backend):
d = Display()
d.create()
backends = list(pick_backend.call_args[0][0].items())
self.assertTrue(backends[0][0] == 'X11')
self.assertTrue(backends[1][0] == 'UPA')
./autopilot/tests/unit/test_start_final_events.py 0000644 0000041 0000041 00000006503 13061552435 022717 0 ustar www-data www-data # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Autopilot Functional Test Tool
# Copyright (C) 2013 Canonical
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
from testtools import TestCase
from testtools.matchers import Equals, Contains
from unittest.mock import patch, Mock
from autopilot.utilities import (
CleanupRegistered,
_cleanup_objects,
action_on_test_start,
action_on_test_end,
)
from autopilot.utilities import on_test_started
class StartFinalExecutionTests(TestCase):
def test_conformant_class_is_added(self):
class Conformant(CleanupRegistered):
pass
self.assertThat(_cleanup_objects, Contains(Conformant))
def test_on_test_start_and_end_methods_called(self):
class Conformant(CleanupRegistered):
"""This class defines the classmethods to be called at test
start/end.
"""
_on_start_test = False
_on_end_test = False
@classmethod
def on_test_start(cls, test_instance):
cls._on_start_test = True
@classmethod
def on_test_end(cls, test_instance):
cls._on_end_test = True
class InnerTest(TestCase):
def setUp(self):
super().setUp()
on_test_started(self)
def test_foo(self):
pass
test_run = InnerTest('test_foo').run()
InnerTest('test_foo').run()
self.assertThat(test_run.wasSuccessful(), Equals(True))
self.assertThat(Conformant._on_start_test, Equals(True))
self.assertThat(Conformant._on_end_test, Equals(True))
def test_action_on_test_start_reports_raised_exception(self):
"""Any Exceptions raised during action_on_test_start must be caught and
reported.
"""
class Cleanup(object):
def on_test_start(self, test_instance):
raise IndexError
obj = Cleanup()
mockTestCase = Mock(spec=TestCase)
with patch('autopilot.utilities._cleanup_objects', new=[obj]):
action_on_test_start(mockTestCase)
self.assertThat(mockTestCase._report_traceback.call_count, Equals(1))
def test_action_on_test_end_reports_raised_exception(self):
"""Any Exceptions raised during action_on_test_end must be caught and
reported.
"""
class Cleanup(object):
def on_test_end(self, test_instance):
raise IndexError
obj = Cleanup()
mockTestCase = Mock(spec=TestCase)
with patch('autopilot.utilities._cleanup_objects', new=[obj]):
action_on_test_end(mockTestCase)
self.assertThat(mockTestCase._report_traceback.call_count, Equals(1))
./autopilot/tests/unit/test_introspection_object_registry.py 0000644 0000041 0000041 00000032054 13061552435 025203 0 ustar www-data www-data # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Autopilot Functional Test Tool
# Copyright (C) 2014, 2015 Canonical
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
from unittest.mock import patch
from testtools import TestCase
from testtools.matchers import (
Equals,
raises,
)
from autopilot.introspection import CustomEmulatorBase
from autopilot.introspection import _object_registry as object_registry
from autopilot.introspection import _xpathselect as xpathselect
class MakeIntrospectionObjectTests(TestCase):
class DefaultSelector(CustomEmulatorBase):
pass
class AlwaysSelected(CustomEmulatorBase):
@classmethod
def validate_dbus_object(cls, path, state):
"""Validate always.
:returns: True
"""
return True
class NeverSelected(CustomEmulatorBase):
@classmethod
def validate_dbus_object(cls, path, state):
"""Validate never.
:returns: False
"""
return False
@patch.object(object_registry, '_try_custom_proxy_classes')
@patch.object(object_registry, '_get_default_proxy_class')
def test_get_proxy_object_class_return_from_list(self, gdpc, tcpc):
"""_get_proxy_object_class should return the value of
_try_custom_proxy_classes if there is one."""
token = self.getUniqueString()
tcpc.return_value = token
gpoc_return = object_registry._get_proxy_object_class(
"fake_id", # cannot set to none.
None,
None
)
self.assertThat(gpoc_return, Equals(token))
self.assertFalse(gdpc.called)
@patch.object(object_registry, '_try_custom_proxy_classes')
def test_get_proxy_object_class_send_right_args(self, tcpc):
"""_get_proxy_object_class should send the right arguments to
_try_custom_proxy_classes."""
class_dict = {'DefaultSelector': self.DefaultSelector}
path = '/path/to/DefaultSelector'
state = {}
object_registry._get_proxy_object_class(class_dict, path, state)
tcpc.assert_called_once_with(class_dict, path, state)
@patch.object(object_registry, '_try_custom_proxy_classes')
def test_get_proxy_object_class_not_handle_error(self, tcpc):
"""_get_proxy_object_class should not handle an exception raised by
_try_custom_proxy_classes."""
tcpc.side_effect = ValueError
self.assertThat(
lambda: object_registry._get_proxy_object_class(
"None", # Cannot be set to None, but don't care about value
None,
None
),
raises(ValueError))
@patch.object(object_registry, '_try_custom_proxy_classes')
@patch.object(object_registry, '_get_default_proxy_class')
@patch.object(object_registry, 'get_classname_from_path')
def test_get_proxy_object_class_call_default_call(self, gcfp, gdpc, tcpc):
"""_get_proxy_object_class should call _get_default_proxy_class if
_try_custom_proxy_classes returns None."""
tcpc.return_value = None
object_registry._get_proxy_object_class(None, None, None)
self.assertTrue(gdpc.called)
@patch.object(object_registry, '_try_custom_proxy_classes')
@patch.object(object_registry, '_get_default_proxy_class')
def test_get_proxy_object_class_default_args(self, gdpc, tcpc):
"""_get_proxy_object_class should pass the correct arguments to
_get_default_proxy_class"""
tcpc.return_value = None
obj_id = 123
path = '/path/to/DefaultSelector'
object_registry._get_proxy_object_class(obj_id, path, None)
gdpc.assert_called_once_with(
obj_id,
xpathselect.get_classname_from_path(path)
)
@patch.object(object_registry, '_try_custom_proxy_classes')
@patch.object(object_registry, '_get_default_proxy_class')
@patch.object(object_registry, 'get_classname_from_path')
def test_get_proxy_object_class_default(self, gcfp, gdpc, tcpc):
"""_get_proxy_object_class should return the value of
_get_default_proxy_class if _try_custom_proxy_classes returns None."""
token = self.getUniqueString()
gdpc.return_value = token
tcpc.return_value = None
gpoc_return = object_registry._get_proxy_object_class(
None,
None,
None,
)
self.assertThat(gpoc_return, Equals(token))
def test_try_custom_proxy_classes_zero_results(self):
"""_try_custom_proxy_classes must return None if no classes match."""
proxy_class_dict = {'NeverSelected': self.NeverSelected}
fake_id = self.getUniqueInteger()
path = '/path/to/NeverSelected'
state = {}
with object_registry.patch_registry({fake_id: proxy_class_dict}):
class_type = object_registry._try_custom_proxy_classes(
fake_id,
path,
state
)
self.assertThat(class_type, Equals(None))
def test_try_custom_proxy_classes_one_result(self):
"""_try_custom_proxy_classes must return the matching class if there is
exacly 1."""
proxy_class_dict = {'DefaultSelector': self.DefaultSelector}
fake_id = self.getUniqueInteger()
path = '/path/to/DefaultSelector'
state = {}
with object_registry.patch_registry({fake_id: proxy_class_dict}):
class_type = object_registry._try_custom_proxy_classes(
fake_id,
path,
state
)
self.assertThat(class_type, Equals(self.DefaultSelector))
def test_try_custom_proxy_classes_two_results(self):
"""_try_custom_proxy_classes must raise ValueError if multiple classes
match."""
proxy_class_dict = {'DefaultSelector': self.DefaultSelector,
'AlwaysSelected': self.AlwaysSelected}
path = '/path/to/DefaultSelector'
state = {}
object_id = self.getUniqueInteger()
with object_registry.patch_registry({object_id: proxy_class_dict}):
self.assertThat(
lambda: object_registry._try_custom_proxy_classes(
object_id,
path,
state
),
raises(ValueError)
)
@patch('autopilot.introspection._object_registry.get_debug_logger')
def test_get_default_proxy_class_logging(self, gdl):
"""_get_default_proxy_class should log a message."""
object_registry._get_default_proxy_class(self.DefaultSelector, "None")
gdl.assert_called_once_with()
def test_get_default_proxy_class_base(self):
"""Subclass must return an emulator of base class."""
class SubclassedProxy(self.DefaultSelector):
pass
result = object_registry._get_default_proxy_class(
SubclassedProxy,
'Object'
)
self.assertTrue(result, Equals(self.DefaultSelector))
def test_get_default_proxy_class_base_instead_of_self(self):
"""Subclass must not use self if base class works."""
class SubclassedProxy(self.DefaultSelector):
pass
result = object_registry._get_default_proxy_class(
SubclassedProxy,
'Object'
)
self.assertFalse(issubclass(result, SubclassedProxy))
def test_get_default_proxy_class(self):
"""Must default to own class if no usable bases present."""
result = object_registry._get_default_proxy_class(
self.DefaultSelector,
'Object'
)
self.assertTrue(result, Equals(self.DefaultSelector))
def test_get_default_proxy_name(self):
"""Must default to own class if no usable bases present."""
token = self.getUniqueString()
result = object_registry._get_default_proxy_class(
self.DefaultSelector,
token
)
self.assertThat(result.__name__, Equals(token))
class ObjectRegistryPatchTests(TestCase):
def test_patch_registry_sets_new_registry(self):
new_registry = dict(foo=123)
with object_registry.patch_registry(new_registry):
self.assertEqual(object_registry._object_registry, new_registry)
def test_patch_registry_undoes_patch(self):
old_registry = object_registry._object_registry.copy()
with object_registry.patch_registry({}):
pass
self.assertEqual(object_registry._object_registry, old_registry)
def test_patch_registry_undoes_patch_when_exception_raised(self):
def patch_reg():
with object_registry.patch_registry({}):
raise RuntimeError()
old_registry = object_registry._object_registry.copy()
try:
patch_reg()
except RuntimeError:
pass
self.assertEqual(object_registry._object_registry, old_registry)
def test_patch_registry_reraised_caught_exception(self):
def patch_reg():
with object_registry.patch_registry({}):
raise RuntimeError()
self.assertThat(patch_reg, raises(RuntimeError()))
def test_modifications_are_unwound(self):
token = self.getUniqueString()
with object_registry.patch_registry(dict()):
object_registry._object_registry[token] = token
self.assertFalse(token in object_registry._object_registry)
class CombineBasesTests(TestCase):
def test_returns_original_if_no_extensions(self):
class Base():
pass
class Sub(Base):
pass
self.assertEqual(
object_registry._combine_base_and_extensions(Sub, ()),
(Base,)
)
def test_returns_addition_of_extension_classes(self):
class Base():
pass
class Sub(Base):
pass
class Ext1():
pass
class Ext2():
pass
mixed = object_registry._combine_base_and_extensions(Sub, (Ext1, Ext2))
self.assertIn(Ext1, mixed)
self.assertIn(Ext2, mixed)
def test_excludes_duplication_of_base_class_within_extension_classes(self):
class Base():
pass
class Sub(Base):
pass
class Ext1():
pass
class Ext2(Ext1):
pass
self.assertEqual(
object_registry._combine_base_and_extensions(Sub, (Ext2, Base)),
(Ext2, Base,)
)
def test_excludes_addition_of_extension_base_classes(self):
class Base():
pass
class Sub(Base):
pass
class Ext1():
pass
class Ext2(Ext1):
pass
self.assertNotIn(
object_registry._combine_base_and_extensions(Sub, (Ext2,)),
Ext1
)
def test_excludes_addition_of_subject_class(self):
class Base():
pass
class Sub(Base):
pass
class Ext1():
pass
self.assertEqual(
object_registry._combine_base_and_extensions(Sub, (Ext1, Sub)),
(Ext1, Base)
)
def test_maintains_mro_order(self):
class Base():
pass
class Sub1(Base):
pass
class Sub2(Sub1, Base):
pass
class Ext1():
pass
mixed = object_registry._combine_base_and_extensions(Sub2, (Ext1,))
self.assertLess(
mixed.index(Sub1),
mixed.index(Base)
)
class MROSortOrderTests(TestCase):
def test_returns_lower_values_for_lower_classes_in_mro(self):
class Parent():
pass
class Child(Parent):
pass
class GrandChild(Child):
pass
self.assertGreater(
object_registry._get_mro_sort_order(Child),
object_registry._get_mro_sort_order(Parent),
)
self.assertGreater(
object_registry._get_mro_sort_order(GrandChild),
object_registry._get_mro_sort_order(Child),
)
def test_return_higher_values_for_promoted_collections_in_ties(self):
class Parent1():
pass
class Child1(Parent1):
pass
class Parent2():
pass
class Child2(Parent2):
pass
promoted_collection = (Parent1, Child1)
self.assertGreater(
object_registry._get_mro_sort_order(
Child1, promoted_collection),
object_registry._get_mro_sort_order(
Child2, promoted_collection),
)
./autopilot/tests/unit/test_logging.py 0000644 0000041 0000041 00000006610 13061552435 020452 0 ustar www-data www-data # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Autopilot Functional Test Tool
# Copyright (C) 2013 Canonical
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
import logging
from unittest.mock import Mock
import testtools
from autopilot import tests
from autopilot.logging import log_action
from autopilot._logging import TestCaseLoggingFixture
class ObjectWithLogDecorator(object):
@log_action(logging.info)
def do_something_without_docstring(self, *args, **kwargs):
pass
@log_action(logging.info)
def do_something_with_docstring(self, *args, **kwargs):
"""Do something with docstring."""
pass
@log_action(logging.info)
def do_something_with_multiline_docstring(self, *args, **kwargs):
"""Do something with a multiline docstring.
This should not be logged.
"""
pass
class LoggingTestCase(tests.LogHandlerTestCase):
def setUp(self):
super(LoggingTestCase, self).setUp()
self.root_logger.setLevel(logging.INFO)
self.logged_object = ObjectWithLogDecorator()
def test_logged_action_without_docstring(self):
self.logged_object.do_something_without_docstring(
'arg1', 'arg2', arg3='arg3', arg4='arg4')
self.assertLogLevelContains(
'INFO',
"ObjectWithLogDecorator: do_something_without_docstring. "
"Arguments ('arg1', 'arg2'). "
"Keyword arguments: {'arg3': 'arg3', 'arg4': 'arg4'}.")
def test_logged_action_with_docstring(self):
self.logged_object.do_something_with_docstring(
'arg1', 'arg2', arg3='arg3', arg4='arg4')
self.assertLogLevelContains(
'INFO',
"ObjectWithLogDecorator: Do something with docstring. "
"Arguments ('arg1', 'arg2'). "
"Keyword arguments: {'arg3': 'arg3', 'arg4': 'arg4'}.")
def test_logged_action_with_multiline_docstring(self):
self.logged_object.do_something_with_multiline_docstring(
'arg1', 'arg2', arg3='arg3', arg4='arg4')
self.assertLogLevelContains(
'INFO',
"ObjectWithLogDecorator: "
"Do something with a multiline docstring. "
"Arguments ('arg1', 'arg2'). "
"Keyword arguments: {'arg3': 'arg3', 'arg4': 'arg4'}.")
class TestCaseLoggingFixtureTests(testtools.TestCase):
def test_test_log_is_added(self):
token = self.getUniqueString()
add_detail_fn = Mock()
fixture = TestCaseLoggingFixture("Test.id", add_detail_fn)
fixture.setUp()
logging.getLogger(__name__).info(token)
fixture.cleanUp()
self.assertEqual(1, add_detail_fn.call_count)
self.assertEqual('test-log', add_detail_fn.call_args[0][0])
self.assertIn(token, add_detail_fn.call_args[0][1].as_text())
./autopilot/tests/unit/test_platform.py 0000644 0000041 0000041 00000023105 13061552435 020646 0 ustar www-data www-data # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Autopilot Functional Test Tool
# Copyright (C) 2012-2013 Canonical
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
"""Tests for the autopilot platform code."""
from io import StringIO
from testtools import TestCase
from testtools.matchers import Equals
from tempfile import NamedTemporaryFile
from unittest.mock import patch
import autopilot.platform as platform
class PublicAPITests(TestCase):
def setUp(self):
super().setUp()
@patch('autopilot.platform._PlatformDetector')
def test_model_creates_platform_detector(self, mock_detector):
platform.model()
mock_detector.create.assert_called_once_with()
@patch('autopilot.platform._PlatformDetector._cached_detector')
def test_model_returns_correct_value(self, mock_detector):
mock_detector.model = "test123"
self.assertThat(platform.model(), Equals('test123'))
@patch('autopilot.platform._PlatformDetector')
def test_image_codename_creates_platform_detector(self, mock_detector):
platform.image_codename()
mock_detector.create.assert_called_once_with()
@patch('autopilot.platform._PlatformDetector._cached_detector')
def test_image_codename_returns_correct_value(self, mock_detector):
mock_detector.image_codename = "test123"
self.assertThat(platform.image_codename(), Equals('test123'))
class PlatformGetProcessNameTests(TestCase):
def test_returns_callable_value(self):
test_callable = lambda: "foo"
self.assertEqual("foo", platform._get_process_name(test_callable))
def test_returns_string(self):
self.assertEqual("foo", platform._get_process_name("foo"))
def test_raises_exception_if_unsupported_passed(self):
self.assertRaises(ValueError, platform._get_process_name, 0)
class PlatformDetectorTests(TestCase):
def setUp(self):
super(PlatformDetectorTests, self).setUp()
# platform detector is cached, so make sure we destroy the cache before
# each test runs, and after each test completes.
self._destroy_platform_detector_cache()
self.addCleanup(self._destroy_platform_detector_cache)
def _destroy_platform_detector_cache(self):
platform._PlatformDetector._cached_detector = None
def test_platform_detector_is_cached(self):
"""Test that the platform detector is only created once."""
detector1 = platform._PlatformDetector.create()
detector2 = platform._PlatformDetector.create()
self.assertThat(id(detector1), Equals(id(detector2)))
@patch('autopilot.platform._get_property_file')
def test_default_model(self, mock_get_property_file):
"""The default model name must be 'Desktop'."""
mock_get_property_file.return_value = None
detector = platform._PlatformDetector.create()
self.assertThat(detector.model, Equals('Desktop'))
@patch('autopilot.platform._get_property_file')
def test_default_image_codename(self, mock_get_property_file):
"""The default image codename must be 'Desktop'."""
mock_get_property_file.return_value = None
detector = platform._PlatformDetector.create()
self.assertThat(detector.image_codename, Equals('Desktop'))
@patch('autopilot.platform._get_property_file')
def test_model_is_set_from_property_file(self, mock_get_property_file):
"""Detector must read product model from android properties file."""
mock_get_property_file.return_value = StringIO(
"ro.product.model=test123")
detector = platform._PlatformDetector.create()
self.assertThat(detector.model, Equals('test123'))
@patch('autopilot.platform._get_property_file', new=lambda: StringIO(""))
def test_model_has_default_when_not_in_property_file(self):
"""Detector must use 'Desktop' as a default value for the model name
when the property file exists, but does not contain a model
description.
"""
detector = platform._PlatformDetector.create()
self.assertThat(detector.model, Equals('Desktop'))
@patch('autopilot.platform._get_property_file')
def test_product_codename_is_set_from_property_file(
self, mock_get_property_file):
"""Detector must read product model from android properties file."""
mock_get_property_file.return_value = StringIO(
"ro.product.name=test123")
detector = platform._PlatformDetector.create()
self.assertThat(detector.image_codename, Equals('test123'))
@patch('autopilot.platform._get_property_file', new=lambda: StringIO(""))
def test_product_codename_has_default_when_not_in_property_file(self):
"""Detector must use 'Desktop' as a default value for the product
codename when the property file exists, but does not contain a model
description.
"""
detector = platform._PlatformDetector.create()
self.assertThat(detector.image_codename, Equals('Desktop'))
def test_has_correct_file_name(self):
observed = platform._get_property_file_path()
self.assertEqual("/system/build.prop", observed)
def test_get_property_file_opens_path(self):
token = self.getUniqueString()
with NamedTemporaryFile(mode='w+') as f:
f.write(token)
f.flush()
with patch('autopilot.platform._get_property_file_path') as p:
p.return_value = f.name
observed = platform._get_property_file().read()
self.assertEqual(token, observed)
@patch('autopilot.platform._get_property_file')
def test_get_tablet_from_property_file(
self, mock_get_property_file):
"""Detector must read tablet from android properties file."""
mock_get_property_file.return_value = StringIO(
"ro.build.characteristics=tablet")
detector = platform._PlatformDetector.create()
self.assertThat(detector.is_tablet, Equals(True))
@patch('autopilot.platform._get_property_file')
def test_get_not_tablet_from_property_file(
self, mock_get_property_file):
"""Detector must read lack of tablet from android properties file."""
mock_get_property_file.return_value = StringIO(
"ro.build.characteristics=nosdcard")
detector = platform._PlatformDetector.create()
self.assertThat(detector.is_tablet, Equals(False))
@patch('autopilot.platform._get_property_file')
def test_tablet_without_property_file(self, mock_get_property_file):
"""Detector must return False for tablet when there is no properties
file.
"""
mock_get_property_file.return_value = None
detector = platform._PlatformDetector.create()
self.assertThat(detector.is_tablet, Equals(False))
class BuildPropertyParserTests(TestCase):
"""Tests for the android build properties file parser."""
def test_empty_file_returns_empty_dictionary(self):
"""An empty file must result in an empty dictionary."""
prop_file = StringIO("")
properties = platform._parse_build_properties_file(prop_file)
self.assertThat(len(properties), Equals(0))
def test_whitespace_is_ignored(self):
"""Whitespace in build file must be ignored."""
prop_file = StringIO("\n\n\n\n\n")
properties = platform._parse_build_properties_file(prop_file)
self.assertThat(len(properties), Equals(0))
def test_comments_are_ignored(self):
"""Comments in build file must be ignored."""
prop_file = StringIO("# Hello World\n #Hello Again\n#####")
properties = platform._parse_build_properties_file(prop_file)
self.assertThat(len(properties), Equals(0))
def test_invalid_lines_are_ignored(self):
"""lines without ana ssignment must be ignored."""
prop_file = StringIO("Hello")
properties = platform._parse_build_properties_file(prop_file)
self.assertThat(len(properties), Equals(0))
def test_simple_value(self):
"""Test a simple a=b expression."""
prop_file = StringIO("a=b")
properties = platform._parse_build_properties_file(prop_file)
self.assertThat(properties, Equals(dict(a='b')))
def test_multiple_values(self):
"""Test several expressions over multiple lines."""
prop_file = StringIO("a=b\nb=23")
properties = platform._parse_build_properties_file(prop_file)
self.assertThat(properties, Equals(dict(a='b', b='23')))
def test_values_with_equals_in_them(self):
"""Test that we can parse values with a '=' in them."""
prop_file = StringIO("a=b=c")
properties = platform._parse_build_properties_file(prop_file)
self.assertThat(properties, Equals(dict(a='b=c')))
def test_dotted_values_work(self):
"""Test that we can use dotted values as the keys."""
prop_file = StringIO("ro.product.model=maguro")
properties = platform._parse_build_properties_file(prop_file)
self.assertThat(properties, Equals({'ro.product.model': 'maguro'}))
./autopilot/tests/unit/test_test_fixtures.py 0000644 0000041 0000041 00000020223 13061552435 021730 0 ustar www-data www-data # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Autopilot Functional Test Tool
# Copyright (C) 2014 Canonical
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
from autopilot.tests.functional.fixtures import (
ExecutableScript,
Timezone,
TempDesktopFile,
)
import os
import os.path
import stat
from unittest.mock import patch
from shutil import rmtree
import tempfile
from testtools import TestCase
from testtools.matchers import Contains, EndsWith, Equals, FileContains
class TempDesktopFileTests(TestCase):
def test_setUp_creates_desktop_file(self):
desktop_file_dir = tempfile.mkdtemp(dir="/tmp")
self.addCleanup(rmtree, desktop_file_dir)
with patch.object(
TempDesktopFile, '_desktop_file_dir', return_value=desktop_file_dir
):
temp_desktop_file = TempDesktopFile()
temp_desktop_file.setUp()
desktop_file_path = temp_desktop_file.get_desktop_file_path()
self.assertTrue(os.path.exists(desktop_file_path))
temp_desktop_file.cleanUp()
self.assertFalse(os.path.exists(desktop_file_path))
def test_desktop_file_dir_returns_expected_directory(self):
expected_directory = os.path.join(
os.getenv('HOME'),
'.local',
'share',
'applications'
)
self.assertThat(
TempDesktopFile._desktop_file_dir(),
Equals(expected_directory)
)
def test_ensure_desktop_dir_exists_returns_empty_string_when_exists(self):
desktop_file_dir = tempfile.mkdtemp(dir="/tmp")
self.addCleanup(rmtree, desktop_file_dir)
with patch.object(
TempDesktopFile, '_desktop_file_dir', return_value=desktop_file_dir
):
self.assertThat(
TempDesktopFile._ensure_desktop_dir_exists(),
Equals("")
)
def test_ensure_desktop_dir_exists_creates_dir_when_needed(self):
tmp_dir = tempfile.mkdtemp(dir="/tmp")
self.addCleanup(rmtree, tmp_dir)
desktop_file_dir = os.path.join(
tmp_dir, '.local', 'share', 'applications'
)
with patch.object(
TempDesktopFile, '_desktop_file_dir', return_value=desktop_file_dir
):
TempDesktopFile._ensure_desktop_dir_exists()
self.assertTrue(os.path.exists(desktop_file_dir))
def test_ensure_desktop_dir_exists_returns_path_to_delete(self):
tmp_dir = tempfile.mkdtemp(dir="/tmp")
self.addCleanup(rmtree, tmp_dir)
desktop_file_dir = os.path.join(
tmp_dir, '.local', 'share', 'applications'
)
expected_to_remove = os.path.join(tmp_dir, '.local')
with patch.object(
TempDesktopFile, '_desktop_file_dir', return_value=desktop_file_dir
):
self.assertThat(
TempDesktopFile._ensure_desktop_dir_exists(),
Equals(expected_to_remove)
)
def test_create_desktop_file_dir_returns_path_to_delete(self):
tmp_dir = tempfile.mkdtemp(dir="/tmp")
self.addCleanup(rmtree, tmp_dir)
desktop_file_dir = os.path.join(
tmp_dir, '.local', 'share', 'applications'
)
expected_to_remove = os.path.join(tmp_dir, '.local')
self.assertThat(
TempDesktopFile._create_desktop_file_dir(desktop_file_dir),
Equals(expected_to_remove)
)
def test_create_desktop_file_dir_returns_empty_str_when_path_exists(self):
desktop_file_dir = tempfile.mkdtemp(dir="/tmp")
self.addCleanup(rmtree, desktop_file_dir)
self.assertThat(
TempDesktopFile._create_desktop_file_dir(desktop_file_dir),
Equals("")
)
def test_remove_desktop_file_removes_created_file_when_path_exists(self):
test_created_path = self.getUniqueString()
with patch('autopilot.tests.functional.fixtures.rmtree') as p_rmtree:
TempDesktopFile._remove_desktop_file_components(
test_created_path, ""
)
p_rmtree.assert_called_once_with(test_created_path)
def test_remove_desktop_file_removes_created_path(self):
test_created_file = self.getUniqueString()
with patch('autopilot.tests.functional.os.remove') as p_remove:
TempDesktopFile._remove_desktop_file_components(
"", test_created_file
)
p_remove.assert_called_once_with(test_created_file)
def test_create_desktop_file_creates_file_in_correct_place(self):
desktop_file_dir = tempfile.mkdtemp(dir="/tmp")
self.addCleanup(rmtree, desktop_file_dir)
with patch.object(
TempDesktopFile, '_desktop_file_dir', return_value=desktop_file_dir
):
desktop_file = TempDesktopFile._create_desktop_file("")
path, head = os.path.split(desktop_file)
self.assertThat(path, Equals(desktop_file_dir))
def test_create_desktop_file_writes_correct_data(self):
desktop_file_dir = tempfile.mkdtemp(dir="/tmp")
self.addCleanup(rmtree, desktop_file_dir)
token = self.getUniqueString()
with patch.object(
TempDesktopFile, '_desktop_file_dir', return_value=desktop_file_dir
):
desktop_file = TempDesktopFile._create_desktop_file(token)
self.assertTrue(desktop_file.endswith('.desktop'))
self.assertThat(desktop_file, FileContains(token))
def do_parameter_contents_test(self, matcher, **kwargs):
fixture = self.useFixture(TempDesktopFile(**kwargs))
self.assertThat(
fixture.get_desktop_file_path(),
FileContains(matcher=matcher),
)
def test_can_specify_exec_path(self):
token = self.getUniqueString()
self.do_parameter_contents_test(
Contains("Exec="+token),
exec_=token
)
def test_can_specify_type(self):
token = self.getUniqueString()
self.do_parameter_contents_test(
Contains("Type="+token),
type=token
)
def test_can_specify_name(self):
token = self.getUniqueString()
self.do_parameter_contents_test(
Contains("Name="+token),
name=token
)
def test_can_specify_icon(self):
token = self.getUniqueString()
self.do_parameter_contents_test(
Contains("Icon="+token),
icon=token
)
class ExecutableScriptTests(TestCase):
def test_creates_file_with_content(self):
token = self.getUniqueString()
fixture = self.useFixture(ExecutableScript(script=token))
self.assertThat(fixture.path, FileContains(token))
def test_creates_file_with_correct_extension(self):
token = self.getUniqueString()
fixture = self.useFixture(ExecutableScript(script="", extension=token))
self.assertThat(fixture.path, EndsWith(token))
def test_creates_file_with_execute_bit_set(self):
fixture = self.useFixture(ExecutableScript(script=""))
self.assertTrue(os.stat(fixture.path).st_mode & stat.S_IXUSR)
class TimezoneFixtureTests(TestCase):
def test_sets_environment_variable_to_timezone(self):
token = self.getUniqueString()
self.useFixture(Timezone(token))
self.assertEqual(os.environ.get('TZ'), token)
def test_resets_timezone_back_to_original(self):
original_tz = os.environ.get('TZ', None)
token = self.getUniqueString()
with Timezone(token):
pass # Trigger cleanup
self.assertEqual(os.environ.get('TZ', None), original_tz)
./autopilot/tests/unit/test_types.py 0000644 0000041 0000041 00000073474 13061552435 020204 0 ustar www-data www-data # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Autopilot Functional Test Tool
# Copyright (C) 2013 Canonical
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
from datetime import datetime, time
from testscenarios import TestWithScenarios, multiply_scenarios
from testtools import TestCase
from testtools.matchers import Equals, IsInstance, NotEquals, raises
import dbus
from unittest.mock import patch, Mock
from autopilot.tests.functional.fixtures import Timezone
from autopilot.introspection.types import (
Color,
create_value_instance,
DateTime,
PlainType,
Point,
Point3D,
Rectangle,
Size,
Time,
ValueType,
_integer_repr,
_boolean_repr,
_text_repr,
_bytes_repr,
_dict_repr,
_list_repr,
_float_repr,
_tuple_repr,
_get_repr_callable_for_value_class,
_boolean_str,
_integer_str,
)
from autopilot.introspection.dbus import DBusIntrospectionObject
from autopilot.utilities import compatible_repr
from dateutil import tz
class PlainTypeTests(TestWithScenarios, TestCase):
scenarios = [
('bool true', dict(t=dbus.Boolean, v=True)),
('bool false', dict(t=dbus.Boolean, v=False)),
('byte', dict(t=dbus.Byte, v=12)),
('int16 +ve', dict(t=dbus.Int16, v=123)),
('int16 -ve', dict(t=dbus.Int16, v=-23000)),
('int32 +ve', dict(t=dbus.Int32, v=30000000)),
('int32 -ve', dict(t=dbus.Int32, v=-3002050)),
('int64 +ve', dict(t=dbus.Int64, v=9223372036854775807)),
('int64 -ve', dict(t=dbus.Int64, v=-9223372036854775807)),
('ascii string', dict(t=dbus.String, v="Hello World")),
('unicode string', dict(t=dbus.String, v="\u2603")),
('bytearray', dict(t=dbus.ByteArray, v=b"Hello World")),
('object path', dict(t=dbus.ObjectPath, v="/path/to/object")),
('dbus signature', dict(t=dbus.Signature, v="is")),
('dictionary', dict(t=dbus.Dictionary, v={'hello': 'world'})),
('double', dict(t=dbus.Double, v=3.1415)),
('struct', dict(t=dbus.Struct, v=('some', 42, 'value'))),
('array', dict(t=dbus.Array, v=['some', 42, 'value'])),
]
def test_can_construct(self):
p = PlainType(self.t(self.v))
self.assertThat(p, Equals(self.v))
self.assertThat(hasattr(p, 'wait_for'), Equals(True))
self.assertThat(p, IsInstance(self.t))
def test_repr(self):
"""repr for PlainType must be the same as the pythonic type."""
p = PlainType(self.t(self.v))
expected = repr(self.v)
expected = expected.rstrip('L')
self.assertThat(repr(p), Equals(expected))
def test_str(self):
"""str(p) for PlainType must be the same as the pythonic type."""
p = PlainType(self.t(self.v))
expected = str(self.v)
observed = str(p)
self.assertEqual(expected, observed)
def test_wait_for_raises_RuntimeError(self):
"""The wait_for method must raise a RuntimeError if it's called."""
p = PlainType(self.t(self.v))
self.assertThat(
lambda: p.wait_for(object()),
raises(RuntimeError(
"This variable was not constructed as part of "
"an object. The wait_for method cannot be used."
))
)
class RectangleTypeTests(TestCase):
def test_can_construct_rectangle(self):
r = Rectangle(1, 2, 3, 4)
self.assertThat(r, IsInstance(dbus.Array))
def test_rectangle_has_xywh_properties(self):
r = Rectangle(1, 2, 3, 4)
self.assertThat(r.x, Equals(1))
self.assertThat(r.y, Equals(2))
self.assertThat(r.w, Equals(3))
self.assertThat(r.width, Equals(3))
self.assertThat(r.h, Equals(4))
self.assertThat(r.height, Equals(4))
def test_rectangle_has_slice_access(self):
r = Rectangle(1, 2, 3, 4)
self.assertThat(r[0], Equals(1))
self.assertThat(r[1], Equals(2))
self.assertThat(r[2], Equals(3))
self.assertThat(r[3], Equals(4))
def test_equality_with_rectangle(self):
r1 = Rectangle(1, 2, 3, 4)
r2 = Rectangle(1, 2, 3, 4)
self.assertThat(r1, Equals(r2))
def test_equality_with_list(self):
r1 = Rectangle(1, 2, 3, 4)
r2 = [1, 2, 3, 4]
self.assertThat(r1, Equals(r2))
def test_repr(self):
expected = repr_type("Rectangle(1, 2, 3, 4)")
observed = repr(Rectangle(1, 2, 3, 4))
self.assertEqual(expected, observed)
def test_repr_equals_str(self):
r = Rectangle(1, 2, 3, 4)
self.assertEqual(repr(r), str(r))
class PointTypeTests(TestCase):
def test_can_construct_point(self):
r = Point(1, 2)
self.assertThat(r, IsInstance(dbus.Array))
def test_point_has_xy_properties(self):
r = Point(1, 2)
self.assertThat(r.x, Equals(1))
self.assertThat(r.y, Equals(2))
def test_point_has_slice_access(self):
r = Point(1, 2)
self.assertThat(r[0], Equals(1))
self.assertThat(r[1], Equals(2))
def test_equality_with_point(self):
p1 = Point(1, 2)
p2 = Point(1, 2)
self.assertThat(p1, Equals(p2))
def test_equality_with_list(self):
p1 = Point(1, 2)
p2 = [1, 2]
self.assertThat(p1, Equals(p2))
def test_repr(self):
expected = repr_type('Point(1, 2)')
observed = repr(Point(1, 2))
self.assertEqual(expected, observed)
def test_repr_equals_str(self):
p = Point(1, 2)
self.assertEqual(repr(p), str(p))
class SizeTypeTests(TestCase):
def test_can_construct_size(self):
r = Size(1, 2)
self.assertThat(r, IsInstance(dbus.Array))
def test_size_has_wh_properties(self):
r = Size(1, 2)
self.assertThat(r.w, Equals(1))
self.assertThat(r.width, Equals(1))
self.assertThat(r.h, Equals(2))
self.assertThat(r.height, Equals(2))
def test_size_has_slice_access(self):
r = Size(1, 2)
self.assertThat(r[0], Equals(1))
self.assertThat(r[1], Equals(2))
def test_equality_with_size(self):
s1 = Size(50, 100)
s2 = Size(50, 100)
self.assertThat(s1, Equals(s2))
def test_equality_with_list(self):
s1 = Size(50, 100)
s2 = [50, 100]
self.assertThat(s1, Equals(s2))
def test_repr(self):
expected = repr_type('Size(1, 2)')
observed = repr(Size(1, 2))
self.assertEqual(expected, observed)
def test_repr_equals_str(self):
s = Size(3, 4)
self.assertEqual(repr(s), str(s))
class ColorTypeTests(TestCase):
def test_can_construct_color(self):
r = Color(123, 234, 55, 255)
self.assertThat(r, IsInstance(dbus.Array))
def test_color_has_rgba_properties(self):
r = Color(123, 234, 55, 255)
self.assertThat(r.red, Equals(123))
self.assertThat(r.green, Equals(234))
self.assertThat(r.blue, Equals(55))
self.assertThat(r.alpha, Equals(255))
def test_color_has_slice_access(self):
r = Color(123, 234, 55, 255)
self.assertThat(r[0], Equals(123))
self.assertThat(r[1], Equals(234))
self.assertThat(r[2], Equals(55))
self.assertThat(r[3], Equals(255))
def test_eqiality_with_color(self):
c1 = Color(123, 234, 55, 255)
c2 = Color(123, 234, 55, 255)
self.assertThat(c1, Equals(c2))
def test_eqiality_with_list(self):
c1 = Color(123, 234, 55, 255)
c2 = [123, 234, 55, 255]
self.assertThat(c1, Equals(c2))
def test_repr(self):
expected = repr_type('Color(1, 2, 3, 4)')
observed = repr(Color(1, 2, 3, 4))
self.assertEqual(expected, observed)
def test_repr_equals_str(self):
c = Color(255, 255, 255, 0)
self.assertEqual(repr(c), str(c))
def unable_to_handle_timestamp(timestamp):
"""Return false if the platform can handle timestamps larger than 32bit
limit.
"""
try:
datetime.fromtimestamp(timestamp)
return False
except:
return True
class DateTimeCreationTests(TestCase):
timestamp = 1405382400 # No significance, just a timestamp
def test_can_construct_datetime(self):
dt = DateTime(self.timestamp)
self.assertThat(dt, IsInstance(dbus.Array))
def test_datetime_has_slice_access(self):
dt = DateTime(self.timestamp)
self.assertThat(dt[0], Equals(self.timestamp))
def test_datetime_has_properties(self):
dt = DateTime(self.timestamp)
self.assertTrue(hasattr(dt, 'timestamp'))
self.assertTrue(hasattr(dt, 'year'))
self.assertTrue(hasattr(dt, 'month'))
self.assertTrue(hasattr(dt, 'day'))
self.assertTrue(hasattr(dt, 'hour'))
self.assertTrue(hasattr(dt, 'minute'))
self.assertTrue(hasattr(dt, 'second'))
def test_repr(self):
# Use a well known timezone for comparison
self.useFixture(Timezone('UTC'))
dt = DateTime(self.timestamp)
observed = repr(dt)
expected = "DateTime({:%Y-%m-%d %H:%M:%S})".format(
datetime.fromtimestamp(self.timestamp)
)
self.assertEqual(expected, observed)
def test_repr_equals_str(self):
dt = DateTime(self.timestamp)
self.assertEqual(repr(dt), str(dt))
def test_can_create_DateTime_using_large_timestamp(self):
"""Must be able to create a DateTime object using a timestamp larger
than the 32bit time_t limit.
"""
# Use a well known timezone for comparison
self.useFixture(Timezone('UTC'))
large_timestamp = 2**32+1
dt = DateTime(large_timestamp)
self.assertEqual(dt.year, 2106)
self.assertEqual(dt.month, 2)
self.assertEqual(dt.day, 7)
self.assertEqual(dt.hour, 6)
self.assertEqual(dt.minute, 28)
self.assertEqual(dt.second, 17)
self.assertEqual(dt.timestamp, large_timestamp)
class DateTimeTests(TestWithScenarios, TestCase):
timestamps = [
# This timestamp uncovered an issue during development.
('Explicit US/Pacific test', dict(
timestamp=1090123200
)),
('September 2014', dict(
timestamp=1411992000
)),
('NZ DST example', dict(
timestamp=2047570047
)),
('Winter', dict(
timestamp=1389744000
)),
('Summer', dict(
timestamp=1405382400
)),
('32bit max', dict(
timestamp=2**32+1
)),
('32bit limit', dict(
timestamp=2983579200
)),
]
timezones = [
('UTC', dict(
timezone='UTC'
)),
('London', dict(
timezone='Europe/London'
)),
('New Zealand', dict(
timezone='NZ',
)),
('Pacific', dict(
timezone='US/Pacific'
)),
('Hongkong', dict(
timezone='Hongkong'
)),
('Moscow', dict(
timezone='Europe/Moscow'
)),
('Copenhagen', dict(
timezone='Europe/Copenhagen',
)),
]
scenarios = multiply_scenarios(timestamps, timezones)
def skip_if_timestamp_too_large(self, timestamp):
if unable_to_handle_timestamp(self.timestamp):
self.skip("Timestamp to large for platform time_t")
def test_datetime_properties_have_correct_values(self):
self.skip_if_timestamp_too_large(self.timestamp)
self.useFixture(Timezone(self.timezone))
dt1 = DateTime(self.timestamp)
dt2 = datetime.fromtimestamp(self.timestamp, tz.gettz())
self.assertThat(dt1.year, Equals(dt2.year))
self.assertThat(dt1.month, Equals(dt2.month))
self.assertThat(dt1.day, Equals(dt2.day))
self.assertThat(dt1.hour, Equals(dt2.hour))
self.assertThat(dt1.minute, Equals(dt2.minute))
self.assertThat(dt1.second, Equals(dt2.second))
self.assertThat(dt1.timestamp, Equals(dt2.timestamp()))
def test_equality_with_datetime(self):
self.skip_if_timestamp_too_large(self.timestamp)
self.useFixture(Timezone(self.timezone))
dt1 = DateTime(self.timestamp)
dt2 = datetime(
dt1.year, dt1.month, dt1.day, dt1.hour, dt1.minute, dt1.second
)
self.assertThat(dt1, Equals(dt2))
def test_equality_with_list(self):
self.skip_if_timestamp_too_large(self.timestamp)
self.useFixture(Timezone(self.timezone))
dt1 = DateTime(self.timestamp)
dt2 = [self.timestamp]
self.assertThat(dt1, Equals(dt2))
def test_equality_with_datetime_object(self):
self.skip_if_timestamp_too_large(self.timestamp)
self.useFixture(Timezone(self.timezone))
dt1 = DateTime(self.timestamp)
dt2 = datetime.fromtimestamp(self.timestamp, tz.gettz())
dt3 = datetime.fromtimestamp(self.timestamp + 1, tz.gettz())
self.assertThat(dt1, Equals(dt2))
self.assertThat(dt1, NotEquals(dt3))
def test_can_convert_to_datetime(self):
self.skip_if_timestamp_too_large(self.timestamp)
dt1 = DateTime(self.timestamp)
self.assertThat(dt1.datetime, IsInstance(datetime))
class TimeTests(TestCase):
def test_can_construct_time(self):
dt = Time(0, 0, 0, 0)
self.assertThat(dt, IsInstance(dbus.Array))
def test_time_has_slice_access(self):
dt = Time(0, 1, 2, 3)
self.assertThat(dt[0], Equals(0))
self.assertThat(dt[1], Equals(1))
self.assertThat(dt[2], Equals(2))
self.assertThat(dt[3], Equals(3))
def test_time_has_properties(self):
dt = Time(0, 1, 2, 3)
self.assertThat(dt.hour, Equals(0))
self.assertThat(dt.minute, Equals(1))
self.assertThat(dt.second, Equals(2))
self.assertThat(dt.millisecond, Equals(3))
def test_equality_with_time(self):
dt1 = Time(0, 1, 2, 3)
dt2 = Time(0, 1, 2, 3)
dt3 = Time(4, 1, 2, 3)
self.assertThat(dt1, Equals(dt2))
self.assertThat(dt1, NotEquals(dt3))
def test_equality_with_real_time(self):
dt1 = Time(2, 3, 4, 5)
dt2 = time(2, 3, 4, 5000)
dt3 = time(5, 4, 3, 2000)
self.assertThat(dt1, Equals(dt2))
self.assertThat(dt1, NotEquals(dt3))
def test_can_convert_to_time(self):
dt1 = Time(1, 2, 3, 4)
self.assertThat(dt1.time, IsInstance(time))
def test_repr(self):
expected = repr_type('Time(01:02:03.004)')
observed = repr(Time(1, 2, 3, 4))
self.assertEqual(expected, observed)
def test_repr_equals_str(self):
t = Time(2, 3, 4, 5)
self.assertEqual(repr(t), str(t))
class Point3DTypeTests(TestCase):
def test_can_construct_point3d(self):
r = Point3D(1, 2, 3)
self.assertThat(r, IsInstance(dbus.Array))
def test_point3d_has_xyz_properties(self):
r = Point3D(1, 2, 3)
self.assertThat(r.x, Equals(1))
self.assertThat(r.y, Equals(2))
self.assertThat(r.z, Equals(3))
def test_point3d_has_slice_access(self):
r = Point3D(1, 2, 3)
self.assertThat(r[0], Equals(1))
self.assertThat(r[1], Equals(2))
self.assertThat(r[2], Equals(3))
def test_equality_with_point3d(self):
p1 = Point3D(1, 2, 3)
p2 = Point3D(1, 2, 3)
self.assertThat(p1, Equals(p2))
def test_inequality_with_point3d(self):
p1 = Point3D(1, 2, 3)
p2 = Point3D(1, 2, 4)
self.assertThat(p1, NotEquals(p2))
def test_equality_with_list(self):
p1 = Point3D(1, 2, 3)
p2 = [1, 2, 3]
self.assertThat(p1, Equals(p2))
def test_inequality_with_list(self):
p1 = Point3D(1, 2, 3)
p2 = [1, 2, 4]
self.assertThat(p1, NotEquals(p2))
def test_repr(self):
expected = repr_type('Point3D(1, 2, 3)')
observed = repr(Point3D(1, 2, 3))
self.assertEqual(expected, observed)
def test_repr_equals_str(self):
p3d = Point3D(1, 2, 3)
self.assertEqual(repr(p3d), str(p3d))
class CreateValueInstanceTests(TestCase):
"""Tests to check that create_value_instance does the right thing."""
def test_plain_string(self):
data = dbus.Array([dbus.Int32(ValueType.PLAIN), dbus.String("Hello")])
attr = create_value_instance(data, None, None)
self.assertThat(attr, Equals("Hello"))
self.assertThat(attr, IsInstance(PlainType))
def test_plain_boolean(self):
data = dbus.Array([dbus.Int32(ValueType.PLAIN), dbus.Boolean(False)])
attr = create_value_instance(data, None, None)
self.assertThat(attr, Equals(False))
self.assertThat(attr, IsInstance(PlainType))
def test_plain_int16(self):
data = dbus.Array([dbus.Int32(ValueType.PLAIN), dbus.Int16(-2**14)])
attr = create_value_instance(data, None, None)
self.assertThat(attr, Equals(-2**14))
self.assertThat(attr, IsInstance(PlainType))
def test_plain_int32(self):
data = dbus.Array([dbus.Int32(ValueType.PLAIN), dbus.Int32(-2**30)])
attr = create_value_instance(data, None, None)
self.assertThat(attr, Equals(-2**30))
self.assertThat(attr, IsInstance(PlainType))
def test_plain_int64(self):
data = dbus.Array([dbus.Int32(ValueType.PLAIN), dbus.Int64(-2**40)])
attr = create_value_instance(data, None, None)
self.assertThat(attr, Equals(-2**40))
self.assertThat(attr, IsInstance(PlainType))
def test_plain_uint16(self):
data = dbus.Array([dbus.Int32(ValueType.PLAIN), dbus.UInt16(2**14)])
attr = create_value_instance(data, None, None)
self.assertThat(attr, Equals(2**14))
self.assertThat(attr, IsInstance(PlainType))
def test_plain_uint32(self):
data = dbus.Array([dbus.Int32(ValueType.PLAIN), dbus.UInt32(2**30)])
attr = create_value_instance(data, None, None)
self.assertThat(attr, Equals(2**30))
self.assertThat(attr, IsInstance(PlainType))
def test_plain_uint64(self):
data = dbus.Array([dbus.Int32(ValueType.PLAIN), dbus.UInt64(2**40)])
attr = create_value_instance(data, None, None)
self.assertThat(attr, Equals(2**40))
self.assertThat(attr, IsInstance(PlainType))
def test_plain_array(self):
data = dbus.Array([
dbus.Int32(ValueType.PLAIN),
dbus.Array([
dbus.String("Hello"),
dbus.String("World")
])
])
attr = create_value_instance(data, None, None)
self.assertThat(attr, Equals(["Hello", "World"]))
self.assertThat(attr, IsInstance(PlainType))
def test_rectangle(self):
data = dbus.Array(
[
dbus.Int32(ValueType.RECTANGLE),
dbus.Int32(0),
dbus.Int32(10),
dbus.Int32(20),
dbus.Int32(30),
]
)
attr = create_value_instance(data, None, None)
self.assertThat(attr, IsInstance(Rectangle))
def test_invalid_rectangle(self):
data = dbus.Array(
[
dbus.Int32(ValueType.RECTANGLE),
dbus.Int32(0),
]
)
fn = lambda: create_value_instance(data, None, None)
self.assertThat(fn, raises(
ValueError("Rectangle must be constructed with 4 arguments, not 1")
))
def test_color(self):
data = dbus.Array(
[
dbus.Int32(ValueType.COLOR),
dbus.Int32(10),
dbus.Int32(20),
dbus.Int32(230),
dbus.Int32(255),
]
)
attr = create_value_instance(data, None, None)
self.assertThat(attr, IsInstance(Color))
def test_invalid_color(self):
data = dbus.Array(
[
dbus.Int32(ValueType.COLOR),
dbus.Int32(0),
]
)
fn = lambda: create_value_instance(data, None, None)
self.assertThat(fn, raises(
ValueError("Color must be constructed with 4 arguments, not 1")
))
def test_point(self):
data = dbus.Array(
[
dbus.Int32(ValueType.POINT),
dbus.Int32(0),
dbus.Int32(10),
]
)
attr = create_value_instance(data, None, None)
self.assertThat(attr, IsInstance(Point))
def test_invalid_point(self):
data = dbus.Array(
[
dbus.Int32(ValueType.POINT),
dbus.Int32(0),
dbus.Int32(0),
dbus.Int32(0),
]
)
fn = lambda: create_value_instance(data, None, None)
self.assertThat(fn, raises(
ValueError("Point must be constructed with 2 arguments, not 3")
))
def test_size(self):
data = dbus.Array(
[
dbus.Int32(ValueType.SIZE),
dbus.Int32(0),
dbus.Int32(10),
]
)
attr = create_value_instance(data, None, None)
self.assertThat(attr, IsInstance(Size))
def test_invalid_size(self):
data = dbus.Array(
[
dbus.Int32(ValueType.SIZE),
dbus.Int32(0),
]
)
fn = lambda: create_value_instance(data, None, None)
self.assertThat(fn, raises(
ValueError("Size must be constructed with 2 arguments, not 1")
))
def test_date_time(self):
data = dbus.Array(
[
dbus.Int32(ValueType.DATETIME),
dbus.Int32(0),
]
)
attr = create_value_instance(data, None, None)
self.assertThat(attr, IsInstance(DateTime))
def test_invalid_date_time(self):
data = dbus.Array(
[
dbus.Int32(ValueType.DATETIME),
dbus.Int32(0),
dbus.Int32(0),
dbus.Int32(0),
]
)
fn = lambda: create_value_instance(data, None, None)
self.assertThat(fn, raises(
ValueError("DateTime must be constructed with 1 arguments, not 3")
))
def test_time(self):
data = dbus.Array(
[
dbus.Int32(ValueType.TIME),
dbus.Int32(0),
dbus.Int32(0),
dbus.Int32(0),
dbus.Int32(0),
]
)
attr = create_value_instance(data, None, None)
self.assertThat(attr, IsInstance(Time))
def test_invalid_time(self):
data = dbus.Array(
[
dbus.Int32(ValueType.TIME),
dbus.Int32(0),
dbus.Int32(0),
dbus.Int32(0),
]
)
fn = lambda: create_value_instance(data, None, None)
self.assertThat(fn, raises(
ValueError("Time must be constructed with 4 arguments, not 3")
))
def test_unknown_type_id(self):
"""Unknown type Ids should result in a plain type, along with a log
message.
"""
data = dbus.Array(
[
dbus.Int32(543),
dbus.Int32(0),
dbus.Boolean(False),
dbus.String("Hello World")
]
)
attr = create_value_instance(data, None, None)
self.assertThat(attr, IsInstance(PlainType))
self.assertThat(attr, IsInstance(dbus.Array))
self.assertThat(attr, Equals([0, False, "Hello World"]))
def test_invalid_no_data(self):
data = dbus.Array(
[
dbus.Int32(0),
]
)
fn = lambda: create_value_instance(data, None, None)
self.assertThat(fn, raises(
ValueError("Cannot create attribute, no data supplied")
))
def test_point3d(self):
data = dbus.Array(
[
dbus.Int32(ValueType.POINT3D),
dbus.Int32(0),
dbus.Int32(10),
dbus.Int32(20),
]
)
attr = create_value_instance(data, None, None)
self.assertThat(attr, IsInstance(Point3D))
def test_invalid_point3d(self):
data = dbus.Array(
[
dbus.Int32(ValueType.POINT3D),
dbus.Int32(0),
dbus.Int32(0),
]
)
fn = lambda: create_value_instance(data, None, None)
self.assertThat(fn, raises(
ValueError("Point3D must be constructed with 3 arguments, not 2")
))
class DBusIntrospectionObjectTests(TestCase):
@patch('autopilot.introspection.dbus._logger.warning')
def test_dbus_introspection_object_logs_bad_data(self, error_logger):
"""The DBusIntrospectionObject class must log an error when it gets
bad data from the autopilot backend.
"""
DBusIntrospectionObject(
dict(foo=[0], id=[0, 42]),
b'/some/dummy/path',
Mock()
)
error_logger.assert_called_once_with(
"While constructing attribute '%s.%s': %s",
"ProxyBase",
"foo",
"Cannot create attribute, no data supplied"
)
class TypeReprTests(TestCase):
def test_integer_repr(self):
expected = repr_type('42')
observed = _integer_repr(42)
self.assertEqual(expected, observed)
def test_dbus_int_types_all_work(self):
expected = repr_type('42')
int_types = (
dbus.Byte,
dbus.Int16,
dbus.Int32,
dbus.UInt16,
dbus.UInt32,
dbus.Int64,
dbus.UInt64,
)
for t in int_types:
observed = _integer_repr(t(42))
self.assertEqual(expected, observed)
def test_get_repr_gets_integer_repr_for_all_integer_types(self):
int_types = (
dbus.Byte,
dbus.Int16,
dbus.Int32,
dbus.UInt16,
dbus.UInt32,
dbus.Int64,
dbus.UInt64,
)
for t in int_types:
observed = _get_repr_callable_for_value_class(t)
self.assertEqual(_integer_repr, observed)
def test_boolean_repr_true(self):
expected = repr_type('True')
for values in (True, dbus.Boolean(True)):
observed = _boolean_repr(True)
self.assertEqual(expected, observed)
def test_boolean_repr_false(self):
expected = repr_type('False')
for values in (False, dbus.Boolean(False)):
observed = _boolean_repr(False)
self.assertEqual(expected, observed)
def test_get_repr_gets_boolean_repr_for_dbus_boolean_type(self):
observed = _get_repr_callable_for_value_class(dbus.Boolean)
self.assertEqual(_boolean_repr, observed)
def test_text_repr_handles_dbus_string(self):
unicode_text = "plɹoʍ ollǝɥ"
observed = _text_repr(dbus.String(unicode_text))
self.assertEqual(repr(unicode_text), observed)
def test_text_repr_handles_dbus_object_path(self):
path = "/path/to/some/object"
observed = _text_repr(dbus.ObjectPath(path))
self.assertEqual(repr(path), observed)
def test_binry_repr_handles_dbys_byte_array(self):
data = b'Some bytes'
observed = _bytes_repr(dbus.ByteArray(data))
self.assertEqual(repr(data), observed)
def test_get_repr_gets_bytes_repr_for_dbus_byte_array(self):
observed = _get_repr_callable_for_value_class(dbus.ByteArray)
self.assertEqual(_bytes_repr, observed)
def test_dict_repr_handles_dbus_dictionary(self):
token = dict(foo='bar')
observed = _dict_repr(dbus.Dictionary(token))
self.assertEqual(repr(token), observed)
def test_get_repr_gets_dict_repr_on_dbus_dictionary(self):
observed = _get_repr_callable_for_value_class(dbus.Dictionary)
self.assertEqual(_dict_repr, observed)
def test_float_repr_handles_dbus_double(self):
token = 1.2345
observed = _float_repr(token)
self.assertEqual(repr(token), observed)
def test_get_repr_gets_float_repr_on_dbus_double(self):
observed = _get_repr_callable_for_value_class(dbus.Double)
self.assertEqual(_float_repr, observed)
def test_tuple_repr_handles_dbus_struct(self):
data = (1, 2, 3)
observed = _tuple_repr(dbus.Struct(data))
self.assertEqual(repr(data), observed)
def test_get_repr_gets_tuple_repr_on_dbus_struct(self):
observed = _get_repr_callable_for_value_class(dbus.Struct)
self.assertEqual(_tuple_repr, observed)
def test_list_repr_handles_dbus_array(self):
data = [1, 2, 3]
observed = _list_repr(dbus.Array(data))
self.assertEqual(repr(data), observed)
def test_get_repr_gets_list_repr_on_dbus_array(self):
observed = _get_repr_callable_for_value_class(dbus.Array)
self.assertEqual(_list_repr, observed)
class TypeStrTests(TestCase):
def test_boolean_str_handles_dbus_boolean(self):
observed = _boolean_str(dbus.Boolean(False))
self.assertEqual(str(False), observed)
def test_integer_str_handles_dbus_byte(self):
observed = _integer_str(dbus.Byte(14))
self.assertEqual(str(14), observed)
def repr_type(value):
"""Convert a text or bytes object into the appropriate return type for
the __repr__ method."""
return compatible_repr(lambda: value)()
./autopilot/tests/unit/test_testresults.py 0000644 0000041 0000041 00000026005 13061552435 021425 0 ustar www-data www-data # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Autopilot Functional Test Tool
# Copyright (C) 2013 Canonical
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
import codecs
from unittest.mock import Mock, patch
import os
import tempfile
from fixtures import FakeLogger
from testtools import TestCase, PlaceHolder
from testtools.content import Content, ContentType, text_content
from testtools.matchers import Contains, raises, NotEquals
from testscenarios import WithScenarios
import unittest
from autopilot import testresult
from autopilot import run
from autopilot.testcase import multiply_scenarios
from autopilot.tests.unit.fixtures import AutopilotVerboseLogging
class LoggedTestResultDecoratorTests(TestCase):
def construct_simple_content_object(self):
return text_content(self.getUniqueString())
def test_can_construct(self):
testresult.LoggedTestResultDecorator(Mock())
def test_addSuccess_calls_decorated_test(self):
wrapped = Mock()
result = testresult.LoggedTestResultDecorator(wrapped)
fake_test = PlaceHolder('fake_test')
fake_details = self.construct_simple_content_object()
result.addSuccess(fake_test, fake_details)
wrapped.addSuccess.assert_called_once_with(
fake_test,
details=fake_details
)
def test_addError_calls_decorated_test(self):
wrapped = Mock()
result = testresult.LoggedTestResultDecorator(wrapped)
fake_test = PlaceHolder('fake_test')
fake_error = object()
fake_details = self.construct_simple_content_object()
result.addError(fake_test, fake_error, fake_details)
wrapped.addError.assert_called_once_with(
fake_test,
fake_error,
details=fake_details
)
def test_addFailure_calls_decorated_test(self):
wrapped = Mock()
result = testresult.LoggedTestResultDecorator(wrapped)
fake_test = PlaceHolder('fake_test')
fake_error = object()
fake_details = self.construct_simple_content_object()
result.addFailure(fake_test, fake_error, fake_details)
wrapped.addFailure.assert_called_once_with(
fake_test,
fake_error,
details=fake_details
)
def test_log_details_handles_binary_data(self):
fake_details = dict(
TestBinary=Content(ContentType('image', 'png'), lambda: b'')
)
result = testresult.LoggedTestResultDecorator(None)
result._log_details(0, fake_details)
def test_log_details_logs_binary_attachment_details(self):
fake_test = Mock()
fake_test.getDetails = lambda: dict(
TestBinary=Content(ContentType('image', 'png'), lambda: b'')
)
result = testresult.LoggedTestResultDecorator(None)
with patch.object(result, '_log') as p_log:
result._log_details(0, fake_test)
p_log.assert_called_once_with(
0,
"Binary attachment: \"{name}\" ({type})".format(
name="TestBinary",
type="image/png"
)
)
class TestResultLogMessageTests(WithScenarios, TestCase):
scenarios = multiply_scenarios(
# Scenarios for each format we support:
[(f, dict(format=f)) for f in testresult.get_output_formats().keys()],
# Scenarios for each test outcome:
[
('success', dict(outcome='addSuccess', log='OK: %s')),
('error', dict(outcome='addError', log='ERROR: %s')),
('fail', dict(outcome='addFailure', log='FAIL: %s')),
(
'unexpected success',
dict(
outcome='addUnexpectedSuccess',
log='UNEXPECTED SUCCESS: %s',
)
),
('skip', dict(outcome='addSkip', log='SKIP: %s')),
(
'expected failure',
dict(
outcome='addExpectedFailure',
log='EXPECTED FAILURE: %s',
)
),
]
)
def make_result_object(self):
output_path = tempfile.mktemp()
self.addCleanup(remove_if_exists, output_path)
result_constructor = testresult.get_output_formats()[self.format]
return result_constructor(
stream=run.get_output_stream(self.format, output_path),
failfast=False,
)
def test_outcome_logs(self):
test_id = self.getUniqueString()
test = PlaceHolder(test_id, outcome=self.outcome)
result = self.make_result_object()
result.startTestRun()
self.useFixture(AutopilotVerboseLogging())
with FakeLogger() as log:
test.run(result)
self.assertThat(log.output, Contains(self.log % test_id))
class OutputFormatFactoryTests(TestCase):
def test_has_text_format(self):
self.assertTrue('text' in testresult.get_output_formats())
def test_has_xml_format(self):
self.assertTrue('xml' in testresult.get_output_formats())
def test_has_subunit_format(self):
self.assertTrue('subunit' in testresult.get_output_formats())
def test_default_format_is_available(self):
self.assertThat(
testresult.get_output_formats(),
Contains(testresult.get_default_format())
)
class TestResultOutputStreamTests(WithScenarios, TestCase):
scenarios = [
(f, dict(format=f)) for f in testresult.get_output_formats().keys()
]
def get_supported_options(self, **kwargs):
"""Get a dictionary of all supported keyword arguments for the current
result class.
Pass in keyword arguments to override default options.
"""
output_path = tempfile.mktemp()
self.addCleanup(remove_if_exists, output_path)
options = {
'stream': run.get_output_stream(self.format, output_path),
'failfast': False
}
options.update(kwargs)
return options
def run_test_with_result(self, test_suite, **kwargs):
"""Run the given test with the current result object.
Returns the test result and output file path.
Use keyword arguments to alter result object options.
"""
ResultClass = testresult.get_output_formats()[self.format]
result_options = self.get_supported_options(**kwargs)
output_path = result_options['stream'].name
result = ResultClass(**result_options)
result.startTestRun()
test_result = test_suite.run(result)
result.stopTestRun()
result_options['stream'].flush()
return test_result, output_path
def test_factory_function_is_a_callable(self):
self.assertTrue(
callable(testresult.get_output_formats()[self.format])
)
def test_factory_callable_raises_on_unknown_kwargs(self):
factory_fn = testresult.get_output_formats()[self.format]
options = self.get_supported_options()
options['unknown_kwarg'] = True
self.assertThat(
lambda: factory_fn(**options),
raises(ValueError)
)
def test_creates_non_empty_file_on_passing_test(self):
class PassingTests(TestCase):
def test_passes(self):
pass
test_result, output_path = self.run_test_with_result(
PassingTests('test_passes')
)
self.assertTrue(test_result.wasSuccessful())
self.assertThat(open(output_path, 'rb').read(), NotEquals(b''))
def test_creates_non_empty_file_on_failing_test(self):
class FailingTests(TestCase):
def test_fails(self):
self.fail("Failing Test: ")
test_result, output_path = self.run_test_with_result(
FailingTests('test_fails')
)
self.assertFalse(test_result.wasSuccessful())
self.assertThat(open(output_path, 'rb').read(), NotEquals(b''))
def test_creates_non_empty_file_on_erroring_test(self):
class ErroringTests(TestCase):
def test_errors(self):
raise RuntimeError("Uncaught Exception!")
test_result, output_path = self.run_test_with_result(
ErroringTests('test_errors')
)
self.assertFalse(test_result.wasSuccessful())
self.assertThat(open(output_path, 'rb').read(), NotEquals(b''))
def test_creates_non_empty_log_file_when_failing_with_unicode(self):
class FailingTests(TestCase):
def test_fails_unicode(self):
self.fail(
'\xa1pl\u0279oM \u01ddpo\u0254\u0131u\u2229 oll\u01ddH'
)
test_result, output_path = self.run_test_with_result(
FailingTests('test_fails_unicode')
)
# We need to specify 'errors="ignore"' because subunit write non-valid
# unicode data.
log_contents = codecs.open(
output_path,
'r',
encoding='utf-8',
errors='ignore'
).read()
self.assertFalse(test_result.wasSuccessful())
self.assertThat(
log_contents,
Contains('\xa1pl\u0279oM \u01ddpo\u0254\u0131u\u2229 oll\u01ddH')
)
def test_result_object_supports_many_tests(self):
class ManyFailingTests(TestCase):
def test_fail1(self):
self.fail("Failing test")
def test_fail2(self):
self.fail("Failing test")
suite = unittest.TestSuite(
tests=(
ManyFailingTests('test_fail1'),
ManyFailingTests('test_fail2'),
)
)
test_result, output_path = self.run_test_with_result(suite)
self.assertFalse(test_result.wasSuccessful())
self.assertEqual(2, test_result.testsRun)
def test_result_object_supports_failfast(self):
class ManyFailingTests(TestCase):
def test_fail1(self):
self.fail("Failing test")
def test_fail2(self):
self.fail("Failing test")
suite = unittest.TestSuite(
tests=(
ManyFailingTests('test_fail1'),
ManyFailingTests('test_fail2'),
)
)
test_result, output_path = self.run_test_with_result(
suite,
failfast=True
)
self.assertFalse(test_result.wasSuccessful())
self.assertEqual(1, test_result.testsRun)
def remove_if_exists(path):
if os.path.exists(path):
os.remove(path)
./autopilot/tests/unit/test_content.py 0000644 0000041 0000041 00000011026 13061552435 020473 0 ustar www-data www-data # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Autopilot Functional Test Tool
# Copyright (C) 2013 Canonical
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
from tempfile import NamedTemporaryFile
from unittest.mock import Mock, patch
import os
from testtools import TestCase
from testtools.matchers import Contains, Equals, Not, Raises
from autopilot.content import follow_file
class FileFollowerTests(TestCase):
def test_follow_file_adds_addDetail_cleanup(self):
fake_test = Mock()
with NamedTemporaryFile() as f:
follow_file(f.name, fake_test)
self.assertTrue(fake_test.addCleanup.called)
fake_test.addCleanup.call_args[0][0]()
self.assertTrue(fake_test.addDetail.called)
def test_follow_file_content_object_contains_new_file_data(self):
fake_test = Mock()
with NamedTemporaryFile() as f:
follow_file(f.name, fake_test)
f.write(b"Hello")
f.flush()
fake_test.addCleanup.call_args[0][0]()
actual = fake_test.addDetail.call_args[0][1].as_text()
self.assertEqual("Hello", actual)
def test_follow_file_does_not_contain_old_file_data(self):
fake_test = Mock()
with NamedTemporaryFile() as f:
f.write(b"Hello")
f.flush()
follow_file(f.name, fake_test)
f.write(b"World")
f.flush()
fake_test.addCleanup.call_args[0][0]()
actual = fake_test.addDetail.call_args[0][1].as_text()
self.assertEqual("World", actual)
def test_follow_file_uses_filename_by_default(self):
fake_test = Mock()
with NamedTemporaryFile() as f:
follow_file(f.name, fake_test)
fake_test.addCleanup.call_args[0][0]()
actual = fake_test.addDetail.call_args[0][0]
self.assertEqual(f.name, actual)
def test_follow_file_uses_content_name(self):
fake_test = Mock()
content_name = self.getUniqueString()
with NamedTemporaryFile() as f:
follow_file(f.name, fake_test, content_name)
fake_test.addCleanup.call_args[0][0]()
actual = fake_test.addDetail.call_args[0][0]
self.assertEqual(content_name, actual)
def test_follow_file_does_not_raise_on_IOError(self):
fake_test = Mock()
content_name = self.getUniqueString()
with NamedTemporaryFile() as f:
os.chmod(f.name, 0)
self.assertThat(
lambda: follow_file(f.name, fake_test, content_name),
Not(Raises())
)
def test_follow_file_logs_error_on_IOError(self):
fake_test = Mock()
content_name = self.getUniqueString()
with NamedTemporaryFile() as f:
os.chmod(f.name, 0)
with patch('autopilot.content._logger') as fake_logger:
follow_file(f.name, fake_test, content_name)
fake_logger.error.assert_called_once_with(
"Could not add content object '%s' due to IO Error: %s",
content_name,
"[Errno 13] Permission denied: '%s'" % f.name
)
def test_follow_file_returns_empty_content_object_on_error(self):
fake_test = Mock()
content_name = self.getUniqueString()
with NamedTemporaryFile() as f:
os.chmod(f.name, 0)
content_obj = follow_file(f.name, fake_test, content_name)
self.assertThat(content_obj.as_text(), Equals(''))
def test_real_test_has_detail_added(self):
with NamedTemporaryFile() as f:
class FakeTest(TestCase):
def test_foo(self):
follow_file(f.name, self)
f.write(b"Hello")
f.flush()
test = FakeTest('test_foo')
result = test.run()
self.assertTrue(result.wasSuccessful)
self.assertThat(test.getDetails(), Contains(f.name))
./autopilot/tests/unit/test_application_launcher.py 0000644 0000041 0000041 00000124134 13061552435 023212 0 ustar www-data www-data # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Autopilot Functional Test Tool
# Copyright (C) 2013,2017 Canonical
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
from contextlib import ExitStack
from gi.repository import GLib
import signal
import subprocess
from testtools import TestCase
from testtools.matchers import (
Contains,
Equals,
GreaterThan,
HasLength,
IsInstance,
MatchesListwise,
Not,
raises,
)
from testtools.content import text_content
from unittest.mock import MagicMock, Mock, patch
from autopilot.application import (
ClickApplicationLauncher,
NormalApplicationLauncher,
UpstartApplicationLauncher,
)
from autopilot.application._environment import (
GtkApplicationEnvironment,
QtApplicationEnvironment,
)
import autopilot.application._launcher as _l
from autopilot.application._launcher import (
ApplicationLauncher,
get_application_launcher_wrapper,
launch_process,
_attempt_kill_pid,
_get_app_env_from_string_hint,
_get_application_environment,
_get_application_path,
_get_click_app_id,
_get_click_manifest,
_is_process_running,
_kill_process,
)
from autopilot.utilities import sleep
class ApplicationLauncherTests(TestCase):
def test_raises_on_attempt_to_use_launch(self):
self.assertThat(
lambda: ApplicationLauncher(self.addDetail).launch(),
raises(
NotImplementedError("Sub-classes must implement this method.")
)
)
def test_init_uses_default_values(self):
launcher = ApplicationLauncher()
self.assertEqual(launcher.caseAddDetail, launcher.addDetail)
self.assertEqual(launcher.proxy_base, None)
self.assertEqual(launcher.dbus_bus, 'session')
def test_init_uses_passed_values(self):
case_addDetail = self.getUniqueString()
emulator_base = self.getUniqueString()
dbus_bus = self.getUniqueString()
launcher = ApplicationLauncher(
case_addDetail=case_addDetail,
emulator_base=emulator_base,
dbus_bus=dbus_bus,
)
self.assertEqual(launcher.caseAddDetail, case_addDetail)
self.assertEqual(launcher.proxy_base, emulator_base)
self.assertEqual(launcher.dbus_bus, dbus_bus)
@patch('autopilot.application._launcher.fixtures.EnvironmentVariable')
def test_setUp_patches_environment(self, ev):
self.useFixture(ApplicationLauncher(dbus_bus=''))
ev.assert_called_with('DBUS_SESSION_BUS_ADDRESS', '')
class NormalApplicationLauncherTests(TestCase):
def test_kill_process_and_attach_logs(self):
mock_addDetail = Mock()
app_launcher = NormalApplicationLauncher(mock_addDetail)
with patch.object(
_l, '_kill_process', return_value=("stdout", "stderr", 0)
):
app_launcher._kill_process_and_attach_logs(0, 'app')
self.assertThat(
mock_addDetail.call_args_list,
MatchesListwise([
Equals(
[('process-return-code (app)', text_content('0')), {}]
),
Equals(
[('process-stdout (app)', text_content('stdout')), {}]
),
Equals(
[('process-stderr (app)', text_content('stderr')), {}]
),
])
)
def test_setup_environment_returns_prepare_environment_return_value(self):
app_launcher = self.useFixture(NormalApplicationLauncher())
with patch.object(_l, '_get_application_environment') as gae:
self.assertThat(
app_launcher._setup_environment(
self.getUniqueString(), None, []),
Equals(gae.return_value.prepare_environment.return_value)
)
@patch('autopilot.application._launcher.'
'get_proxy_object_for_existing_process')
@patch('autopilot.application._launcher._get_application_path')
def test_launch_call_to_get_application_path(self, gap, _):
"""Test that NormalApplicationLauncher.launch calls
_get_application_path with the arguments it was passed,"""
launcher = NormalApplicationLauncher()
with patch.object(launcher, '_launch_application_process'):
with patch.object(launcher, '_setup_environment') as se:
se.return_value = ('', [])
token = self.getUniqueString()
launcher.launch(token)
gap.assert_called_once_with(token)
@patch('autopilot.application._launcher.'
'get_proxy_object_for_existing_process')
@patch('autopilot.application._launcher._get_application_path')
def test_launch_call_to_setup_environment(self, gap, _):
"""Test the NornmalApplicationLauncher.launch calls
self._setup_environment with the correct application path from
_get_application_path and the arguments passed to it."""
launcher = NormalApplicationLauncher()
with patch.object(launcher, '_launch_application_process'):
with patch.object(launcher, '_setup_environment') as se:
se.return_value = ('', [])
token_a = self.getUniqueString()
token_b = self.getUniqueString()
token_c = self.getUniqueString()
launcher.launch(token_a, arguments=[token_b, token_c])
se.assert_called_once_with(
gap.return_value,
None,
[token_b, token_c],
)
@patch('autopilot.application._launcher.'
'get_proxy_object_for_existing_process')
@patch('autopilot.application._launcher._get_application_path')
def test_launch_call_to_launch_application_process(self, _, __):
"""Test that NormalApplicationLauncher.launch calls
launch_application_process with the return values of
setup_environment."""
launcher = NormalApplicationLauncher()
with patch.object(launcher, '_launch_application_process') as lap:
with patch.object(launcher, '_setup_environment') as se:
token_a = self.getUniqueString()
token_b = self.getUniqueString()
token_c = self.getUniqueString()
se.return_value = (token_a, [token_b, token_c])
launcher.launch('', arguments=['', ''])
lap.assert_called_once_with(
token_a,
True,
None,
[token_b, token_c],
)
@patch('autopilot.application._launcher.'
'get_proxy_object_for_existing_process')
@patch('autopilot.application._launcher._get_application_path')
def test_launch_gets_correct_proxy_object(self, _, gpofep):
"""Test that NormalApplicationLauncher.launch calls
get_proxy_object_for_existing_process with the correct return values of
other functions."""
launcher = NormalApplicationLauncher()
with patch.object(launcher, '_launch_application_process') as lap:
with patch.object(launcher, '_setup_environment') as se:
se.return_value = ('', [])
launcher.launch('')
gpofep.assert_called_once_with(process=lap.return_value,
pid=lap.return_value.pid,
emulator_base=None,
dbus_bus='session')
@patch('autopilot.application._launcher.'
'get_proxy_object_for_existing_process')
@patch('autopilot.application._launcher._get_application_path')
def test_launch_sets_process_of_proxy_object(self, _, gpofep):
"""Test that NormalApplicationLauncher.launch returns the proxy object
returned by get_proxy_object_for_existing_process."""
launcher = NormalApplicationLauncher()
with patch.object(launcher, '_launch_application_process') as lap:
with patch.object(launcher, '_setup_environment') as se:
se.return_value = ('', [])
launcher.launch('')
set_process = gpofep.return_value.set_process
set_process.assert_called_once_with(lap.return_value)
@patch('autopilot.application._launcher.'
'get_proxy_object_for_existing_process')
@patch('autopilot.application._launcher._get_application_path')
def test_launch_returns_proxy_object(self, _, gpofep):
"""Test that NormalApplicationLauncher.launch returns the proxy object
returned by get_proxy_object_for_existing_process."""
launcher = NormalApplicationLauncher()
with patch.object(launcher, '_launch_application_process'):
with patch.object(launcher, '_setup_environment') as se:
se.return_value = ('', [])
result = launcher.launch('')
self.assertEqual(result, gpofep.return_value)
def test_launch_application_process(self):
"""The _launch_application_process method must return the process
object, must add the _kill_process_and_attach_logs method to the
fixture cleanups, and must call the launch_process function with the
correct arguments.
"""
launcher = NormalApplicationLauncher(self.addDetail)
launcher.setUp()
expected_process_return = self.getUniqueString()
with patch.object(
_l, 'launch_process', return_value=expected_process_return
) as patched_launch_process:
process = launcher._launch_application_process(
"/foo/bar", False, None, [])
self.assertThat(process, Equals(expected_process_return))
self.assertThat(
[f[0] for f in launcher._cleanups._cleanups],
Contains(launcher._kill_process_and_attach_logs)
)
patched_launch_process.assert_called_with(
"/foo/bar",
[],
False,
cwd=None
)
class ClickApplicationLauncherTests(TestCase):
def test_raises_exception_on_unknown_kwargs(self):
self.assertThat(
lambda: ClickApplicationLauncher(self.addDetail, unknown=True),
raises(TypeError("__init__() got an unexpected keyword argument "
"'unknown'"))
)
@patch('autopilot.application._launcher._get_click_app_id')
def test_handle_string(self, gcai):
class FakeUpstartBase(_l.ApplicationLauncher):
launch_call_args = []
def launch(self, *args):
FakeUpstartBase.launch_call_args = list(args)
patcher = patch.object(
_l.ClickApplicationLauncher,
'__bases__',
(FakeUpstartBase,)
)
token = self.getUniqueString()
with patcher:
# Prevent mock from trying to delete __bases__
patcher.is_local = True
launcher = self.useFixture(
_l.ClickApplicationLauncher())
launcher.launch('', '', token)
self.assertEqual(
FakeUpstartBase.launch_call_args,
[gcai.return_value, [token]])
@patch('autopilot.application._launcher._get_click_app_id')
def test_handle_bytes(self, gcai):
class FakeUpstartBase(_l.ApplicationLauncher):
launch_call_args = []
def launch(self, *args):
FakeUpstartBase.launch_call_args = list(args)
patcher = patch.object(
_l.ClickApplicationLauncher,
'__bases__',
(FakeUpstartBase,)
)
token = self.getUniqueString()
with patcher:
# Prevent mock from trying to delete __bases__
patcher.is_local = True
launcher = self.useFixture(
_l.ClickApplicationLauncher())
launcher.launch('', '', token.encode())
self.assertEqual(
FakeUpstartBase.launch_call_args,
[gcai.return_value, [token]])
@patch('autopilot.application._launcher._get_click_app_id')
def test_handle_list(self, gcai):
class FakeUpstartBase(_l.ApplicationLauncher):
launch_call_args = []
def launch(self, *args):
FakeUpstartBase.launch_call_args = list(args)
patcher = patch.object(
_l.ClickApplicationLauncher,
'__bases__',
(FakeUpstartBase,)
)
token = self.getUniqueString()
with patcher:
# Prevent mock from trying to delete __bases__
patcher.is_local = True
launcher = self.useFixture(
_l.ClickApplicationLauncher())
launcher.launch('', '', [token])
self.assertEqual(
FakeUpstartBase.launch_call_args,
[gcai.return_value, [token]])
@patch('autopilot.application._launcher._get_click_app_id')
def test_call_get_click_app_id(self, gcai):
class FakeUpstartBase(_l.ApplicationLauncher):
launch_call_args = []
def launch(self, *args):
FakeUpstartBase.launch_call_args = list(args)
patcher = patch.object(
_l.ClickApplicationLauncher,
'__bases__',
(FakeUpstartBase,)
)
token_a = self.getUniqueString()
token_b = self.getUniqueString()
with patcher:
# Prevent mock from trying to delete __bases__
patcher.is_local = True
launcher = self.useFixture(
_l.ClickApplicationLauncher())
launcher.launch(token_a, token_b)
gcai.assert_called_once_with(token_a, token_b)
@patch('autopilot.application._launcher._get_click_app_id')
def test_call_upstart_launch(self, gcai):
class FakeUpstartBase(_l.ApplicationLauncher):
launch_call_args = []
def launch(self, *args):
FakeUpstartBase.launch_call_args = list(args)
patcher = patch.object(
_l.ClickApplicationLauncher,
'__bases__',
(FakeUpstartBase,)
)
with patcher:
# Prevent mock from trying to delete __bases__
patcher.is_local = True
launcher = self.useFixture(
_l.ClickApplicationLauncher())
launcher.launch('', '')
self.assertEqual(launcher.launch_call_args,
[gcai.return_value, []])
class ClickFunctionTests(TestCase):
def test_get_click_app_id_raises_runtimeerror_on_empty_manifest(self):
"""_get_click_app_id must raise a RuntimeError if the requested
package id is not found in the click manifest.
"""
with patch.object(_l, '_get_click_manifest', return_value=[]):
self.assertThat(
lambda: _get_click_app_id("com.autopilot.testing"),
raises(
RuntimeError(
"Unable to find package 'com.autopilot.testing' in "
"the click manifest."
)
)
)
def test_get_click_app_id_raises_runtimeerror_on_missing_package(self):
with patch.object(_l, '_get_click_manifest') as cm:
cm.return_value = [
{
'name': 'com.not.expected.name',
'hooks': {'bar': {}}, 'version': '1.0'
}
]
self.assertThat(
lambda: _get_click_app_id("com.autopilot.testing"),
raises(
RuntimeError(
"Unable to find package 'com.autopilot.testing' in "
"the click manifest."
)
)
)
def test_get_click_app_id_raises_runtimeerror_on_wrong_app(self):
"""get_click_app_id must raise a RuntimeError if the requested
application is not found within the click package.
"""
with patch.object(_l, '_get_click_manifest') as cm:
cm.return_value = [{'name': 'com.autopilot.testing', 'hooks': {}}]
self.assertThat(
lambda: _get_click_app_id("com.autopilot.testing", "bar"),
raises(
RuntimeError(
"Application 'bar' is not present within the click "
"package 'com.autopilot.testing'."
)
)
)
def test_get_click_app_id_returns_id(self):
with patch.object(_l, '_get_click_manifest') as cm:
cm.return_value = [
{
'name': 'com.autopilot.testing',
'hooks': {'bar': {}}, 'version': '1.0'
}
]
self.assertThat(
_get_click_app_id("com.autopilot.testing", "bar"),
Equals("com.autopilot.testing_bar_1.0")
)
def test_get_click_app_id_returns_id_without_appid_passed(self):
with patch.object(_l, '_get_click_manifest') as cm:
cm.return_value = [
{
'name': 'com.autopilot.testing',
'hooks': {'bar': {}}, 'version': '1.0'
}
]
self.assertThat(
_get_click_app_id("com.autopilot.testing"),
Equals("com.autopilot.testing_bar_1.0")
)
class UpstartApplicationLauncherTests(TestCase):
def test_can_construct_UpstartApplicationLauncher(self):
UpstartApplicationLauncher(self.addDetail)
def test_raises_exception_on_unknown_kwargs(self):
self.assertThat(
lambda: UpstartApplicationLauncher(self.addDetail, unknown=True),
raises(TypeError("__init__() got an unexpected keyword argument "
"'unknown'"))
)
def test_on_failed_only_sets_status_on_correct_app_id(self):
state = {
'expected_app_id': 'gedit',
}
UpstartApplicationLauncher._on_failed('some_game', None, state)
self.assertThat(state, Not(Contains('status')))
def assertFunctionSetsCorrectStateAndQuits(self, observer, expected_state):
"""Assert that the observer observer sets the correct state id.
:param observer: The observer callable you want to test.
:param expected_state: The state id the observer callable must set.
"""
expected_app_id = self.getUniqueString()
state = {
'expected_app_id': expected_app_id,
'loop': Mock()
}
if observer == UpstartApplicationLauncher._on_failed:
observer(expected_app_id, None, state)
elif observer == UpstartApplicationLauncher._on_started or \
observer == UpstartApplicationLauncher._on_stopped:
observer(expected_app_id, state)
else:
observer(state)
self.assertThat(
state['status'],
Equals(expected_state)
)
state['loop'].quit.assert_called_once_with()
def test_on_failed_sets_status_with_correct_app_id(self):
self.assertFunctionSetsCorrectStateAndQuits(
UpstartApplicationLauncher._on_failed,
UpstartApplicationLauncher.Failed
)
def test_on_started_sets_status_with_correct_app_id(self):
self.assertFunctionSetsCorrectStateAndQuits(
UpstartApplicationLauncher._on_started,
UpstartApplicationLauncher.Started
)
def test_on_timeout_sets_status_and_exits_loop(self):
self.assertFunctionSetsCorrectStateAndQuits(
UpstartApplicationLauncher._on_timeout,
UpstartApplicationLauncher.Timeout
)
def test_on_started_only_sets_status_on_correct_app_id(self):
state = {
'expected_app_id': 'gedit',
}
UpstartApplicationLauncher._on_started('some_game', state)
self.assertThat(state, Not(Contains('status')))
def test_on_stopped_only_sets_status_on_correct_app_id(self):
state = {
'expected_app_id': 'gedit',
}
UpstartApplicationLauncher._on_stopped('some_game', state)
self.assertThat(state, Not(Contains('status')))
def test_on_stopped_sets_status_and_exits_loop(self):
self.assertFunctionSetsCorrectStateAndQuits(
UpstartApplicationLauncher._on_stopped,
UpstartApplicationLauncher.Stopped
)
def test_get_pid_calls_upstart_module(self):
expected_return = self.getUniqueInteger()
with patch.object(_l, 'UbuntuAppLaunch') as mock_ual:
mock_ual.get_primary_pid.return_value = expected_return
observed = UpstartApplicationLauncher._get_pid_for_launched_app(
'gedit'
)
mock_ual.get_primary_pid.assert_called_once_with('gedit')
self.assertThat(expected_return, Equals(observed))
def test_launch_app_calls_upstart_module(self):
with patch.object(_l, 'UbuntuAppLaunch') as mock_ual:
UpstartApplicationLauncher._launch_app(
'gedit',
['some', 'uris']
)
mock_ual.start_application_test.assert_called_once_with(
'gedit',
['some', 'uris']
)
def test_check_error_raises_RuntimeError_on_timeout(self):
fn = lambda: UpstartApplicationLauncher._check_status_error(
UpstartApplicationLauncher.Timeout
)
self.assertThat(
fn,
raises(
RuntimeError(
"Timed out while waiting for application to launch"
)
)
)
def test_check_error_raises_RuntimeError_on_failure(self):
fn = lambda: UpstartApplicationLauncher._check_status_error(
UpstartApplicationLauncher.Failed
)
self.assertThat(
fn,
raises(
RuntimeError(
"Application Launch Failed"
)
)
)
def test_check_error_raises_RuntimeError_with_extra_message(self):
fn = lambda: UpstartApplicationLauncher._check_status_error(
UpstartApplicationLauncher.Failed,
"extra message"
)
self.assertThat(
fn,
raises(
RuntimeError(
"Application Launch Failed: extra message"
)
)
)
def test_check_error_does_nothing_on_None(self):
UpstartApplicationLauncher._check_status_error(None)
def test_get_loop_returns_glib_mainloop_instance(self):
loop = UpstartApplicationLauncher._get_glib_loop()
self.assertThat(loop, IsInstance(GLib.MainLoop))
@patch('autopilot.application._launcher.'
'get_proxy_object_for_existing_process')
def test_handle_string(self, _):
launcher = UpstartApplicationLauncher()
token_a = self.getUniqueString()
token_b = self.getUniqueString()
with patch.object(launcher, '_launch_app') as la:
with patch.object(launcher, '_get_pid_for_launched_app'):
with patch.object(launcher, '_get_glib_loop'):
launcher.launch(token_a, token_b)
la.assert_called_once_with(token_a, [token_b])
@patch('autopilot.application._launcher.'
'get_proxy_object_for_existing_process')
def test_handle_bytes(self, _):
launcher = UpstartApplicationLauncher()
token_a = self.getUniqueString()
token_b = self.getUniqueString()
with patch.object(launcher, '_launch_app') as la:
with patch.object(launcher, '_get_pid_for_launched_app'):
with patch.object(launcher, '_get_glib_loop'):
launcher.launch(token_a, token_b.encode())
la.assert_called_once_with(token_a, [token_b])
@patch('autopilot.application._launcher.'
'get_proxy_object_for_existing_process')
def test_handle_list(self, _):
launcher = UpstartApplicationLauncher()
token_a = self.getUniqueString()
token_b = self.getUniqueString()
with patch.object(launcher, '_launch_app') as la:
with patch.object(launcher, '_get_pid_for_launched_app'):
with patch.object(launcher, '_get_glib_loop'):
launcher.launch(token_a, [token_b])
la.assert_called_once_with(token_a, [token_b])
@patch('autopilot.application._launcher.'
'get_proxy_object_for_existing_process')
def test_calls_get_pid(self, _):
launcher = UpstartApplicationLauncher()
token = self.getUniqueString()
with patch.object(launcher, '_launch_app'):
with patch.object(launcher, '_get_pid_for_launched_app') as gp:
with patch.object(launcher, '_get_glib_loop'):
launcher.launch(token)
gp.assert_called_once_with(token)
@patch('autopilot.application._launcher.'
'get_proxy_object_for_existing_process')
def test_gets_correct_proxy_object(self, gpofep):
launcher = UpstartApplicationLauncher()
with patch.object(launcher, '_launch_app'):
with patch.object(launcher, '_get_pid_for_launched_app') as gp:
with patch.object(launcher, '_get_glib_loop'):
launcher.launch('')
gpofep.assert_called_once_with(pid=gp.return_value,
emulator_base=None,
dbus_bus='session')
@patch('autopilot.application._launcher.'
'get_proxy_object_for_existing_process')
def test_returns_proxy_object(self, gpofep):
launcher = UpstartApplicationLauncher()
with patch.object(launcher, '_launch_app'):
with patch.object(launcher, '_get_pid_for_launched_app'):
with patch.object(launcher, '_get_glib_loop'):
result = launcher.launch('')
self.assertEqual(result, gpofep.return_value)
@patch('autopilot.application._launcher.'
'get_proxy_object_for_existing_process')
def test_calls_get_glib_loop(self, gpofep):
launcher = UpstartApplicationLauncher()
with patch.object(launcher, '_launch_app'):
with patch.object(launcher, '_get_pid_for_launched_app'):
with patch.object(launcher, '_get_glib_loop') as ggl:
launcher.launch('')
ggl.assert_called_once_with()
def assertFailedObserverSetsExtraMessage(self, fail_type, expected_msg):
"""Assert that the on_failed observer must set the expected message
for a particular failure_type."""
expected_app_id = self.getUniqueString()
state = {
'expected_app_id': expected_app_id,
'loop': Mock()
}
UpstartApplicationLauncher._on_failed(
expected_app_id,
fail_type,
state
)
self.assertEqual(expected_msg, state['message'])
def test_on_failed_sets_message_for_app_crash(self):
self.assertFailedObserverSetsExtraMessage(
_l.UbuntuAppLaunch.AppFailed.CRASH,
'Application crashed.'
)
def test_on_failed_sets_message_for_app_start_failure(self):
self.assertFailedObserverSetsExtraMessage(
_l.UbuntuAppLaunch.AppFailed.START_FAILURE,
'Application failed to start.'
)
def test_add_application_cleanups_adds_both_cleanup_actions(self):
token = self.getUniqueString()
state = {
'status': UpstartApplicationLauncher.Started,
'expected_app_id': token,
}
launcher = UpstartApplicationLauncher(Mock())
launcher.setUp()
launcher._maybe_add_application_cleanups(state)
self.assertThat(
launcher._cleanups._cleanups,
Contains(
(launcher._attach_application_log, (token,), {})
)
)
self.assertThat(
launcher._cleanups._cleanups,
Contains(
(launcher._stop_application, (token,), {})
)
)
def test_add_application_cleanups_does_nothing_when_app_timedout(self):
state = {
'status': UpstartApplicationLauncher.Timeout,
}
launcher = UpstartApplicationLauncher(Mock())
launcher.setUp()
launcher._maybe_add_application_cleanups(state)
self.assertThat(launcher._cleanups._cleanups, HasLength(0))
def test_add_application_cleanups_does_nothing_when_app_failed(self):
state = {
'status': UpstartApplicationLauncher.Failed,
}
launcher = UpstartApplicationLauncher(Mock())
launcher.setUp()
launcher._maybe_add_application_cleanups(state)
self.assertThat(launcher._cleanups._cleanups, HasLength(0))
def test_attach_application_log_does_nothing_wth_no_log_specified(self):
app_id = self.getUniqueString()
case_addDetail = Mock()
launcher = UpstartApplicationLauncher(case_addDetail)
j = MagicMock(spec=_l.journal.Reader)
with patch.object(_l.journal, 'Reader', return_value=j):
launcher._attach_application_log(app_id)
expected = launcher._get_user_unit_match(app_id)
j.add_match.assert_called_once_with(_SYSTEMD_USER_UNIT=expected)
self.assertEqual(0, case_addDetail.call_count)
def test_attach_application_log_attaches_log(self):
token = self.getUniqueString()
case_addDetail = Mock()
launcher = UpstartApplicationLauncher(case_addDetail)
app_id = self.getUniqueString()
j = MagicMock(spec=_l.journal.Reader)
j.__iter__ = lambda x: iter([token])
with patch.object(_l.journal, 'Reader', return_value=j):
launcher._attach_application_log(app_id)
self.assertEqual(1, case_addDetail.call_count)
content_name, content_obj = case_addDetail.call_args[0]
self.assertEqual(
"Application Log (%s)" % app_id,
content_name
)
self.assertThat(content_obj.as_text(), Contains(token))
def test_stop_adds_app_stopped_observer(self):
mock_add_detail = Mock()
mock_glib_loop = Mock()
patch_get_loop = patch.object(
UpstartApplicationLauncher,
'_get_glib_loop',
new=mock_glib_loop,
)
mock_UAL = Mock()
patch_UAL = patch.object(_l, 'UbuntuAppLaunch', new=mock_UAL)
launcher = UpstartApplicationLauncher(mock_add_detail)
app_id = self.getUniqueString()
with ExitStack() as patches:
patches.enter_context(patch_get_loop)
patches.enter_context(patch_UAL)
launcher._stop_application(app_id)
call_args = mock_UAL.observer_add_app_stop.call_args[0]
self.assertThat(
call_args[0],
Equals(UpstartApplicationLauncher._on_stopped)
)
self.assertThat(call_args[1]['expected_app_id'], Equals(app_id))
def test_stop_calls_libUAL_stop_function(self):
mock_add_detail = Mock()
mock_glib_loop = Mock()
patch_get_loop = patch.object(
UpstartApplicationLauncher,
'_get_glib_loop',
new=mock_glib_loop,
)
mock_UAL = Mock()
patch_UAL = patch.object(_l, 'UbuntuAppLaunch', new=mock_UAL)
launcher = UpstartApplicationLauncher(mock_add_detail)
app_id = self.getUniqueString()
with ExitStack() as patches:
patches.enter_context(patch_get_loop)
patches.enter_context(patch_UAL)
launcher._stop_application(app_id)
mock_UAL.stop_application.assert_called_once_with(app_id)
def test_stop_logs_error_on_timeout(self):
mock_add_detail = Mock()
mock_glib_loop = Mock()
patch_get_loop = patch.object(
UpstartApplicationLauncher,
'_get_glib_loop',
new=mock_glib_loop,
)
mock_UAL = Mock()
# we replace the add_observer function with one that can set the
# tiemout state, so we can ibject the timeout condition within the
# glib loop. This is ugly, but necessary.
def fake_add_observer(fn, state):
state['status'] = UpstartApplicationLauncher.Timeout
mock_UAL.observer_add_app_stop = fake_add_observer
patch_UAL = patch.object(_l, 'UbuntuAppLaunch', new=mock_UAL)
launcher = UpstartApplicationLauncher(mock_add_detail)
app_id = self.getUniqueString()
mock_logger = Mock()
patch_logger = patch.object(_l, '_logger', new=mock_logger)
with ExitStack() as patches:
patches.enter_context(patch_get_loop)
patches.enter_context(patch_UAL)
patches.enter_context(patch_logger)
launcher._stop_application(app_id)
mock_logger.error.assert_called_once_with(
"Timed out waiting for Application with app_id '%s' to stop.",
app_id
)
class ApplicationLauncherInternalTests(TestCase):
def test_get_app_env_from_string_hint_returns_qt_env(self):
self.assertThat(
_get_app_env_from_string_hint('QT'),
IsInstance(QtApplicationEnvironment)
)
def test_get_app_env_from_string_hint_returns_gtk_env(self):
self.assertThat(
_get_app_env_from_string_hint('GTK'),
IsInstance(GtkApplicationEnvironment)
)
def test_get_app_env_from_string_hint_raises_on_unknown(self):
self.assertThat(
lambda: _get_app_env_from_string_hint('FOO'),
raises(ValueError("Unknown hint string: FOO"))
)
def test_get_application_environment_uses_app_type_argument(self):
with patch.object(_l, '_get_app_env_from_string_hint') as from_hint:
_get_application_environment(app_type="app_type")
from_hint.assert_called_with("app_type")
def test_get_application_environment_uses_app_path_argument(self):
with patch.object(
_l, 'get_application_launcher_wrapper'
) as patched_wrapper:
_get_application_environment(app_path="app_path")
patched_wrapper.assert_called_with("app_path")
def test_get_application_environment_raises_runtime_with_no_args(self):
self.assertThat(
lambda: _get_application_environment(),
raises(
ValueError(
"Must specify either app_type or app_path."
)
)
)
def test_get_application_environment_raises_on_app_type_error(self):
unknown_app_type = self.getUniqueString()
with patch.object(
_l, '_get_app_env_from_string_hint',
side_effect=ValueError()
):
self.assertThat(
lambda: _get_application_environment(
app_type=unknown_app_type
),
raises(RuntimeError(
"Autopilot could not determine the correct introspection "
"type to use. You can specify this by providing app_type."
))
)
def test_get_application_environment_raises_on_app_path_error(self):
unknown_app_path = self.getUniqueString()
with patch.object(
_l, 'get_application_launcher_wrapper', side_effect=RuntimeError()
):
self.assertThat(
lambda: _get_application_environment(
app_path=unknown_app_path
),
raises(RuntimeError(
"Autopilot could not determine the correct introspection "
"type to use. You can specify this by providing app_type."
))
)
@patch.object(_l.os, 'killpg')
def test_attempt_kill_pid_logs_if_process_already_exited(self, killpg):
killpg.side_effect = OSError()
with patch.object(_l, '_logger') as patched_log:
_attempt_kill_pid(0)
patched_log.info.assert_called_with(
"Appears process has already exited."
)
@patch.object(_l, '_attempt_kill_pid')
def test_kill_process_succeeds(self, patched_kill_pid):
mock_process = Mock()
mock_process.returncode = 0
mock_process.communicate.return_value = ("", "",)
with patch.object(
_l, '_is_process_running', return_value=False
):
self.assertThat(_kill_process(mock_process), Equals(("", "", 0)))
@patch.object(_l, '_attempt_kill_pid')
def test_kill_process_tries_again(self, patched_kill_pid):
with sleep.mocked():
mock_process = Mock()
mock_process.pid = 123
mock_process.communicate.return_value = ("", "",)
with patch.object(
_l, '_is_process_running', return_value=True
) as proc_running:
_kill_process(mock_process)
self.assertThat(proc_running.call_count, GreaterThan(1))
self.assertThat(patched_kill_pid.call_count, Equals(2))
patched_kill_pid.assert_called_with(123, signal.SIGKILL)
@patch.object(_l.subprocess, 'Popen')
def test_launch_process_uses_arguments(self, popen):
launch_process("testapp", ["arg1", "arg2"])
self.assertThat(
popen.call_args_list[0][0],
Contains(['testapp', 'arg1', 'arg2'])
)
@patch.object(_l.subprocess, 'Popen')
def test_launch_process_default_capture_is_false(self, popen):
launch_process("testapp", [])
self.assertThat(
popen.call_args[1]['stderr'],
Equals(None)
)
self.assertThat(
popen.call_args[1]['stdout'],
Equals(None)
)
@patch.object(_l.subprocess, 'Popen')
def test_launch_process_can_set_capture_output(self, popen):
launch_process("testapp", [], capture_output=True)
self.assertThat(
popen.call_args[1]['stderr'],
Not(Equals(None))
)
self.assertThat(
popen.call_args[1]['stdout'],
Not(Equals(None))
)
@patch.object(_l.subprocess, 'check_output')
def test_get_application_launcher_wrapper_finds_qt(self, check_output):
check_output.return_value = "LIBQTCORE"
self.assertThat(
get_application_launcher_wrapper("/fake/app/path"),
IsInstance(QtApplicationEnvironment)
)
@patch.object(_l.subprocess, 'check_output')
def test_get_application_launcher_wrapper_finds_gtk(self, check_output):
check_output.return_value = "LIBGTK"
self.assertThat(
get_application_launcher_wrapper("/fake/app/path"),
IsInstance(GtkApplicationEnvironment)
)
@patch.object(_l.subprocess, 'check_output')
def test_get_application_path_returns_stripped_path(self, check_output):
check_output.return_value = "/foo/bar "
self.assertThat(_get_application_path("bar"), Equals('/foo/bar'))
check_output.assert_called_with(
['which', 'bar'], universal_newlines=True
)
def test_get_application_path_raises_when_cant_find_app(self):
test_path = self.getUniqueString()
expected_error = "Unable to find path for application {app}: Command"\
" '['which', '{app}']' returned non-zero exit "\
"status 1".format(app=test_path)
with patch.object(_l.subprocess, 'check_output') as check_output:
check_output.side_effect = subprocess.CalledProcessError(
1,
['which', test_path]
)
self.assertThat(
lambda: _get_application_path(test_path),
raises(ValueError(expected_error))
)
def test_get_application_launcher_wrapper_raises_runtimeerror(self):
test_path = self.getUniqueString()
expected_error = "Command '['ldd', '%s']' returned non-zero exit"\
" status 1" % test_path
with patch.object(_l.subprocess, 'check_output') as check_output:
check_output.side_effect = subprocess.CalledProcessError(
1,
['ldd', test_path]
)
self.assertThat(
lambda: get_application_launcher_wrapper(test_path),
raises(RuntimeError(expected_error))
)
def test_get_application_launcher_wrapper_returns_none_for_unknown(self):
with patch.object(_l.subprocess, 'check_output') as check_output:
check_output.return_value = self.getUniqueString()
self.assertThat(
get_application_launcher_wrapper(""), Equals(None)
)
def test_get_click_manifest_returns_python_object(self):
example_manifest = """
[{
"description": "Calculator application",
"framework": "ubuntu-sdk-13.10",
"hooks": {
"calculator": {
"apparmor": "apparmor/calculator.json",
"desktop": "ubuntu-calculator-app.desktop"
}
},
"icon": "calculator64.png"
}]
"""
with patch.object(_l.subprocess, 'check_output') as check_output:
check_output.return_value = example_manifest
self.assertThat(_get_click_manifest(), IsInstance(list))
@patch.object(_l.psutil, 'pid_exists')
def test_is_process_running_checks_with_pid(self, pid_exists):
pid_exists.return_value = True
self.assertThat(_is_process_running(123), Equals(True))
pid_exists.assert_called_with(123)
./autopilot/introspection/ 0000755 0000041 0000041 00000000000 13061552435 016167 5 ustar www-data www-data ./autopilot/introspection/qt.py 0000644 0000041 0000041 00000013450 13061552435 017170 0 ustar www-data www-data # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Autopilot Functional Test Tool
# Copyright (C) 2012-2013 Canonical
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
"""Classes and tools to support Qt introspection."""
import functools
class QtSignalWatcher(object):
"""A utility class to make watching Qt signals easy."""
def __init__(self, proxy, signal_name):
"""Initialise the signal watcher.
:param QtObjectProxyMixin proxy:
:param string signal_name: Name of the signal being monitored.
'proxy' is an instance of QtObjectProxyMixin.
'signal_name' is the name of the signal being monitored.
Do not construct this object yourself. Instead, call 'watch_signal' on
a QtObjectProxyMixin instance.
"""
self._proxy = proxy
self.signal_name = signal_name
self._data = None
def _refresh(self):
self._data = self._proxy.get_signal_emissions(self.signal_name)
@property
def num_emissions(self):
"""Get the number of times the signal has been emitted since we started
monitoring it.
"""
self._refresh()
return len(self._data)
@property
def was_emitted(self):
"""True if the signal was emitted at least once."""
self._refresh()
return len(self._data) > 0
class QtObjectProxyMixin(object):
"""A class containing methods specific to querying Qt applications."""
def _get_qt_iface(self):
"""Get the autopilot Qt-specific interface for the specified service
name and object path.
"""
return self._backend.ipc_address.qt_introspection_iface
@property
def slots(self):
"""An object that contains all the slots available to be called in
this object."""
if getattr(self, '_slots', None) is None:
self._slots = QtSlotProxy(self)
return self._slots
def watch_signal(self, signal_name):
"""Start watching the 'signal_name' signal on this object.
signal_name must be the C++ signal signature, as is usually used within
the Qt 'SIGNAL' macro. Examples of valid signal names are:
* 'clicked(bool)'
* 'pressed()'
A list of valid signal names can be retrieved from 'get_signals()'. If
an invalid signal name is given ValueError will be raised.
This method returns a QtSignalWatcher instance.
By default, no signals are monitored. You must call this method once
for each signal you are interested in.
"""
valid_signals = self.get_signals()
if signal_name not in valid_signals:
raise ValueError(
"Signal name %r is not in the valid signal list of %r" %
(signal_name, valid_signals))
self._get_qt_iface().RegisterSignalInterest(self.id, signal_name)
return QtSignalWatcher(self, signal_name)
def get_signal_emissions(self, signal_name):
"""Get a list of all the emissions of the 'signal_name' signal.
If signal_name is not a valid signal, ValueError is raised.
The QtSignalWatcher class provides a more convenient API than calling
this method directly. A QtSignalWatcher instance is returned from
'watch_signal'.
Each item in the returned list is a tuple containing the arguments in
the emission (possibly an empty list if the signal has no arguments).
If the signal was not emitted, the list will be empty. You must first
call 'watch_signal(signal_name)' in order to monitor this signal.
Note: Some signal arguments may not be marshallable over DBus. If this
is the case, they will be omitted from the argument list.
"""
valid_signals = self.get_signals()
if signal_name not in valid_signals:
raise ValueError(
"Signal name %r is not in the valid signal list of %r" %
(signal_name, valid_signals))
return self._get_qt_iface().GetSignalEmissions(self.id, signal_name)
def get_signals(self):
"""Get a list of the signals available on this object."""
dbus_signal_list = self._get_qt_iface().ListSignals(self.id)
if dbus_signal_list is not None:
return [str(sig) for sig in dbus_signal_list]
else:
return []
def get_slots(self):
"""Get a list of the slots available on this object."""
dbus_slot_list = self._get_qt_iface().ListMethods(self.id)
if dbus_slot_list is not None:
return [str(sig) for sig in dbus_slot_list]
else:
return []
class QtSlotProxy(object):
"""A class that transparently calls slots in a Qt object."""
def __init__(self, qt_mixin):
self._dbus_iface = qt_mixin._get_qt_iface()
self._object_id = qt_mixin.id
methods = self._dbus_iface.ListMethods(self._object_id)
for method_name in methods:
method = functools.partial(self._call_method, method_name)
stripped_method_name = method_name[:method_name.find('(')]
setattr(self, stripped_method_name, method)
def _call_method(self, name, *args):
self._dbus_iface.InvokeMethod(self._object_id, name, args)
./autopilot/introspection/utilities.py 0000644 0000041 0000041 00000010647 13061552435 020564 0 ustar www-data www-data # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Autopilot Functional Test Tool
# Copyright (C) 2012-2013 Canonical
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
import os
from contextlib import contextmanager
from dbus import Interface
from autopilot.utilities import process_iter
def _pid_is_running(pid):
"""Check for the existence of a currently running PID.
:returns: **True** if PID is running **False** otherwise.
"""
return os.path.exists("/proc/%d" % pid)
def _get_bus_connections_pid(bus, connection_name):
"""Returns the pid for the connection **connection_name** on **bus**
:raises: **DBusException** if connection_name is invalid etc.
"""
bus_obj = bus.get_object('org.freedesktop.DBus', '/org/freedesktop/DBus')
bus_iface = Interface(bus_obj, 'org.freedesktop.DBus')
return bus_iface.GetConnectionUnixProcessID(connection_name)
def translate_state_keys(state_dict):
"""Translates the *state_dict* passed in so the keys are usable as python
attributes."""
return {k.replace('-', '_'): v for k, v in state_dict.items()}
def sort_by_keys(instances, sort_keys):
"""Sorts DBus object instances by requested keys."""
def get_sort_key(item):
sort_key = []
for sk in sort_keys:
if not isinstance(sk, str):
raise ValueError(
'Parameter `sort_keys` must be a list of strings'
)
value = item
for key in sk.split('.'):
value = getattr(value, key)
sort_key.append(value)
return sort_key
if sort_keys and not isinstance(sort_keys, list):
raise ValueError('Parameter `sort_keys` must be a list.')
if len(instances) > 1 and sort_keys:
return sorted(instances, key=get_sort_key)
return instances
class ProcessUtil:
"""Helper class to manipulate running processes."""
@contextmanager
def mocked(self, fake_processes):
"""Enable mocking for the ProcessUtil class
Also mocks all calls to autopilot.utilities.process_iter.
One may use it like::
from autopilot.introspection.utilities import ProcessUtil
process_util = ProcessUtil()
with process_util.mocked([{'pid': -9, 'name': 'xx'}]):
self.assertThat(
process_util.get_pid_for_process('xx'),
Equals(-9)
)
)
"""
process_iter.enable_mock(fake_processes)
try:
yield self
finally:
process_iter.disable_mock()
def _query_pids_for_process(self, process_name):
if not isinstance(process_name, str):
raise ValueError('Process name must be a string.')
pids = [process.pid for process in process_iter()
if process.name() == process_name]
if not pids:
raise ValueError('Process \'{}\' not running'.format(process_name))
return pids
def get_pid_for_process(self, process_name):
"""Returns the PID associated with a process name.
:param process_name: Process name to get PID for. This must
be a string.
:return: PID of the requested process.
"""
pids = self._query_pids_for_process(process_name)
if len(pids) > 1:
raise ValueError(
'More than one PID exists for process \'{}\''.format(
process_name
)
)
return pids[0]
def get_pids_for_process(self, process_name):
"""Returns PID(s) associated with a process name.
:param process_name: Process name to get PID(s) for.
:return: A list containing the PID(s) of the requested process.
"""
return self._query_pids_for_process(process_name)
process_util = ProcessUtil()
./autopilot/introspection/__init__.py 0000644 0000041 0000041 00000004322 13061552435 020301 0 ustar www-data www-data # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Autopilot Functional Test Tool
# Copyright (C) 2012-2014 Canonical
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
"""Package for introspection object support and search.
This package contains the methods and classes that are of use for accessing
dbus proxy objects and creating Custom Proxy Object classes.
For retrieving proxy objects for already existing processes use
:meth:`~autopilot.introspection.get_proxy_object_for_existing_process`
This takes search criteria and return a proxy object that can be queried and
introspected.
For creating your own Custom Proxy Classes use
:class:`autopilot.introspection.CustomEmulatorBase`
.. seealso::
The tutorial section :ref:`custom_proxy_classes` for further details on
using 'CustomEmulatorBase' to write custom proxy classes.
"""
from autopilot.introspection.dbus import CustomEmulatorBase, is_element
from autopilot.introspection._xpathselect import (
get_classname_from_path,
get_path_root,
)
from autopilot.exceptions import ProcessSearchError
from autopilot.introspection._search import (
get_proxy_object_for_existing_process,
get_proxy_object_for_existing_process_by_name,
)
# TODO: Remove ProcessSearchError from here once all our clients have stopped
# using it from this location.
__all__ = [
'CustomEmulatorBase',
'is_element',
'get_classname_from_path',
'get_path_root',
'ProxyBase',
'ProcessSearchError',
'get_proxy_object_for_existing_process',
'get_proxy_object_for_existing_process_by_name',
]
ProxyBase = CustomEmulatorBase
ProxyBase.__name__ = 'ProxyBase'
./autopilot/introspection/backends.py 0000644 0000041 0000041 00000026236 13061552435 020324 0 ustar www-data www-data # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Autopilot Functional Test Tool
# Copyright (C) 2012-2013 Canonical
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
"""Backend IPC interface for autopilot.
This module contains two primative classes that Autopilot uses for it's IPC
routines.
The first is the DBusAddress class. This contains knowledge of how to talk dbus
to a particular application exposed over dbus. In the future, this interface
could be provided by some alternative IPC mechanism.
The second class is the Backend class. This holds a reference to a DBusAddress
class, and contains code that turns a query object into proxy classes.
"""
from collections import namedtuple
import dbus
import logging
from autopilot.dbus_handler import (
get_session_bus,
get_system_bus,
get_custom_bus,
)
from autopilot.introspection.constants import (
AP_INTROSPECTION_IFACE,
CURRENT_WIRE_PROTOCOL_VERSION,
DBUS_INTROSPECTION_IFACE,
QT_AUTOPILOT_IFACE,
)
from autopilot.utilities import Timer
from autopilot.introspection.utilities import (
_pid_is_running,
_get_bus_connections_pid,
)
from autopilot.introspection._object_registry import _get_proxy_object_class
_logger = logging.getLogger(__name__)
class WireProtocolVersionMismatch(RuntimeError):
"""Wire protocols mismatch."""
class DBusAddress(object):
"""Store information about an Autopilot dbus backend, from keyword
arguments."""
_checked_backends = []
AddrTuple = namedtuple(
'AddressTuple', ['bus', 'connection', 'object_path'])
@staticmethod
def SessionBus(connection, object_path):
"""Construct a DBusAddress that backs on to the session bus."""
return DBusAddress(get_session_bus(), connection, object_path)
@staticmethod
def SystemBus(connection, object_path):
"""Construct a DBusAddress that backs on to the system bus."""
return DBusAddress(get_system_bus(), connection, object_path)
@staticmethod
def CustomBus(bus_address, connection, object_path):
"""Construct a DBusAddress that backs on to a custom bus.
:param bus_address: A string representing the address of the dbus bus
to connect to.
"""
return DBusAddress(
get_custom_bus(bus_address), connection, object_path)
def __init__(self, bus, connection, object_path):
"""Construct a DBusAddress instance.
:param bus: A valid DBus bus object.
:param connection: A string connection name to look at, or None to
search all dbus connections for objects that resemble an autopilot
conection.
:param object_path: The path to the object that provides the autopilot
interface, or None to search for the object.
"""
# We cannot evaluate kwargs for accuracy now, since this class will be
# created at module import time, at which point the bus backend
# probably does not exist yet.
self._addr_tuple = DBusAddress.AddrTuple(bus, connection, object_path)
@property
def introspection_iface(self):
if not isinstance(self._addr_tuple.connection, str):
raise TypeError("Service name must be a string.")
if not isinstance(self._addr_tuple.object_path, str):
raise TypeError("Object name must be a string")
if not self._check_pid_running():
raise RuntimeError(
"Lost dbus backend communication. It appears the "
"application under test exited before the test "
"finished!"
)
proxy_obj = self._addr_tuple.bus.get_object(
self._addr_tuple.connection,
self._addr_tuple.object_path
)
iface = dbus.Interface(proxy_obj, AP_INTROSPECTION_IFACE)
if self._addr_tuple not in DBusAddress._checked_backends:
try:
self._check_version(iface)
except WireProtocolVersionMismatch:
raise
else:
DBusAddress._checked_backends.append(self._addr_tuple)
return iface
def _check_version(self, iface):
"""Check the wire protocol version on 'iface', and raise an error if
the version does not match what we were expecting.
"""
try:
version = iface.GetVersion()
except dbus.DBusException:
version = "1.2"
if version != CURRENT_WIRE_PROTOCOL_VERSION:
raise WireProtocolVersionMismatch(
"Wire protocol mismatch at %r: is %s, expecting %s" % (
self,
version,
CURRENT_WIRE_PROTOCOL_VERSION)
)
def _check_pid_running(self):
try:
process_pid = _get_bus_connections_pid(
self._addr_tuple.bus,
self._addr_tuple.connection
)
return _pid_is_running(process_pid)
except dbus.DBusException as e:
if e.get_dbus_name() == \
'org.freedesktop.DBus.Error.NameHasNoOwner':
return False
else:
raise
@property
def dbus_introspection_iface(self):
dbus_object = self._addr_tuple.bus.get_object(
self._addr_tuple.connection,
self._addr_tuple.object_path
)
return dbus.Interface(dbus_object, DBUS_INTROSPECTION_IFACE)
@property
def qt_introspection_iface(self):
proxy_obj = self._addr_tuple.bus.get_object(
self._addr_tuple.connection,
self._addr_tuple.object_path
)
return dbus.Interface(proxy_obj, QT_AUTOPILOT_IFACE)
def __hash__(self):
return hash(self._addr_tuple)
def __eq__(self, other):
return self._addr_tuple.bus == other._addr_tuple.bus and \
self._addr_tuple.connection == other._addr_tuple.connection and \
self._addr_tuple.object_path == other._addr_tuple.object_path
def __ne__(self, other):
return (self._addr_tuple.object_path !=
other._addr_tuple.object_path or
self._addr_tuple.connection != other._addr_tuple.connection or
self._addr_tuple.bus != other._addr_tuple.bus)
def __str__(self):
return repr(self)
def __repr__(self):
if self._addr_tuple.bus._bus_type == dbus.Bus.TYPE_SESSION:
name = "session"
elif self._addr_tuple.bus._bus_type == dbus.Bus.TYPE_SYSTEM:
name = "system"
else:
name = "custom"
return "<%s bus %s %s>" % (
name, self._addr_tuple.connection, self._addr_tuple.object_path)
class Backend(object):
"""A Backend object that works with an ipc address interface.
Will raise a RunTimeError if the dbus backend communication is lost."""
def __init__(self, ipc_address):
self.ipc_address = ipc_address
def execute_query_get_data(self, query):
"""Execute 'query', return the raw dbus reply."""
with Timer("GetState %r" % query):
try:
data = self.ipc_address.introspection_iface.GetState(
query.server_query_bytes()
)
except dbus.DBusException as e:
desired_exception = 'org.freedesktop.DBus.Error.ServiceUnknown'
if e.get_dbus_name() != desired_exception:
raise
else:
raise RuntimeError(
"Lost dbus backend communication. It appears the "
"application under test exited before the test "
"finished!"
)
if len(data) > 15:
_logger.warning(
"Your query '%r' returned a lot of data (%d items). This "
"is likely to be slow. You may want to consider optimising"
" your query to return fewer items.",
query,
len(data)
)
return data
def execute_query_get_proxy_instances(self, query, id):
"""Execute 'query', returning proxy instances."""
data = self.execute_query_get_data(query)
objects = [
make_introspection_object(
t,
type(self)(self.ipc_address),
id,
)
for t in data
]
if query.needs_client_side_filtering():
return list(filter(
lambda i: _object_passes_filters(
i,
**query.get_client_side_filters()
),
objects
))
return objects
class FakeBackend(Backend):
"""A backend that always returns fake data, useful for testing."""
def __init__(self, fake_ipc_return_data):
"""Create a new FakeBackend instance.
If this backend creates any proxy objects, they will be created with
a FakeBackend with the same fake ipc return data.
:param fake_ipc_return_data: The data you want to pretend was returned
by the applicatoin under test. This must be in the correct protocol
format, or the results are undefined.
"""
super(FakeBackend, self).__init__(fake_ipc_return_data)
self.fake_ipc_return_data = fake_ipc_return_data
def execute_query_get_data(self, query):
return self.fake_ipc_return_data
def make_introspection_object(dbus_tuple, backend, object_id):
"""Make an introspection object given a DBus tuple of
(path, state_dict).
:param dbus_tuple: A two-item iterable containing a dbus.String object that
contains the object path, and a dbus.Dictionary object that contains
the objects state dictionary.
:param backend: An instance of the Backend class.
:returns: A proxy object that derives from DBusIntrospectionObject
:raises ValueError: if more than one class is appropriate for this
dbus_tuple
"""
path, state = dbus_tuple
path = path.encode('utf-8')
class_object = _get_proxy_object_class(object_id, path, state)
return class_object(state, path, backend)
def _object_passes_filters(instance, **kwargs):
"""Return true if *instance* satisifies all the filters present in
kwargs."""
with instance.no_automatic_refreshing():
for attr, val in kwargs.items():
if not hasattr(instance, attr) or getattr(instance, attr) != val:
# Either attribute is not present, or is present but with
# the wrong value - don't add this instance to the results
# list.
return False
return True
./autopilot/introspection/_object_registry.py 0000644 0000041 0000041 00000024254 13061552435 022105 0 ustar www-data www-data # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Autopilot Functional Test Tool
# Copyright (C) 2014, 2015 Canonical
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
"""Object registry.
This is an internal module, and is not supposed to be used directly.
This module contains the object registry, which keeps track to the various
classes we use when creating proxy classes. The object registry allows test
authors to write their own classes to be used instead of the generic one that
autopilot creates.
This module contains two global dictionaries, both are keys by unique
connection id's (UUID objects). The values are described below.
* ``_object_registry`` contains dictionaries of class names (as strings)
to class objects. Custom proxy classes defined by test authors will end up
with their class object in this dictionary. This is used when we want to
create a proxy instance - if nothing matches in this dictionary then we
create a generic proxy instance instead.
* ``_proxy_extensions`` contains a tuple of extension classes to mix in to
*every proxy class*. This is used to extend the proxy API on a
per-connection basis. For example, Qt-based apps allow us to monitor
signals and slots in the application, but Gtk apps do not.
"""
from uuid import uuid4
from autopilot.introspection._xpathselect import get_classname_from_path
from autopilot.utilities import get_debug_logger
from contextlib import contextmanager
_object_registry = {}
_proxy_extensions = {}
def register_extension_classes_for_proxy_base(proxy_base, extensions):
global _proxy_extensions
_proxy_extensions[proxy_base._id] = (proxy_base,) + extensions
def _get_proxy_bases_for_id(id):
global _proxy_extensions
return _proxy_extensions.get(id, ())
class IntrospectableObjectMetaclass(type):
"""Metaclass to insert appropriate classes into the object registry."""
def __new__(cls, classname, bases, classdict):
"""Create a new proxy class, possibly adding it to the object registry.
Test authors may derive a class from DBusIntrospectionObject or the
CustomEmulatorBase alias and use this as their 'emulator base'. This
class will be given a unique '_id' attribute. That attribute is the
first level index into the object registry. It's used so we can have
custom proxy classes for more than one process at the same time and
avoid clashes in the dictionary.
"""
cls_id = None
for base in bases:
if hasattr(base, '_id'):
cls_id = base._id
break
else:
# Ignore classes that are in the autopilot class heirarchy:
if classname not in (
'ApplicationProxyObject',
'CustomEmulatorBase',
'DBusIntrospectionObject',
'DBusIntrospectionObjectBase',
):
# Add the '_id' attribute as a class attr:
cls_id = classdict['_id'] = uuid4()
# use the bases passed to us, but extend it with whatever is stored in
# the proxy_extensions dictionary.
extensions = _get_proxy_bases_for_id(cls_id)
for extension in extensions:
if extension not in bases:
bases += (extension,)
# make the object. Nothing special here.
class_object = type.__new__(cls, classname, bases, classdict)
if not classdict.get('__generated', False):
# If the newly made object has an id, add it to the object
# registry.
if getattr(class_object, '_id', None) is not None:
if class_object._id in _object_registry:
_object_registry[class_object._id][classname] = \
class_object
else:
_object_registry[class_object._id] = \
{classname: class_object}
# in all cases, return the class unchanged.
return class_object
DBusIntrospectionObjectBase = IntrospectableObjectMetaclass(
'DBusIntrospectionObjectBase',
(object,),
{}
)
def _get_proxy_object_class(object_id, path, state):
"""Return a custom proxy class, from the object registry or the default.
This function first inspects the object registry using the object_id passed
in. The object_id will be unique to all custom proxy classes for the same
application.
If that fails, we create a class on the fly based on the default class.
:param object_id: The _id attribute of the class doing the lookup. This is
used to index into the object registry to retrieve the dict of proxy
classes to try.
:param path: dbus path
:param state: dbus state
:returns: appropriate custom proxy class
:raises ValueError: if more than one class in the dict matches
"""
class_type = _try_custom_proxy_classes(object_id, path, state)
return class_type or _get_default_proxy_class(
object_id,
get_classname_from_path(path)
)
def _try_custom_proxy_classes(object_id, path, state):
"""Identify which custom proxy class matches the dbus path and state.
If more than one class in proxy_class_dict matches, raise an exception.
:param object_id: id to use to get the dict of proxy classes to try
:param path: dbus path
:param state: dbus state dict
:returns: matching custom proxy class
:raises ValueError: if more than one class matches
"""
proxy_class_dict = _object_registry[object_id]
possible_classes = [c for c in proxy_class_dict.values() if
c.validate_dbus_object(path, state)]
if len(possible_classes) > 1:
raise ValueError(
'More than one custom proxy class matches this object: '
'Matching classes are: %s. State is %s. Path is %s.' % (
','.join([repr(c) for c in possible_classes]),
repr(state),
path,
)
)
if len(possible_classes) == 1:
extended_proxy_bases = _get_proxy_bases_for_id(object_id)
mixed = _combine_base_and_extensions(
possible_classes[0],
extended_proxy_bases
)
possible_classes[0].__bases__ = mixed
return possible_classes[0]
return None
def _combine_base_and_extensions(kls, extensions):
"""Returns the bases of the given class augmented with extensions
In order to get the right bases tuple, the given class is removed
from the result (to prevent cyclic dependencies), there's only one
occurrence of each final base class in the result and the result
is ordered following the inheritance order (classes lower in the
inheritance tree are listed before in the resulting tuple)
:param kls: class for which we are combining bases and extensions
:param extensions: tuple of extensions to be added to kls' bases
:returns: bases tuple for kls, including its former bases and the
extensions
"""
# set of bases + extensions removing the original class to prevent
# TypeError: a __bases__ item causes an inheritance cycle
unique_bases = {x for x in kls.__bases__ + extensions if x != kls}
# sort them taking into account inheritance to prevent
# TypeError: Cannot create a consistent method resolution order (MRO)
return tuple(
sorted(
unique_bases,
key=lambda cls: _get_mro_sort_order(cls, extensions),
reverse=True
)
)
def _get_mro_sort_order(cls, promoted_collection=()):
"""Returns the comparable numerical order for the given class honouring
its MRO
It accepts an optional parameter for promoting classes in a certain
group, this can give more control over the sorting when two classes
have the a MRO of the same length
:param cls: the subject class
:param promoted_collection: tuple of classes which must be promoted
:returns: comparable numerical order, higher for classes with MROs of
greater length
"""
# Multiplying by 2 the lenght of the MRO list gives the chance to promote
# items in the promoted_collection by adding them 1 later: non promoted
# classes will have even scores and promoted classes with MRO of the same
# length will have odd scores one point higher
order = 2 * len(cls.mro())
if cls in promoted_collection:
order += 1
return order
def _get_default_proxy_class(id, name):
"""Return a custom proxy object class of the default or a base class.
We want the object to inherit from the class that is set as the emulator
base class, not the class that is doing the selecting. Using the passed id
we retrieve the relevant bases from the object registry.
:param id: The object id (_id attribute) of the class doing the lookup.
:param name: name of new class
:returns: custom proxy object class
"""
get_debug_logger().warning(
"Generating introspection instance for type '%s' based on generic "
"class.", name)
if isinstance(name, bytes):
name = name.decode('utf-8')
return type(name, _get_proxy_bases_for_id(id), dict(__generated=True))
@contextmanager
def patch_registry(new_registry):
"""A utility context manager that allows us to patch the object registry.
Within the scope of the context manager, the object registry will be set
to the 'new_registry' value passed in. When the scope exits, the old object
registry will be restored.
"""
global _object_registry
old_registry = _object_registry
_object_registry = new_registry
try:
yield
except Exception:
raise
finally:
_object_registry = old_registry
./autopilot/introspection/_search.py 0000644 0000041 0000041 00000074065 13061552435 020161 0 ustar www-data www-data # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Autopilot Functional Test Tool
# Copyright (C) 2014 Canonical
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
"""Private module for searching dbus for useful connections."""
import dbus
import logging
import os
import psutil
import subprocess
from functools import partial
from operator import methodcaller
from autopilot import dbus_handler
from autopilot._timeout import Timeout
from autopilot.exceptions import ProcessSearchError
from autopilot.introspection import backends
from autopilot.introspection import constants
from autopilot.introspection import dbus as ap_dbus
from autopilot.introspection import _object_registry
from autopilot.introspection._xpathselect import get_classname_from_path
from autopilot.introspection.backends import WireProtocolVersionMismatch
from autopilot.introspection.utilities import (
_get_bus_connections_pid,
_pid_is_running,
process_util,
)
from autopilot.utilities import deprecated
logger = logging.getLogger(__name__)
@deprecated('get_proxy_object_for_existing_process')
def get_autopilot_proxy_object_for_process(
process,
emulator_base,
dbus_bus='session'
):
"""Return the autopilot proxy object for the given *process*.
:raises RuntimeError: if no autopilot interface was found.
"""
pid = process.pid
proxy_obj = get_proxy_object_for_existing_process(
pid,
process=process,
emulator_base=emulator_base,
dbus_bus=dbus_bus,
)
proxy_obj.set_process(process)
return proxy_obj
def get_proxy_object_for_existing_process(**kwargs):
"""Return a single proxy object for an application that is already running
(i.e. launched outside of Autopilot).
Searches the given bus (supplied by the kwarg **dbus_bus**) for an
application matching the search criteria (also supplied in kwargs, see
further down for explaination on what these can be.)
Returns a proxy object created using the supplied custom emulator
**emulator_base** (which defaults to None).
This function take kwargs arguments containing search parameter values to
use when searching for the target application.
**Possible search criteria**:
*(unless specified otherwise these parameters default to None)*
:param pid: The PID of the application to search for.
:param process: The process of the application to search for.
If provided only the pid of the process is used in the search, but if
the process exits before the search is complete it is used to supply
details provided by the process object.
:param connection_name: A string containing the DBus connection name to
use with the search criteria.
:param application_name: A string containing the applications name to
search for.
:param object_path: A string containing the object path to use as the
search criteria.
Defaults to:
:py:data:`autopilot.introspection.constants.AUTOPILOT_PATH`.
**Non-search parameters:**
:param dbus_bus: The DBus bus to search for the application.
Must be a string containing either 'session', 'system' or the
custom buses name (i.e. 'unix:abstract=/tmp/dbus-IgothuMHNk').
Defaults to 'session'
:param emulator_base: The custom emulator to use when creating the
resulting proxy object.
Defaults to None
**Exceptions possibly thrown by this function:**
:raises ProcessSearchError: If no search criteria match.
:raises RuntimeError: If the search criteria results in many matches.
:raises RuntimeError: If both ``process`` and ``pid`` are supplied, but
``process.pid != pid``.
**Examples:**
Retrieving an application on the system bus where the applications PID is
known::
app_proxy = get_proxy_object_for_existing_process(pid=app_pid)
Multiple criteria are allowed, for instance you could search on **pid**
and **connection_name**::
app_proxy = get_proxy_object_for_existing_process(
pid=app_pid,
connection_name='org.gnome.Gedit'
)
If the application from the previous example was on the system bus::
app_proxy = get_proxy_object_for_existing_process(
dbus_bus='system',
pid=app_pid,
connection_name='org.gnome.Gedit'
)
It is possible to search for the application given just the applications
name.
An example for an application running on a custom bus searching using the
applications name::
app_proxy = get_proxy_object_for_existing_process(
application_name='qmlscene',
dbus_bus='unix:abstract=/tmp/dbus-IgothuMHNk'
)
"""
# Pop off non-search stuff.
dbus_bus = _get_dbus_bus_from_string(kwargs.pop('dbus_bus', 'session'))
process = kwargs.pop('process', None)
emulator_base = kwargs.pop('emulator_base', None)
# Force default object_path
kwargs['object_path'] = kwargs.get('object_path', constants.AUTOPILOT_PATH)
# Special handling of pid.
pid = _check_process_and_pid_details(process, kwargs.get('pid', None))
if pid is not None:
kwargs['pid'] = pid
matcher_function = _filter_function_from_search_params(kwargs)
connections = _find_matching_connections(
dbus_bus,
matcher_function,
process
)
if pid is not None:
# Due to the filtering including children parents, if there exists a
# top-level pid, take that instead of any children that may have
# matched.
connections = _filter_parent_pids_from_children(
pid,
connections,
dbus_bus
)
_raise_if_not_single_result(
connections,
_get_search_criteria_string_representation(**kwargs)
)
object_path = kwargs['object_path']
connection_name = connections[0]
return _make_proxy_object(
_get_dbus_address_object(connection_name, object_path, dbus_bus),
emulator_base
)
def get_proxy_object_for_existing_process_by_name(
process_name,
emulator_base=None
):
"""Return the proxy object for a process by its name.
:param process_name: name of the process to get proxy object.
This must be a string.
:param emulator_base: emulator base to use with the custom proxy object.
:raises ValueError: if process not running or more than one PIDs
associated with the process.
:return: proxy object for the requested process.
"""
pid = process_util.get_pid_for_process(process_name)
return get_proxy_object_for_existing_process(
pid=pid,
emulator_base=emulator_base
)
def _map_connection_to_pid(connection, dbus_bus):
try:
return _get_bus_connections_pid(dbus_bus, connection)
except dbus.DBusException as e:
logger.info(
"dbus.DBusException while attempting to get PID for %s: %r" %
(connection, e))
def _filter_parent_pids_from_children(
pid, connections, dbus_bus, _connection_pid_fn=_map_connection_to_pid):
"""Return any connections that have an actual pid matching the requested
and aren't just a child of that requested pid.
:param pid: Pid passed in for matching
:param connections: List of connections to filter
:param dbus_bus: Dbus object that the connections are contained.
:param _connection_pid_fn: Function that takes 2 args 'connection' 'dbus
object' that returns the pid (or None if not found) of the connection on
that dbus bus.
(Note: Useful for testing.)
:returns: List of suitable connections (e.g. returns what was passed if no
connections match the pid (i.e. all matches are children)).
"""
for conn in connections:
if pid == _connection_pid_fn(conn, dbus_bus):
logger.info('Found the parent pid, ignoring any others.')
return [conn]
return connections
def _get_dbus_bus_from_string(dbus_string):
if dbus_string == 'session':
return dbus_handler.get_session_bus()
elif dbus_string == 'system':
return dbus_handler.get_system_bus()
else:
return dbus_handler.get_custom_bus(dbus_string)
def _check_process_and_pid_details(process=None, pid=None):
"""Do error checking on process and pid specification.
:raises RuntimeError: if both process and pid are specified, but the
process's 'pid' attribute is different to the pid attribute specified.
:raises ProcessSearchError: if the process specified is not running.
:returns: the pid to use in all search queries.
"""
if process is not None:
if pid is None:
pid = process.pid
elif pid != process.pid:
raise RuntimeError("Supplied PID and process.pid do not match.")
if pid is not None and not _pid_is_running(pid):
raise ProcessSearchError("PID %d could not be found" % pid)
return pid
def _filter_function_from_search_params(search_params, filter_lookup=None):
filters = _mandatory_filters() + _filters_from_search_parameters(
search_params,
filter_lookup
)
return _filter_function_with_sorted_filters(filters, search_params)
def _mandatory_filters():
"""Returns a list of Filters that are considered mandatory regardless of
the search parameters supplied by the user.
"""
return [
ConnectionIsNotOurConnection,
ConnectionIsNotOrgFreedesktopDBus
]
def _filter_lookup_map():
return dict(
connection_name=ConnectionHasName,
application_name=ConnectionHasAppName,
object_path=ConnectionHasPathWithAPInterface,
pid=ConnectionHasPid,
)
def _filters_from_search_parameters(parameters, filter_lookup=None):
parameter_filter_lookup = filter_lookup or _filter_lookup_map()
try:
filter_list = list({
parameter_filter_lookup[key]
for key in parameters.keys()
})
return filter_list
except KeyError as e:
raise KeyError(
"Search parameter %s doesn't have a corresponding filter in %r"
% (e, parameter_filter_lookup),
)
def _filter_function_with_sorted_filters(filters, search_params):
"""Returns a callable filter function that will take the argument
(dbus_tuple).
The returned filter function will be bound to use a prioritised filter list
and the supplied search parameters dictionary.
"""
sorted_filter_list = _priority_sort_filters(filters)
return partial(_filter_runner, sorted_filter_list, search_params)
def _priority_sort_filters(filter_list):
return sorted(filter_list, key=methodcaller('priority'), reverse=True)
def _filter_runner(filter_list, search_parameters, dbus_tuple):
"""Helper function to run filters over dbus connections.
:param filter_list: List of filters to call matches on passing the provided
dbus details and search parameters.
:param dbus_tuple: 2 length tuple containing (bus, connection_name) where
bus is a SessionBus, SystemBus or BusConnection object and
connection_name is a string.
:param search_parameters: Dictionary of search parameters that the filters
will consume to make their decisions.
"""
if not filter_list:
raise ValueError("Filter list must not be empty")
return all(
f.matches(dbus_tuple, search_parameters)
for f in filter_list
)
def _find_matching_connections(bus, connection_matcher, process=None):
"""Returns a list of connection names that have passed the
connection_matcher.
:param dbus_bus: A DBus bus object to search
(i.e. SessionBus, SystemBus or BusConnection)
:param connection_matcher: Callable that takes a connection name and
returns True if it is what we're looking for, False otherwise.
:param process: (optional) A process object that we're looking for it's
dbus connection.
Used to ensure that the process is in fact still running
while we're searching for it.
"""
for _ in Timeout.default():
_get_child_pids.reset_cache()
_raise_if_process_has_exited(process)
connections = bus.list_names()
valid_connections = [
c for c
in connections
if connection_matcher((bus, c))
]
if len(valid_connections) >= 1:
return _dedupe_connections_on_pid(valid_connections, bus)
return []
def _raise_if_process_has_exited(process):
"""Raises ProcessSearchError if process is no longer running."""
if process is not None and not _process_is_running(process):
return_code = process.poll()
raise ProcessSearchError(
"Process exited with exit code: %d"
% return_code
)
def _process_is_running(process):
return process.poll() is None
def _dedupe_connections_on_pid(valid_connections, bus):
seen_pids = []
deduped_connections = []
for connection in valid_connections:
pid = _get_bus_connections_pid(bus, connection)
if pid not in seen_pids:
seen_pids.append(pid)
deduped_connections.append(connection)
return deduped_connections
def _raise_if_not_single_result(connections, criteria_string):
if connections is None or len(connections) == 0:
raise ProcessSearchError(
"Search criteria (%s) returned no results" %
(criteria_string)
)
if len(connections) > 1:
raise RuntimeError(
"Search criteria (%s) returned multiple results"
% (criteria_string)
)
def _get_search_criteria_string_representation(**kwargs):
# Some slight re-naming for process objects
if kwargs.get('process') is not None:
kwargs['process_object'] = "%r" % kwargs.pop('process')
return ", ".join([
"%s = %r" % (k.replace("_", " "), v)
for k, v
in kwargs.items()
])
def _make_proxy_object(dbus_address, emulator_base):
"""Returns a root proxy object given a DBus service name.
:param dbus_address: The DBusAddress object we're querying.
:param emulator_base: The emulator base object (or None), as provided by
the user.
"""
# make sure we always have an emulator base. Either use the one the user
# gave us, or make one:
emulator_base = emulator_base or _make_default_emulator_base()
_raise_if_base_class_not_actually_base(emulator_base)
# Get the dbus introspection Xml for the backend.
intro_xml = _get_introspection_xml_from_backend(dbus_address)
try:
# Figure out if the backend has any extension methods, and return
# classes that understand how to use each of those extensions:
extension_classes = _get_proxy_bases_from_introspection_xml(intro_xml)
# Register those base classes for everything that will derive from this
# emulator base class.
_object_registry.register_extension_classes_for_proxy_base(
emulator_base,
extension_classes,
)
except RuntimeError as e:
e.args = (
"Could not find Autopilot interface on dbus address '%s'."
% dbus_address,
)
raise e
cls_name, path, cls_state = _get_proxy_object_class_name_and_state(
dbus_address
)
proxy_class = _object_registry._get_proxy_object_class(
emulator_base._id,
path,
cls_state
)
# For this object only, add the ApplicationProxy class, since it's the
# root of the tree. Ideally this would be nicer...
if ApplicationProxyObject not in proxy_class.__bases__:
proxy_class.__bases__ += (ApplicationProxyObject, )
return proxy_class(cls_state, path, backends.Backend(dbus_address))
def _make_default_emulator_base():
"""Make a default base class for all proxy classes to derive from."""
return type("DefaultEmulatorBase", (ap_dbus.DBusIntrospectionObject,), {})
WRONG_CPO_CLASS_MSG = '''\
base_class: {passed} does not appear to be the actual base CPO class.
Perhaps you meant to use: {actual}.'''
def _raise_if_base_class_not_actually_base(base_class):
"""Raises ValueError if the provided base_class is not actually the
base_class
To ensure that the expected base classes are used when creating proxy
objects.
:param base_class: The base class to check.
:raises ValueError: The actual base class is not the one provided
"""
actual_base_class = base_class
for cls in base_class.mro():
if hasattr(cls, '_id'):
actual_base_class = cls
if actual_base_class != base_class:
raise(
ValueError(
WRONG_CPO_CLASS_MSG.format(
passed=base_class,
actual=actual_base_class
)
)
)
def _make_proxy_object_async(
data_source, emulator_base, reply_handler, error_handler):
"""Make a proxy object for a dbus backend.
Similar to :meth:`_make_proxy_object` except this method runs
asynchronously and must have a reply_handler callable set. The
reply_handler will be called with a single argument: The proxy object.
"""
# Note: read this function backwards!
#
# Due to the callbacks, I need to define the end of the callback chain
# first, so start reading from the bottom of the function upwards, and
# it'll make a whole lot more sense.
# Final phase: We have all the information we need, now we construct
# everything. This phase has no dbus calls, and so is very fast:
def build_proxy(introspection_xml, cls_name, path, cls_state):
# Figure out if the backend has any extension methods, and return
# classes that understand how to use each of those extensions:
extension_classes = _get_proxy_bases_from_introspection_xml(
introspection_xml
)
# Register those base classes for everything that will derive from this
# emulator base class.
_object_registry.register_extension_classes_for_proxy_base(
emulator_base,
extension_classes,
)
proxy_class = _object_registry._get_proxy_object_class(
emulator_base._id,
path,
cls_state
)
reply_handler(
proxy_class(cls_state, path, backends.Backend(data_source))
)
# Phase 2: We recieve the introspection string, and make an asynchronous
# dbus call to get the state information for the root of this applicaiton.
def get_root_state(introspection_xml):
_get_proxy_object_class_name_and_state(
data_source,
reply_handler=partial(build_proxy, introspection_xml),
error_handler=error_handler,
)
# Phase 1: Make an asynchronous dbus call to get the introspection xml
# from the data_source provided for us.
emulator_base = emulator_base or _make_default_emulator_base()
_get_introspection_xml_from_backend(
data_source,
reply_handler=get_root_state,
error_handler=error_handler
)
def _get_introspection_xml_from_backend(
backend, reply_handler=None, error_handler=None):
"""Get DBus Introspection xml from a backend.
:param backend: The backend object to query.
:param reply_handler: If set, makes a dbus async call, and the result will
be sent to reply_handler. This must be a callable object.
:param error_handler: If set, this callable will recieve any errors, and
the call will be made asyncronously.
:returns: A string containing introspection xml, if called synchronously.
:raises ValueError: if one, but not both of 'reply_handler' and
'error_handler' are set.
"""
if callable(reply_handler) and callable(error_handler):
backend.dbus_introspection_iface.Introspect(
reply_handler=reply_handler,
error_handler=error_handler,
)
elif reply_handler or error_handler:
raise ValueError(
"Both 'reply_handler' and 'error_handler' must be set."
)
else:
return backend.dbus_introspection_iface.Introspect()
def _get_proxy_object_class_name_and_state(
backend, reply_handler=None, error_handler=None):
"""Get details about this autopilot backend via a dbus GetState call.
:param reply_handler: A callable that must accept three positional
arguments, which correspond to the return value of this function when
called synchronously.
:param error_handler: A callable which will recieve any dbus errors, should
they occur.
:raises ValueError: if one, but not both of reply_handler and error_handler
are set.
:returns: A tuple containing the class name of the root of the
introspection tree, the full path to the root of the introspection
tree, and the state dictionary of the root node in the introspection
tree.
"""
if callable(reply_handler) and callable(error_handler):
# Async call:
# Since we get an array of state, and we only care about the first one
# we use a lambda to unpack it and get the details we want.
backend.introspection_iface.GetState(
"/",
reply_handler=lambda r: reply_handler(
*_get_details_from_state_data(r[0])
),
error_handler=error_handler,
)
elif reply_handler or error_handler:
raise ValueError(
"Both 'reply_handler' and 'error_handler' must be set."
)
else:
# Sync call
state = backend.introspection_iface.GetState("/")[0]
return _get_details_from_state_data(state)
def _get_details_from_state_data(state_data):
"""Get details from a state data array.
Returns class name, path, and state dictionary.
"""
object_path, object_state = state_data
return (
get_classname_from_path(object_path),
object_path.encode('utf-8'),
object_state,
)
def _get_proxy_bases_from_introspection_xml(introspection_xml):
"""Return tuple of the base classes to use when creating a proxy object.
Currently this works by looking for certain interface names in the XML. In
the future we may want to parse the XML and perform more rigerous checks.
:param introspection_xml: An xml string that describes the exported object
on the dbus backend. This determines which capabilities are present in
the backend, and therefore which base classes should be used to create
the proxy object.
:raises RuntimeError: if the autopilot interface cannot be found.
"""
bases = []
if constants.AP_INTROSPECTION_IFACE not in introspection_xml:
raise RuntimeError("Could not find Autopilot interface.")
if constants.QT_AUTOPILOT_IFACE in introspection_xml:
from autopilot.introspection.qt import QtObjectProxyMixin
bases.append(QtObjectProxyMixin)
return tuple(bases)
class ApplicationProxyObject(object):
"""A class that better supports query data from an application."""
def __init__(self):
self._process = None
def set_process(self, process):
"""Set the subprocess.Popen object of the process that this is a proxy
for.
You should never normally need to call this method.
"""
self._process = process
@property
def pid(self):
return self._process.pid
@property
def process(self):
return self._process
@deprecated(
"the AutopilotTestCase launch_test_application method to handle"
" cleanup of launched applications."
)
def kill_application(self):
"""Kill the running process that this is a proxy for using
'kill `pid`'."""
subprocess.call(["kill", "%d" % self._process.pid])
def _extend_proxy_bases_with_emulator_base(proxy_bases, emulator_base):
if emulator_base is None:
emulator_base = type(
'DefaultEmulatorBase',
(ap_dbus.CustomEmulatorBase,),
{}
)
return proxy_bases + (emulator_base, )
def _get_dbus_address_object(connection_name, object_path, bus):
return backends.DBusAddress(bus, connection_name, object_path)
class _cached_get_child_pids(object):
"""Get a list of all child process Ids, for the given parent.
Since we call this often, and it's a very expensive call, we optimise this
such that the return value will be cached for each scan through the dbus
bus.
Calling reset_cache() at the end of each dbus scan will ensure that you get
fresh values on the next call.
"""
def __init__(self):
self._cached_result = None
def __call__(self, pid):
if self._cached_result is None:
self._cached_result = [
p.pid for p in psutil.Process(pid).children(recursive=True)
]
return self._cached_result
def reset_cache(self):
self._cached_result = None
_get_child_pids = _cached_get_child_pids()
# Filters
class ConnectionIsNotOrgFreedesktopDBus(object):
"""Not interested in any connections with names 'org.freedesktop.DBus'."""
@classmethod
def priority(cls):
"""A connection with this name will never be valid."""
return 13
@classmethod
def matches(cls, dbus_tuple, params):
bus, connection_name = dbus_tuple
return connection_name != 'org.freedesktop.DBus'
class ConnectionIsNotOurConnection(object):
"""Ensure we're not inspecting our own bus connection."""
@classmethod
def priority(cls):
"""The connection from this process will never be valid."""
return 12
@classmethod
def matches(cls, dbus_tuple, params):
try:
bus, connection_name = dbus_tuple
bus_pid = _get_bus_connections_pid(bus, connection_name)
return bus_pid != os.getpid()
except dbus.DBusException:
return False
class ConnectionHasName(object):
"""Ensure connection_name within dbus_tuple is the name we want."""
@classmethod
def priority(cls):
"""Connection name is easy to check for and if not valid, nothing else
will be.
"""
return 11
@classmethod
def matches(cls, dbus_tuple, params):
"""Returns true if the connection name in dbus_tuple is the name
in the search criteria params.
"""
requested_connection_name = params['connection_name']
bus, connection_name = dbus_tuple
return connection_name == requested_connection_name
class ConnectionHasPid(object):
"""Match a connection based on the connections pid."""
@classmethod
def priority(cls):
return 9
@classmethod
def matches(cls, dbus_tuple, params):
"""Match a connection based on the connections pid.
:raises KeyError: if the pid parameter isn't passed in params.
"""
pid = params['pid']
bus, connection_name = dbus_tuple
try:
bus_pid = _get_bus_connections_pid(bus, connection_name)
except dbus.DBusException as e:
logger.info(
"dbus.DBusException while attempting to get PID for %s: %r" %
(connection_name, e))
return False
eligible_pids = [pid] + _get_child_pids(pid)
return bus_pid in eligible_pids
class ConnectionHasPathWithAPInterface(object):
"""Ensure that the connection exposes the Autopilot interface."""
@classmethod
def priority(cls):
return 8
@classmethod
def matches(cls, dbus_tuple, params):
"""Ensure the connection has the path that we expect to be there.
:raises KeyError: if the object_path parameter isn't included in
params.
"""
try:
bus, connection_name = dbus_tuple
path = params['object_path']
obj = bus.get_object(connection_name, path)
dbus.Interface(
obj,
'com.canonical.Autopilot.Introspection'
).GetVersion()
return True
except dbus.DBusException:
return False
class ConnectionHasAppName(object):
"""Ensure the connection has the requested Application name."""
@classmethod
def priority(cls):
return 0
@classmethod
def matches(cls, dbus_tuple, params):
"""Returns True if dbus_tuple has the required application name.
Can be provided an optional object_path parameter. This defaults to
:py:data:`autopilot.introspection.constants.AUTOPILOT_PATH` if not
provided.
This filter should only activated if the application_name is provided
in the search criteria.
:raises KeyError: if the 'application_name' parameter isn't passed in
params
"""
requested_app_name = params['application_name']
object_path = params.get('object_path', constants.AUTOPILOT_PATH)
bus, connection_name = dbus_tuple
try:
app_name = cls._get_application_name(
bus,
connection_name,
object_path
)
return app_name == requested_app_name
except WireProtocolVersionMismatch:
return False
@classmethod
def _get_application_name(cls, bus, connection_name, object_path):
dbus_object = _get_dbus_address_object(
connection_name,
object_path,
bus
)
return cls._get_application_name_from_dbus_address(dbus_object)
@classmethod
def _get_application_name_from_dbus_address(cls, dbus_address):
"""Return the application name from a dbus_address object."""
return get_classname_from_path(
dbus_address.introspection_iface.GetState('/')[0][0]
)
./autopilot/introspection/constants.py 0000644 0000041 0000041 00000002137 13061552435 020560 0 ustar www-data www-data # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Autopilot Functional Test Tool
# Copyright (C) 2012-2013 Canonical
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
"""Package for the constants used for introspection"""
AUTOPILOT_PATH = "/com/canonical/Autopilot/Introspection"
QT_AUTOPILOT_IFACE = 'com.canonical.Autopilot.Qt'
AP_INTROSPECTION_IFACE = 'com.canonical.Autopilot.Introspection'
DBUS_INTROSPECTION_IFACE = 'org.freedesktop.DBus.Introspectable'
CURRENT_WIRE_PROTOCOL_VERSION = "1.4"
./autopilot/introspection/types.py 0000644 0000041 0000041 00000060007 13061552435 017710 0 ustar www-data www-data # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Autopilot Functional Test Tool
# Copyright (C) 2013 Canonical
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
"""
Autopilot proxy type support.
=============================
This module defines the classes that are used for all attributes on proxy
objects. All proxy objects contain attributes that transparently mirror the
values present in the application under test. Autopilot takes care of keeping
these values up to date.
Object attributes fall into two categories. Attributes that are a single
string, boolean, or integer property are sent directly across DBus. These are
called "plain" types, and are stored in autopilot as instnaces of the
:class:`PlainType` class. Attributes that are more complex (a rectangle, for
example) are called "complex" types, and are split into several component
values, sent across dbus, and are then reconstituted in autopilot into useful
objects.
"""
import pytz
from datetime import datetime, time, timedelta
from dateutil.tz import gettz
import dbus
import logging
from testtools.matchers import Equals
from autopilot.introspection.utilities import translate_state_keys
from autopilot.utilities import sleep, compatible_repr
_logger = logging.getLogger(__name__)
class ValueType(object):
"""Store constants for different special types that autopilot understands.
DO NOT add items here unless you have documented them correctly in
docs/appendix/protocol.rst.
"""
PLAIN = 0
RECTANGLE = 1
POINT = 2
SIZE = 3
COLOR = 4
DATETIME = 5
TIME = 6
POINT3D = 7
UNKNOWN = -1
def create_value_instance(value, parent, name):
"""Create an object that exposes the interesing part of the value
specified, given the value_type_id.
:param parent: The object this attribute belongs to.
:param name: The name of this attribute.
:param value: The value array from DBus.
"""
type_dict = {
ValueType.PLAIN: _make_plain_type,
ValueType.RECTANGLE: Rectangle,
ValueType.COLOR: Color,
ValueType.POINT: Point,
ValueType.SIZE: Size,
ValueType.DATETIME: DateTime,
ValueType.TIME: Time,
ValueType.POINT3D: Point3D,
ValueType.UNKNOWN: _make_plain_type,
}
type_id = value[0]
value = value[1:]
if type_id not in type_dict:
_logger.warning("Unknown type id %d", type_id)
type_id = ValueType.UNKNOWN
type_class = type_dict.get(type_id, None)
if type_id == ValueType.UNKNOWN:
value = [dbus.Array(value)]
if len(value) == 0:
raise ValueError("Cannot create attribute, no data supplied")
return type_class(*value, parent=parent, name=name)
class TypeBase(object):
def wait_for(self, expected_value, timeout=10):
"""Wait up to 10 seconds for our value to change to
*expected_value*.
*expected_value* can be a testtools.matcher. Matcher subclass (like
LessThan, for example), or an ordinary value.
This works by refreshing the value using repeated dbus calls.
:raises AssertionError: if the attribute was not equal to the
expected value after 10 seconds.
:raises RuntimeError: if the attribute you called this on was not
constructed as part of an object.
"""
# It's guaranteed that our value is up to date, since __getattr__
# calls refresh_state. This if statement stops us waiting if the
# value is already what we expect:
if self == expected_value:
return
if self.name is None or self.parent is None:
raise RuntimeError(
"This variable was not constructed as part of "
"an object. The wait_for method cannot be used."
)
def make_unicode(value):
if isinstance(value, bytes):
return value.decode('utf8')
return value
if hasattr(expected_value, 'expected'):
expected_value.expected = make_unicode(expected_value.expected)
# unfortunately not all testtools matchers derive from the Matcher
# class, so we can't use issubclass, isinstance for this:
match_fun = getattr(expected_value, 'match', None)
is_matcher = match_fun and callable(match_fun)
if not is_matcher:
expected_value = Equals(expected_value)
time_left = timeout
while True:
# TODO: These next three lines are duplicated from the parent...
# can we just have this code once somewhere?
_, new_state = self.parent._get_new_state()
new_state = translate_state_keys(new_state)
new_value = new_state[self.name][1:]
if len(new_value) == 1:
new_value = make_unicode(new_value[0])
# Support for testtools.matcher classes:
mismatch = expected_value.match(new_value)
if mismatch:
failure_msg = mismatch.describe()
else:
self.parent._set_properties(new_state)
return
if time_left >= 1:
sleep(1)
time_left -= 1
else:
sleep(time_left)
break
raise AssertionError(
"After %.1f seconds test on %s.%s failed: %s" % (
timeout, self.parent.__class__.__name__, self.name,
failure_msg))
class PlainType(TypeBase):
"""Plain type support in autopilot proxy objects.
Instances of this class will be used for all plain attrubites. The word
"plain" in this context means anything that's marshalled as a string,
boolean or integer type across dbus.
Instances of these classes can be used just like the underlying type. For
example, given an object property called 'length' that is marshalled over
dbus as an integer value, the following will be true::
>>> isinstance(object.length, PlainType)
True
>>> isinstance(object.length, int)
True
>>> print(object.length)
123
>>> print(object.length + 32)
155
However, a special case exists for boolean values: because you cannot
subclass from the 'bool' type, the following check will fail (
``object.visible`` is a boolean property)::
>>> isinstance(object.visible, bool)
False
However boolean values will behave exactly as you expect them to.
"""
def __new__(cls, value, parent=None, name=None):
return _make_plain_type(value, parent=parent, name=name)
def _get_repr_callable_for_value_class(cls):
repr_map = {
dbus.Byte: _integer_repr,
dbus.Int16: _integer_repr,
dbus.Int32: _integer_repr,
dbus.UInt16: _integer_repr,
dbus.UInt32: _integer_repr,
dbus.Int64: _integer_repr,
dbus.UInt64: _integer_repr,
dbus.String: _text_repr,
dbus.ObjectPath: _text_repr,
dbus.Signature: _text_repr,
dbus.ByteArray: _bytes_repr,
dbus.Boolean: _boolean_repr,
dbus.Dictionary: _dict_repr,
dbus.Double: _float_repr,
dbus.Struct: _tuple_repr,
dbus.Array: _list_repr,
}
return repr_map.get(cls, None)
def _get_str_callable_for_value_class(cls):
str_map = {
dbus.Boolean: _boolean_str,
dbus.Byte: _integer_str,
}
return str_map.get(cls, None)
@compatible_repr
def _integer_repr(self):
return str(int(self))
def _create_generic_repr(target_type):
return compatible_repr(lambda self: repr(target_type(self)))
_bytes_repr = _create_generic_repr(bytes)
_text_repr = _create_generic_repr(str)
_dict_repr = _create_generic_repr(dict)
_list_repr = _create_generic_repr(list)
_tuple_repr = _create_generic_repr(tuple)
_float_repr = _create_generic_repr(float)
_boolean_repr = _create_generic_repr(bool)
def _create_generic_str(target_type):
return compatible_repr(lambda self: str(target_type(self)))
_boolean_str = _create_generic_str(bool)
_integer_str = _integer_repr
def _make_plain_type(value, parent=None, name=None):
new_type = _get_plain_type_class(type(value), parent, name)
return new_type(value)
# Thomi 2014-03-27: dbus types are immutable, which means that we cannot set
# parent and name on the instances we create. This means we have to set them
# as type attributes, which means that this cache doesn't speed things up that
# much. Ideally we'd not rely on the dbus types at all, and simply transform
# them into our own types, but that's work for a separate branch.
#
# Further to the above, we cannot cache these results, since the hash for
# the parent parameter is almost always the same, leading to incorrect cache
# hits. We really need to implement our own types here I think.
def _get_plain_type_class(value_class, parent, name):
new_type_name = value_class.__name__
new_type_bases = (value_class, PlainType)
new_type_dict = dict(parent=parent, name=name)
repr_callable = _get_repr_callable_for_value_class(value_class)
if repr_callable:
new_type_dict['__repr__'] = repr_callable
str_callable = _get_str_callable_for_value_class(value_class)
if str_callable:
new_type_dict['__str__'] = str_callable
return type(new_type_name, new_type_bases, new_type_dict)
def _array_packed_type(num_args):
"""Return a base class that accepts 'num_args' and is packed into a dbus
Array type.
"""
class _ArrayPackedType(dbus.Array, TypeBase):
def __init__(self, *args, **kwargs):
if len(args) != self._required_arg_count:
raise ValueError(
"%s must be constructed with %d arguments, not %d"
% (
self.__class__.__name__,
self._required_arg_count,
len(args)
)
)
super(_ArrayPackedType, self).__init__(args)
# TODO: pop instead of get, and raise on unknown kwarg
self.parent = kwargs.get("parent", None)
self.name = kwargs.get("name", None)
return type(
"_ArrayPackedType_{}".format(num_args),
(_ArrayPackedType,),
dict(_required_arg_count=num_args)
)
class Rectangle(_array_packed_type(4)):
"""The RectangleType class represents a rectangle in cartesian space.
To construct a rectangle, pass the x, y, width and height parameters in to
the class constructor::
my_rect = Rectangle(12,13,100,150)
These attributes can be accessed either using named attributes, or via
sequence indexes::
>>>my_rect = Rectangle(12,13,100,150)
>>> my_rect.x == my_rect[0] == 12
True
>>> my_rect.y == my_rect[1] == 13
True
>>> my_rect.w == my_rect[2] == 100
True
>>> my_rect.h == my_rect[3] == 150
True
You may also access the width and height values using the ``width`` and
``height`` properties::
>>> my_rect.width == my_rect.w
True
>>> my_rect.height == my_rect.h
True
Rectangles can be compared using ``==`` and ``!=``, either to another
Rectangle instance, or to any mutable sequence type::
>>> my_rect == [12, 13, 100, 150]
True
>>> my_rect != Rectangle(1,2,3,4)
True
"""
@property
def x(self):
return self[0]
@property
def y(self):
return self[1]
@property
def w(self):
return self[2]
@property
def width(self):
return self[2]
@property
def h(self):
return self[3]
@property
def height(self):
return self[3]
@compatible_repr
def __repr__(self):
coords = ', '.join((str(c) for c in self))
return 'Rectangle(%s)' % (coords)
class Point(_array_packed_type(2)):
"""The Point class represents a 2D point in cartesian space.
To construct a Point, pass in the x, y parameters to the class
constructor::
>>> my_point = Point(50,100)
These attributes can be accessed either using named attributes, or via
sequence indexes::
>>> my_point.x == my_point[0] == 50
True
>>> my_point.y == my_point[1] == 100
True
Point instances can be compared using ``==`` and ``!=``, either to another
Point instance, or to any mutable sequence type with the correct number of
items::
>>> my_point == [50, 100]
True
>>> my_point != Point(5, 10)
True
"""
@property
def x(self):
return self[0]
@property
def y(self):
return self[1]
@compatible_repr
def __repr__(self):
return 'Point(%d, %d)' % (self.x, self.y)
class Size(_array_packed_type(2)):
"""The Size class represents a 2D size in cartesian space.
To construct a Size, pass in the width, height parameters to the class
constructor::
>>> my_size = Size(50,100)
These attributes can be accessed either using named attributes, or via
sequence indexes::
>>> my_size.width == my_size.w == my_size[0] == 50
True
>>> my_size.height == my_size.h == my_size[1] == 100
True
Size instances can be compared using ``==`` and ``!=``, either to another
Size instance, or to any mutable sequence type with the correct number of
items::
>>> my_size == [50, 100]
True
>>> my_size != Size(5, 10)
True
"""
@property
def w(self):
return self[0]
@property
def width(self):
return self[0]
@property
def h(self):
return self[1]
@property
def height(self):
return self[1]
@compatible_repr
def __repr__(self):
return 'Size(%d, %d)' % (self.w, self.h)
class Color(_array_packed_type(4)):
"""The Color class represents an RGBA Color.
To construct a Color, pass in the red, green, blue and alpha parameters to
the class constructor::
>>> my_color = Color(50, 100, 200, 255)
These attributes can be accessed either using named attributes, or via
sequence indexes::
>>> my_color.red == my_color[0] == 50
True
>>> my_color.green == my_color[1] == 100
True
>>> my_color.blue == my_color[2] == 200
True
>>> my_color.alpha == my_color[3] == 255
True
Color instances can be compared using ``==`` and ``!=``, either to another
Color instance, or to any mutable sequence type with the correct number of
items::
>>> my_color == [50, 100, 200, 255]
True
>>> my_color != Color(5, 10, 0, 0)
True
"""
@property
def red(self):
return self[0]
@property
def green(self):
return self[1]
@property
def blue(self):
return self[2]
@property
def alpha(self):
return self[3]
@compatible_repr
def __repr__(self):
return 'Color(%d, %d, %d, %d)' % (
self.red,
self.green,
self.blue,
self.alpha
)
class DateTime(_array_packed_type(1)):
"""The DateTime class represents a date and time in the UTC timezone.
DateTime is constructed by passing a unix timestamp in to the constructor.
The incoming timestamp is assumed to be in UTC.
.. note:: This class expects the passed in timestamp to be in UTC but will
display the resulting date and time in local time (using the local
timezone).
This is done to mimic the behaviour of most applications which will
display date and time in local time by default
Timestamps are expressed as the number of seconds since 1970-01-01T00:00:00
in the UTC timezone::
>>> my_dt = DateTime(1377209927)
This timestamp can always be accessed either using index access or via a
named property::
>>> my_dt[0] == my_dt.timestamp == 1377209927
True
DateTime objects also expose the usual named properties you would expect on
a date/time object::
>>> my_dt.year
2013
>>> my_dt.month
8
>>> my_dt.day
22
>>> my_dt.hour
22
>>> my_dt.minute
18
>>> my_dt.second
47
Two DateTime objects can be compared for equality::
>>> my_dt == DateTime(1377209927)
True
You can also compare a DateTime with any mutable sequence type containing
the timestamp (although this probably isn't very useful for test authors)::
>>> my_dt == [1377209927]
True
Finally, you can also compare a DateTime instance with a python datetime
instance::
>>> my_datetime = datetime.datetime.utcfromtimestamp(1377209927)
True
.. note:: Autopilot supports dates beyond 2038 on 32-bit platforms. To
achieve this the underlying mechanisms require to work with timezone aware
datetime objects.
This means that the following won't always be true (due to the naive
timestamp not having the correct daylight-savings time details)::
>>> # This time stamp is within DST in the 'Europe/London' timezone
>>> dst_ts = 1405382400
>>> os.environ['TZ'] ='Europe/London'
>>> time.tzset()
>>> datetime.fromtimestamp(dst_ts).hour == DateTime(dst_ts).hour
False
But this will work::
>>> from dateutil.tz import gettz
>>> datetime.fromtimestamp(
dst_ts, gettz()).hour == DateTime(dst_ts).hour
True
And this will always work to::
>>> dt1 = DateTime(nz_dst_timestamp)
>>> dt2 = datetime(
dt1.year, dt1.month, dt1.day, dt1.hour, dt1.minute, dt1.second
)
>>> dt1 == dt2
True
.. note:: DateTime.timestamp() will not always equal the passed in
timestamp.
To paraphrase a message from [http://bugs.python.org/msg229393]
"datetime.timestamp is supposed to be inverse of
datetime.fromtimestamp(), but since the later is not monotonic, no such
inverse exists in the strict mathematical sense."
DateTime instances can be converted to datetime instances::
>>> isinstance(my_dt.datetime, datetime.datetime)
True
"""
def __init__(self, *args, **kwargs):
super(DateTime, self).__init__(*args, **kwargs)
# Using timedelta in this manner is a workaround so that we can support
# timestamps larger than the 32bit time_t limit on 32bit hardware.
# We then apply the timezone information to this to get the correct
# datetime.
#
# Note. self[0] is a UTC timestamp
utc = pytz.timezone('UTC')
EPOCH = datetime(1970, 1, 1, tzinfo=utc)
utc_dt = EPOCH + timedelta(seconds=self[0])
self._cached_dt = utc_dt.astimezone(gettz())
@property
def year(self):
return self._cached_dt.year
@property
def month(self):
return self._cached_dt.month
@property
def day(self):
return self._cached_dt.day
@property
def hour(self):
return self._cached_dt.hour
@property
def minute(self):
return self._cached_dt.minute
@property
def second(self):
return self._cached_dt.second
@property
def timestamp(self):
return self._cached_dt.timestamp()
@property
def datetime(self):
return self._cached_dt
def __eq__(self, other):
# A little 'magic' here, if the datetime object to test against is
# naive, use the tzinfo from the cached datetime (just for the
# comparison)
if isinstance(other, datetime):
if other.tzinfo is None:
return other.replace(
tzinfo=self._cached_dt.tzinfo
) == self._cached_dt
return other == self._cached_dt
return super(DateTime, self).__eq__(other)
@compatible_repr
def __repr__(self):
return 'DateTime(%d-%02d-%02d %02d:%02d:%02d)' % (
self.year,
self.month,
self.day,
self.hour,
self.minute,
self.second
)
class Time(_array_packed_type(4)):
"""The Time class represents a time, without a date component.
You can construct a Time instnace by passing the hours, minutes, seconds,
and milliseconds to the class constructor::
>>> my_time = Time(12, 34, 01, 23)
The values passed in must be valid for their positions (ie..- 0-23 for
hours, 0-59 for minutes and seconds, and 0-999 for milliseconds). Passing
invalid values will cause a ValueError to be raised.
The hours, minutes, seconds, and milliseconds can be accessed using either
index access or named properties::
>>> my_time.hours == my_time[0] == 12
True
>>> my_time.minutes == my_time[1] == 34
True
>>> my_time.seconds == my_time[2] == 01
True
>>> my_time.milliseconds == my_time[3] == 23
True
Time instances can be compared to other time instances, any mutable
sequence containing four integers, or datetime.time instances::
>>> my_time == Time(12, 34, 01, 23)
True
>>> my_time == Time(1,2,3,4)
False
>>> my_time == [12, 34, 01, 23]
True
>>> my_time == datetime.time(12, 34, 01, 23000)
True
Note that the Time class stores milliseconds, while the ``datettime.time``
class stores microseconds.
Finally, you can get a ``datetime.time`` instance from a Time instance::
>>> isinstance(my_time.time, datetime.time)
True
"""
def __init__(self, *args, **kwargs):
super(Time, self).__init__(*args, **kwargs)
# datetime.time uses microseconds, instead of mulliseconds:
self._cached_time = time(self[0], self[1], self[2], self[3] * 1000)
@property
def hour(self):
return self._cached_time.hour
@property
def minute(self):
return self._cached_time.minute
@property
def second(self):
return self._cached_time.second
@property
def millisecond(self):
return self._cached_time.microsecond / 1000
@property
def time(self):
return self._cached_time
def __eq__(self, other):
if isinstance(other, time):
return other == self._cached_time
return super(Time, self).__eq__(other)
@compatible_repr
def __repr__(self):
return 'Time(%02d:%02d:%02d.%03d)' % (
self.hour,
self.minute,
self.second,
self.millisecond
)
class Point3D(_array_packed_type(3)):
"""The Point3D class represents a 3D point in cartesian space.
To construct a Point3D, pass in the x, y and z parameters to the class
constructor::
>>> my_point = Point(50,100,20)
These attributes can be accessed either using named attributes, or via
sequence indexes::
>>> my_point.x == my_point[0] == 50
True
>>> my_point.y == my_point[1] == 100
True
>>> my_point.z == my_point[2] == 20
True
Point3D instances can be compared using ``==`` and ``!=``, either to
another Point3D instance, or to any mutable sequence type with the correct
number of items::
>>> my_point == [50, 100, 20]
True
>>> my_point != Point(5, 10, 2)
True
"""
@property
def x(self):
return self[0]
@property
def y(self):
return self[1]
@property
def z(self):
return self[2]
@compatible_repr
def __repr__(self):
return 'Point3D(%d, %d, %d)' % (
self.x,
self.y,
self.z,
)
./autopilot/introspection/dbus.py 0000644 0000041 0000041 00000101074 13061552435 017501 0 ustar www-data www-data # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Autopilot Functional Test Tool
# Copyright (C) 2012-2013 Canonical
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
"""This module contains the code to retrieve state via DBus calls.
Under normal circumstances, the only thing you should need to use from this
module is the DBusIntrospectableObject class.
"""
import logging
import sys
from contextlib import contextmanager
from autopilot.exceptions import StateNotFoundError
from autopilot.introspection import _xpathselect as xpathselect
from autopilot.introspection._object_registry import (
DBusIntrospectionObjectBase,
)
from autopilot.introspection.types import create_value_instance
from autopilot.introspection.utilities import (
translate_state_keys,
sort_by_keys,
)
from autopilot.utilities import sleep
_logger = logging.getLogger(__name__)
class DBusIntrospectionObject(DBusIntrospectionObjectBase):
"""A class that supports transparent data retrieval from the application
under test.
This class is the base class for all objects retrieved from the application
under test. It handles transparently refreshing attribute values when
needed, and contains many methods to select child objects in the
introspection tree.
This class must be used as a base class for any custom proxy classes.
.. seealso::
Tutorial Section :ref:`custom_proxy_classes`
Information on how to write custom proxy classes.
"""
def __init__(self, state_dict, path, backend):
"""Construct a new proxy instance.
:param state_dict: A dictionary of state data for the proxy object.
:param path: A bytestring describing the path to the object within the
introspection tree.
:param backend: The data source backend this proxy should use then
retrieving additional state data.
The state dictionary must contain an 'id' element, as this is used to
uniquely identify this object.
"""
self.__state = {}
self.__refresh_on_attribute = True
self._set_properties(state_dict)
self._path = path
self._backend = backend
self._query = xpathselect.Query.new_from_path_and_id(
self._path,
self.id
)
def _execute_query(self, query):
"""Execute query object 'query' and return the result."""
return self._backend.execute_query_get_proxy_instances(
query,
getattr(self, '_id', None),
)
def _set_properties(self, state_dict):
"""Creates and set attributes of *self* based on contents of
*state_dict*.
.. note:: Translates '-' to '_', so a key of 'icon-type' for example
becomes 'icon_type'.
"""
# don't store id in state dictionary - make it a proper instance
# attribute. If id is not present, raise a ValueError.
try:
self.id = int(state_dict['id'][1])
except KeyError:
raise ValueError(
"State dictionary does not contain required 'id' key."
)
self.__state = {}
for key, value in translate_state_keys(state_dict).items():
if key == 'id':
continue
try:
self.__state[key] = create_value_instance(value, self, key)
except ValueError as e:
_logger.warning(
"While constructing attribute '%s.%s': %s",
self.__class__.__name__,
key,
str(e)
)
def get_children_by_type(self, desired_type, **kwargs):
"""Get a list of children of the specified type.
Keyword arguments can be used to restrict returned instances. For
example::
get_children_by_type('Launcher', monitor=1)
will return only Launcher instances that have an attribute 'monitor'
that is equal to 1. The type can also be specified as a string, which
is useful if there is no emulator class specified::
get_children_by_type('Launcher', monitor=1)
Note however that if you pass a string, and there is an emulator class
defined, autopilot will not use it.
:param desired_type: Either a string naming the type you want, or a
class of the type you want (the latter is used when defining
custom emulators)
.. seealso::
Tutorial Section :ref:`custom_proxy_classes`
"""
new_query = self._query.select_child(
get_type_name(desired_type),
kwargs
)
return self._execute_query(new_query)
def get_properties(self):
"""Returns a dictionary of all the properties on this class.
This can be useful when you want to log all the properties exported
from your application for a particular object. Every property in the
returned dictionary can be accessed as attributes of the object as
well.
"""
# Since we're grabbing __state directly there's no implied state
# refresh, so do it manually:
self.refresh_state()
props = self.__state.copy()
props['id'] = self.id
return props
def get_children(self):
"""Returns a list of all child objects.
This returns a list of all children. To return only children of a
specific type, use :meth:`get_children_by_type`. To get objects
further down the introspection tree (i.e.- nodes that may not
necessarily be immeadiate children), use :meth:`select_single` and
:meth:`select_many`.
"""
# Thomi: 2014-03-20: There used to be a call to 'self.refresh_state()'
# here. That's not needed, since the only thing we use is the proxy
# path, which isn't affected by the current state.
new_query = self._query.select_child(xpathselect.Query.WILDCARD)
return self._execute_query(new_query)
def _get_parent(self, base_object=None, level=1):
"""Returns the parent of this object.
Note: *level* is in ascending order, i.e. its value as 1 will return
the immediate parent of this object or (optionally) *base_object*,
if provided.
"""
obj = base_object or self
new_query = obj._query
for i in range(level):
new_query = new_query.select_parent()
return obj._execute_query(new_query)[0]
def _get_parent_nodes(self):
parent_nodes = self.get_path().split('/')
parent_nodes.pop()
# return a list without any NoneType elements. Only needed for the
# case when we try to get parent of a root object
return [node for node in parent_nodes if node]
def get_parent(self, type_name='', **kwargs):
"""Returns the parent of this object.
One may also use this method to get a specific parent node from the
introspection tree, with type equal to *type_name* or matching the
keyword filters present in *kwargs*.
Note: The priority order is closest parent.
If no filters are provided and this object has no parent (i.e.- it is
the root of the introspection tree). Then it returns itself.
:param type_name: Either a string naming the type you want, or a class
of the appropriate type (the latter case is for overridden emulator
classes).
:raises StateNotFoundError: if the requested object was not found.
"""
if not type_name and not kwargs:
return self._get_parent()
parent_nodes = self._get_parent_nodes()
type_name_str = get_type_name(type_name)
if type_name:
# Raise if type_name is not a parent.
if type_name_str not in parent_nodes:
raise StateNotFoundError(type_name_str, **kwargs)
for index, node in reversed(list(enumerate(parent_nodes))):
if node == type_name_str:
parent_level = len(parent_nodes) - index
parent = self._get_parent(level=parent_level)
if _validate_object_properties(parent, **kwargs):
return parent
else:
# Keep a reference of the parent object to improve performance.
parent = self
for i in range(len(parent_nodes)):
parent = self._get_parent(base_object=parent)
if _validate_object_properties(parent, **kwargs):
return parent
raise StateNotFoundError(type_name_str, **kwargs)
def _select(self, type_name_str, **kwargs):
"""Base method to execute search query on the DBus."""
new_query = self._query.select_descendant(type_name_str, kwargs)
_logger.debug(
"Selecting object(s) of %s with attributes: %r",
'any type' if type_name_str == '*' else 'type ' + type_name_str,
kwargs
)
return self._execute_query(new_query)
def _select_single(self, type_name, **kwargs):
"""
Ensures a single search result is produced from the query
and returns it.
"""
type_name_str = get_type_name(type_name)
instances = self._select(type_name_str, **kwargs)
if not instances:
raise StateNotFoundError(type_name_str, **kwargs)
if len(instances) > 1:
raise ValueError("More than one item was returned for query")
return instances[0]
def select_single(self, type_name='*', **kwargs):
"""Get a single node from the introspection tree, with type equal to
*type_name* and (optionally) matching the keyword filters present in
*kwargs*.
You must specify either *type_name*, keyword filters or both.
This method searches recursively from the instance this method is
called on. Calling :meth:`select_single` on the application (root)
proxy object will search the entire tree. Calling
:meth:`select_single` on an object in the tree will only search it's
descendants.
Example usage::
app.select_single('QPushButton', objectName='clickme')
# returns a QPushButton whose 'objectName' property is 'clickme'.
If nothing is returned from the query, this method raises
StateNotFoundError.
:param type_name: Either a string naming the type you want, or a class
of the appropriate type (the latter case is for overridden emulator
classes).
:raises ValueError: if the query returns more than one item. *If
you want more than one item, use select_many instead*.
:raises ValueError: if neither *type_name* or keyword filters are
provided.
:raises StateNotFoundError: if the requested object was not found.
.. seealso::
Tutorial Section :ref:`custom_proxy_classes`
"""
return self._select_single(type_name, **kwargs)
def wait_select_single(self, type_name='*', ap_query_timeout=10, **kwargs):
"""Get a proxy object matching some search criteria, retrying if no
object is found until a timeout is reached.
This method is identical to the :meth:`select_single` method, except
that this method will poll the application under test for 10 seconds
in the event that the search criteria does not match anything.
This method will return single proxy object from the introspection
tree, with type equal to *type_name* and (optionally) matching the
keyword filters present in *kwargs*.
You must specify either *type_name*, keyword filters or both.
This method searches recursively from the proxy object this method is
called on. Calling :meth:`select_single` on the application (root)
proxy object will search the entire tree. Calling
:meth:`select_single` on an object in the tree will only search it's
descendants.
Example usage::
app.wait_select_single('QPushButton', objectName='clickme')
# returns a QPushButton whose 'objectName' property is 'clickme'.
# will poll the application until such an object exists, or will
# raise StateNotFoundError after 10 seconds.
If nothing is returned from the query, this method raises
StateNotFoundError after *ap_query_timeout* seconds.
:param type_name: Either a string naming the type you want, or a class
of the appropriate type (the latter case is for overridden emulator
classes).
:param ap_query_timeout: Time in seconds to wait for search criteria
to match.
:raises ValueError: if the query returns more than one item. *If
you want more than one item, use select_many instead*.
:raises ValueError: if neither *type_name* or keyword filters are
provided.
:raises StateNotFoundError: if the requested object was not found.
.. seealso::
Tutorial Section :ref:`custom_proxy_classes`
"""
if ap_query_timeout <= 0:
return self._select_single(type_name, **kwargs)
for i in range(ap_query_timeout):
try:
return self._select_single(type_name, **kwargs)
except StateNotFoundError:
sleep(1)
raise StateNotFoundError(type_name, **kwargs)
def _select_many(self, type_name, **kwargs):
"""Executes a query, with no restraints on the number of results."""
type_name_str = get_type_name(type_name)
return self._select(type_name_str, **kwargs)
def select_many(self, type_name='*', ap_result_sort_keys=None, **kwargs):
"""Get a list of nodes from the introspection tree, with type equal to
*type_name* and (optionally) matching the keyword filters present in
*kwargs*.
You must specify either *type_name*, keyword filters or both.
This method searches recursively from the instance this method is
called on. Calling :meth:`select_many` on the application (root) proxy
object will search the entire tree. Calling :meth:`select_many` on an
object in the tree will only search it's descendants.
Example Usage::
app.select_many('QPushButton', enabled=True)
# returns a list of QPushButtons that are enabled.
As mentioned above, this method searches the object tree recursively::
file_menu = app.select_one('QMenu', title='File')
file_menu.select_many('QAction')
# returns a list of QAction objects who appear below file_menu in
# the object tree.
.. warning::
The order in which objects are returned is not guaranteed. It is
bad practise to write tests that depend on the order in which
this method returns objects. (see :ref:`object_ordering` for more
information).
If you want to ensure a certain count of results retrieved from this
method, use :meth:`wait_select_many` or if you only want to get one
item, use :meth:`select_single` instead.
:param type_name: Either a string naming the type you want, or a class
of the appropriate type (the latter case is for overridden emulator
classes).
:param ap_result_sort_keys: list of object properties to sort the
query result with (sort key priority starts with element 0 as
highest priority and then descends down the list).
:raises ValueError: if neither *type_name* or keyword filters are
provided.
.. seealso::
Tutorial Section :ref:`custom_proxy_classes`
"""
instances = self._select_many(type_name, **kwargs)
return sort_by_keys(instances, ap_result_sort_keys)
def wait_select_many(
self,
type_name='*',
ap_query_timeout=10,
ap_result_count=1,
ap_result_sort_keys=None,
**kwargs
):
"""Get a list of nodes from the introspection tree, with type equal to
*type_name* and (optionally) matching the keyword filters present in
*kwargs*.
This method is identical to the :meth:`select_many` method, except
that this method will poll the application under test for
*ap_query_timeout* seconds in the event that the search result count
is not greater than or equal to *ap_result_count*.
You must specify either *type_name*, keyword filters or both.
This method searches recursively from the instance this method is
called on. Calling :meth:`wait_select_many` on the application (root)
proxy object will search the entire tree. Calling
:meth:`wait_select_many` on an object in the tree will only search
it's descendants.
Example Usage::
app.wait_select_many(
'QPushButton',
ap_query_timeout=5,
ap_result_count=2,
enabled=True
)
# returns at least 2 QPushButtons that are enabled, within
# 5 seconds.
.. warning::
The order in which objects are returned is not guaranteed. It is
bad practise to write tests that depend on the order in which
this method returns objects. (see :ref:`object_ordering` for more
information).
:param type_name: Either a string naming the type you want, or a class
of the appropriate type (the latter case is for overridden emulator
classes).
:param ap_query_timeout: Time in seconds to wait for search criteria
to match.
:param ap_result_count: Minimum number of results to return.
:param ap_result_sort_keys: list of object properties to sort the
query result with (sort key priority starts with element 0 as
highest priority and then descends down the list).
:raises ValueError: if neither *type_name* or keyword filters are
provided. Also raises, if search result count does not match the
number specified by *ap_result_count* within *ap_query_timeout*
seconds.
.. seealso::
Tutorial Section :ref:`custom_proxy_classes`
"""
exception_message = 'Failed to find the requested number of elements.'
if ap_query_timeout <= 0:
instances = self._select_many(type_name, **kwargs)
if len(instances) < ap_result_count:
raise ValueError(exception_message)
return sort_by_keys(instances, ap_result_sort_keys)
for i in range(ap_query_timeout):
instances = self._select_many(type_name, **kwargs)
if len(instances) >= ap_result_count:
return sort_by_keys(instances, ap_result_sort_keys)
sleep(1)
raise ValueError(exception_message)
def refresh_state(self):
"""Refreshes the object's state.
You should probably never have to call this directly. Autopilot
automatically retrieves new state every time this object's attributes
are read.
:raises StateNotFound: if the object in the application under test
has been destroyed.
"""
_, new_state = self._get_new_state()
self._set_properties(new_state)
def get_all_instances(self):
"""Get all instances of this class that exist within the Application
state tree.
For example, to get all the LauncherIcon instances::
icons = LauncherIcon.get_all_instances()
.. warning::
Using this method is slow - it requires a complete scan of the
introspection tree. You should only use this when you're not sure
where the objects you are looking for are located. Depending on
the application you are testing, you may get duplicate results
using this method.
:return: List (possibly empty) of class instances.
"""
cls_name = type(self).__name__
return self._execute_query(
xpathselect.Query.whole_tree_search(cls_name)
)
def get_root_instance(self):
"""Get the object at the root of this tree.
This will return an object that represents the root of the
introspection tree.
"""
query = xpathselect.Query.pseudo_tree_root()
return self._execute_query(query)[0]
def __getattr__(self, name):
# avoid recursion if for some reason we have no state set (should never
# happen).
if name == '__state':
raise AttributeError()
if name in self.__state:
if self.__refresh_on_attribute:
self.refresh_state()
return self.__state[name]
# attribute not found.
raise AttributeError(
"Class '%s' has no attribute '%s'." %
(self.__class__.__name__, name))
def _get_new_state(self):
"""Retrieve a new state dictionary for this class instance.
You should probably never need to call this directly.
.. note:: The state keys in the returned dictionary are not translated.
"""
try:
return self._backend.execute_query_get_data(self._query)[0]
except IndexError:
raise StateNotFoundError(self.__class__.__name__, id=self.id)
def wait_until_destroyed(self, timeout=10):
"""Block until this object is destroyed in the application.
Block until the object this instance is a proxy for has been destroyed
in the applicaiton under test. This is commonly used to wait until a
UI component has been destroyed.
:param timeout: The number of seconds to wait for the object to be
destroyed. If not specified, defaults to 10 seconds.
:raises RuntimeError: if the method timed out.
"""
for i in range(timeout):
try:
self._get_new_state()
sleep(1)
except StateNotFoundError:
return
else:
raise RuntimeError(
"Object was not destroyed after %d seconds" % timeout
)
def is_moving(self, gap_interval=0.1):
"""Check if the element is moving.
:param gap_interval: Time in seconds to wait before
re-inquiring the object co-ordinates to be able
to evaluate if, the element is moving.
:return: True, if the element is moving, otherwise False.
"""
return _MockableDbusObject(self).is_moving(gap_interval)
def wait_until_not_moving(
self,
retry_attempts_count=20,
retry_interval=0.5,
):
"""Block until this object is not moving.
Block until both x and y of the object stop changing. This is
normally useful for cases, where there is a need to ensure an
object is static before interacting with it.
:param retry_attempts_count: number of attempts to check
if the object is moving.
:param retry_interval: time in fractional seconds to be
slept, between each attempt to check if the object
moving.
:raises RuntimeError: if DBus node is still moving after
number of retries specified in *retry_attempts_count*.
"""
# In case *retry_attempts_count* is something smaller than
# 1, sanitize it.
if retry_attempts_count < 1:
retry_attempts_count = 1
for i in range(retry_attempts_count):
if not self.is_moving(retry_interval):
return
raise RuntimeError(
'Object was still moving after {} second(s)'.format(
retry_attempts_count * retry_interval
)
)
def print_tree(self, output=None, maxdepth=None, _curdepth=0):
"""Print properties of the object and its children to a stream.
When writing new tests, this can be called when it is too difficult to
find the widget or property that you are interested in in "vis".
.. warning:: Do not use this in production tests, this is expensive and
not at all appropriate for actual testing. Only call this
temporarily and replace with proper select_single/select_many
calls.
:param output: A file object or path name where the output will be
written to. If not given, write to stdout.
:param maxdepth: If given, limit the maximum recursion level to that
number, i. e. only print children which have at most maxdepth-1
intermediate parents.
"""
if maxdepth is not None and _curdepth > maxdepth:
return
indent = " " * _curdepth
if output is None:
output = sys.stdout
elif isinstance(output, str):
output = open(output, 'w')
# print path
if _curdepth > 0:
output.write("\n")
output.write("%s== %s ==\n" % (indent, self._path.decode('utf-8')))
# Thomi 2014-03-20: For all levels other than the top level, we can
# avoid an entire dbus round trip if we grab the underlying property
# dictionary directly. We can do this since the print_tree function
# that called us will have retrieved us via a call to get_children(),
# which gets the latest state anyway.
if _curdepth > 0:
properties = self.__state.copy()
else:
properties = self.get_properties()
# print properties
try:
for key in sorted(properties.keys()):
output.write("%s%s: %r\n" % (indent, key, properties[key]))
# print children
if maxdepth is None or _curdepth < maxdepth:
for c in self.get_children():
c.print_tree(output, maxdepth, _curdepth + 1)
except StateNotFoundError as error:
output.write("%sError: %s\n" % (indent, error))
def get_path(self):
"""Return the absolute path of the dbus node"""
if isinstance(self._path, str):
return self._path
return self._path.decode('utf-8')
@contextmanager
def no_automatic_refreshing(self):
"""Context manager function to disable automatic DBus refreshing when
retrieving attributes.
Example usage:
with instance.no_automatic_refreshing():
# access lots of attributes.
This can be useful if you need to check lots of attributes in a tight
loop, or if you want to atomicaly check several attributes at once.
"""
try:
self.__refresh_on_attribute = False
yield
finally:
self.__refresh_on_attribute = True
@classmethod
def validate_dbus_object(cls, path, _state):
"""Return whether this class is the appropriate proxy object class for
a given dbus path and state.
The default version matches the name of the dbus object and the class.
Subclasses of CustomProxyObject can override it to define a different
validation method.
:param path: The dbus path of the object to check
:param state: The dbus state dict of the object to check
(ignored in default implementation)
:returns: Whether this class is appropriate for the dbus object
"""
state_name = xpathselect.get_classname_from_path(path)
if isinstance(state_name, str):
state_name = state_name.encode('utf-8')
class_name = cls.__name__.encode('utf-8')
return state_name == class_name
@classmethod
def get_type_query_name(cls):
"""Return the Type node name to use within the search query.
This allows for a Custom Proxy Object to be named differently to the
underlying node type name.
For instance if you have a QML type defined in the file RedRect.qml::
import QtQuick 2.0
Rectangle {
color: red;
}
You can then define a Custom Proxy Object for this type like so::
class RedRect(DBusIntrospectionObject):
@classmethod
def get_type_query_name(cls):
return 'QQuickRectangle'
This is due to the qml engine storing 'RedRect' as a QQuickRectangle in
the UI tree and the xpathquery query needs a node type to query for.
By default the query will use the class name (in this case RedRect) but
this will not match any node type in the tree.
"""
return cls.__name__
# TODO - can we add a deprecation warning around this somehow?
CustomEmulatorBase = DBusIntrospectionObject
def get_type_name(maybe_string_or_class):
"""Get a type name from something that might be a class or a string.
This is a temporary funtion that will be removed once custom proxy classes
can specify the query to be used to select themselves.
"""
if not isinstance(maybe_string_or_class, str):
return _get_class_type_name(maybe_string_or_class)
return maybe_string_or_class
def _get_class_type_name(maybe_cpo_class):
if hasattr(maybe_cpo_class, 'get_type_query_name'):
return maybe_cpo_class.get_type_query_name()
else:
return maybe_cpo_class.__name__
def _validate_object_properties(item, **kwargs):
"""Returns bool representing if the properties specified in *kwargs*
match the provided object *item*."""
props = item.get_properties()
for key in kwargs.keys():
if key not in props or props[key] != kwargs[key]:
return False
return True
def raises(exception_class, func, *args, **kwargs):
"""Evaluate if the callable *func* raises the expected
exception.
:param exception_class: Expected exception to be raised.
:param func: The callable that is to be evaluated.
:param args: Optional *args* to call the *func* with.
:param kwargs: Optional *kwargs* to call the *func* with.
:returns: bool, if the exception was raised.
"""
try:
func(*args, **kwargs)
except exception_class:
return True
else:
return False
def is_element(ap_query_func, *args, **kwargs):
"""Call the *ap_query_func* with the args and indicate if it
raises StateNotFoundError.
:param: ap_query_func: The dbus query call to be evaluated.
:param: *args: The *ap_query_func* positional parameters.
:param: **kwargs: The *ap_query_func* optional parameters.
:returns: False if the *ap_query_func* raises StateNotFoundError,
True otherwise.
"""
return not raises(StateNotFoundError, ap_query_func, *args, **kwargs)
class _MockableDbusObject:
"""Mockable DBus object."""
def __init__(self, dbus_object):
self._dbus_object = dbus_object
self._mocked = False
self._dbus_object_secondary = None
sleep.disable_mock()
@contextmanager
def mocked(self, dbus_object_secondary):
try:
self.enable_mock(dbus_object_secondary)
yield self
finally:
self.disable_mock()
def enable_mock(self, dbus_object_secondary):
self._dbus_object_secondary = dbus_object_secondary
sleep.enable_mock()
self._mocked = True
def disable_mock(self):
self._dbus_object_secondary = None
sleep.disable_mock()
self._mocked = False
def _get_default_dbus_object(self):
return self._dbus_object
def _get_secondary_dbus_object(self):
if not self._mocked:
return self._get_default_dbus_object()
else:
return self._dbus_object_secondary
def is_moving(self, gap_interval=0.1):
"""Check if the element is moving.
:param gap_interval: Time in seconds to wait before
re-inquiring the object co-ordinates to be able
to evaluate if, the element has moved.
:return: True, if the element is moving, otherwise False.
"""
x1, y1, h1, w1 = self._get_default_dbus_object().globalRect
sleep(gap_interval)
x2, y2, h2, w2 = self._get_secondary_dbus_object().globalRect
return x1 != x2 or y1 != y2
./autopilot/introspection/_xpathselect.py 0000644 0000041 0000041 00000034323 13061552435 021231 0 ustar www-data www-data # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Autopilot Functional Test Tool
# Copyright (C) 2014 Canonical
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
"""Classes and functions that encode knowledge of the xpathselect query
language.
This module is internal, and should not be used directly.
The main class is 'Query', which represents an xpathselect query. Query is a
read-only object - once it has been constructed, it cannot be changed. This is
partially to ease testing, but also to provide guarantees in the proxy object
classes.
To create a query, you must either have a reference to an existing query, or
you must know the name of the root note. To create a query from an existing
query::
>>> new_query = existing_query.select_child("NodeName")
To create a query given the root node name::
>>> new_root_query = Query.root("AppName")
Since the XPathSelect language is not perfect, and since we'd like to support
a rich set of selection criteria, not all queries can be executed totally on
the server side. Query instnaces are intelligent enough to know when they must
invoke some client-side processing - in this case the
'needs_client_side_filtering' method will return True.
Queries are executed in the autopilot.introspection.backends module.
"""
from pathlib import Path
import re
from autopilot.utilities import compatible_repr
from autopilot.exceptions import InvalidXPathQuery
class Query(object):
"""Encapsulate an XPathSelect query."""
class Operation(object):
ROOT = b'/'
CHILD = b'/'
DESCENDANT = b'//'
ALL = (ROOT, CHILD, DESCENDANT)
PARENT = b'..'
WILDCARD = b'*'
def __init__(self, parent, operation, query, filters={}):
"""Create a new query object.
You shouldn't need to call this directly.
:param parent: The parent query object. Pass in None to make the root
query object.
:param operation: The operation object to perform on the result from
the parent node.
:param query: The query expression for this node.
:param filters: A dictionary of filters to apply.
:raises TypeError: If the 'query' parameter is not 'bytes'.
:raises TypeError: If the operation parameter is not 'bytes'.
:raises InvalidXPathQuery: If parent is specified, and the parent
query needs client side filter processing. Only the last query in
the query chain can have filters that need to be executed on the
client-side.
:raises InvalidXPathQuery: If operation is not one of the members of
the Query.Operation class.
:raises InvalidXPathQuery: If the query is set to Query.WILDCARD and
'filters' does not contain any server-side filters, and operation
is set to Query.Operation.DESCENDANT.
:raises InvalidXPathQuery: When 'filters' are specified while trying
to select a parent node in the introspection tree.
"""
if not isinstance(query, bytes):
raise TypeError(
"'query' parameter must be bytes, not %s"
% type(query).__name__
)
if not isinstance(operation, bytes):
raise TypeError(
"'operation' parameter must be bytes, not '%s'"
% type(operation).__name__)
if (
parent
and parent.needs_client_side_filtering()
):
raise InvalidXPathQuery(
"Cannot create a new query from a parent that requires "
"client-side filter processing."
)
if query == Query.PARENT and filters:
raise InvalidXPathQuery(
"Cannot specify filters while selecting a parent"
)
if query == Query.PARENT and operation != Query.Operation.CHILD:
raise InvalidXPathQuery(
"Operation must be CHILD while selecting a parent"
)
if operation not in Query.Operation.ALL:
raise InvalidXPathQuery(
"Invalid operation '%s'." % operation.decode()
)
if parent and parent.server_query_bytes() == b'/':
raise InvalidXPathQuery(
"Cannot select children from a pseudo-tree-root query."
)
self._parent = parent
self._operation = operation
self._query = query
self._server_filters = {
k: v for k, v in filters.items()
if _is_valid_server_side_filter_param(k, v)
}
self._client_filters = {
k: v for k, v in filters.items() if k not in self._server_filters
}
if (
operation == Query.Operation.DESCENDANT
and query == Query.WILDCARD
and not self._server_filters
):
raise InvalidXPathQuery(
"Must provide at least one server-side filter when searching "
"for descendants and using a wildcard node."
)
@staticmethod
def root(app_name):
"""Create a root query object.
:param app_name: The name of the root node in the introspection tree.
This is also typically the application name.
:returns: A new Query instance, representing the root of the tree.
"""
app_name = _try_encode_type_name(app_name)
return Query(
None,
Query.Operation.ROOT,
app_name
)
@staticmethod
def new_from_path_and_id(path, id):
"""Create a new Query object from a path and id.
:param path: The full path to the node you want to construct the query
for.
:param id: The object id of the node you want to construct the query
for.
:raises TypeError: If the path attribute is not 'bytes'.
:raises ValueError: If the path does not start with b'/'
"""
if not isinstance(path, bytes):
raise TypeError(
"'path' attribute must be bytes, not '%s'"
% type(path).__name__
)
nodes = list(filter(None, path.split(b'/')))
if not path.startswith(b'/') or not nodes:
raise InvalidXPathQuery("Invalid path '%s'." % path.decode())
query = None
for i, n in enumerate(nodes):
if query is None:
query = Query.root(n)
else:
if i == len(nodes) - 1:
query = query.select_child(n, dict(id=id))
else:
query = query.select_child(n)
return query
@staticmethod
def pseudo_tree_root():
"""Return a Query instance that will select the root of the tree.
Unlike the 'root' method, this method does not need to know the name of
the tree root. However, the query returned by this method cannot be
used as the parent for any other query. In other words, calling any
of the 'select_child', 'select_parent', 'select_descendant' method will
raise a InvalidXPathQuery error.
If at all possible, it's better to use the 'root' method instead of
this one. The queries returned by this method are useful for getting
the root proxy object, and then discarding the query.
"""
return Query(None, Query.Operation.CHILD, b'')
@staticmethod
def whole_tree_search(child_name, filters={}):
"""Return a query capable of searching the entire introspection tree.
.. warning::
This method returns a query that can be extremely slow on larger
applications. The execution time can easily extend beyond the dbus
timeout period, which can result in tests that fail on some
machines but not others. Test authors are strongly encouraged to
use the 'Query.root' method and absolute queries instead.
"""
child_name = _try_encode_type_name(child_name)
return Query(None, Query.Operation.DESCENDANT, child_name, filters)
def needs_client_side_filtering(self):
"""Return true if this query requires some filtering on the client-side
"""
return self._client_filters or (
self._parent.needs_client_side_filtering()
if self._parent else False
)
def get_client_side_filters(self):
"""Return a dictionary of filters that must be processed on the client
rather than the server.
"""
return self._client_filters
def server_query_bytes(self):
"""Get a bytestring representing the entire query.
This method returns a bytestring suitable for sending to the server.
"""
parent_query = self._parent.server_query_bytes() \
if self._parent is not None else b''
return parent_query + \
self._operation + \
self._query + \
self._get_server_filter_bytes()
def _get_server_filter_bytes(self):
if self._server_filters:
keys = sorted(self._server_filters.keys())
return b'[' + \
b",".join(
[
_get_filter_string_for_key_value_pair(
k,
self._server_filters[k]
) for k in keys
if _is_valid_server_side_filter_param(
k,
self._server_filters[k]
)
]
) + \
b']'
return b''
@compatible_repr
def __repr__(self):
return "Query(%r)" % self.server_query_bytes()
def select_child(self, child_name, filters={}):
"""Return a query matching an immediate child.
Keyword arguments may be used to restrict which nodes to match.
:param child_name: The name of the child node to match.
:returns: A Query instance that will match the child.
"""
child_name = _try_encode_type_name(child_name)
return Query(
self,
Query.Operation.CHILD,
child_name,
filters
)
def select_descendant(self, ancestor_name, filters={}):
"""Return a query matching an ancestor of the current node.
:param ancestor_name: The name of the ancestor node to match.
:returns: A Query instance that will match the ancestor.
"""
ancestor_name = _try_encode_type_name(ancestor_name)
return Query(
self,
Query.Operation.DESCENDANT,
ancestor_name,
filters
)
def select_parent(self):
"""Return a query matching the parent node of the current node.
Calling this on the root node will return a query that looks like it
ought to select the parent of the root (something like: b'/root/..').
This is however perfectly safe, as the server-side will just return
the root node in this case.
:returns: A Query instance that will match the parent node.
"""
return Query(self, Query.Operation.CHILD, Query.PARENT)
def _try_encode_type_name(name):
if isinstance(name, str):
try:
name = name.encode('ascii')
except UnicodeEncodeError:
raise InvalidXPathQuery(
"Type name '%s', must be ASCII encodable" % (name)
)
return name
def _is_valid_server_side_filter_param(key, value):
"""Return True if the key and value parameters are valid for server-side
processing.
"""
key_is_valid = re.match(
r'^[a-zA-Z0-9_\-]+( [a-zA-Z0-9_\-])*$',
key
) is not None
if type(value) == int:
return key_is_valid and (-2**31 <= value <= 2**31 - 1)
elif type(value) == bool:
return key_is_valid
elif type(value) == bytes:
return key_is_valid
elif type(value) == str:
try:
value.encode('ascii')
return key_is_valid
except UnicodeEncodeError:
pass
return False
def _get_filter_string_for_key_value_pair(key, value):
"""Return bytes representing the filter query for this key/value pair.
The value must be suitable for server-side filtering. Raises ValueError if
this is not the case.
"""
if isinstance(value, str):
escaped_value = value.encode("unicode_escape")\
.decode('ASCII')\
.replace("'", "\\'")
return '{}="{}"'.format(key, escaped_value).encode('utf-8')
elif isinstance(value, bytes):
escaped_value = value.decode('utf-8')\
.encode("unicode_escape")\
.decode('ASCII')\
.replace("'", "\\'")
return '{}="{}"'.format(key, escaped_value).encode('utf-8')
elif isinstance(value, int) or isinstance(value, bool):
return "{}={}".format(key, repr(value)).encode('utf-8')
else:
raise ValueError(
"Unsupported value type: {}".format(type(value).__name__)
)
def _get_node(object_path, index):
# TODO: Find places where paths are strings, and convert them to
# bytestrings. Figure out what to do with the whole string vs. bytestring
# mess.
try:
return Path(object_path).parts[index]
except TypeError:
if not isinstance(object_path, bytes):
raise TypeError(
'Object path needs to be a string literal or a bytes literal'
)
return object_path.split(b"/")[index]
def get_classname_from_path(object_path):
"""Given an object path, return the class name component."""
return _get_node(object_path, -1)
def get_path_root(object_path):
"""Return the name of the root node of specified path."""
return _get_node(object_path, 1)
./autopilot/content.py 0000644 0000041 0000041 00000006146 13061552435 015322 0 ustar www-data www-data # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Autopilot Functional Test Tool
# Copyright (C) 2013 Canonical
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
"""Content objects and helpers for autopilot tests."""
import io
import logging
from testtools.content import ContentType, content_from_stream
from autopilot.utilities import safe_text_content
_logger = logging.getLogger(__name__)
def follow_file(path, test_case, content_name=None):
"""Start monitoring a file.
Use this convenience function to attach the contents of a file to a test.
:param path: The path to the file on disk you want to monitor.
:param test_case: An object that supports attaching details and cleanup
actions (i.e.- has the ``addDetail`` and ``addCleanup`` methods).
:param content_name: A name to give this content. If not specified, the
file path will be used instead.
"""
try:
file_obj = io.open(path, mode='rb')
except IOError as e:
_logger.error(
"Could not add content object '%s' due to IO Error: %s",
content_name,
str(e)
)
return safe_text_content('')
else:
file_obj.seek(0, io.SEEK_END)
return follow_stream(
file_obj,
test_case,
content_name or file_obj.name
)
def follow_stream(stream, test_case, content_name):
"""Start monitoring the content from a stream.
This function can be used to attach a portion of a stream to a test.
:param stream: an open file-like object (that supports a read method that
returns bytes).
:param test_case: An object that supports attaching details and cleanup
actions (i.e.- has the ``addDetail`` and ``addCleanup`` methods).
:param content_name: A name to give this content. If not specified, the
file path will be used instead.
"""
def make_content():
content_obj = content_from_stream(
stream,
ContentType('text', 'plain', {'charset': 'iso8859-1'}),
buffer_now=True
)
# Work around a bug in older testtools where an empty file would result
# in None being decoded and exploding.
# See: https://bugs.launchpad.net/autopilot/+bug/1517289
if list(content_obj.iter_text()) == []:
_logger.warning('Followed stream is empty.')
content_obj = safe_text_content('Unable to read file data.')
test_case.addDetail(content_name, content_obj)
test_case.addCleanup(make_content)
./autopilot/_fixtures.py 0000644 0000041 0000041 00000006674 13061552435 015666 0 ustar www-data www-data # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Autopilot Functional Test Tool
# Copyright (C) 2014 Canonical
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
from fixtures import Fixture
import logging
from gi.repository import Gio
logger = logging.getLogger(__name__)
class FixtureWithDirectAddDetail(Fixture):
"""A test fixture that has a 'caseAddDetail' method that corresponds
to the addDetail method of the test case in use.
You must derive from this class in order to add detail objects to tests
from within cleanup actions.
"""
def __init__(self, caseAddDetail=None):
"""Create the fixture.
:param caseAddDetail: A closure over the testcase's addDetail
method, or a similar substitution method. This may be omitted, in
which case the 'caseAddDetail' method will be set to the fixtures
normal 'addDetail' method.
"""
super().__init__()
self.caseAddDetail = caseAddDetail or self.addDetail
class OSKAlwaysEnabled(Fixture):
"""Enable the OSK to be shown regardless of if there is a keyboard (virtual
or real) plugged in.
This is a workaround for bug lp:1474444
"""
osk_schema = 'com.canonical.keyboard.maliit'
osk_show_key = 'stay-hidden'
def setUp(self):
super().setUp()
try:
_original_value = get_bool_gsettings_value(
self.osk_schema,
self.osk_show_key
)
set_bool_gsettings_value(
self.osk_schema,
self.osk_show_key,
False
)
self.addCleanup(
set_bool_gsettings_value,
self.osk_schema,
self.osk_show_key,
_original_value
)
except ValueError as e:
logger.warning('Failed to set OSK gsetting: {}'.format(e))
def get_bool_gsettings_value(schema, key):
"""Return the boolean value for schema/key combo.
:raises ValueError: If either ``schema`` or ``key`` are not valid.
"""
setting = _gsetting_get_setting(schema, key)
return setting.get_boolean(key)
def set_bool_gsettings_value(schema, key, value):
"""Set the boolean value ``value`` for schema/key combo.
:raises ValueError: If either ``schema`` or ``key`` are not valid.
"""
setting = _gsetting_get_setting(schema, key)
setting.set_boolean(key, value)
def _gsetting_get_setting(schema, key):
if schema not in Gio.Settings.list_schemas():
raise ValueError('schema {} is not installed.'.format(schema))
setting = Gio.Settings.new(schema)
if key not in setting.keys():
raise ValueError(
'key \'{key}\' is not available for schema \'{schema}\''.format(
key=key,
schema=schema
)
)
return setting
./autopilot/input/ 0000755 0000041 0000041 00000000000 13061552443 014425 5 ustar www-data www-data ./autopilot/input/_common.py 0000644 0000041 0000041 00000005263 13061552435 016435 0 ustar www-data www-data # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Autopilot Functional Test Tool
# Copyright (C) 2012-2013 Canonical
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
"""Common, private utility code for input emulators."""
import logging
_logger = logging.getLogger(__name__)
def get_center_point(object_proxy):
"""Get the center point of an object.
It searches for several different ways of determining exactly where the
center is. The attributes used are (in order):
* globalRect (x,y,w,h)
* center_x, center_y
* x, y, w, h
:raises ValueError: if `object_proxy` has the globalRect attribute but it
is not of the correct type.
:raises ValueError: if `object_proxy` doesn't have the globalRect
attribute, it has the x and y attributes instead, but they are not of
the correct type.
:raises ValueError: if `object_proxy` doesn't have any recognised position
attributes.
"""
try:
x, y, w, h = object_proxy.globalRect
_logger.debug("Moving to object's globalRect coordinates.")
return x+w/2, y+h/2
except AttributeError:
pass
except (TypeError, ValueError):
raise ValueError(
"Object '%r' has globalRect attribute, but it is not of the "
"correct type" % object_proxy)
try:
x, y = object_proxy.center_x, object_proxy.center_y
_logger.debug("Moving to object's center_x, center_y coordinates.")
return x, y
except AttributeError:
pass
try:
x, y, w, h = (
object_proxy.x, object_proxy.y, object_proxy.w, object_proxy.h)
_logger.debug(
"Moving to object's center point calculated from x,y,w,h "
"attributes.")
return x+w/2, y+h/2
except AttributeError:
raise ValueError(
"Object '%r' does not have any recognised position attributes" %
object_proxy)
except (TypeError, ValueError):
raise ValueError(
"Object '%r' has x,y attribute, but they are not of the correct "
"type" % object_proxy)
./autopilot/input/_osk.py 0000644 0000041 0000041 00000007261 13061552435 015741 0 ustar www-data www-data # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Autopilot Functional Test Tool
# Copyright (C) 2013 Canonical
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
import logging
from contextlib import contextmanager
from ubuntu_keyboard.emulators.keyboard import Keyboard as KeyboardDriver
from autopilot.input import Keyboard as KeyboardBase
from autopilot.utilities import sleep
_logger = logging.getLogger(__name__)
class Keyboard(KeyboardBase):
_keyboard = KeyboardDriver()
@contextmanager
def focused_type(self, input_target, pointer=None):
"""Ensures that the keyboard is up and ready for input as well as
dismisses the keyboard afterward.
"""
with super(Keyboard, self).focused_type(input_target, pointer):
try:
self._keyboard.wait_for_keyboard_ready()
yield self
finally:
self._keyboard.dismiss()
def press(self, keys, delay=0.2):
raise NotImplementedError(
"OSK Backend does not support the press method"
)
def release(self, keys, delay=0.2):
raise NotImplementedError(
"OSK Backend does not support the release method"
)
def press_and_release(self, key, delay=0.2):
"""Press and release the key *key*.
The 'key' argument must be a string of the single key you want pressed
and released.
For example::
press_and_release('A')
presses then releases the 'A' key.
:raises: *ValueError* if the provided key is not supported by the
OSK Backend (or the current OSK langauge layout).
:raises: *ValueError* if there is more than a single key supplied in
the *key* argument.
"""
if len(self._sanitise_keys(key)) != 1:
raise ValueError("Only a single key can be passed in.")
try:
self._keyboard.press_key(key)
sleep(delay)
except ValueError as e:
e.args += ("OSK Backend is unable to type the key '%s" % key,)
raise
def type(self, string, delay=0.1):
"""Simulate a user typing a string of text.
Only 'normal' keys can be typed with this method. There is no such
thing as Alt or Ctrl on the Onscreen Keyboard.
The OSK class back end will take care of ensuring that capitalized
keys are in fact capitalized.
:raises: *ValueError* if there is a key within the string that is
not supported by the OSK Backend (or the current OSK langauge layout.)
"""
if not isinstance(string, str):
raise TypeError("'string' argument must be a string.")
_logger.debug("Typing text: %s", string)
self._keyboard.type(string, delay)
@classmethod
def on_test_end(cls, test_instance):
"""Dismiss (swipe hide) the keyboard.
"""
_logger.debug("Dismissing the OSK with a swipe.")
cls._keyboard.dismiss()
def _sanitise_keys(self, keys):
if keys == '+':
return [keys]
else:
return keys.split('+')
./autopilot/input/__init__.py 0000644 0000041 0000041 00000064005 13061552443 016543 0 ustar www-data www-data # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Autopilot Functional Test Tool
# Copyright (C) 2012, 2013, 2014, 2015 Canonical
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
"""
Autopilot unified input system.
===============================
This package provides input methods for various platforms. Autopilot aims to
provide an appropriate implementation for the currently running system. For
example, not all systems have an X11 stack running: on those systems, autopilot
will instantiate input classes class that use something other than X11 to
generate events (possibly UInput).
Test authors should instantiate the appropriate class using the ``create``
method on each class. Calling ``create()`` with no arguments will get an
instance of the specified class that suits the current platform. In this case,
autopilot will do it's best to pick a suitable backend. Calling ``create``
with a backend name will result in that specific backend type being returned,
or, if it cannot be created, an exception will be raised. For more information
on creating backends, see :ref:`tut-picking-backends`
There are three basic input types available:
* :class:`Keyboard` - traditional keyboard devices.
* :class:`Mouse` - traditional mouse devices (Currently only avaialble on the
desktop).
* :class:`Touch` - single point-of-contact touch device.
The :class:`Pointer` class is a wrapper that unifies the API of the
:class:`Mouse` and :class:`Touch` classes, which can be helpful if you want to
write a test that can use either a mouse of a touch device. A common pattern is
to use a Touch device when running on a mobile device, and a Mouse device when
running on a desktop.
.. seealso::
Module :mod:`autopilot.gestures`
Multitouch and gesture support for touch devices.
"""
from collections import OrderedDict
from contextlib import contextmanager
import psutil
from autopilot.input._common import get_center_point
from autopilot.utilities import _pick_backend, CleanupRegistered
import logging
_logger = logging.getLogger(__name__)
__all__ = ['get_center_point']
class Keyboard(CleanupRegistered):
"""A simple keyboard device class.
The keyboard class is used to generate key events while in an autopilot
test. This class should not be instantiated directly. To get an
instance of the keyboard class, call :py:meth:`create` instead.
"""
@staticmethod
def create(preferred_backend=''):
"""Get an instance of the :py:class:`Keyboard` class.
For more infomration on picking specific backends, see
:ref:`tut-picking-backends`
For details regarding backend limitations please see:
:ref:`Keyboard backend limitations`
.. warning:: The **OSK** (On Screen Keyboard) backend option does not
implement either :py:meth:`press` or :py:meth:`release` methods due to
technical implementation details and will raise a NotImplementedError
exception if used.
:param preferred_backend: A string containing a hint as to which
backend you would like. Possible backends are:
* ``X11`` - Generate keyboard events using the X11 client
libraries.
* ``UInput`` - Use UInput kernel-level device driver.
* ``OSK`` - Use the graphical On Screen Keyboard as a backend.
:raises: RuntimeError if autopilot cannot instantate any of the
possible backends.
:raises: RuntimeError if the preferred_backend is specified and is not
one of the possible backends for this device class.
:raises: :class:`~autopilot.BackendException` if the preferred_backend
is set, but that backend could not be instantiated.
"""
def get_x11_kb():
from autopilot.input._X11 import Keyboard
return Keyboard()
def get_uinput_kb():
from autopilot.input._uinput import Keyboard
return Keyboard()
def get_osk_kb():
try:
maliit = [p for p in
psutil.process_iter() if p.name() == 'maliit-server']
if maliit:
from autopilot.input._osk import Keyboard
return Keyboard()
else:
raise RuntimeError('maliit-server is not running')
except ImportError as e:
e.args += ("Unable to import the OSK backend",)
raise
backends = OrderedDict()
backends['X11'] = get_x11_kb
backends['OSK'] = get_osk_kb
backends['UInput'] = get_uinput_kb
return _pick_backend(backends, preferred_backend)
@contextmanager
def focused_type(self, input_target, pointer=None):
"""Type into an input widget.
This context manager takes care of making sure a particular
*input_target* UI control is selected before any text is entered.
Some backends extend this method to perform cleanup actions at the end
of the context manager block. For example, the OSK backend dismisses
the keyboard.
If the *pointer* argument is None (default) then either a Mouse or
Touch pointer will be created based on the current platform.
An example of using the context manager (with an OSK backend)::
from autopilot.input import Keyboard
text_area = self._launch_test_input_area()
keyboard = Keyboard.create('OSK')
with keyboard.focused_type(text_area) as kb:
kb.type("Hello World.")
self.assertThat(text_area.text, Equals("Hello World"))
# Upon leaving the context managers scope the keyboard is dismissed
# with a swipe
"""
if pointer is None:
from autopilot.platform import model
if model() == 'Desktop':
pointer = Pointer(Mouse.create())
else:
pointer = Pointer(Touch.create())
pointer.click_object(input_target)
yield self
def press(self, keys, delay=0.2):
"""Send key press events only.
:param keys: Keys you want pressed.
:param delay: The delay (in Seconds) after pressing the keys before
returning control to the caller.
:raises: NotImplementedError If called when using the OSK Backend.
.. warning:: The **OSK** backend does not implement the press method
and will raise a NotImplementedError if called.
Example::
press('Alt+F2')
presses the 'Alt' and 'F2' keys, but does not release them.
"""
raise NotImplementedError("You cannot use this class directly.")
def release(self, keys, delay=0.2):
"""Send key release events only.
:param keys: Keys you want released.
:param delay: The delay (in Seconds) after releasing the keys before
returning control to the caller.
:raises: NotImplementedError If called when using the OSK Backend.
.. warning:: The **OSK** backend does not implement the press method
and will raise a NotImplementedError if called.
Example::
release('Alt+F2')
releases the 'Alt' and 'F2' keys.
"""
raise NotImplementedError("You cannot use this class directly.")
def press_and_release(self, keys, delay=0.2):
"""Press and release all items in 'keys'.
This is the same as calling 'press(keys);release(keys)'.
:param keys: Keys you want pressed and released.
:param delay: The delay (in Seconds) after pressing and releasing each
key.
Example::
press_and_release('Alt+F2')
presses both the 'Alt' and 'F2' keys, and then releases both keys.
"""
raise NotImplementedError("You cannot use this class directly.")
def type(self, string, delay=0.1):
"""Simulate a user typing a string of text.
:param string: The string to text to type.
:param delay: The delay (in Seconds) after pressing and releasing each
key. Note that the default value here is shorter than for the
press, release and press_and_release methods.
.. note:: Only 'normal' keys can be typed with this method. Control
characters (such as 'Alt' will be interpreted as an 'A', and 'l',
and a 't').
"""
raise NotImplementedError("You cannot use this class directly.")
class Mouse(CleanupRegistered):
"""A simple mouse device class.
The mouse class is used to generate mouse events while in an autopilot
test. This class should not be instantiated directly however. To get an
instance of the mouse class, call :py:meth:`create` instead.
For example, to create a mouse object and click at (100,50)::
mouse = Mouse.create()
mouse.move(100, 50)
mouse.click()
"""
@staticmethod
def create(preferred_backend=''):
"""Get an instance of the :py:class:`Mouse` class.
For more infomration on picking specific backends, see
:ref:`tut-picking-backends`
:param preferred_backend: A string containing a hint as to which
backend you would like. Possible backends are:
* ``X11`` - Generate mouse events using the X11 client libraries.
:raises: RuntimeError if autopilot cannot instantate any of the
possible backends.
:raises: RuntimeError if the preferred_backend is specified and is not
one of the possible backends for this device class.
:raises: :class:`~autopilot.BackendException` if the preferred_backend
is set, but that backend could not be instantiated.
"""
def get_x11_mouse():
from autopilot.input._X11 import Mouse
return Mouse()
def get_uinput_mouse():
# Return the Touch device for now as Mouse under a Mir desktop
# is a challenge for now.
from autopilot.input._uinput import Touch
return Touch()
backends = OrderedDict()
backends['X11'] = get_x11_mouse
backends['UInput'] = get_uinput_mouse
return _pick_backend(backends, preferred_backend)
@property
def x(self):
"""Mouse position X coordinate."""
raise NotImplementedError("You cannot use this class directly.")
@property
def y(self):
"""Mouse position Y coordinate."""
raise NotImplementedError("You cannot use this class directly.")
def press(self, button=1):
"""Press mouse button at current mouse location."""
raise NotImplementedError("You cannot use this class directly.")
def release(self, button=1):
"""Releases mouse button at current mouse location."""
raise NotImplementedError("You cannot use this class directly.")
def click(self, button=1, press_duration=0.10, time_between_events=0.1):
"""Click mouse at current location.
:param time_between_events: takes floating point to represent the
delay time between subsequent clicks. Default value 0.1 represents
tenth of a second.
"""
raise NotImplementedError("You cannot use this class directly.")
def click_object(
self,
object_proxy,
button=1,
press_duration=0.10,
time_between_events=0.1):
"""Click the center point of a given object.
It does this by looking for several attributes, in order. The first
attribute found will be used. The attributes used are (in order):
* globalRect (x,y,w,h)
* center_x, center_y
* x, y, w, h
:param time_between_events: takes floating point to represent the
delay time between subsequent clicks. Default value 0.1 represents
tenth of a second.
:raises: **ValueError** if none of these attributes are found, or if an
attribute is of an incorrect type.
"""
self.move_to_object(object_proxy)
self.click(button, press_duration, time_between_events)
def move(self, x, y, animate=True, rate=10, time_between_events=0.01):
"""Moves mouse to location (x,y).
Callers should avoid specifying the *rate* or *time_between_events*
parameters unless they need a specific rate of movement.
"""
raise NotImplementedError("You cannot use this class directly.")
def move_to_object(self, object_proxy):
"""Attempts to move the mouse to 'object_proxy's centre point.
It does this by looking for several attributes, in order. The first
attribute found will be used. The attributes used are (in order):
* globalRect (x,y,w,h)
* center_x, center_y
* x, y, w, h
:raises: **ValueError** if none of these attributes are found, or if an
attribute is of an incorrect type.
"""
raise NotImplementedError("You cannot use this class directly.")
def position(self):
"""
Returns the current position of the mouse pointer.
:return: (x,y) tuple
"""
raise NotImplementedError("You cannot use this class directly.")
def drag(self, x1, y1, x2, y2, rate=10, time_between_events=0.01):
"""Perform a press, move and release.
This is to keep a common API between Mouse and Finger as long as
possible.
The pointer will be dragged from the starting point to the ending point
with multiple moves. The number of moves, and thus the time that it
will take to complete the drag can be altered with the `rate`
parameter.
:param x1: The point on the x axis where the drag will start from.
:param y1: The point on the y axis where the drag will starts from.
:param x2: The point on the x axis where the drag will end at.
:param y2: The point on the y axis where the drag will end at.
:param rate: The number of pixels the mouse will be moved per
iteration. Default is 10 pixels. A higher rate will make the drag
faster, and lower rate will make it slower.
:param time_between_events: The number of seconds that the drag will
wait between iterations.
"""
raise NotImplementedError("You cannot use this class directly.")
class Touch(object):
"""A simple touch driver class.
This class can be used for any touch events that require a single active
touch at once. If you want to do complex gestures (including multi-touch
gestures), look at the :py:mod:`autopilot.gestures` module.
"""
@staticmethod
def create(preferred_backend=''):
"""Get an instance of the :py:class:`Touch` class.
:param preferred_backend: A string containing a hint as to which
backend you would like. If left blank, autopilot will pick a
suitable backend for you. Specifying a backend will guarantee that
either that backend is returned, or an exception is raised.
possible backends are:
* ``UInput`` - Use UInput kernel-level device driver.
:raises: RuntimeError if autopilot cannot instantate any of the
possible backends.
:raises: RuntimeError if the preferred_backend is specified and is not
one of the possible backends for this device class.
:raises: :class:`~autopilot.BackendException` if the preferred_backend
is set, but that backend could not be instantiated.
"""
def get_uinput_touch():
from autopilot.input._uinput import Touch
return Touch()
backends = OrderedDict()
backends['UInput'] = get_uinput_touch
return _pick_backend(backends, preferred_backend)
@property
def pressed(self):
"""Return True if this touch is currently in use (i.e.- pressed on the
'screen').
"""
raise NotImplementedError("You cannot use this class directly.")
def tap(self, x, y, press_duration=0.1, time_between_events=0.1):
"""Click (or 'tap') at given x,y coordinates.
:param time_between_events: takes floating point to represent the
delay time between subsequent taps. Default value 0.1 represents
tenth of a second.
"""
raise NotImplementedError("You cannot use this class directly.")
def tap_object(self, object, press_duration=0.1, time_between_events=0.1):
"""Tap the center point of a given object.
It does this by looking for several attributes, in order. The first
attribute found will be used. The attributes used are (in order):
* globalRect (x,y,w,h)
* center_x, center_y
* x, y, w, h
:param time_between_events: takes floating point to represent the
delay time between subsequent taps. Default value 0.1 represents
tenth of a second.
:raises: **ValueError** if none of these attributes are found, or if an
attribute is of an incorrect type.
"""
raise NotImplementedError("You cannot use this class directly.")
def press(self, x, y):
"""Press and hold at the given x,y coordinates."""
raise NotImplementedError("You cannot use this class directly.")
def move(self, x, y):
"""Move the pointer coords to (x,y).
.. note:: The touch 'finger' must be pressed for a call to this
method to be successful. (see :py:meth:`press` for further details on
touch presses.)
:raises: **RuntimeError** if called and the touch 'finger' isn't
pressed.
"""
raise NotImplementedError("You cannot use this class directly.")
def release(self):
"""Release a previously pressed finger"""
raise NotImplementedError("You cannot use this class directly.")
def drag(self, x1, y1, x2, y2, rate=10, time_between_events=0.01):
"""Perform a drag gesture.
The finger will be dragged from the starting point to the ending point
with multiple moves. The number of moves, and thus the time that it
will take to complete the drag can be altered with the `rate`
parameter.
:param x1: The point on the x axis where the drag will start from.
:param y1: The point on the y axis where the drag will starts from.
:param x2: The point on the x axis where the drag will end at.
:param y2: The point on the y axis where the drag will end at.
:param rate: The number of pixels the finger will be moved per
iteration. Default is 10 pixels. A higher rate will make the drag
faster, and lower rate will make it slower.
:param time_between_events: The number of seconds that the drag will
wait between iterations.
:raises RuntimeError: if the finger is already pressed.
:raises RuntimeError: if no more finger slots are available.
"""
raise NotImplementedError("You cannot use this class directly.")
class Pointer(object):
"""A wrapper class that represents a pointing device which can either be a
mouse or a touch, and provides a unified API.
This class is useful if you want to run tests with either a mouse or a
touch device, and want to write your tests to use a single API. Create
this wrapper by passing it either a mouse or a touch device, like so::
pointer_device = Pointer(Mouse.create())
or, like so::
pointer_device = Pointer(Touch.create())
.. warning::
Some operations only make sense for certain devices. This class
attempts to minimise the differences between the Mouse and Touch APIs,
but there are still some operations that will cause exceptions to be
raised. These are documented in the specific methods below.
"""
def __init__(self, device):
if not isinstance(device, Mouse) and not isinstance(device, Touch):
raise TypeError(
"`device` must be either a Touch or a Mouse instance.")
self._device = device
@property
def x(self):
"""Pointer X coordinate.
If the wrapped device is a :class:`Touch` device, this will return the
last known X coordinate, which may not be a sensible value.
"""
return self._device.x
@property
def y(self):
"""Pointer Y coordinate.
If the wrapped device is a :class:`Touch` device, this will return the
last known Y coordinate, which may not be a sensible value.
"""
return self._device.y
def press(self, button=1):
"""Press the pointer at it's current location.
If the wrapped device is a mouse, you may pass a button specification.
If it is a touch device, passing anything other than 1 will raise a
ValueError exception.
"""
if isinstance(self._device, Mouse):
self._device.press(button)
else:
if button != 1:
raise ValueError(
"Touch devices do not have button %d" % (button))
self._device.press(self.x, self.y)
def release(self, button=1):
"""Releases the pointer at it's current location.
If the wrapped device is a mouse, you may pass a button specification.
If it is a touch device, passing anything other than 1 will raise a
ValueError exception.
"""
if isinstance(self._device, Mouse):
self._device.release(button)
else:
if button != 1:
raise ValueError(
"Touch devices do not have button %d" % (button))
self._device.release()
def click(self, button=1, press_duration=0.10, time_between_events=0.1):
"""Press and release at the current pointer location.
If the wrapped device is a mouse, the button specification is used. If
it is a touch device, passing anything other than 1 will raise a
ValueError exception.
:param time_between_events: takes floating point to represent the
delay time between subsequent clicks/taps. Default value 0.1
represents tenth of a second.
"""
if isinstance(self._device, Mouse):
self._device.click(button, press_duration, time_between_events)
else:
if button != 1:
raise ValueError(
"Touch devices do not have button %d" % (button))
self._device.tap(
self.x,
self.y,
press_duration=press_duration,
time_between_events=time_between_events
)
def move(self, x, y):
"""Moves the pointer to the specified coordinates.
If the wrapped device is a mouse, the mouse will animate to the
specified coordinates. If the wrapped device is a touch device, this
method will determine where the next :meth:`press`, :meth:`release` or
:meth:`click` will occur.
"""
self._device.move(x, y)
def click_object(
self,
object_proxy,
button=1,
press_duration=0.10,
time_between_events=0.1):
"""
Attempts to move the pointer to 'object_proxy's centre point
and click a button.
See :py:meth:`~autopilot.input.get_center_point` for details on how
the center point is calculated.
If the wrapped device is a mouse, the button specification is used. If
it is a touch device, passing anything other than 1 will raise a
ValueError exception.
:param time_between_events: takes floating point to represent the
delay time between subsequent clicks/taps. Default value 0.1
represents tenth of a second.
"""
self.move_to_object(object_proxy)
self.click(button, press_duration, time_between_events)
def move_to_object(self, object_proxy):
"""Attempts to move the pointer to 'object_proxy's centre point.
See :py:meth:`~autopilot.input.get_center_point` for details on how
the center point is calculated.
"""
x, y = get_center_point(object_proxy)
self.move(x, y)
def position(self):
"""
Returns the current position of the pointer.
:return: (x,y) tuple
"""
if isinstance(self._device, Mouse):
return self._device.position()
else:
return (self.x, self.y)
def drag(self, x1, y1, x2, y2, rate=10, time_between_events=0.01):
"""Perform a press, move and release.
This is to keep a common API between Mouse and Finger as long as
possible.
The pointer will be dragged from the starting point to the ending point
with multiple moves. The number of moves, and thus the time that it
will take to complete the drag can be altered with the `rate`
parameter.
:param x1: The point on the x axis where the drag will start from.
:param y1: The point on the y axis where the drag will starts from.
:param x2: The point on the x axis where the drag will end at.
:param y2: The point on the y axis where the drag will end at.
:param rate: The number of pixels the mouse will be moved per
iteration. Default is 10 pixels. A higher rate will make the drag
faster, and lower rate will make it slower.
:param time_between_events: The number of seconds that the drag will
wait between iterations.
"""
self._device.drag(
x1, y1, x2, y2, rate=rate, time_between_events=time_between_events)
./autopilot/input/_X11.py 0000644 0000041 0000041 00000033526 13061552435 015521 0 ustar www-data www-data # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Autopilot Functional Test Tool
# Copyright (C) 2012, 2013, 2014 Canonical
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
"""A collection of emulators for X11 - namely keyboards and mice.
In the future we may also need other devices.
"""
import logging
from autopilot.input import get_center_point
from autopilot.display import is_point_on_any_screen, move_mouse_to_screen
from autopilot.utilities import (
EventDelay,
Silence,
sleep,
StagnantStateDetector,
)
from autopilot.input import (
Keyboard as KeyboardBase,
Mouse as MouseBase,
)
from Xlib import X, XK
from Xlib.display import Display
from Xlib.ext.xtest import fake_input
_PRESSED_KEYS = []
_PRESSED_MOUSE_BUTTONS = []
_DISPLAY = None
_logger = logging.getLogger(__name__)
def get_display():
"""Return the Xlib display object.
It is created (silently) if it doesn't exist.
"""
global _DISPLAY
if _DISPLAY is None:
with Silence():
_DISPLAY = Display()
return _DISPLAY
def reset_display():
global _DISPLAY
_DISPLAY = None
class Keyboard(KeyboardBase):
"""Wrapper around xlib to make faking keyboard input possible."""
_special_X_keysyms = {
' ': "space",
'\t': "Tab",
'\n': "Return", # for some reason this needs to be cr, not lf
'\r': "Return",
'\e': "Escape",
'\b': "BackSpace",
'!': "exclam",
'#': "numbersign",
'%': "percent",
'$': "dollar",
'&': "ampersand",
'"': "quotedbl",
'\'': "apostrophe",
'(': "parenleft",
')': "parenright",
'*': "asterisk",
'=': "equal",
'+': "plus",
',': "comma",
'-': "minus",
'.': "period",
'/': "slash",
':': "colon",
';': "semicolon",
'<': "less",
'>': "greater",
'?': "question",
'@': "at",
'[': "bracketleft",
']': "bracketright",
'\\': "backslash",
'^': "asciicircum",
'_': "underscore",
'`': "grave",
'{': "braceleft",
'|': "bar",
'}': "braceright",
'~': "asciitilde"
}
_keysym_translations = {
'Control': 'Control_L',
'Ctrl': 'Control_L',
'Alt': 'Alt_L',
'AltR': 'Alt_R',
'Super': 'Super_L',
'Shift': 'Shift_L',
'Enter': 'Return',
'Space': ' ',
'Backspace': 'BackSpace',
}
def __init__(self):
super(Keyboard, self).__init__()
self.shifted_keys = [k[1] for k in get_display()._keymap_codes if k]
def press(self, keys, delay=0.2):
"""Send key press events only.
:param string keys: Keys you want pressed.
Example::
press('Alt+F2')
presses the 'Alt' and 'F2' keys.
"""
if not isinstance(keys, str):
raise TypeError("'keys' argument must be a string.")
_logger.debug("Pressing keys %r with delay %f", keys, delay)
for key in self.__translate_keys(keys):
self.__perform_on_key(key, X.KeyPress)
sleep(delay)
def release(self, keys, delay=0.2):
"""Send key release events only.
:param string keys: Keys you want released.
Example::
release('Alt+F2')
releases the 'Alt' and 'F2' keys.
"""
if not isinstance(keys, str):
raise TypeError("'keys' argument must be a string.")
_logger.debug("Releasing keys %r with delay %f", keys, delay)
# release keys in the reverse order they were pressed in.
keys = self.__translate_keys(keys)
keys.reverse()
for key in keys:
self.__perform_on_key(key, X.KeyRelease)
sleep(delay)
def press_and_release(self, keys, delay=0.2):
"""Press and release all items in 'keys'.
This is the same as calling 'press(keys);release(keys)'.
:param string keys: Keys you want pressed and released.
Example::
press_and_release('Alt+F2')
presses both the 'Alt' and 'F2' keys, and then releases both keys.
"""
self.press(keys, delay)
self.release(keys, delay)
def type(self, string, delay=0.1):
"""Simulate a user typing a string of text.
.. note:: Only 'normal' keys can be typed with this method. Control
characters (such as 'Alt' will be interpreted as an 'A', and 'l',
and a 't').
"""
if not isinstance(string, str):
raise TypeError("'keys' argument must be a string.")
_logger.debug("Typing text %r", string)
for key in string:
# Don't call press or release here, as they translate keys to
# keysyms.
self.__perform_on_key(key, X.KeyPress)
sleep(delay)
self.__perform_on_key(key, X.KeyRelease)
sleep(delay)
@classmethod
def on_test_end(cls, test_instance):
"""Generate KeyRelease events for any un-released keys.
.. important:: Ensure you call this at the end of any test to release
any keys that were pressed and not released.
"""
global _PRESSED_KEYS
for keycode in _PRESSED_KEYS:
_logger.warning(
"Releasing key %r as part of cleanup call.", keycode)
fake_input(get_display(), X.KeyRelease, keycode)
_PRESSED_KEYS = []
def __perform_on_key(self, key, event):
if not isinstance(key, str):
raise TypeError("Key parameter must be a string")
keycode = 0
shift_mask = 0
keycode, shift_mask = self.__char_to_keycode(key)
if shift_mask != 0:
fake_input(get_display(), event, 50)
if event == X.KeyPress:
_logger.debug("Sending press event for key: %s", key)
_PRESSED_KEYS.append(keycode)
elif event == X.KeyRelease:
_logger.debug("Sending release event for key: %s", key)
if keycode in _PRESSED_KEYS:
_PRESSED_KEYS.remove(keycode)
else:
_logger.warning(
"Generating release event for keycode %d that was not "
"pressed.", keycode)
fake_input(get_display(), event, keycode)
get_display().sync()
def __get_keysym(self, key):
keysym = XK.string_to_keysym(key)
if keysym == 0:
# Unfortunately, although this works to get the correct keysym
# i.e. keysym for '#' is returned as "numbersign"
# the subsequent display.keysym_to_keycode("numbersign") is 0.
keysym = XK.string_to_keysym(self._special_X_keysyms[key])
return keysym
def __is_shifted(self, key):
return len(key) == 1 and ord(key) in self.shifted_keys and key != '<'
def __char_to_keycode(self, key):
keysym = self.__get_keysym(key)
keycode = get_display().keysym_to_keycode(keysym)
if keycode == 0:
_logger.warning("Sorry, can't map '%s'", key)
if (self.__is_shifted(key)):
shift_mask = X.ShiftMask
else:
shift_mask = 0
return keycode, shift_mask
def __translate_keys(self, key_string):
if len(key_string) > 1:
return [self._keysym_translations.get(k, k)
for k in key_string.split('+')]
else:
# workaround that lets us press_and_release '+' by itself.
return [self._keysym_translations.get(key_string, key_string)]
class Mouse(MouseBase):
"""Wrapper around xlib to make moving the mouse easier."""
def __init__(self):
super(Mouse, self).__init__()
# Try to access the screen to see if X11 mouse is supported
get_display()
self.event_delayer = EventDelay()
@property
def x(self):
"""Mouse position X coordinate."""
return self.position()[0]
@property
def y(self):
"""Mouse position Y coordinate."""
return self.position()[1]
def press(self, button=1):
"""Press mouse button at current mouse location."""
_logger.debug("Pressing mouse button %d", button)
_PRESSED_MOUSE_BUTTONS.append(button)
fake_input(get_display(), X.ButtonPress, button)
get_display().sync()
def release(self, button=1):
"""Releases mouse button at current mouse location."""
_logger.debug("Releasing mouse button %d", button)
if button in _PRESSED_MOUSE_BUTTONS:
_PRESSED_MOUSE_BUTTONS.remove(button)
else:
_logger.warning(
"Generating button release event or button %d that was not "
"pressed.", button)
fake_input(get_display(), X.ButtonRelease, button)
get_display().sync()
def click(self, button=1, press_duration=0.10, time_between_events=0.1):
"""Click mouse at current location."""
self.event_delayer.delay(time_between_events)
self.press(button)
sleep(press_duration)
self.release(button)
def move(self, x, y, animate=True, rate=10, time_between_events=0.01):
"""Moves mouse to location (x, y).
Callers should avoid specifying the *rate* or *time_between_events*
parameters unless they need a specific rate of movement.
"""
def perform_move(x, y, sync):
fake_input(
get_display(),
X.MotionNotify,
sync,
X.CurrentTime,
X.NONE,
x=int(x),
y=int(y))
get_display().sync()
sleep(time_between_events)
dest_x, dest_y = int(x), int(y)
_logger.debug(
"Moving mouse to position %d,%d %s animation.", dest_x, dest_y,
"with" if animate else "without")
if not animate:
perform_move(dest_x, dest_y, False)
return
coordinate_valid = is_point_on_any_screen((dest_x, dest_y))
if x < -1000 or y < -1000:
raise ValueError(
"Invalid mouse coordinates: %d, %d" % (dest_x, dest_y))
loop_detector = StagnantStateDetector(threshold=1000)
curr_x, curr_y = self.position()
while curr_x != dest_x or curr_y != dest_y:
dx = abs(dest_x - curr_x)
dy = abs(dest_y - curr_y)
intx = float(dx) / max(dx, dy)
inty = float(dy) / max(dx, dy)
step_x = min(rate * intx, dx)
step_y = min(rate * inty, dy)
if dest_x < curr_x:
step_x *= -1
if dest_y < curr_y:
step_y *= -1
perform_move(step_x, step_y, True)
if coordinate_valid:
curr_x, curr_y = self.position()
else:
curr_x += step_x
curr_y += step_y
try:
loop_detector.check_state(curr_x, curr_y)
except StagnantStateDetector.StagnantState as e:
e.args = ("Mouse cursor is stuck.", )
raise
x, y = self.position()
_logger.debug('The mouse is now at position %d,%d.', x, y)
def move_to_object(self, object_proxy):
"""Attempts to move the mouse to 'object_proxy's centre point.
See :py:meth:`~autopilot.input.get_center_point` for details on how
the center point is calculated.
"""
x, y = get_center_point(object_proxy)
self.move(x, y)
def position(self):
"""
Returns the current position of the mouse pointer.
:return: (x,y) tuple
"""
coord = get_display().screen().root.query_pointer()._data
x, y = coord["root_x"], coord["root_y"]
return x, y
def drag(self, x1, y1, x2, y2, rate=10, time_between_events=0.01):
"""Perform a press, move and release.
This is to keep a common API between Mouse and Finger as long as
possible.
The pointer will be dragged from the starting point to the ending point
with multiple moves. The number of moves, and thus the time that it
will take to complete the drag can be altered with the `rate`
parameter.
:param x1: The point on the x axis where the drag will start from.
:param y1: The point on the y axis where the drag will starts from.
:param x2: The point on the x axis where the drag will end at.
:param y2: The point on the y axis where the drag will end at.
:param rate: The number of pixels the mouse will be moved per
iteration. Default is 10 pixels. A higher rate will make the drag
faster, and lower rate will make it slower.
:param time_between_events: The number of seconds that the drag will
wait between iterations.
"""
self.move(x1, y1)
self.press()
self.move(x2, y2, rate=rate, time_between_events=time_between_events)
self.release()
@classmethod
def on_test_end(cls, test_instance):
"""Put mouse in a known safe state."""
global _PRESSED_MOUSE_BUTTONS
for btn in _PRESSED_MOUSE_BUTTONS:
_logger.debug("Releasing mouse button %d as part of cleanup", btn)
fake_input(get_display(), X.ButtonRelease, btn)
_PRESSED_MOUSE_BUTTONS = []
move_mouse_to_screen(0)
./autopilot/input/_uinput.py 0000644 0000041 0000041 00000055525 13061552435 016477 0 ustar www-data www-data # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Autopilot Functional Test Tool
# Copyright (C) 2012, 2013, 2014, 2015 Canonical
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
"""UInput device drivers."""
import logging
from evdev import UInput, ecodes as e
from autopilot.input import Keyboard as KeyboardBase
from autopilot.input import Touch as TouchBase
from autopilot.input import get_center_point
from autopilot.platform import model
from autopilot.utilities import deprecated, EventDelay, sleep
_logger = logging.getLogger(__name__)
def _get_devnode_path():
"""Return the uinput device node"""
return '/dev/uinput'
class _UInputKeyboardDevice(object):
"""Wrapper for the UInput Keyboard to execute its primitives."""
def __init__(self, device_class=UInput):
super(_UInputKeyboardDevice, self).__init__()
self._device = device_class(devnode=_get_devnode_path())
self._pressed_keys_ecodes = []
def press(self, key):
"""Press one key button.
It ignores case, so, for example, 'a' and 'A' are mapped to the same
key.
"""
ecode = self._get_ecode_for_key(key)
_logger.debug('Pressing %s (%r).', key, ecode)
self._emit_press_event(ecode)
self._pressed_keys_ecodes.append(ecode)
def _get_ecode_for_key(self, key):
key_name = key if key.startswith('KEY_') else 'KEY_' + key
key_name = key_name.upper()
ecode = e.ecodes.get(key_name, None)
if ecode is None:
raise ValueError('Unknown key name: %s.' % key)
return ecode
def _emit_press_event(self, ecode):
press_value = 1
self._emit(ecode, press_value)
def _emit(self, ecode, value):
self._device.write(e.EV_KEY, ecode, value)
self._device.syn()
def release(self, key):
"""Release one key button.
It ignores case, so, for example, 'a' and 'A' are mapped to the same
key.
:raises ValueError: if ``key`` is not pressed.
"""
ecode = self._get_ecode_for_key(key)
if ecode in self._pressed_keys_ecodes:
_logger.debug('Releasing %s (%r).', key, ecode)
self._emit_release_event(ecode)
self._pressed_keys_ecodes.remove(ecode)
else:
raise ValueError('Key %r not pressed.' % key)
def _emit_release_event(self, ecode):
release_value = 0
self._emit(ecode, release_value)
def release_pressed_keys(self):
"""Release all the keys that are currently pressed."""
for ecode in self._pressed_keys_ecodes:
self._emit_release_event(ecode)
self._pressed_keys_ecodes = []
class Keyboard(KeyboardBase):
_device = None
def __init__(self, device_class=_UInputKeyboardDevice):
super(Keyboard, self).__init__()
if Keyboard._device is None:
Keyboard._device = device_class()
def _sanitise_keys(self, keys):
if keys == '+':
return [keys]
else:
return keys.split('+')
def press(self, keys, delay=0.1):
"""Send key press events only.
The 'keys' argument must be a string of keys you want
pressed. For example:
press('Alt+F2')
presses the 'Alt' and 'F2' keys.
:raises TypeError: if ``keys`` is not a string.
"""
if not isinstance(keys, str):
raise TypeError("'keys' argument must be a string.")
for key in self._sanitise_keys(keys):
for key_button in self._get_key_buttons(key):
self._device.press(key_button)
sleep(delay)
def release(self, keys, delay=0.1):
"""Send key release events only.
The 'keys' argument must be a string of keys you want
released. For example:
release('Alt+F2')
releases the 'Alt' and 'F2' keys.
Keys are released in the reverse order in which they are specified.
:raises TypeError: if ``keys`` is not a string.
:raises ValueError: if one of the keys to be released is not pressed.
"""
if not isinstance(keys, str):
raise TypeError("'keys' argument must be a string.")
for key in reversed(self._sanitise_keys(keys)):
for key_button in reversed(self._get_key_buttons(key)):
self._device.release(key_button)
sleep(delay)
def press_and_release(self, keys, delay=0.1):
"""Press and release all items in 'keys'.
This is the same as calling 'press(keys);release(keys)'.
The 'keys' argument must be a string of keys you want
pressed and released.. For example:
press_and_release('Alt+F2')
presses both the 'Alt' and 'F2' keys, and then releases both keys.
:raises TypeError: if ``keys`` is not a string.
"""
_logger.debug("Pressing and Releasing: %s", keys)
self.press(keys, delay)
self.release(keys, delay)
def type(self, string, delay=0.1):
"""Simulate a user typing a string of text.
Only 'normal' keys can be typed with this method. Control characters
(such as 'Alt' will be interpreted as an 'A', and 'l', and a 't').
:raises TypeError: if ``keys`` is not a string.
"""
if not isinstance(string, str):
raise TypeError("'keys' argument must be a string.")
_logger.debug("Typing text %r", string)
for key in string:
self.press(key, delay)
self.release(key, delay)
@classmethod
def on_test_end(cls, test_instance):
"""Generate KeyRelease events for any un-released keys.
Make sure you call this at the end of any test to release
any keys that were pressed and not released.
"""
if cls._device is not None:
cls._device.release_pressed_keys()
def _get_key_buttons(self, key):
"""Return a list of the key buttons required to press.
Multiple buttons will be returned when the key specified requires more
than one keypress to generate (for example, upper-case letters).
"""
key_buttons = []
if key.isupper() or key in _SHIFTED_KEYS:
key_buttons.append('KEY_LEFTSHIFT')
key_name = _UINPUT_CODE_TRANSLATIONS.get(key.upper(), key)
key_buttons.append(key_name)
return key_buttons
@deprecated('the Touch class to instantiate a device object')
def create_touch_device(res_x=None, res_y=None):
"""Create and return a UInput touch device.
If res_x and res_y are not specified, they will be queried from the system.
"""
return UInput(events=_get_touch_events(res_x, res_y),
name='autopilot-finger',
version=0x2, devnode=_get_devnode_path())
# Multiouch notes:
# ----------------
# We're simulating a class of device that can track multiple touches, and keep
# them separate. This is how most modern track devices work anyway. The device
# is created with a capability to track a certain number of distinct touches at
# once. This is the ABS_MT_SLOT capability. Since our target device can track 9
# separate touches, we'll do the same.
# Each finger contact starts by registering a slot number (0-8) with a tracking
# Id. The Id should be unique for this touch - this can be an
# auto-inctrementing integer. The very first packets to tell the kernel that
# we have a touch happening should look like this:
# ABS_MT_SLOT 0
# ABS_MT_TRACKING_ID 45
# ABS_MT_POSITION_X x[0]
# ABS_MT_POSITION_Y y[0]
# This associates Tracking id 45 (could be any number) with slot 0. Slot 0 can
# now not be use by any other touch until it is released.
# If we want to move this contact's coordinates, we do this:
# ABS_MT_SLOT 0
# ABS_MT_POSITION_X 123
# ABS_MT_POSITION_Y 234
# Technically, the 'SLOT 0' part isn't needed, since we're already in slot 0,
# but it doesn't hurt to have it there.
# To lift the contact, we simply specify a tracking Id of -1:
# ABS_MT_SLOT 0
# ABS_MT_TRACKING_ID -1
# The initial association between slot and tracking Id is made when the
# 'finger' first makes contact with the device (well, not technically true,
# but close enough). Multiple touches can be active simultaniously, as long
# as they all have unique slots, and tracking Ids. The simplest way to think
# about this is that the SLOT refers to a finger number, and the TRACKING_ID
# identifies a unique touch for the duration of it's existance.
def _get_touch_events(res_x=None, res_y=None):
if res_x is None or res_y is None:
res_x, res_y = _get_system_resolution()
touch_tool = _get_touch_tool()
events = {
e.EV_ABS: [
(e.ABS_X, (0, res_x, 0, 0)),
(e.ABS_Y, (0, res_y, 0, 0)),
(e.ABS_PRESSURE, (0, 65535, 0, 0)),
(e.ABS_MT_POSITION_X, (0, res_x, 0, 0)),
(e.ABS_MT_POSITION_Y, (0, res_y, 0, 0)),
(e.ABS_MT_TOUCH_MAJOR, (0, 30, 0, 0)),
(e.ABS_MT_TRACKING_ID, (0, 65535, 0, 0)),
(e.ABS_MT_PRESSURE, (0, 255, 0, 0)),
(e.ABS_MT_SLOT, (0, 9, 0, 0)),
],
e.EV_KEY: [
touch_tool,
]
}
return events
def _get_system_resolution():
from autopilot.display import Display
display = Display.create()
# TODO: This calculation needs to become part of the display module:
l = r = t = b = 0
for screen in range(display.get_num_screens()):
geometry = display.get_screen_geometry(screen)
if geometry[0] < l:
l = geometry[0]
if geometry[1] < t:
t = geometry[1]
if geometry[0] + geometry[2] > r:
r = geometry[0] + geometry[2]
if geometry[1] + geometry[3] > b:
b = geometry[1] + geometry[3]
res_x = r - l
res_y = b - t
return res_x, res_y
def _get_touch_tool():
return e.BTN_TOUCH
class _UInputTouchDevice(object):
"""Wrapper for the UInput Touch to execute its primitives."""
_device = None
_touch_fingers_in_use = []
_last_tracking_id = 0
def __init__(self, res_x=None, res_y=None, device_class=UInput):
"""Class constructor.
If res_x and res_y are not specified, they will be queried from the
system.
"""
super(_UInputTouchDevice, self).__init__()
if _UInputTouchDevice._device is None:
_UInputTouchDevice._device = device_class(
events=_get_touch_events(res_x, res_y),
name='autopilot-finger',
version=0x2, devnode=_get_devnode_path())
self._touch_finger_slot = None
@property
def pressed(self):
return self._touch_finger_slot is not None
def finger_down(self, x, y):
"""Internal: moves finger "finger" down on the touchscreen.
:param x: The finger will be moved to this x coordinate.
:param y: The finger will be moved to this y coordinate.
:raises RuntimeError: if the finger is already pressed.
:raises RuntimeError: if no more touch slots are available.
"""
if self.pressed:
raise RuntimeError("Cannot press finger: it's already pressed.")
self._touch_finger_slot = self._get_free_touch_finger_slot()
self._device.write(e.EV_ABS, e.ABS_MT_SLOT, self._touch_finger_slot)
self._device.write(
e.EV_ABS, e.ABS_MT_TRACKING_ID, self._get_next_tracking_id())
press_value = 1
self._device.write(e.EV_KEY, _get_touch_tool(), press_value)
self._device.write(e.EV_ABS, e.ABS_MT_POSITION_X, int(x))
self._device.write(e.EV_ABS, e.ABS_MT_POSITION_Y, int(y))
self._device.write(e.EV_ABS, e.ABS_MT_PRESSURE, 400)
self._device.syn()
def _get_free_touch_finger_slot(self):
"""Return the id of a free touch finger.
:raises RuntimeError: if no more touch slots are available.
"""
max_number_of_fingers = 9
for i in range(max_number_of_fingers):
if i not in _UInputTouchDevice._touch_fingers_in_use:
_UInputTouchDevice._touch_fingers_in_use.append(i)
return i
raise RuntimeError('All available fingers have been used already.')
def _get_next_tracking_id(self):
_UInputTouchDevice._last_tracking_id += 1
return _UInputTouchDevice._last_tracking_id
def finger_move(self, x, y):
"""Internal: moves finger "finger" on the touchscreen to pos (x,y)
NOTE: The finger has to be down for this to have any effect.
:raises RuntimeError: if the finger is not pressed.
"""
if not self.pressed:
raise RuntimeError('Attempting to move without finger being down.')
_logger.debug("Moving pointing 'finger' to position %d,%d.", x, y)
self._device.write(e.EV_ABS, e.ABS_MT_SLOT, self._touch_finger_slot)
self._device.write(e.EV_ABS, e.ABS_MT_POSITION_X, int(x))
self._device.write(e.EV_ABS, e.ABS_MT_POSITION_Y, int(y))
self._device.syn()
_logger.debug("The pointing 'finger' is now at position %d,%d.", x, y)
def finger_up(self):
"""Internal: moves finger "finger" up from the touchscreen
:raises RuntimeError: if the finger is not pressed.
"""
if not self.pressed:
raise RuntimeError("Cannot release finger: it's not pressed.")
self._device.write(e.EV_ABS, e.ABS_MT_SLOT, self._touch_finger_slot)
lift_tracking_id = -1
self._device.write(e.EV_ABS, e.ABS_MT_TRACKING_ID, lift_tracking_id)
release_value = 0
self._device.write(e.EV_KEY, _get_touch_tool(), release_value)
self._device.syn()
self._release_touch_finger()
def _release_touch_finger(self):
"""Release the touch finger.
:raises RuntimeError: if the finger was not claimed before or was
already released.
"""
if (self._touch_finger_slot not in
_UInputTouchDevice._touch_fingers_in_use):
raise RuntimeError(
"Finger %d was never claimed, or has already been released." %
self._touch_finger_slot)
_UInputTouchDevice._touch_fingers_in_use.remove(
self._touch_finger_slot)
self._touch_finger_slot = None
class Touch(TouchBase):
"""Low level interface to generate single finger touch events."""
def __init__(self, device_class=_UInputTouchDevice):
super(Touch, self).__init__()
self._device = device_class()
self.event_delayer = EventDelay()
self._x = 0
self._y = 0
@property
def x(self):
"""Finger position X coordinate."""
return self._x
@property
def y(self):
"""Finger position Y coordinate."""
return self._y
@property
def pressed(self):
return self._device.pressed
def tap(self, x, y, press_duration=0.1, time_between_events=0.1):
"""Click (or 'tap') at given x and y coordinates.
:raises RuntimeError: if the finger is already pressed.
:raises RuntimeError: if no more finger slots are available.
"""
_logger.debug("Tapping at: %d,%d", x, y)
self.event_delayer.delay(time_between_events)
self._finger_down(x, y)
sleep(press_duration)
self._device.finger_up()
def _finger_down(self, x, y):
self._device.finger_down(x, y)
self._x = x
self._y = y
def tap_object(self, object_, press_duration=0.1, time_between_events=0.1):
"""Click (or 'tap') a given object.
:raises RuntimeError: if the finger is already pressed.
:raises RuntimeError: if no more finger slots are available.
:raises ValueError: if `object_` doesn't have any recognised position
attributes or if they are not of the correct type.
"""
_logger.debug("Tapping object: %r", object)
x, y = get_center_point(object_)
self.tap(
x,
y,
press_duration=press_duration,
time_between_events=time_between_events
)
def press(self, x, y):
"""Press and hold a given object or at the given coordinates.
Call release() when the object has been pressed long enough.
:raises RuntimeError: if the finger is already pressed.
:raises RuntimeError: if no more finger slots are available.
"""
_logger.debug("Pressing at: %d,%d", x, y)
self._finger_down(x, y)
def release(self):
"""Release a previously pressed finger.
:raises RuntimeError: if the touch is not pressed.
"""
_logger.debug("Releasing")
self._device.finger_up()
def move(self, x, y, animate=True, rate=10, time_between_events=0.01):
"""Moves the pointing "finger" to pos(x,y).
NOTE: The finger has to be down for this to have any effect.
:param x: The point on the x axis where the move will end at.
:param y: The point on the y axis where the move will end at.
:param animate: Indicates if the move should be immediate or it should
be animated moving the finger slowly accross the screen as a real
user would do. By default, when the finger is down the finger is
animated. When the finger is up, the parameter is ignored and the
move is always immediate.
:type animate: boolean.
:param rate: The number of pixels the finger will be moved per
iteration. Default is 10 pixels. A higher rate will make the drag
faster, and lower rate will make it slower.
:param time_between_events: The number of seconds that the drag will
wait between iterations.
:raises RuntimeError: if the finger is not pressed.
"""
if self.pressed:
if animate:
self._move_with_animation(x, y, rate, time_between_events)
else:
self._device.finger_move(x, y)
self._x = x
self._y = y
def _move_with_animation(self, x, y, rate, time_between_events):
current_x, current_y = self.x, self.y
while current_x != x or current_y != y:
dx = abs(x - current_x)
dy = abs(y - current_y)
intx = float(dx) / max(dx, dy)
inty = float(dy) / max(dx, dy)
step_x = min(rate * intx, dx)
step_y = min(rate * inty, dy)
if x < current_x:
step_x *= -1
if y < current_y:
step_y *= -1
current_x += step_x
current_y += step_y
self._device.finger_move(current_x, current_y)
sleep(time_between_events)
def drag(self, x1, y1, x2, y2, rate=10, time_between_events=0.01):
"""Perform a drag gesture.
The finger will be dragged from the starting point to the ending point
with multiple moves. The number of moves, and thus the time that it
will take to complete the drag can be altered with the `rate`
parameter.
:param x1: The point on the x axis where the drag will start from.
:param y1: The point on the y axis where the drag will starts from.
:param x2: The point on the x axis where the drag will end at.
:param y2: The point on the y axis where the drag will end at.
:param rate: The number of pixels the finger will be moved per
iteration. Default is 10 pixels. A higher rate will make the drag
faster, and lower rate will make it slower.
:param time_between_events: The number of seconds that the drag will
wait between iterations.
:raises RuntimeError: if the finger is already pressed.
:raises RuntimeError: if no more finger slots are available.
"""
_logger.debug("Dragging from %d,%d to %d,%d", x1, y1, x2, y2)
self._finger_down(x1, y1)
self.move(
x2, y2, animate=True, rate=rate,
time_between_events=time_between_events)
self._device.finger_up()
# veebers: there should be a better way to handle this.
_SHIFTED_KEYS = "~!@#$%^&*()_+{}|:\"?><"
# The double-ups are due to the 'shifted' keys.
_UINPUT_CODE_TRANSLATIONS = {
'/': 'SLASH',
'?': 'SLASH',
'.': 'DOT',
',': 'COMMA',
'>': 'DOT',
'<': 'COMMA',
'\'': 'APOSTROPHE',
'"': 'APOSTROPHE',
';': 'SEMICOLON',
':': 'SEMICOLON',
'\\': 'BACKSLASH',
'|': 'BACKSLASH',
']': 'RIGHTBRACE',
'[': 'LEFTBRACE',
'}': 'RIGHTBRACE',
'{': 'LEFTBRACE',
'=': 'EQUAL',
'+': 'EQUAL',
'-': 'MINUS',
'_': 'MINUS',
')': '0',
'(': '9',
'*': '8',
'&': '7',
'^': '6',
'%': '5',
'$': '4',
'#': '3',
'@': '2',
'!': '1',
'~': 'GRAVE',
'`': 'GRAVE',
' ': 'SPACE',
'\t': 'TAB',
'\n': 'ENTER',
'\b': 'BACKSPACE',
'CTRL': 'LEFTCTRL',
'ALT': 'LEFTALT',
'SHIFT': 'LEFTSHIFT',
}
class UInputHardwareKeysDevice:
_device = None
def __init__(self, device_class=UInput):
if not UInputHardwareKeysDevice._device:
UInputHardwareKeysDevice._device = device_class(
devnode=_get_devnode_path(),
)
# This workaround is not needed on desktop.
if model() != 'Desktop':
self._wait_for_device_to_ready()
def press_and_release_power_button(self):
self._device.write(e.EV_KEY, e.KEY_POWER, 1)
self._device.write(e.EV_KEY, e.KEY_POWER, 0)
self._device.syn()
def _wait_for_device_to_ready(
self,
retry_attempts_count=10,
retry_interval=0.1,
):
"""Wait for UInput device to initialize.
This is a workaround for a bug in evdev where the input device
is not instantly created.
:param retry_attempts_count: number of attempts to check
if device is ready.
:param retry_interval: time in fractional seconds to be
slept, between each attempt to check if device is
ready.
:raises RuntimeError: if device is not initialized after
number of retries specified in *retry_attempts_count*.
"""
for i in range(retry_attempts_count):
device = self._device._find_device()
if device:
self._device.device = device
return
else:
sleep(retry_interval)
raise RuntimeError('Failed to find UInput device.')
./autopilot/clipboard.py 0000644 0000041 0000041 00000002335 13061552435 015603 0 ustar www-data www-data # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Autopilot Functional Test Tool
# Copyright (C) 2012-2013 Canonical
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
"""A collection of functions relating to the X11clipboards."""
import autopilot._glib
def get_clipboard_contents():
"""Get the contents of the clipboard.
This function returns the text copied to the 'CLIPBOARD' clipboard. Text
can be added to this clipbaord using Ctrl+C.
"""
Gtk = autopilot._glib._import_gtk()
Gdk = autopilot._glib._import_gdk()
cb = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
return cb.wait_for_text()
./autopilot/emulators.py 0000644 0000041 0000041 00000003031 13061552435 015651 0 ustar www-data www-data # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Autopilot Functional Test Tool
# Copyright (C) 2012-2013 Canonical
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
"""
.. otto:: **Deprecated Namespace!**
This module contains modules that were in the ``autopilot.emulators``
package in autopilot version 1.2 and earlier, but have now been moved to
the ``autopilot`` package.
This module exists to ease the transition to autopilot 1.3, but is not
guaranteed to exist in the future.
.. seealso::
Modulule :mod:`autopilot.display`
Get display information.
Module :mod:`autopilot.input`
Create input events to interact with the application under test.
"""
import autopilot.display as display # flake8: noqa
import autopilot.clipboard as clipboard # flake8: noqa
import autopilot.dbus_handler as dbus_handler # flake8: noqa
import autopilot.input as input # flake8: noqa
./bin/ 0000755 0000041 0000041 00000000000 13061552435 012017 5 ustar www-data www-data ./bin/autopilot3-sandbox-run 0000755 0000041 0000041 00000011615 13061552435 016312 0 ustar www-data www-data #!/bin/sh
#
# Runner to execute autopilot locally
#
# This scripts run autopilot in a "fake" X server, and optionally a
# window manager with either headless with xvfb or nested with Xephyr
#
# Copyright (C) 2013-2015 Canonical
#
# Authors: Jean-Baptiste Lallement
#
# This program is free software; you can redistribute it and/or modify it # under
# the terms of the GNU General Public License as published by the Free # Software
# Foundation; version 3.
#
# 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 General Public License for more # details.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
set -eu
# General Settings
RC=0 # Main return code
BINDIR=$(dirname $(readlink -f $0))
XEPHYR=0
XEPHYR_CMD="$(which Xephyr||true)"
XEPHYR_OPT="-ac -br -noreset"
XVFB_CMD="$(which Xvfb||true)"
XVFB_OPT=""
AP_OPT=""
SERVERNUM=5
SCREEN="1024x768x24"
USEWM=0
WINDOWMANAGER="ratpoison"
WINDOWMANAGER_CMD="$(which $WINDOWMANAGER||true)"
DBUS_SESSION_BUS_PID=""
X_PID=""
usage() {
# Display usage and exit with error
cat</dev/null 2>&1 || true
done
exit $RC
}
trap on_exit EXIT INT QUIT ABRT PIPE TERM
SHORTOPTS="hda:s:Xw:"
LONGOPTS="help,debug,autopilot:,screen:,xephyr,windowmanager:"
TEMP=$(getopt -o $SHORTOPTS --long $LONGOPTS -- "$@")
eval set -- "$TEMP"
exec 2>&1
while true ; do
case "$1" in
-h|--help)
usage;;
-d|--debug)
set -x
shift;;
-a|--autopilot)
AP_OPT=$2
shift 2;;
-s|-screen)
SCREEN=$2
shift 2;;
-w|--windowmanager)
USEWM=1
WINDOWMANAGER=$2
WINDOWMANAGER_CMD="$(which $WINDOWMANAGER||true)"
[ ! -x "$WINDOWMANAGER_CMD" ] && \
echo "E: $WINDOWMANAGER Executable not found." &&\
RC=1 && exit 1
shift 2;;
-X|--xephyr)
XEPHYR=1
[ ! -x "$XEPHYR_CMD" ] && \
echo "E: Xephyr executable not found. Please install Xephyr" &&\
RC=1 && exit 1
shift;;
--) shift;
break;;
*) usage;;
esac
done
[ $# -eq 0 ] && usage
if [ $XEPHYR -eq 0 -a ! -x "$XVFB_CMD" ]; then
echo "E: Xvfb executable not found. Please install xvfb"
RC=1
exit
fi
TESTLIST="$@"
echo "I: Running tests $TESTLIST"
SERVERNUM=$(find_free_servernum)
XCMD="$XVFB_CMD :$SERVERNUM $XVFB_OPT -screen 0 $SCREEN"
[ $XEPHYR -eq 1 ] && XCMD="$XEPHYR_CMD :$SERVERNUM $XEPHYR_OPT -screen $SCREEN"
echo "I: Starting X Server: $XCMD"
$XCMD >/dev/null 2>&1 &
X_PID=$!
export DISPLAY=:${SERVERNUM}.0
export XAUTHORITY=/dev/null
wait_for_x $SERVERNUM
if [ "$USEWM" -eq 1 ]; then
echo "I: Starting window manager: $WINDOWMANAGER_CMD"
dbus-launch --exit-with-session $WINDOWMANAGER_CMD &
fi
echo "I: Starting autopilot"
dbus-launch --exit-with-session python3 -m autopilot.run run $AP_OPT $TESTLIST || RC=$?
echo "I: autopilot tests done"
./README 0000644 0000041 0000041 00000010036 13061552435 012127 0 ustar www-data www-data Welcome to the Autopilot source code!
#####################################
Autopilot is a tool for writing functional tests for GUI applications.
Autopilot is free software, licensed under GNU General Public License (GPLv3+).
Links
=====
- Project Home (Source Code, Version Control, Bug Tracking, etc):
https://launchpad.net/autopilot
- Documentation (Tutorial, FAQ, API Reference, etc):
https://developer.ubuntu.com/api/autopilot/python/1.6.0/
- IRC channel is #ubuntu-autopilot on irc.freenode.net
Build Instructions
==================
Autopilot is not buildable within a python virtualenv, as it requires several packages that are not available on ``pypi``. Instead, either use autopilot from the source tree, or build debian packages instead. Instructions for building debian packages are below:
Assuming a current ubuntu installation, make sure you have the build tools that are required installed::
$ sudo apt-get install devscripts equivs bzr-builddeb
Then install the build-dependencies for the autopilot packages::
$ sudo mk-build-deps -i
Then build the debian packages::
$ bzr bd
bzr-builddeb will build binary packages into the parent directory, or into '../build-area/' if you do not own the correct gpg key to sign the package. The resulting ``.deb`` files can be installed as normal with ``sudo dpkg -i package.deb``.
The documentation can be built separately from the debian packages::
$ python3 setup.py build_sphinx
The docs are built into 'build/sphinx/html' and can be opened in the default browser with::
$ xdg-open build/sphinx/html/index.html
Running and Listing tests
=========================
Normally running autopilot tests is as easy as::
$ autopilot3 run
There are some complexities when attempting to run the autopilot tests from
within a development branch (this related to how autopilot modules are loaded
and then used to attempt to collect the required tests).
For that reason when running autopilot's tests while hacking on it it is
advised to run autopilot in this manner::
$ python3 -m autopilot.run run autopilot.tests.unit
Listing is similar::
$ python3 -m autopilot.run list autopilot.tests.unit
For a more complete explanation for running or listing tests please see the
full documentation found here:
https://developer.ubuntu.com/api/autopilot/python/1.6.0/guides-running_ap/
If you are in the root of the autopilot source tree this will run/list the tests from
within that local module. Otherwise autopilot will look in the system python path.
Release Autopilot
=================
1. Open a new request on bileto: https://bileto.ubuntu.com/ with the lp:autopilot -> lp:autopilot/1.5 merge proposal
2. Add the relevant details (i.e. bug fix details in the landing description and a link to the testplan: https://wiki.ubuntu.com/Process/Merges/TestPlan/autopilot)
3. Build the silo and run the tests
4. Once happy with all tests approve and publish the result
Release Manual Tests
====================
Not all our tests are automated at the moment. Specifically, the vis tool is lacking some automated tests due to deficiancies in other packages. Until we remedy this situation, the following things need to be manually tested upon an autopilot release:
- Run the following tests by running both: ``autopilot vis`` and ``autopilot3 vis``.
- Run 'window-mocker -testability' and the vis tool.
- Make sure you can select window-mocker from the connection list.
- Make sure the top-level tree node is 'window-mocker'
- Run the vis tool with the '-testability' flag enabled. Run a second vis tool, and make sure that the second vis tool can introspect the first.
- Make sure that the component overlay feature highlights the selected item in the tree view, as long as it has a globalRect.
- Make sure that searching works:
- Searching narrows down tree view to just the nodes that match the search criteria.
- Searching turns off the current component overlay (if any).
- Resetting the search restores the tree view to the full tree.
- Resetting the search turns off the current component overlay (if any).
./icons/ 0000755 0000041 0000041 00000000000 13061552435 012362 5 ustar www-data www-data ./icons/autopilot.svg 0000644 0000041 0000041 00000246245 13061552435 015140 0 ustar www-data www-data
./icons/autopilot-toggle-overlay.svg 0000644 0000041 0000041 00000003664 13061552435 020072 0 ustar www-data www-data
./docs/ 0000755 0000041 0000041 00000000000 13061552435 012177 5 ustar www-data www-data ./docs/tutorial/ 0000755 0000041 0000041 00000000000 13061552435 014042 5 ustar www-data www-data ./docs/tutorial/getting_started.rst 0000644 0000041 0000041 00000044322 13061552435 017770 0 ustar www-data www-data Writing Your First Test
#######################
This document contains everything you need to know to write your first autopilot test. It covers writing several simple tests for a sample Qt5/Qml application. However, it's important to note that nothing in this tutorial is specific to Qt5/Qml, and will work equally well with any other kind of application.
Files and Directories
=====================
Your autopilot test suite will grow to several files, possibly spread across several directories. We recommend that you follow this simple directory layout::
autopilot/
autopilot//
autopilot//tests/
The ``autopilot`` folder can be anywhere within your project's source tree. It will likely contain a `setup.py `_ file.
The ``autopilot//`` folder is the base package for your autopilot tests. This folder, and all child folders, are python packages, and so must contain an `__init__.py file `_. If you ever find yourself writing custom proxy classes (This is an advanced topic, and is covered here: :ref:`custom_proxy_classes`), they should be imported from this top-level package.
Each test file should be named ``test_.py``, where ** is the logical component you are testing in that file. Test files must be written in the ``autopilot//tests/`` folder.
A Minimal Test Case
+++++++++++++++++++
Autopilot tests follow a similar pattern to other python test libraries: you must declare a class that derives from :class:`~autopilot.testcase.AutopilotTestCase`. A minimal test case looks like this::
from autopilot.testcase import AutopilotTestCase
class MyTests(AutopilotTestCase):
def test_something(self):
"""An example test case that will always pass."""
self.assertTrue(True)
.. otto:: **Make your tests expressive!**
It's important to make sure that your tests express your *intent* as clearly as possible. We recommend choosing long, descriptive names for test functions and classes (even breaking :pep:`8`, if you need to), and give your tests a detailed docstring explaining exactly what you are trying to test. For more detailed advice on this point, see :ref:`write-expressive-tests`
The Setup Phase
===============
Before each test is run, the ``setUp`` method is called. Test authors may override this method to run any setup that needs to happen before the test is run. However, care must be taken when using the ``setUp`` method: it tends to hide code from the test case, which can make your tests less readable. It is our recommendation, therefore, that you use this feature sparingly. A more suitable alternative is often to put the setup code in a separate function or method and call it from the test function.
Should you wish to put code in a setup method, it looks like this:
.. code-block:: python
from autopilot.testcase import AutopilotTestCase
class MyTests(AutopilotTestCase):
def setUp(self):
super(MyTests, self).setUp()
# This code gets run before every test!
def test_something(self):
"""An example test case that will always pass."""
self.assertTrue(True)
.. note::
Any action you take in the setup phase must be undone if it alters the system state. See :ref:`cleaning-up` for more details.
Starting the Application
++++++++++++++++++++++++
At the start of your test, you need to tell autopilot to launch your application. To do this, call :meth:`~autopilot.testcase.AutopilotTestCase.launch_test_application`. The minimum required argument to this method is the application name or path. If you pass in the application name, autopilot will look in the current working directory, and then will search the :envvar:`PATH` environment variable. Otherwise, autopilot looks for the executable at the path specified. Positional arguments to this method are passed to the executable being launched.
Autopilot will try and guess what type of application you are launching, and therefore what kind of introspection libraries it should load. Sometimes autopilot will need some assistance however. For example, at the time of writing, autopilot cannot automatically detect the introspection type for python / Qt4 applications. In that case, a :class:`RuntimeError` will be raised. To provide autopilot with a hint as to which introspection type to load, you can provide the ``app_type`` keyword argument. For example::
class MyTests(AutopilotTestCase):
def test_python_qt4_application(self):
self.app = self.launch_test_application(
'my-pyqt4-app',
app_type='qt'
)
See the documentation for :meth:`~autopilot.testcase.AutopilotTestCase.launch_test_application` for more details.
The return value from :meth:`~autopilot.testcase.AutopilotTestCase.launch_test_application` is a proxy object representing the root of the introspection tree of the application you just launched.
.. otto:: **What is a Proxy Object?**
Whenever you launch an application, autopilot gives you a "proxy object". These are instances of the :class:`~autopilot.introspection.ProxyBase` class, with all the data from your application mirrored in the proxy object instances. For example, if you have a proxy object for a push button class (say, ``QPushButton``, for example), the proxy object will have attribute to match every attribute in the class within your application. Autopilot automatically keeps the data in these instances up to date, so you can use them in your test assertions.
User interfaces are made up of a tree of widgets, and autopilot represents these widgets as a tree of proxy objects. Proxy objects have a number of methods on them for selecting child objects in the introspection tree, so test authors can easily inspect the parts of the UI tree they care about.
A Simple Test
=============
To demonstrate the material covered so far, this selection will outline a simple application, and a single test for it. Instead of testing a third-party application, we will write the simplest possible application in Python and Qt4. The application, named 'testapp.py', is listed below::
#!/usr/bin/env python
from PyQt4 import QtGui
from sys import argv
def main():
app = QtGui.QApplication(argv)
win = QtGui.QMainWindow()
win.show()
win.setWindowTitle("Hello World")
app.exec_()
if __name__ == '__main__':
main()
As you can see, this is a trivial application, but it serves our purpose. For the upcoming tests to run this file must be executable::
$ chmod u+x testapp.py
We will write a single autopilot test that asserts that the title of the main window is equal to the string "Hello World". Our test file is named "test_window.py", and contains the following code::
from autopilot.testcase import AutopilotTestCase
from os.path import abspath, dirname, join
from testtools.matchers import Equals
class MainWindowTitleTests(AutopilotTestCase):
def launch_application(self):
"""Work out the full path to the application and launch it.
This is necessary since our test application will not be in $PATH.
:returns: The application proxy object.
"""
full_path = abspath(join(dirname(__file__), '..', '..', 'testapp.py'))
return self.launch_test_application(full_path, app_type='qt')
def test_main_window_title_string(self):
"""The main window title must be 'Hello World'."""
app_root = self.launch_application()
main_window = app_root.select_single('QMainWindow')
self.assertThat(main_window.windowTitle, Equals("Hello World"))
Note that we have made the test method as readable as possible by hiding the complexities of finding the full path to the application we want to test. Of course, if you can guarantee that the application is in :envvar:`PATH`, then this step becomes a lot simpler.
The entire directory structure looks like this::
./example/__init__.py
./example/tests/__init__.py
./example/tests/test_window.py
./testapp.py
The ``__init__.py`` files are empty, and are needed to make these directories importable by python.
Running Autopilot
+++++++++++++++++
From the root of this directory structure, we can ask autopilot to list all the tests it can find::
$ autopilot3 list example
Loading tests from: /home/thomi/code/canonical/autopilot/example_test
example.tests.test_window.MainWindowTitleTests.test_main_window_title_string
1 total tests.
Note that on the first line, autopilot will tell you where it has loaded the test definitions from. Autopilot will look in the current directory for a python package that matches the package name specified on the command line. If it does not find any suitable packages, it will look in the standard python module search path instead.
To run our test, we use the autopilot 'run' command::
$ autopilot3 run example
Loading tests from: /home/thomi/code/canonical/autopilot/example_test
Tests running...
Ran 1 test in 2.342s
OK
You will notice that the test application launches, and then dissapears shortly afterwards. Since this test doesn't manipulate the application in any way, this is a rather boring test to look at. If you ever want more output from the run command, you may specify the '-v' flag::
$ autopilot3 run -v example
Loading tests from: /home/thomi/code/canonical/autopilot/example_test
Tests running...
13:41:11.614 INFO globals:49 - ************************************************************
13:41:11.614 INFO globals:50 - Starting test example.tests.test_window.MainWindowTitleTests.test_main_window_title_string
13:41:11.693 INFO __init__:136 - Launching process: ['/home/thomi/code/canonical/autopilot/example_test/testapp.py', '-testability']
13:41:11.699 INFO __init__:169 - Looking for autopilot interface for PID 12013 (and children)
13:41:11.727 WARNING __init__:185 - Caught exception while searching for autopilot interface: 'DBusException("Could not get PID of name 'org.freedesktop.DBus': no such name",)'
13:41:12.773 WARNING __init__:185 - Caught exception while searching for autopilot interface: 'DBusException("Could not get PID of name 'org.freedesktop.DBus': no such name",)'
13:41:12.848 WARNING __init__:185 - Caught exception while searching for autopilot interface: 'RuntimeError("Could not find Autopilot interface on DBus backend ''",)'
13:41:12.852 WARNING __init__:185 - Caught exception while searching for autopilot interface: 'RuntimeError("Could not find Autopilot interface on DBus backend ''",)'
13:41:12.863 WARNING dbus:464 - Generating introspection instance for type 'Root' based on generic class.
13:41:12.864 DEBUG dbus:338 - Selecting objects of type QMainWindow with attributes: {}
13:41:12.871 WARNING dbus:464 - Generating introspection instance for type 'QMainWindow' based on generic class.
13:41:12.886 INFO testcase:380 - waiting for process to exit.
13:41:13.983 INFO testresult:35 - OK: example.tests.test_window.MainWindowTitleTests.test_main_window_title_string
Ran 1 test in 2.370s
OK
You may also specify '-v' twice for even more output (this is rarely useful for test authors however).
Both the 'list' and 'run' commands take a test id as an argument. You may be as generic, or as specific as you like. In the examples above, we will list and run all tests in the 'example' package (i.e.- all tests), but we could specify a more specific run criteria if we only wanted to run some of the tests. For example, to only run the single test we've written, we can execute::
$ autopilot3 run example.tests.test_window.MainWindowTitleTests.test_main_window_title_string
.. _tut_test_with_interaction:
A Test with Interaction
=======================
Now lets take a look at some simple tests with some user interaction. First, update the test application with some input and output controls::
#!/usr/bin/env python
# File: testapp.py
from PyQt4 import QtGui
from sys import argv
class AutopilotHelloWorld(QtGui.QWidget):
def __init__(self):
super(AutopilotHelloWorld, self).__init__()
self.hello = QtGui.QPushButton("Hello")
self.hello.clicked.connect(self.say_hello)
self.goodbye = QtGui.QPushButton("Goodbye")
self.goodbye.clicked.connect(self.say_goodbye)
self.response = QtGui.QLabel("Response: None")
grid = QtGui.QGridLayout()
grid.addWidget(self.hello, 0, 0)
grid.addWidget(self.goodbye, 0, 1)
grid.addWidget(self.response, 1, 0, 1, 2)
self.setLayout(grid)
self.show()
self.setWindowTitle("Hello World")
def say_hello(self):
self.response.setText('Response: Hello')
def say_goodbye(self):
self.response.setText('Response: Goodbye')
def main():
app = QtGui.QApplication(argv)
ahw = AutopilotHelloWorld()
app.exec_()
if __name__ == '__main__':
main()
We've reorganized the application code into a class to make the event handling easier. Then we added two input controls, the ``hello`` and ``goodbye`` buttons and an output control, the ``response`` label.
The operation of the application is still very trivial, but now we can test that it actually does something in response to user input. Clicking either of the two buttons will cause the response text to change. Clicking the ``Hello`` button should result in ``Response: Hello`` while clicking the ``Goodbye`` button should result in ``Response: Goodbye``.
Since we're adding a new category of tests, button response tests, we should organize them into a new class. Our tests module now looks like::
from autopilot.testcase import AutopilotTestCase
from os.path import abspath, dirname, join
from testtools.matchers import Equals
from autopilot.matchers import Eventually
class HelloWorldTestBase(AutopilotTestCase):
def launch_application(self):
"""Work out the full path to the application and launch it.
This is necessary since our test application will not be in $PATH.
:returns: The application proxy object.
"""
full_path = abspath(join(dirname(__file__), '..', '..', 'testapp.py'))
return self.launch_test_application(full_path, app_type='qt')
class MainWindowTitleTests(HelloWorldTestBase):
def test_main_window_title_string(self):
"""The main window title must be 'Hello World'."""
app_root = self.launch_application()
main_window = app_root.select_single('AutopilotHelloWorld')
self.assertThat(main_window.windowTitle, Equals("Hello World"))
class ButtonResponseTests(HelloWorldTestBase):
def test_hello_response(self):
"""The response text must be 'Response: Hello' after a Hello click."""
app_root = self.launch_application()
response = app_root.select_single('QLabel')
hello = app_root.select_single('QPushButton', text='Hello')
self.mouse.click_object(hello)
self.assertThat(response.text, Eventually(Equals('Response: Hello')))
def test_goodbye_response(self):
"""The response text must be 'Response: Goodbye' after a Goodbye
click."""
app_root = self.launch_application()
response = app_root.select_single('QLabel')
goodbye = app_root.select_single('QPushButton', text='Goodbye')
self.mouse.click_object(goodbye)
self.assertThat(response.text, Eventually(Equals('Response: Goodbye')))
In addition to the new class, ``ButtonResponseTests``, you'll notice a few other changes. First, two new import lines were added to support the new tests. Next, the existing ``MainWindowTitleTests`` class was refactored to subclass from a base class, ``HelloWorldTestBase``. The base class contains the ``launch_application`` method which is used for all test cases. Finally, the object type of the main window changed from ``QMainWindow`` to ``AutopilotHelloWorld``. The change in object type is a result of our test application being refactored into a class called ``AutopilotHelloWorld``.
.. otto:: **Be careful when identifing user interface controls**
Notice that our simple refactoring of the test application forced a change to the test for the main window. When developing application code, put a little extra thought into how the user interface controls will be identified in the tests. Identify objects with attributes that are likely to remain constant as the application code is developed.
The ``ButtonResponseTests`` class adds two new tests, one for each input control. Each test identifies the user interface controls that need to be used, performs a single, specific action, and then verifies the outcome. In ``test_hello_response``, we first identify the ``QLabel`` control which contains the output we need to check. We then identify the ``Hello`` button. As the application has two ``QPushButton`` controls, we must further refine the ``select_single`` call by specifing an additional property. In this case, we use the button text. Next, an input action is triggered by instructing the ``mouse`` to click the ``Hello`` button. Finally, the test asserts that the response label text matches the expected string. The second test repeats the same process with the ``Goodbye`` button.
The Eventually Matcher
======================
Notice that in the ButtonResponseTests tests above, the autopilot method :class:`~autopilot.matchers.Eventually` is used in the assertion. This allows the assertion to be retried continuously until it either becomes true, or times out (the default timout is 10 seconds). This is necessary because the application and the autopilot tests run in different processes. Autopilot could test the assert before the application has completed its action. Using :class:`~autopilot.matchers.Eventually` allows the application to complete its action without having to explicitly add delays to the tests.
.. otto:: **Use Eventually when asserting any user interface condition**
You may find that when running tests, the application is often ready with the outcome by the time autopilot is able to test the assertion without using :class:`~autopilot.matchers.Eventually`. However, this may not always be true when running your test suite on different hardware.
.. TODO: Continue to discuss the issues with running tests & application in separate processes, and how the Eventually matcher helps us overcome these problems. Cover the various ways the matcher can be used.
./docs/tutorial/what_is_autopilot.rst 0000644 0000041 0000041 00000011177 13061552435 020341 0 ustar www-data www-data What is Autopilot, and what can it do?
######################################
Autopilot is a tool for writing functional tests. Functional tests are tests that:
* Run out-of-process. I.e.- the tests run in a separate process to the application under test.
* Simulate user interaction. Autopilot provides methods to generate keyboard, mouse, and touch events. These events are delivered to the application under test in exactly the same way as normal input events. The application under test therefore cannot distinguish between a "real" user and an autopilot test case.
* Validate design decisions. The primary function of a functional test is to determine whether of not an application has met the design criteria. Functional tests evaluate high-level design correctness.
Where is Autopilot used?
########################
Autopilot was designed to test the `Unity 3D `_ shell. However, since then it has been used to test a number of other applications, including:
* Core Ubuntu GUI applications.
* Mobile phone applications for the Ubuntu Phone & Ubuntu Tablet.
How does Autopilot fit with other test frameworks?
##################################################
.. image:: /images/test_pyramid.*
Autopilot exists at the apex of the "testing pyramid". It is designed to test high-level functionality, and complement a solid base of unit and integration tests. *Using autopilot is not a substitute for testing your application with unit and integration tests!*. Autopilot is a very capable tool for testing high-level feature functionality. It is not an appropriate tool for testing low-level implementation details.
Autopilot is built on top of several other python test frameworks, including:
* `Python Testtools `_ - :class:`~autopilot.testcase.AutopilotTestCase` derives from the testtools :class:`~testtools.TestCase` class, which allows test author to use all the extended features found in testtools. Specifically, Autopilot includes the :class:`~autopilot.matchers.Eventually` matcher class, which allows test authors to make assertions about the application under test without having to worry about the timing between the tests and the application under test.
* `Python Test Scenarios `_ - :class:`~autopilot.testcase.AutopilotTestCase` contains the necessary plumbing in order to allow test authors to use test scenarios out of the box. This is extremely useful when you want to test several different modes of operation.
* `Python Test Fixtures `_ - Several parts of autopilot are built as fixtures. While this is rarely exposed to the test author, it can be useful to know that this functionality is always present whenever autopilot is installed.
What do Autopilot Tests Contain?
################################
A typical autopilot test has three distinct stages:
.. image:: /images/test_stages.*
**The Setup Stage**
There are several concerns that must be addressed in the setup Phase. The most important step is to launch the application to be tested. Most autopilot test suites launch the application under test anew for each test. This ensures that the test starts with the application under test in a known, clean state. Autopilot can launch normal applications, launch applications via upstart, or launch apps contained within a click package.
Tests may also wish to take other actions in the setup stage, including:
* Setting environment variables to certain values.
* Starting external applications that are required for the test to run.
* Creating files or folders (or any kind of external data) on disk.
The purpose of the setup stage is to make sure that everything that is required for the test to run is in place.
**The Interaction Stage**
Once the setup has been completed, it's time to start interacting with your application. This typically involves generating input events. For example, if you are testing a text editor you might have a test whose specification is similar to the following::
Type some text into the document area, open the 'Edit' menu and click
the 'Search and Replace' menu item.
During this stage you will most likely need to read the applications internal state. For example, your test will need to know where the 'Edit' menu is on the screen. Thankfully, autopilot takes care of the details, allowing you to write expressive tests.
**The Assertion Stage**
The final stage is where you determine if your test should pass or fail. Most tests will contain more than one assertion (:ref:`why? `). Autopilot contains several custom assertions that make testing high-level concepts easier.
./docs/tutorial/advanced_autopilot.rst 0000644 0000041 0000041 00000105712 13061552435 020447 0 ustar www-data www-data Advanced Autopilot Features
###########################
This document covers advanced features in autopilot.
.. _cleaning-up:
Cleaning Up
===========
It is vitally important that every test you run leaves the system in exactly the same state as it found it. This means that:
* Any files written to disk need to be removed.
* Any environment variables set during the test run need to be un-set.
* Any applications opened during the test run need to be closed again.
* Any :class:`~autopilot.input.Keyboard` keys pressed during the test need to be released again.
All of the methods on :class:`~autopilot.testcase.AutopilotTestCase` that alter the system state will automatically revert those changes at the end of the test. Similarly, the various input devices will release any buttons or keys that were pressed during the test. However, for all other changes, it is the responsibility of the test author to clean up those changes.
For example, a test might require that a file with certain content be written to disk at the start of the test. The test case might look something like this::
class MyTests(AutopilotTestCase):
def make_data_file(self):
open('/tmp/datafile', 'w').write("Some data...")
def test_application_opens_data_file(self):
"""Our application must be able to open a data file from disk."""
self.make_data_file()
# rest of the test code goes here
However this will leave the :file:`/tmp/datafile` on disk after the test has finished. To combat this, use the :meth:`addCleanup` method. The arguments to :meth:`addCleanup` are a callable, and then zero or more positional or keyword arguments. The Callable will be called with the positional and keyword arguments after the test has ended.
Cleanup actions are called in the reverse order in which they are added, and are called regardless of whether the test passed, failed, or raised an uncaught exception. To fix the above test, we might write something similar to::
import os
class MyTests(AutopilotTestCase):
def make_data_file(self):
open('/tmp/datafile', 'w').write("Some data...")
self.addCleanup(os.remove, '/tmp/datafile')
def test_application_opens_data_file(self):
"""Our application must be able to open a data file from disk."""
self.make_data_file()
# rest of the test code goes here
Note that by having the code to generate the ``/tmp/datafile`` file on disk in a separate method, the test itself can ignore the fact that these resources need to be cleaned up. This makes the tests cleaner and easier to read.
Test Scenarios
==============
Occasionally test authors will find themselves writing multiple tests that differ in one or two subtle ways. For example, imagine a hypothetical test case that tests a dictionary application. The author wants to test that certain words return no results. Without using test scenarios, there are two basic approaches to this problem. The first is to create many test cases, one for each specific scenario (*don't do this*)::
class DictionaryResultsTests(AutopilotTestCase):
def test_empty_string_returns_no_results(self):
self.dictionary_app.enter_search_term("")
self.assertThat(len(self.dictionary_app.results), Equals(0))
def test_whitespace_string_returns_no_results(self):
self.dictionary_app.enter_search_term(" \t ")
self.assertThat(len(self.dictionary_app.results), Equals(0))
def test_punctuation_string_returns_no_results(self):
self.dictionary_app.enter_search_term(".-?<>{}[]")
self.assertThat(len(self.dictionary_app.results), Equals(0))
def test_garbage_string_returns_no_results(self):
self.dictionary_app.enter_search_term("ljdzgfhdsgjfhdgjh")
self.assertThat(len(self.dictionary_app.results), Equals(0))
The main problem here is that there's a lot of typing in order to change exactly one thing (and this hypothetical test is deliberately short, to ease clarity. Imagine a 100 line test case!). Another approach is to make the entire thing one large test (*don't do this either*)::
class DictionaryResultsTests(AutopilotTestCase):
def test_bad_strings_returns_no_results(self):
bad_strings = ("",
" \t ",
".-?<>{}[]",
"ljdzgfhdsgjfhdgjh",
)
for input in bad_strings:
self.dictionary_app.enter_search_term(input)
self.assertThat(len(self.dictionary_app.results), Equals(0))
This approach makes it easier to add new input strings, but what happens when just one of the input strings stops working? It becomes very hard to find out which input string is broken, and the first string that breaks will prevent the rest of the test from running, since tests stop running when the first assertion fails.
The solution is to use test scenarios. A scenario is a class attribute that specifies one or more scenarios to run on each of the tests. This is best demonstrated with an example::
class DictionaryResultsTests(AutopilotTestCase):
scenarios = [
('empty string', {'input': ""}),
('whitespace', {'input': " \t "}),
('punctuation', {'input': ".-?<>{}[]"}),
('garbage', {'input': "ljdzgfhdsgjfhdgjh"}),
]
def test_bad_strings_return_no_results(self):
self.dictionary_app.enter_search_term(self.input)
self.assertThat(len(self.dictionary_app.results), Equals(0))
Autopilot will run the ``test_bad_strings_return_no_results`` once for each scenario. On each test, the values from the scenario dictionary will be mapped to attributes of the test case class. In this example, that means that the 'input' dictionary item will be mapped to ``self.input``. Using scenarios has several benefits over either of the other strategies outlined above:
* Tests that use strategies will appear as separate tests in the test output. The test id will be the normal test id, followed by the strategy name in parenthesis. So in the example above, the list of test ids will be::
DictionaryResultsTests.test_bad_strings_return_no_results(empty string)
DictionaryResultsTests.test_bad_strings_return_no_results(whitespace)
DictionaryResultsTests.test_bad_strings_return_no_results(punctuation)
DictionaryResultsTests.test_bad_strings_return_no_results(garbage)
* Since scenarios are treated as separate tests, it's easier to debug which scenario has broken, and re-run just that one scenario.
* Scenarios get applied before the ``setUp`` method, which means you can use scenario values in the ``setUp`` and ``tearDown`` methods. This makes them more flexible than either of the approaches listed above.
.. TODO: document the use of the multiply_scenarios feature.
Test Logging
============
Autopilot integrates the `python logging framework `_ into the :class:`~autopilot.testcase.AutopilotTestCase` class. Various autopilot components write log messages to the logging framework, and all these log messages are attached to each test result when the test completes. By default, these log messages are shown when a test fails, or if autopilot is run with the ``-v`` option.
Test authors are encouraged to write to the python logging framework whenever doing so would make failing tests clearer. To do this, there are a few simple steps to follow:
1. Import the logging module::
import logging
2. Create a ``logger`` object. You can either do this at the file level scope, or within a test case class::
logger = logging.getLogger(__name__)
3. Log some messages. You may choose which level the messages should be logged at. For example::
logger.debug("This is debug information, not shown by default.")
logger.info("This is some information")
logger.warning("This is a warning")
logger.error("This is an error")
.. note:: To view log messages when using ``debug`` level of logging pass ``-vv`` when running autopilot.
For more information on the various logging levels, see the `python documentation on Logger objects `_. All messages logged in this way will be picked up by the autopilot test runner. This is a valuable tool when debugging failing tests.
Environment Patching
====================
Sometimes you need to change the value of an environment variable for the duration of a single test. It is important that the variable is changed back to it's original value when the test has ended, so future tests are run in a pristine environment. The :mod:`fixtures` module includes a :class:`fixtures.EnvironmentVariable` fixture which takes care of this for you. For example, to set the ``FOO`` environment variable to ``"Hello World"`` for the duration of a single test, the code would look something like this::
from fixtures import EnvironmentVariable
from autopilot.testcase import AutopilotTestCase
class MyTests(AutopilotTestCase):
def test_that_needs_custom_environment(self):
self.useFixture(EnvironmentVariable("FOO", "Hello World"))
# Test code goes here.
The :class:`fixtures.EnvironmentVariable` fixture will revert the value of the environment variable to it's initial value, or will delete it altogether if the environment variable did not exist when :class:`fixtures.EnvironmentVariable` was instantiated. This happens in the cleanup phase of the test execution.
.. _custom_assertions:
Custom Assertions
=================
Autopilot provides additional custom assertion methods within the :class:`~autopilot.testcase.AutopilotTestCase` base class. These assertion methods can be used for validating the visible window stack and also properties on objects whose attributes do not have the ``wait_for`` method, such as :class:`~autopilot.process.Window` objects (See :ref:`wait_for` for more information about ``wait_for``).
:py:mod:`autopilot.testcase.AutopilotTestCase.assertVisibleWindowStack`
This assertion allows the test to check the start of the visible window stack by passing an iterable item of :class:`~autopilot.process.Window` instances. Minimised windows will be ignored::
from autopilot.process import ProcessManager
from autopilot.testcase import AutopilotTestCase
class WindowTests(AutopilotTestCase):
def test_window_stack(self):
self.launch_some_test_apps()
pm = ProcessManager.create()
test_app_windows = []
for window in pm.get_open_windows():
if self.is_test_app(window.name):
test_app_windows.append(window)
self.assertVisibleWindowStack(test_app_windows)
.. note:: The process manager is only available on environments that use bamf, i.e. desktop running Unity 7. There is currently no process manager for any other platform.
.. _custom_assertions_assertProperty:
:py:mod:`autopilot.testcase.AutopilotTestCase.assertProperty`
This assertion allows the test to check properties of an object that does not have a **wait_for** method (i.e.- objects that do not come from the autopilot DBus interface). For example the :py:mod:`~autopilot.process.Window` object::
from autopilot.process import ProcessManager
from autopilot.testcase import AutopilotTestCase
class WindowTests(AutopilotTestCase):
def test_window_stack(self):
self.launch_some_test_apps()
pm = ProcessManager.create()
for window in pm.get_open_windows():
if self.is_test_app(window.name):
self.assertProperty(window, is_maximized=True)
.. note:: :py:mod:`~autopilot.testcase.AutopilotTestCase.assertProperties` is a synonym for this method.
.. note:: The process manager is only available on environments that use bamf, i.e. desktop running Unity 7. There is currently no process manager for any other platform.
:py:mod:`autopilot.testcase.AutopilotTestCase.assertProperties`
See :ref:`autopilot.testcase.AutopilotTestCase.assertProperty `.
.. note:: :py:mod:`~autopilot.testcase.AutopilotTestCase.assertProperty` is a synonym for this method.
.. _platform_selection:
Platform Selection
==================
.. Document the methods we have to get information about the platform we're running on, and how we can skip tests based on this information.
Autopilot provides functionality that allows the test author to determine which
platform a test is running on so that they may either change behaviour within
the test or skipping the test all together.
For examples and API documentaion please see :py:mod:`autopilot.platform`.
.. _gestures_multitouch:
Gestures and Multi-touch
========================
Autopilot provides API support for both :ref:`single-touch ` and :ref:`multi-touch ` gestures which can be used to simulate user input required to drive an application or system under test. These APIs should be used in conjunction with :ref:`platform_selection` to detect platform capabilities and ensure the correct input API is being used.
.. _single_touch:
Single-Touch
++++++++++++
:class:`autopilot.input.Touch` provides single-touch input gestures, which includes:
* :meth:`~autopilot.input.Touch.tap` which can be used to tap a specified [x,y] point on the screen
* :meth:`~autopilot.input.Touch.drag` which will drag between 2 [x,y] points and can be customised by altering the speed of the action
* :meth:`~autopilot.input.Touch.press`, :meth:`~autopilot.input.Touch.release` and :meth:`~autopilot.input.Touch.move` operations which can be combined to create custom gestures
* :meth:`~autopilot.input.Touch.tap_object` can be used to tap the center point of a given introspection object, where the screen co-ordinates are taken from one of several properties of the object
Autopilot additionally provides the class :class:`autopilot.input.Pointer` as a means to provide a single unified API that can be used with both :class:`~autopilot.input.Mouse` input and :class:`~autopilot.input.Touch` input . See the :class:`documentation ` for this class for further details of this, as not all operations can be performed on both of these input types.
This example demonstrates swiping from the center of the screen to the left edge, which could for example be used in `Ubuntu Touch `_ to swipe a new scope into view.
1. First calculate the center point of the screen (see: :ref:`display_information`): ::
>>> from autopilot.display import Display
>>> display = Display.create()
>>> center_x = display.get_screen_width() // 2
>>> center_y = display.get_screen_height() // 2
2. Then perform the swipe operation from the center of the screen to the left edge, using :meth:`autopilot.input.Pointer.drag`: ::
>>> from autopilot.input import Touch, Pointer
>>> pointer = Pointer(Touch.create())
>>> pointer.drag(center_x, center_y, 0, center_y)
.. _multi_touch:
Multi-Touch
+++++++++++
:class:`autopilot.gestures` provides support for multi-touch input which includes:
* :meth:`autopilot.gestures.pinch` provides a 2-finger pinch gesture centered around an [x,y] point on the screen
This example demonstrates how to use the pinch gesture, which for example could be used on `Ubuntu Touch `_ web-browser, or gallery application to zoom in or out of currently displayed content.
1. To zoom in, pinch vertically outwards from the center point by 100 pixels: ::
>>> from autopilot import gestures
>>> gestures.pinch([center_x, center_y], [0, 0], [0, 100])
2. To zoom back out, pinch vertically 100 pixels back towards the center point: ::
>>> gestures.pinch([center_x, center_y], [0, 100], [0, 0])
.. note:: The multi-touch :meth:`~autopilot.gestures.pinch` method is intended for use on a touch enabled device. However, if run on a desktop environment it will behave as if the mouse select button is pressed whilst moving the mouse pointer. For example to select some text in a document.
.. _tut-picking-backends:
Advanced Backend Picking
========================
Several features in autopilot are provided by more than one backend. For example, the :mod:`autopilot.input` module contains the :class:`~autopilot.input.Keyboard`, :class:`~autopilot.input.Mouse` and :class:`~autopilot.input.Touch` classes, each of which can use more than one implementation depending on the platform the tests are being run on.
For example, when running autopilot on a traditional ubuntu desktop platform, :class:`~autopilot.input.Keyboard` input events are probably created using the X11 client libraries. On a phone platform, X11 is not present, so autopilot will instead choose to generate events using the kernel UInput device driver instead.
Other autopilot systems that make use of multiple backends include the :mod:`autopilot.display` and :mod:`autopilot.process` modules. Every class in these modules follows the same construction pattern:
Default Creation
++++++++++++++++
By default, calling the ``create()`` method with no arguments will return an instance of the class that is appropriate to the current platform. For example::
>>> from autopilot.input import Keyboard
>>> kbd = Keyboard.create()
The code snippet above will create an instance of the Keyboard class that uses X11 on Desktop systems, and UInput on other systems. On the rare occaison when test authors need to construct these objects themselves, we expect that the default creation pattern to be used.
.. _adv_picking_backend:
Picking a Backend
+++++++++++++++++
Test authors may sometimes want to pick a specific backend. The possible backends are documented in the API documentation for each class. For example, the documentation for the :meth:`autopilot.input.Keyboard.create` method says there are three backends available: the ``X11`` backend, the ``UInput`` backend, and the ``OSK`` backend. These backends can be specified in the create method. For example, to specify that you want a Keyboard that uses X11 to generate it's input events::
>>> from autopilot.input import Keyboard
>>> kbd = Keyboard.create("X11")
Similarly, to specify that a UInput keyboard should be created::
>>> from autopilot.input import Keyboard
>>> kbd = Keyboard.create("UInput")
Finally, for the Onscreen Keyboard::
>>> from autopilot.input import Keyboard
>>> kbd = Keyboard.create("OSK")
.. warning:: Care must be taken when specifying specific backends. There is no guarantee that the backend you ask for is going to be available across all platforms. For that reason, using the default creation method is encouraged.
.. warning:: The **OSK** backend has some known implementation limitations, please see :meth:`autopilot.input.Keyboard.create` method documenation for further details.
Possible Errors when Creating Backends
++++++++++++++++++++++++++++++++++++++
Lots of things can go wrong when creating backends with the ``create`` method.
If autopilot is unable to create any backends for your current platform, a :exc:`RuntimeError` exception will be raised. It's ``message`` attribute will contain the error message from each backend that autopilot tried to create.
If a preferred backend was specified, but that backend doesn't exist (probably the test author mis-spelled it), a :exc:`RuntimeError` will be raised::
>>> from autopilot.input import Keyboard
>>> try:
... kbd = Keyboard.create("uinput")
... except RuntimeError as e:
... print("Unable to create keyboard: " + str(e))
...
Unable to create keyboard: Unknown backend 'uinput'
In this example, ``uinput`` was mis-spelled (backend names are case sensitive). Specifying the correct backend name works as expected::
>>> from autopilot.input import Keyboard
>>> kbd = Keyboard.create("UInput")
Finally, if the test author specifies a preferred backend, but that backend could not be created, a :exc:`autopilot.BackendException` will be raised. This is an important distinction to understand: While calling ``create()`` with no arguments will try more than one backend, specifying a backend to create will only try and create that one backend type. The BackendException instance will contain the original exception raised by the backed in it's ``original_exception`` attribute. In this example, we try and create a UInput keyboard, which fails because we don't have the correct permissions (this is something that autopilot usually handles for you)::
>>> from autopilot.input import Keyboard
>>> from autopilot import BackendException
>>> try:
... kbd = Keyboard.create("UInput")
... except BackendException as e:
... repr(e.original_exception)
... repr(e)
...
'UInputError(\'"/dev/uinput" cannot be opened for writing\',)'
'BackendException(\'Error while initialising backend. Original exception was: "/dev/uinput" cannot be opened for writing\',)'
Keyboard Backends
=================
A quick introduction to the Keyboard backends
+++++++++++++++++++++++++++++++++++++++++++++
Each backend has a different method of operating behind the scenes to provide
the Keyboard interface.
Here is a quick overview of how each backend works.
.. list-table::
:widths: 15, 85
:header-rows: 1
* - Backend
- Description
* - X11
- The X11 backend generates X11 events using a mock input device which it
then syncs with X to actually action the input.
* - Uinput
- The UInput backend injects events directly in to the kernel using the
UInput device driver to produce input.
* - OSK
- The Onscreen Keyboard backend uses the GUI pop-up keyboard to enter
input. Using a pointer object it taps on the required keys to get the
expected output.
.. _keyboard_backend_limitations:
Limitations of the different Keyboard backends
++++++++++++++++++++++++++++++++++++++++++++++
While every effort has been made so that the Keyboard devices act the same
regardless of which backend or platform is in use, the simple fact is that
there can be some technical limitations for some backends.
Some of these limitations are hidden when using the "create" method and won't
cause any concern (e.g. X11 backend on desktop, UInput on an Ubuntu Touch device.)
while others will raise exceptions (that are fully documented in the API docs).
Here is a list of known limitations:
**X11**
* Only available on desktop platforms
- X11 isn't available on Ubuntu Touch devices
**UInput**
* Requires correct device access permissions
- The user (or group) that are running the autopilot tests need read/write
access to the UInput device (usually /dev/uinput).
* Specific kernel support is required
- The kernel on the system running the tests must be running a kernel that
includes UInput support (as well as have the module loaded.
**OSK**
* Currently only available on Ubuntu Touch devices
- At the time of writing this the OSK/Ubuntu Keyboard is only
supported/available on the Ubuntu Touch devices. It is possible that it
will be available on the desktop in the near future.
* Unable to type 'special' keys e.g. Alt
- This shouldn't be an issue as applications running on Ubuntu Touch devices
will be using the expected patterns of use on these platforms.
* The following methods have limitations or are not implemented:
- :meth:`autopilot.input.Keyboard.press`: Raises NotImplementedError if
called.
- :meth:`autopilot.input.Keyboard.release`: Raises NotImplementedError if
called.
- :meth:`autopilot.input.Keyboard.press_and_release`: can can only handle
single keys/characters. Raises either ValueError if passed more than a
single character key or UnsupportedKey if passed a key that is not
supported by the OSK backend (or the current language layout).
.. _process_control:
Process Control
===============
The :mod:`autopilot.process` module provides the :class:`~autopilot.process.ProcessManager` class to provide a high-level interface for managing applications and windows during testing. Features of the :class:`~autopilot.process.ProcessManager` allow the user to start and stop applications easily and to query the current state of an application and its windows. It also provides automatic cleanup for apps that have been launched during testing.
.. note:: :class:`~autopilot.process.ProcessManager` is not intended for introspecting an application's object tree, for this see :ref:`launching_applications`. Also it does not provide a method for interacting with an application's UI or specific features.
Properties of an application and its windows can be accessed using the classes :class:`~autopilot.process.Application` and :class:`~autopilot.process.Window`, which also allows the window instance to be focused and closed.
A list of known applications is defined in :meth:`~autopilot.process.ProcessManager.KNOWN_APPS` and these can easily be referenced by name. This list can also be updated using :meth:`~autopilot.process.ProcessManager.register_known_application` and :meth:`~autopilot.process.ProcessManager.unregister_known_application` for easier use during the test.
To use the :class:`~autopilot.process.ProcessManager` the static :meth:`~autopilot.process.ProcessManager.create` method should be called, which returns an initialised object instance.
A simple example to launch the gedit text editor and check it is in focus: ::
from autopilot.process import ProcessManager
from autopilot.testcase import AutopilotTestCase
class ProcessManagerTestCase(AutopilotTestCase):
def test_launch_app(self):
pm = ProcessManager.create()
app_window = pm.start_app_window('Text Editor')
app_window.set_focus()
self.assertTrue(app_window.is_focused)
.. note:: :class:`~autopilot.process.ProcessManager` is only available on environments that use bamf, i.e. desktop running Unity 7. There is currently no process manager for any other platform.
.. _display_information:
Display Information
===================
Autopilot provides the :mod:`autopilot.display` module to get information about the displays currently being used. This information can be used in tests to implement gestures or input events that are specific to the current test environment. For example a test could be run on a desktop environment with multiple screens, or on a variety of touch devices that have different screen sizes.
The user must call the static :meth:`~autopilot.display.Display.create` method to get an instance of the :class:`~autopilot.display.Display` class.
This example shows how to get the size of each available screen, which could be used to calculate coordinates for a swipe or input event (See the :mod:`autopilot.input` module for more details about generating input events).::
from autopilot.display import Display
display = Display.create()
for screen in range(0, display.get_num_screens()):
width = display.get_screen_width(screen)
height = display.get_screen_height(screen)
print('screen {0}: {1}x{2}'.format(screen, width, height))
.. _custom_proxy_classes:
Writing Custom Proxy Classes
============================
By default, autopilot will generate an object for every introspectable item in your application under test. These are generated on the fly, and derive from
:class:`~autopilot.introspection.ProxyBase`. This gives you the usual methods of selecting other nodes in the object tree, as well the the means to inspect all the properties in that class.
However, sometimes you want to customize the class used to create these objects. The most common reason to want to do this is to provide methods that make it easier to inspect or interact with these objects. Autopilot allows test authors to provide their own custom classes, through a couple of simple steps:
1. First, you must define your own base class, to be used by all custom proxy objects in your test suite. This base class can be empty, but must derive from :class:`~autopilot.introspection.ProxyBase`. An example class might look like this::
from autopilot.introspection import ProxyBase
class CustomProxyObjectBase(ProxyBase):
"""A base class for all custom proxy objects within this test suite."""
For Ubuntu applications using Ubuntu UI Toolkit objects, you should derive your custom proxy object from UbuntuUIToolkitCustomProxyObjectBase. This base class is also derived from :class:`~autopilot.introspection.ProxyBase` and is used for all Ubuntu UI Toolkit custom proxy objects. So if you are introspecting objects from Ubuntu UI Toolkit then this is the base class to use.
2. Define the classes you want autopilot to use, instead of the default. The simplest method is to give the class the same name as the type you wish to override. For example, if you want to define your own custom class to be used every time autopilot generates an instance of a 'QLabel' object, the class definition would look like this::
class QLabel(CustomProxyObjectBase):
# Add custom methods here...
If you wish to implement more specific selection criteria, your class can override the validate_dbus_object method, which takes as arguments the dbus path and state. For example::
class SpecificQLabel(CustomProxyObjectBase):
def validate_dbus_object(path, state):
return (path.endswith('object_we_want') or
state['some_property'] == 'desired_value')
This method should return True if the object matches this custom proxy class, and False otherwise. If more than one custom proxy class matches an object, a :exc:`ValueError` will be raised at runtime.
An example using Ubuntu UI Toolkit which would be used to swipe up a PageWithBottomEdge object to reveal it's bottom edge menu could look like this::
import ubuntuuitoolkit
class PageWithBottomEdge(ubuntuuitoolkit.UbuntuUIToolkitCustomProxyObjectBase):
"""An emulator class that makes it easy to interact with the bottom edge
swipe page"""
def reveal_bottom_edge_page(self):
"""Swipe up from the bottom edge of the Page
to reveal it's bottom edge menu."""
3. Pass the custom proxy base class as an argument to the launch_test_application method on your test class. This base class should be the same base class that is used to write all of your custom proxy objects::
from autopilot.testcase import AutopilotTestCase
class TestCase(AutopilotTestCase):
def setUp(self):
super().setUp()
self.app = self.launch_test_application(
'/path/to/the/application',
emulator_base=CustomProxyObjectBase)
For applications using objects from Ubuntu UI Toolkit, the emulator_base parameter should be::
emulator_base=ubuntuuitoolkit.UbuntuUIToolkitCustomProxyObjectBase
4. You can pass the custom proxy class to methods like :meth:`~autopilot.introspection.ProxyBase.select_single` instead of a string. So, for example, the following is a valid way of selecting the QLabel instances in an application::
# Get all QLabels in the applicaton:
labels = self.app.select_many(QLabel)
If you are introspecting an application that already has a custom proxy base class defined, then this class can simply be imported and passed to the appropriate application launcher method. See :ref:`launching applications ` for more details on launching an application for introspection. This will allow you to call all of the public methods of the application's proxy base class directly in your test.
This example will run on desktop and uses the webbrowser application to navigate to a url using the base class go_to_url() method::
from autopilot.testcase import AutopilotTestCase
from webbrowser_app.emulators import browser
class ClickAppTestCase(AutopilotTestCase):
def test_go_to_url(self):
app = self.launch_test_application(
'webbrowser-app',
emulator_base=browser.Webbrowser)
# main_window is a property of the Webbrowser class
app.main_window.go_to_url('http://www.ubuntu.com')
.. _launching_applications:
Launching Applications
======================
Applications can be launched inside of a testcase using the application launcher methods from the :class:`~autopilot.testcase.AutopilotTestCase` class. The exact method required will depend upon the type of application being launched:
* :meth:`~autopilot.testcase.AutopilotTestCase.launch_test_application` is used to launch regular executables
* :meth:`~autopilot.testcase.AutopilotTestCase.launch_upstart_application` is used to launch upstart-based applications
* :meth:`~autopilot.testcase.AutopilotTestCase.launch_click_package` is used to launch applications inside a `click package `_
This example shows how to launch an installed click application from within a test case::
from autopilot.testcase import AutopilotTestCase
class ClickAppTestCase(AutopilotTestCase):
def test_something(self):
app_proxy = self.launch_click_package('com.ubuntu.calculator')
Outside of testcase classes, the :class:`~autopilot.application.NormalApplicationLauncher`, :class:`~autopilot.application.UpstartApplicationLauncher`, and :class:`~autopilot.application.ClickApplicationLauncher` fixtures can be used, e.g.::
from autopilot.application import NormalApplicationLauncher
with NormalApplicationLauncher() as launcher:
launcher.launch('gedit')
or a similar example for an installed click package::
from autopilot.application import ClickApplicationLauncher
with ClickApplicationLauncher() as launcher:
app_proxy = launcher.launch('com.ubuntu.calculator')
Within a fixture or a testcase, ``self.useFixture`` can be used::
launcher = self.useFixture(NormalApplicationLauncher())
launcher.launch('gedit', ['--new-window', '/path/to/file'])
or for an installed click package::
launcher = self.useFixture(ClickApplicationLauncher())
app_proxy = launcher.launch('com.ubuntu.calculator')
Additional options can also be specified to set a custom addDetail method, a custom proxy base, or a custom dbus bus with which to patch the environment::
launcher = self.useFixture(NormalApplicationLauncher(
case_addDetail=self.addDetail,
dbus_bus='some_other_bus',
proxy_base=my_proxy_class,
))
.. note:: You must pass the test case's 'addDetail' method to these application launch fixtures if you want application logs to be attached to the test result. This is due to the way fixtures are cleaned up, and is unavoidable.
The main qml file of some click applications can also be launched directly from source. This can be done using the `qmlscene `_ application directly on the target application's main qml file. This example uses :meth:`~autopilot.testcase.AutopilotTestCase.launch_test_application` method from within a test case::
app_proxy = self.launch_test_application('qmlscene', 'application.qml', app_type='qt')
However, using this method it will not be possible to return an application specific custom proxy object, see :ref:`custom_proxy_classes`.
./docs/tutorial/tutorial.rst 0000644 0000041 0000041 00000000314 13061552435 016435 0 ustar www-data www-data Autopilot Tutorial
==================
This tutorial will guide users new to Autopilot through creating a minimal autopilot test.
.. toctree::
:maxdepth: 3
getting_started
advanced_autopilot
./docs/conf.py 0000644 0000041 0000041 00000021625 13061552435 013504 0 ustar www-data www-data # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Autopilot Functional Test Tool
# Copyright (C) 2012-2014 Canonical
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
# This file is execfile()d with the current directory set to its containing
# dir.
#
# Note that not all possible configuration values are present in this
# autogenerated file.
#
# All configuration values have a default; values that are commented out
# serve to show the default.
import os
import sys
# If extensions (or modules to document with autodoc) are in another
# directory, add these directories to sys.path here. If the directory is
# relative to the documentation root, use os.path.abspath to make it absolute,
# like shown here.
sys.path.insert(0, os.path.abspath('..'))
sys.path.insert(0, os.path.abspath('.'))
# -- General configuration ----------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
# needs_sphinx = '1.0'
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
extensions = [
'sphinx.ext.autodoc',
'sphinx.ext.graphviz',
'sphinx.ext.pngmath',
'sphinx.ext.viewcode',
'otto',
'man',
]
autodoc_member_order = 'bysource'
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# The suffix of source filenames.
source_suffix = '.rst'
# The encoding of source files.
# source_encoding = 'utf-8-sig'
# The master toctree document.
master_doc = 'index'
# General information about the project.
project = u'Autopilot'
copyright = u'2012-2014, Canonical'
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
version = '1.5'
# The full version, including alpha/beta/rc tags.
try:
from debian import changelog
chl = changelog.Changelog(open('../debian/changelog'))
release = str(chl.version).split('ubuntu')[0]
except ImportError:
# If we don't have python-debian installed, guess a coarse-grained version
# string
release = version
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
# language = None
# There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used:
# today = ''
# Else, today_fmt is used as the format for a strftime call.
# today_fmt = '%B %d, %Y'
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
exclude_patterns = ['_build']
# The reST default role (used for this markup: `text`) to use for all
# documents.
# default_role = None
# If true, '()' will be appended to :func: etc. cross-reference text.
add_function_parentheses = False
# If true, the current module name will be prepended to all description
# unit titles (such as .. function::).
# add_module_names = True
# If true, sectionauthor and moduleauthor directives will be shown in the
# output. They are ignored by default.
# show_authors = False
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
# A list of ignored prefixes for module index sorting.
# modindex_common_prefix = ['']
# nitpicky = True
# -- Options for HTML output --------------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
html_theme = 'nature'
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
# html_theme_options = {}
# Add any paths that contain custom themes here, relative to this directory.
# html_theme_path = []
# The name for this set of Sphinx documents. If None, it defaults to
# " v documentation".
# html_title = None
# A shorter title for the navigation bar. Default is the same as html_title.
# html_short_title = None
# The name of an image file (relative to this directory) to place at the top
# of the sidebar.
html_logo = './images/otto-64.png'
# The name of an image file (within the static path) to use as favicon of the
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
# pixels large.
html_favicon = './images/favicon.ico'
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
# using the given strftime format.
# html_last_updated_fmt = '%b %d, %Y'
# If true, SmartyPants will be used to convert quotes and dashes to
# typographically correct entities.
# html_use_smartypants = True
# Custom sidebar templates, maps document names to template names.
html_sidebars = {
'**': ['localtoc.html', 'searchbox.html', 'relations.html']
}
# Additional templates that should be rendered to pages, maps page names to
# template names.
html_additional_pages = {
'index': 'indexcontent.html',
}
# If false, no module index is generated.
# html_domain_indices = True
# If false, no index is generated.
# html_use_index = True
# If true, the index is split into individual pages for each letter.
# html_split_index = False
# If true, links to the reST sources are added to the pages.
html_show_sourcelink = False
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
html_show_sphinx = False
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
# html_show_copyright = True
# If true, an OpenSearch description file will be output, and all pages will
# contain a tag referring to it. The value of this option must be the
# base URL from which the finished HTML is served.
# html_use_opensearch = ''
# This is the file name suffix for HTML files (e.g. ".xhtml").
# html_file_suffix = None
# Output file base name for HTML help builder.
htmlhelp_basename = 'Autopilotdoc'
# -- Options for LaTeX output -------------------------------------------------
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
# 'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
# 'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
# 'preamble': '',
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, author, documentclass
# [howto/manual]).
latex_documents = [
('index', 'Autopilot.tex', u'Autopilot Documentation',
u'Thomi Richards', 'manual'),
]
# The name of an image file (relative to this directory) to place at the top of
# the title page.
# latex_logo = None
# For "manual" documents, if this is true, then toplevel headings are parts,
# not chapters.
# latex_use_parts = False
# If true, show page references after internal links.
# latex_show_pagerefs = False
# If true, show URL addresses after external links.
# latex_show_urls = False
# Documents to append as an appendix to all manuals.
# latex_appendices = []
# If false, no module index is generated.
# latex_domain_indices = True
# -- Options for manual page output -------------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
('man', 'autopilot3', u'Automated acceptance test tool',
[u'Thomi Richards'], 1)
]
# If true, show URL addresses after external links.
# man_show_urls = False
# -- Options for Texinfo output -----------------------------------------------
# Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
('index', 'Autopilot', u'Autopilot Documentation',
u'Thomi Richards', 'Autopilot', 'One line description of project.',
'Miscellaneous'),
]
# Documents to append as an appendix to all manuals.
# texinfo_appendices = []
# If false, no module index is generated.
# texinfo_domain_indices = True
# How to display URL addresses: 'footnote', 'no', or 'inline'.
# texinfo_show_urls = 'footnote'
./docs/contents.rst 0000644 0000041 0000041 00000000511 13061552435 014563 0 ustar www-data www-data :orphan:
Global Contents
###############
.. toctree::
:maxdepth: 5
tutorial/what_is_autopilot
tutorial/tutorial
guides/good_tests
guides/running_ap
guides/installation
api/index
porting/porting
guides/page_object
faq/faq
faq/contribute
faq/troubleshooting
appendix/appendix
./docs/guides/ 0000755 0000041 0000041 00000000000 13061552435 013457 5 ustar www-data www-data ./docs/guides/page_object.rst 0000644 0000041 0000041 00000021776 13061552435 016470 0 ustar www-data www-data .. _page_object_guide:
Page Object Pattern
####################
.. contents::
Introducing the Page Object Pattern
-----------------------------------
Automated testing of an application through the Graphical User Interface (GUI) is inherently fragile.
These tests require regular review and attention during the development cycle. This is known as Interface Sensitivity (`"even minor changes to the interface can cause tests to fail" `_).
Utilizing the page-object pattern, alleviates some of the problems stemming from this fragility, allowing us to do automated user acceptance testing (UAT) in a sustainable manner.
The Page Object Pattern comes from the `Selenium community `_ and is the best way to turn a flaky and unmaintainable user acceptance test into a stable and useful
part of your release process. A page is what's visible on the screen at a single moment.
A user story consists of a user jumping from page to page until they achieve their goal.
Thus pages are modeled as objects following these guidelines:
#. The public methods represent the services that the page offers.
#. Try not to expose the internals of the page.
#. Methods return other PageObjects.
#. Assertions should exist only in tests
#. Objects need not represent the entire page.
#. Actions which produce multiple results should have a test for each result
Lets take the page objects of the `Ubuntu Clock App `__ as an example, with some simplifications. This application is written in
QML and Javascript using the `Ubuntu SDK `__.
1. The public methods represent the services that the page offers.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
This application has a stopwatch page that lets users measure elapsed
time. It offers services to start, stop and reset the watch, so we start
by defining the stop watch page object as follows:
.. code-block:: python
class Stopwatch(object):
def start(self):
raise NotImplementedError()
def stop(self):
raise NotImplementedError()
def reset(self):
raise NotImplementedError()
2. Try not to expose the internals of the page.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The internals of the page are more likely to change than the services it
offers. A stopwatch will keep the same three services we defined above
even if the whole design changes. In this case, we reset the stop watch
by clicking a button on the bottom-left of the window, but we hide that
as an implementation detail behind the public methods. In Python, we can
indicate that a method is for internal use only by adding a single
leading underscore to its name. So, lets implement the reset\_stopwatch
method:
.. code-block:: python
def reset(self):
self._click_reset_button()
def _click_reset_button(self):
reset_button = self.wait_select_single(
'ImageButton', objectName='resetButton')
self.pointing_device.click_object(reset_button)
Now if the designers go crazy and decide that it's better to reset the
stop watch in a different way, we will have to make the change only in
one place to keep all the tests working. Remember that this type of
tests has Interface Sensitivity, that's unavoidable; but we can reduce
the impact of interface changes with proper encapsulation and turn these
tests into a useful way to verify that a change in the GUI didn't
introduce any regressions.
.. _page_object_guide_guideline_3:
3. Methods return other PageObjects
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
An UAT checks a user story. It will involve the journey of the user
through the system, so he will move from one page to another. Lets take
a look at how a journey to reset the stop watch will look like:
.. code-block:: python
stopwatch = clock_page.open_stopwatch()
stopwatch.start()
stopwatch.reset()
In our sample application, the first page that the user will encounter
is the Clock. One of the things the user can do from this page is to
open the stopwatch page, so we model that as a service that the Clock
page provides. Then return the new page object that will be visible to
the user after completing that step.
.. code-block:: python
class Clock(object):
...
def open_stopwatch(self):
self._switch_to_tab('StopwatchTab')
return self.wait_select_single(Stopwatch)
Now the return value of open\_stopwatch will make available to the
caller all the available services that the stopwatch exposes to the
user. Thus it can be chained as a user journey from one page to the
other.
4. Assertions should exist only in tests
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
A well written UAT consists of a sequence of
steps or user actions and ends with one single assertion that verifies
that the user achieved its goal. The page objects are the helpers for
the user actions part of the test, so it's better to leave the check for
success out of them. With that in mind, a test for the reset of the
stopwatch would look like this:
.. code-block:: python
def test_restart_button_must_restart_stopwatch_time(self):
# Set up.
stopwatch = self.clock_page.open_stopwatch()
stopwatch.start()
stopwatch.reset_stopwatch()
# Check that the stopwatch has been reset.
self.assertThat(
stopwatch.get_time,
Eventually(Equals('00:00.0')))
We have to add a new method to the stopwatch page object: get\_time. But
it only returns the state of the GUI as the user sees it. We leave in
the test method the assertion that checks it's the expected value.
.. code-block:: python
class Stopwatch(object):
...
def get_time(self):
return self.wait_select_single(
'Label', objectName='time').text
5. Need not represent an entire page
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The objects we are modeling here can just represent a part of the page.
Then we build the entire page that the user is seeing by composition of
page parts. This way we can reuse test code for parts of the GUI that
are reused in the application or between different applications. As an
example, take the \_switch\_to\_tab('StopwatchTab') method that we are
using to open the stopwatch page. The Clock application is using the
Header component provided by the Ubuntu SDK, as all the other Ubuntu
applications are doing too. So, the Ubuntu SDK also provides helpers to
make it easier the user acceptance testing of the applications, and you
will find an object like this:
.. code-block:: python
class Header(object):
def switch_to_tab(tab_object_name):
"""Open a tab.
:parameter tab_object_name: The QML objectName property of the tab.
:return: The newly opened tab.
:raise ToolkitException: If there is no tab with that object
name.
"""
...
This object just represents the header of the page, and inside the
object we define the services that the header provides to the users. If
you dig into the full implementation of the Clock test class you will
find that in order to open the stopwatch page we end up calling Header
methods.
6. Actions which produce multiple results should have a test for each result
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
According to the guideline :ref:`page_object_guide_guideline_3`, we are returning page objects every time
that a user action opens the option for new actions to execute.
Sometimes the same action has different results depending on the context
or the values used for the action. For example, the Clock app has an
Alarm page. In this page you can add new alarms, but if you try to add
an alarm for sometime in the past, it will result in an error. So, we
will have two different tests that will look something like this:
.. code-block:: python
def test_add_alarm_for_tomorrow_must_add_to_alarm_list(self):
tomorrow = ...
test_alarm_name = 'Test alarm for tomorrow'
alarm_page = self.alarm_page.add_alarm(
test_alarm_name, tomorrow)
saved_alarms = alarm_page.get_saved_alarms()
self.assertIn(
(test_alarm_name, tomorrow),
saved_alarms)
def test_add_alarm_for_earlier_today_must_display_error(self):
earlier_today = ...
test_alarm_name = 'Test alarm for earlier_today'
error_dialog = self.alarm_page.add_alarm_with_error(
test_alarm_name, earlier_today)
self.assertEqual(
error_dialog.text,
'Please select a time in the future.')
Take a look at the methods add\_alarm and add\_alarm\_with\_error. The
first one returns the Alarm page again, where the user can continue his
journey or finish the test checking the result. The second one returns
the error dialog that's expected when you try to add an alarm with the
wrong values.
./docs/guides/installation.rst 0000644 0000041 0000041 00000003235 13061552435 016715 0 ustar www-data www-data .. _installing_autopilot:
Installing Autopilot
####################
.. contents::
Autopilot is in continuous development, and the best way to get the latest version of autopilot is to run the latest Ubuntu development image. The autopilot developers traditionally support the Ubuntu release immediately prior to the development release via the autopilot PPA.
Ubuntu
======
**I am running the latest development image!**
In that case you can install autopilot directly from the repository and know you are getting the latest release. Check out the packages below.
**I am running a stable version of Ubuntu!**
You may install the version of autopilot in the archive directly, however it will not be up to date. Instead, you should add the latest autopilot ppa to your system (as of this writing, that is autopilot 1.5).
To add the PPA to your system, run the following command::
sudo add-apt-repository ppa:autopilot/1.5 && sudo apt-get update
Once the PPA has been added to your system, you should be able to install the autopilot packages below.
**Which packages should I install?**
Are you working on ubuntu touch applications? The ``autopilot-touch`` metapackage is for you::
sudo apt-get install autopilot-touch
If you are sticking with gtk desktop applications, install the ``autopilot-desktop`` metapackage instead::
sudo apt-get install autopilot-desktop
Feel free to install both metapackages to ensure you have support for all autopilot tests.
Other Linux's
=============
You may have to download the source code, and either run from source, or build the packages locally. Your best bet is to ask in the autopilot IRC channel ( :ref:`help_and_support`).
./docs/guides/running_ap.rst 0000644 0000041 0000041 00000015560 13061552435 016360 0 ustar www-data www-data Running Autopilot
=================
Autopilot test suites can be run with any python test runner (for example, the built-in testtools runner). However, several autopilot features are only available if you use the autopilot runner.
List Tests
----------
Autopilot can list all tests found within a particular module::
$ autopilot3 list
where ** is the base name of the module you want to look at. The module must either be in the current working directory, or be importable by python. For example, to list the tests inside autopilot itself, you can run::
$ autopilot3 list autopilot
autopilot.tests.test_ap_apps.GtkTests.test_can_launch_qt_app
autopilot.tests.test_ap_apps.QtTests.test_can_launch_qt_app
autopilot.tests.test_application_mixin.ApplicationSupportTests.test_can_create
autopilot.tests.test_application_mixin.ApplicationSupportTests.test_launch_raises_ValueError_on_unknown_kwargs
autopilot.tests.test_application_mixin.ApplicationSupportTests.test_launch_raises_ValueError_on_unknown_kwargs_with_known
autopilot.tests.test_application_mixin.ApplicationSupportTests.test_launch_with_bad_types_raises_typeerror
autopilot.tests.test_application_registration.ApplicationRegistrationTests.test_can_register_new_application
autopilot.tests.test_application_registration.ApplicationRegistrationTests.test_can_unregister_application
autopilot.tests.test_application_registration.ApplicationRegistrationTests.test_registering_app_twice_raises_KeyError
autopilot.tests.test_application_registration.ApplicationRegistrationTests.test_unregistering_unknown_application_raises_KeyError
...
81 total tests.
Some results have been omitted for clarity.
The list command takes only one option:
-ro, --run-order Display tests in the order in which they will be run,
rather than alphabetical order (which is the default).
Run Tests
---------
Running autopilot tests is very similar to listing tests::
$ autopilot3 run
However, the run command has many more options to customize the run behavior:
-h, --help show this help message and exit
-o OUTPUT, --output OUTPUT
Write test result report to file. Defaults to stdout.
If given a directory instead of a file will write to a
file in that directory named:
_.log
-f FORMAT, --format FORMAT
Specify desired output format. Default is "text".
Other option is 'xml' to produce junit xml format.
-r, --record Record failing tests. Required 'recordmydesktop' app
to be installed. Videos are stored in /tmp/autopilot.
-rd PATH, --record-directory PATH
Directory to put recorded tests (only if -r)
specified.
-v, --verbose If set, autopilot will output test log data to stderr
during a test run.
Common use cases
++++++++++++++++
1. **Run autopilot and save the test log**::
$ autopilot3 run -o .
Autopilot will create a text log file named _.log with the contents of the test log.
2. **Run autopilot and record failing tests**::
$ autopilot3 run -r --rd .
Videos are recorded as *ogg-vorbis* files, with an .ogv extension. They will be named with the test id that failed. All videos will be placed in the directory specified by the ``-rd`` option - in this case the currect directory. If this option is omitted, videos will be placed in ``/tmp/autopilot/``.
3. **Save the test log as jUnitXml format**::
$ autopilot3 run -o results.xml -f xml
The file 'results.xml' will be created when all the tests have completed, and will be in the jUnitXml file format. This is useful when running the autopilot tests within a jenkins environment.
.. _launching_application_to_introspect:
Launching an Application to Introspect
--------------------------------------
In order to be able to introspect an application, it must first be launched with introspection enabled. Autopilot provides the **launch** command to enable this: ::
$ autopilot3 launch
The ** parameter could be the full path to the application, or the name of an application located somewhere on ``$PATH``. ** is passed on to the application being launched.
A simple Gtk example to launch gedit::
$ autopilot3 launch gedit
A Qt example which passes on parameters to the application being launched::
$ autopilot3 launch qmlscene my_app.qml
Autopilot launch attempts to detect if you are launching either a Gtk or Qt application so that it can enable the correct libraries. If it is unable to determine this you will need to specify the type of application it is by using the -i argument. This allows "Gtk" or "Qt" frameworks to be specified when launching the application. The default value ("Auto") will try to detect which interface to load automatically.
A typical error in this situation will be "Error: Could not determine introspection type to use for application". In which case the -i option should be specified with the correct application framework type to fix the problem::
$ autopilot3 launch -i Qt address-book-app
Once an application has launched with introspection enabled, it will be possible to launch autopilot vis and view the introspection tree, see: :ref:`visualise_introspection_tree`.
.. _visualise_introspection_tree:
Visualise Introspection Tree
----------------------------
A very common thing to want to do while writing autopilot tests is see the structure of the application being tested. To support this, autopilot includes a simple application to help visualize the introspection tree. To start it, make sure the application you wish to test is running (see: :ref:`launching_application_to_introspect`), and then run::
$ autopilot3 vis
The result should be a window similar to below:
.. image:: /images/ap_vis_front_page.png
Selecting a connection from the drop-down box allows you to inspect different autopilot-supporting applications. If Unity is running, the Unity connection should always be present. If other applications have been started with the autopilot support enabled, they should appear in this list as well. Once a connection is selected, the introspection tree is rendered in the left-hand pane, and the details of each object appear in the right-hand pane.
.. image:: /images/ap_vis_object.png
Autopilot vis also has the ability to search the object tree for nodes that match a given name (such as "LauncherController", for example), and draw a transparent overlay over a widget if it contains position information. These tools, when combined can make finding certain parts of an application introspection tree much easier.
./docs/guides/good_tests.rst 0000644 0000041 0000041 00000063173 13061552435 016375 0 ustar www-data www-data Writing Good Autopilot Tests
============================
This document is an introduction to writing good autopilot tests. This should be treated as additional material on top of all the things you'd normally do to write good code. Put another way: test code follows all the same rules as production code - it must follow the coding standards, and be of a professional quality.
Several points in this document are written with respect to the unity autopilot test suite. This is incidental, and doesn't mean that these points do not apply to other test suites!
.. _write-expressive-tests:
Write Expressive Tests
++++++++++++++++++++++
Unit tests are often used as a reference for how your public API should be used. Functional (Autopilot) tests are no different: they can be used to figure out how your application should work from a functional standpoint. However, this only works if your tests are written in a clear, concise, and most importantly expressive style. There are many things you can do to make your tests easier to read:
**Pick Good Test Case Class Names**
Pick a name that encapsulates all the tests in the class, but is as specific as possible. If necessary, break your tests into several classes, so your class names can be more specific. This is important because when a test fails, the test id is the primary means of identifying the failure. The more descriptive the test id is, the easier it is to find the fault and fix the test.
**Pick Good Test Case Method Names**
Similar to picking good test case class names, picking good method names makes your test id more descriptive. We recommend writing very long test method names, for example:
.. code-block:: python
# bad example:
def test_save_dialog(self):
# test goes here
# better example:
def test_save_dialog_can_cancel(self):
# test goes here
# best example:
def test_save_dialog_cancels_on_escape_key(self):
# test goes here
**Write Docstrings**
You should write docstrings for your tests. Often the test method is enough to describe what the test does, but an English description is still useful when reading the test code. For example:
.. code-block:: python
def test_save_dialog_cancels_on_escape_key(self):
"""The Save dialog box must cancel when the escape key is pressed."""
We recommend following :pep:`257` when writing all docstrings.
Test One Thing Only
+++++++++++++++++++
Tests should test one thing, and one thing only. Since we're not writing unit tests, it's fine to have more than one assert statement in a test, but the test should test one feature only. How do you tell if you're testing more than one thing? There's two primary ways:
1. Can you describe the test in a single sentence without using words like 'and', 'also', etc? If not, you should consider splitting your tests into multiple smaller tests.
2. Tests usually follow a simple pattern:
a. Set up the test environment.
b. Perform some action.
c. Test things with assert statements.
If you feel you're repeating steps 'b' and 'c' you're likely testing more than one thing, and should consider splitting your tests up.
**Good Example:**
.. code-block:: python
def test_alt_f4_close_dash(self):
"""Dash must close on alt+F4."""
self.dash.ensure_visible()
self.keyboard.press_and_release("Alt+F4")
self.assertThat(self.dash.visible, Eventually(Equals(False)))
This test tests one thing only. Its three lines match perfectly with the typical three stages of a test (see above), and it only tests for things that it's supposed to. Remember that it's fine to assume that other parts of unity work as expected, as long as they're covered by an autopilot test somewhere else - that's why we don't need to verify that the dash really did open when we called ``self.dash.ensure_visible()``.
Fail Well
+++++++++
Make sure your tests test what they're supposed to. It's very easy to write a test that passes. It's much more difficult to write a test that only passes when the feature it's testing is working correctly, and fails otherwise. There are two main ways to achieve this:
* Write the test first. This is easy to do if you're trying to fix a bug in Unity. In fact, having a test that's exploitable via an autopilot test will help you fix the bug as well. Once you think you have fixed the bug, make sure the autopilot test you wrote now passed. The general workflow will be:
0. Branch unity trunk.
1. Write autopilot test that reproduces the bug.
2. Commit.
3. Write code that fixes the bug.
4. Verify that the test now passes.
5. Commit. Push. Merge.
6. Celebrate!
* If you're writing tests for a bug-fix that's already been written but is waiting on tests before it can be merged, the workflow is similar but slightly different:
0. Branch unity trunk.
1. Write autopilot test that reproduces the bug.
2. Commit.
3. Merge code that supposedly fixes the bug.
4. Verify that the test now passes.
5. Commit. Push. Superseed original merge proposal with your branch.
6. Celebrate!
Think about design
++++++++++++++++++
Much in the same way you might choose a functional or objective-oriented paradigm for a piece of code, a testsuite can benefit from choosing a good design pattern. One such design pattern is the page object model. The page object model can reduce testcase complexity and allow the testcase to grow and easily adapt to changes within the underlying application. Check out :ref:`page_object_guide`.
Test Length
+++++++++++
Tests should be short - as short as possible while maintaining readability. Longer tests are harder to read, harder to understand, and harder to debug. Long tests are often symptomatic of several possible problems:
* Your test requires complicated setup that should be encapsulated in a method or function.
* Your test is actually several tests all jammed into one large test.
**Bad Example:**
.. code-block:: python
def test_panel_title_switching_active_window(self):
"""Tests the title shown in the panel with a maximized application."""
# Locked Launchers on all monitors
self.set_unity_option('num_launchers', 0)
self.set_unity_option('launcher_hide_mode', 0)
text_win = self.open_new_application_window("Text Editor", maximized=True)
self.assertTrue(text_win.is_maximized)
self.assertThat(self.panel.title, Equals(text_win.title))
sleep(.25)
calc_win = self.open_new_application_window("Calculator")
self.assertThat(self.panel.title, Equals(calc_win.application.name))
icon = self.launcher.model.get_icon_by_desktop_id(text_win.application.desktop_file)
launcher = self.launcher.get_launcher_for_monitor(self.panel_monitor)
launcher.click_launcher_icon(icon)
self.assertTrue(text_win.is_focused)
self.assertThat(self.panel.title, Equals(text_win.title))
This test can be simplified into the following:
.. code-block:: python
def test_panel_title_switching_active_window(self):
"""Tests the title shown in the panel with a maximized application."""
text_win = self.open_new_application_window("Text Editor", maximized=True)
self.open_new_application_window("Calculator")
icon = self.launcher.model.get_icon_by_desktop_id(text_win.application.desktop_file)
launcher = self.launcher.get_launcher_for_monitor(self.panel_monitor)
launcher.click_launcher_icon(icon)
self.assertTrue(text_win.is_focused)
self.assertThat(self.panel.title, Equals(text_win.title))
Here's what we changed:
* Removed the ``set_unity_option`` lines, as they didn't affect the test results at all.
* Removed assertions that were duplicated from other tests. For example, there's already an autopilot test that ensures that new applications have their title displayed on the panel.
With a bit of refactoring, this test could be even smaller (the launcher proxy classes could have a method to click an icon given a desktop id), but this is now perfectly readable and understandable within a few seconds of reading.
Good docstrings
+++++++++++++++
Test docstrings are used to communicate to other developers what the test is supposed to be testing. Test Docstrings must:
1. Conform to `PEP8 `_ and `PEP257 `_ guidelines.
2. Avoid words like "should" in favor of stronger words like "must".
3. Contain a one-line summary of the test.
Additionally, they should:
1. Include the launchpad bug number (if applicable).
**Good Example:**
.. code-block:: python
def test_launcher_switcher_next_keeps_shortcuts(self):
"""Launcher switcher next action must keep shortcuts after they've been shown."""
Within the context of the test case, the docstring is able to explain exactly what the test does, without any ambiguity. In contrast, here's a poorer example:
**Bad Example:**
.. code-block:: python
def test_switcher_all_mode_shows_all_apps(self):
"""Test switcher 'show_all' mode shows apps from all workspaces."""
The docstring explains what the desired outcome is, but without how we're testing it. This style of sentence assumes test success, which is not what we want! A better version of this code might look like this:
.. code-block:: python
def test_switcher_all_mode_shows_all_apps(self):
"""Switcher 'show all' mode must show apps from all workspaces."""
The difference between these two are subtle, but important.
Test Readability
++++++++++++++++
The most important attribute for a test is that it is correct - it must test what's it's supposed to test. The second most important attribute is that it is readable. Tests should be able to be examined by themselves by someone other than the test author without any undue hardship. There are several things you can do to improve test readability:
1. Don't abuse the ``setUp()`` method. It's tempting to put code that's common to every test in a class into the ``setUp`` method, but it leads to tests that are not readable by themselves. For example, this test uses the ``setUp`` method to start the launcher switcher, and ``tearDown`` to cancel it:
**Bad Example:**
.. code-block:: python
def test_launcher_switcher_next(self):
"""Moving to the next launcher item while switcher is activated must work."""
self.launcher_instance.switcher_next()
self.assertThat(self.launcher.key_nav_selection, Eventually(GreaterThan(0)))
This leads to a shorter test (which we've already said is a good thing), but the test itself is incomplete. Without scrolling up to the ``setUp`` and ``tearDown`` methods, it's hard to tell how the launcher switcher is started. The situation gets even worse when test classes derive from each other, since the code that starts the launcher switcher may not even be in the same class!
A much better solution in this example is to initiate the switcher explicitly, and use ``addCleanup()`` to cancel it when the test ends, like this:
**Good Example:**
.. code-block:: python
def test_launcher_switcher_next(self):
"""Moving to the next launcher item while switcher is activated must work."""
self.launcher_instance.switcher_start()
self.addCleanup(self.launcher_instance.switcher_cancel)
self.launcher_instance.switcher_next()
self.assertThat(self.launcher.key_nav_selection, Eventually(GreaterThan(0)))
The code is longer, but it's still very readable. It also follows the setup/action/test convention discussed above.
Appropriate uses of the ``setUp()`` method include:
* Initialising test class member variables.
* Setting unity options that are required for the test. For example, many of the switcher autopilot tests set a unity option to prevent the switcher going into details mode after a timeout. This isn't part of the test, but makes the test easier to write.
* Setting unity log levels. The unity log is captured after each test. Some tests may adjust the verbosity of different parts of the Unity logging tree.
2. Put common setup code into well-named methods. If the "setup" phase of a test is more than a few lines long, it makes sense to put this code into it's own method. Pay particular attention to the name of the method you use. You need to make sure that the method name is explicit enough to keep the test readable. Here's an example of a test that doesn't do this:
**Bad Example:**
.. code-block:: python
def test_showdesktop_hides_apps(self):
"""Show Desktop keyboard shortcut must hide applications."""
self.start_app('Character Map', locale='C')
self.start_app('Calculator', locale='C')
self.start_app('Text Editor', locale='C')
# show desktop, verify all windows are hidden:
self.keybinding("window/show_desktop")
self.addCleanup(self.keybinding, "window/show_desktop")
open_wins = self.bamf.get_open_windows()
for win in open_wins:
self.assertTrue(win.is_hidden)
In contrast, we can refactor the test to look a lot nicer:
**Good Example:**
.. code-block:: python
def test_showdesktop_hides_apps(self):
"""Show Desktop keyboard shortcut must hide applications."""
self.launch_test_apps()
# show desktop, verify all windows are hidden:
self.keybinding("window/show_desktop")
self.addCleanup(self.keybinding, "window/show_desktop")
open_wins = self.bamf.get_open_windows()
for win in open_wins:
self.assertTrue(win.is_hidden)
The test is now shorter, and the ``launch_test_apps`` method can be re-used elsewhere. Importantly - even though I've hidden the implementation of the ``launch_test_apps`` method, the test still makes sense.
3. Hide complicated assertions behind custom ``assertXXX`` methods or custom matchers. If you find that you frequently need to use a complicated assertion pattern, it may make sense to either:
* Write a custom matcher. As long as you follow the protocol laid down by the ``testtools.matchers.Matcher`` class, you can use a hand-written Matcher just like you would use an ordinary one. Matchers should be written in the ``autopilot.matchers`` module if they're likely to be reusable outside of a single test, or as local classes if they're specific to one test.
* Write custom assertion methods. For example:
.. code-block:: python
def test_multi_key_copyright(self):
"""Pressing the sequences 'Multi_key' + 'c' + 'o' must produce '©'."""
self.dash.reveal_application_lens()
self.keyboard.press_and_release('Multi_key')
self.keyboard.type("oc")
self.assertSearchText("©")
This test uses a custom method named ``assertSearchText`` that hides the complexity involved in getting the dash search text and comparing it to the given parameter.
Prefer ``wait_for`` and ``Eventually`` to ``sleep``
++++++++++++++++++++++++++++++++++++++++++++++++++++
Early autopilot tests relied on extensive use of the python ``sleep`` call to halt tests long enough for unity to change its state before the test continued. Previously, an autopilot test might have looked like this:
**Bad Example:**
.. code-block:: python
def test_alt_f4_close_dash(self):
"""Dash must close on alt+F4."""
self.dash.ensure_visible()
sleep(2)
self.keyboard.press_and_release("Alt+F4")
sleep(2)
self.assertThat(self.dash.visible, Equals(False))
This test uses two ``sleep`` calls. The first makes sure the dash has had time to open before the test continues, and the second makes sure that the dash has had time to respond to our key presses before we start testing things.
There are several issues with this approach:
1. On slow machines (like a jenkins instance running on a virtual machine), we may not be sleeping long enough. This can lead to tests failing on jenkins that pass on developers machines.
2. On fast machines, we may be sleeping too long. This won't cause the test to fail, but it does make running the test suite longer than it has to be.
There are two solutions to this problem:
In Tests
--------
Tests should use the ``Eventually`` matcher. This can be imported as follows:
.. code-block:: python
from autopilot.matchers import Eventually
The ``Eventually`` matcher works on all attributes in a proxy class that derives from ``UnityIntrospectableObject`` (at the time of writing that is almost all the autopilot unity proxy classes).
The ``Eventually`` matcher takes a single argument, which is another testtools matcher instance. For example, the bad assertion from the example above could be rewritten like so:
.. code-block:: python
self.assertThat(self.dash.visible, Eventually(Equals(False)))
Since we can use any testtools matcher, we can also write code like this:
.. code-block:: python
self.assertThat(self.launcher.key_nav_selection, Eventually(GreaterThan(prev_icon)))
Note that you can pass any object that follows the testtools matcher protocol (so you can write your own matchers, if you like).
.. _wait_for:
In Proxy Classes
----------------
Proxy classes are not test cases, and do not have access to the ``self.assertThat`` method. However, we want proxy class methods to block until unity has had time to process the commands given. For example, the ``ensure_visible`` method on the Dash controller should block until the dash really is visible.
To achieve this goal, all attributes on unity proxy classes have been patched with a ``wait_for`` method that takes a testtools matcher (just like ``Eventually`` - in fact, the ``Eventually`` matcher just calls wait_for under the hood). For example, previously the ``ensure_visible`` method on the Dash controller might have looked like this:
**Bad Example:**
.. code-block:: python
def ensure_visible(self):
"""Ensures the dash is visible."""
if not self.visible:
self.toggle_reveal()
sleep(2)
In this example we're assuming that two seconds is long enough for the dash to open. To use the ``wait_for`` feature, the code looks like this:
**Good Example:**
.. code-block:: python
def ensure_visible(self):
"""Ensures the dash is visible."""
if not self.visible:
self.toggle_reveal()
self.visible.wait_for(True)
Note that wait_for assumes you want to use the ``Equals`` matcher if you don't specify one. Here's another example where we're using it with a testtools matcher:
.. code-block:: python
key_nav_selection.wait_for(NotEquals(old_selection))
Scenarios
+++++++++
Autopilot uses the ``python-testscenarios`` package to run a test multiple times in different scenarios. A good example of scenarios in use is the launcher keyboard navigation tests: each test is run once with the launcher hide mode set to 'always show launcher', and again with it set to 'autohide launcher'. This allows test authors to write their test once and have it execute in multiple environments.
In order to use test scenarios, the test author must create a list of scenarios and assign them to the test case's ``scenarios`` *class* attribute. The autopilot ibus test case classes use scenarios in a very simple fashion:
**Good Example:**
.. code-block:: python
class IBusTestsPinyin(IBusTests):
"""Tests for the Pinyin(Chinese) input engine."""
scenarios = [
('basic', {'input': 'abc1', 'result': u'\u963f\u5e03\u4ece'}),
('photo', {'input': 'zhaopian ', 'result': u'\u7167\u7247'}),
('internet', {'input': 'hulianwang ', 'result': u'\u4e92\u8054\u7f51'}),
('disk', {'input': 'cipan ', 'result': u'\u78c1\u76d8'}),
('disk_management', {'input': 'cipan guanli ', 'result': u'\u78c1\u76d8\u7ba1\u7406'}),
]
def test_simple_input_dash(self):
self.dash.ensure_visible()
self.addCleanup(self.dash.ensure_hidden)
self.activate_ibus(self.dash.searchbar)
self.keyboard.type(self.input)
self.deactivate_ibus(self.dash.searchbar)
self.assertThat(self.dash.search_string, Eventually(Equals(self.result)))
This is a simplified version of the IBus tests. In this case, the ``test_simple_input_dash`` test will be called 5 times. Each time, the ``self.input`` and ``self.result`` attribute will be set to the values in the scenario list. The first part of the scenario tuple is the scenario name - this is appended to the test id, and can be whatever you want.
.. Important::
It is important to notice that the test does not change its behavior depending on the scenario it is run under. Exactly the same steps are taken - the only difference in this case is what gets typed on the keyboard, and what result is expected.
Scenarios are applied before the test's ``setUp`` or ``tearDown`` methods are called, so it's safe (and indeed encouraged) to set up the test environment based on these attributes. For example, you may wish to set certain unity options for the duration of the test based on a scenario parameter.
Multiplying Scenarios
---------------------
Scenarios are very helpful, but only represent a single-dimension of parameters. For example, consider the launcher keyboard navigation tests. We may want several different scenarios to come into play:
1. A scenario that controls whether the launcher is set to 'autohide' or 'always visible'.
2. A scenario that controls which monitor the test is run on (in case we have multiple monitors configured).
We can generate two separate scenario lists to represent these two scenario axis, and then produce the dot-product of thw two lists like this:
.. code-block:: python
from autopilot.tests import multiply_scenarios
class LauncherKeynavTests(AutopilotTestCase):
hide_mode_scenarios = [
('autohide', {'hide_mode': 1}),
('neverhide', {'hide_mode': 0}),
]
monitor_scenarios = [
('monitor_0', {'launcher_monitor': 0}),
('monitor_1', {'launcher_monitor': 1}),
]
scenarios = multiply_scenarios(hide_mode_scenarios, monitor_scenarios)
(please ignore the fact that we're assuming that we always have two monitors!)
In the test classes ``setUp`` method, we can then set the appropriate unity option and make sure we're using the correct launcher:
.. code-block:: python
def setUp(self):
self.set_unity_option('launcher_hide_mode', self.hide_mode)
self.launcher_instance = self.launcher.get_launcher_for_monitor(self.launcher_monitor)
Which allows us to write tests that work automatically in all the scenarios:
.. code-block:: python
def test_keynav_initiates(self):
"""Launcher must start keyboard navigation mode."""
self.launcher.keynav_start()
self.assertThat(self.launcher.kaynav_mode, Eventually(Equals(True)))
This works fine. So far we've not done anything to cause undue pain.... until we decide that we want to extend the scenarios with an additional axis:
.. code-block:: python
from autopilot.tests import multiply_scenarios
class LauncherKeynavTests(AutopilotTestCase):
hide_mode_scenarios = [
('autohide', {'hide_mode': 1}),
('neverhide', {'hide_mode': 0}),
]
monitor_scenarios = [
('monitor_0', {'launcher_monitor': 0}),
('monitor_1', {'launcher_monitor': 1}),
]
launcher_monitor_scenarios = [
('launcher on all monitors', {'monitor_mode': 0}),
('launcher on primary monitor only', {'monitor_mode': 1}),
]
scenarios = multiply_scenarios(hide_mode_scenarios, monitor_scenarios, launcher_monitor_scenarios)
Now we have a problem: Some of the generated scenarios won't make any sense. For example, one such scenario will be ``(autohide, monitor_1, launcher on primary monitor only)``. If monitor 0 is the primary monitor, this will leave us running launcher tests on a monitor that doesn't contain a launcher!
There are two ways to get around this problem, and they both lead to terrible tests:
1. Detect these situations and skip the test. This is bad for several reasons - first, skipped tests should be viewed with the same level of suspicion as commented out code. Test skips should only be used in exceptional circumstances. A test skip in the test results is just as serious as a test failure.
2. Detect the situation in the test, and run different code using an if statement. For example, we might decode to do this:
.. code-block:: python
def test_something(self):
# ... setup code here ...
if self.monitor_mode == 1 and self.launcher_monitor == 1:
# test something else
else:
# test the original thing.
As a general rule, tests shouldn't have assert statements inside an if statement unless there's a very good reason for doing so.
Scenarios can be useful, but we must be careful not to abuse them. It is far better to spend more time typing and end up with clear, readable tests than it is to end up with fewer, less readable tests. Like all code, tests are read far more often than they're written.
.. _object_ordering:
Do Not Depend on Object Ordering
++++++++++++++++++++++++++++++++
Calls such as :meth:`~autopilot.introspection.ProxyBase.select_many` return several objects at once. These objects are explicitly unordered, and test authors must take care not to make assumptions about their order.
**Bad Example:**
.. code-block:: python
buttons = self.select_many('Button')
save_button = buttons[0]
print_button = buttons[1]
This code may work initially, but there's absolutely no guarantee that the order of objects won't change in the future. A better approach is to select the individual components you need:
**Good Example:**
.. code-block:: python
save_button = self.select_single('Button', objectName='btnSave')
print_button = self.select_single('Button', objectName='btnPrint')
This code will continue to work in the future.
./docs/otto.py 0000644 0000041 0000041 00000004121 13061552435 013534 0 ustar www-data www-data # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
#
# Autopilot Functional Test Tool
# Copyright (C) 2012-2013 Canonical
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
from docutils import nodes
from sphinx.util.compat import Directive
from sphinx.util.compat import make_admonition
def setup(app):
app.add_node(otto,
html=(visit_todo_node, depart_todo_node),
latex=(visit_todo_node, depart_todo_node),
text=(visit_todo_node, depart_todo_node))
app.add_directive('otto', OttoSaysDirective)
app.add_stylesheet('otto.css')
class otto(nodes.Admonition, nodes.Element):
pass
def visit_todo_node(self, node):
self.visit_admonition(node)
def depart_todo_node(self, node):
self.depart_admonition(node)
class OttoSaysDirective(Directive):
# this enables content in the directive
has_content = True
def run(self):
ad = make_admonition(otto, self.name, ['Autopilot Says'], self.options,
self.content, self.lineno, self.content_offset,
self.block_text, self.state, self.state_machine)
image_container = nodes.container()
image_container.append(nodes.image(uri='/images/otto-64.png'))
image_container['classes'] = ['otto-image-container']
outer_container = nodes.container()
outer_container.extend([image_container] + ad)
outer_container['classes'] = ['otto-says-container']
return [outer_container]
./docs/api/ 0000755 0000041 0000041 00000000000 13061552435 012750 5 ustar www-data www-data ./docs/api/autopilot.exceptions.rst 0000644 0000041 0000041 00000000220 13061552435 017674 0 ustar www-data www-data ``autopilot.exceptions`` - Autopilot Exceptions
+++++++++++++++++++++++++++++++++++++++++++++++
.. automodule:: autopilot.exceptions
:members: ./docs/api/autopilot.matchers.rst 0000644 0000041 0000041 00000000255 13061552435 017331 0 ustar www-data www-data ``autopilot.matchers`` - Custom matchers for test assertions
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
.. automodule:: autopilot.matchers
:members:
./docs/api/autopilot.introspection.rst 0000644 0000041 0000041 00000000243 13061552435 020420 0 ustar www-data www-data ``autopilot.introspection`` - Retrieve proxy objects
+++++++++++++++++++++++++++++++++++++++++++++++++++++
.. automodule:: autopilot.introspection
:members:
./docs/api/autopilot.gestures.rst 0000644 0000041 0000041 00000000247 13061552435 017365 0 ustar www-data www-data ``autopilot.gestures`` - Gestural and multi-touch support
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++
.. automodule:: autopilot.gestures
:members:
./docs/api/autopilot.rst 0000644 0000041 0000041 00000000171 13061552435 015521 0 ustar www-data www-data ``autopilot`` - Global stuff
++++++++++++++++++++++++++++
.. automodule:: autopilot
:members:
:undoc-members:
./docs/api/autopilot.display.rst 0000644 0000041 0000041 00000000320 13061552435 017161 0 ustar www-data www-data ``autopilot.display`` - Get information about the current display(s)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
.. automodule:: autopilot.display
:members:
:undoc-members:
./docs/api/autopilot.process.rst 0000644 0000041 0000041 00000000226 13061552435 017177 0 ustar www-data www-data ``autopilot.process`` - Process Control
+++++++++++++++++++++++++++++++++++++++
.. automodule:: autopilot.process
:members:
:undoc-members:
./docs/api/index.rst 0000644 0000041 0000041 00000000146 13061552435 014612 0 ustar www-data www-data Autopilot API Documentation
===========================
.. toctree::
:maxdepth: 1
:glob:
*
./docs/api/autopilot.introspection.types.rst 0000644 0000041 0000041 00000000352 13061552435 021564 0 ustar www-data www-data ``autopilot.introspection.types`` - Introspection Type details
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
.. automodule:: autopilot.introspection.types
:members: PlainType, Rectangle, Point, Size, DateTime, Time
./docs/api/autopilot.emulators.rst 0000644 0000041 0000041 00000000260 13061552435 017532 0 ustar www-data www-data ``autopilot.emulators`` - Backwards compatibility for autopilot v1.2
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
.. automodule:: autopilot.emulators
./docs/api/autopilot.application.rst 0000644 0000041 0000041 00000000252 13061552435 020023 0 ustar www-data www-data ``autopilot.application`` - Autopilot Application Launchers
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
.. automodule:: autopilot.application
:members:
./docs/api/autopilot.platform.rst 0000644 0000041 0000041 00000000273 13061552435 017347 0 ustar www-data www-data ``autopilot.platform`` - Functions for platform detection
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++
.. automodule:: autopilot.platform
:members:
:undoc-members:
./docs/api/autopilot.input.rst 0000644 0000041 0000041 00000000322 13061552435 016655 0 ustar www-data www-data ``autopilot.input`` - Generate keyboard, mouse, and touch input events
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
.. automodule:: autopilot.input
:members:
:undoc-members:
./docs/api/autopilot.testcase.rst 0000644 0000041 0000041 00000000265 13061552435 017337 0 ustar www-data www-data ``autopilot.testcase`` - Base class for all Autopilot Test Cases
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
.. automodule:: autopilot.testcase
:members:
./docs/faq/ 0000755 0000041 0000041 00000000000 13061552435 012746 5 ustar www-data www-data ./docs/faq/troubleshooting.rst 0000644 0000041 0000041 00000013061 13061552435 016730 0 ustar www-data www-data ===============
Troubleshooting
===============
.. contents::
.. _troubleshooting_general_techniques:
------------------
General Techniques
------------------
The single hardest thing to do while writing autopilot tests is to understand the state of the application's object tree. This is especially important for applications that change their object tree during the lifetime of the test. There are three techniques you can use to discover the state of the object tree:
**Using Autopilot Vis**
The :ref:`Autopilot vis tool ` is a useful tool for exploring the entire structure of an application, and allows you to search for a particular node in the object tree. If you want to find out what parts of the application to select to gain access to certain information, the vis tool is probably the best way to do that.
**Using print_tree**
The :meth:`~autopilot.introspection.ProxyBase.print_tree` method is available on every proxy class. This method will print every child of the proxy object recursively, either to ``stdout`` or a file on disk. This technique can be useful when:
* The application cannot easily be put into the state required before launching autopilot vis, so the vis tool is no longer an option.
* The application state that has to be captured only exists for a short amount of time.
* The application only runs on platforms where the vis tool isn't available.
The :meth:`~autopilot.introspection.ProxyBase.print_tree` method often produces a lot of output. There are two ways this information overload can be handled:
#. Specify a file path to write to, so the console log doesn't get flooded. This log file can then be searched with tools such as ``grep``.
#. Specify a ``maxdepth`` limit. This controls how many levels deep the recursive search will go.
Of course, these techniques can be used in combination.
**Using get_properties**
The :meth:`~autopilot.introspection.ProxyBase.get_properties` method can be used on any proxy object, and will return a python dictionary containing all the properties of that proxy object. This is useful when you want to explore what information is provided by a single proxy object. The information returned by this method is exactly the same as is shown in the right-hand pane of ``autopilot vis``.
----------------------------------------
Common Questions regarding Failing Tests
----------------------------------------
.. _failing_tests:
Q. Why is my test failing? It works some of the time. What causes "flakyness?"
==============================================================================
Sometimes a tests fails because the application under tests has issues, but what happens when the failing test can't be reproduced manually? It means the test itself has an issue.
Here is a troubleshooting guide you can use with some of the common problems that developers can overlook while writing tests.
StateNotFoundError Exception
============================
.. _state_not_found:
1. Not waiting for an animation to finish before looking for an object. Did you add animations to your app recently?
* problem::
self.main_view.select_single('Button', text='click_this')
* solution::
page.animationRunning.wait_for(False)
self.main_view.select_single('Button', text='click_this')
2. Not waiting for an object to become visible before trying to select it. Is your app slower than it used to be for some reason? Does its properties have null values? Do you see errors in stdout/stderr while using your app, if you run it from the commandline?
Python code is executed in series which takes milliseconds, whereas the actions (clicking a button etc.) will take longer as well as the dbus query time. This is why wait_select_* is useful i.e. click a button and wait for that click to happen (including the dbus query times taken).
* problem::
self.main_view.select_single('QPushButton', objectName='clickme')
* solution::
self.main_view.wait_select_single('QPushButton', objectName='clickme')
3. Waiting for an item that is destroyed to be not visible, sometimes the objects is destroyed before it returns false:
* problem::
self.assertThat(dialogButton.visible, Eventually(Equals(False)))
* problem::
self._get_activity_indicator().running.wait_for(False)
* solution::
dialogButton.wait_for_destroyed()
* solution::
self._get_activity_indicator().running.wait_for_destroyed()
4. Trying to use select_many like a list. The order in which the objects are returned are non-deterministic.
* problem::
def get_first_photo(self):
"""Returns first photo"""
return event.select_many(
'OrganicItemInteraction',
objectName='eventsViewPhoto'
)[0]
* solution::
def _get_named_photo_element(self, photo_name):
"""Return the ShapeItem container object for the named photo
This object can be clicked to enable the photo to be selected.
"""
photo_element = self.grid_view().wait_select_single(
'QQuickImage',
source=photo_name
)
return photo_element.get_parent()
def select_named_photo(self, photo_name):
"""Select the named photo from the picker view."""
photo_element = self._get_named_photo_element(photo_name)
self.pointing_device.click_object(photo_element)
./docs/faq/contribute.rst 0000644 0000041 0000041 00000010110 13061552435 015647 0 ustar www-data www-data Contribute
##########################
.. contents::
Autopilot: Contributing
+++++++++++++++++++++++
Q. How can I contribute to autopilot?
=====================================
* Documentation: We can always use more documentation.
* if you don't know how to submit a merge proposal on launchpad, you can write a `bug `_ with new documentation and someone will submit a merge proposal for you. They will give you credit for your documentation in the merge proposal.
* New Features: Check out our existing `Blueprints `_ or create some yourself... Then code!
* Test and Fix: No project is perfect, log some `bugs `_ or `fix some bugs `_.
Q. Where can I get help / support?
==================================
The developers hang out in the #ubuntu-autopilot IRC channel on irc.freenode.net.
Q. How do I download the code?
==============================
Autopilot is using Launchpad and Bazaar for source code hosting. If you're new to Bazaar, or distributed version control in general, take a look at the `Bazaar mini-tutorial first. `_
Install bzr open a terminal and type::
$ sudo apt-get install bzr
Download the code::
$ bzr branch lp:autopilot
This will create an autopilot directory and place the latest code there. You can also view the autopilot code `on the web `_.
Q. How do I submit the code for a merge proposal?
=================================================
After making the desired changes to the code or documentation and making sure the tests still run type::
$ bzr commit
Write a quick one line description of the bug that was fixed or the documentation that was written.
Signup for a `launchpad account `_, if you don't have one. Then using your launchpad id type::
$ bzr push lp:~/autopilot/
Example::
$ bzr push lp:~chris.gagnon/autopilot/bug-fix-lp234567
All new features should have unit and/or functional test to make sure someone doesn't remove or break your new code with a future commit.
.. _listing_source_tests:
Q. How do I list or run the tests for the autopilot source code?
================================================================
Running autopilot from the source code root directory (the directory containing
the autopilot/ bin/ docs/ debian/ etc. directories) will use the local copy and
not the system installed version.
An example from branching to running::
$ bzr branch lp:autopilot ~/src/autopilot/trunk
$ cd ~/src/autopilot/trunk
$ python3 -m autopilot.run list autopilot.tests
Loading tests from: /home/example/src/autopilot/trunk
autopilot.tests.functional.test_ap_apps.ApplicationLaunchTests.test_creating_app_for_non_running_app_fails
autopilot.tests.functional.test_ap_apps.ApplicationLaunchTests.test_creating_app_proxy_for_running_app_not_on_dbus_fails
# .. snip ..
autopilot.tests.unit.test_version_utility_fns.VersionFnTests.test_package_version_returns_none_when_running_from_source
255 total tests.
.. note:: The 'Loading tests from:' or 'Running tests from:' line will inform
you where autopilot is loading the tests from.
To run a specific suite or a single test in a suite, be more specific with the
tests path.
For example, running all unit tests::
$ python3 -m autopilot.run run autopilot.tests.unit
For example, running just the 'InputStackKeyboardTypingTests' suite::
$ python3 -m autopilot.run run autopilot.tests.functional.test_input_stack.InputStackKeyboardTypingTests
Or running a single test in the 'test_version_utility_fns' suite::
$ python3 -m autopilot.run run autopilot.tests.unit.test_version_utility_fns.VersionFnTests.test_package_version_returns_none_when_running_from_source
Q. Which version of Python can Autopilot use?
=============================================
Autopilot supports Python 3.4.
./docs/faq/faq.rst 0000644 0000041 0000041 00000030756 13061552435 014262 0 ustar www-data www-data Frequently Asked Questions
##########################
.. contents::
Autopilot: The Project
++++++++++++++++++++++
.. _help_and_support:
Q. Where can I get help / support?
==================================
The developers hang out in the #ubuntu-autopilot IRC channel on irc.freenode.net.
Q. Which version of autopilot should I install?
===============================================
Ideally you should adopt and utilize the latest version of autopilot. If your testcase requires you to utilize an
older version of autopilot for reasons other than :ref:`porting`, please `file a bug `_ and let the development team know about your issue.
Q. Should I write my tests in python2 or python3?
=================================================
As Autopilot fully supports python3 (see :ref:`python3_support`), you should seek to use python3 for new tests. Before making a decision, you should also ensure any 3rd party modules your test may depend on also support python3.
Q: Should I convert my existing tests to python3?
=================================================
See above. In a word, yes. Converting python2 to python3 (see :ref:`python3_support`) is generally straightforward and converting a testcase is likely much easier than a full python application. You can also consider retaining python2 compatibility upon conversion.
Q. Where can I report a bug?
============================
Autopilot is hosted on launchpad - bugs can be reported on the `launchpad bug page for autopilot `_ (this requires a launchpad account).
Q. What type of applications can autopilot test?
================================================
Autopilot works with severall different types of applications, including:
* The Unity desktop shell.
* Gtk 2 & 3 applications.
* Qt4, Qt5, and Qml applications.
Autopilot is designed to work across all the form factors Ubuntu runs on, including the phone and tablet.
Autopilot Tests
+++++++++++++++
.. _faq-many-asserts:
Q. Autopilot tests often include multiple assertions. Isn't this bad practise?
==============================================================================
Maybe. But probably not.
Unit tests should test a single unit of code, and ideally be written such that they can fail in exactly a single way. Therefore, unit tests should have a single assertion that determines whether the test passes or fails.
However, autopilot tests are not unit tests, they are functional tests. Functional test suites tests features, not units of code, so there's several very good reasons to have more than assertion in a single test:
* Some features require several assertions to prove that the feature is working correctly. For example, you may wish to verify that the 'Save' dialog box opens correctly, using the following code::
self.assertThat(save_win.title, Eventually(Equals("Save Document")))
self.assertThat(save_win.visible, Equals(True))
self.assertThat(save_win.has_focus, Equals(True))
* Some tests need to wait for the application to respond to user input before the test continues. The easiest way to do this is to use the :class:`~autopilot.matchers.Eventually` matcher in the middle of your interaction with the application. For example, if testing the `Firefox `_ browsers ability to print a certain web comic, we might produce a test that looks similar to this::
def test_firefox_can_print_xkcd(self):
"""Firefox must be able to print xkcd.com."""
# Put keyboard focus in URL bar:
self.keyboard.press_and_release('Ctrl+l')
self.keyboard.type('http://xkcd.com')
self.keyboard.press_and_release('Enter')
# wait for page to load:
self.assertThat(self.app.loading, Eventually(Equals(False)))
# open print dialog:
self.keyboard.press_and_release('Ctrl+p')
# wait for dialog to open:
self.assertThat(self.app.print_dialog.open, Eventually(Equals(True)))
self.keyboard.press_and_release('Enter')
# ensure something was sent to our faked printer:
self.assertThat(self.fake_printer.documents_printed, Equals(1))
In general, autopilot tests are more relaxed about the 'one assertion per test' rule. However, care should still be taken to produce tests that are as small and understandable as possible.
Q. How do I write a test that uses either a Mouse or a Touch device interchangeably?
====================================================================================
The :class:`autopilot.input.Pointer` class is a simple wrapper that unifies some of the differences between the :class:`~autopilot.input.Touch` and :class:`~autopilot.input.Mouse` classes. To use it, pass in the device you want to use under the hood, like so::
pointer1 = Pointer(Touch.create())
pointer2 = Pointer(Mouse.create())
# pointer1 and pointer2 now have identical APIs
Combined with test scenarios, this can be used to write tests that are run twice - once with a mouse device and once with a touch device::
from autopilot.input import Mouse, Touch, Pointer
from autopilot.testcase import AutopilotTestCase
class TestCase(AutopilotTestCase):
scenarios = [
('with mouse', dict(pointer=Pointer(Mouse.create()))),
('with touch', dict(pointer=Pointer(Touch.create()))),
]
def test_something(self):
"""Click the pointer at 100,100."""
self.pointer.move(100,100)
self.pointer.click()
If you only want to use the mouse on certain platforms, use the :mod:`autopilot.platform` module to determine the current platform at runtime.
Q. How do I use the Onscreen Keyboard (OSK) to input text in my test?
=====================================================================
The OSK is an backend option for the :meth:`autopilot.input.Keyboard.create`
method (see this :ref:`Advanced Autopilot` section for
details regarding backend selection.)
Unlike the other backends (X11, UInput) the OSK has a GUI presence and thus can
be displayed on the screen.
The :class:`autopilot.input.Keyboard` class provides a context manager that
handles any cleanup required when dealing with the input backends.
For example in the instance when the backend is the OSK, when leaving the scope
of the context manager the OSK will be dismissed with a swipe::
from autopilot.input import Keyboard
text_area = self._launch_test_input_area()
keyboard = Keyboard.create('OSK')
with keyboard.focused_type(text_area) as kb:
kb.type("Hello World.")
self.assertThat(text_area.text, Equals("Hello World"))
# At this point now the OSK has been swiped away.
self.assertThat()
Autopilot Tests and Launching Applications
++++++++++++++++++++++++++++++++++++++++++
Q. How do I launch a Click application from within a test so I can introspect it?
=================================================================================
Launching a Click application is similar to launching a traditional application
and is as easy as using
:meth:`~autopilot.testcase.AutopilotTestCase.launch_click_package`::
app_proxy = self.launch_click_package(
"com.ubuntu.dropping-letters"
)
Q. How do I access an already running application so that I can test/introspect it?
===================================================================================
In instances where it's impossible to launch the application-under-test from
within the testsuite use
:meth:`~autopilot.introspection.get_proxy_object_for_existing_process` to get a
proxy object for the running application.
In all other cases the recommended way to launch and retrieve a proxy object
for an application is by calling either
:meth:`~autopilot.testcase.AutopilotTestCase.launch_test_application` or
:meth:`~autopilot.testcase.AutopilotTestCase.launch_click_package`
For example, to access a long running process that is running before your test starts::
application_pid = get_long_running_processes_pid()
app_proxy = get_proxy_object_for_existing_process(pid=application_pid)
Autopilot Qt & Gtk Support
++++++++++++++++++++++++++
Q. How do I launch my application so that I can explore it with the vis tool?
=============================================================================
Autopilot can launch applications with Autopilot support enabled allowing you to
explore and introspect the application using the :ref:`vis
tool`
For instance launching gedit is as easy as::
$ autopilot3 launch gedit
*Autopilot launch* attempts to detect if you are launching either a Gtk or Qt
application so that it can enable the correct libraries. If is is unable to
determine this you will need to specify the type of application it is by using
the **-i** argument.
For example, in our previous example Autopilot was able to automatically
determine that gedit is a Gtk application and thus no further arguments were
required.
If we want to use the vis tool to introspect something like the :ref:`testapp.py
script ` from an earlier tutorial we will need to
inform autopilot that it is a Qt application so that it can enable the correct
support::
$ autopilot3 launch -i Qt testapp.py
Now that it has been launched with Autopilot support we can introspect and
explore out application using the :ref:`vis tool `.
Q. What is the impact on memory of adding objectNames to QML items?
===================================================================
The objectName is a QString property of QObject which defaults to an empty QString.
QString is UTF-16 representation and because it uses some general purpose
optimisations it usually allocates twice the space it needs to be able to grow
fast. It also uses implicit sharing with copy-on-write and other similar
tricks to increase performance again. These properties makes the used memory
not straightforward to predict. For example, copying an object with an
objectName, shares the memory between both as long as they are not changed.
When measuring memory consumption, things like memory alignment come into play.
Due to the fact that QML is interpreted by a JavaScript engine, we are working
in levels where lots of abstraction layers are in between the code and the
hardware and we have no chance to exactly measure consumption of a single
objectName property. Therefore the taken approach is to measure lots of items
and calculate the average consumption.
.. table:: Measurement of memory consumption of 10000 Items
================== ====================== ====================
Without objectName With unique objectName With same objectName
================== ====================== ====================
65292 kB 66628 kB 66480 kB
================== ====================== ====================
=> With 10000 different objectNames 1336 kB of memory are consumed which is
around 127 Bytes per Item.
Indeed, this is more than only the string. Some of the memory is certainly lost
due to memory alignment where certain areas are just not perfectly filled in
but left empty. However, certainly not all of the overhead can be blamed on
that. Additional memory is used by the QObject meta object information that is
needed to do signal/slot connections. Also, QML does some optimisations: It
does not connect signals/slots when not needed. So the fact that the object
name is set could trigger some more connections.
Even if more than the actual string size is used and QString uses a large
representation, this is very little compared to the rest. A qmlscene with just
the item is 27MB. One full screen image in the Nexus 10 tablet can easily
consume around 30MB of memory. So objectNames are definitely not the first
places where to search for optimisations.
Writing the test code snippets, one interesting thing came up frequently: Just
modifying the code around to set the objectName often influences the results
more than the actual string. For example, having a javascript function that
assigns the objectName definitely uses much more memory than the objectName
itself. Unless it makes sense from a performance point of view (frequently
changing bindings can be slow), objectNames should be added by directly
binding the value to the property instead using helper code to assign it.
Conclusion: If an objectName is needed for testing, this is definitely worth
it. objectName's should obviously not be added when not needed. When adding
them, the `general QML guidelines for performance should be followed. `_
./docs/index.rst 0000644 0000041 0000041 00000000431 13061552435 014036 0 ustar www-data www-data Autopilot Documentation Front Page
##################################
.. toctree::
tutorial/what_is_autopilot
tutorial/tutorial
guides/good_tests
guides/running_ap
guides/installation
api/index
faq/faq
porting/porting
appendix/appendix
man
./docs/man.rst 0000644 0000041 0000041 00000007413 13061552435 013511 0 ustar www-data www-data Autopilot Man Page
##################
SYNOPSIS
--------
.. argparse_usage::
DESCRIPTION
-----------
autopilot is a tool for writing functional test suites for graphical applications for Ubuntu.
OPTIONS
-------
.. argparse_options::
General Options
-h, --help
Get help from autopilot. This command can also be present after a
sub-command (such as run or list) to get help on the specific com‐
mand. Further options are restricted to particular autopilot com‐
mands.
-v, --version
Display autopilot version and exit.
--enable-profile
Enable collection of profile data for autopilot itself. If enabled,
profile data will be stored in 'autopilot_.profile' in the
current working directory.
list [options] suite [suite...]
List the autopilot tests found in the given test suite.
suite
See `SPECIFYING SUITES`_
-ro, --run-order
List tests in the order they will be run in, rather than alphabet‐
ically (which is the default).
--suites
Lists only available suites, not tests contained within the suite.
run [options] suite [suite...]
Run one or more test suites.
suite
See `SPECIFYING SUITES`_
-o FILE, --output FILE
Specify where the test log should be written. Defaults to stdout.
If a directory is specified the file will be created with a file‐
name of _.log
-f FORMAT, --format FORMAT
Specify the format for the log. Valid options are 'xml' and 'text'
'subunit' for JUnit XML, plain text, and subunit, respectively.
-ff, --failfast
Stop the test run on the first error or failure.
-r, --record
Record failed tests. Using this option requires the 'recordmydesk‐
top' application be installed. By default, videos are stored in
/tmp/autopilot
--record-options
Comma separated list of options to pass to recordmydesktop
-rd DIR, --record-directory DIR
Directory where videos should be stored (overrides the default set
by the -r option).
-ro, --random-order
Run the tests in random order
-v, --verbose
Causes autopilot to print the test log to stdout while the test is
running.
--debug-profile
Select a profile for what additional debugging information should
be attached to failed test results.
--timeout-profile
Alter the timeout values Autopilot uses. Selecting 'long' will
make autopilot use longer timeouts for various polling loops. This
can be useful if autopilot is running on very slow hardware
launch [options] application
Launch an application with introspection enabled.
-v, --verbose
Show autopilot log messages. Set twice to also log data useful
for debugging autopilot itself.
-i INTERFACE, --interface INTERFACE
Specify which introspection interace to load. The default
('Auto') uses ldd to try and detect which interface to load.
Options are Gtk and Qt.
vis [options]
Open the autopilot visualizer tool.
-v, --verbose
Show autopilot log messages. Set twice to also log data useful
for debugging autopilot itself.
-testability
Start the vis tool in testability mode. Used for self-tests only.
SPECIFYING SUITES
-----------------
Suites are listed as a python dotted package name. Autopilot will do a
recursive import in order to find all tests within a python package.
./docs/images/ 0000755 0000041 0000041 00000000000 13061552435 013444 5 ustar www-data www-data ./docs/images/favicon.ico 0000644 0000041 0000041 00000002576 13061552435 015577 0 ustar www-data www-data h ( :w