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(