autopilot-1.4+14.04.20140416/ 0000755 0000153 0177776 00000000000 12323561350 015775 5 ustar pbuser nogroup 0000000 0000000 autopilot-1.4+14.04.20140416/setup.py 0000644 0000153 0177776 00000002703 12323560055 017512 0 ustar pbuser nogroup 0000000 0000000 #!/usr/bin/env python
#
# 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 setuptools import find_packages, setup, Extension
VERSION = '1.4.0'
autopilot_tracepoint = Extension(
'autopilot.tracepoint',
libraries=['lttng-ust'],
include_dirs=['lttng_module'],
sources=['lttng_module/autopilot_tracepoint.c']
)
setup(
name='autopilot',
version=VERSION,
description='Functional testing tool for Ubuntu.',
author='Thomi Richards',
author_email='thomi.richards@canonical.com',
url='https://launchpad.net/autopilot',
license='GPLv3',
packages=find_packages(),
test_suite='autopilot.tests',
scripts=['bin/autopilot-sandbox-run'],
ext_modules=[autopilot_tracepoint],
entry_points={
'console_scripts': ['autopilot = autopilot.run:main']
}
)
autopilot-1.4+14.04.20140416/autopilot/ 0000755 0000153 0177776 00000000000 12323561350 020015 5 ustar pbuser nogroup 0000000 0000000 autopilot-1.4+14.04.20140416/autopilot/content.py 0000644 0000153 0177776 00000005362 12323560055 022050 0 ustar pbuser nogroup 0000000 0000000 # -*- 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."""
from __future__ import absolute_import
import io
import logging
from testtools.content import ContentType, content_from_stream, 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 text_content(u'')
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
)
test_case.addDetail(content_name, content_obj)
test_case.addCleanup(make_content)
autopilot-1.4+14.04.20140416/autopilot/_debug.py 0000644 0000153 0177776 00000010753 12323560055 021623 0 ustar pbuser nogroup 0000000 0000000 # -*- 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 fixtures import Fixture
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):
"""Create the fixture.
:param caseAddDetail: A closure over the testcase's addDetail
method, or a similar substitution method.
"""
self.caseAddDetail = caseAddDetail
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-1.4+14.04.20140416/autopilot/application/ 0000755 0000153 0177776 00000000000 12323561350 022320 5 ustar pbuser nogroup 0000000 0000000 autopilot-1.4+14.04.20140416/autopilot/application/__init__.py 0000644 0000153 0177776 00000002243 12323560055 024433 0 ustar pbuser nogroup 0000000 0000000 # -*- 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',
'get_application_launcher_wrapper',
'NormalApplicationLauncher',
'UpstartApplicationLauncher',
]
autopilot-1.4+14.04.20140416/autopilot/application/_environment.py 0000644 0000153 0177776 00000004524 12323560055 025403 0 ustar pbuser nogroup 0000000 0000000 # -*- 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-1.4+14.04.20140416/autopilot/application/_launcher.py 0000644 0000153 0177776 00000032701 12323560055 024636 0 ustar pbuser nogroup 0000000 0000000 # -*- 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 for application launchers."""
import fixtures
from gi.repository import GLib, UpstartAppLaunch
import json
import logging
import os
import psutil
import six
import subprocess
import signal
from testtools.content import content_from_file, text_content
from autopilot._timeout import Timeout
from autopilot.utilities import _raise_on_unknown_kwargs
from autopilot.application._environment import (
GtkApplicationEnvironment,
QtApplicationEnvironment,
)
_logger = logging.getLogger(__name__)
class ApplicationLauncher(fixtures.Fixture):
"""A class that knows how to launch an application with a certain type of
introspection enabled.
"""
def __init__(self, case_addDetail):
self.case_addDetail = case_addDetail
super(ApplicationLauncher, self).__init__()
def launch(self, *arguments):
raise NotImplementedError("Sub-classes must implement this method.")
class UpstartApplicationLauncher(ApplicationLauncher):
"""A launcher class that launched applicaitons with UpstartAppLaunch."""
Timeout = object()
Failed = object()
Started = object()
Stopped = object()
def __init__(self, case_addDetail, **kwargs):
super(UpstartApplicationLauncher, self).__init__(case_addDetail)
self.emulator_base = kwargs.pop('emulator_base', None)
self.dbus_bus = kwargs.pop('dbus_bus', 'session')
self.dbus_application_name = kwargs.pop('application_name', None)
_raise_on_unknown_kwargs(kwargs)
def launch(self, app_id, app_uris=[]):
state = {}
state['loop'] = self._get_glib_loop()
state['expected_app_id'] = app_id
state['message'] = ''
UpstartAppLaunch.observer_add_app_failed(self._on_failed, state)
UpstartAppLaunch.observer_add_app_started(self._on_started, state)
UpstartAppLaunch.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()
UpstartAppLaunch.observer_delete_app_failed(self._on_failed)
UpstartAppLaunch.observer_delete_app_started(self._on_started)
UpstartAppLaunch.observer_delete_app_focus(self._on_started)
self._maybe_add_application_cleanups(state)
self._check_status_error(
state.get('status', None),
state.get('message', '')
)
return self._get_pid_for_launched_app(app_id)
@staticmethod
def _on_failed(launched_app_id, failure_type, state):
if launched_app_id == state['expected_app_id']:
if failure_type == UpstartAppLaunch.AppFailed.CRASH:
state['message'] = 'Application crashed.'
elif failure_type == UpstartAppLaunch.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)
def _attach_application_log(self, app_id):
log_path = UpstartAppLaunch.application_log_path(app_id)
if log_path:
self.case_addDetail(
"Application Log",
content_from_file(log_path)
)
def _stop_application(self, app_id):
state = {}
state['loop'] = self._get_glib_loop()
state['expected_app_id'] = app_id
UpstartAppLaunch.observer_add_app_stop(self._on_stopped, state)
GLib.timeout_add_seconds(10.0, self._on_timeout, state)
UpstartAppLaunch.stop_application(app_id)
state['loop'].run()
UpstartAppLaunch.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 UpstartAppLaunch.get_primary_pid(app_id)
@staticmethod
def _launch_app(app_name, app_uris):
UpstartAppLaunch.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 ClickApplicationLauncher(UpstartApplicationLauncher):
def launch(self, package_id, app_name, app_uris):
app_id = _get_click_app_id(package_id, app_name)
return self._do_upstart_launch(app_id, app_uris)
def _do_upstart_launch(self, app_id, app_uris):
return super(ClickApplicationLauncher, self).launch(app_id, app_uris)
class NormalApplicationLauncher(ApplicationLauncher):
def __init__(self, case_addDetail, **kwargs):
super(NormalApplicationLauncher, self).__init__(case_addDetail)
self.app_type = kwargs.pop('app_type', None)
self.cwd = kwargs.pop('launch_dir', None)
self.capture_output = kwargs.pop('capture_output', True)
self.dbus_bus = kwargs.pop('dbus_bus', 'session')
self.emulator_base = kwargs.pop('emulator_base', None)
_raise_on_unknown_kwargs(kwargs)
def launch(self, application, *arguments):
app_path = _get_application_path(application)
app_path, arguments = self._setup_environment(app_path, *arguments)
self.process = self._launch_application_process(app_path, *arguments)
return self.process.pid
def _setup_environment(self, app_path, *arguments):
app_env = self.useFixture(
_get_application_environment(self.app_type, app_path)
)
return app_env.prepare_environment(
app_path,
list(arguments),
)
def _launch_application_process(self, app_path, *arguments):
process = launch_process(
app_path,
arguments,
self.capture_output,
cwd=self.cwd,
)
self.addCleanup(self._kill_process_and_attach_logs, process)
return process
def _kill_process_and_attach_logs(self, process):
stdout, stderr, return_code = _kill_process(process)
self.case_addDetail(
'process-return-code',
text_content(str(return_code))
)
self.case_addDetail(
'process-stdout',
text_content(stdout)
)
self.case_addDetail(
'process-stderr',
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, six.binary_type):
tmp_out = tmp_out.decode('utf-8', errors='replace')
if isinstance(tmp_err, six.binary_type):
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 u''.join(stdout_parts), u''.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-1.4+14.04.20140416/autopilot/emulators.py 0000644 0000153 0177776 00000003031 12323560055 022400 0 ustar pbuser nogroup 0000000 0000000 # -*- 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
autopilot-1.4+14.04.20140416/autopilot/tests/ 0000755 0000153 0177776 00000000000 12323561350 021157 5 ustar pbuser nogroup 0000000 0000000 autopilot-1.4+14.04.20140416/autopilot/tests/__init__.py 0000644 0000153 0177776 00000001420 12323560055 023266 0 ustar pbuser nogroup 0000000 0000000 # -*- 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-1.4+14.04.20140416/autopilot/tests/unit/ 0000755 0000153 0177776 00000000000 12323561350 022136 5 ustar pbuser nogroup 0000000 0000000 autopilot-1.4+14.04.20140416/autopilot/tests/unit/test_globals.py 0000644 0000153 0177776 00000005141 12323560055 025174 0 ustar pbuser nogroup 0000000 0000000 # -*- 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')
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())
autopilot-1.4+14.04.20140416/autopilot/tests/unit/test_introspection_features.py 0000644 0000153 0177776 00000107333 12323560055 030355 0 ustar pbuser nogroup 0000000 0000000 # -*- 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 mock import patch, Mock, MagicMock
from textwrap import dedent
from testtools import TestCase
from testtools.matchers import (
Equals,
IsInstance,
Not,
NotEquals,
Raises,
raises,
)
from testscenarios import TestWithScenarios
from six import StringIO, u, PY3
from contextlib import contextmanager
if PY3:
from contextlib import ExitStack
else:
from contextlib2 import ExitStack
from autopilot.introspection import (
_check_process_and_pid_details,
_get_application_name_from_dbus_address,
_get_search_criteria_string_representation,
_maybe_filter_connections_by_app_name,
get_classname_from_path,
get_proxy_object_for_existing_process,
ProcessSearchError,
)
from autopilot.introspection.dbus import (
_get_filter_string_for_key_value_pair,
_get_default_proxy_class,
_is_valid_server_side_filter_param,
_get_proxy_object_class,
_object_passes_filters,
_object_registry,
_try_custom_proxy_classes,
CustomEmulatorBase,
DBusIntrospectionObject,
StateNotFoundError,
)
from autopilot.introspection.qt import QtObjectProxyMixin
import autopilot.introspection as _i
from autopilot.utilities import sleep
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 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=u'H\u2026i', result=False)),
]
def test_valid_server_side_param(self):
self.assertThat(
_is_valid_server_side_filter_param(self.key, self.value),
Equals(self.result)
)
class ServerSideParameterFilterStringTests(TestWithScenarios, TestCase):
scenarios = [
('bool true', dict(k='visible', v=True, r="visible=True")),
('bool false', dict(k='visible', v=False, r="visible=False")),
('int +ve', dict(k='size', v=123, r="size=123")),
('int -ve', dict(k='prio', v=-12, r="prio=-12")),
('simple string', dict(k='Name', v=u"btn1", r="Name=\"btn1\"")),
('simple bytes', dict(k='Name', v=b"btn1", r="Name=\"btn1\"")),
('string space', dict(k='Name', v=u"a b c ", r="Name=\"a b c \"")),
('bytes space', dict(k='Name', v=b"a b c ", r="Name=\"a b c \"")),
('string escapes', dict(
k='a',
v=u"\a\b\f\n\r\t\v\\",
r=r'a="\x07\x08\x0c\n\r\t\x0b\\"')),
('byte escapes', dict(
k='a',
v=b"\a\b\f\n\r\t\v\\",
r=r'a="\x07\x08\x0c\n\r\t\x0b\\"')),
('escape quotes (str)', dict(k='b', v="'", r='b="\\' + "'" + '"')),
('escape quotes (bytes)', dict(k='b', v=b"'", r='b="\\' + "'" + '"')),
]
def test_query_string(self):
s = _get_filter_string_for_key_value_pair(self.k, self.v)
self.assertThat(s, Equals(self.r))
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()
_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(_object_passes_filters(obj))
def test_object_passes_filters_fails_when_attr_missing(self):
obj = self.get_empty_fake_object()
self.assertFalse(_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(_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(_object_passes_filters(obj, foo=123))
class DBusIntrospectionObjectTests(TestCase):
def test_can_access_path_attribute(self):
fake_object = DBusIntrospectionObject(
dict(id=[0, 123], path=[0, '/some/path']),
'/',
Mock()
)
with fake_object.no_automatic_refreshing():
self.assertThat(fake_object.path, Equals('/some/path'))
@patch('autopilot.introspection.dbus._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.
"""
fake_object = DBusIntrospectionObject(
dict(id=[0, 123], path=[0, '/some/path']),
'/',
Mock()
)
fake_object._backend.introspection_iface.GetState.return_value = \
[('/path', {}) for i in range(16)]
fake_object.get_state_by_path('some_query')
mock_logger.warning.assert_called_once_with(
"Your query '%s' 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.",
"some_query",
16)
@patch('autopilot.introspection.dbus._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.
"""
fake_object = DBusIntrospectionObject(
dict(id=[0, 123], path=[0, '/some/path']),
'/',
Mock()
)
fake_object._backend.introspection_iface.GetState.return_value = \
[('/path', {}) for i in range(15)]
fake_object.get_state_by_path('some_query')
self.assertThat(mock_logger.warning.called, Equals(False))
def test_wait_until_destroyed_works(self):
"""wait_until_destroyed must return if no new state is found."""
fake_object = DBusIntrospectionObject(
dict(id=[0, 123]),
'/',
Mock()
)
fake_object._backend.introspection_iface.GetState.return_value = []
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 = DBusIntrospectionObject(
fake_state,
'/',
Mock()
)
fake_object._backend.introspection_iface.GetState.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 _print_test_fake_object(self):
"""common fake object for print_tree tests"""
fake_object = DBusIntrospectionObject(
dict(id=[0, 123], path=[0, '/some/path'], text=[0, 'Hello']),
'/some/path',
Mock()
)
# get_properties() always refreshes state, so can't use
# no_automatic_refreshing()
fake_object.refresh_state = lambda: None
fake_object.get_state_by_path = lambda query: []
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'.
"""))
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 ProcessSearchErrorStringRepTests(TestCase):
"""Various tests for the _get_search_criteria_string_representation
function.
"""
def test_get_string_rep_defaults_to_empty_string(self):
observed = _get_search_criteria_string_representation()
self.assertEqual("", observed)
def test_pid(self):
self.assertEqual(
u('pid = 123'),
_get_search_criteria_string_representation(pid=123)
)
def test_dbus_bus(self):
self.assertEqual(
u("dbus bus = 'foo'"),
_get_search_criteria_string_representation(dbus_bus='foo')
)
def test_connection_name(self):
self.assertEqual(
u("connection name = 'foo'"),
_get_search_criteria_string_representation(connection_name='foo')
)
def test_object_path(self):
self.assertEqual(
u("object path = 'foo'"),
_get_search_criteria_string_representation(object_path='foo')
)
def test_application_name(self):
self.assertEqual(
u("application name = 'foo'"),
_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(
u("process object = 'foo'"),
_get_search_criteria_string_representation(process=process)
)
def test_all_parameters_combined(self):
class FakeProcess(object):
def __repr__(self):
return 'foo'
process = FakeProcess()
observed = _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 = "pid = 123, dbus bus = 'session_bus', " \
"connection name = 'com.Canonical.Unity', " \
"object path = '/com/Canonical/Autopilot', " \
"application name = 'MyApp', process object = 'foo'"
self.assertEqual(expected, observed)
class ProcessAndPidErrorCheckingTests(TestCase):
def test_raises_ProcessSearchError_when_process_is_not_running(self):
with patch('autopilot.introspection._pid_is_running') as pir:
pir.return_value = False
self.assertThat(
lambda: _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: _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('autopilot.introspection._pid_is_running') as pir:
pir.return_value = True
observed = _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('autopilot.introspection._pid_is_running') as pir:
pir.return_value = True
observed = _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,
_check_process_and_pid_details()
)
def test_returns_pid_when_both_specified(self):
fake_process = Mock()
fake_process.pid = self.getUniqueInteger()
with patch('autopilot.introspection._pid_is_running') as pir:
pir.return_value = True
observed = _check_process_and_pid_details(
fake_process,
fake_process.pid
)
self.assertEqual(fake_process.pid, observed)
class ApplicationFilteringTests(TestCase):
def get_mock_dbus_address_with_application_name(slf, app_name):
mock_dbus_address = Mock()
mock_dbus_address.introspection_iface.GetState.return_value = (
('/' + app_name, {}),
)
return mock_dbus_address
def test_can_extract_application_name(self):
mock_connection = self.get_mock_dbus_address_with_application_name(
'SomeAppName'
)
self.assertEqual(
'SomeAppName',
_get_application_name_from_dbus_address(mock_connection)
)
def test_maybe_filter_returns_addresses_when_app_name_not_specified(self):
self.assertEqual(
[],
_maybe_filter_connections_by_app_name(None, [])
)
def test_maybe_filter_works_with_partial_match(self):
mock_connections = [
self.get_mock_dbus_address_with_application_name('Match'),
self.get_mock_dbus_address_with_application_name('Mismatch'),
]
expected = mock_connections[:1]
observed = _maybe_filter_connections_by_app_name(
'Match',
mock_connections
)
self.assertEqual(expected, observed)
def test_maybe_filter_works_with_no_match(self):
mock_connections = [
self.get_mock_dbus_address_with_application_name('Mismatch1'),
self.get_mock_dbus_address_with_application_name('Mismatch2'),
]
expected = []
observed = _maybe_filter_connections_by_app_name(
'Match',
mock_connections
)
self.assertEqual(expected, observed)
def test_maybe_filter_works_with_full_match(self):
mock_connections = [
self.get_mock_dbus_address_with_application_name('Match'),
self.get_mock_dbus_address_with_application_name('Match'),
]
expected = mock_connections
observed = _maybe_filter_connections_by_app_name(
'Match',
mock_connections
)
self.assertEqual(expected, observed)
class ProxyObjectGenerationTests(TestCase):
@contextmanager
def mock_all_child_calls(self):
mock_dict = {}
with ExitStack() as all_the_mocks:
mock_dict['check_process'] = all_the_mocks.enter_context(
patch(
'autopilot.introspection._check_process_and_pid_details'
)
)
mock_dict['get_addresses'] = all_the_mocks.enter_context(
patch(
'autopilot.introspection.'
'_get_dbus_addresses_from_search_parameters'
)
)
mock_dict['filter_addresses'] = all_the_mocks.enter_context(
patch(
'autopilot.introspection.'
'_maybe_filter_connections_by_app_name'
)
)
mock_dict['make_proxy_object'] = all_the_mocks.enter_context(
patch(
'autopilot.introspection._make_proxy_object'
)
)
yield mock_dict
def test_makes_child_calls(self):
"""Mock out all child functions, and assert that they're called.
This test is somewhat ugly, and should be refactored once the search
criteria has been refactored into a separate object, rather than a
bunch of named parameters.
"""
with self.mock_all_child_calls() as mocks:
fake_address_list = [Mock()]
mocks['get_addresses'].return_value = fake_address_list
mocks['filter_addresses'].return_value = fake_address_list
get_proxy_object_for_existing_process()
self.assertEqual(
1,
mocks['check_process'].call_count
)
self.assertEqual(
1,
mocks['get_addresses'].call_count
)
self.assertEqual(
1,
mocks['make_proxy_object'].call_count
)
def test_raises_ProcessSearchError(self):
"""Function must raise ProcessSearchError if no addresses are found.
This test is somewhat ugly, and should be refactored once the search
criteria has been refactored into a separate object, rather than a
bunch of named parameters.
"""
with self.mock_all_child_calls() as mocks:
fake_address_list = [Mock()]
mocks['check_process'].return_value = 123
mocks['get_addresses'].return_value = fake_address_list
mocks['filter_addresses'].return_value = []
self.assertThat(
lambda: get_proxy_object_for_existing_process(),
raises(
ProcessSearchError(
"Search criteria (pid = 123, dbus bus = 'session', "
"object path = "
"'/com/canonical/Autopilot/Introspection') returned "
"no results"
)
)
)
def test_raises_RuntimeError(self):
"""Function must raise RuntimeError if several addresses are found.
This test is somewhat ugly, and should be refactored once the search
criteria has been refactored into a separate object, rather than a
bunch of named parameters.
"""
with self.mock_all_child_calls() as mocks:
fake_address_list = [Mock(), Mock()]
mocks['get_addresses'].return_value = fake_address_list
mocks['filter_addresses'].return_value = fake_address_list
self.assertThat(
lambda: get_proxy_object_for_existing_process(),
raises(
RuntimeError(
"Search criteria (pid = 1, dbus bus = 'session', "
"object path = "
"'/com/canonical/Autopilot/Introspection') "
"returned multiple results"
)
)
)
class MakeProxyClassObjectTests(TestCase):
class BaseOne(object):
pass
class BaseTwo(object):
pass
def test_merges_multiple_proxy_bases(self):
cls = _i._make_proxy_class_object(
"MyProxy",
(self.BaseOne, self.BaseTwo)
)
self.assertThat(
len(cls.__bases__),
Equals(1)
)
self.assertThat(cls.__bases__[0].__name__, Equals("MyProxyBase"))
def test_uses_class_name(self):
cls = _i._make_proxy_class_object(
"MyProxy",
(self.BaseOne, self.BaseTwo)
)
self.assertThat(cls.__name__, Equals("MyProxy"))
class GetDetailsFromStateDataTests(TestCase):
fake_state_data = ('/some/path', dict(foo=123))
def test_returns_classname(self):
class_name, _, _ = _i._get_details_from_state_data(
self.fake_state_data
)
self.assertThat(class_name, Equals('path'))
def test_returns_path(self):
_, path, _ = _i._get_details_from_state_data(self.fake_state_data)
self.assertThat(path, Equals('/some/path'))
def test_returns_state_dict(self):
_, _, state = _i._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: _i._get_proxy_bases_from_introspection_xml(""),
raises(RuntimeError("Could not find Autopilot interface."))
)
def test_returns_ApplicationProxyObject_claws_for_base_interface(self):
self.assertThat(
_i._get_proxy_bases_from_introspection_xml(
self.fake_data_with_ap_interface
),
Equals((_i.ApplicationProxyObject,))
)
def test_returns_both_base_and_qt_interface(self):
self.assertThat(
_i._get_proxy_bases_from_introspection_xml(
self.fake_data_with_ap_and_qt_interfaces
),
Equals((_i.ApplicationProxyObject, QtObjectProxyMixin))
)
class ExtendProxyBasesWithEmulatorBaseTests(TestCase):
def test_default_emulator_base_name(self):
bases = _i._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 = _i._extend_proxy_bases_with_emulator_base(
existing_bases,
custom_emulator_base
)
self.assertThat(
new_bases,
Equals(existing_bases + (custom_emulator_base,))
)
class MakeIntrospectionObjectTests(TestCase):
"""Test selection of custom proxy object class."""
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
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('autopilot.introspection.dbus._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_object = self.DefaultSelector(
dict(id=[0, 123], path=[0, '/some/path']),
'/',
Mock()
)
new_fake = fake_object.make_introspection_object(('/Object', {}))
self.assertThat(new_fake, IsInstance(self.DefaultSelector))
gpoc.assert_called_once_with(
_object_registry[fake_object._id],
self.DefaultSelector,
'/Object',
{}
)
@patch('autopilot.introspection.dbus._try_custom_proxy_classes')
@patch('autopilot.introspection.dbus._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 = _get_proxy_object_class(None, None, None, None)
self.assertThat(gpoc_return, Equals(token))
self.assertFalse(gdpc.called)
@patch('autopilot.introspection.dbus._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 = {}
_get_proxy_object_class(class_dict, None, path, state)
tcpc.assert_called_once_with(class_dict, path, state)
@patch('autopilot.introspection.dbus._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: _get_proxy_object_class(
None,
None,
None,
None
),
raises(ValueError))
@patch('autopilot.introspection.dbus._try_custom_proxy_classes')
@patch('autopilot.introspection.dbus._get_default_proxy_class')
@patch('autopilot.introspection.dbus.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
_get_proxy_object_class(None, None, None, None)
self.assertTrue(gdpc.called)
@patch('autopilot.introspection.dbus._try_custom_proxy_classes')
@patch('autopilot.introspection.dbus._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
default = self.DefaultSelector
path = '/path/to/DefaultSelector'
_get_proxy_object_class(None, default, path, None)
gdpc.assert_called_once_with(default, get_classname_from_path(path))
@patch('autopilot.introspection.dbus._try_custom_proxy_classes')
@patch('autopilot.introspection.dbus._get_default_proxy_class')
@patch('autopilot.introspection.dbus.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 = _get_proxy_object_class(None, 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}
path = '/path/to/NeverSelected'
state = {}
class_type = _try_custom_proxy_classes(proxy_class_dict, 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}
path = '/path/to/DefaultSelector'
state = {}
class_type = _try_custom_proxy_classes(proxy_class_dict, 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 = {}
self.assertThat(
lambda: _try_custom_proxy_classes(
proxy_class_dict,
path,
state
),
raises(ValueError)
)
@patch('autopilot.introspection.dbus.get_debug_logger')
def test_get_default_proxy_class_logging(self, gdl):
"""_get_default_proxy_class should log a message."""
_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 = _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 = _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 = _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 = _get_default_proxy_class(self.DefaultSelector, token)
self.assertThat(result.__name__, Equals(token))
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-1.4+14.04.20140416/autopilot/tests/unit/test_platform.py 0000644 0000153 0177776 00000022255 12323560055 025402 0 ustar pbuser nogroup 0000000 0000000 # -*- 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."""
import autopilot.platform as platform
try:
# Python 2
from StringIO import StringIO
except ImportError:
# Python 3
from io import StringIO
from mock import patch
from testtools import TestCase
from testtools.matchers import Equals
from tempfile import NamedTemporaryFile
class PublicAPITests(TestCase):
@patch('autopilot.platform._PlatformDetector')
def test_model_creates_platform_detector(self, mock_detector):
platform.model()
mock_detector.create.assert_called_once()
@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()
@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 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-1.4+14.04.20140416/autopilot/tests/unit/test_types.py 0000644 0000153 0177776 00000064661 12323560055 024731 0 ustar pbuser nogroup 0000000 0000000 # -*- 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 __future__ import absolute_import
from datetime import datetime, time
from mock import patch, Mock
import six
from testscenarios import TestWithScenarios
from testtools import TestCase
from testtools.matchers import Equals, IsInstance, NotEquals, raises
import dbus
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
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=u"Hello World")),
('unicode string', dict(t=dbus.String, v=u"\u2603")),
('bytearray', dict(t=dbus.ByteArray, v=b"Hello World")),
('object path', dict(t=dbus.ObjectPath, v=u"/path/to/object")),
('dbus signature', dict(t=dbus.Signature, v=u"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'])),
]
if not six.PY3:
scenarios.append(
('utf8 string', dict(t=dbus.UTF8String, v=b"Hello World"))
)
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))
try:
expected = str(self.v)
observed = str(p)
self.assertEqual(expected, observed)
# in Python 2.x, str(u'\2603') *should* raise a UnicodeEncode error:
except UnicodeEncodeError:
if not six.PY2:
raise
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))
class DateTimeTests(TestCase):
def test_can_construct_datetime(self):
dt = DateTime(1377209927)
self.assertThat(dt, IsInstance(dbus.Array))
def test_datetime_has_slice_access(self):
dt = DateTime(1377209927)
self.assertThat(dt[0], Equals(1377209927))
def test_datetime_has_properties(self):
dt = DateTime(1377209927)
self.assertThat(dt.timestamp, Equals(1377209927))
self.assertThat(dt.year, Equals(2013))
self.assertThat(dt.month, Equals(8))
self.assertThat(dt.day, Equals(22))
self.assertThat(dt.hour, Equals(22))
self.assertThat(dt.minute, Equals(18))
self.assertThat(dt.second, Equals(47))
def test_equality_with_datetime(self):
dt1 = DateTime(1377209927)
dt2 = DateTime(1377209927)
self.assertThat(dt1, Equals(dt2))
def test_equality_with_list(self):
dt1 = DateTime(1377209927)
dt2 = [1377209927]
self.assertThat(dt1, Equals(dt2))
def test_equality_with_datetime_timestamp(self):
dt1 = DateTime(1377209927)
dt2 = datetime.utcfromtimestamp(1377209927)
dt3 = datetime.utcfromtimestamp(1377209928)
self.assertThat(dt1, Equals(dt2))
self.assertThat(dt1, NotEquals(dt3))
def test_can_convert_to_datetime(self):
dt1 = DateTime(1377209927)
self.assertThat(dt1.datetime, IsInstance(datetime))
def test_repr(self):
dt = DateTime(1377209927)
expected = repr_type('DateTime(2013-08-22 22:18:47)')
observed = repr(dt)
self.assertEqual(expected, observed)
def test_repr_equals_str(self):
dt = DateTime(1377209927)
self.assertEqual(repr(dt), str(dt))
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]),
'/some/dummy/path',
Mock()
)
error_logger.assert_called_once_with(
"While constructing attribute '%s.%s': %s",
"DBusIntrospectionObject",
"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 = u"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 = u"/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-1.4+14.04.20140416/autopilot/tests/unit/test_testcase.py 0000644 0000153 0177776 00000006356 12323560055 025375 0 ustar pbuser nogroup 0000000 0000000 # -*- 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 mock import Mock, patch
from testtools import TestCase
from testtools.matchers import Contains, raises
from autopilot.testcase import (
_compare_system_with_process_snapshot,
AutopilotTestCase
)
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())
def test_using_pick_app_launcher_produces_deprecation_message(self):
class InnerTest(AutopilotTestCase):
def test_foo(self):
self.pick_app_launcher()
with patch('autopilot.testcase.get_application_launcher_wrapper'):
with patch('autopilot.utilities.logger') as patched_log:
InnerTest('test_foo').run()
self.assertThat(
patched_log.warning.call_args[0][0],
Contains(
"This function is deprecated. Please use "
"'the 'app_type' argument to the "
"launch_test_application method' instead."
)
)
autopilot-1.4+14.04.20140416/autopilot/tests/unit/test_backend.py 0000644 0000153 0177776 00000004304 12323560055 025140 0 ustar pbuser nogroup 0000000 0000000 # -*- 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, Not, NotEquals
from autopilot.introspection.backends import DBusAddress
class DBusAddressTests(TestCase):
def test_can_construct(self):
fake_bus = object()
DBusAddress(fake_bus, "conn", "path")
def test_can_store_address_in_dictionary(self):
fake_bus = object()
DBusAddress(fake_bus, "conn", "path")
dict(addr=object())
def test_equality_operator(self):
fake_bus = object()
addr1 = DBusAddress(fake_bus, "conn", "path")
self.assertThat(
addr1, Equals(DBusAddress(fake_bus, "conn", "path")))
self.assertThat(
addr1, Not(Equals(DBusAddress(fake_bus, "conn", "new_path"))))
self.assertThat(
addr1, Not(Equals(DBusAddress(fake_bus, "conn2", "path"))))
self.assertThat(
addr1, Not(Equals(DBusAddress(object(), "conn", "path"))))
def test_inequality_operator(self):
fake_bus = object()
addr1 = DBusAddress(fake_bus, "conn", "path")
self.assertThat(
addr1, Not(NotEquals(DBusAddress(fake_bus, "conn", "path"))))
self.assertThat(
addr1, NotEquals(DBusAddress(fake_bus, "conn", "new_path")))
self.assertThat(
addr1, NotEquals(DBusAddress(fake_bus, "conn2", "path")))
self.assertThat(
addr1, NotEquals(DBusAddress(object(), "conn", "path")))
autopilot-1.4+14.04.20140416/autopilot/tests/unit/test_pick_backend.py 0000644 0000153 0177776 00000011650 12323560055 026150 0 ustar pbuser nogroup 0000000 0000000 # -*- 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-1.4+14.04.20140416/autopilot/tests/unit/test_version_utility_fns.py 0000644 0000153 0177776 00000006602 12323560055 027672 0 ustar pbuser nogroup 0000000 0000000 # -*- 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 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-1.4+14.04.20140416/autopilot/tests/unit/test_start_final_events.py 0000644 0000153 0177776 00000006340 12323560055 027445 0 ustar pbuser nogroup 0000000 0000000 # -*- 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 mock import patch, Mock
from autopilot.utilities import (
CleanupRegistered,
_cleanup_objects,
action_on_test_start,
action_on_test_end,
)
from autopilot.testcase import AutopilotTestCase
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(AutopilotTestCase):
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-1.4+14.04.20140416/autopilot/tests/unit/test_logging.py 0000644 0000153 0177776 00000011653 12323560055 025204 0 ustar pbuser nogroup 0000000 0000000 # -*- 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 __future__ import absolute_import
import logging
import testtools
from autopilot.logging import log_action
class LogHandlerTestCase(testtools.TestCase):
"""A mixin that adds a memento loghandler for testing logging.
Originally written by:
- Guillermo Gonzalez
- Facundo Batista
- Natalia Bidart
"""
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."""
logging.Handler.__init__(self, *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(LogHandlerTestCase, self).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(LogHandlerTestCase, self).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)))
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(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'}.")
autopilot-1.4+14.04.20140416/autopilot/tests/unit/test_command_line_args.py 0000644 0000153 0177776 00000033660 12323560055 027221 0 ustar pbuser nogroup 0000000 0000000 #!/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 mock import patch
try:
# Python 2
from StringIO import StringIO
except ImportError:
# Python 3
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"]))
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-1.4+14.04.20140416/autopilot/tests/unit/test_run.py 0000644 0000153 0177776 00000104023 12323560055 024354 0 ustar pbuser nogroup 0000000 0000000 # -*- 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 mock import Mock, patch
import logging
import os.path
from shutil import rmtree
import six
import subprocess
import tempfile
from testtools import TestCase
from testtools.matchers import (
Contains,
DirExists,
Equals,
FileExists,
IsInstance,
Not,
raises,
Raises,
StartsWith,
)
if six.PY3:
from contextlib import ExitStack
else:
from contextlib2 import ExitStack
from autopilot import run
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_configure_video_recording_not_called(self, patched_globals):
args = Namespace(record_directory='', record=False, record_options='')
run._configure_video_recording(args)
self.assertFalse(patched_globals._configure_video_recording.called)
@patch.object(run, '_have_video_recording_facilities', new=lambda: True)
def test_configure_video_recording_called_with_record_set(self):
args = Namespace(record_directory='', record=True, record_options='')
with patch('autopilot.run.autopilot.globals') as patched_globals:
run._configure_video_recording(args)
patched_globals.configure_video_recording.assert_called_once_with(
True,
'/tmp/autopilot',
''
)
@patch.object(run, '_have_video_recording_facilities', new=lambda: True)
def test_configure_video_record_directory_imples_record(self):
token = self.getUniqueString()
args = Namespace(
record_directory=token,
record=False,
record_options=''
)
with patch('autopilot.run.autopilot.globals') as patched_globals:
run._configure_video_recording(args)
patched_globals.configure_video_recording.assert_called_once_with(
True,
token,
''
)
@patch.object(run, '_have_video_recording_facilities', new=lambda: False)
def test_configure_video_recording_raises_RuntimeError(self):
args = Namespace(record_directory='', record=True, record_options='')
self.assertThat(
lambda: run._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(run.subprocess, 'call') as patched_call:
run._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(run.subprocess, 'call') as patched_call:
patched_call.return_value = 0
self.assertTrue(run._have_video_recording_facilities())
def test_video_record_check_returns_false_on_nonzero_return_code(self):
with patch.object(run.subprocess, 'call') as patched_call:
patched_call.return_value = 1
self.assertFalse(run._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=six.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")
)
@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'])
@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=six.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):
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(0)
fake_get_root_logger.assert_called_once_with()
def test_setup_logging_sets_root_logging_level_to_debug(self):
with patch.object(run, 'get_root_logger') as fake_get_logger:
run.setup_logging(0)
fake_get_logger.return_value.setLevel.assert_called_once_with(
logging.DEBUG
)
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)
if six.PY2:
self.assertThat(stream.mode, Equals('w'))
else:
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(u'Text!'),
Not(Raises())
)
if six.PY3:
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(u'Text!'),
Not(Raises())
)
if six.PY3:
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())
)
if six.PY3:
self.assertThat(
lambda: stream.write(u'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=six.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')
)
configure_video = stack.enter_context(
patch.object(run, '_configure_video_recording')
)
load_tests.return_value = (mock_test_suite, False)
fake_construct.return_value = mock_construct_test_result
program.run()
configure_video.assert_called_once_with(fake_args)
config_timeout.assert_called_once_with(fake_args)
configure_debug.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=six.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',
)
defaults.update(kwargs)
return Namespace(**defaults)
autopilot-1.4+14.04.20140416/autopilot/tests/unit/__init__.py 0000644 0000153 0177776 00000001420 12323560055 024245 0 ustar pbuser nogroup 0000000 0000000 # -*- 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-1.4+14.04.20140416/autopilot/tests/unit/test_stagnate_state.py 0000644 0000153 0177776 00000004455 12323560055 026566 0 ustar pbuser nogroup 0000000 0000000 # -*- 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-1.4+14.04.20140416/autopilot/tests/unit/test_debug.py 0000644 0000153 0177776 00000015654 12323560055 024651 0 ustar pbuser nogroup 0000000 0000000 # -*- 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 mock import Mock, patch
import six
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):
if six.PY2:
open_args = dict(bufsize=0)
else:
open_args = dict(buffering=0)
with NamedTemporaryFile(**open_args) as temp_file:
temp_file.write(six.b("Hello\n"))
log_debug_object = d.LogFileDebugObject(
self.fake_caseAddDetail,
temp_file.name
)
log_debug_object.setUp()
temp_file.write(six.b("World\n"))
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(six.u("World\n")))
def test_can_follow_file_with_binary_content(self):
if six.PY2:
open_args = dict(bufsize=0)
else:
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(six.b("Hello\x88World"))
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-1.4+14.04.20140416/autopilot/tests/unit/test_utilities.py 0000644 0000153 0177776 00000016662 12323560055 025576 0 ustar pbuser nogroup 0000000 0000000 # -*- 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 mock import Mock, patch
import re
import six
from testtools import skipIf, TestCase
from testtools.matchers import (
Equals,
IsInstance,
LessThan,
MatchesRegex,
Not,
raises,
Raises,
)
import time
from autopilot.utilities import (
_raise_on_unknown_kwargs,
cached_result,
compatible_repr,
deprecated,
sleep,
)
class ElapsedTimeCounter(object):
"""A simple utility to count the amount of real time that passes."""
def __enter__(self):
self._start_time = time.time()
return self
def __exit__(self, *args):
pass
@property
def elapsed_time(self):
return time.time() - 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 CompatibleReprTests(TestCase):
@skipIf(six.PY3, "Applicable to python 2 only")
def test_py2_unicode_is_returned_as_bytes(self):
repr_fn = compatible_repr(lambda: u"unicode")
result = repr_fn()
self.assertThat(result, IsInstance(six.binary_type))
self.assertThat(result, Equals(b'unicode'))
@skipIf(six.PY3, "Applicable to python 2 only")
def test_py2_bytes_are_untouched(self):
repr_fn = compatible_repr(lambda: b"bytes")
result = repr_fn()
self.assertThat(result, IsInstance(six.binary_type))
self.assertThat(result, Equals(b'bytes'))
@skipIf(six.PY2, "Applicable to python 3 only")
def test_py3_unicode_is_untouched(self):
repr_fn = compatible_repr(lambda: u"unicode")
result = repr_fn()
self.assertThat(result, IsInstance(six.text_type))
self.assertThat(result, Equals(u'unicode'))
@skipIf(six.PY2, "Applicable to python 3 only.")
def test_py3_bytes_are_returned_as_unicode(self):
repr_fn = compatible_repr(lambda: b"bytes")
result = repr_fn()
self.assertThat(result, IsInstance(six.text_type))
self.assertThat(result, Equals(u'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:
if six.PY2:
inner.__name__ = ""
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))
autopilot-1.4+14.04.20140416/autopilot/tests/unit/test_exceptions.py 0000644 0000153 0177776 00000005407 12323560055 025737 0 ustar pbuser nogroup 0000000 0000000 # -*- 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 six
from testtools import TestCase
from testtools.matchers import raises, Equals
from autopilot.introspection.dbus 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'.")
)
if not six.PY3:
self.assertThat(
unicode(err),
Equals(u"Object not found with name 'MyClass'.")
)
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 {'foo': 'bar'}.")
)
if not six.PY3:
self.assertThat(
unicode(err),
Equals(u"Object not found with properties {'foo': 'bar'}.")
)
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 {'foo': 'bar'}.")
)
if not six.PY3:
self.assertThat(
unicode(err),
Equals(u"Object not found with name 'MyClass'"
" and properties {'foo': 'bar'}.")
)
autopilot-1.4+14.04.20140416/autopilot/tests/unit/test_process.py 0000644 0000153 0177776 00000005366 12323560055 025240 0 ustar pbuser nogroup 0000000 0000000 # -*- 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 mock import Mock, patch
from testtools import TestCase
from testtools.matchers import (
Not,
Raises,
)
import autopilot.process._bamf as _b
from autopilot.process._bamf import _launch_application
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-1.4+14.04.20140416/autopilot/tests/unit/test_test_loader.py 0000644 0000153 0177776 00000017547 12323560055 026073 0 ustar pbuser nogroup 0000000 0000000 # -*- 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 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-1.4+14.04.20140416/autopilot/tests/unit/test_proxy_objects.py 0000644 0000153 0177776 00000025433 12323560055 026451 0 ustar pbuser nogroup 0000000 0000000 # -*- 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 DBusException
from testtools import TestCase
from testtools.matchers import Equals
from mock import patch, Mock
from autopilot.introspection import (
_connection_matches_pid,
_get_possible_connections,
_connection_has_path,
_match_connection,
_bus_pid_is_our_pid,
)
class ProxyObjectTests(TestCase):
fake_bus = "fake_bus"
fake_connection_name = "fake_connection_name"
fake_path = "fake_path"
fake_pid = 123
@patch('autopilot.introspection._check_connection_has_ap_interface')
def test_connection_has_path_succeeds_with_valid_connection_path(
self, patched_fn):
result = _connection_has_path(
self.fake_bus, self.fake_connection_name, self.fake_path)
patched_fn.assert_called_once_with(
self.fake_bus, self.fake_connection_name, self.fake_path)
self.assertThat(result, Equals(True))
@patch('autopilot.introspection._check_connection_has_ap_interface',
side_effect=DBusException)
def test_connection_has_path_fails_with_invalid_connection_name(
self, patched_fn):
result = _connection_has_path(
self.fake_bus, self.fake_connection_name, self.fake_path)
patched_fn.assert_called_once_with(
self.fake_bus, self.fake_connection_name, self.fake_path)
self.assertThat(result, Equals(False))
@patch('autopilot.introspection._get_child_pids', return_value=[])
def test_connection_matches_pid_matches_pid_returns_true(
self, child_pid_fn):
"""_connection_matches_pid must return True if the passed pid matches
the buses Unix pid.
"""
matching_pid = 1
with patch('autopilot.introspection._get_bus_connections_pid',
return_value=matching_pid) as bus_pid_fn:
result = _connection_matches_pid(
self.fake_bus, self.fake_connection_name, matching_pid)
child_pid_fn.assert_called_with(matching_pid)
bus_pid_fn.assert_called_with(
self.fake_bus, self.fake_connection_name)
self.assertThat(result, Equals(True))
@patch('autopilot.introspection._get_child_pids')
@patch('autopilot.introspection._get_bus_connections_pid')
def test_connection_matches_pid_child_pid_returns_true(
self, bus_connection_pid_fn, child_pid_fn):
"""_connection_matches_pid must return True if the passed pid doesn't
match but a child pid of the pids' process does.
"""
parent_pid = 1
child_pid = 2
child_pid_fn.return_value = [child_pid]
bus_connection_pid_fn.return_value = child_pid
result = _connection_matches_pid(
self.fake_bus, self.fake_connection_name, parent_pid)
child_pid_fn.assert_called_with(parent_pid)
bus_connection_pid_fn.assert_called_with(
self.fake_bus, self.fake_connection_name)
self.assertThat(result, Equals(True))
@patch('autopilot.introspection._get_child_pids')
@patch('autopilot.introspection._get_bus_connections_pid')
def test_connection_matches_pid_doesnt_match_with_no_children_pids(
self, bus_connection_pid_fn, child_pid_fn):
"""_connection_matches_pid must return False if passed pid doesn't
match.
"""
non_matching_pid = 10
child_pid_fn.return_value = []
bus_connection_pid_fn.return_value = 0
result = _connection_matches_pid(
self.fake_bus, self.fake_connection_name, non_matching_pid)
child_pid_fn.assert_called_with(non_matching_pid)
bus_connection_pid_fn.assert_called_with(
self.fake_bus, self.fake_connection_name)
self.assertThat(result, Equals(False))
@patch('autopilot.introspection._get_child_pids')
@patch('autopilot.introspection._get_bus_connections_pid')
def test_connection_matches_pid_with_no_matching_children_pids(
self, bus_connection_pid_fn, child_pid_fn):
"""_connection_matches_pid must return False if neither passed pid or
pids' processes children match.
"""
non_matching_pid = 10
child_pid_fn.return_value = [1, 2, 3, 4]
bus_connection_pid_fn.return_value = 0
result = _connection_matches_pid(
self.fake_bus, self.fake_connection_name, non_matching_pid)
child_pid_fn.assert_called_with(non_matching_pid)
bus_connection_pid_fn.assert_called_with(
self.fake_bus, self.fake_connection_name)
self.assertThat(result, Equals(False))
@patch('autopilot.introspection._connection_matches_pid')
@patch('autopilot.introspection._connection_has_path')
def test_match_connection_succeeds_with_connection_path_no_pid(
self, conn_has_path_fn, conn_matches_pid_fn):
conn_has_path_fn.return_value = True
conn_matches_pid_fn.side_effect = Exception("Shouldn't be called")
result = _match_connection(
self.fake_bus, None, self.fake_path, self.fake_connection_name)
conn_has_path_fn.assert_called_with(
self.fake_bus, self.fake_connection_name, self.fake_path)
self.assertThat(conn_matches_pid_fn.called, Equals(False))
self.assertThat(result, Equals(True))
@patch('autopilot.introspection._connection_matches_pid')
@patch('autopilot.introspection._connection_has_path')
def test_match_connection_succeeds_with_connection_path_and_pid_match(
self, conn_has_path_fn, conn_matches_pid_fn):
test_pid = 1
conn_matches_pid_fn.return_value = True
conn_has_path_fn.return_value = True
result = _match_connection(
self.fake_bus, test_pid, self.fake_path, self.fake_connection_name)
conn_matches_pid_fn.assert_called_once_with(
self.fake_bus, self.fake_connection_name, test_pid)
self.assertThat(result, Equals(True))
@patch('autopilot.introspection._connection_matches_pid')
@patch('autopilot.introspection._connection_has_path')
def test_match_connection_fails_path_matches_but_no_pid_match(
self, conn_has_path_fn, conn_matches_pid_fn):
test_pid = 0
conn_has_path_fn.return_value = True
conn_matches_pid_fn.return_value = False
result = _match_connection(
self.fake_bus, test_pid, self.fake_path, self.fake_connection_name)
conn_matches_pid_fn.assert_called_once_with(
self.fake_bus, self.fake_connection_name, test_pid)
self.assertThat(result, Equals(False))
@patch('autopilot.introspection._connection_matches_pid')
@patch('autopilot.introspection._connection_has_path')
def test_match_connection_fails_no_path_match_but_pid_match(
self, conn_has_path_fn, conn_matches_pid_fn):
test_pid = 0
conn_has_path_fn.return_value = False
conn_matches_pid_fn.return_value = True
result = _match_connection(
self.fake_bus, test_pid, self.fake_path, self.fake_connection_name)
conn_matches_pid_fn.assert_called_once_with(
self.fake_bus, self.fake_connection_name, test_pid)
conn_matches_pid_fn.assert_called_once_with(
self.fake_bus, self.fake_connection_name, test_pid)
self.assertThat(result, Equals(False))
@patch('autopilot.introspection._bus_pid_is_our_pid')
def test_match_connection_fails_bus_pid_is_our_pid(self, bus_pid_fn):
test_pid = 0
bus_pid_fn.return_value = True
result = _match_connection(
self.fake_bus, test_pid, self.fake_path, self.fake_connection_name)
bus_pid_fn.assert_called_once_with(
self.fake_bus, self.fake_connection_name, test_pid)
self.assertThat(result, Equals(False))
@patch('autopilot.introspection._get_bus_connections_pid')
@patch('os.getpid')
def test_bus_pid_is_our_pid_returns_true_when_pids_match(
self, getpid_fn, get_bus_conn_pid_fn):
script_pid = 0
getpid_fn.return_value = script_pid
get_bus_conn_pid_fn.return_value = script_pid
result = _bus_pid_is_our_pid(
self.fake_bus, self.fake_connection_name, script_pid)
get_bus_conn_pid_fn.assert_called_with(
self.fake_bus, self.fake_connection_name)
self.assertThat(result, Equals(True))
@patch('autopilot.introspection._get_bus_connections_pid')
@patch('os.getpid')
def test_bus_pid_is_our_pid_returns_false_when_pids_dont_match(
self, getpid_fn, get_bus_conn_pid_fn):
script_pid = 0
getpid_fn.return_value = script_pid
get_bus_conn_pid_fn.return_value = 3
result = _bus_pid_is_our_pid(
self.fake_bus, self.fake_connection_name, script_pid)
get_bus_conn_pid_fn.assert_called_with(
self.fake_bus, self.fake_connection_name)
self.assertThat(result, Equals(False))
def test_get_possible_connections_returns_all_with_none_arg(self):
all_connections = ["com.test.something", ":1.234"]
test_bus = Mock(spec_set=["list_names"])
test_bus.list_names.return_value = all_connections
results = _get_possible_connections(test_bus, None)
self.assertThat(results, Equals(all_connections))
def test_get_possible_connections_returns_matching_connection(self):
matching_connection_name = "com.test.success"
all_connections = [
"com.test.something", matching_connection_name, ":1.234"]
test_bus = Mock(spec_set=["list_names"])
test_bus.list_names.return_value = all_connections
results = _get_possible_connections(test_bus, matching_connection_name)
self.assertThat(results, Equals([matching_connection_name]))
def test_get_possible_connections_raises_error_with_no_match(self):
non_matching_connection_name = "com.test.failure"
test_bus = Mock(spec_set=["list_names"])
test_bus.list_names.return_value = ["com.test.something", ":1.234"]
results = _get_possible_connections(
test_bus, non_matching_connection_name)
self.assertThat(results, Equals([]))
autopilot-1.4+14.04.20140416/autopilot/tests/unit/test_timeout.py 0000644 0000153 0177776 00000006137 12323560055 025245 0 ustar pbuser nogroup 0000000 0000000 # -*- 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-1.4+14.04.20140416/autopilot/tests/unit/test_application_launcher.py 0000644 0000153 0177776 00000077104 12323560055 027745 0 ustar pbuser nogroup 0000000 0000000 # -*- 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 six import PY3
if PY3:
from contextlib import ExitStack
else:
from contextlib2 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,
raises,
)
from testtools.content import text_content
import tempfile
from mock import 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.")
)
)
class NormalApplicationLauncherTests(TestCase):
def test_consumes_all_known_kwargs(self):
test_kwargs = dict(
app_type=True,
launch_dir=True,
capture_output=True,
dbus_bus=True,
emulator_base=True
)
self.assertThat(
lambda: NormalApplicationLauncher(self.addDetail, **test_kwargs),
Not(Raises())
)
def test_raises_value_error_on_unknown_kwargs(self):
self.assertThat(
lambda: NormalApplicationLauncher(self.addDetail, unknown=True),
raises(ValueError("Unknown keyword arguments: 'unknown'."))
)
def test_kill_process_and_attach_logs(self):
mock_addDetail = Mock()
app_launcher = NormalApplicationLauncher(mock_addDetail)
with patch.object(
_l, '_kill_process', return_value=(u"stdout", u"stderr", 0)
):
app_launcher._kill_process_and_attach_logs(0)
self.assertThat(
mock_addDetail.call_args_list,
MatchesListwise([
Equals([('process-return-code', text_content('0')), {}]),
Equals([('process-stdout', text_content('stdout')), {}]),
Equals([('process-stderr', text_content('stderr')), {}]),
])
)
def test_setup_environment_returns_prepare_environment_return_value(self):
token = self.getUniqueString()
fake_env = Mock()
fake_env.prepare_environment.return_value = token
app_launcher = NormalApplicationLauncher(self.addDetail)
app_launcher.setUp()
with patch.object(
_l, '_get_application_environment', return_value=fake_env
):
self.assertThat(
app_launcher._setup_environment(self.getUniqueString()),
Equals(token)
)
def test_launch_returns_process_id(self):
app_launcher = NormalApplicationLauncher(self.addDetail)
with patch.object(_l, '_get_application_path', return_value=""):
app_launcher._setup_environment = Mock(return_value=("", "",))
app_launcher._launch_application_process = Mock(
return_value=Mock(pid=123)
)
self.assertThat(app_launcher.launch(""), Equals(123))
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")
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",
(),
launcher.capture_output,
cwd=launcher.cwd
)
class ClickApplicationLauncherTests(TestCase):
def test_raises_exception_on_unknown_kwargs(self):
self.assertThat(
lambda: ClickApplicationLauncher(self.addDetail, unknown=True),
raises(ValueError("Unknown keyword arguments: 'unknown'."))
)
def test_application_name_kwarg_stored(self):
app_name = self.getUniqueString()
launcher = ClickApplicationLauncher(
self.addDetail,
application_name=app_name
)
self.assertThat(
launcher.dbus_application_name, Equals(app_name)
)
def test_click_launch_calls_upstart_launch(self):
launcher = ClickApplicationLauncher(self.addDetail)
token = self.getUniqueString()
with patch.object(launcher, '_do_upstart_launch') as p_dul:
with patch.object(_l, '_get_click_app_id') as p_gcai:
p_gcai.return_value = token
launcher.launch('some_app_id', 'some_app_name', [])
p_dul.assert_called_once_with(token, [])
def test_upcalls_to_upstart(self):
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 = ClickApplicationLauncher(self.addDetail)
launcher._do_upstart_launch('app_id', [])
self.assertEqual(
FakeUpstartBase.launch_call_args,
['app_id', []])
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_default_values_are_set(self):
launcher = UpstartApplicationLauncher(self.addDetail)
self.assertThat(launcher.emulator_base, Equals(None))
self.assertThat(launcher.dbus_bus, Equals('session'))
def test_can_set_emulator_base(self):
mock_emulator_base = Mock()
launcher = UpstartApplicationLauncher(
self.addDetail,
emulator_base=mock_emulator_base
)
self.assertThat(launcher.emulator_base, Equals(mock_emulator_base))
def test_can_set_dbus_bus(self):
launcher = UpstartApplicationLauncher(
self.addDetail,
dbus_bus='system'
)
self.assertThat(launcher.dbus_bus, Equals('system'))
def test_raises_exception_on_unknown_kwargs(self):
self.assertThat(
lambda: UpstartApplicationLauncher(self.addDetail, unknown=True),
raises(ValueError("Unknown keyword arguments: '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, 'UpstartAppLaunch') 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, 'UpstartAppLaunch') 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))
def test_launch(self):
launcher = UpstartApplicationLauncher(self.addDetail)
with patch.object(launcher, '_launch_app') as patched_launch:
with patch.object(launcher, '_get_glib_loop'):
launcher.launch('gedit')
patched_launch.assert_called_once_with('gedit', [])
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.UpstartAppLaunch.AppFailed.CRASH,
'Application crashed.'
)
def test_on_failed_sets_message_for_app_start_failure(self):
self.assertFailedObserverSetsExtraMessage(
_l.UpstartAppLaunch.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)
with patch.object(_l.UpstartAppLaunch, 'application_log_path') as p:
p.return_value = None
launcher._attach_application_log(app_id)
p.assert_called_once_with(app_id)
self.assertEqual(0, case_addDetail.call_count)
def test_attach_application_log_attaches_log_file(self):
token = self.getUniqueString()
case_addDetail = Mock()
launcher = UpstartApplicationLauncher(case_addDetail)
app_id = self.getUniqueString()
with tempfile.NamedTemporaryFile(mode='w') as f:
f.write(token)
f.flush()
with patch.object(_l.UpstartAppLaunch, 'application_log_path',
return_value=f.name):
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", 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, 'UpstartAppLaunch', 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, 'UpstartAppLaunch', 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, 'UpstartAppLaunch', 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-1.4+14.04.20140416/autopilot/tests/unit/test_input.py 0000644 0000153 0177776 00000077703 12323560055 024725 0 ustar pbuser nogroup 0000000 0000000 # -*- 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 .
#
import testscenarios
from evdev import ecodes, uinput
from mock import ANY, call, Mock, patch
from testtools import TestCase
from testtools.matchers import Contains, raises
import autopilot.input
from autopilot import utilities
from autopilot.input import _uinput
from autopilot.input._common import get_center_point
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(TestCase):
"""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_TOOL_FINGER, 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_TOOL_FINGER, 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_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 UInputTouchTestCase(TestCase):
"""Test UInput Touch helper for autopilot tests."""
def setUp(self):
super(UInputTouchTestCase, 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_tap_must_put_finger_down_and_then_up(self):
expected_calls = [
call.finger_down(0, 0),
call.finger_up()
]
touch = self.get_touch_with_mocked_backend()
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_ = type('Dummy', (object,), {'globalRect': (0, 0, 10, 10)})
expected_calls = [
call.finger_down(5, 5),
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_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_drag_must_move_with_specified_rate(self):
expected_calls = [
call.finger_down(0, 0),
call.finger_move(5, 5),
call.finger_move(10, 10),
call.finger_move(15, 15),
call.finger_up()]
touch = self.get_touch_with_mocked_backend()
touch.drag(0, 0, 15, 15, rate=5)
self.assertEqual(
expected_calls, touch._device.mock_calls)
def test_drag_without_rate_must_use_default(self):
expected_calls = [
call.finger_down(0, 0),
call.finger_move(10, 10),
call.finger_move(20, 20),
call.finger_up()]
touch = self.get_touch_with_mocked_backend()
touch.drag(0, 0, 20, 20)
self.assertEqual(
expected_calls, touch._device.mock_calls)
def test_drag_to_same_place_must_not_move(self):
expected_calls = [
call.finger_down(0, 0),
call.finger_up()
]
touch = self.get_touch_with_mocked_backend()
touch.drag(0, 0, 0, 0)
self.assertEqual(expected_calls, touch._device.mock_calls)
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 DragUInputTouchTestCase(testscenarios.TestWithScenarios, TestCase):
scenarios = [
('drag 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)])),
('drag 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)])),
('drag 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)])),
('drag 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)])),
('drag 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)])),
('drag 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)])),
('drag 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)])),
('drag 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)])),
('drag less than rate', dict(
start_x=50, start_y=50, stop_x=55, stop_y=55,
expected_moves=[call.finger_move(55, 55)])),
('drag 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(DragUInputTouchTestCase, 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.drag(
self.start_x, self.start_y, self.stop_x, self.stop_y)
# We don't check the finger down and finger up. They are already
# tested.
expected_calls = [ANY] + self.expected_moves + [ANY]
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_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)
autopilot-1.4+14.04.20140416/autopilot/tests/unit/test_testresults.py 0000644 0000153 0177776 00000020456 12323560055 026160 0 ustar pbuser nogroup 0000000 0000000 # -*- 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 mock import Mock
import os
import tempfile
from testtools import TestCase, PlaceHolder
from testtools.content import text_content
from testtools.matchers import Contains, raises, NotEquals
from testscenarios import WithScenarios
import unittest
from autopilot import testresult
from autopilot import run
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
)
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(
u'\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(u'\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-1.4+14.04.20140416/autopilot/tests/unit/test_test_fixtures.py 0000644 0000153 0177776 00000017175 12323560055 026473 0 ustar pbuser nogroup 0000000 0000000 # -*- 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,
TempDesktopFile,
)
import os
import os.path
import stat
from 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)
autopilot-1.4+14.04.20140416/autopilot/tests/unit/test_custom_exceptions.py 0000644 0000153 0177776 00000003441 12323560055 027325 0 ustar pbuser nogroup 0000000 0000000 # -*- 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-1.4+14.04.20140416/autopilot/tests/unit/test_vis_bus_enumerator.py 0000644 0000153 0177776 00000005220 12323560055 027462 0 ustar pbuser nogroup 0000000 0000000 # -*- 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 mock import patch, Mock
from testtools import TestCase
from textwrap import dedent
from autopilot.vis.dbus_search import XmlProcessor
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-1.4+14.04.20140416/autopilot/tests/unit/test_matchers.py 0000644 0000153 0177776 00000020637 12323560055 025366 0 ustar pbuser nogroup 0000000 0000000 # -*- 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 __future__ import absolute_import
from contextlib import contextmanager
import dbus
from testscenarios import TestWithScenarios
import six
from testtools import TestCase
from testtools.matchers import (
Contains,
Equals,
Is,
IsInstance,
Mismatch,
raises,
)
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, "/FakeObject", None)
FakeObject._fake_props = props
@classmethod
def get_state_by_path(cls, piece):
return [('/FakeObject', cls._fake_props)]
if attribute_type == 'callable':
return lambda: result
elif attribute_type == 'wait_for':
if isinstance(result, six.text_type):
obj = FakeObject(dict(id=[0, 123], attr=[0, dbus.String(result)]))
return obj.attr
elif isinstance(result, six.binary_type):
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(
u'\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(u'\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(
u'\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(
u'\u963f\u5e03\u4ece'), timeout=.5).match(attr)
self.assertThat(
mismatch.describe(), Contains("阿布从11"))
autopilot-1.4+14.04.20140416/autopilot/tests/unit/test_application_environment.py 0000644 0000153 0177776 00000007043 12323560055 030503 0 ustar pbuser nogroup 0000000 0000000 # -*- 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 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-1.4+14.04.20140416/autopilot/tests/unit/test_out_of_test_addcleanup.py 0000644 0000153 0177776 00000003027 12323560055 030264 0 ustar pbuser nogroup 0000000 0000000 # -*- 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-1.4+14.04.20140416/autopilot/tests/unit/test_query_resolution.py 0000644 0000153 0177776 00000007417 12323560055 027211 0 ustar pbuser nogroup 0000000 0000000 # -*- 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 testscenarios import WithScenarios
from textwrap import dedent
from mock import patch
from autopilot.display import _upa as upa
class QueryResolutionFunctionTests(TestCase):
@patch('subprocess.check_output', return_value=b'')
def test_fbset_output_calls_subprocess(self, patched_check_output):
upa._get_fbset_output()
patched_check_output.assert_called_once_with(
["fbset", "-s", "-x"]
)
def test_get_fbset_resolution(self):
patched_fbset_resolution = dedent(
'''
Mode "768x1280"
# D: 0.002 MHz, H: 0.002 kHz, V: 0.002 Hz
DotClock 0.003
HTimings 768 776 780 960
VTimings 1280 1288 1290 1312
Flags "-HSync" "-VSync"
EndMode
'''
)
with patch.object(upa, '_get_fbset_output') as patched_gfo:
patched_gfo.return_value = patched_fbset_resolution
observed = upa._get_fbset_resolution()
self.assertEqual((768, 1280), observed)
def test_get_fbset_resolution_raises_runtimeError(self):
patched_fbset_resolution = 'something went wrong!'
with patch.object(upa, '_get_fbset_output') as patched_gfo:
patched_gfo.return_value = patched_fbset_resolution
self.assertThat(
upa._get_fbset_resolution,
raises(RuntimeError),
)
def test_hardcoded_raises_error_on_unknown_model(self):
with patch.object(upa, 'image_codename', return_value="unknown"):
self.assertThat(
upa._get_hardcoded_resolution,
raises(
NotImplementedError(
'Device "unknown" is not supported by Autopilot.'
)
)
)
def test_query_resolution_uses_fbset_first(self):
with patch.object(upa, '_get_fbset_resolution', return_value=(1, 2)):
self.assertEqual((1, 2), upa.query_resolution())
def test_query_resolution_uses_hardcoded_second(self):
with patch.object(upa, '_get_fbset_resolution', side_effect=Exception):
with patch.object(
upa, '_get_hardcoded_resolution', return_value=(2, 3)
):
self.assertEqual((2, 3), upa.query_resolution())
class HardCodedResolutionTests(WithScenarios, TestCase):
scenarios = [
("generic", dict(name="generic", expected=(480, 800))),
("mako", dict(name="mako", expected=(768, 1280))),
("maguro", dict(name="maguro", expected=(720, 1280))),
("manta", dict(name="manta", expected=(2560, 1600))),
("grouper", dict(name="grouper", expected=(800, 1280))),
]
def test_hardcoded_resolutions_works_for_known_codenames(self):
with patch.object(upa, 'image_codename', return_value=self.name):
observed = upa._get_hardcoded_resolution()
self.assertEqual(self.expected, observed)
autopilot-1.4+14.04.20140416/autopilot/tests/unit/test_content.py 0000644 0000153 0177776 00000011020 12323560055 025214 0 ustar pbuser nogroup 0000000 0000000 # -*- 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 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(u"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(u"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(u''))
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-1.4+14.04.20140416/autopilot/tests/functional/ 0000755 0000153 0177776 00000000000 12323561350 023321 5 ustar pbuser nogroup 0000000 0000000 autopilot-1.4+14.04.20140416/autopilot/tests/functional/test_custom_assertions.py 0000644 0000153 0177776 00000010060 12323560055 030514 0 ustar pbuser nogroup 0000000 0000000 # -*- 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 __future__ import absolute_import
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-1.4+14.04.20140416/autopilot/tests/functional/test_introspection_features.py 0000644 0000153 0177776 00000025455 12323560055 031544 0 ustar pbuser nogroup 0000000 0000000 # -*- 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 SessionBus
import json
import logging
from mock import patch
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 six import StringIO
from autopilot import platform
from autopilot.matchers import Eventually
from autopilot.testcase import AutopilotTestCase
from autopilot.tests.functional.fixtures import TempDesktopFile
from autopilot.introspection.dbus import CustomEmulatorBase
from autopilot.introspection import _connection_matches_pid
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_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 IntrospectionFunctionTests(AutopilotTestCase):
@patch('autopilot.introspection._connection_matches_pid')
@patch('autopilot.introspection._bus_pid_is_our_pid')
def test_connection_matches_pid_ignores_dbus_daemon(
self, bus_pid_is_our_pid, conn_matches_pid_fn):
_connection_matches_pid(SessionBus(), 'org.freedesktop.DBus', 123)
self.assertThat(bus_pid_is_our_pid.called, Equals(False))
self.assertThat(conn_matches_pid_fn.called, Equals(False))
autopilot-1.4+14.04.20140416/autopilot/tests/functional/test_dbus_query.py 0000644 0000153 0177776 00000022120 12323560055 027112 0 ustar pbuser nogroup 0000000 0000000 # -*- 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 time import time
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.introspection.dbus 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_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.assertThat(fn, raises(TypeError))
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_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.assertThat(fn, raises(ValueError))
def test_select_many_no_name_no_parameter_raises_exception(self):
app = self.start_fully_featured_app()
fn = lambda: app.select_single()
self.assertThat(fn, raises(TypeError))
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_wait_select_single_succeeds_quickly(self):
app = self.start_fully_featured_app()
start_time = time()
main_window = app.wait_select_single('QMainWindow')
end_time = time()
self.assertThat(main_window, NotEquals(None))
self.assertThat(abs(end_time - start_time), LessThan(1))
def test_wait_select_single_fails_slowly(self):
app = self.start_fully_featured_app()
start_time = time()
fn = lambda: app.wait_select_single('QMadeupType')
self.assertThat(fn, raises(StateNotFoundError('QMadeupType')))
end_time = time()
self.assertThat(abs(end_time - start_time), GreaterThan(9))
self.assertThat(abs(end_time - start_time), LessThan(11))
@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-1.4+14.04.20140416/autopilot/tests/functional/test_autopilot_performance.py 0000644 0000153 0177776 00000005176 12323560055 031345 0 ustar pbuser nogroup 0000000 0000000 # -*- 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 __future__ import absolute_import
from contextlib import contextmanager
from time import time
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 = time()
yield
total_time = abs(time() - 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-1.4+14.04.20140416/autopilot/tests/functional/test_mouse_emulator.py 0000644 0000153 0177776 00000003213 12323560055 027772 0 ustar pbuser nogroup 0000000 0000000 # -*- 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 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-1.4+14.04.20140416/autopilot/tests/functional/test_open_window.py 0000644 0000153 0177776 00000004606 12323560055 027271 0 ustar pbuser nogroup 0000000 0000000 # -*- 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 __future__ import absolute_import
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-1.4+14.04.20140416/autopilot/tests/functional/__init__.py 0000644 0000153 0177776 00000011740 12323560055 025436 0 ustar pbuser nogroup 0000000 0000000 # -*- 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 __future__ import absolute_import
from codecs import open
import os
import os.path
import sys
import logging
from shutil import rmtree
import subprocess
from tempfile import mkdtemp
from testtools.content import text_content
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.4.0'
import sys
from pkg_resources import load_entry_point
if __name__ == '__main__':
sys.exit(
load_entry_point('autopilot==1.4.0', 'console_scripts', 'autopilot')()
)
"""
autopilot-1.4+14.04.20140416/autopilot/tests/functional/test_ap_apps.py 0000644 0000153 0177776 00000035151 12323560055 026363 0 ustar pbuser nogroup 0000000 0000000 # -*- 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
import six
from mock import patch
from testtools import skipIf
from testtools.matchers import (
Equals,
LessThan,
Not,
Raises,
raises,
)
from textwrap import dedent
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,
ProcessSearchError,
_pid_is_running,
)
from autopilot.utilities import sleep
# backwards compatible alias for Python 3
if six.PY3:
xrange = range
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()
u'\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 xrange(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))
)
@patch(
'autopilot.introspection._search_for_valid_connections',
new=lambda *args: []
)
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_from_get_state_by_path(self):
"""Testing an application that closes before the test ends must
produce a good error message when calling get_state_by_path 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.get_state_by_path("/"))
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.patch_environment("QT_SELECT", "qt4")
if 'qt5' in qtversions:
path = check_func('qt5', 'qmlscene')
if path:
not_found = False
self.qml_viewer_app_path = path
self.patch_environment("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,))
app_proxy = self.launch_test_application(
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,))
self.launch_upstart_application(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(not locale_is_supported(), "Current locale is not supported")
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-1.4+14.04.20140416/autopilot/tests/functional/test_autopilot_functional.py 0000644 0000153 0177776 00000101301 12323560055 031171 0 ustar pbuser nogroup 0000000 0000000 # -*- 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 __future__ import absolute_import
import os
import os.path
import re
from tempfile import mktemp
from testtools import skipIf
from testtools.matchers import Contains, Equals, MatchesRegex, Not
from textwrap import dedent
from autopilot import platform
from autopilot.testcase import AutopilotTestCase
from autopilot.tests.functional import AutopilotRunTestBase, remove_if_exists
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))
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."""
# 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()
""")
)
should_delete = not os.path.exists('/tmp/autopilot')
if should_delete:
self.addCleanup(remove_if_exists, "/tmp/autopilot")
else:
self.addCleanup(
remove_if_exists,
'/tmp/autopilot/tests.test_simple.SimpleTest.test_simple.ogv')
code, output, error = self.run_autopilot(["run", "-r", "tests"])
self.assertThat(code, Equals(1))
self.assertTrue(os.path.exists('/tmp/autopilot'))
self.assertTrue(os.path.exists(
'/tmp/autopilot/tests.test_simple.SimpleTest.test_simple.ogv'))
if should_delete:
self.addCleanup(remove_if_exists, "/tmp/autopilot")
@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_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(u"""\
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')))
class AutopilotPatchEnvironmentTests(AutopilotTestCase):
def test_patch_environment_new_patch_is_unset_to_none(self):
"""patch_environment must unset the environment variable if previously
was unset.
"""
class PatchEnvironmentSubTests(AutopilotTestCase):
def test_patch_env_sets_var(self):
"""Setting the environment variable must make it available."""
self.patch_environment("APABC321", "Foo")
self.assertThat(os.getenv("APABC321"), Equals("Foo"))
self.assertThat(os.getenv('APABC321'), Equals(None))
result = PatchEnvironmentSubTests("test_patch_env_sets_var").run()
self.assertThat(result.wasSuccessful(), Equals(True))
self.assertThat(os.getenv('APABC321'), Equals(None))
def test_patch_environment_existing_patch_is_reset(self):
"""patch_environment must reset the environment back to it's previous
value.
"""
class PatchEnvironmentSubTests(AutopilotTestCase):
def test_patch_env_sets_var(self):
"""Setting the environment variable must make it available."""
self.patch_environment("APABC987", "InnerTest")
self.assertThat(os.getenv("APABC987"), Equals("InnerTest"))
self.patch_environment('APABC987', "OuterTest")
self.assertThat(os.getenv('APABC987'), Equals("OuterTest"))
result = PatchEnvironmentSubTests("test_patch_env_sets_var").run()
self.assertThat(result.wasSuccessful(), Equals(True))
self.assertThat(os.getenv('APABC987'), Equals("OuterTest"))
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_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-1.4+14.04.20140416/autopilot/tests/functional/fixtures.py 0000644 0000153 0177776 00000012676 12323560055 025561 0 ustar pbuser nogroup 0000000 0000000 # -*- 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."""
from __future__ import absolute_import
import logging
import os
import stat
from shutil import rmtree
import tempfile
from textwrap import dedent
from fixtures import 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
autopilot-1.4+14.04.20140416/autopilot/tests/functional/test_application_mixin.py 0000644 0000153 0177776 00000005134 12323560055 030445 0 ustar pbuser nogroup 0000000 0000000 # -*- 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 __future__ import absolute_import
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))
def test_launch_raises_ValueError_on_unknown_kwargs(self):
"""launch_test_application must raise ValueError when given unknown
keyword arguments.
"""
fn = lambda: self.launch_test_application(
'gedit', arg1=123, arg2='asd')
self.assertThat(
fn,
raises(ValueError("Unknown keyword arguments: 'arg1', 'arg2'.")))
def test_launch_raises_ValueError_on_unknown_kwargs_with_known(self):
"""launch_test_application must raise ValueError when given unknown
keyword arguments.
"""
fn = lambda: self.launch_test_application(
'gedit', arg1=123, arg2='asd', launch_dir='/')
self.assertThat(
fn,
raises(ValueError("Unknown keyword arguments: 'arg1', 'arg2'.")))
autopilot-1.4+14.04.20140416/autopilot/tests/functional/test_input_stack.py 0000644 0000153 0177776 00000045264 12323560055 027272 0 ustar pbuser nogroup 0000000 0000000 # -*- 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 .
#
import json
import os
from tempfile import mktemp
from testtools import TestCase, skipIf
from testtools.matchers import IsInstance, Equals, raises
from textwrap import dedent
from time import sleep
from unittest import SkipTest
from mock import patch
from autopilot.display import Display
from autopilot import platform
from autopilot.gestures import pinch
from autopilot.input import Keyboard, Mouse, Pointer, Touch
from autopilot.input._common import get_center_point
from autopilot.matchers import Eventually
from autopilot.testcase import AutopilotTestCase, multiply_scenarios
from autopilot.tests.functional.fixtures import TempDesktopFile
from autopilot.utilities import on_test_started
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/autopilot-uinput', os.W_OK) or
os.access('/dev/uinput', os.W_OK)):
raise SkipTest(
"UInput backend currently requires write access to "
"/dev/autopilot-uinput or /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 backend "
+ 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):
"""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 _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)
desktop_file = self.useFixture(
TempDesktopFile()
).get_desktop_file_path()
return self.launch_test_application(
"qmlscene",
"-qt=qt5",
qml_path,
'--desktop_file_hint=%s' % desktop_file,
app_type='qt',
)
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 MouseTestCase(AutopilotTestCase):
@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.
"""
screen_geometry = Display.create().get_screen_geometry(0)
device = Mouse.create()
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 (WinMocker)")
class TouchTests(AutopilotTestCase):
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 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')
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):
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',
)
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):
pass
def drag(self, x1, y1, x2, y2, rate='dummy',
time_between_events='dummy'):
pass
p = Pointer(FakeTouch())
p.drag(0, 0, 100, 123)
self.assertThat(p.x, Equals(100))
self.assertThat(p.y, Equals(123))
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-1.4+14.04.20140416/autopilot/tests/functional/test_process_emulator.py 0000644 0000153 0177776 00000014603 12323560055 030325 0 ustar pbuser nogroup 0000000 0000000 # -*- 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 os
import sys
from subprocess import Popen, call
from textwrap import dedent
from threading import Thread
from time import sleep, time
from testtools import skipIf
from testtools.matchers import Equals, 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 = time()
t = Thread(target=start_gedit())
t.start()
ret = self.process_manager.wait_until_application_is_running(
'gedit.desktop', 10)
end = time()
t.join()
self.assertThat(ret, Equals(True))
self.assertThat(abs(end - start - 5.0), LessThan(1))
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 = time()
ret = self.process_manager.wait_until_application_is_running(
'gedit.desktop', 5)
end = time()
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))
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")
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-1.4+14.04.20140416/autopilot/tests/README 0000644 0000153 0177776 00000000571 12323560055 022043 0 ustar pbuser nogroup 0000000 0000000 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-1.4+14.04.20140416/autopilot/process/ 0000755 0000153 0177776 00000000000 12323561350 021473 5 ustar pbuser nogroup 0000000 0000000 autopilot-1.4+14.04.20140416/autopilot/process/__init__.py 0000644 0000153 0177776 00000035563 12323560055 023621 0 ustar pbuser nogroup 0000000 0000000 # -*- 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 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': 'mahjongg.desktop',
'process-name': 'gnome-mahjongg',
},
'Remmina': {
'desktop-file': 'remmina.desktop',
'process-name': 'remmina',
},
'System Settings': {
'desktop-file': 'gnome-control-center.desktop',
'process-name': 'gnome-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()
def get_upa_pm():
from autopilot.process._upa 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-1.4+14.04.20140416/autopilot/process/_bamf.py 0000644 0000153 0177776 00000054046 12323560055 023123 0 ustar pbuser nogroup 0000000 0000000 # -*- 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"""
from __future__ import absolute_import
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
from autopilot.dbus_handler import get_session_bus
from autopilot.utilities import (
addCleanup,
Silence,
)
from autopilot._timeout import Timeout
from autopilot.process import (
ProcessManager as ProcessManagerBase,
Application as ApplicationBase,
Window as WindowBase
)
_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.
from gi import require_version
require_version('GdkX11', '3.0')
from gi.repository import Gdk, 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 not self._x_win is 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-1.4+14.04.20140416/autopilot/logging.py 0000644 0000153 0177776 00000003326 12323560055 022022 0 ustar pbuser nogroup 0000000 0000000 # -*- 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-1.4+14.04.20140416/autopilot/__init__.py 0000644 0000153 0177776 00000001633 12323560055 022132 0 ustar pbuser nogroup 0000000 0000000 # -*- 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
__all__ = [
'get_version_string',
'have_vis',
'version',
]
autopilot-1.4+14.04.20140416/autopilot/testcase.py 0000644 0000153 0177776 00000051515 12323560055 022212 0 ustar pbuser nogroup 0000000 0000000 # -*- 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 .
#
"""
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.
"""
from __future__ import absolute_import
import logging
import os
import six
from testscenarios import TestWithScenarios
from testtools import TestCase
from testtools.matchers import Equals
from autopilot.application import (
ClickApplicationLauncher,
get_application_launcher_wrapper,
NormalApplicationLauncher,
UpstartApplicationLauncher,
)
from autopilot.display import Display
from autopilot.globals import get_debug_profile_fixture
from autopilot.input import Keyboard, Mouse
from autopilot.introspection import (
get_proxy_object_for_existing_process,
)
from autopilot.keybindings import KeybindingsHelper
from autopilot.matchers import Eventually
from autopilot.process import ProcessManager
from autopilot.utilities import deprecated, on_test_started
from autopilot._timeout import Timeout
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 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.
"""
def setUp(self):
super(AutopilotTestCase, self).setUp()
on_test_started(self)
self.useFixture(get_debug_profile_fixture()(self.addDetail))
_lttng_trace_test_started(self.id())
self.addCleanup(_lttng_trace_test_ended, self.id())
self._process_manager = None
self._mouse = None
self._kb = None
self._display = None
# Work around for bug lp:1297592.
_ensure_uinput_device_created()
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.")
@property
def process_manager(self):
if self._process_manager is None:
self._process_manager = ProcessManager.create()
return self._process_manager
@property
def keyboard(self):
if self._kb is None:
self._kb = Keyboard.create()
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.
:raises ValueError: if unknown keyword arguments are passed.
: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)
)
launcher = self.useFixture(
NormalApplicationLauncher(self.addDetail, **kwargs)
)
return self._launch_test_application(launcher, application, *arguments)
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
"""
if isinstance(app_uris, (six.text_type, six.binary_type)):
app_uris = [app_uris]
_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)
)
launcher = self.useFixture(
ClickApplicationLauncher(self.addDetail, **kwargs)
)
return self._launch_test_application(launcher, package_id, app_name,
app_uris)
def launch_upstart_application(self, application_name, uris=[], **kwargs):
"""Launch an application with upstart.
This method launched an application via the ``upstart-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.
: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.
:raises ValueError: If unknown keyword arguments are specified.
"""
if isinstance(uris, (six.text_type, six.binary_type)):
uris = [uris]
_logger.info(
"Attempting to launch application '%s' with URIs '%s' via "
"upstart-app-launch",
application_name,
','.join(uris)
)
launcher = self.useFixture(
UpstartApplicationLauncher(self.addDetail, **kwargs)
)
return self._launch_test_application(launcher, application_name, uris)
# Wrapper function tying the newer ApplicationLauncher behaviour with the
# previous (to be depreciated) behaviour
def _launch_test_application(self, launcher_instance, application, *args):
dbus_bus = launcher_instance.dbus_bus
if dbus_bus != 'session':
self.patch_environment("DBUS_SESSION_BUS_ADDRESS", dbus_bus)
pid = launcher_instance.launch(application, *args)
process = getattr(launcher_instance, 'process', None)
application_name = getattr(
launcher_instance,
'dbus_application_name',
None
)
proxy_obj = get_proxy_object_for_existing_process(
pid=pid,
process=process,
dbus_bus=dbus_bus,
emulator_base=launcher_instance.emulator_base,
application_name=application_name,
)
proxy_obj.set_process(process)
return proxy_obj
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 patch_environment(self, key, value):
"""Patch the process environment, setting *key* with value *value*.
This patches os.environ for the duration of the test only. After
calling this method, the following should be True::
os.environ[key] == value
After the test, the patch will be undone (including deleting the key if
if didn't exist before this method was called).
.. note:: Be aware that patching the environment in this way only
affects the current autopilot process, and any processes spawned by
autopilot. If you are planing on starting an application from within
autopilot and you want this new application to read the patched
environment variable, you must patch the environment *before*
launching the new process.
:param string key: The name of the key you wish to set. If the key
does not already exist in the process environment it will be created
(and then deleted when the test ends).
:param string value: The value you wish to set.
"""
if key in os.environ:
def _undo_patch(key, old_value):
_logger.info(
"Resetting environment variable '%s' to '%s'",
key,
old_value
)
os.environ[key] = old_value
old_value = os.environ[key]
self.addCleanup(_undo_patch, key, old_value)
else:
def _remove_patch(key):
try:
_logger.info(
"Deleting previously-created environment "
"variable '%s'",
key
)
del os.environ[key]
except KeyError:
_logger.warning(
"Attempted to delete environment key '%s' that doesn't"
"exist in the environment",
key
)
self.addCleanup(_remove_patch, key)
_logger.info(
"Setting environment variable '%s' to '%s'",
key,
value
)
os.environ[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
@deprecated(
"the 'app_type' argument to the launch_test_application method"
)
def pick_app_launcher(self, app_path):
"""Given an application path, return an object suitable for launching
the application.
This function attempts to guess what kind of application you are
launching. If, for some reason the default implementation returns the
wrong launcher, test authors may override this method to provide their
own implemetnation.
The default implementation calls
:py:func:`autopilot.application.get_application_launcher_wrapper`
"""
# default implementation is in autopilot.application:
return get_application_launcher_wrapper(app_path)
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)
)
autopilot-1.4+14.04.20140416/autopilot/display/ 0000755 0000153 0177776 00000000000 12323561350 021462 5 ustar pbuser nogroup 0000000 0000000 autopilot-1.4+14.04.20140416/autopilot/display/_X11.py 0000644 0000153 0177776 00000005346 12323560055 022555 0 ustar pbuser nogroup 0000000 0000000 # -*- 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.display import Display as DisplayBase
from autopilot.utilities import Silence
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.
with Silence():
from gi import require_version
require_version('Gdk', '3.0')
from gi.repository 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._default_screen.get_width()
return self.get_screen_geometry(screen_number)[2]
def get_screen_height(self, screen_number=0):
#return self._default_screen.get_height()
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-1.4+14.04.20140416/autopilot/display/__init__.py 0000644 0000153 0177776 00000013350 12323560055 023576 0 ustar pbuser nogroup 0000000 0000000 # -*- 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
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 (x >= mx and x < mx + mw and y >= my and 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)
# veebers TODO: Write this so it's usable.
# def drag_window_to_screen(self, window, screen):
# """Drags *window* to *screen*
# :param BamfWindow window: The window to drag
# :param integer screen: The screen to drag the *window* to
# :raises: **TypeError** if *window* is not a BamfWindow
# """
# if not isinstance(window, BamfWindow):
# raise TypeError("Window must be a BamfWindow")
# if window.monitor == screen:
# logger.debug(
# "Window %r is already on screen %d." % (window.x_id, screen))
# return
# assert(not window.is_maximized)
# (win_x, win_y, win_w, win_h) = window.geometry
# (mx, my, mw, mh) = self.get_screen_geometry(screen)
# logger.debug("Dragging window %r to screen %d." % (window.x_id, screen))
# mouse = Mouse()
# keyboard = Keyboard()
# mouse.move(win_x + win_w/2, win_y + win_h/2)
# keyboard.press("Alt")
# mouse.press()
# keyboard.release("Alt")
# # We do the movements in two steps, to reduce the risk of being
# # blocked by the pointer barrier
# target_x = mx + mw/2
# target_y = my + mh/2
# mouse.move(win_x, target_y, rate=20, time_between_events=0.005)
# mouse.move(target_x, target_y, rate=20, time_between_events=0.005)
# mouse.release()
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-1.4+14.04.20140416/autopilot/display/_upa.py 0000644 0000153 0177776 00000005371 12323560055 022767 0 ustar pbuser nogroup 0000000 0000000 # -*- 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.display import Display as DisplayBase
from autopilot.platform import image_codename
import subprocess
def query_resolution():
try:
return _get_fbset_resolution()
except Exception:
return _get_hardcoded_resolution()
def _get_fbset_resolution():
"""Return the resolution, as determined by fbset, or None."""
fbset_output = _get_fbset_output()
for line in fbset_output.split('\n'):
line = line.strip()
if line.startswith('Mode'):
quoted_resolution = line.split()[1]
resolution_string = quoted_resolution.strip('"')
return tuple(int(piece) for piece in resolution_string.split('x'))
raise RuntimeError("No modes found from fbset output")
def _get_fbset_output():
return subprocess.check_output(["fbset", "-s", "-x"]).decode().strip()
def _get_hardcoded_resolution():
name = image_codename()
resolutions = {
"generic": (480, 800),
"mako": (768, 1280),
"maguro": (720, 1280),
"manta": (2560, 1600),
"grouper": (800, 1280),
}
if name not in resolutions:
raise NotImplementedError(
'Device "{}" is not supported by Autopilot.'.format(name))
return resolutions[name]
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-1.4+14.04.20140416/autopilot/dbus_handler.py 0000644 0000153 0177776 00000003545 12323560055 023031 0 ustar pbuser nogroup 0000000 0000000 # -*- 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 __future__ import absolute_import
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-1.4+14.04.20140416/autopilot/vis/ 0000755 0000153 0177776 00000000000 12323561350 020616 5 ustar pbuser nogroup 0000000 0000000 autopilot-1.4+14.04.20140416/autopilot/vis/objectproperties.py 0000644 0000153 0177776 00000016536 12323560055 024567 0 ustar pbuser nogroup 0000000 0000000 # -*- 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."""
import six
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)
if six.PY3:
header_titles = ["Name", "Value"]
else:
header_titles = QtCore.QStringList(["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 True
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 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 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-1.4+14.04.20140416/autopilot/vis/dbus_search.py 0000644 0000153 0177776 00000010113 12323560055 023447 0 ustar pbuser nogroup 0000000 0000000 # -*- 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
from xml.etree import ElementTree
_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 = ElementTree.fromstring(xml)
for child in root.getchildren():
child_name = join(obj_name, child.attrib['name'])
# 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 ElementTree.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-1.4+14.04.20140416/autopilot/vis/__init__.py 0000644 0000153 0177776 00000003034 12323560055 022730 0 ustar pbuser nogroup 0000000 0000000 # -*- 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-1.4+14.04.20140416/autopilot/vis/bus_enumerator.py 0000644 0000153 0177776 00000005476 12323560055 024237 0 ustar pbuser nogroup 0000000 0000000 # -*- 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-1.4+14.04.20140416/autopilot/vis/resources.py 0000644 0000153 0177776 00000003432 12323560055 023205 0 ustar pbuser nogroup 0000000 0000000 # -*- 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
from PyQt4 import QtGui
def get_qt_icon():
return QtGui.QIcon(":/trolltech/qmessagebox/images/qtlogo-64.png")
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-1.4+14.04.20140416/autopilot/vis/main_window.py 0000644 0000153 0177776 00000021174 12323560055 023511 0 ustar pbuser nogroup 0000000 0000000 # -*- 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 __future__ import absolute_import
import dbus
import logging
from PyQt4 import QtGui, QtCore
import six
from autopilot.introspection import (
_get_dbus_address_object,
_make_proxy_object_async
)
from autopilot.introspection.constants import AP_INTROSPECTION_IFACE
from autopilot.introspection.dbus import StateNotFoundError
from autopilot.introspection.qt import QtObjectProxyMixin
from autopilot.vis.objectproperties import TreeNodeDetailWidget
from autopilot.vis.resources import 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
def readSettings(self):
settings = QtCore.QSettings()
if six.PY3:
self.restoreGeometry(settings.value("geometry").data())
self.restoreState(settings.value("windowState").data())
else:
self.restoreGeometry(settings.value("geometry").toByteArray())
self.restoreState(settings.value("windowState").toByteArray())
def closeEvent(self, event):
settings = QtCore.QSettings()
settings.setValue("geometry", self.saveGeometry())
settings.setValue("windowState", self.saveState())
def initUI(self):
self.statusBar().showMessage('Waiting for first valid dbus connection')
self.splitter = QtGui.QSplitter(self)
self.splitter.setChildrenCollapsible(False)
self.tree_view = QtGui.QTreeView(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 = QtGui.QComboBox()
self.connection_list.setSizeAdjustPolicy(
QtGui.QComboBox.AdjustToContents)
self.connection_list.activated.connect(self.conn_list_activated)
self.toolbar = self.addToolBar('Connection')
self.toolbar.setObjectName('Connection Toolbar')
self.toolbar.addWidget(self.connection_list)
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 not cls_name 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):
dbus_details = self.connection_list.itemData(index)
if not six.PY3:
dbus_details = dbus_details.toPyObject()
if dbus_details:
self.tree_model = VisTreeModel(dbus_details)
self.tree_view.setModel(self.tree_model)
self.tree_view.selectionModel().currentChanged.connect(
self.tree_item_changed)
def tree_item_changed(self, current, previous):
proxy = current.internalPointer().dbus_object
self.detail_widget.tree_node_changed(proxy)
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, name='', dbus_object=None):
self.parent = parent
self.name = 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():
name = child.__class__.__name__
self._children.append(TreeNode(self, name, 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.
"""
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, introspectable_obj):
super(VisTreeModel, self).__init__()
name = introspectable_obj.__class__.__name__
self.tree_root = TreeNode(name=name, dbus_object=introspectable_obj)
def index(self, row, col, parent):
if not self.hasIndex(row, col, parent):
return QtCore.QModelIndex()
if not parent.isValid():
parentItem = self.tree_root
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 == self.tree_root:
return QtCore.QModelIndex()
row = parentItem.children.index(childItem)
return self.createIndex(row, 0, parentItem)
def rowCount(self, parent):
if not parent.isValid():
p_Item = self.tree_root
else:
p_Item = parent.internalPointer()
return p_Item.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):
try:
return "Tree Node"
except IndexError:
pass
return None
autopilot-1.4+14.04.20140416/autopilot/platform.py 0000644 0000153 0177776 00000013041 12323560055 022213 0 ustar pbuser nogroup 0000000 0000000 # -*- 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 .
#
"""
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
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-1.4+14.04.20140416/autopilot/matchers/ 0000755 0000153 0177776 00000000000 12323561350 021623 5 ustar pbuser nogroup 0000000 0000000 autopilot-1.4+14.04.20140416/autopilot/matchers/__init__.py 0000644 0000153 0177776 00000012234 12323560055 023737 0 ustar pbuser nogroup 0000000 0000000 # -*- 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 __future__ import absolute_import
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-1.4+14.04.20140416/autopilot/globals.py 0000644 0000153 0177776 00000020065 12323560055 022016 0 ustar pbuser nogroup 0000000 0000000 # -*- 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 __future__ import absolute_import
try:
# Python 2
from StringIO import StringIO
except ImportError:
# Python 3
from io import StringIO
from autopilot._debug import DebugProfile
from autopilot.utilities import LogFormatter, CleanupRegistered
from testtools.content import text_content
import subprocess
import os.path
import logging
logger = logging.getLogger(__name__)
def get_log_verbose():
"""Return true if the user asked for verbose logging."""
return _test_logger._log_verbose
class _TestLogger(CleanupRegistered):
"""A class that handles adding test logs as test result content."""
def __init__(self):
self._log_verbose = False
self._log_buffer = None
def __call__(self, test_instance):
self._setUpTestLogging(test_instance)
if self._log_verbose:
global logger
logger.info("*" * 60)
logger.info("Starting test %s", test_instance.shortDescription())
@classmethod
def on_test_start(cls, test_instance):
if _test_logger._log_verbose:
_test_logger(test_instance)
def log_verbose(self, verbose):
self._log_verbose = verbose
def _setUpTestLogging(self, test_instance):
if self._log_buffer is None:
self._log_buffer = StringIO()
root_logger = logging.getLogger()
root_logger.setLevel(logging.DEBUG)
formatter = LogFormatter()
self._log_handler = logging.StreamHandler(stream=self._log_buffer)
self._log_handler.setFormatter(formatter)
root_logger.addHandler(self._log_handler)
test_instance.addCleanup(self._tearDownLogging, test_instance)
def _tearDownLogging(self, test_instance):
root_logger = logging.getLogger()
self._log_handler.flush()
self._log_buffer.seek(0)
test_instance.addDetail(
'test-log', text_content(self._log_buffer.getvalue()))
root_logger.removeHandler(self._log_handler)
self._log_buffer = None
_test_logger = _TestLogger()
def set_log_verbose(verbose):
"""Set whether or not we should log verbosely."""
if type(verbose) is not bool:
raise TypeError("Verbose flag must be a boolean.")
_test_logger.log_verbose(verbose)
class _VideoLogger(CleanupRegistered):
"""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):
self._enable_recording = False
self._currently_recording_description = None
def __call__(self, test_instance):
if not self._have_recording_app():
logger.warning(
"Disabling video capture since '%s' is not present",
self._recording_app)
if self._currently_recording_description is not None:
logger.warning(
"Video capture already in progress for %s",
self._currently_recording_description)
return
self._currently_recording_description = \
test_instance.shortDescription()
self._test_passed = True
test_instance.addOnException(self._on_test_failed)
test_instance.addCleanup(self._stop_video_capture, test_instance)
self._start_video_capture(test_instance.shortDescription())
@classmethod
def on_test_start(cls, test_instance):
if _video_logger._enable_recording:
_video_logger(test_instance)
def enable_recording(self, enable_recording):
self._enable_recording = enable_recording
def set_recording_dir(self, directory):
self.recording_directory = directory
def set_recording_opts(self, opts):
if opts is None:
return
self._recording_opts = opts.split(',') + self._recording_opts
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)
)
self._ensure_directory_exists_but_not_file(self._capture_file)
args.append(self._capture_file)
logger.debug("Starting: %r", args)
self._capture_process = subprocess.Popen(
args,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT
)
def _stop_video_capture(self, test_instance):
"""Stop the video capture. If the test failed, save the resulting
file."""
if self._test_passed:
# We use kill here because we don't want the recording app to start
# encoding the video file (since we're removing it anyway.)
self._capture_process.kill()
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',
text_content(self._capture_process.stdout.read()))
self._capture_process = None
self._currently_recording_description = None
def _get_capture_command_line(self):
return [self._recording_app] + self._recording_opts
def _ensure_directory_exists_but_not_file(self, 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)
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
_video_logger = _VideoLogger()
def configure_video_recording(enable_recording, record_dir, record_opts=None):
"""Configure video logging.
enable_recording is a boolean, and enables or disables recording globally.
record_dir is a string that specifies where videos will be stored.
"""
if type(enable_recording) is not bool:
raise TypeError("enable_recording must be a boolean.")
if not isinstance(record_dir, str):
raise TypeError("record_dir must be a string.")
_video_logger.enable_recording(enable_recording)
_video_logger.set_recording_dir(record_dir)
_video_logger.set_recording_opts(record_opts)
_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
autopilot-1.4+14.04.20140416/autopilot/_info.py 0000644 0000153 0177776 00000004753 12323560055 021473 0 ustar pbuser nogroup 0000000 0000000 # -*- 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.4.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",
"python-autopilot",
],
universal_newlines=True
).strip()
except subprocess.CalledProcessError:
return None
autopilot-1.4+14.04.20140416/autopilot/run.py 0000644 0000153 0177776 00000062320 12323560055 021177 0 ustar pbuser nogroup 0000000 0000000 # -*- 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 __future__ import absolute_import
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 six
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._debug import (
get_all_debug_profiles,
get_default_debug_profile,
)
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 _parse_arguments(argv=None):
"""Parse command-line arguments, and return an argparse arguments
object.
"""
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.")
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 to also log data 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 "
"useful if autopilot is running on very slow hardware"
)
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).")
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.DEBUG)
if verbose == 0:
set_null_log_handler(root_logger)
if verbose >= 1:
set_stderr_stream_handler(root_logger)
if verbose >= 2:
enable_debug_log_messages()
#log autopilot version
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):
if six.PY2:
return open(
log_file,
'w'
)
else:
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_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.
"""
if args.record_directory:
args.record = True
if args.record:
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."
)
autopilot.globals.configure_video_recording(
True,
args.record_directory,
args.record_options
)
def _have_video_recording_facilities():
call_ret_code = subprocess.call(
['which', 'recordmydesktop'],
stdout=subprocess.PIPE
)
return call_ret_code == 0
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))
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:
#
#https://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`."""
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")
_configure_debug_profile(self.args)
_configure_timeout_profile(self.args)
try:
_configure_video_recording(self.args)
except RuntimeError as e:
print("Error: %s" % str(e))
exit(1)
if self.args.verbose:
autopilot.globals.set_log_verbose(True)
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-1.4+14.04.20140416/autopilot/gestures.py 0000644 0000153 0177776 00000005221 12323560055 022231 0 ustar pbuser nogroup 0000000 0000000 # -*- 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-1.4+14.04.20140416/autopilot/introspection/ 0000755 0000153 0177776 00000000000 12323561350 022715 5 ustar pbuser nogroup 0000000 0000000 autopilot-1.4+14.04.20140416/autopilot/introspection/gtk.py 0000644 0000153 0177776 00000002531 12323560055 024056 0 ustar pbuser nogroup 0000000 0000000 # -*- 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 autopilot.introspection import ApplicationLauncher
class GtkApplicationLauncher(ApplicationLauncher):
"""A mix-in class to make Gtk application introspection easier."""
def prepare_environment(self, app_path, arguments):
"""Prepare the application, or environment to launch with
autopilot-support.
"""
modules = os.getenv('GTK_MODULES', '').split(':')
if 'autopilot' not in modules:
modules.append('autopilot')
os.putenv('GTK_MODULES', ':'.join(modules))
return app_path, arguments
autopilot-1.4+14.04.20140416/autopilot/introspection/__init__.py 0000644 0000153 0177776 00000053554 12323560055 025043 0 ustar pbuser nogroup 0000000 0000000 # -*- 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 introspection support.
This package contains the internal implementation of the autopilot
introspection mechanism, and probably isn't useful to most test authors.
"""
from __future__ import absolute_import
from dbus import DBusException, Interface
import logging
import subprocess
from functools import partial
import os
import psutil
from six import u
from autopilot.dbus_handler import (
get_session_bus,
get_system_bus,
get_custom_bus,
)
from autopilot.introspection.backends import DBusAddress
from autopilot.introspection.constants import (
AUTOPILOT_PATH,
QT_AUTOPILOT_IFACE,
AP_INTROSPECTION_IFACE,
)
from autopilot.introspection.dbus import (
CustomEmulatorBase,
DBusIntrospectionObject,
get_classname_from_path,
)
from autopilot.introspection.utilities import (
_get_bus_connections_pid,
_pid_is_running,
)
from autopilot._timeout import Timeout
_logger = logging.getLogger(__name__)
# Keep track of known connections during search
connection_list = []
class ProcessSearchError(RuntimeError):
pass
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(
pid=None, dbus_bus='session', connection_name=None, process=None,
object_path=AUTOPILOT_PATH, application_name=None, emulator_base=None):
"""Return a single proxy object for an application that is already running
(i.e. launched outside of Autopilot).
Searches on the given bus (supplied by **dbus_bus**) for an application
matching the search criteria, creating the proxy object using the supplied
custom emulator **emulator_base** (defaults to None).
For example for 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')
:param pid: The PID of the application to search for.
:param dbus_bus: A string containing either 'session', 'system' or the
custom buses name (i.e. 'unix:abstract=/tmp/dbus-IgothuMHNk').
:param connection_name: A string containing the DBus connection name to
use with the search criteria.
:param object_path: A string containing the object path to use as the
search criteria. Defaults to
:py:data:`autopilot.introspection.constants.AUTOPILOT_PATH`.
:param application_name: A string containing the applications name to
search for.
:param emulator_base: The custom emulator to create the resulting proxy
object with.
: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``.
"""
pid = _check_process_and_pid_details(process, pid)
dbus_addresses = _get_dbus_addresses_from_search_parameters(
pid,
dbus_bus,
connection_name,
object_path,
process
)
dbus_addresses = _maybe_filter_connections_by_app_name(
application_name,
dbus_addresses
)
if dbus_addresses is None or len(dbus_addresses) == 0:
criteria_string = _get_search_criteria_string_representation(
pid,
dbus_bus,
connection_name,
process,
object_path,
application_name
)
message_string = "Search criteria (%s) returned no results" % \
(criteria_string)
raise ProcessSearchError(message_string)
if len(dbus_addresses) > 1:
criteria_string = _get_search_criteria_string_representation(
pid,
dbus_bus,
connection_name,
process,
object_path,
application_name
)
message_string = "Search criteria (%s) returned multiple results" % \
(criteria_string)
raise RuntimeError(message_string)
return _make_proxy_object(dbus_addresses[0], emulator_base)
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 _maybe_filter_connections_by_app_name(application_name, dbus_addresses):
"""Filter `dbus_addresses` by the application name exported, if
`application_name` has been specified.
:returns: a filtered list of connections.
"""
if application_name:
dbus_addresses = [
a for a in dbus_addresses
if _get_application_name_from_dbus_address(a) == application_name
]
return dbus_addresses
def _get_application_name_from_dbus_address(dbus_address):
"""Return the application name from a dbus_address object."""
return get_classname_from_path(
dbus_address.introspection_iface.GetState('/')[0][0]
)
def _get_search_criteria_string_representation(
pid=None, dbus_bus=None, connection_name=None, process=None,
object_path=None, application_name=None):
"""Get a string representation of the search criteria.
Used to represent the search criteria to users in error messages.
"""
description_parts = []
if pid is not None:
description_parts.append(u('pid = %d') % pid)
if dbus_bus is not None:
description_parts.append(u("dbus bus = '%s'") % dbus_bus)
if connection_name is not None:
description_parts.append(
u("connection name = '%s'") % connection_name
)
if object_path is not None:
description_parts.append(u("object path = '%s'") % object_path)
if application_name is not None:
description_parts.append(
u("application name = '%s'") % application_name
)
if process is not None:
description_parts.append(u("process object = '%r'") % process)
return ", ".join(description_parts)
def _get_dbus_addresses_from_search_parameters(
pid, dbus_bus, connection_name, object_path, process):
"""Returns a list of :py:class: `DBusAddress` for all successfully matched
criteria.
"""
_reset_known_connection_list()
for _ in Timeout.default():
_get_child_pids.reset_cache()
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
)
bus = _get_dbus_bus_from_string(dbus_bus)
valid_connections = _search_for_valid_connections(
pid,
bus,
connection_name,
object_path
)
if len(valid_connections) >= 1:
return [_get_dbus_address_object(name, object_path, bus) for name
in valid_connections]
return []
def _reset_known_connection_list():
global connection_list
del connection_list[:]
def _search_for_valid_connections(pid, bus, connection_name, object_path):
global connection_list
def _get_unchecked_connections(all_connections):
return list(set(all_connections).difference(set(connection_list)))
possible_connections = _get_possible_connections(bus, connection_name)
connection_list = _get_unchecked_connections(possible_connections)
valid_connections = _get_valid_connections(
connection_list,
bus,
pid,
object_path
)
return valid_connections
def _process_is_running(process):
return process.poll() is None
def _get_valid_connections(connections, bus, pid, object_path):
filter_fn = partial(_match_connection, bus, pid, object_path)
valid_connections = filter(filter_fn, connections)
unique_connections = _dedupe_connections_on_pid(valid_connections, bus)
return unique_connections
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 _get_dbus_address_object(connection_name, object_path, bus):
return DBusAddress(bus, connection_name, object_path)
def _get_dbus_bus_from_string(dbus_string):
if dbus_string == 'session':
return get_session_bus()
elif dbus_string == 'system':
return get_system_bus()
else:
return get_custom_bus(dbus_string)
def _get_possible_connections(bus, connection_name):
all_connection_names = bus.list_names()
if connection_name is None:
return all_connection_names
else:
matching_connections = [
c for c in all_connection_names if c == connection_name]
return matching_connections
def _match_connection(bus, pid, path, connection_name):
"""Does the connection match our search criteria?"""
success = True
if pid is not None:
success = _connection_matches_pid(bus, connection_name, pid)
if success:
success = _connection_has_path(bus, connection_name, path)
return success
def _connection_matches_pid(bus, connection_name, pid):
"""Given a PID checks wherever it or its children are connected on this
bus.
"""
if connection_name == 'org.freedesktop.DBus':
return False
try:
if _bus_pid_is_our_pid(bus, connection_name, pid):
return False
bus_pid = _get_bus_connections_pid(bus, connection_name)
except 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
def _bus_pid_is_our_pid(bus, connection_name, pid):
"""Returns True if this scripts pid is the bus connections pid supplied."""
bus_pid = _get_bus_connections_pid(bus, connection_name)
return bus_pid == os.getpid()
def _connection_has_path(bus, connection_name, path):
"""Ensure the connection has the path that we expect to be there."""
try:
_check_connection_has_ap_interface(bus, connection_name, path)
return True
except DBusException:
return False
def _check_connection_has_ap_interface(bus, connection_name, path):
"""Simple check if a bus with connection + path provide the Autopilot
Introspection Interface.
:raises: **DBusException** if it does not.
"""
obj = bus.get_object(connection_name, path)
obj_iface = Interface(obj, 'com.canonical.Autopilot.Introspection')
obj_iface.GetVersion()
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).get_children(recursive=True)
]
return self._cached_result
def reset_cache(self):
self._cached_result = None
_get_child_pids = _cached_get_child_pids()
def _make_proxy_object(data_source, emulator_base):
"""Returns a root proxy object given a DBus service name.
:param data_source: The dbus backend address object we're querying.
:param emulator_base: The emulator base object (or None), as provided by
the user.
"""
intro_xml = _get_introspection_xml_from_backend(data_source)
cls_name, path, cls_state = _get_proxy_object_class_name_and_state(
data_source
)
try:
proxy_bases = _get_proxy_bases_from_introspection_xml(intro_xml)
except RuntimeError as e:
e.args = (
"Could not find Autopilot interface on dbus address '%s'."
% data_source,
)
raise e
proxy_bases = _extend_proxy_bases_with_emulator_base(
proxy_bases,
emulator_base
)
proxy_class = _make_proxy_class_object(cls_name, proxy_bases)
return proxy_class(cls_state, path, data_source)
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):
proxy_bases = _get_proxy_bases_from_introspection_xml(
introspection_xml
)
proxy_bases = _extend_proxy_bases_with_emulator_base(
proxy_bases,
emulator_base
)
proxy_class = _make_proxy_class_object(cls_name, proxy_bases)
reply_handler(proxy_class(cls_state, path, 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.
_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_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 = [ApplicationProxyObject]
if AP_INTROSPECTION_IFACE not in introspection_xml:
raise RuntimeError("Could not find Autopilot interface.")
if QT_AUTOPILOT_IFACE in introspection_xml:
from autopilot.introspection.qt import QtObjectProxyMixin
bases.append(QtObjectProxyMixin)
return tuple(bases)
def _extend_proxy_bases_with_emulator_base(proxy_bases, emulator_base):
if emulator_base is None:
emulator_base = type('DefaultEmulatorBase', (CustomEmulatorBase,), {})
return proxy_bases + (emulator_base, )
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,
object_state,
)
def _make_proxy_class_object(cls_name, proxy_bases):
"""Return a class object for a proxy.
:param cls_name: The name of the class to be created. This is usually the
type as returned over the wire from an autopilot backend.
:param proxy_bases: A list of base classes this proxy class should derive
from. This determines the specific abilities of the new proxy.
"""
clsobj = type(str("%sBase" % cls_name), proxy_bases, {})
proxy_class = type(str(cls_name), (clsobj,), {})
return proxy_class
class ApplicationProxyObject(DBusIntrospectionObject):
"""A class that better supports query data from an application."""
def __init__(self, state, path, backend):
super(ApplicationProxyObject, self).__init__(state, path, backend)
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
def kill_application(self):
"""Kill the running process that this is a proxy for using
'kill `pid`'."""
subprocess.call(["kill", "%d" % self._process.pid])
autopilot-1.4+14.04.20140416/autopilot/introspection/types.py 0000644 0000153 0177776 00000053245 12323560055 024445 0 ustar pbuser nogroup 0000000 0000000 # -*- 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.
"""
from __future__ import absolute_import
from datetime import datetime, time
import dbus
import logging
from testtools.matchers import Equals
import six
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,
}
if not six.PY3:
repr_map[dbus.UTF8String] = _bytes_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 six.text_type(int(self))
def _create_generic_repr(target_type):
return compatible_repr(lambda self: repr(target_type(self)))
_bytes_repr = _create_generic_repr(six.binary_type)
_text_repr = _create_generic_repr(six.text_type)
_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 = u', '.join((str(c) for c in self))
return u'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 u'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 u'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 u'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.
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.fromutctimestamp(1377209927)
True
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)
self._cached_dt = datetime.utcfromtimestamp(self[0])
@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[0]
@property
def datetime(self):
return self._cached_dt
def __eq__(self, other):
if isinstance(other, datetime):
return other == self._cached_dt
return super(DateTime, self).__eq__(other)
@compatible_repr
def __repr__(self):
return u'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 u'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 u'Point3D(%d, %d, %d)' % (
self.x,
self.y,
self.z,
)
autopilot-1.4+14.04.20140416/autopilot/introspection/dbus.py 0000644 0000153 0177776 00000075544 12323560055 024244 0 ustar pbuser nogroup 0000000 0000000 # -*- 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.
"""
from __future__ import absolute_import
from contextlib import contextmanager
import sys
import logging
import re
import six
from uuid import uuid4
from autopilot.introspection.types import create_value_instance
from autopilot.introspection.utilities import translate_state_keys
from autopilot.utilities import (
get_debug_logger,
sleep,
Timer,
)
_object_registry = {}
_logger = logging.getLogger(__name__)
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:`~DBusIntrospectionObject.select_single` or
:py:meth:`~DBusIntrospectionObject.wait_select_single` or
:py:meth:`~DBusIntrospectionObject.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 = \
u"Object not found with properties {}.".format(
repr(filters)
)
elif not filters:
self._message = u"Object not found with name '{}'.".format(
class_name
)
else:
self._message = \
u"Object not found with name '{}' and properties {}.".format(
class_name,
repr(filters)
)
def __str__(self):
if six.PY3:
return self._message
else:
return self._message.encode('utf8')
def __unicode__(self):
return self._message
class IntrospectableObjectMetaclass(type):
"""Metaclass to insert appropriate classes into the object registry."""
def __new__(cls, classname, bases, classdict):
"""Add class name to type registry."""
class_object = type.__new__(cls, classname, bases, classdict)
if classname in (
'ApplicationProxyObject',
'CustomEmulatorBase',
'DBusIntrospectionObject',
):
return class_object
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}
return class_object
def get_classname_from_path(object_path):
return object_path.split("/")[-1]
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
DBusIntrospectionObjectBase = IntrospectableObjectMetaclass(
'DBusIntrospectionObjectBase',
(object,),
{}
)
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.
"""
def __init__(self, state_dict, path, backend):
self.__state = {}
self.__refresh_on_attribute = True
self._set_properties(state_dict)
self._path = path
self._poll_time = 10
self._backend = backend
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'.
"""
self.__state = {}
for key, value in translate_state_keys(state_dict).items():
# don't store id in state dictionary -make it a proper instance
# attribute
if key == 'id':
self.id = int(value[1])
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`
"""
#TODO: if kwargs has exactly one item in it we should specify the
# restriction in the XPath query, so it gets processed in the Unity C++
# code rather than in Python.
instances = self.get_children()
result = []
for instance in instances:
# Skip items that are not instances of the desired type:
if isinstance(desired_type, six.string_types):
if instance.__class__.__name__ != desired_type:
continue
elif not isinstance(instance, desired_type):
continue
#skip instances that fail attribute check:
if _object_passes_filters(instance, **kwargs):
result.append(instance)
return result
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.
query = self.get_class_query_string() + "/*"
state_dicts = self.get_state_by_path(query)
children = [self.make_introspection_object(i) for i in state_dicts]
return children
def get_parent(self):
"""Returns the parent of this object.
If this object has no parent (i.e.- it is the root of the introspection
tree). Then it returns itself.
"""
query = self.get_class_query_string() + "/.."
parent_state_dicts = self.get_state_by_path(query)
parent = self.make_introspection_object(parent_state_dicts[0])
return parent
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 TypeError: 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`
"""
instances = self.select_many(type_name, **kwargs)
if len(instances) > 1:
raise ValueError("More than one item was returned for query")
if not instances:
raise StateNotFoundError(type_name, **kwargs)
return instances[0]
def wait_select_single(self, type_name='*', **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 10 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).
:raises ValueError: if the query returns more than one item. *If
you want more than one item, use select_many instead*.
:raises TypeError: 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`
"""
for i in range(self._poll_time):
try:
return self.select_single(type_name, **kwargs)
except StateNotFoundError:
if i == self._poll_time - 1:
raise
sleep(1)
def select_many(self, type_name='*', **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 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).
:raises TypeError: if neither *type_name* or keyword filters are
provided.
.. seealso::
Tutorial Section :ref:`custom_proxy_classes`
"""
if not isinstance(type_name, str) and issubclass(
type_name, DBusIntrospectionObject):
type_name = type_name.__name__
if type_name == "*" and not kwargs:
raise TypeError("You must specify either a type name or a filter.")
_logger.debug(
"Selecting objects of %s with attributes: %r",
'any type' if type_name == '*' else 'type ' + type_name, kwargs)
server_side_filters = []
client_side_filters = {}
for k, v in kwargs.items():
# LP Bug 1209029: The XPathSelect protocol does not allow all valid
# node names or values. We need to decide here whether the filter
# parameters are going to work on the backend or not. If not, we
# just do the processing client-side. See the
# _is_valid_server_side_filter_param function (below) for the
# specific requirements.
if _is_valid_server_side_filter_param(k, v):
server_side_filters.append(
_get_filter_string_for_key_value_pair(k, v)
)
else:
client_side_filters[k] = v
filter_str = '[{}]'.format(','.join(server_side_filters)) \
if server_side_filters else ""
query_path = "%s//%s%s" % (
self.get_class_query_string(),
type_name,
filter_str
)
state_dicts = self.get_state_by_path(query_path)
instances = [self.make_introspection_object(i) for i in state_dicts]
return [i for i in instances
if _object_passes_filters(i, **client_side_filters)]
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__
instances = self.get_state_by_path("//%s" % (cls_name))
return [self.make_introspection_object(i) for i in instances]
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.
"""
instances = self.get_state_by_path("/")
if len(instances) != 1:
_logger.error("Could not retrieve root object.")
return None
return self.make_introspection_object(instances[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_state_by_path(self, piece):
"""Get state for a particular piece of the state tree.
You should probably never need to call this directly.
:param piece: an XPath-like query that specifies which bit of the tree
you want to look at.
:raises TypeError: on invalid *piece* parameter.
"""
if not isinstance(piece, six.string_types):
raise TypeError(
"XPath query must be a string, not %r", type(piece))
with Timer("GetState %s" % piece):
data = self._backend.introspection_iface.GetState(piece)
if len(data) > 15:
_logger.warning(
"Your query '%s' 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.",
piece,
len(data)
)
return data
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.get_state_by_path(self.get_class_query_string())[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 get_class_query_string(self):
"""Get the XPath query string required to refresh this class's
state."""
if not self._path.startswith('/'):
return "//" + self._path + "[id=%d]" % self.id
else:
return self._path + "[id=%d]" % self.id
def make_introspection_object(self, dbus_tuple):
"""Make an introspection object given a DBus tuple of
(path, state_dict).
This only works for classes that derive from DBusIntrospectionObject.
: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
class_object = _get_proxy_object_class(
_object_registry[self._id], type(self), path, state)
return class_object(state, path, self._backend)
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, six.string_types):
output = open(output, 'w')
# print path
if _curdepth > 0:
output.write("\n")
output.write("%s== %s ==\n" % (indent, self._path))
# 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))
@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
"""
name = get_classname_from_path(path)
return cls.__name__ == 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) == six.binary_type:
return key_is_valid
elif type(value) == six.text_type:
try:
value.encode('ascii')
return key_is_valid
except UnicodeEncodeError:
pass
return False
def _get_filter_string_for_key_value_pair(key, value):
"""Return a string 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, six.text_type):
if six.PY3:
escaped_value = value.encode("unicode_escape")\
.decode('ASCII')\
.replace("'", "\\'")
else:
escaped_value = value.encode('utf-8').encode("string_escape")
# note: string_escape codec escapes "'" but not '"'...
escaped_value = escaped_value.replace('"', r'\"')
return '{}="{}"'.format(key, escaped_value)
elif isinstance(value, six.binary_type):
if six.PY3:
escaped_value = value.decode('utf-8')\
.encode("unicode_escape")\
.decode('ASCII')\
.replace("'", "\\'")
else:
escaped_value = value.encode("string_escape")
# note: string_escape codec escapes "'" but not '"'...
escaped_value = escaped_value.replace('"', r'\"')
return '{}="{}"'.format(key, escaped_value)
elif isinstance(value, int) or isinstance(value, bool):
return "{}={}".format(key, repr(value))
else:
raise ValueError("Unsupported value type: {}".format(type(value)))
def _get_proxy_object_class(proxy_class_dict, default_class, path, state):
"""Return a custom proxy class, either from the list or the default.
Use helper functions to check the class list or return the default.
:param proxy_class_dict: dict of proxy classes to try
:param default_class: default class to use if nothing in dict matches
: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(proxy_class_dict, path, state)
if class_type:
return class_type
return _get_default_proxy_class(default_class,
get_classname_from_path(path))
def _try_custom_proxy_classes(proxy_class_dict, 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 proxy_class_dict: 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
"""
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:
return possible_classes[0]
return None
def _get_default_proxy_class(default_class, name):
"""Return a custom proxy object class of the default or a base class.
We want the object to inherit from CustomEmulatorBase, not the object
class that is doing the selecting.
:param default_class: default class to use if no bases match
: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)
for base in default_class.__bases__:
if issubclass(base, CustomEmulatorBase):
base_class = base
break
else:
base_class = default_class
return type(str(name), (base_class,), {})
class _CustomEmulatorMeta(IntrospectableObjectMetaclass):
def __new__(cls, name, bases, d):
# only consider classes derived from CustomEmulatorBase
if name != 'CustomEmulatorBase':
# and only if they don't already have an Id set.
have_id = False
for base in bases:
if hasattr(base, '_id'):
have_id = True
break
if not have_id:
d['_id'] = uuid4()
return super(_CustomEmulatorMeta, cls).__new__(cls, name, bases, d)
CustomEmulatorBase = _CustomEmulatorMeta('CustomEmulatorBase',
(DBusIntrospectionObject, ),
{})
CustomEmulatorBase.__doc__ = \
"""This class must be used as a base class for any custom emulators defined
within a test case.
.. seealso::
Tutorial Section :ref:`custom_proxy_classes`
Information on how to write custom emulators.
"""
autopilot-1.4+14.04.20140416/autopilot/introspection/constants.py 0000644 0000153 0177776 00000002137 12323560055 025307 0 ustar pbuser nogroup 0000000 0000000 # -*- 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-1.4+14.04.20140416/autopilot/introspection/qt.py 0000644 0000153 0177776 00000013434 12323560055 023721 0 ustar pbuser nogroup 0000000 0000000 # -*- 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.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-1.4+14.04.20140416/autopilot/introspection/backends.py 0000644 0000153 0177776 00000015014 12323560055 025043 0 ustar pbuser nogroup 0000000 0000000 # -*- 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 interface for autopilot."
from __future__ import absolute_import
from collections import namedtuple
import dbus
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.introspection.utilities import (
_pid_is_running,
_get_bus_connections_pid,
)
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(
"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)
autopilot-1.4+14.04.20140416/autopilot/introspection/utilities.py 0000644 0000153 0177776 00000003215 12323560055 025304 0 ustar pbuser nogroup 0000000 0000000 # -*- 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 __future__ import absolute_import
from dbus import Interface
import os.path
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()}
autopilot-1.4+14.04.20140416/autopilot/input/ 0000755 0000153 0177776 00000000000 12323561350 021154 5 ustar pbuser nogroup 0000000 0000000 autopilot-1.4+14.04.20140416/autopilot/input/_common.py 0000644 0000153 0177776 00000005106 12323560055 023160 0 ustar pbuser nogroup 0000000 0000000 # -*- 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.
: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-1.4+14.04.20140416/autopilot/input/_X11.py 0000644 0000153 0177776 00000036561 12323560055 022252 0 ustar pbuser nogroup 0000000 0000000 # -*- 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.
"""
from __future__ import absolute_import
import logging
import six
from autopilot.display import is_point_on_any_screen, move_mouse_to_screen
from autopilot.utilities import (
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, six.string_types):
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, six.string_types):
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, six.string_types):
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, six.string_types):
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()
@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):
"""Click mouse at current location."""
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
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.
"""
try:
x, y, w, h = object_proxy.globalRect
_logger.debug("Moving to object's globalRect coordinates.")
self.move(x+w/2, y+h/2)
return
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.")
self.move(x, y)
return
except AttributeError:
pass
except (TypeError, ValueError):
raise ValueError(
"Object '%r' has center_x, center_y attributes, but they are "
"not of the correct type" % object_proxy)
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.")
self.move(x+w/2, y+h/2)
return
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)
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-1.4+14.04.20140416/autopilot/input/__init__.py 0000644 0000153 0177776 00000062237 12323560055 023300 0 ustar pbuser nogroup 0000000 0000000 # -*- 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 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
from autopilot.utilities import _pick_backend, CleanupRegistered
from autopilot.input._common import get_center_point
import logging
_logger = logging.getLogger(__name__)
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:
from autopilot.input._osk import Keyboard
return Keyboard()
except ImportError as e:
e.args += ("Unable to import the OSK backend",)
raise
backends = OrderedDict()
backends['X11'] = get_x11_kb
backends['UInput'] = get_uinput_kb
backends['OSK'] = get_osk_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()
from autopilot.platform import model
if model() != 'Desktop':
_logger.info(
"You cannot create a Mouse on the devices where X11 is not "
"available. consider using a Touch or Pointer device. "
"For more information, see: "
"http://unity.ubuntu.com/autopilot/api/input.html"
"#autopilot-unified-input-system"
)
raise RuntimeError(
"Cannot create a Mouse on devices where X11 is not available."
)
backends = OrderedDict()
backends['X11'] = get_x11_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):
"""Click mouse at current location."""
raise NotImplementedError("You cannot use this class directly.")
def click_object(self, object_proxy, button=1, press_duration=0.10):
"""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
: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)
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):
"""Click (or 'tap') at given x,y coordinates."""
raise NotImplementedError("You cannot use this class directly.")
def tap_object(self, object):
"""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
: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
self._x = 0
self._y = 0
@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.
"""
if isinstance(self._device, Mouse):
return self._device.x
else:
return self._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.
"""
if isinstance(self._device, Mouse):
return self._device.y
else:
return self._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):
"""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.
"""
if isinstance(self._device, Mouse):
self._device.click(button, press_duration)
else:
if button != 1:
raise ValueError(
"Touch devices do not have button %d" % (button))
self._device.tap(self._x, self._y)
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.
"""
if isinstance(self._device, Mouse):
self._device.move(x, y)
else:
self._x = x
self._y = y
def click_object(self, object_proxy, button=1, press_duration=0.10):
"""
Attempts to move the pointer to 'object_proxy's centre point.
and click a button
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
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.
"""
self.move_to_object(object_proxy)
self.click(button, press_duration)
def move_to_object(self, object_proxy):
"""Attempts to move the pointer 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.
"""
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)
if isinstance(self._device, Touch):
self._x = x2
self._y = y2
autopilot-1.4+14.04.20140416/autopilot/input/_uinput.py 0000644 0000153 0177776 00000047255 12323560055 023227 0 ustar pbuser nogroup 0000000 0000000 # -*- 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 .
#
"""UInput device drivers."""
import logging
import os.path
import six
from evdev import UInput, ecodes as e
import autopilot.platform
from autopilot.input import Keyboard as KeyboardBase
from autopilot.input import Touch as TouchBase
from autopilot.input._common import get_center_point
from autopilot.utilities import deprecated, sleep
_logger = logging.getLogger(__name__)
def _get_devnode_path():
"""Provide a fallback uinput node for devices which don't support udev"""
devnode = '/dev/autopilot-uinput'
if not os.path.exists(devnode):
devnode = '/dev/uinput'
return devnode
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, six.string_types):
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, six.string_types):
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, six.string_types):
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():
# android uses BTN_TOOL_FINGER, whereas desktop uses BTN_TOUCH. I have
# no idea why...
if autopilot.platform.model() == 'Desktop':
touch_tool = e.BTN_TOUCH
else:
touch_tool = e.BTN_TOOL_FINGER
return touch_tool
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, e.BTN_TOOL_FINGER, 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.')
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()
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, e.BTN_TOOL_FINGER, 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()
@property
def pressed(self):
return self._device.pressed
def tap(self, x, y):
"""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._device.finger_down(x, y)
sleep(0.1)
self._device.finger_up()
def tap_object(self, object_):
"""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)
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._device.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):
"""Moves the pointing "finger" 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.
"""
self._device.finger_move(x, y)
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._device.finger_down(x1, y1)
current_x, current_y = x1, y1
while current_x != x2 or current_y != y2:
dx = abs(x2 - current_x)
dy = abs(y2 - 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 x2 < current_x:
step_x *= -1
if y2 < 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)
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',
}
autopilot-1.4+14.04.20140416/autopilot/input/_osk.py 0000644 0000153 0177776 00000007311 12323560055 022464 0 ustar pbuser nogroup 0000000 0000000 # -*- 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 six
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, six.string_types):
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-1.4+14.04.20140416/autopilot/exceptions.py 0000644 0000153 0177776 00000002202 12323560055 022545 0 ustar pbuser nogroup 0000000 0000000 # -*- 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 .
#
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
autopilot-1.4+14.04.20140416/autopilot/keybindings.py 0000644 0000153 0177776 00000026563 12323560055 022712 0 ustar pbuser nogroup 0000000 0000000 # -*- 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.
"""
from __future__ import absolute_import
import logging
import re
import six
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, six.string_types):
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, six.string_types):
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, six.string_types):
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-1.4+14.04.20140416/autopilot/testresult.py 0000644 0000153 0177776 00000010004 12323560055 022601 0 ustar pbuser nogroup 0000000 0000000 # -*- 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"""
from __future__ import absolute_import
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, details):
"""Log the relavent test details."""
for detail in details:
# Skip the test-log as it was logged while the test executed
if detail == "test-log":
continue
text = "%s: {{{\n%s}}}" % (detail, details[detail].as_text())
self._log(level, text)
def addSuccess(self, test, details=None):
self._log(logging.INFO, "OK: %s" % (test.id()))
return super(LoggedTestResultDecorator, self).addSuccess(test, details)
def addError(self, test, err=None, details=None):
self._log(logging.ERROR, "ERROR: %s" % (test.id()))
if hasattr(test, "getDetails"):
self._log_details(logging.ERROR, test.getDetails())
return super(type(self), self).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()))
if hasattr(test, "getDetails"):
self._log_details(logging.ERROR, test.getDetails())
return super(type(self), self).addFailure(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-1.4+14.04.20140416/autopilot/clipboard.py 0000644 0000153 0177776 00000002403 12323560055 022326 0 ustar pbuser nogroup 0000000 0000000 # -*- 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."""
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.
"""
from gi import require_version
require_version('Gdk', '3.0')
require_version('Gtk', '3.0')
from gi.repository import Gtk, Gdk
cb = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
return cb.wait_for_text()
autopilot-1.4+14.04.20140416/autopilot/_timeout.py 0000644 0000153 0177776 00000006660 12323560055 022225 0 ustar pbuser nogroup 0000000 0000000 # -*- 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-1.4+14.04.20140416/autopilot/utilities.py 0000644 0000153 0177776 00000033332 12323560055 022407 0 ustar pbuser nogroup 0000000 0000000 # -*- 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 __future__ import absolute_import
from contextlib import contextmanager
from decorator import decorator
import functools
import inspect
import logging
import os
import six
import time
from functools import wraps
from autopilot.exceptions import BackendException
logger = logging.getLogger(__name__)
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 = time.time()
def __exit__(self, *args):
self.end = time.time()
self.logger.log(
self.log_level, "'%s' took %.3fS", self.code_name,
self.end - self.start)
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 six.PY3 and isinstance(result, six.text_type):
return result.encode('utf-8')
if six.PY3 and not isinstance(result, six.text_type):
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()
autopilot-1.4+14.04.20140416/bin/ 0000755 0000153 0177776 00000000000 12323561350 016545 5 ustar pbuser nogroup 0000000 0000000 autopilot-1.4+14.04.20140416/bin/autopilot-sandbox-run 0000755 0000153 0177776 00000010274 12323560055 022756 0 ustar pbuser nogroup 0000000 0000000 #!/bin/sh
#
# Runner to execute autopilot locally
#
# This scripts run autopilot in a "fake" X server, either headless with xvfb
# or nested with Xephyr
#
# Copyright (C) 2013 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_CMD="$(which autopilot)"
AP_OPT=""
SERVERNUM=5
SCREEN="1024x768x24"
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:X"
LONGOPTS="help,debug,autopilot:,screen:,xephyr"
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;;
-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
echo "I: Starting autopilot"
dbus-launch --exit-with-session autopilot run $AP_OPT $TESTLIST
echo "I: autopilot tests done"
RC=$?
autopilot-1.4+14.04.20140416/tox.ini 0000644 0000153 0177776 00000001056 12323560055 017313 0 ustar pbuser nogroup 0000000 0000000 # Tox (http://tox.testrun.org/) is a tool for running tests
# in multiple virtualenvs. This configuration file will run the
# test suite on all supported python versions.
#
# To use it, install the `tox` python package:
# "sudo pip install tox" or "sudo apt-get install python-tox"
# then run "tox" from this directory.
[tox]
envlist = flake8, py27, py33
[flake8]
exclude = .tox, debian, lttng_module
[testenv]
sitepackages = True
commands =
autopilot run {posargs:autopilot.tests}
[testenv:flake8]
deps =
flake8
commands =
flake8 autopilot
autopilot-1.4+14.04.20140416/autopilot.1 0000644 0000153 0177776 00000003371 12323560055 020104 0 ustar pbuser nogroup 0000000 0000000 .TH AUTOPILOT 1 LOCAL
.SH NAME
autopilot - functional test tool for Ubuntu
.SH SYNOPSYS
.B autopilot
[-h]
.br
.B autopilot list suite [suite...]
.br
.B autopilot run [-v] [-r] [--rd directory] suite [suite...]
.SH DESCRIPTION
.B autopilot is a tool for writing functional test suites for graphical
applications for Ubuntu.
.SH OPTIONS
.SS General Options
.TP 5
-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 command.
Further options are restricted to particular autopilot commands.
.TP 5
suite
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.
.SS list [options] suite [suite...]
List the autopilot tests found in the given test suite.
.TP 5
-ro, --run-order
List tests in the order they will be run in, rather than alphabetically (which
is the default).
.SS run [options]suite [suite...]
Run one or more test suites.
.TP 5
-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 filename of
_.log
.TP 5
-f FORMAT, --format FORMAT
Specify the format for the log. Valid options are 'xml' and 'text' for JUnit XML
and text format, respectively.
.TP 5
-r, --record
Record failed tests. Using this option requires the 'recordmydesktop'
application be installed. By default, videos are stored in /tmp/autopilot
.TP 5
-rd DIR, --record-directory DIR
Directory where videos should be stored (overrides the default set by the -r
option).
.TP 5
-v, --verbose
Causes autopilot to print the test log to stdout while the test is running.
.SH AUTHOR
Thomi Richards (thomi.richards@canonical.com)
autopilot-1.4+14.04.20140416/apparmor/ 0000755 0000153 0177776 00000000000 12323561350 017616 5 ustar pbuser nogroup 0000000 0000000 autopilot-1.4+14.04.20140416/apparmor/click.rules 0000644 0000153 0177776 00000000115 12323560055 021755 0 ustar pbuser nogroup 0000000 0000000 dbus (receive, send)
bus=session
path=/com/canonical/Autopilot/**,
autopilot-1.4+14.04.20140416/make_coverage.sh 0000755 0000153 0177776 00000003527 12323560055 021134 0 ustar pbuser nogroup 0000000 0000000 #!/bin/sh
#
# 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 .
#
set -e
SHOW_IN_BROWSER="yes"
INCLUDE_TEST_FILES="no"
usage() {
echo "usage: $0 [-h] [-n] [-t]"
echo
echo "Runs unit tests under both python2 and python 3, gathering coverage data."
echo "By default, will open HTML coverage report in the default browser. If -n"
echo "is specified, will re-generate coverage data, but won't open the browser."
echo "If -t is specified the HTML coverage report will include the test files."
}
while getopts ":hnt" o; do
case "${o}" in
n)
SHOW_IN_BROWSER="no"
;;
t)
INCLUDE_TEST_FILES="yes"
;;
*)
usage
exit 0
;;
esac
done
if [ -d htmlcov ]; then
rm -r htmlcov
fi
python -m coverage erase
python -m coverage run --branch --include "autopilot/*" -m autopilot.run run autopilot.tests.unit
python3 -m coverage run --append --branch --include "autopilot/*" -m autopilot.run run autopilot.tests.unit
if [ "$INCLUDE_TEST_FILES" = "yes" ]; then
python -m coverage html
else
python -m coverage html --omit "autopilot/tests/*"
fi
if [ "$SHOW_IN_BROWSER" = "yes" ]; then
xdg-open htmlcov/index.html
fi
autopilot-1.4+14.04.20140416/lttng_module/ 0000755 0000153 0177776 00000000000 12323561350 020472 5 ustar pbuser nogroup 0000000 0000000 autopilot-1.4+14.04.20140416/lttng_module/autopilot_tracepoint.c 0000644 0000153 0177776 00000003560 12323560055 025113 0 ustar pbuser nogroup 0000000 0000000
#define TRACEPOINT_CREATE_PROBES
/*
* The header containing our TRACEPOINT_EVENTs.
*/
#define TRACEPOINT_DEFINE
#include "autopilot_tracepoint.h"
/// Python module stuff below here:
#include
static PyObject *
emit_test_started(PyObject *self, PyObject *args)
{
const char *mesg_text;
/* In Python 3, the argument must be a UTF-8 encoded Unicode. */
if(!PyArg_ParseTuple(args, "s", &mesg_text))
{
return NULL;
}
tracepoint(com_canonical_autopilot, test_event, "started", mesg_text);
Py_RETURN_NONE;
}
static PyObject *
emit_test_ended(PyObject *self, PyObject *args)
{
const char *mesg_text;
/* In Python 3, the argument must be a UTF-8 encoded Unicode. */
if(!PyArg_ParseTuple(args, "s", &mesg_text))
{
return NULL;
}
tracepoint(com_canonical_autopilot, test_event, "ended", mesg_text);
Py_RETURN_NONE;
}
static PyMethodDef TracepointMethods[] = {
{"emit_test_started", emit_test_started, METH_VARARGS, "Generate a tracepoint for test started."},
{"emit_test_ended", emit_test_ended, METH_VARARGS, "Generate a tracepoint for test started."},
{NULL, NULL, 0, NULL} /* Sentinel */
};
#if PY_MAJOR_VERSION >= 3
static struct PyModuleDef moduledef = {
PyModuleDef_HEAD_INIT,
"tracepoint", /* m_name */
NULL, /* m_doc */
-1, /* m_size */
TracepointMethods, /* m_methods */
NULL, /* m_reload */
NULL, /* m_traverse */
NULL, /* m_clear */
NULL /* m_free */
};
PyMODINIT_FUNC
PyInit_tracepoint(void)
{
return PyModule_Create(&moduledef);
}
#else /* Python 2 */
PyMODINIT_FUNC
inittracepoint(void)
{
(void) Py_InitModule("tracepoint", TracepointMethods);
}
#endif /* PY_MAJOR_VERSION >= 3 */
autopilot-1.4+14.04.20140416/lttng_module/autopilot_tracepoint.h 0000644 0000153 0177776 00000001376 12323560055 025123 0 ustar pbuser nogroup 0000000 0000000
#undef TRACEPOINT_PROVIDER
#define TRACEPOINT_PROVIDER com_canonical_autopilot
#undef TRACEPOINT_INCLUDE
#define TRACEPOINT_INCLUDE "./autopilot_tracepoint.h"
#ifdef __cplusplus
extern "C"{
#endif /* __cplusplus */
#if !defined(AUTOPILOT_TRACEPOINT_H) || defined(TRACEPOINT_HEADER_MULTI_READ)
#define AUTOPILOT_TRACEPOINT_H
#include
TRACEPOINT_EVENT(
com_canonical_autopilot,
test_event,
TP_ARGS(const char *, started_or_stopped, const char *, test_id),
/* Next are the fields */
TP_FIELDS(
ctf_string(started_or_stopped, started_or_stopped)
ctf_string(test_id, test_id)
)
)
#endif /* AUTOPILOT_TRACEPOINT_H */
#include
#ifdef __cplusplus
}
#endif /* __cplusplus */
autopilot-1.4+14.04.20140416/COPYING 0000644 0000153 0177776 00000104513 12323560055 017035 0 ustar pbuser nogroup 0000000 0000000 GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc.
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
Copyright (C)
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 .
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
Copyright (C)
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
.
autopilot-1.4+14.04.20140416/docs/ 0000755 0000153 0177776 00000000000 12323561350 016725 5 ustar pbuser nogroup 0000000 0000000 autopilot-1.4+14.04.20140416/docs/otto.py 0000644 0000153 0177776 00000004205 12323560055 020266 0 ustar pbuser nogroup 0000000 0000000 # -*- 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.children.append(nodes.image(uri='/images/otto-64.png'))
image_container['classes'] = ['otto-image-container']
outer_container = nodes.container()
outer_container.children.extend(
[image_container]
+ ad
)
outer_container['classes'] = ['otto-says-container']
return [outer_container]
autopilot-1.4+14.04.20140416/docs/faq/ 0000755 0000153 0177776 00000000000 12323561350 017474 5 ustar pbuser nogroup 0000000 0000000 autopilot-1.4+14.04.20140416/docs/faq/faq.rst 0000644 0000153 0177776 00000031266 12323560055 021006 0 ustar pbuser nogroup 0000000 0000000 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. How do I install Autopilot?
==============================
Autopilot is in continuous development, and the best way to get the latest version of autopilot is to be running the latest Ubuntu development image. The autopilot developers traditionally support the Ubuntu release immediately prior to the development release via the autopilot PPA.
**I am running the latest development image!**
In that case you can install autopilot directly - either by installing the ``autopilot-desktop`` or ``autopilot-touch`` packages, depending on whether you are installing to a desktop or an Ubuntu Touch device.
**I am running the Ubuntu release previous to the development release!**
You may find that there are packages available for your Ubuntu release in the autopilot PPA. To add the PPA to your system, run the following command::
sudo add-apt-repository ppa:autopilot/ppa && sudo apt-get update
Once the PPA has been added to your system, you should be able to install the same autopilot packages as if you were running the latest development release (see above).
**I am running some other Linux system!**
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`).
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::
$ autopilot 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::
$ autopilot 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. `_
autopilot-1.4+14.04.20140416/docs/faq/contribute.rst 0000644 0000153 0177776 00000011072 12323560055 022406 0 ustar pbuser nogroup 0000000 0000000 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 both Python 2.7 and Python 3.3 from a single code base.
Q. How do I run tests with `tox`?
=================================
You can use `tox ` to test against multiple Python versions. See `tox.ini` in the Autopilot's root directory for details.
Install `tox`::
$ sudo apt-get install python-tox
To run all tests against all supported Python versions:
$ tox
To run specific tests using `tox`::
$ tox --
Example::
$ tox -- autopilot.tests.unit
autopilot-1.4+14.04.20140416/docs/porting/ 0000755 0000153 0177776 00000000000 12323561350 020407 5 ustar pbuser nogroup 0000000 0000000 autopilot-1.4+14.04.20140416/docs/porting/porting.rst 0000644 0000153 0177776 00000021135 12323560055 022626 0 ustar pbuser nogroup 0000000 0000000 Porting Autopilot Tests
#######################
This document contains hints as to what is required to port a test suite from any version of autopilot to any newer version.
.. contents::
A note on Versions
==================
Autopilot releases are reasonably tightly coupled with Ubuntu releases. However, the autopilot authors maintain separate version numbers, with the aim of separating the autopilot release cadence from the Ubuntu platform release cadence.
Autopilot versions earlier than 1.2 were not publicly announced, and were only used within Canonical. For that reason, this document assumes that version 1.2 is the lowest version of autopilot present `"in the wild"`.
Porting to Autopilot v1.4.x
===========================
The 1.4 release contains several changes that required a break in the DBus wire protocol between autopilot and the applications under test. Most of these changes require no change to test code.
Gtk Tests and Boolean Parameters
++++++++++++++++++++++++++++++++
Version 1.3 of the autopilot-gtk backend contained `a bug `_ that caused all Boolean properties to be exported as integers instead of boolean values. This in turn meant that test code would fail to return the correct objects when using selection criteria such as::
visible_buttons = app.select_many("GtkPushButton", visible=True)
and instead had to write something like this::
visible_buttons = app.select_many("GtkPushButton", visible=1)
This bug has now been fixed, and using the integer selection will fail.
:py:meth:`~autopilot.testcase.AutopilotTestCase.select_single` Changes
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
The :meth:`~autopilot.introspection.dbus.DBusIntrospectionObject.select_single` method used to return ``None`` in the case where no object was found that matched the search criteria. This led to rather awkward code in places where the object you are searching for is being created dynamically::
for i in range(10):
my_obj = self.app.select_single("MyObject")
if my_obj is not None:
break
time.sleep(1)
else:
self.fail("Object 'MyObject' was not found within 10 seconds.")
This makes the authors intent harder to discern. To improve this situation, two changes have been made:
1. :meth:`~autopilot.introspection.dbus.DBusIntrospectionObject.select_single` raises a :class:`~autopilot.introspection.dbus.StateNotFoundError` exception if the search terms returned no values, rather than returning ``None``.
2. If the object being searched for is likely to not exist, there is a new method: :meth:`~autopilot.introspection.dbus.DBusIntrospectionObject.wait_select_single` will try to retrieve an object for 10 seconds. If the object does not exist after that timeout, a :class:`~autopilot.introspection.dbus.StateNotFoundError` exception is raised. This means that the above code example should now be written as::
my_obj = self.app.wait_select_single("MyObject")
.. _dbus_backends:
DBus backends and :class:`~autopilot.introspection.dbus.DBusIntrospectionObject` changes
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Due to a change in how
:class:`~autopilot.introspection.dbus.DBusIntrospectionObject` objects store
their DBus backend a couple of classmethods have now become instance methods.
These affected methods are:
* :meth:`~autopilot.introspection.dbus.DBusIntrospectionObject.get_all_instances`
* :meth:`~autopilot.introspection.dbus.DBusIntrospectionObject.get_root_instance`
* :meth:`~autopilot.introspection.dbus.DBusIntrospectionObject.get_state_by_path`
For example, if your old code is something along the lines of::
all_keys = KeyCustomProxy.get_all_instances()
You will instead need to have something like this instead::
all_keys = app_proxy.select_many(KeyCustomProxy)
Python 3
++++++++
Starting from version 1.4, autopilot supports python 3 as well as python 2. Test authors can choose to target either version of python.
Porting to Autopilot v1.3.x
===========================
The 1.3 release included many API breaking changes. Earlier versions of autopilot made several assumptions about where tests would be run, that turned out not to be correct. Autopilot 1.3 brought several much-needed features, including:
* A system for building pluggable implementations for several core components. This system is used in several areas:
* The input stack can now generate events using either the X11 client libraries, or the UInput kernel driver. This is necessary for devices that do not use X11.
* The display stack can now report display information for systems that use both X11 and the mir display server.
* The process stack can now report details regarding running processes & their windows on both Desktop, tablet, and phone platforms.
* A large code cleanup and reorganisation. In particular, lots of code that came from the Unity 3D codebase has been removed if it was deemed to not be useful to the majority of test authors. This code cleanup includes a flattening of the autopilot namespace. Previously, many useful classes lived under the ``autopilot.emulators`` namespace. These have now been moved into the ``autopilot`` namespace.
.. note:: There is an API breakage in autopilot 1.3. The changes outlined under
the heading ":ref:`dbus_backends`" apply to version
1.3.1+13.10.20131003.1-0ubuntu1 and onwards .
``QtIntrospectionTestMixin`` and ``GtkIntrospectionTestMixin`` no longer exist
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
In autopilot 1.2, tests enabled application introspection services by inheriting from one of two mixin classes: ``QtIntrospectionTestMixin`` to enable testing Qt4, Qt5, and Qml applications, and ``GtkIntrospectionTestMixin`` to enable testing Gtk 2 and Gtk3 applications. For example, a test case class in autopilot 1.2 might look like this::
from autopilot.introspection.qt import QtIntrospectionTestMixin
from autopilot.testcase import AutopilotTestCase
class MyAppTestCase(AutopilotTestCase, QtIntrospectionTestMixin):
def setUp(self):
super(MyAppTestCase, self).setUp()
self.app = self.launch_test_application("../../my-app")
In Autopilot 1.3, the :class:`~autopilot.testcase.AutopilotTestCase` class contains this functionality directly, so the ``QtIntrospectionTestMixin`` and ``GtkIntrospectionTestMixin`` classes no longer exist. The above example becomes simpler::
from autopilot.testcase import AutopilotTestCase
class MyAppTestCase(AutopilotTestCase):
def setUp(self):
super(MyAppTestCase, self).setUp()
self.app = self.launch_test_application("../../my-app")
Autopilot will try and determine the introspection type automatically. If this process fails, you can specify the application type manually::
from autopilot.testcase import AutopilotTestCase
class MyAppTestCase(AutopilotTestCase):
def setUp(self):
super(MyAppTestCase, self).setUp()
self.app = self.launch_test_application("../../my-app", app_type='qt')
.. seealso::
Method :py:meth:`autopilot.testcase.AutopilotTestCase.launch_test_application`
Launch test applications.
``autopilot.emulators`` namespace has been deprecated
+++++++++++++++++++++++++++++++++++++++++++++++++++++
In autopilot 1.2 and earlier, the ``autopilot.emulators`` package held several modules and classes that were used frequently in tests. This package has been removed, and it's contents merged into the autopilot package. Below is a table showing the basic translations that need to be made:
+-------------------------------+--------------------------------------+
| Old module | New Module |
+===============================+======================================+
| ``autopilot.emulators.input`` | :py:mod:`autopilot.input` |
+-------------------------------+--------------------------------------+
| ``autopilot.emulators.X11`` | Deprecated - use |
| | :py:mod:`autopilot.input` for input |
| | and :py:mod:`autopilot.display` for |
| | getting display information. |
+-------------------------------+--------------------------------------+
| ``autopilot.emulators.bamf`` | Deprecated - use |
| | :py:mod:`autopilot.process` instead. |
+-------------------------------+--------------------------------------+
.. TODO - add specific instructions on how to port tests from the 'old and busted' autopilot to the 'new hotness'. Do this when we actually start the porting work ourselves.
autopilot-1.4+14.04.20140416/docs/contents.rst 0000644 0000153 0177776 00000000267 12323560055 021322 0 ustar pbuser nogroup 0000000 0000000 :orphan:
Global Contents
###############
.. toctree::
:maxdepth: 5
tutorial/tutorial
api/index
porting/porting
faq/faq
faq/contribute
appendix/appendix
autopilot-1.4+14.04.20140416/docs/_static/ 0000755 0000153 0177776 00000000000 12323561350 020353 5 ustar pbuser nogroup 0000000 0000000 autopilot-1.4+14.04.20140416/docs/_static/nature.css 0000644 0000153 0177776 00000006173 12323560055 022373 0 ustar pbuser nogroup 0000000 0000000 @import url(basic.css);
body {
font-family:Ubuntu, "DejaVu Sans", "Trebuchet MS", sans-serif;
font-size:100%;
background-color:#2C001E;
margin:0;
padding:0;
}
div.documentwrapper {
float:left;
width:100%;
}
div.bodywrapper {
margin:0 0 0 250px;
}
hr {
border:1px solid #B1B4B6;
}
div.body {
background-color:#FFF;
color:#3E4349;
font-size:.9em;
padding:0 30px 30px;
}
div.footer {
color:#CCC;
width:100%;
text-align:center;
font-size:75%;
padding:13px 0;
}
div.footer a {
color:#444;
text-decoration:underline;
}
div.related {
background-color:#DD4814;
line-height:32px;
color:#fff;
font-size:.9em;
}
div.related a {
color:#EEE;
}
div.sphinxsidebar {
font-size:.75em;
line-height:1.5em;
}
div.sphinxsidebarwrapper {
padding:20px 0;
}
div.sphinxsidebar h3,
div.sphinxsidebar h4 {
font-family:Ubuntu, "DejaVu Sans", "Trebuchet MS", sans-serif;
color:#222;
font-size:1.2em;
font-weight:400;
background-color:#ddd;
margin:0;
padding:5px 10px;
}
div.sphinxsidebar p {
color:#444;
padding:2px 10px;
}
div.sphinxsidebar ul {
color:#000;
margin:10px 5px;
padding:0;
}
div.sphinxsidebar input {
border:1px solid #ccc;
font-family:Ubuntu, "DejaVu Sans", "Trebuchet MS", sans-serif;
font-size:1em;
}
div.sphinxsidebar input[type=text] {
margin-left:5px;
}
a {
color:#5E2750;
text-decoration:none;
}
a:hover {
color:#DD4814;
text-decoration:underline;
}
div.body h1 {
font-family:Ubuntu, "DejaVu Sans", "Trebuchet MS", sans-serif;
font-weight:bold;
font-size:200%;
color:#444;
margin:20px 0 10px;
padding:5px 0 5px 10px;
}
div.body h2 {
font-family:Ubuntu, "DejaVu Sans", "Trebuchet MS", sans-serif;
font-size:130%;
background-color:#CCC;
margin:20px 0 10px;
padding:5px 0 5px 10px;
}
div.body h6 {
font-family:Ubuntu, "DejaVu Sans", "Trebuchet MS", sans-serif;
background-color:#BED4EB;
font-weight:400;
color:#333;
margin:30px 0 10px;
padding:5px 0 5px 10px;
}
a.headerlink {
text-decoration:none;
padding:0 4px;
}
a.headerlink:hover {
background-color:#AEA79F;
color:#FFF;
}
div.body p,div.body dd,div.body li {
line-height:1.5em;
}
div.highlight {
background-color:#FFF;
}
div.note {
background-color:#eee;
border:1px solid #ccc;
}
div.seealso {
background-color:#ffc;
border:1px solid #ff6;
}
div.warning {
background-color:#ffe4e4;
border:1px solid #f66;
}
p.admonition-title:after {
content:":";
}
pre {
background-color:#EFEFEF;
color:#000;
line-height:1.1em;
border:1px solid #C6C9CB;
font-size:1.2em;
font-family:"Ubuntu Mono", Monaco, Consolas, "DejaVu Sans Mono", "Lucida Console", monospace;
margin:1.5em 0;
padding:6px;
}
tt {
background-color:#EFEFEF;
color:#222;
font-size:1.1em;
font-family:"Ubuntu Mono", Monaco, Consolas, "DejaVu Sans Mono", "Lucida Console", monospace;
}
.viewcode-back {
font-family:Ubuntu, "DejaVu Sans", "Trebuchet MS", sans-serif;
}
div.viewcode-block:target {
background-color:#f4debf;
border-top:1px solid #ac9;
border-bottom:1px solid #ac9;
}
div.document,div.topic {
background-color:#eee;
}
div.sphinxsidebar h3 a,div.sphinxsidebar a {
color:#444;
}
div.admonition p.admonition-title + p,p.admonition-title {
display:inline;
}
autopilot-1.4+14.04.20140416/docs/_static/otto.css 0000644 0000153 0177776 00000000637 12323560055 022061 0 ustar pbuser nogroup 0000000 0000000 div.otto-says-container .otto-image-container {
position: absolute;
margin-top: 4px;
margin-left: 4px;
}
div.otto-says-container .admonition-autopilot-says {
vertical-align: top;
padding:5px;
padding-left: 74px;
margin-top: 8px;
margin-bottom: 8px;
}
div.otto-says-container {
min-height: 70px;
background-color: #EEE;
border: 1px solid #CCC;
margin-bottom: 2px;
}
autopilot-1.4+14.04.20140416/docs/_static/centertext.css 0000644 0000153 0177776 00000000154 12323560055 023253 0 ustar pbuser nogroup 0000000 0000000 div.center_text {
text-align: center;
color: #ffffff;
}
div.center_text a {
color: #ffffff;
}
autopilot-1.4+14.04.20140416/docs/conf.py 0000644 0000153 0177776 00000021543 12323560055 020232 0 ustar pbuser nogroup 0000000 0000000 # -*- 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 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',
]
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-2013, 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.4'
# 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 = [
('index', 'autopilot', u'Autopilot Documentation',
[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'
autopilot-1.4+14.04.20140416/docs/tutorial/ 0000755 0000153 0177776 00000000000 12323561350 020570 5 ustar pbuser nogroup 0000000 0000000 autopilot-1.4+14.04.20140416/docs/tutorial/what_is_autopilot.rst 0000644 0000153 0177776 00000010357 12323560055 025067 0 ustar pbuser nogroup 0000000 0000000 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.
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.
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.
autopilot-1.4+14.04.20140416/docs/tutorial/tutorial.rst 0000644 0000153 0177776 00000000375 12323560055 023173 0 ustar pbuser nogroup 0000000 0000000 Autopilot Tutorial
==================
This tutorial will guide users new to Autopilot through creating a minimal autopilot test.
.. toctree::
:maxdepth: 3
what_is_autopilot
getting_started
advanced_autopilot
good_tests
running_ap
autopilot-1.4+14.04.20140416/docs/tutorial/running_ap.rst 0000644 0000153 0177776 00000011637 12323560055 023473 0 ustar pbuser nogroup 0000000 0000000 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::
$ autopilot 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::
$ autopilot 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::
$ autopilot 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**::
$ autopilot 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**::
$ autopilot 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**::
$ autopilot 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.
.. _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, and then run::
$ autopilot 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-1.4+14.04.20140416/docs/tutorial/good_tests.rst 0000644 0000153 0177776 00000062313 12323560055 023502 0 ustar pbuser nogroup 0000000 0000000 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!
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).
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.dbus.DBusIntrospectionObject.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.
autopilot-1.4+14.04.20140416/docs/tutorial/getting_started.rst 0000644 0000153 0177776 00000044337 12323560055 024525 0 ustar pbuser nogroup 0000000 0000000 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//emulators/
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 `_.
The ``autopilot//emulators/`` directory is optional, and will only be used if you write custom proxy classes. This is an advanced topic, and is covered here: :ref:`custom_proxy_classes`
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.dbus.DBusIntrospectionObject` 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. 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::
$ autopilot 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::
$ autopilot 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::
$ autopilot 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::
$ autopilot 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.input import Mouse
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.
autopilot-1.4+14.04.20140416/docs/tutorial/advanced_autopilot.rst 0000644 0000153 0177776 00000051327 12323560055 025200 0 ustar pbuser nogroup 0000000 0000000 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.info("This is some information")
logger.warning("This is a warning")
logger.error("This is an error")
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 :class:`~autopilot.testcase.AutopilotTestCase` class includes a :meth:`~autopilot.testcase.AutopilotTestCase.patch_environment` method 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 autopilot.testcase import AutopilotTestCase
class MyTests(AutopilotTestCase):
def test_that_needs_custom_environment(self):
self.patch_environment("FOO", "Hello World")
# Test code goes here.
The :meth:`~autopilot.testcase.AutopilotTestCase.patch_environment` method 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 :meth:`~autopilot.testcase.AutopilotTestCase.patch_environment` was called. This happens in the cleanup phase of the test execution.
Custom Assertions
=================
.. Document the custom assertion methods present in AutopilotTestCase
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 and Multitouch
=======================
.. How do we do multi-touch & gestures?
.. _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 (i.e. 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 i.e. 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
===============
.. Document the process stack.
Display Information
===================
.. Document the display stack.
.. _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.dbus.DBusIntrospectionObject`. 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 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.dbus.CustomEmulatorBase`. An example class might look like this::
from autopilot.introspection.dbus import CustomEmulatorBase
class CustomProxyObjectBase(CustomEmulatorBase):
"""A base class for all custom proxy objects within this test suite."""
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):
if (path.endswith('object_we_want') or
state['some_property'] == 'desired_value'):
return True
return False
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.
3. Pass the custom proxy class as an argument to the launch_test_application method on your test class. Something like this::
from autopilot.testcase import AutopilotTestCase
class TestCase(AutopilotTestCase):
def setUp(self):
super(TestCase, self).setUp()
self.app = self.launch_test_application(
'/path/to/the/application',
emulator_base=CustomProxyObjectBase)
4. You can pass the custom proxy class to methods like :meth:`~autopilot.introspection.dbus.DBusIntrospectionObject.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)
autopilot-1.4+14.04.20140416/docs/api/ 0000755 0000153 0177776 00000000000 12323561350 017476 5 ustar pbuser nogroup 0000000 0000000 autopilot-1.4+14.04.20140416/docs/api/platform.rst 0000644 0000153 0177776 00000000273 12323560055 022057 0 ustar pbuser nogroup 0000000 0000000 ``autopilot.platform`` - Functions for platform detection
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++
.. automodule:: autopilot.platform
:members:
:undoc-members:
autopilot-1.4+14.04.20140416/docs/api/display.rst 0000644 0000153 0177776 00000000320 12323560055 021671 0 ustar pbuser nogroup 0000000 0000000 ``autopilot.display`` - Get information about the current display(s)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
.. automodule:: autopilot.display
:members:
:undoc-members:
autopilot-1.4+14.04.20140416/docs/api/input.rst 0000644 0000153 0177776 00000000322 12323560055 021365 0 ustar pbuser nogroup 0000000 0000000 ``autopilot.input`` - Generate keyboard, mouse, and touch input events
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
.. automodule:: autopilot.input
:members:
:undoc-members:
autopilot-1.4+14.04.20140416/docs/api/matchers.rst 0000644 0000153 0177776 00000000255 12323560055 022041 0 ustar pbuser nogroup 0000000 0000000 ``autopilot.matchers`` - Custom matchers for test assertions
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
.. automodule:: autopilot.matchers
:members:
autopilot-1.4+14.04.20140416/docs/api/process.rst 0000644 0000153 0177776 00000000226 12323560055 021707 0 ustar pbuser nogroup 0000000 0000000 ``autopilot.process`` - Process Control
+++++++++++++++++++++++++++++++++++++++
.. automodule:: autopilot.process
:members:
:undoc-members:
autopilot-1.4+14.04.20140416/docs/api/autopilot.rst 0000644 0000153 0177776 00000000171 12323560055 022250 0 ustar pbuser nogroup 0000000 0000000 ``autopilot`` - Global stuff
++++++++++++++++++++++++++++
.. automodule:: autopilot
:members:
:undoc-members:
autopilot-1.4+14.04.20140416/docs/api/testcase.rst 0000644 0000153 0177776 00000000265 12323560055 022047 0 ustar pbuser nogroup 0000000 0000000 ``autopilot.testcase`` - Base class for all Autopilot Test Cases
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
.. automodule:: autopilot.testcase
:members:
autopilot-1.4+14.04.20140416/docs/api/emulators.rst 0000644 0000153 0177776 00000000260 12323560055 022242 0 ustar pbuser nogroup 0000000 0000000 ``autopilot.emulators`` - Backwards compatibility for autopilot v1.2
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
.. automodule:: autopilot.emulators
autopilot-1.4+14.04.20140416/docs/api/gestures.rst 0000644 0000153 0177776 00000000247 12323560055 022075 0 ustar pbuser nogroup 0000000 0000000 ``autopilot.gestures`` - Gestural and multi-touch support
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++
.. automodule:: autopilot.gestures
:members:
autopilot-1.4+14.04.20140416/docs/api/introspection.rst 0000644 0000153 0177776 00000000703 12323560055 023131 0 ustar pbuser nogroup 0000000 0000000 ``autopilot.introspection`` - Autopilot introspection internals
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
.. automodule:: autopilot.introspection
:members: get_proxy_object_for_existing_process
.. automodule:: autopilot.introspection.dbus
:members: CustomEmulatorBase, DBusIntrospectionObject, StateNotFoundError
.. automodule:: autopilot.introspection.types
:members: PlainType, Rectangle, Point, Size, DateTime, Time
autopilot-1.4+14.04.20140416/docs/api/index.rst 0000644 0000153 0177776 00000000146 12323560055 021341 0 ustar pbuser nogroup 0000000 0000000 Autopilot API Documentation
===========================
.. toctree::
:maxdepth: 1
:glob:
*
autopilot-1.4+14.04.20140416/docs/images/ 0000755 0000153 0177776 00000000000 12323561350 020172 5 ustar pbuser nogroup 0000000 0000000 autopilot-1.4+14.04.20140416/docs/images/test_pyramid.svg 0000644 0000153 0177776 00000011465 12323560055 023427 0 ustar pbuser nogroup 0000000 0000000
autopilot-1.4+14.04.20140416/docs/images/ap_vis_front_page.png 0000644 0000153 0177776 00000032474 12323560055 024400 0 ustar pbuser nogroup 0000000 0000000 PNG
IHDR pИ bKGD pHYs tIME
7` tEXtComment Created with GIMPW IDATxyuawr@.ITPPDDEmm=*mVZ[UUxߊrk~f7$$!%y>y|l|%_Qu*nn2 kE"ZF-it*nvY^,^_Rxܣil}OsXenMc7Xl5;t>zצYdwȖ{n[ev]wtx]/vqTM/tg3nMLvwXShun[evzο>՝BnO;]YvS]x_xwi:]NիbW7xȦkҭikitܹs$##
pI6^*[~\9sf+>r v̙˲xuja Vi\ZٳgݩUUTeoߺ]FUU)bqUz;wj:UE")RK^KQj fϞ<-;2v;n7n5944YUU/*UUvSeʪJm|@_l֓;n~UW;1Fjz^0]P=XL}` u l[cWPUUnvё9~ɼ0gf16unͧ.>}i4F?;%Kg;wN֮]5knu]+2}VFoAlYHћ^X/^UVfܹ9cF6nܘunW\k6}Vj^oPv^ON6ݐFfJʢR~~ Rt ̙3v>2cc,l3Rݤ,SeZ-R~h^_n֖@:nl攓Oμysv˔e'Z#zo7[o]~kii5[mInSr'e}IYn{:ܲ&^oRo?ͺM_4/MfsUkU'ǖgU>W%cs.f a]ݨʴ,m
۽Gy_yVw;i4z[ʲJM_/yi7lܸw\C9\RԊ)24c =|_JNL6%M^CrZZ}lԛN2f!yHN;H^ݮۺ~vM9o^o]O;@>so/ .nTI=gUn?`4co:]|Ridxsg|к6q@Qti6[ϗ4jcʲhSR%Ûw0NfN:>H)GLz7[I-Csgf^aC֍V)ZC?JUԲe]f۲ޟysƿ_flH6)VO@* 3.H@)j9i593 =2vfKBU%N;g7}i[$Yy,s^vsf4Y
oƜhF_3zv%K?7'gF*rوH`/YoZVn/3Ƨ\ptvjtV^F+T9Yė<IFhXؼ#?yZVwߐ3yE281}77OL?fV'yezڷ^o~ܜkjdtt^ܧÖf9ʦ~?7oNk(.WO9=6֧~Ykp~Ez- FݹlZN4͛xM^xE9|`.]77g}:甅Mév,Z(vRN~?aVާeYfEҬNŋHo:=n:\Eo^K,
߰W0v:29
=}=%\C7eC'ysZޘxrփM9oeNyKj֫OߘN1;G{U7cZZtxWYg&Վh^!s-2W {b$PHͲ"ͩ2Ir>sprٯ'-b}
k,HJTv3ch(ccj$u
NR
elll4MgBtvuUff̞y_r9|GfƔ?ߔOe}5TݺOyIw"omɛvP>1%ߟP>o[9g'+vsQQ3:;=י$ ;uU{rp$庛svwQ7~Qޚ[F=?unAEQ[vg}0ؾzʪJv=9g{[z.mJ26ܟ#