~ubuntu-branches/ubuntu/saucy/autopilot/saucy-proposed

« back to all changes in this revision

Viewing changes to autopilot/introspection/dbus.py

  • Committer: Package Import Robot
  • Author(s): Didier Roche
  • Date: 2013-06-07 13:33:46 UTC
  • mfrom: (57.1.1 saucy-proposed)
  • Revision ID: package-import@ubuntu.com-20130607133346-42zvbl1h2k1v54ac
Tags: 1.3daily13.06.05-0ubuntu2
autopilot-touch only suggests python-ubuntu-platform-api for now.
It's not in distro and we need that requirement to be fulfilled to
have unity 7, 100 scopes and the touch stack to distro.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
1
# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
2
 
# Copyright 2012 Canonical
3
 
# Author: Thomi Richards
4
 
#
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.
8
 
#
 
2
#
 
3
# Autopilot Functional Test Tool
 
4
# Copyright (C) 2012-2013 Canonical
 
5
#
 
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.
 
10
#
 
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.
 
15
#
 
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/>.
 
18
#
 
19
 
9
20
 
10
21
"""This module contains the code to retrieve state via DBus calls.
11
22
 
22
33
from testtools.matchers import Equals
23
34
from time import sleep
24
35
from textwrap import dedent
 
36
from uuid import uuid4
25
37
 
26
 
from autopilot.emulators.dbus_handler import get_session_bus
27
38
from autopilot.introspection.constants import AP_INTROSPECTION_IFACE
28
39
from autopilot.utilities import Timer
29
40
 
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
 
61
        if classname in (
 
62
            'ApplicationProxyObject',
 
63
            'CustomEmulatorBase',
 
64
            'DBusIntrospectionObject',
 
65
            ):
 
66
            return class_object
 
67
 
 
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
 
71
            else:
 
72
                _object_registry[class_object._id] = {classname:class_object}
51
73
        return class_object
52
74
 
53
75
 
54
 
def clear_object_registry():
55
 
    """Clear the object registry.
56
 
 
57
 
    .. important:: DO NOT CALL THIS UNLESS YOU REALLY KNOW WHAT YOU ARE DOING!
58
 
     ... and even then, are you *sure*?
59
 
    """
60
 
    global _object_registry
61
 
 
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
69
 
    # - Thomi Richards
70
 
    to_delete = []
71
 
    for k,v in _object_registry.iteritems():
72
 
        if v.DBUS_SERVICE != "com.canonical.Unity":
73
 
            to_delete.append(k)
74
 
 
75
 
    for k in to_delete:
76
 
        del _object_registry[k]
77
 
 
78
 
 
79
 
def get_introspection_iface(service_name, object_path):
80
 
    """Get the autopilot introspection interface for the specified service name
81
 
    and object path.
82
 
 
83
 
    :param string service_name:
84
 
    :param string object_name:
85
 
    :raises: **TypeError** on invalid parameter type
86
 
 
87
 
    """
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")
92
 
 
93
 
    _debug_proxy_obj = get_session_bus().get_object(service_name, object_path)
94
 
    return Interface(_debug_proxy_obj, AP_INTROSPECTION_IFACE)
95
 
 
96
 
 
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() }
100
79
 
101
80
 
 
81
def get_classname_from_path(object_path):
 
82
    return object_path.split("/")[-1]
 
83
 
 
84
 
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():
111
94
 
112
95
 
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
 
98
    under test.
115
99
 
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.
119
103
 
120
104
    """
121
105
 
122
106
    __metaclass__ = IntrospectableObjectMetaclass
123
107
 
124
 
    DBUS_SERVICE = None
125
 
    DBUS_OBJECT = None
 
108
    _Backend = None
126
109
 
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)
 
114
        self.path = path
137
115
 
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*.
140
118
 
141
119
        .. note:: Translates '-' to '_', so a key of 'icon-type' for example
180
158
 
181
159
            time_left = timeout
182
160
            while True:
183
 
                name, new_state = self.parent.get_new_state()
 
161
                _, new_state = self.parent.get_new_state()
184
162
                new_state = translate_state_keys(new_state)
185
163
                new_value = new_state[self.name]
186
164
                # Support for testtools.matcher classes:
188
166
                if mismatch:
189
167
                    failure_msg = mismatch.describe()
190
168
                else:
191
 
                    self.parent.set_properties(new_state)
 
169
                    self.parent._set_properties(new_state)
192
170
                    return
193
171
 
194
172
                if time_left >= 1:
224
202
 
225
203
        >>> get_children_by_type(Launcher, monitor=1)
226
204
 
227
 
        will return only LauncherInstances that have an attribute 'monitor' that
 
205
        will return only Launcher instances that have an attribute 'monitor' that
228
206
        is equal to 1. The type can also be specified as a string, which is
229
207
        useful if there is no emulator class specified:
230
208
 
