1
1
# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
2
# Copyright 2012 Canonical
3
# Author: Thomi Richards
5
# This program is free software: you can redistribute it and/or modify it
6
# under the terms of the GNU General Public License version 3, as published
7
# by the Free Software Foundation.
3
# Autopilot Functional Test Tool
4
# Copyright (C) 2012-2013 Canonical
6
# This program is free software: you can redistribute it and/or modify
7
# it under the terms of the GNU General Public License as published by
8
# the Free Software Foundation, either version 3 of the License, or
9
# (at your option) any later version.
11
# This program is distributed in the hope that it will be useful,
12
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
# GNU General Public License for more details.
16
# You should have received a copy of the GNU General Public License
17
# along with this program. If not, see <http://www.gnu.org/licenses/>.
10
21
"""This module contains the code to retrieve state via DBus calls.
47
58
def __new__(cls, classname, bases, classdict):
48
59
"""Add class name to type registry."""
49
60
class_object = type.__new__(cls, classname, bases, classdict)
50
_object_registry[classname] = class_object
62
'ApplicationProxyObject',
64
'DBusIntrospectionObject',
68
if getattr(class_object, '_id', None) is not None:
69
if class_object._id in _object_registry:
70
_object_registry[class_object._id][classname] = class_object
72
_object_registry[class_object._id] = {classname:class_object}
51
73
return class_object
54
def clear_object_registry():
55
"""Clear the object registry.
57
.. important:: DO NOT CALL THIS UNLESS YOU REALLY KNOW WHAT YOU ARE DOING!
58
... and even then, are you *sure*?
60
global _object_registry
62
# NOTE: We used to do '_object_registry.clear()' here, but that causes issues
63
# when trying to use the unity emulators together with an application backend
64
# since the application launch code clears the object registry. This is a case
65
# of the autopilot backend abstraction leaking through into the visible
66
# implementation. I'm planning on fixing that, but it's a sizable amount of work.
67
# Until that happens, we need to live with this hack: don't delete objects if
68
# their DBus service name is com.canonical.Unity
71
for k,v in _object_registry.iteritems():
72
if v.DBUS_SERVICE != "com.canonical.Unity":
76
del _object_registry[k]
79
def get_introspection_iface(service_name, object_path):
80
"""Get the autopilot introspection interface for the specified service name
83
:param string service_name:
84
:param string object_name:
85
:raises: **TypeError** on invalid parameter type
88
if not isinstance(service_name, basestring):
89
raise TypeError("Service name must be a string.")
90
if not isinstance(object_path, basestring):
91
raise TypeError("Object name must be a string")
93
_debug_proxy_obj = get_session_bus().get_object(service_name, object_path)
94
return Interface(_debug_proxy_obj, AP_INTROSPECTION_IFACE)
97
76
def translate_state_keys(state_dict):
98
77
"""Translates the *state_dict* passed in so the keys are usable as python attributes."""
99
78
return {k.replace('-','_'):v for k,v in state_dict.iteritems() }
81
def get_classname_from_path(object_path):
82
return object_path.split("/")[-1]
102
85
def object_passes_filters(instance, **kwargs):
103
86
"""Return true if *instance* satisifies all the filters present in kwargs."""
104
87
with instance.no_automatic_refreshing():
113
96
class DBusIntrospectionObject(object):
114
"""A class that can be created using a dictionary of state from DBus.
97
"""A class that supports transparent data retrieval from the application
116
To use this class properly you must set the DBUS_SERVICE and DBUS_OBJECT
117
class attributes. They should be set to the Service name and object path
118
where the autopilot interface is being exposed.
100
This class is the base class for all objects retrieved from the application
101
under test. It handles transparently refreshing attribute values when needed,
102
and contains many methods to select child objects in the introspection tree.
122
106
__metaclass__ = IntrospectableObjectMetaclass
127
def __init__(self, state_dict, path_info=None):
110
def __init__(self, state_dict, path):
128
111
self.__state = {}
129
112
self.__refresh_on_attribute = True
130
self.set_properties(state_dict)
131
if path_info is None:
132
logger.warning("Constructing object '%s' without path information. This will make \
133
queries on this object, and all child objects considerably slower." % self.__class__.__name__)
134
logger.warning("To avoid this, make sure objects are _not_ constructed with the \
135
get_all_instances(...) class method.")
136
self.path_info = path_info
113
self._set_properties(state_dict)
138
def set_properties(self, state_dict):
116
def _set_properties(self, state_dict):
139
117
"""Creates and set attributes of *self* based on contents of *state_dict*.
141
119
.. note:: Translates '-' to '_', so a key of 'icon-type' for example
271
255
def get_children(self):
272
"""Returns a list of all child objects."""
256
"""Returns a list of all child objects.
258
This returns a list of all children. To return only children of a specific
259
type, use :meth:`get_children_by_type`. To get objects further down the
260
introspection tree (i.e.- nodes that may not necessarily be immeadiate
261
children), use :meth:`select_single` and :meth:`select_many`.
273
264
self.refresh_state()
275
266
query = self.get_class_query_string() + "/*"
276
267
state_dicts = self.get_state_by_path(query)
277
path_info = self.path_info + "/" if self.path_info else None
278
children = [self.make_introspection_object(i, path_info) for i in state_dicts]
268
children = [self.make_introspection_object(i) for i in state_dicts]
271
def select_single(self, type_name='*', **kwargs):
272
"""Get a single node from the introspection tree, with type equal to
273
*type_name* and (optionally) matching the keyword filters present in
276
You must specify either *type_name*, keyword filters or both.
278
This method searches recursively from the instance this method is called
279
on. Calling :meth:`select_single` on the application (root) proxy object
280
will search the entire tree. Calling :meth:`select_single` on an object
281
in the tree will only search it's descendants.
285
app.select_single('QPushButton', objectName='clickme')
286
# returns a QPushButton whose 'objectName' property is 'clickme'.
288
If nothing is returned from the query, this method returns None.
290
:param type_name: Either a string naming the type you want, or a class of
291
the appropriate type (the latter case is for overridden emulator
294
:raises: **ValueError** if the query returns more than one item. *If you
295
want more than one item, use select_many instead*.
297
:raises: **TypeError** if neither *type_name* or keyword filters are
301
Tutorial Section :ref:`custom_emulators`
304
instances = self.select_many(type_name, **kwargs)
305
if len(instances) > 1:
306
raise ValueError("More than one item was returned for query")
311
def select_many(self, type_name='*', **kwargs):
312
"""Get a list of nodes from the introspection tree, with type equal to
313
*type_name* and (optionally) matching the keyword filters present in
316
You must specify either *type_name*, keyword filters or both.
318
This method searches recursively from the instance this method is called
319
on. Calling :meth:`select_many` on the application (root) proxy object
320
will search the entire tree. Calling :meth:`select_many` on an object
321
in the tree will only search it's descendants.
325
app.select_many('QPushButton', enabled=True)
326
# returns a list of QPushButtons that are enabled.
328
As mentioned above, this method searches the object tree recurseivly::
329
file_menu = app.select_one('QMenu', title='File')
330
file_menu.select_many('QAction')
331
# returns a list of QAction objects who appear below file_menu in the object tree.
333
If you only want to get one item, use :meth:`select_single` instead.
335
:param type_name: Either a string naming the type you want, or a class of
336
the appropriate type (the latter case is for overridden emulator
339
:raises: **TypeError** if neither *type_name* or keyword filters are
343
Tutorial Section :ref:`custom_emulators`
346
if not isinstance(type_name, str) and issubclass(type_name, DBusIntrospectionObject):
347
type_name = type_name.__name__
349
if type_name == "*" and not kwargs:
350
raise TypeError("You must specify either a type name or a filter.")
352
logger.debug("Selecting objects of %s with attributes: %r",
353
'any type' if type_name == '*' else 'type ' + type_name,
357
for k,v in kwargs.iteritems():
358
if isinstance(v, str) and '_' not in k:
359
first_param = '[{}={}]'.format(k,v)
362
query_path = "%s//%s%s" % (self.get_class_query_string(),
366
state_dicts = self.get_state_by_path(query_path)
367
instances = [self.make_introspection_object(i) for i in state_dicts]
368
return filter(lambda i: object_passes_filters(i, **kwargs), instances)
281
370
def refresh_state(self):
282
371
"""Refreshes the object's state from unity.
373
You should probably never have to call this directly. Autopilot automatically
374
retrieves new state every time this object's attributes are read.
284
376
:raises: **StateNotFound** if the object in unity has been destroyed.
287
name, new_state = self.get_new_state()
288
self.set_properties(new_state)
379
_, new_state = self.get_new_state()
380
self._set_properties(new_state)
291
383
def get_all_instances(cls):
292
"""Get all instances of this class that exist within the Unity state tree.
294
For example, to get all the BamfLauncherIcons:
296
>>> icons = BamfLauncherIcons.get_all_instances()
384
"""Get all instances of this class that exist within the Application state tree.
386
For example, to get all the LauncherIcon instances:
388
>>> icons = LauncherIcon.get_all_instances()
391
Using this method is slow - it requires a complete scan of the
392
introspection tree. You should only use this when you're not sure where
393
the objects you are looking for are located. Depending on the application
394
you are testing, you may get duplicate results using this method.
298
396
:return: List (possibly empty) of class instances.
300
WARNING: Using this method is slow - it requires a complete scan of the
301
introspection tree. Instead, get the root tree object with
302
get_root_instance, and then navigate to the desired node.
305
399
cls_name = cls.__name__
306
400
instances = cls.get_state_by_path("//%s" % (cls_name))
363
461
def get_class_query_string(self):
364
462
"""Get the XPath query string required to refresh this class's state."""
365
if self.path_info is None:
366
return "//%s[id=%d]" % (self.__class__.__name__, self.id)
463
if not self.path.startswith('/'):
464
return "//" + self.path + "[id=%d]" % self.id
368
return self.path_info + "[id=%d]" % self.id
466
return self.path + "[id=%d]" % self.id
371
def make_introspection_object(cls, dbus_tuple, path_info=None):
372
"""Make an introspection object given a DBus tuple of (name, state_dict).
374
The optional 'path_info' parameter can be set to a string that contains
375
the full, absolute path in the introspection tree to this object.
469
def make_introspection_object(cls, dbus_tuple):
470
"""Make an introspection object given a DBus tuple of (path, state_dict).
377
472
This only works for classes that derive from DBusIntrospectionObject.
379
name, state = dbus_tuple
474
path, state = dbus_tuple
475
name = get_classname_from_path(path)
381
class_type = _object_registry[name]
477
class_type = _object_registry[cls._id][name]
383
479
logger.warning("Generating introspection instance for type '%s' based on generic class.", name)
384
class_type = type(str(name), (cls,), {})
385
if isinstance(path_info, basestring):
386
if not path_info.endswith(name):
387
if not path_info.endswith("/"):
388
logger.error("path_info must end with '/' or class name.")
392
return class_type(state, path_info)
480
# override the _id attr from cls, since we don't want generated types
481
# to end up in the object registry.
482
class_type = type(str(name), (cls,), {'_id':None})
483
return class_type(state, path)
395
486
def no_automatic_refreshing(self):
400
491
>>> with instance.no_automatic_refreshing():
401
492
# access lots of attributes.
494
This can be useful if you need to check lots of attributes in a tight loop,
495
or if you want to atomicaly check several attributes at once.
405
499
self.__refresh_on_attribute = False
408
502
self.__refresh_on_attribute = True
505
class _CustomEmulatorMeta(IntrospectableObjectMetaclass):
507
def __new__(cls, name, bases, d):
508
# only consider classes derived from CustomEmulatorBase
509
if name != 'CustomEmulatorBase':
510
# and only if they don't already have an Id set.
513
if hasattr(base, '_id'):
518
return super(_CustomEmulatorMeta, cls).__new__(cls, name, bases, d)
521
class CustomEmulatorBase(DBusIntrospectionObject):
523
"""This class must be used as a base class for any custom emulators defined
527
Tutorial Section :ref:`custom_emulators`
528
Information on how to write custom emulators.
531
__metaclass__ = _CustomEmulatorMeta