diff -Nru autopilot-1.5.1+16.04.20160428/autopilot/exceptions.py autopilot-1.6.0+16.10.20160818.1/autopilot/exceptions.py
--- autopilot-1.5.1+16.04.20160428/autopilot/exceptions.py 2016-04-28 19:12:35.000000000 +0000
+++ autopilot-1.6.0+16.10.20160818.1/autopilot/exceptions.py 2016-08-18 19:23:59.000000000 +0000
@@ -91,7 +91,7 @@
_troubleshoot_url_message = (
'Tips on minimizing the occurrence of this failure '
'are available here: '
- 'https://developer.ubuntu.com/api/autopilot/python/1.5.0/'
+ 'https://developer.ubuntu.com/api/autopilot/python/1.6.0/'
'faq-troubleshooting/'
)
diff -Nru autopilot-1.5.1+16.04.20160428/autopilot/_info.py autopilot-1.6.0+16.10.20160818.1/autopilot/_info.py
--- autopilot-1.5.1+16.04.20160428/autopilot/_info.py 2016-04-28 19:12:35.000000000 +0000
+++ autopilot-1.6.0+16.10.20160818.1/autopilot/_info.py 2016-08-18 19:23:59.000000000 +0000
@@ -28,7 +28,7 @@
'version',
]
-version = '1.5.0'
+version = '1.6.0'
def have_vis():
diff -Nru autopilot-1.5.1+16.04.20160428/autopilot/input/_uinput.py autopilot-1.6.0+16.10.20160818.1/autopilot/input/_uinput.py
--- autopilot-1.5.1+16.04.20160428/autopilot/input/_uinput.py 2016-04-28 19:12:35.000000000 +0000
+++ autopilot-1.6.0+16.10.20160818.1/autopilot/input/_uinput.py 2016-08-18 19:23:59.000000000 +0000
@@ -26,6 +26,7 @@
from autopilot.input import Keyboard as KeyboardBase
from autopilot.input import Touch as TouchBase
from autopilot.input import get_center_point
+from autopilot.platform import model
from autopilot.utilities import deprecated, EventDelay, sleep
@@ -653,3 +654,51 @@
'ALT': 'LEFTALT',
'SHIFT': 'LEFTSHIFT',
}
+
+
+class UInputHardwareKeysDevice:
+
+ _device = None
+
+ def __init__(self, device_class=UInput):
+ if not UInputHardwareKeysDevice._device:
+ UInputHardwareKeysDevice._device = device_class(
+ devnode=_get_devnode_path(),
+ )
+ # This workaround is not needed on desktop.
+ if model() != 'Desktop':
+ self._wait_for_device_to_ready()
+
+ def press_and_release_power_button(self):
+ self._device.write(e.EV_KEY, e.KEY_POWER, 1)
+ self._device.write(e.EV_KEY, e.KEY_POWER, 0)
+ self._device.syn()
+
+ def _wait_for_device_to_ready(
+ self,
+ retry_attempts_count=10,
+ retry_interval=0.1,
+ ):
+ """Wait for UInput device to initialize.
+
+ This is a workaround for a bug in evdev where the input device
+ is not instantly created.
+
+ :param retry_attempts_count: number of attempts to check
+ if device is ready.
+
+ :param retry_interval: time in fractional seconds to be
+ slept, between each attempt to check if device is
+ ready.
+
+ :raises RuntimeError: if device is not initialized after
+ number of retries specified in *retry_attempts_count*.
+ """
+ for i in range(retry_attempts_count):
+ device = self._device._find_device()
+ if device:
+ self._device.device = device
+ return
+ else:
+ sleep(retry_interval)
+ raise RuntimeError('Failed to find UInput device.')
diff -Nru autopilot-1.5.1+16.04.20160428/autopilot/introspection/dbus.py autopilot-1.6.0+16.10.20160818.1/autopilot/introspection/dbus.py
--- autopilot-1.5.1+16.04.20160428/autopilot/introspection/dbus.py 2016-04-28 19:12:35.000000000 +0000
+++ autopilot-1.6.0+16.10.20160818.1/autopilot/introspection/dbus.py 2016-08-18 19:23:59.000000000 +0000
@@ -25,20 +25,22 @@
"""
-from contextlib import contextmanager
-import sys
import logging
+import sys
+from contextlib import contextmanager
from autopilot.exceptions import StateNotFoundError
+from autopilot.introspection import _xpathselect as xpathselect
from autopilot.introspection._object_registry import (
DBusIntrospectionObjectBase,
)
from autopilot.introspection.types import create_value_instance
-from autopilot.introspection.utilities import translate_state_keys
-from autopilot.introspection import _xpathselect as xpathselect
+from autopilot.introspection.utilities import (
+ translate_state_keys,
+ sort_by_keys,
+)
from autopilot.utilities import sleep
-
_logger = logging.getLogger(__name__)
@@ -76,7 +78,6 @@
self.__refresh_on_attribute = True
self._set_properties(state_dict)
self._path = path
- self._poll_time = 10
self._backend = backend
self._query = xpathselect.Query.new_from_path_and_id(
self._path,
@@ -185,15 +186,89 @@
new_query = self._query.select_child(xpathselect.Query.WILDCARD)
return self._execute_query(new_query)
- def get_parent(self):
+ def _get_parent(self, base_object=None, level=1):
"""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.
+ Note: *level* is in ascending order, i.e. its value as 1 will return
+ the immediate parent of this object or (optionally) *base_object*,
+ if provided.
+ """
+ obj = base_object or self
+ new_query = obj._query
+ for i in range(level):
+ new_query = new_query.select_parent()
+ return obj._execute_query(new_query)[0]
+
+ def _get_parent_nodes(self):
+ parent_nodes = self.get_path().split('/')
+ parent_nodes.pop()
+ # return a list without any NoneType elements. Only needed for the
+ # case when we try to get parent of a root object
+ return [node for node in parent_nodes if node]
+
+ def get_parent(self, type_name='', **kwargs):
+ """Returns the parent of this object.
+
+ One may also use this method to get a specific parent node from the
+ introspection tree, with type equal to *type_name* or matching the
+ keyword filters present in *kwargs*.
+ Note: The priority order is closest parent.
+
+ If no filters are provided and this object has no parent (i.e.- it is
+ the root of the introspection tree). Then it returns itself.
+
+ :param type_name: Either a string naming the type you want, or a class
+ of the appropriate type (the latter case is for overridden emulator
+ classes).
+
+ :raises StateNotFoundError: if the requested object was not found.
+ """
+ if not type_name and not kwargs:
+ return self._get_parent()
+
+ parent_nodes = self._get_parent_nodes()
+ type_name_str = get_type_name(type_name)
+ if type_name:
+ # Raise if type_name is not a parent.
+ if type_name_str not in parent_nodes:
+ raise StateNotFoundError(type_name_str, **kwargs)
+ for index, node in reversed(list(enumerate(parent_nodes))):
+ if node == type_name_str:
+ parent_level = len(parent_nodes) - index
+ parent = self._get_parent(level=parent_level)
+ if _validate_object_properties(parent, **kwargs):
+ return parent
+ else:
+ # Keep a reference of the parent object to improve performance.
+ parent = self
+ for i in range(len(parent_nodes)):
+ parent = self._get_parent(base_object=parent)
+ if _validate_object_properties(parent, **kwargs):
+ return parent
+ raise StateNotFoundError(type_name_str, **kwargs)
+
+ def _select(self, type_name_str, **kwargs):
+ """Base method to execute search query on the DBus."""
+ new_query = self._query.select_descendant(type_name_str, kwargs)
+ _logger.debug(
+ "Selecting object(s) of %s with attributes: %r",
+ 'any type' if type_name_str == '*' else 'type ' + type_name_str,
+ kwargs
+ )
+ return self._execute_query(new_query)
+ def _select_single(self, type_name, **kwargs):
+ """
+ Ensures a single search result is produced from the query
+ and returns it.
"""
- new_query = self._query.select_parent()
- return self._execute_query(new_query)[0]
+ type_name_str = get_type_name(type_name)
+ instances = self._select(type_name_str, **kwargs)
+ if not instances:
+ raise StateNotFoundError(type_name_str, **kwargs)
+ if len(instances) > 1:
+ raise ValueError("More than one item was returned for query")
+ return instances[0]
def select_single(self, type_name='*', **kwargs):
"""Get a single node from the introspection tree, with type equal to
@@ -232,19 +307,9 @@
Tutorial Section :ref:`custom_proxy_classes`
"""
- type_name_str = get_type_name(type_name)
- new_query = self._query.select_descendant(
- type_name_str,
- kwargs
- )
- instances = self._execute_query(new_query)
- if len(instances) > 1:
- raise ValueError("More than one item was returned for query")
- if not instances:
- raise StateNotFoundError(type_name_str, **kwargs)
- return instances[0]
+ return self._select_single(type_name, **kwargs)
- def wait_select_single(self, type_name='*', **kwargs):
+ def wait_select_single(self, type_name='*', ap_query_timeout=10, **kwargs):
"""Get a proxy object matching some search criteria, retrying if no
object is found until a timeout is reached.
@@ -272,12 +337,15 @@
# raise StateNotFoundError after 10 seconds.
If nothing is returned from the query, this method raises
- StateNotFoundError after 10 seconds.
+ StateNotFoundError after *ap_query_timeout* seconds.
:param type_name: Either a string naming the type you want, or a class
of the appropriate type (the latter case is for overridden emulator
classes).
+ :param ap_query_timeout: Time in seconds to wait for search criteria
+ to match.
+
:raises ValueError: if the query returns more than one item. *If
you want more than one item, use select_many instead*.
@@ -290,15 +358,22 @@
Tutorial Section :ref:`custom_proxy_classes`
"""
- for i in range(self._poll_time):
+ if ap_query_timeout <= 0:
+ return self._select_single(type_name, **kwargs)
+
+ for i in range(ap_query_timeout):
try:
- return self.select_single(type_name, **kwargs)
+ return self._select_single(type_name, **kwargs)
except StateNotFoundError:
- if i == self._poll_time - 1:
- raise
sleep(1)
+ raise StateNotFoundError(type_name, **kwargs)
- def select_many(self, type_name='*', **kwargs):
+ def _select_many(self, type_name, **kwargs):
+ """Executes a query, with no restraints on the number of results."""
+ type_name_str = get_type_name(type_name)
+ return self._select(type_name_str, **kwargs)
+
+ def select_many(self, type_name='*', ap_result_sort_keys=None, **kwargs):
"""Get a list of nodes from the introspection tree, with type equal to
*type_name* and (optionally) matching the keyword filters present in
*kwargs*.
@@ -328,12 +403,18 @@
this method returns objects. (see :ref:`object_ordering` for more
information).
- If you only want to get one item, use :meth:`select_single` instead.
+ If you want to ensure a certain count of results retrieved from this
+ method, use :meth:`wait_select_many` or if you only want to get one
+ item, use :meth:`select_single` instead.
:param type_name: Either a string naming the type you want, or a class
of the appropriate type (the latter case is for overridden emulator
classes).
+ :param ap_result_sort_keys: list of object properties to sort the
+ query result with (sort key priority starts with element 0 as
+ highest priority and then descends down the list).
+
:raises ValueError: if neither *type_name* or keyword filters are
provided.
@@ -341,17 +422,87 @@
Tutorial Section :ref:`custom_proxy_classes`
"""
- type_name_str = get_type_name(type_name)
- new_query = self._query.select_descendant(
- type_name_str,
- kwargs
- )
- _logger.debug(
- "Selecting objects of %s with attributes: %r",
- 'any type'
- if type_name_str == '*' else 'type ' + type_name_str, kwargs
- )
- return self._execute_query(new_query)
+ instances = self._select_many(type_name, **kwargs)
+ return sort_by_keys(instances, ap_result_sort_keys)
+
+ def wait_select_many(
+ self,
+ type_name='*',
+ ap_query_timeout=10,
+ ap_result_count=1,
+ ap_result_sort_keys=None,
+ **kwargs
+ ):
+ """Get a list of nodes from the introspection tree, with type equal to
+ *type_name* and (optionally) matching the keyword filters present in
+ *kwargs*.
+
+ This method is identical to the :meth:`select_many` method, except
+ that this method will poll the application under test for
+ *ap_query_timeout* seconds in the event that the search result count
+ is not greater than or equal to *ap_result_count*.
+
+ You must specify either *type_name*, keyword filters or both.
+
+ This method searches recursively from the instance this method is
+ called on. Calling :meth:`wait_select_many` on the application (root)
+ proxy object will search the entire tree. Calling
+ :meth:`wait_select_many` on an object in the tree will only search
+ it's descendants.
+
+ Example Usage::
+
+ app.wait_select_many(
+ 'QPushButton',
+ ap_query_timeout=5,
+ ap_result_count=2,
+ enabled=True
+ )
+ # returns at least 2 QPushButtons that are enabled, within
+ # 5 seconds.
+
+ .. warning::
+ The order in which objects are returned is not guaranteed. It is
+ bad practise to write tests that depend on the order in which
+ this method returns objects. (see :ref:`object_ordering` for more
+ information).
+
+ :param type_name: Either a string naming the type you want, or a class
+ of the appropriate type (the latter case is for overridden emulator
+ classes).
+
+ :param ap_query_timeout: Time in seconds to wait for search criteria
+ to match.
+
+ :param ap_result_count: Minimum number of results to return.
+
+ :param ap_result_sort_keys: list of object properties to sort the
+ query result with (sort key priority starts with element 0 as
+ highest priority and then descends down the list).
+
+ :raises ValueError: if neither *type_name* or keyword filters are
+ provided. Also raises, if search result count does not match the
+ number specified by *ap_result_count* within *ap_query_timeout*
+ seconds.
+
+ .. seealso::
+ Tutorial Section :ref:`custom_proxy_classes`
+
+ """
+ exception_message = 'Failed to find the requested number of elements.'
+
+ if ap_query_timeout <= 0:
+ instances = self._select_many(type_name, **kwargs)
+ if len(instances) < ap_result_count:
+ raise ValueError(exception_message)
+ return sort_by_keys(instances, ap_result_sort_keys)
+
+ for i in range(ap_query_timeout):
+ instances = self._select_many(type_name, **kwargs)
+ if len(instances) >= ap_result_count:
+ return sort_by_keys(instances, ap_result_sort_keys)
+ sleep(1)
+ raise ValueError(exception_message)
def refresh_state(self):
"""Refreshes the object's state.
@@ -451,6 +602,51 @@
"Object was not destroyed after %d seconds" % timeout
)
+ def is_moving(self, gap_interval=0.1):
+ """Check if the element is moving.
+
+ :param gap_interval: Time in seconds to wait before
+ re-inquiring the object co-ordinates to be able
+ to evaluate if, the element is moving.
+
+ :return: True, if the element is moving, otherwise False.
+ """
+ return _MockableDbusObject(self).is_moving(gap_interval)
+
+ def wait_until_not_moving(
+ self,
+ retry_attempts_count=20,
+ retry_interval=0.5,
+ ):
+ """Block until this object is not moving.
+
+ Block until both x and y of the object stop changing. This is
+ normally useful for cases, where there is a need to ensure an
+ object is static before interacting with it.
+
+ :param retry_attempts_count: number of attempts to check
+ if the object is moving.
+
+ :param retry_interval: time in fractional seconds to be
+ slept, between each attempt to check if the object
+ moving.
+
+ :raises RuntimeError: if DBus node is still moving after
+ number of retries specified in *retry_attempts_count*.
+ """
+ # In case *retry_attempts_count* is something smaller than
+ # 1, sanitize it.
+ if retry_attempts_count < 1:
+ retry_attempts_count = 1
+ for i in range(retry_attempts_count):
+ if not self.is_moving(retry_interval):
+ return
+ raise RuntimeError(
+ 'Object was still moving after {} second(s)'.format(
+ retry_attempts_count * retry_interval
+ )
+ )
+
def print_tree(self, output=None, maxdepth=None, _curdepth=0):
"""Print properties of the object and its children to a stream.
@@ -503,6 +699,13 @@
except StateNotFoundError as error:
output.write("%sError: %s\n" % (indent, error))
+ def get_path(self):
+ """Return the absolute path of the dbus node"""
+ if isinstance(self._path, str):
+ return self._path
+
+ return self._path.decode('utf-8')
+
@contextmanager
def no_automatic_refreshing(self):
"""Context manager function to disable automatic DBus refreshing when
@@ -596,3 +799,103 @@
return maybe_cpo_class.get_type_query_name()
else:
return maybe_cpo_class.__name__
+
+
+def _validate_object_properties(item, **kwargs):
+ """Returns bool representing if the properties specified in *kwargs*
+ match the provided object *item*."""
+ props = item.get_properties()
+ for key in kwargs.keys():
+ if key not in props or props[key] != kwargs[key]:
+ return False
+ return True
+
+
+def raises(exception_class, func, *args, **kwargs):
+ """Evaluate if the callable *func* raises the expected
+ exception.
+
+ :param exception_class: Expected exception to be raised.
+
+ :param func: The callable that is to be evaluated.
+
+ :param args: Optional *args* to call the *func* with.
+
+ :param kwargs: Optional *kwargs* to call the *func* with.
+
+ :returns: bool, if the exception was raised.
+ """
+ try:
+ func(*args, **kwargs)
+ except exception_class:
+ return True
+ else:
+ return False
+
+
+def is_element(ap_query_func, *args, **kwargs):
+ """Call the *ap_query_func* with the args and indicate if it
+ raises StateNotFoundError.
+
+ :param: ap_query_func: The dbus query call to be evaluated.
+
+ :param: *args: The *ap_query_func* positional parameters.
+
+ :param: **kwargs: The *ap_query_func* optional parameters.
+
+ :returns: False if the *ap_query_func* raises StateNotFoundError,
+ True otherwise.
+ """
+ return not raises(StateNotFoundError, ap_query_func, *args, **kwargs)
+
+
+class _MockableDbusObject:
+ """Mockable DBus object."""
+
+ def __init__(self, dbus_object):
+ self._dbus_object = dbus_object
+ self._mocked = False
+ self._dbus_object_secondary = None
+ sleep.disable_mock()
+
+ @contextmanager
+ def mocked(self, dbus_object_secondary):
+ try:
+ self.enable_mock(dbus_object_secondary)
+ yield self
+ finally:
+ self.disable_mock()
+
+ def enable_mock(self, dbus_object_secondary):
+ self._dbus_object_secondary = dbus_object_secondary
+ sleep.enable_mock()
+ self._mocked = True
+
+ def disable_mock(self):
+ self._dbus_object_secondary = None
+ sleep.disable_mock()
+ self._mocked = False
+
+ def _get_default_dbus_object(self):
+ return self._dbus_object
+
+ def _get_secondary_dbus_object(self):
+ if not self._mocked:
+ return self._get_default_dbus_object()
+ else:
+ return self._dbus_object_secondary
+
+ def is_moving(self, gap_interval=0.1):
+ """Check if the element is moving.
+
+ :param gap_interval: Time in seconds to wait before
+ re-inquiring the object co-ordinates to be able
+ to evaluate if, the element has moved.
+
+ :return: True, if the element is moving, otherwise False.
+ """
+ x1, y1, h1, w1 = self._get_default_dbus_object().globalRect
+ sleep(gap_interval)
+ x2, y2, h2, w2 = self._get_secondary_dbus_object().globalRect
+
+ return x1 != x2 or y1 != y2
diff -Nru autopilot-1.5.1+16.04.20160428/autopilot/introspection/__init__.py autopilot-1.6.0+16.10.20160818.1/autopilot/introspection/__init__.py
--- autopilot-1.5.1+16.04.20160428/autopilot/introspection/__init__.py 2016-04-28 19:12:35.000000000 +0000
+++ autopilot-1.6.0+16.10.20160818.1/autopilot/introspection/__init__.py 2016-08-18 19:23:59.000000000 +0000
@@ -37,21 +37,28 @@
"""
-from autopilot.introspection.dbus import CustomEmulatorBase
-from autopilot.introspection._xpathselect import get_classname_from_path
+from autopilot.introspection.dbus import CustomEmulatorBase, is_element
+from autopilot.introspection._xpathselect import (
+ get_classname_from_path,
+ get_path_root,
+)
from autopilot.exceptions import ProcessSearchError
from autopilot.introspection._search import (
- get_proxy_object_for_existing_process
+ get_proxy_object_for_existing_process,
+ get_proxy_object_for_existing_process_by_name,
)
# TODO: Remove ProcessSearchError from here once all our clients have stopped
# using it from this location.
__all__ = [
'CustomEmulatorBase',
+ 'is_element',
'get_classname_from_path',
+ 'get_path_root',
'ProxyBase',
'ProcessSearchError',
'get_proxy_object_for_existing_process',
+ 'get_proxy_object_for_existing_process_by_name',
]
ProxyBase = CustomEmulatorBase
diff -Nru autopilot-1.5.1+16.04.20160428/autopilot/introspection/_search.py autopilot-1.6.0+16.10.20160818.1/autopilot/introspection/_search.py
--- autopilot-1.5.1+16.04.20160428/autopilot/introspection/_search.py 2016-04-28 19:12:35.000000000 +0000
+++ autopilot-1.6.0+16.10.20160818.1/autopilot/introspection/_search.py 2016-08-18 19:23:59.000000000 +0000
@@ -39,6 +39,7 @@
from autopilot.introspection.utilities import (
_get_bus_connections_pid,
_pid_is_running,
+ process_util,
)
from autopilot.utilities import deprecated
@@ -194,6 +195,29 @@
)
+def get_proxy_object_for_existing_process_by_name(
+ process_name,
+ emulator_base=None
+):
+ """Return the proxy object for a process by its name.
+
+ :param process_name: name of the process to get proxy object.
+ This must be a string.
+
+ :param emulator_base: emulator base to use with the custom proxy object.
+
+ :raises ValueError: if process not running or more than one PIDs
+ associated with the process.
+
+ :return: proxy object for the requested process.
+ """
+ pid = process_util.get_pid_for_process(process_name)
+ return get_proxy_object_for_existing_process(
+ pid=pid,
+ emulator_base=emulator_base
+ )
+
+
def _map_connection_to_pid(connection, dbus_bus):
try:
return _get_bus_connections_pid(dbus_bus, connection)
diff -Nru autopilot-1.5.1+16.04.20160428/autopilot/introspection/utilities.py autopilot-1.6.0+16.10.20160818.1/autopilot/introspection/utilities.py
--- autopilot-1.5.1+16.04.20160428/autopilot/introspection/utilities.py 2016-04-28 19:12:35.000000000 +0000
+++ autopilot-1.6.0+16.10.20160818.1/autopilot/introspection/utilities.py 2016-08-18 19:23:59.000000000 +0000
@@ -17,8 +17,12 @@
# along with this program. If not, see .
#
+import os
+from contextlib import contextmanager
+
from dbus import Interface
-import os.path
+
+from autopilot.utilities import process_iter
def _pid_is_running(pid):
@@ -44,3 +48,94 @@
"""Translates the *state_dict* passed in so the keys are usable as python
attributes."""
return {k.replace('-', '_'): v for k, v in state_dict.items()}
+
+
+def sort_by_keys(instances, sort_keys):
+ """Sorts DBus object instances by requested keys."""
+ def get_sort_key(item):
+ sort_key = []
+ for sk in sort_keys:
+ if not isinstance(sk, str):
+ raise ValueError(
+ 'Parameter `sort_keys` must be a list of strings'
+ )
+ value = item
+ for key in sk.split('.'):
+ value = getattr(value, key)
+ sort_key.append(value)
+ return sort_key
+
+ if sort_keys and not isinstance(sort_keys, list):
+ raise ValueError('Parameter `sort_keys` must be a list.')
+ if len(instances) > 1 and sort_keys:
+ return sorted(instances, key=get_sort_key)
+ return instances
+
+
+class ProcessUtil:
+ """Helper class to manipulate running processes."""
+
+ @contextmanager
+ def mocked(self, fake_processes):
+ """Enable mocking for the ProcessUtil class
+
+ Also mocks all calls to autopilot.utilities.process_iter.
+ One may use it like::
+
+ from autopilot.introspection.utilities import ProcessUtil
+
+ process_util = ProcessUtil()
+ with process_util.mocked([{'pid': -9, 'name': 'xx'}]):
+ self.assertThat(
+ process_util.get_pid_for_process('xx'),
+ Equals(-9)
+ )
+ )
+
+ """
+ process_iter.enable_mock(fake_processes)
+ try:
+ yield self
+ finally:
+ process_iter.disable_mock()
+
+ def _query_pids_for_process(self, process_name):
+ if not isinstance(process_name, str):
+ raise ValueError('Process name must be a string.')
+
+ pids = [process.pid for process in process_iter()
+ if process.name() == process_name]
+
+ if not pids:
+ raise ValueError('Process \'{}\' not running'.format(process_name))
+
+ return pids
+
+ def get_pid_for_process(self, process_name):
+ """Returns the PID associated with a process name.
+
+ :param process_name: Process name to get PID for. This must
+ be a string.
+
+ :return: PID of the requested process.
+ """
+ pids = self._query_pids_for_process(process_name)
+ if len(pids) > 1:
+ raise ValueError(
+ 'More than one PID exists for process \'{}\''.format(
+ process_name
+ )
+ )
+
+ return pids[0]
+
+ def get_pids_for_process(self, process_name):
+ """Returns PID(s) associated with a process name.
+
+ :param process_name: Process name to get PID(s) for.
+
+ :return: A list containing the PID(s) of the requested process.
+ """
+ return self._query_pids_for_process(process_name)
+
+process_util = ProcessUtil()
diff -Nru autopilot-1.5.1+16.04.20160428/autopilot/introspection/_xpathselect.py autopilot-1.6.0+16.10.20160818.1/autopilot/introspection/_xpathselect.py
--- autopilot-1.5.1+16.04.20160428/autopilot/introspection/_xpathselect.py 2016-04-28 19:12:35.000000000 +0000
+++ autopilot-1.6.0+16.10.20160818.1/autopilot/introspection/_xpathselect.py 2016-08-18 19:23:59.000000000 +0000
@@ -46,6 +46,7 @@
Queries are executed in the autopilot.introspection.backends module.
"""
+from pathlib import Path
import re
from autopilot.utilities import compatible_repr
@@ -390,15 +391,25 @@
)
-def get_classname_from_path(object_path):
- """Given an object path, return the class name component."""
+def _get_node(object_path, index):
# TODO: Find places where paths are strings, and convert them to
# bytestrings. Figure out what to do with the whole string vs. bytestring
# mess.
- is_string = isinstance(object_path, str)
- if is_string:
- object_path = object_path.encode('utf-8')
- class_name = object_path.split(b"/")[-1]
- if is_string:
- class_name = class_name.decode('utf-8')
- return class_name
+ try:
+ return Path(object_path).parts[index]
+ except TypeError:
+ if not isinstance(object_path, bytes):
+ raise TypeError(
+ 'Object path needs to be a string literal or a bytes literal'
+ )
+ return object_path.split(b"/")[index]
+
+
+def get_classname_from_path(object_path):
+ """Given an object path, return the class name component."""
+ return _get_node(object_path, -1)
+
+
+def get_path_root(object_path):
+ """Return the name of the root node of specified path."""
+ return _get_node(object_path, 1)
diff -Nru autopilot-1.5.1+16.04.20160428/autopilot/tests/functional/__init__.py autopilot-1.6.0+16.10.20160818.1/autopilot/tests/functional/__init__.py
--- autopilot-1.5.1+16.04.20160428/autopilot/tests/functional/__init__.py 2016-04-28 19:12:35.000000000 +0000
+++ autopilot-1.6.0+16.10.20160818.1/autopilot/tests/functional/__init__.py 2016-08-18 19:23:59.000000000 +0000
@@ -173,13 +173,13 @@
load_entry_point_script = """\
#!/usr/bin/python
-__requires__ = 'autopilot==1.5.0'
+__requires__ = 'autopilot==1.6.0'
import sys
from pkg_resources import load_entry_point
if __name__ == '__main__':
sys.exit(
- load_entry_point('autopilot==1.5.0', 'console_scripts', 'autopilot3')()
+ load_entry_point('autopilot==1.6.0', 'console_scripts', 'autopilot3')()
)
"""
diff -Nru autopilot-1.5.1+16.04.20160428/autopilot/tests/functional/test_dbus_query.py autopilot-1.6.0+16.10.20160818.1/autopilot/tests/functional/test_dbus_query.py
--- autopilot-1.5.1+16.04.20160428/autopilot/tests/functional/test_dbus_query.py 2016-04-28 19:12:35.000000000 +0000
+++ autopilot-1.6.0+16.10.20160818.1/autopilot/tests/functional/test_dbus_query.py 2016-08-18 19:23:59.000000000 +0000
@@ -90,6 +90,35 @@
window_parent = main_window.get_parent()
self.assertThat(window_parent.id, Equals(root.id))
+ def test_can_select_specific_parent(self):
+ root = self.start_fully_featured_app()
+ action_item = root.select_single('QAction', text='Save')
+ window_parent = action_item.get_parent('window-mocker')
+ self.assertThat(window_parent.id, Equals(root.id))
+
+ def test_select_parent_raises_if_node_not_parent(self):
+ root = self.start_fully_featured_app()
+ action_item = root.select_single('QAction', text='Save')
+ match_fn = lambda: action_item.get_parent('QMadeUpType')
+ self.assertThat(match_fn, raises(StateNotFoundError('QMadeUpType')))
+
+ def test_select_parent_with_property_only(self):
+ root = self.start_fully_featured_app()
+ action_item = root.select_single('QAction', text='Save')
+ # The ID of parent of a tree is always 1.
+ window_parent = action_item.get_parent(id=1)
+ self.assertThat(window_parent.id, Equals(root.id))
+
+ def test_select_parent_raises_if_property_not_match(self):
+ root = self.start_fully_featured_app()
+ action_item = root.select_single('QAction', text='Save')
+ self.assertIsNotNone(action_item.get_parent('QMenu'))
+ match_fn = lambda: action_item.get_parent('QMenu', visible=True)
+ self.assertThat(
+ match_fn,
+ raises(StateNotFoundError('QMenu', visible=True))
+ )
+
def test_single_select_on_object(self):
"""Must be able to select a single unique child of an object."""
app = self.start_fully_featured_app()
@@ -141,7 +170,7 @@
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(ValueError))
+ self.assertRaises(ValueError, fn)
def test_select_single_no_match_raises_exception(self):
app = self.start_fully_featured_app()
@@ -183,12 +212,12 @@
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))
+ self.assertRaises(ValueError, fn)
def test_select_many_no_name_no_parameter_raises_exception(self):
app = self.start_fully_featured_app()
fn = lambda: app.select_single()
- self.assertThat(fn, raises(ValueError))
+ self.assertRaises(ValueError, fn)
def test_select_many_only_using_parameters(self):
app = self.start_fully_featured_app()
@@ -200,6 +229,18 @@
failed_match = app.select_many('QMenu', title='qwerty')
self.assertThat(failed_match, Equals([]))
+ def test_select_many_sorted_result(self):
+ app = self.start_fully_featured_app()
+ un_sorted_texts = [item.text for item in app.select_many('QAction')]
+ sorted_texts = [
+ item.text for item in app.select_many(
+ 'QAction',
+ ap_result_sort_keys=['text']
+ )
+ ]
+ self.assertNotEqual(un_sorted_texts, sorted_texts)
+ self.assertEqual(sorted_texts, sorted(un_sorted_texts))
+
def test_wait_select_single_succeeds_quickly(self):
app = self.start_fully_featured_app()
start_time = default_timer()
@@ -208,14 +249,76 @@
self.assertThat(main_window, NotEquals(None))
self.assertThat(abs(end_time - start_time), LessThan(1))
- def test_wait_select_single_fails_slowly(self):
+ def test_wait_select_single_timeout_less_than_ten_seconds(self):
+ app = self.start_fully_featured_app()
+ match_fn = lambda: app.wait_select_single(
+ 'QMadeupType',
+ ap_query_timeout=3
+ )
+ start_time = default_timer()
+ self.assertThat(match_fn, raises(StateNotFoundError('QMadeupType')))
+ end_time = default_timer()
+ self.assertThat(abs(end_time - start_time), GreaterThan(2))
+ self.assertThat(abs(end_time - start_time), LessThan(4))
+
+ def test_wait_select_single_timeout_more_than_ten_seconds(self):
+ app = self.start_fully_featured_app()
+ match_fn = lambda: app.wait_select_single(
+ 'QMadeupType',
+ ap_query_timeout=12
+ )
+ start_time = default_timer()
+ self.assertThat(match_fn, raises(StateNotFoundError('QMadeupType')))
+ end_time = default_timer()
+ self.assertThat(abs(end_time - start_time), GreaterThan(11))
+ self.assertThat(abs(end_time - start_time), LessThan(13))
+
+ def test_wait_select_many_requested_elements_count_not_match_raises(self):
app = self.start_fully_featured_app()
+ fn = lambda: app.wait_select_many(
+ 'QMadeupType',
+ ap_query_timeout=4,
+ ap_result_count=2
+ )
start_time = default_timer()
- fn = lambda: app.wait_select_single('QMadeupType')
- self.assertThat(fn, raises(StateNotFoundError('QMadeupType')))
+ self.assertRaises(ValueError, fn)
end_time = default_timer()
- self.assertThat(abs(end_time - start_time), GreaterThan(9))
- self.assertThat(abs(end_time - start_time), LessThan(11))
+ self.assertThat(abs(end_time - start_time), GreaterThan(3))
+ self.assertThat(abs(end_time - start_time), LessThan(5))
+
+ def test_wait_select_many_requested_elements_count_matches(self):
+ app = self.start_fully_featured_app()
+ start_time = default_timer()
+ menus = app.wait_select_many(
+ 'QMenu',
+ ap_query_timeout=4,
+ ap_result_count=3
+ )
+ end_time = default_timer()
+ self.assertThat(len(menus), GreaterThan(2))
+ self.assertThat(abs(end_time - start_time), LessThan(5))
+
+ def test_wait_select_many_sorted_result(self):
+ app = self.start_fully_featured_app()
+ start_time = default_timer()
+ sorted_action_items = app.wait_select_many(
+ 'QAction',
+ ap_query_timeout=4,
+ ap_result_count=10,
+ ap_result_sort_keys=['text']
+ )
+ end_time = default_timer()
+ self.assertThat(len(sorted_action_items), GreaterThan(9))
+ self.assertThat(abs(end_time - start_time), LessThan(5))
+ unsorted_action_items = app.wait_select_many(
+ 'QAction',
+ ap_query_timeout=4,
+ ap_result_count=10
+ )
+ sorted_texts = [item.text for item in sorted_action_items]
+ un_sorted_texts = [item.text for item in unsorted_action_items]
+ self.assertNotEqual(un_sorted_texts, sorted_texts)
+ self.assertEqual(sorted_texts, sorted(un_sorted_texts))
@skipIf(platform.model() != "Desktop", "Only suitable on Desktop (WinMocker)")
diff -Nru autopilot-1.5.1+16.04.20160428/autopilot/tests/unit/introspection_base.py autopilot-1.6.0+16.10.20160818.1/autopilot/tests/unit/introspection_base.py
--- autopilot-1.5.1+16.04.20160428/autopilot/tests/unit/introspection_base.py 1970-01-01 00:00:00.000000000 +0000
+++ autopilot-1.6.0+16.10.20160818.1/autopilot/tests/unit/introspection_base.py 2016-08-18 19:23:59.000000000 +0000
@@ -0,0 +1,45 @@
+# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
+#
+# Autopilot Functional Test Tool
+# Copyright (C) 2016 Canonical
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+#
+
+from unittest.mock import Mock
+from collections import namedtuple
+
+
+X_DEFAULT = 0
+Y_DEFAULT = 0
+W_DEFAULT = 0
+H_DEFAULT = 0
+
+global_rect = namedtuple('globalRect', ['x', 'y', 'w', 'h'])
+
+
+class MockObject(Mock):
+ def __init__(self, *args, **kwargs):
+ super().__init__()
+ for k, v in kwargs.items():
+ setattr(self, k, v)
+
+ def get_properties(self):
+ return self.__dict__
+
+get_mock_object = MockObject
+
+
+def get_global_rect(x=X_DEFAULT, y=Y_DEFAULT, w=W_DEFAULT, h=H_DEFAULT):
+ return global_rect(x, y, w, h)
diff -Nru autopilot-1.5.1+16.04.20160428/autopilot/tests/unit/test_exceptions.py autopilot-1.6.0+16.10.20160818.1/autopilot/tests/unit/test_exceptions.py
--- autopilot-1.5.1+16.04.20160428/autopilot/tests/unit/test_exceptions.py 2016-04-28 19:12:35.000000000 +0000
+++ autopilot-1.6.0+16.10.20160818.1/autopilot/tests/unit/test_exceptions.py 2016-08-18 19:23:59.000000000 +0000
@@ -78,7 +78,7 @@
EndsWith(
'Tips on minimizing the occurrence of this failure '
'are available here: '
- 'https://developer.ubuntu.com/api/autopilot/python/1.5.0/'
+ 'https://developer.ubuntu.com/api/autopilot/python/1.6.0/'
'faq-troubleshooting/'
)
)
diff -Nru autopilot-1.5.1+16.04.20160428/autopilot/tests/unit/test_input.py autopilot-1.6.0+16.10.20160818.1/autopilot/tests/unit/test_input.py
--- autopilot-1.5.1+16.04.20160428/autopilot/tests/unit/test_input.py 2016-04-28 19:12:35.000000000 +0000
+++ autopilot-1.6.0+16.10.20160818.1/autopilot/tests/unit/test_input.py 2016-08-18 19:23:59.000000000 +0000
@@ -1078,3 +1078,26 @@
pointer.press()
pointer._device._device.finger_down.assert_called_once_with(10, 10)
+
+
+class UInputPowerButtonTestCase(TestCase):
+
+ def get_mock_hardware_keys_device(self):
+ power_button = _uinput.UInputHardwareKeysDevice(device_class=Mock)
+ power_button._device.mock_add_spec(uinput.UInput, spec_set=True)
+ return power_button
+
+ def assert_power_button_press_release_emitted_write_and_sync(self, calls):
+ expected_calls = [
+ call.write(ecodes.EV_KEY, ecodes.KEY_POWER, 1),
+ call.write(ecodes.EV_KEY, ecodes.KEY_POWER, 0),
+ call.syn(),
+ ]
+ self.assertEquals(expected_calls, calls)
+
+ def test_power_button_press_release_emitted_write_and_sync(self):
+ device = self.get_mock_hardware_keys_device()
+ device.press_and_release_power_button()
+ self.assert_power_button_press_release_emitted_write_and_sync(
+ device._device.mock_calls
+ )
diff -Nru autopilot-1.5.1+16.04.20160428/autopilot/tests/unit/test_introspection_dbus.py autopilot-1.6.0+16.10.20160818.1/autopilot/tests/unit/test_introspection_dbus.py
--- autopilot-1.5.1+16.04.20160428/autopilot/tests/unit/test_introspection_dbus.py 2016-04-28 19:12:35.000000000 +0000
+++ autopilot-1.6.0+16.10.20160818.1/autopilot/tests/unit/test_introspection_dbus.py 2016-08-18 19:23:59.000000000 +0000
@@ -35,10 +35,25 @@
)
from autopilot.exceptions import StateNotFoundError
-from autopilot.introspection import CustomEmulatorBase
-from autopilot.introspection import dbus
+from autopilot.introspection import (
+ CustomEmulatorBase,
+ dbus,
+ is_element,
+)
+from autopilot.introspection.dbus import (
+ _MockableDbusObject,
+ _validate_object_properties,
+)
from autopilot.utilities import sleep
+from autopilot.tests.unit.introspection_base import (
+ W_DEFAULT,
+ X_DEFAULT,
+ Y_DEFAULT,
+ get_mock_object,
+ get_global_rect,
+)
+
class IntrospectionFeatureTests(TestCase):
@@ -264,3 +279,88 @@
class Foo(object):
pass
self.assertEqual('Foo', dbus.get_type_name(Foo))
+
+
+class IsElementTestCase(TestCase):
+
+ def raise_state_not_found(self, should_raise=True):
+ if should_raise:
+ raise StateNotFoundError('Just throw the exception')
+
+ def test_returns_false_if_not_element(self):
+ self.assertFalse(is_element(self.raise_state_not_found))
+
+ def test_returns_true_if_element(self):
+ self.assertTrue(
+ is_element(
+ self.raise_state_not_found,
+ should_raise=False
+ )
+ )
+
+
+class IsElementMovingTestCase(TestCase):
+
+ def setUp(self):
+ super().setUp()
+ self.dbus_object = _MockableDbusObject(
+ get_mock_object(globalRect=get_global_rect())
+ )
+
+ def test_returns_true_if_x_changed(self):
+ mock_object = get_mock_object(
+ globalRect=get_global_rect(x=X_DEFAULT + 1)
+ )
+ with self.dbus_object.mocked(mock_object) as mocked_dbus_object:
+ self.assertTrue(mocked_dbus_object.is_moving())
+
+ def test_returns_true_if_y_changed(self):
+ mock_object = get_mock_object(
+ globalRect=get_global_rect(y=Y_DEFAULT + 1)
+ )
+ with self.dbus_object.mocked(mock_object) as mocked_dbus_object:
+ self.assertTrue(mocked_dbus_object.is_moving())
+
+ def test_returns_true_if_x_and_y_changed(self):
+ mock_object = get_mock_object(
+ globalRect=get_global_rect(x=X_DEFAULT + 1, y=Y_DEFAULT + 1)
+ )
+ with self.dbus_object.mocked(mock_object) as mocked_dbus_object:
+ self.assertTrue(mocked_dbus_object.is_moving())
+
+ def test_returns_false_if_x_and_y_not_changed(self):
+ mock_object = get_mock_object(globalRect=get_global_rect())
+ with self.dbus_object.mocked(mock_object) as mocked_dbus_object:
+ self.assertFalse(mocked_dbus_object.is_moving())
+
+
+class ValidateObjectPropertiesTestCase(TestCase):
+
+ def test_return_true_if_property_match(self):
+ mock_object = get_mock_object(x=X_DEFAULT)
+ self.assertTrue(_validate_object_properties(mock_object, x=X_DEFAULT))
+
+ def test_returns_true_if_all_properties_match(self):
+ mock_object = get_mock_object(x=X_DEFAULT, y=Y_DEFAULT)
+ self.assertTrue(
+ _validate_object_properties(mock_object, x=X_DEFAULT, y=Y_DEFAULT)
+ )
+
+ def test_return_false_if_property_not_match(self):
+ mock_object = get_mock_object(x=X_DEFAULT + 1)
+ self.assertFalse(_validate_object_properties(mock_object, x=X_DEFAULT))
+
+ def test_returns_false_if_property_invalid(self):
+ mock_object = get_mock_object()
+ self.assertFalse(_validate_object_properties(mock_object, x=X_DEFAULT))
+
+ def test_returns_false_if_any_property_not_match(self):
+ mock_object = get_mock_object(x=X_DEFAULT, y=Y_DEFAULT, w=W_DEFAULT)
+ self.assertFalse(
+ _validate_object_properties(
+ mock_object,
+ x=X_DEFAULT,
+ y=Y_DEFAULT,
+ w=W_DEFAULT + 1
+ )
+ )
diff -Nru autopilot-1.5.1+16.04.20160428/autopilot/tests/unit/test_introspection_utilities.py autopilot-1.6.0+16.10.20160818.1/autopilot/tests/unit/test_introspection_utilities.py
--- autopilot-1.5.1+16.04.20160428/autopilot/tests/unit/test_introspection_utilities.py 1970-01-01 00:00:00.000000000 +0000
+++ autopilot-1.6.0+16.10.20160818.1/autopilot/tests/unit/test_introspection_utilities.py 2016-08-18 19:23:59.000000000 +0000
@@ -0,0 +1,182 @@
+# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
+#
+# Autopilot Functional Test Tool
+# Copyright (C) 2016 Canonical
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+#
+
+from testtools import TestCase
+
+from autopilot.introspection.dbus import raises
+from autopilot.introspection.utilities import process_util, sort_by_keys
+from autopilot.tests.unit.introspection_base import (
+ get_mock_object,
+ get_global_rect,
+)
+
+PROCESS_NAME = 'dummy_process'
+PROCESS_WITH_SINGLE_INSTANCE = [{'name': PROCESS_NAME, 'pid': -80}]
+PROCESS_WITH_MULTIPLE_INSTANCES = [
+ PROCESS_WITH_SINGLE_INSTANCE[0],
+ {'name': PROCESS_NAME, 'pid': -81}
+]
+# a list of dummy co-ordinates
+X_COORDS = [0, 0, 0, 0]
+Y_COORDS = [7, 9, 18, 14]
+
+
+class ProcessUtilitiesTestCase(TestCase):
+
+ def test_passing_non_running_process_raises(self):
+ self.assertRaises(
+ ValueError,
+ process_util._query_pids_for_process,
+ PROCESS_NAME
+ )
+
+ def test_passing_running_process_not_raises(self):
+ with process_util.mocked(PROCESS_WITH_SINGLE_INSTANCE):
+ self.assertFalse(
+ raises(
+ ValueError,
+ process_util._query_pids_for_process,
+ PROCESS_NAME
+ )
+ )
+
+ def test_passing_integer_raises(self):
+ self.assertRaises(
+ ValueError,
+ process_util._query_pids_for_process,
+ 911
+ )
+
+ def test_pid_for_process_is_int(self):
+ with process_util.mocked(PROCESS_WITH_SINGLE_INSTANCE):
+ self.assertIsInstance(
+ process_util.get_pid_for_process(PROCESS_NAME),
+ int
+ )
+
+ def test_pids_for_process_is_list(self):
+ with process_util.mocked(PROCESS_WITH_MULTIPLE_INSTANCES):
+ self.assertIsInstance(
+ process_util.get_pids_for_process(PROCESS_NAME),
+ list
+ )
+
+ def test_passing_process_with_multiple_pids_raises(self):
+ with process_util.mocked(PROCESS_WITH_MULTIPLE_INSTANCES):
+ self.assertRaises(
+ ValueError,
+ process_util.get_pid_for_process,
+ PROCESS_NAME
+ )
+
+
+class SortByKeysTests(TestCase):
+ def _get_root_property_from_object_list(self, objects, prop):
+ return [getattr(obj, prop) for obj in objects]
+
+ def _get_child_property_from_object_list(self, objects, child, prop):
+ return [getattr(getattr(obj, child), prop) for obj in objects]
+
+ def test_sort_by_single_property(self):
+ objects = [get_mock_object(y=y) for y in Y_COORDS]
+ sorted_objects = sort_by_keys(objects, ['y'])
+ self.assertEqual(len(sorted_objects), len(objects))
+ self.assertEqual(
+ self._get_root_property_from_object_list(sorted_objects, 'y'),
+ sorted(Y_COORDS)
+ )
+
+ def test_sort_by_multiple_properties(self):
+ objects = [
+ get_mock_object(x=x, y=y) for x, y in zip(X_COORDS, Y_COORDS)
+ ]
+
+ sorted_objects = sort_by_keys(objects, ['x', 'y'])
+ self.assertEqual(len(sorted_objects), len(objects))
+ self.assertEqual(
+ self._get_root_property_from_object_list(sorted_objects, 'x'),
+ sorted(X_COORDS)
+ )
+ self.assertEqual(
+ self._get_root_property_from_object_list(sorted_objects, 'y'),
+ sorted(Y_COORDS)
+ )
+
+ def test_sort_by_single_nested_property(self):
+ objects = [
+ get_mock_object(globalRect=get_global_rect(y=y)) for y in Y_COORDS
+ ]
+ sorted_objects = sort_by_keys(objects, ['globalRect.y'])
+ self.assertEqual(len(sorted_objects), len(objects))
+ self.assertEqual(
+ self._get_child_property_from_object_list(
+ sorted_objects,
+ child='globalRect',
+ prop='y'
+ ),
+ sorted(Y_COORDS)
+ )
+
+ def test_sort_by_multiple_nested_properties(self):
+ objects = [
+ get_mock_object(globalRect=get_global_rect(x=x, y=y))
+ for x, y in zip(X_COORDS, Y_COORDS)
+ ]
+ sorted_objects = sort_by_keys(
+ objects,
+ ['globalRect.x', 'globalRect.y']
+ )
+ self.assertEqual(len(sorted_objects), len(objects))
+ self.assertEqual(
+ self._get_child_property_from_object_list(
+ sorted_objects,
+ child='globalRect',
+ prop='x'
+ ),
+ sorted(X_COORDS)
+ )
+ self.assertEqual(
+ self._get_child_property_from_object_list(
+ sorted_objects,
+ child='globalRect',
+ prop='y'
+ ),
+ sorted(Y_COORDS)
+ )
+
+ def test_sort_three_levels_nested_property(self):
+ objects = [
+ get_mock_object(
+ fake_property=get_global_rect(
+ y=get_global_rect(y=y)
+ )
+ ) for y in Y_COORDS
+ ]
+ sorted_objects = sort_by_keys(objects, ['fake_property.y.y'])
+ self.assertEqual(len(sorted_objects), len(objects))
+ sorted_ys = [i.fake_property.y.y for i in sorted_objects]
+ self.assertEqual(sorted_ys, sorted(Y_COORDS))
+
+ def test_raises_if_sort_keys_not_list(self):
+ self.assertRaises(ValueError, sort_by_keys, None, 'y')
+
+ def test_returns_unchanged_if_one_object(self):
+ obj = [get_mock_object()]
+ output = sort_by_keys(obj, ['x'])
+ self.assertEqual(output, obj)
diff -Nru autopilot-1.5.1+16.04.20160428/autopilot/tests/unit/test_introspection_xpathselect.py autopilot-1.6.0+16.10.20160818.1/autopilot/tests/unit/test_introspection_xpathselect.py
--- autopilot-1.5.1+16.04.20160428/autopilot/tests/unit/test_introspection_xpathselect.py 2016-04-28 19:12:35.000000000 +0000
+++ autopilot-1.6.0+16.10.20160818.1/autopilot/tests/unit/test_introspection_xpathselect.py 2016-08-18 19:23:59.000000000 +0000
@@ -369,3 +369,15 @@
"Baz",
xpathselect.get_classname_from_path("/Foo/Bar/Baz")
)
+
+
+class GetPathRootTests(TestCase):
+
+ def test_get_root_path_on_string_path(self):
+ self.assertEqual("Foo", xpathselect.get_path_root("/Foo/Bar/Baz"))
+
+ def test_get_root_path_on_bytes_literal_path(self):
+ self.assertEqual(b"Foo", xpathselect.get_path_root(b"/Foo/Bar/Baz"))
+
+ def test_get_root_path_on_garbage_path_raises(self):
+ self.assertRaises(IndexError, xpathselect.get_path_root, "asdfgh")
diff -Nru autopilot-1.5.1+16.04.20160428/autopilot/utilities.py autopilot-1.6.0+16.10.20160818.1/autopilot/utilities.py
--- autopilot-1.5.1+16.04.20160428/autopilot/utilities.py 2016-04-28 19:12:40.000000000 +0000
+++ autopilot-1.6.0+16.10.20160818.1/autopilot/utilities.py 2016-08-18 19:23:59.000000000 +0000
@@ -27,9 +27,11 @@
import inspect
import logging
import os
+import psutil
import time
import timeit
from testtools.content import text_content
+from unittest.mock import Mock
from functools import wraps
from autopilot.exceptions import BackendException
@@ -621,3 +623,46 @@
return time_delta
else:
return 0.0
+
+
+class MockableProcessIter:
+ def __init__(self):
+ self._mocked = False
+ self._fake_processes = []
+
+ def __call__(self):
+ if not self._mocked:
+ return psutil.process_iter()
+ else:
+ return self.mocked_process_iter()
+
+ @contextmanager
+ def mocked(self, fake_processes):
+ self.enable_mock(fake_processes)
+ try:
+ yield self
+ finally:
+ self.disable_mock()
+
+ def enable_mock(self, fake_processes):
+ self._mocked = True
+ self._fake_processes = fake_processes
+
+ def disable_mock(self):
+ self._mocked = False
+ self._fake_processes = []
+
+ def create_mock_process(self, name, pid):
+ mock_process = Mock()
+ mock_process.name = lambda: name
+ mock_process.pid = pid
+ return mock_process
+
+ def mocked_process_iter(self):
+ for process in self._fake_processes:
+ yield self.create_mock_process(
+ process.get('name'),
+ process.get('pid')
+ )
+
+process_iter = MockableProcessIter()
diff -Nru autopilot-1.5.1+16.04.20160428/debian/changelog autopilot-1.6.0+16.10.20160818.1/debian/changelog
--- autopilot-1.5.1+16.04.20160428/debian/changelog 2016-08-19 18:36:19.000000000 +0000
+++ autopilot-1.6.0+16.10.20160818.1/debian/changelog 2016-08-19 18:36:19.000000000 +0000
@@ -1,3 +1,17 @@
+autopilot (1.6.0+16.10.20160818.1-0ubuntu1) yakkety; urgency=medium
+
+ * Autopilot proxy object
+ - Update: select_many() method to optionally return sorted results.
+ - New: get_path() to get object path in autopilot tree.
+ - New: wait_until_not_moving() block till object stops moving.
+ - New: wait_select_many() method.
+ * Introspection helper methods addition
+ - is_element() to check existence of a proxy object.
+ - get_path_root() to get root path of a proxy object.
+ - get_proxy_object_for_existing_process_by_name().
+
+ -- Omer Akram Thu, 18 Aug 2016 19:24:27 +0000
+
autopilot (1.5.1+16.04.20160428-0ubuntu1) xenial; urgency=medium
[ Albert Astals Cid ]
@@ -258,7 +272,7 @@
* Remove python2 packages and dependencies, as they're now handled by the
autopilot-legacy source package. (lp: #1308661)
* Autopilot vis tool can now search the introspection tree. (lp: #1097392)
- * Autopilot vis tool can now draw a transparent overlay over the selected
+ * Autopilot vis tool can now draw a transparent overlay over the selected
widget.
* Autopilot vis tool now shows the root of the introspection tree (lp: #1298600)
* Autopilot vis tool can now be launched from the unity dash.
@@ -392,7 +406,7 @@
autopilot (1.4+14.04.20140310.1-0ubuntu1) trusty; urgency=low
- *
+ *
-- Ubuntu daily release Mon, 10 Mar 2014 19:39:00 +0000
diff -Nru autopilot-1.5.1+16.04.20160428/debian/control autopilot-1.6.0+16.10.20160818.1/debian/control
--- autopilot-1.5.1+16.04.20160428/debian/control 2016-08-19 18:36:19.000000000 +0000
+++ autopilot-1.6.0+16.10.20160818.1/debian/control 2016-08-19 18:36:19.000000000 +0000
@@ -34,6 +34,8 @@
python3-xlib (>=0.14+20091101-1ubuntu3),
sphinx-common,
texlive-latex-extra,
+# depend on upstart to not build on s390x until upstart is available.
+ upstart,
Standards-Version: 3.9.5
Homepage: https://launchpad.net/autopilot
Vcs-bzr: https://code.launchpad.net/+branch/ubuntu/autopilot
@@ -48,6 +50,7 @@
python3-dateutil,
python3-dbus,
python3-decorator,
+ python3-evdev,
python3-fixtures,
python3-gi,
python3-junitxml,
@@ -67,7 +70,6 @@
libautopilot-gtk (>= 1.4),
libautopilot-qt (>= 1.4),
python3-autopilot-trace,
- python3-evdev,
python3-xlib (>=0.14+20091101-1ubuntu3),
recordmydesktop,
ubuntu-keyboard-data,
diff -Nru autopilot-1.5.1+16.04.20160428/debian/rules autopilot-1.6.0+16.10.20160818.1/debian/rules
--- autopilot-1.5.1+16.04.20160428/debian/rules 2016-08-19 18:36:19.000000000 +0000
+++ autopilot-1.6.0+16.10.20160818.1/debian/rules 2016-08-19 18:36:19.000000000 +0000
@@ -12,7 +12,7 @@
-dh_sphinxdoc
override_dh_auto_build:
- python3 -m flake8.run --ignore=E731,E226,W503 .
+ python3 -m flake8 --ignore=E731,E226,W503 .
dh_auto_build
python3 setup.py build_sphinx -b html
python3 setup.py build_sphinx -b json
diff -Nru autopilot-1.5.1+16.04.20160428/README autopilot-1.6.0+16.10.20160818.1/README
--- autopilot-1.5.1+16.04.20160428/README 2016-04-28 19:12:35.000000000 +0000
+++ autopilot-1.6.0+16.10.20160818.1/README 2016-08-18 19:23:59.000000000 +0000
@@ -12,7 +12,7 @@
https://launchpad.net/autopilot
- Documentation (Tutorial, FAQ, API Reference, etc):
- https://developer.ubuntu.com/api/autopilot/python/1.5.0/
+ https://developer.ubuntu.com/api/autopilot/python/1.6.0/
- IRC channel is #ubuntu-autopilot on irc.freenode.net
@@ -67,7 +67,7 @@
For a more complete explanation for running or listing tests please see the
full documentation found here:
-https://developer.ubuntu.com/api/autopilot/python/1.5.0/guides-running_ap/
+https://developer.ubuntu.com/api/autopilot/python/1.6.0/guides-running_ap/
If you are in the root of the autopilot source tree this will run/list the tests from
within that local module. Otherwise autopilot will look in the system python path.
diff -Nru autopilot-1.5.1+16.04.20160428/setup.py autopilot-1.6.0+16.10.20160818.1/setup.py
--- autopilot-1.5.1+16.04.20160428/setup.py 2016-04-28 19:12:35.000000000 +0000
+++ autopilot-1.6.0+16.10.20160818.1/setup.py 2016-08-18 19:23:59.000000000 +0000
@@ -23,7 +23,7 @@
assert sys.version_info >= (3,), 'Python 3 is required'
-VERSION = '1.5.0'
+VERSION = '1.6.0'
autopilot_tracepoint = Extension(