233
211
        Note however that if you pass a string, and there is an emulator class
234
212
        defined, autopilot will not use it.
235
213
 
236
 
        :param desired_type:
237
 
        :type desired_type: subclass of DBusIntrospectionObject, or a string.
 
214
        :param desired_type: Either a string naming the type you want, or a class
 
215
            of the type you want (the latter is used when defining custom emulators)
238
216
 
239
 
        .. important:: *desired_type* **must** be a subclass of
240
 
         DBusIntrospectionObject.
 
217
        .. seealso::
 
218
            Tutorial Section :ref:`custom_emulators`
241
219
 
242
220
        """
243
221
        #TODO: if kwargs has exactly one item in it we should specify the
260
238
        return result
261
239
 
262
240
    def get_properties(self):
263
 
        """Returns a dictionary of all the properties on this class."""
 
241
        """Returns a dictionary of all the properties on this class.
 
242
 
 
243
        This can be useful when you want to log all the properties exported from
 
244
        your application for a particular object. Every property in the returned
 
245
        dictionary can be accessed as attributes of the object as well.
 
246
 
 
247
        """
264
248
        # Since we're grabbing __state directly there's no implied state
265
249
        # refresh, so do it manually:
266
250
        self.refresh_state()
269
253
        return props
270
254
 
271
255
    def get_children(self):
272
 
        """Returns a list of all child objects."""
 
256
        """Returns a list of all child objects.
 
257
 
 
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`.
 
262
 
 
263
        """
273
264
        self.refresh_state()
274
265
 
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]
279
269
        return children
280
270
 
 
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
 
274
        *kwargs*.
 
275
 
 
276
        You must specify either *type_name*, keyword filters or both.
 
277
 
 
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.
 
282
 
 
283
        Example usage::
 
284
 
 
285
            app.select_single('QPushButton', objectName='clickme')
 
286
            # returns a QPushButton whose 'objectName' property is 'clickme'.
 
287
 
 
288
        If nothing is returned from the query, this method returns None.
 
289
 
 
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
 
292
            classes).
 
293
 
 
294
        :raises: **ValueError** if the query returns more than one item. *If you
 
295
            want more than one item, use select_many instead*.
 
296
 
 
297
        :raises: **TypeError** if neither *type_name* or keyword filters are
 
298
            provided.
 
299
 
 
300
        .. seealso::
 
301
            Tutorial Section :ref:`custom_emulators`
 
302
 
 
303
        """
 
304
        instances = self.select_many(type_name, **kwargs)
 
305
        if len(instances) > 1:
 
306
            raise ValueError("More than one item was returned for query")
 
307
        if not instances:
 
308
            return None
 
309
        return instances[0]
 
310
 
 
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
 
314
        *kwargs*.
 
315
 
 
316
        You must specify either *type_name*, keyword filters or both.
 
317
 
 
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.
 
322
 
 
323
        Example Usage::
 
324
 
 
325
            app.select_many('QPushButton', enabled=True)
 
326
            # returns a list of QPushButtons that are enabled.
 
327
 
 
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.
 
332
 
 
333
        If you only want to get one item, use :meth:`select_single` instead.
 
334
 
 
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
 
337
            classes).
 
338
 
 
339
        :raises: **TypeError** if neither *type_name* or keyword filters are
 
340
            provided.
 
341
 
 
342
        .. seealso::
 
343
            Tutorial Section :ref:`custom_emulators`
 
344
 
 
345
        """
 
346
        if not isinstance(type_name, str) and issubclass(type_name, DBusIntrospectionObject):
 
347
            type_name = type_name.__name__
 
348
 
 
349
        if type_name == "*" and not kwargs:
 
350
            raise TypeError("You must specify either a type name or a filter.")
 
351
 
 
352
        logger.debug("Selecting objects of %s with attributes: %r",
 
353
            'any type' if type_name == '*' else 'type ' + type_name,
 
354
            kwargs)
 
355
 
 
356
        first_param = ''
 
357
        for k,v in kwargs.iteritems():
 
358
            if isinstance(v, str) and '_' not in k:
 
359
                first_param = '[{}={}]'.format(k,v)
 
360
                kwargs.pop(k)
 
361
                break
 
362
        query_path = "%s//%s%s" % (self.get_class_query_string(),
 
363
                                   type_name,
 
364
                                   first_param)
 
365
 
 
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)
 
369
 
281
370
    def refresh_state(self):
282
371
        """Refreshes the object's state from unity.
283
372
 
 
373
        You should probably never have to call this directly. Autopilot automatically
 
374
        retrieves new state every time this object's attributes are read.
 
375
 
284
376
        :raises: **StateNotFound** if the object in unity has been destroyed.
285
377
 
286
378
        """
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)
289
381
 
290
382
    @classmethod
291
383
    def get_all_instances(cls):
292
 
        """Get all instances of this class that exist within the Unity state tree.
293
 
 
294
 
        For example, to get all the BamfLauncherIcons:
295
 
 
296
 
        >>> icons = BamfLauncherIcons.get_all_instances()
 
384
        """Get all instances of this class that exist within the Application state tree.
 
385
 
 
386
        For example, to get all the LauncherIcon instances:
 
387
 
 
388
        >>> icons = LauncherIcon.get_all_instances()
 
389
 
 
390
        .. warning::
 
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.
297
395
 
298
396
        :return: List (possibly empty) of class instances.
299
397
 
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.
303
 
 
304
398
        """
305
399
        cls_name = cls.__name__
306
400
        instances = cls.get_state_by_path("//%s" % (cls_name))
308
402
 
309
403
    @classmethod
310
404
    def get_root_instance(cls) :
311
 
        """Get the object at the root of this tree."""
 
405
        """Get the object at the root of this tree.
 
406
 
 
407
        This will return an object that represents the root of the introspection
 
408
        tree.
 
409
 
 
410
        """
312
411
        instances = cls.get_state_by_path("/")
313
412
        if len(instances) != 1:
314
413
            logger.error("Could not retrieve root object.")
315
414
            return None
316
 
        return cls.make_introspection_object(instances[0], "/")
 
415
        return cls.make_introspection_object(instances[0])
317
416
 
318
417
    def __getattr__(self, name):
319
418
        # avoid recursion if for some reason we have no state set (should never
333
432
    def get_state_by_path(cls, piece):
334
433
        """Get state for a particular piece of the state tree.
335
434
 
336
 
        *piece* is an XPath-like query that specifies which bit of the tree you
337
 
        want to look at.
 
435
        You should probably never need to call this directly.
338
436
 
339
 
        :param string piece:
 
437
        :param piece: an XPath-like query that specifies which bit of the tree you
 
438
            want to look at.
340
439
        :raises: **TypeError** on invalid *piece* parameter.
341
440
 
342
441
        """
344
443
            raise TypeError("XPath query must be a string, not %r", type(piece))
345
444
 
346
445
        with Timer("GetState %s" % piece):
347
 
            return get_introspection_iface(
348
 
                cls.DBUS_SERVICE,
349
 
                cls.DBUS_OBJECT
350
 
                ).GetState(piece)
 
446
            return cls._Backend.introspection_iface.GetState(piece)
351
447
 
352
448
    def get_new_state(self):
353
449
        """Retrieve a new state dictionary for this class instance.
354
450
 
 
451
        You should probably never need to call this directly.
 
452
 
355
453
        .. note:: The state keys in the returned dictionary are not translated.
356
454
 
357
455
        """
362
460
 
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
367
465
        else:
368
 
            return self.path_info + "[id=%d]" % self.id
 
466
            return self.path + "[id=%d]" % self.id
369
467
 
370
468
    @classmethod
371
 
    def make_introspection_object(cls, dbus_tuple, path_info=None):
372
 
        """Make an introspection object given a DBus tuple of (name, state_dict).
373
 
 
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).
376
471
 
377
472
        This only works for classes that derive from DBusIntrospectionObject.
378
473
        """
379
 
        name, state = dbus_tuple
 
474
        path, state = dbus_tuple
 
475
        name = get_classname_from_path(path)
380
476
        try:
381
 
            class_type = _object_registry[name]
 
477
            class_type = _object_registry[cls._id][name]
382
478
        except KeyError:
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.")
389
 
                    path_info = None
390
 
                else:
391
 
                    path_info += 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)
393
484
 
394
485
    @contextmanager
395
486
    def no_automatic_refreshing(self):
400
491
        >>> with instance.no_automatic_refreshing():
401
492
            # access lots of attributes.
402
493
 
 
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.
 
496
 
403
497
        """
404
498
        try:
405
499
            self.__refresh_on_attribute = False
406
500
            yield
407
501
        finally:
408
502
            self.__refresh_on_attribute = True
 
503
 
 
504
 
 
505
class _CustomEmulatorMeta(IntrospectableObjectMetaclass):
 
506
 
 
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.
 
511
            have_id = False
 
512
            for base in bases:
 
513
                if hasattr(base, '_id'):
 
514
                    have_id = True
 
515
                    break
 
516
            if not have_id:
 
517
                d['_id'] = uuid4()
 
518
        return super(_CustomEmulatorMeta, cls).__new__(cls, name, bases, d)
 
519
 
 
520
 
 
521
class CustomEmulatorBase(DBusIntrospectionObject):
 
522
 
 
523
    """This class must be used as a base class for any custom emulators defined
 
524
    within a test case.
 
525
 
 
526
    .. seealso::
 
527
        Tutorial Section :ref:`custom_emulators`
 
528
            Information on how to write custom emulators.
 
529
    """
 
530
 
 
531
    __metaclass__ = _CustomEmulatorMeta