Merge lp:~veebers/autopilot/remove_object_registry-CPO_creation_explicit into lp:autopilot

Proposed by Christopher Lee
Status: Rejected
Rejected by: Christopher Lee
Proposed branch: lp:~veebers/autopilot/remove_object_registry-CPO_creation_explicit
Merge into: lp:autopilot
Prerequisite: lp:~canonical-platform-qa/autopilot/fix-cpo-having-different-name-1337004
Diff against target: 1350 lines (+450/-485)
12 files modified
autopilot/introspection/_object_registry.py (+0/-110)
autopilot/introspection/_proxy_objects.py (+212/-0)
autopilot/introspection/_search.py (+46/-169)
autopilot/introspection/backends.py (+9/-5)
autopilot/introspection/dbus.py (+27/-14)
autopilot/tests/functional/test_dbus_query.py (+6/-0)
autopilot/tests/functional/test_introspection_features.py (+20/-42)
autopilot/tests/unit/test_introspection.py (+11/-8)
autopilot/tests/unit/test_introspection_backends.py (+12/-8)
autopilot/tests/unit/test_introspection_dbus.py (+26/-36)
autopilot/tests/unit/test_introspection_search.py (+0/-93)
autopilot/tests/unit/test_proxy_object.py (+81/-0)
To merge this branch: bzr merge lp:~veebers/autopilot/remove_object_registry-CPO_creation_explicit
Reviewer Review Type Date Requested Status
platform-qa-bot continuous-integration Approve
PS Jenkins bot continuous-integration Approve
Max Brustkern (community) Approve
Review via email: mp+262048@code.launchpad.net

This proposal supersedes a proposal from 2015-06-12.

Commit message

Remove the idea of (and the actual) object registry. Use of CPO classes are now explicit and passed into the query methods.

Description of the change

Remove the idea of (and actual) the object registry. CPO usage will now be explicit and not magic.
The CPO passed to the select_* methods (and get_children...) will instantiate the passed in proxy object class.

To post a comment you must log in.
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Approve (continuous-integration)
Revision history for this message
Max Brustkern (nuclearbob) wrote :

I have some questions, mostly about things I don't understand, since I don't understand a lot of this very well.

585. By Christopher Lee

Remove un-used method

586. By Christopher Lee

Remove question comment and update docstring.

587. By Christopher Lee

Fix whitespace issue.

588. By Christopher Lee

Remove un-used test class.

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Approve (continuous-integration)
Revision history for this message
Max Brustkern (nuclearbob) wrote :

The new changes seem reasonable to me.

review: Approve
589. By Christopher Lee

Merge pre-req. updates.

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Approve (continuous-integration)
Revision history for this message
platform-qa-bot (platform-qa-bot) wrote :
review: Approve (continuous-integration)
Revision history for this message
Nicholas Skaggs (nskaggs) wrote :

Is this actually going to land?

Revision history for this message
Christopher Lee (veebers) wrote :

Hi Nicholas,

No sorry this won't be landed. I forgot to mark as rejected.

Unmerged revisions

589. By Christopher Lee

Merge pre-req. updates.

588. By Christopher Lee

Remove un-used test class.

587. By Christopher Lee

Fix whitespace issue.

586. By Christopher Lee

Remove question comment and update docstring.

585. By Christopher Lee

Remove un-used method

584. By Christopher Lee

Remove changes that accidently got in

583. By Christopher Lee

Merge pre-req. changes

582. By Christopher Lee

Remove conversion notes

581. By Christopher Lee

Initial renaming from emulator_base to cpo_class

580. By Christopher Lee

Fix tests due to updated code.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'autopilot/introspection/_object_registry.py'
--- autopilot/introspection/_object_registry.py 2015-05-06 10:00:49 +0000
+++ autopilot/introspection/_object_registry.py 2015-07-20 22:41:01 +0000
@@ -42,10 +42,6 @@
4242
43"""43"""
4444
45from uuid import uuid4
46
47from autopilot.introspection._xpathselect import get_classname_from_path
48from autopilot.utilities import get_debug_logger
49from contextlib import contextmanager45from contextlib import contextmanager
5046
51_object_registry = {}47_object_registry = {}
@@ -62,92 +58,6 @@
62 return _proxy_extensions.get(id, ())58 return _proxy_extensions.get(id, ())
6359
6460
65class IntrospectableObjectMetaclass(type):
66 """Metaclass to insert appropriate classes into the object registry."""
67
68 def __new__(cls, classname, bases, classdict):
69 """Create a new proxy class, possibly adding it to the object registry.
70
71 Test authors may derive a class from DBusIntrospectionObject or the
72 CustomEmulatorBase alias and use this as their 'emulator base'. This
73 class will be given a unique '_id' attribute. That attribute is the
74 first level index into the object registry. It's used so we can have
75 custom proxy classes for more than one process at the same time and
76 avoid clashes in the dictionary.
77 """
78 cls_id = None
79
80 for base in bases:
81 if hasattr(base, '_id'):
82 cls_id = base._id
83 break
84 else:
85 # Ignore classes that are in the autopilot class heirarchy:
86 if classname not in (
87 'ApplicationProxyObject',
88 'CustomEmulatorBase',
89 'DBusIntrospectionObject',
90 'DBusIntrospectionObjectBase',
91 ):
92 # Add the '_id' attribute as a class attr:
93 cls_id = classdict['_id'] = uuid4()
94
95 # use the bases passed to us, but extend it with whatever is stored in
96 # the proxy_extensions dictionary.
97 extensions = _get_proxy_bases_for_id(cls_id)
98 for extension in extensions:
99 if extension not in bases:
100 bases += (extension,)
101 # make the object. Nothing special here.
102 class_object = type.__new__(cls, classname, bases, classdict)
103
104 if not classdict.get('__generated', False):
105 # If the newly made object has an id, add it to the object
106 # registry.
107 if getattr(class_object, '_id', None) is not None:
108 if class_object._id in _object_registry:
109 _object_registry[class_object._id][classname] = \
110 class_object
111 else:
112 _object_registry[class_object._id] = \
113 {classname: class_object}
114 # in all cases, return the class unchanged.
115 return class_object
116
117
118DBusIntrospectionObjectBase = IntrospectableObjectMetaclass(
119 'DBusIntrospectionObjectBase',
120 (object,),
121 {}
122)
123
124
125def _get_proxy_object_class(object_id, path, state):
126 """Return a custom proxy class, from the object registry or the default.
127
128 This function first inspects the object registry using the object_id passed
129 in. The object_id will be unique to all custom proxy classes for the same
130 application.
131
132 If that fails, we create a class on the fly based on the default class.
133
134 :param object_id: The _id attribute of the class doing the lookup. This is
135 used to index into the object registry to retrieve the dict of proxy
136 classes to try.
137 :param path: dbus path
138 :param state: dbus state
139 :returns: appropriate custom proxy class
140 :raises ValueError: if more than one class in the dict matches
141
142 """
143 class_type = _try_custom_proxy_classes(object_id, path, state)
144
145 return class_type or _get_default_proxy_class(
146 object_id,
147 get_classname_from_path(path)
148 )
149
150
151def _try_custom_proxy_classes(object_id, path, state):61def _try_custom_proxy_classes(object_id, path, state):
152 """Identify which custom proxy class matches the dbus path and state.62 """Identify which custom proxy class matches the dbus path and state.
15363
@@ -239,26 +149,6 @@
239 return order149 return order
240150
241151
242def _get_default_proxy_class(id, name):
243 """Return a custom proxy object class of the default or a base class.
244
245 We want the object to inherit from the class that is set as the emulator
246 base class, not the class that is doing the selecting. Using the passed id
247 we retrieve the relevant bases from the object registry.
248
249 :param id: The object id (_id attribute) of the class doing the lookup.
250 :param name: name of new class
251 :returns: custom proxy object class
252
253 """
254 get_debug_logger().warning(
255 "Generating introspection instance for type '%s' based on generic "
256 "class.", name)
257 if isinstance(name, bytes):
258 name = name.decode('utf-8')
259 return type(name, _get_proxy_bases_for_id(id), dict(__generated=True))
260
261
262@contextmanager152@contextmanager
263def patch_registry(new_registry):153def patch_registry(new_registry):
264 """A utility context manager that allows us to patch the object registry.154 """A utility context manager that allows us to patch the object registry.
265155
=== added file 'autopilot/introspection/_proxy_objects.py'
--- autopilot/introspection/_proxy_objects.py 1970-01-01 00:00:00 +0000
+++ autopilot/introspection/_proxy_objects.py 2015-07-20 22:41:01 +0000
@@ -0,0 +1,212 @@
1# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
2#
3# Autopilot Functional Test Tool
4# Copyright (C) 2015 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
20"""Functions for dealing with proxy classes and objects."""
21
22
23from autopilot.introspection import (
24 constants,
25 dbus as ap_dbus
26)
27from autopilot.introspection._xpathselect import get_classname_from_path
28from autopilot.utilities import get_debug_logger
29
30
31def _get_introspection_xml_from_backend(
32 backend, reply_handler=None, error_handler=None):
33 """Get DBus Introspection xml from a backend.
34
35 :param backend: The backend object to query.
36 :param reply_handler: If set, makes a dbus async call, and the result will
37 be sent to reply_handler. This must be a callable object.
38 :param error_handler: If set, this callable will recieve any errors, and
39 the call will be made asyncronously.
40 :returns: A string containing introspection xml, if called synchronously.
41 :raises ValueError: if one, but not both of 'reply_handler' and
42 'error_handler' are set.
43
44 """
45 if callable(reply_handler) and callable(error_handler):
46 backend.dbus_introspection_iface.Introspect(
47 reply_handler=reply_handler,
48 error_handler=error_handler,
49 )
50 elif reply_handler or error_handler:
51 raise ValueError(
52 "Both 'reply_handler' and 'error_handler' must be set."
53 )
54 else:
55 return backend.dbus_introspection_iface.Introspect()
56
57
58def _get_proxy_bases_from_introspection_xml(introspection_xml):
59 """Return tuple of the base classes to use when creating a proxy object.
60
61 Currently this works by looking for certain interface names in the XML. In
62 the future we may want to parse the XML and perform more rigerous checks.
63
64 :param introspection_xml: An xml string that describes the exported object
65 on the dbus backend. This determines which capabilities are present in
66 the backend, and therefore which base classes should be used to create
67 the proxy object.
68
69 :raises RuntimeError: if the autopilot interface cannot be found.
70
71 """
72
73 bases = []
74
75 if constants.AP_INTROSPECTION_IFACE not in introspection_xml:
76 raise RuntimeError("Could not find Autopilot interface.")
77
78 if constants.QT_AUTOPILOT_IFACE in introspection_xml:
79 from autopilot.introspection.qt import QtObjectProxyMixin
80 bases.append(QtObjectProxyMixin)
81
82 return tuple(bases)
83
84
85def _get_proxy_object_class_name_and_state(
86 backend, reply_handler=None, error_handler=None):
87 """Get details about this autopilot backend via a dbus GetState call.
88
89 :param reply_handler: A callable that must accept three positional
90 arguments, which correspond to the return value of this function when
91 called synchronously.
92
93 :param error_handler: A callable which will recieve any dbus errors, should
94 they occur.
95
96 :raises ValueError: if one, but not both of reply_handler and error_handler
97 are set.
98
99 :returns: A tuple containing the class name of the root of the
100 introspection tree, the full path to the root of the introspection
101 tree, and the state dictionary of the root node in the introspection
102 tree.
103
104 """
105 if callable(reply_handler) and callable(error_handler):
106 # Async call:
107 # Since we get an array of state, and we only care about the first one
108 # we use a lambda to unpack it and get the details we want.
109 backend.introspection_iface.GetState(
110 "/",
111 reply_handler=lambda r: reply_handler(
112 *_get_details_from_state_data(r[0])
113 ),
114 error_handler=error_handler,
115 )
116 elif reply_handler or error_handler:
117 raise ValueError(
118 "Both 'reply_handler' and 'error_handler' must be set."
119 )
120 else:
121 # Sync call
122 state = backend.introspection_iface.GetState("/")[0]
123 return _get_details_from_state_data(state)
124
125
126def _get_details_from_state_data(state_data):
127 """Get details from a state data array.
128
129 Returns class name, path, and state dictionary.
130 """
131 object_path, object_state = state_data
132 return (
133 get_classname_from_path(object_path),
134 object_path.encode('utf-8'),
135 object_state,
136 )
137
138
139def _get_default_proxy_class(name, bases):
140 """Return a default custom proxy object class.
141
142 :param name: name of new class
143 :param bases: Tuple of base classes to add to the base.
144 :returns: custom proxy object class
145
146 """
147 get_debug_logger().warning(
148 "Generating introspection instance for type '%s' based on generic "
149 "class.", name)
150 if isinstance(name, bytes):
151 name = name.decode('utf-8')
152 return type(
153 name,
154 (ap_dbus.DBusIntrospectionObject,) + bases,
155 dict(__generated=True)
156 )
157
158
159def get_proxy_object_class(cpo_or_string, path, dbus_backend):
160 """Return the proxy class to use with the correct bases added
161
162 If passed a CPO returns that, if passed a string contructs a default class
163 to use instead.
164
165 """
166
167 extension_classes = _get_extension_classes(dbus_backend)
168
169 if _is_cpo_class(cpo_or_string):
170 _apply_mixin_bases(cpo_or_string, extension_classes)
171 return cpo_or_string
172 else:
173 cpo_name = _get_applicable_class_name(cpo_or_string, path)
174 return _get_default_proxy_class(cpo_name, extension_classes)
175
176
177def _get_extension_classes(dbus_backend):
178 """Return any extension classes that this object will need."""
179
180 intro_xml = _get_introspection_xml_from_backend(dbus_backend)
181 try:
182 return _get_proxy_bases_from_introspection_xml(intro_xml)
183 except RuntimeError as e:
184 e.args = (
185 "Could not find Autopilot interface on dbus address '%s'."
186 % dbus_backend,
187 )
188
189
190def _is_cpo_class(cpo_or_string):
191 return cpo_or_string is not None and not isinstance(cpo_or_string, str)
192
193
194def _get_applicable_class_name(cpo_name, path):
195 """Return correct name for class.
196
197 Determine what the actual name should be if cpo_name isn't a class (and
198 thus has its own name.)
199 It is possible that the cpo_name is '*' which is what was used in the
200 original query.
201
202 """
203 if cpo_name is None or cpo_name == '*':
204 cls_name = get_classname_from_path(path)
205 return cls_name.decode('utf-8')
206 else:
207 return cpo_name
208
209
210def _apply_mixin_bases(target_class, mixin_classes):
211 to_add = tuple(set(mixin_classes).difference(target_class.__bases__))
212 target_class.__bases__ += to_add
0213
=== modified file 'autopilot/introspection/_search.py'
--- autopilot/introspection/_search.py 2015-05-06 10:03:43 +0000
+++ autopilot/introspection/_search.py 2015-07-20 22:41:01 +0000
@@ -33,7 +33,7 @@
33from autopilot.introspection import backends33from autopilot.introspection import backends
34from autopilot.introspection import constants34from autopilot.introspection import constants
35from autopilot.introspection import dbus as ap_dbus35from autopilot.introspection import dbus as ap_dbus
36from autopilot.introspection import _object_registry36from autopilot.introspection import _proxy_objects as _po
37from autopilot.introspection._xpathselect import get_classname_from_path37from autopilot.introspection._xpathselect import get_classname_from_path
38from autopilot.introspection.backends import WireProtocolVersionMismatch38from autopilot.introspection.backends import WireProtocolVersionMismatch
39from autopilot.introspection.utilities import (39from autopilot.introspection.utilities import (
@@ -76,8 +76,12 @@
76 Searches the given bus (supplied by the kwarg **dbus_bus**) for an76 Searches the given bus (supplied by the kwarg **dbus_bus**) for an
77 application matching the search criteria (also supplied in kwargs, see77 application matching the search criteria (also supplied in kwargs, see
78 further down for explaination on what these can be.)78 further down for explaination on what these can be.)
79 Returns a proxy object created using the supplied custom emulator79 Returns a proxy object created using the supplied custom proxy object class
80 **emulator_base** (which defaults to None).80 **cpo_class** (which defaults to None).
81
82 Note: for backwards compatibility this method will continue to accept
83 emulator_base instead of cpo_class. Currently will issue a warning, in
84 the future this will potentially be an error.
8185
82 This function take kwargs arguments containing search parameter values to86 This function take kwargs arguments containing search parameter values to
83 use when searching for the target application.87 use when searching for the target application.
@@ -105,9 +109,9 @@
105 Must be a string containing either 'session', 'system' or the109 Must be a string containing either 'session', 'system' or the
106 custom buses name (i.e. 'unix:abstract=/tmp/dbus-IgothuMHNk').110 custom buses name (i.e. 'unix:abstract=/tmp/dbus-IgothuMHNk').
107 Defaults to 'session'111 Defaults to 'session'
108 :param emulator_base: The custom emulator to use when creating the112 :param cpo_class: The custom proxy object class to use when creating the
109 resulting proxy object.113 resulting proxy object.
110 Defaults to None114 Defaults to None resulting in a generated default object.
111115
112 **Exceptions possibly thrown by this function:**116 **Exceptions possibly thrown by this function:**
113117
@@ -154,7 +158,15 @@
154 # Pop off non-search stuff.158 # Pop off non-search stuff.
155 dbus_bus = _get_dbus_bus_from_string(kwargs.pop('dbus_bus', 'session'))159 dbus_bus = _get_dbus_bus_from_string(kwargs.pop('dbus_bus', 'session'))
156 process = kwargs.pop('process', None)160 process = kwargs.pop('process', None)
161 cpo_class = kwargs.pop('cpo_class', None)
157 emulator_base = kwargs.pop('emulator_base', None)162 emulator_base = kwargs.pop('emulator_base', None)
163 if emulator_base is not None:
164 logger.warning(
165 'The use of emulator_base is deprecated. Please use '
166 'cpo_class instead.'
167 )
168
169 cpo_class = cpo_class if cpo_class else emulator_base
158170
159 # Force default object_path171 # Force default object_path
160 kwargs['object_path'] = kwargs.get('object_path', constants.AUTOPILOT_PATH)172 kwargs['object_path'] = kwargs.get('object_path', constants.AUTOPILOT_PATH)
@@ -180,7 +192,7 @@
180 connection_name = connections[0]192 connection_name = connections[0]
181 return _make_proxy_object(193 return _make_proxy_object(
182 _get_dbus_address_object(connection_name, object_path, dbus_bus),194 _get_dbus_address_object(connection_name, object_path, dbus_bus),
183 emulator_base195 cpo_class
184 )196 )
185197
186198
@@ -377,47 +389,25 @@
377 ])389 ])
378390
379391
380def _make_proxy_object(dbus_address, emulator_base):392def _make_proxy_object(dbus_address, cpo_class):
381 """Returns a root proxy object given a DBus service name.393 """Returns a root proxy object given a DBus service name.
382394
383 :param dbus_address: The DBusAddress object we're querying.395 :param dbus_address: The DBusAddress object we're querying.
384 :param emulator_base: The emulator base object (or None), as provided by396 :param cpo_class: The Custom Proxy Object class (or None) to use, as
385 the user.397 provided by the user.
386 """398 """
387 # make sure we always have an emulator base. Either use the one the user399
388 # gave us, or make one:400 # make sure we always have an cpo class. Either use the one the user gave
389 emulator_base = emulator_base or _make_default_emulator_base()401 # us, or make one:
390 _raise_if_base_class_not_actually_base(emulator_base)402 # This will return a default class or the CPO with the correct additions
391403 # made.
392 # Get the dbus introspection Xml for the backend.404 cls_name, path, cls_state = _po._get_proxy_object_class_name_and_state(
393 intro_xml = _get_introspection_xml_from_backend(dbus_address)
394 try:
395 # Figure out if the backend has any extension methods, and return
396 # classes that understand how to use each of those extensions:
397 extension_classes = _get_proxy_bases_from_introspection_xml(intro_xml)
398
399 # Register those base classes for everything that will derive from this
400 # emulator base class.
401 _object_registry.register_extension_classes_for_proxy_base(
402 emulator_base,
403 extension_classes,
404 )
405 except RuntimeError as e:
406 e.args = (
407 "Could not find Autopilot interface on dbus address '%s'."
408 % dbus_address,
409 )
410 raise e
411
412 cls_name, path, cls_state = _get_proxy_object_class_name_and_state(
413 dbus_address405 dbus_address
414 )406 )
407 proxy_class = _po.get_proxy_object_class(
408 cpo_class, path, dbus_address
409 )
415410
416 proxy_class = _object_registry._get_proxy_object_class(
417 emulator_base._id,
418 path,
419 cls_state
420 )
421 # For this object only, add the ApplicationProxy class, since it's the411 # For this object only, add the ApplicationProxy class, since it's the
422 # root of the tree. Ideally this would be nicer...412 # root of the tree. Ideally this would be nicer...
423 if ApplicationProxyObject not in proxy_class.__bases__:413 if ApplicationProxyObject not in proxy_class.__bases__:
@@ -425,11 +415,6 @@
425 return proxy_class(cls_state, path, backends.Backend(dbus_address))415 return proxy_class(cls_state, path, backends.Backend(dbus_address))
426416
427417
428def _make_default_emulator_base():
429 """Make a default base class for all proxy classes to derive from."""
430 return type("DefaultEmulatorBase", (ap_dbus.DBusIntrospectionObject,), {})
431
432
433WRONG_CPO_CLASS_MSG = '''\418WRONG_CPO_CLASS_MSG = '''\
434base_class: {passed} does not appear to be the actual base CPO class.419base_class: {passed} does not appear to be the actual base CPO class.
435Perhaps you meant to use: {actual}.'''420Perhaps you meant to use: {actual}.'''
@@ -482,20 +467,22 @@
482 def build_proxy(introspection_xml, cls_name, path, cls_state):467 def build_proxy(introspection_xml, cls_name, path, cls_state):
483 # Figure out if the backend has any extension methods, and return468 # Figure out if the backend has any extension methods, and return
484 # classes that understand how to use each of those extensions:469 # classes that understand how to use each of those extensions:
485 extension_classes = _get_proxy_bases_from_introspection_xml(470 extension_classes = _po._get_proxy_bases_from_introspection_xml(
486 introspection_xml471 introspection_xml
487 )472 )
488 # Register those base classes for everything that will derive from this473
489 # emulator base class.474 cpo_base = _po._get_applicable_class_name(emulator_base, path)
490 _object_registry.register_extension_classes_for_proxy_base(475
491 emulator_base,476 if not _po._is_cpo_class(cpo_base):
492 extension_classes,477 proxy_class = _po._get_default_proxy_class(
493 )478 cpo_base,
494 proxy_class = _object_registry._get_proxy_object_class(479 extension_classes
495 emulator_base._id,480 )
496 path,481 else:
497 cls_state482 # It's actually a CPO class.
498 )483 _po._apply_mixin_bases(cpo_base, extension_classes)
484 proxy_class = cpo_base
485
499 reply_handler(486 reply_handler(
500 proxy_class(cls_state, path, backends.Backend(data_source))487 proxy_class(cls_state, path, backends.Backend(data_source))
501 )488 )
@@ -503,7 +490,7 @@
503 # Phase 2: We recieve the introspection string, and make an asynchronous490 # Phase 2: We recieve the introspection string, and make an asynchronous
504 # dbus call to get the state information for the root of this applicaiton.491 # dbus call to get the state information for the root of this applicaiton.
505 def get_root_state(introspection_xml):492 def get_root_state(introspection_xml):
506 _get_proxy_object_class_name_and_state(493 _po._get_proxy_object_class_name_and_state(
507 data_source,494 data_source,
508 reply_handler=partial(build_proxy, introspection_xml),495 reply_handler=partial(build_proxy, introspection_xml),
509 error_handler=error_handler,496 error_handler=error_handler,
@@ -511,123 +498,13 @@
511498
512 # Phase 1: Make an asynchronous dbus call to get the introspection xml499 # Phase 1: Make an asynchronous dbus call to get the introspection xml
513 # from the data_source provided for us.500 # from the data_source provided for us.
514 emulator_base = emulator_base or _make_default_emulator_base()501 _po._get_introspection_xml_from_backend(
515
516 _get_introspection_xml_from_backend(
517 data_source,502 data_source,
518 reply_handler=get_root_state,503 reply_handler=get_root_state,
519 error_handler=error_handler504 error_handler=error_handler
520 )505 )
521506
522507
523def _get_introspection_xml_from_backend(
524 backend, reply_handler=None, error_handler=None):
525 """Get DBus Introspection xml from a backend.
526
527 :param backend: The backend object to query.
528 :param reply_handler: If set, makes a dbus async call, and the result will
529 be sent to reply_handler. This must be a callable object.
530 :param error_handler: If set, this callable will recieve any errors, and
531 the call will be made asyncronously.
532 :returns: A string containing introspection xml, if called synchronously.
533 :raises ValueError: if one, but not both of 'reply_handler' and
534 'error_handler' are set.
535
536 """
537 if callable(reply_handler) and callable(error_handler):
538 backend.dbus_introspection_iface.Introspect(
539 reply_handler=reply_handler,
540 error_handler=error_handler,
541 )
542 elif reply_handler or error_handler:
543 raise ValueError(
544 "Both 'reply_handler' and 'error_handler' must be set."
545 )
546 else:
547 return backend.dbus_introspection_iface.Introspect()
548
549
550def _get_proxy_object_class_name_and_state(
551 backend, reply_handler=None, error_handler=None):
552 """Get details about this autopilot backend via a dbus GetState call.
553
554 :param reply_handler: A callable that must accept three positional
555 arguments, which correspond to the return value of this function when
556 called synchronously.
557
558 :param error_handler: A callable which will recieve any dbus errors, should
559 they occur.
560
561 :raises ValueError: if one, but not both of reply_handler and error_handler
562 are set.
563
564 :returns: A tuple containing the class name of the root of the
565 introspection tree, the full path to the root of the introspection
566 tree, and the state dictionary of the root node in the introspection
567 tree.
568
569 """
570 if callable(reply_handler) and callable(error_handler):
571 # Async call:
572 # Since we get an array of state, and we only care about the first one
573 # we use a lambda to unpack it and get the details we want.
574 backend.introspection_iface.GetState(
575 "/",
576 reply_handler=lambda r: reply_handler(
577 *_get_details_from_state_data(r[0])
578 ),
579 error_handler=error_handler,
580 )
581 elif reply_handler or error_handler:
582 raise ValueError(
583 "Both 'reply_handler' and 'error_handler' must be set."
584 )
585 else:
586 # Sync call
587 state = backend.introspection_iface.GetState("/")[0]
588 return _get_details_from_state_data(state)
589
590
591def _get_details_from_state_data(state_data):
592 """Get details from a state data array.
593
594 Returns class name, path, and state dictionary.
595 """
596 object_path, object_state = state_data
597 return (
598 get_classname_from_path(object_path),
599 object_path.encode('utf-8'),
600 object_state,
601 )
602
603
604def _get_proxy_bases_from_introspection_xml(introspection_xml):
605 """Return tuple of the base classes to use when creating a proxy object.
606
607 Currently this works by looking for certain interface names in the XML. In
608 the future we may want to parse the XML and perform more rigerous checks.
609
610 :param introspection_xml: An xml string that describes the exported object
611 on the dbus backend. This determines which capabilities are present in
612 the backend, and therefore which base classes should be used to create
613 the proxy object.
614
615 :raises RuntimeError: if the autopilot interface cannot be found.
616
617 """
618
619 bases = []
620
621 if constants.AP_INTROSPECTION_IFACE not in introspection_xml:
622 raise RuntimeError("Could not find Autopilot interface.")
623
624 if constants.QT_AUTOPILOT_IFACE in introspection_xml:
625 from autopilot.introspection.qt import QtObjectProxyMixin
626 bases.append(QtObjectProxyMixin)
627
628 return tuple(bases)
629
630
631class ApplicationProxyObject(object):508class ApplicationProxyObject(object):
632 """A class that better supports query data from an application."""509 """A class that better supports query data from an application."""
633510
634511
=== modified file 'autopilot/introspection/backends.py'
--- autopilot/introspection/backends.py 2015-06-09 15:35:08 +0000
+++ autopilot/introspection/backends.py 2015-07-20 22:41:01 +0000
@@ -51,7 +51,7 @@
51 _pid_is_running,51 _pid_is_running,
52 _get_bus_connections_pid,52 _get_bus_connections_pid,
53)53)
54from autopilot.introspection._object_registry import _get_proxy_object_class54from autopilot.introspection import _proxy_objects
5555
5656
57_logger = logging.getLogger(__name__)57_logger = logging.getLogger(__name__)
@@ -245,14 +245,14 @@
245 )245 )
246 return data246 return data
247247
248 def execute_query_get_proxy_instances(self, query, id):248 def execute_query_get_proxy_instances(self, query, cpo_class):
249 """Execute 'query', returning proxy instances."""249 """Execute 'query', returning proxy instances."""
250 data = self.execute_query_get_data(query)250 data = self.execute_query_get_data(query)
251 objects = [251 objects = [
252 make_introspection_object(252 make_introspection_object(
253 t,253 t,
254 type(self)(self.ipc_address),254 type(self)(self.ipc_address),
255 id,255 cpo_class,
256 )256 )
257 for t in data257 for t in data
258 ]258 ]
@@ -288,7 +288,7 @@
288 return self.fake_ipc_return_data288 return self.fake_ipc_return_data
289289
290290
291def make_introspection_object(dbus_tuple, backend, object_id):291def make_introspection_object(dbus_tuple, backend, cpo_class):
292 """Make an introspection object given a DBus tuple of292 """Make an introspection object given a DBus tuple of
293 (path, state_dict).293 (path, state_dict).
294294
@@ -303,7 +303,11 @@
303 """303 """
304 path, state = dbus_tuple304 path, state = dbus_tuple
305 path = path.encode('utf-8')305 path = path.encode('utf-8')
306 class_object = _get_proxy_object_class(object_id, path, state)306 class_object = _proxy_objects.get_proxy_object_class(
307 cpo_class,
308 path,
309 backend.ipc_address
310 )
307 return class_object(state, path, backend)311 return class_object(state, path, backend)
308312
309313
310314
=== modified file 'autopilot/introspection/dbus.py'
--- autopilot/introspection/dbus.py 2015-07-20 22:41:00 +0000
+++ autopilot/introspection/dbus.py 2015-07-20 22:41:01 +0000
@@ -30,9 +30,6 @@
30import logging30import logging
3131
32from autopilot.exceptions import StateNotFoundError32from autopilot.exceptions import StateNotFoundError
33from autopilot.introspection._object_registry import (
34 DBusIntrospectionObjectBase,
35)
36from autopilot.introspection.types import create_value_instance33from autopilot.introspection.types import create_value_instance
37from autopilot.introspection.utilities import translate_state_keys34from autopilot.introspection.utilities import translate_state_keys
38from autopilot.introspection import _xpathselect as xpathselect35from autopilot.introspection import _xpathselect as xpathselect
@@ -42,7 +39,7 @@
42_logger = logging.getLogger(__name__)39_logger = logging.getLogger(__name__)
4340
4441
45class DBusIntrospectionObject(DBusIntrospectionObjectBase):42class DBusIntrospectionObject(object):
46 """A class that supports transparent data retrieval from the application43 """A class that supports transparent data retrieval from the application
47 under test.44 under test.
4845
@@ -83,11 +80,16 @@
83 self.id80 self.id
84 )81 )
8582
86 def _execute_query(self, query):83 def _execute_query(self, query, cpo_class=None):
87 """Execute query object 'query' and return the result."""84 """Execute query object 'query' and return the result.
85
86 Returns a proxy object created from ``cpo_class``. If ``cpo_class`` is
87 None the proxy object will be a generated default.
88
89 """
88 return self._backend.execute_query_get_proxy_instances(90 return self._backend.execute_query_get_proxy_instances(
89 query,91 query,
90 getattr(self, '_id', None),92 cpo_class,
91 )93 )
9294
93 def _set_properties(self, state_dict):95 def _set_properties(self, state_dict):
@@ -151,7 +153,7 @@
151 kwargs153 kwargs
152 )154 )
153155
154 return self._execute_query(new_query)156 return self._execute_query(new_query, desired_type)
155157
156 def get_properties(self):158 def get_properties(self):
157 """Returns a dictionary of all the properties on this class.159 """Returns a dictionary of all the properties on this class.
@@ -175,9 +177,12 @@
175 This returns a list of all children. To return only children of a177 This returns a list of all children. To return only children of a
176 specific type, use :meth:`get_children_by_type`. To get objects178 specific type, use :meth:`get_children_by_type`. To get objects
177 further down the introspection tree (i.e.- nodes that may not179 further down the introspection tree (i.e.- nodes that may not
178 necessarily be immeadiate children), use :meth:`select_single` and180 necessarily be immediate children), use :meth:`select_single` and
179 :meth:`select_many`.181 :meth:`select_many`.
180182
183 Note. Any proxy objects return from this call will be generated
184 defaults and will not use any Custom Proxy Object classes.
185
181 """186 """
182 # Thomi: 2014-03-20: There used to be a call to 'self.refresh_state()'187 # Thomi: 2014-03-20: There used to be a call to 'self.refresh_state()'
183 # here. That's not needed, since the only thing we use is the proxy188 # here. That's not needed, since the only thing we use is the proxy
@@ -191,6 +196,9 @@
191 If this object has no parent (i.e.- it is the root of the introspection196 If this object has no parent (i.e.- it is the root of the introspection
192 tree). Then it returns itself.197 tree). Then it returns itself.
193198
199 Note. Any proxy object return from this call will be generated defaults
200 and will not use any Custom Proxy Object classes.
201
194 """202 """
195 new_query = self._query.select_parent()203 new_query = self._query.select_parent()
196 return self._execute_query(new_query)[0]204 return self._execute_query(new_query)[0]
@@ -237,7 +245,7 @@
237 type_name_str,245 type_name_str,
238 kwargs246 kwargs
239 )247 )
240 instances = self._execute_query(new_query)248 instances = self._execute_query(new_query, type_name)
241 if len(instances) > 1:249 if len(instances) > 1:
242 raise ValueError("More than one item was returned for query")250 raise ValueError("More than one item was returned for query")
243 if not instances:251 if not instances:
@@ -351,7 +359,7 @@
351 'any type'359 'any type'
352 if type_name_str == '*' else 'type ' + type_name_str, kwargs360 if type_name_str == '*' else 'type ' + type_name_str, kwargs
353 )361 )
354 return self._execute_query(new_query)362 return self._execute_query(new_query, type_name)
355363
356 def refresh_state(self):364 def refresh_state(self):
357 """Refreshes the object's state.365 """Refreshes the object's state.
@@ -410,10 +418,14 @@
410 if self.__refresh_on_attribute:418 if self.__refresh_on_attribute:
411 self.refresh_state()419 self.refresh_state()
412 return self.__state[name]420 return self.__state[name]
421
422 exception_msg = "{}Class '{}' has no attribute '{}'.".format(
423 'Generated ' if hasattr(self.__class__, '__generated') else '',
424 self.__class__.__name__,
425 name
426 )
413 # attribute not found.427 # attribute not found.
414 raise AttributeError(428 raise AttributeError(exception_msg)
415 "Class '%s' has no attribute '%s'." %
416 (self.__class__.__name__, name))
417429
418 def _get_new_state(self):430 def _get_new_state(self):
419 """Retrieve a new state dictionary for this class instance.431 """Retrieve a new state dictionary for this class instance.
@@ -538,6 +550,7 @@
538 :returns: Whether this class is appropriate for the dbus object550 :returns: Whether this class is appropriate for the dbus object
539551
540 """552 """
553
541 state_name = xpathselect.get_classname_from_path(path)554 state_name = xpathselect.get_classname_from_path(path)
542 if isinstance(state_name, str):555 if isinstance(state_name, str):
543 state_name = state_name.encode('utf-8')556 state_name = state_name.encode('utf-8')
544557
=== modified file 'autopilot/tests/functional/test_dbus_query.py'
--- autopilot/tests/functional/test_dbus_query.py 2015-06-09 15:35:08 +0000
+++ autopilot/tests/functional/test_dbus_query.py 2015-07-20 22:41:01 +0000
@@ -217,6 +217,12 @@
217 self.assertThat(abs(end_time - start_time), GreaterThan(9))217 self.assertThat(abs(end_time - start_time), GreaterThan(9))
218 self.assertThat(abs(end_time - start_time), LessThan(11))218 self.assertThat(abs(end_time - start_time), LessThan(11))
219219
220 def test_selecting_without_classname_sets_proxy_classname(self):
221 app = self.start_fully_featured_app()
222 test_object = app.select_single(windowTitle='Default Window Title')
223
224 self.assertThat(test_object.__class__.__name__, Equals('QMainWindow'))
225
220226
221@skipIf(platform.model() != "Desktop", "Only suitable on Desktop (WinMocker)")227@skipIf(platform.model() != "Desktop", "Only suitable on Desktop (WinMocker)")
222class DbusCustomBusTests(AutopilotTestCase):228class DbusCustomBusTests(AutopilotTestCase):
223229
=== modified file 'autopilot/tests/functional/test_introspection_features.py'
--- autopilot/tests/functional/test_introspection_features.py 2015-07-20 22:41:00 +0000
+++ autopilot/tests/functional/test_introspection_features.py 2015-07-20 22:41:01 +0000
@@ -36,7 +36,6 @@
36 StartsWith,36 StartsWith,
37)37)
38from textwrap import dedent38from textwrap import dedent
39from unittest.mock import patch
40from io import StringIO39from io import StringIO
4140
42from autopilot import platform41from autopilot import platform
@@ -46,7 +45,6 @@
46from autopilot.tests.functional.fixtures import TempDesktopFile45from autopilot.tests.functional.fixtures import TempDesktopFile
47from autopilot.introspection import CustomEmulatorBase46from autopilot.introspection import CustomEmulatorBase
48from autopilot.introspection import _object_registry as object_registry47from autopilot.introspection import _object_registry as object_registry
49from autopilot.introspection import _search
50from autopilot.introspection.qt import QtObjectProxyMixin48from autopilot.introspection.qt import QtObjectProxyMixin
51from autopilot.display import Display49from autopilot.display import Display
5250
@@ -94,15 +92,9 @@
94 return path == b'/window-mocker'92 return path == b'/window-mocker'
9593
96 # verify that the initial proxy object we get back is the correct type:94 # verify that the initial proxy object we get back is the correct type:
97 app = self.start_mock_app(EmulatorBase)95 app = self.start_mock_app(WindowMockerApp)
98 self.assertThat(type(app), Equals(WindowMockerApp))96 self.assertThat(type(app), Equals(WindowMockerApp))
9997
100 # verify that we get the correct type from get_root_instance:
101 self.assertThat(
102 type(app.get_root_instance()),
103 Equals(WindowMockerApp)
104 )
105
106 def test_customised_proxy_classes_have_extension_classes(self):98 def test_customised_proxy_classes_have_extension_classes(self):
107 class WindowMockerApp(EmulatorBase):99 class WindowMockerApp(EmulatorBase):
108 @classmethod100 @classmethod
@@ -113,21 +105,20 @@
113 self.assertThat(app.__class__.__bases__, Contains(QtObjectProxyMixin))105 self.assertThat(app.__class__.__bases__, Contains(QtObjectProxyMixin))
114106
115 def test_customised_proxy_classes_have_multiple_extension_classes(self):107 def test_customised_proxy_classes_have_multiple_extension_classes(self):
116 with object_registry.patch_registry({}):108 class SecondEmulatorBase(CustomEmulatorBase):
117 class SecondEmulatorBase(CustomEmulatorBase):109 pass
118 pass110
119111 class WindowMockerApp(EmulatorBase, SecondEmulatorBase):
120 class WindowMockerApp(EmulatorBase, SecondEmulatorBase):112 @classmethod
121 @classmethod113 def validate_dbus_object(cls, path, _state):
122 def validate_dbus_object(cls, path, _state):114 return path == b'/window-mocker'
123 return path == b'/window-mocker'115
124116 app = self.start_mock_app(WindowMockerApp)
125 app = self.start_mock_app(EmulatorBase)117 self.assertThat(app.__class__.__bases__, Contains(EmulatorBase))
126 self.assertThat(app.__class__.__bases__, Contains(EmulatorBase))118 self.assertThat(
127 self.assertThat(119 app.__class__.__bases__,
128 app.__class__.__bases__,120 Contains(SecondEmulatorBase)
129 Contains(SecondEmulatorBase)121 )
130 )
131122
132 def test_handles_using_app_cpo_base_class(self):123 def test_handles_using_app_cpo_base_class(self):
133 # This test replicates an issue found in an application test suite124 # This test replicates an issue found in an application test suite
@@ -140,30 +131,17 @@
140131
141 self.start_mock_app(WindowMockerApp)132 self.start_mock_app(WindowMockerApp)
142133
143 def test_warns_when_using_incorrect_cpo_base_class(self):134 def test_selecting_using_string_creates_default_cpo(self):
144 # Ensure the warning method is called when launching a proxy.135 """When not passing CPO must return generated default."""
145 with object_registry.patch_registry({}):136
146 class TestCPO(CustomEmulatorBase):
147 pass
148
149 class WindowMockerApp(TestCPO):
150 @classmethod
151 def validate_dbus_object(cls, path, _state):
152 return path == b'/window-mocker'
153
154 with patch.object(_search, 'logger') as p_logger:
155 self.start_mock_app(WindowMockerApp)
156 self.assertTrue(p_logger.warning.called)
157
158 def test_can_select_custom_emulators_by_name(self):
159 """Must be able to select a custom emulator type by name."""
160 class MouseTestWidget(EmulatorBase):137 class MouseTestWidget(EmulatorBase):
161 pass138 pass
162139
163 app = self.start_mock_app(EmulatorBase)140 app = self.start_mock_app(EmulatorBase)
164 test_widget = app.select_single('MouseTestWidget')141 test_widget = app.select_single('MouseTestWidget')
165142
166 self.assertThat(type(test_widget), Equals(MouseTestWidget))143 self.assertThat(type(test_widget), Not(Equals(MouseTestWidget)))
144 self.assertTrue(getattr(test_widget, '__generated'))
167145
168 def test_can_select_custom_emulators_by_type(self):146 def test_can_select_custom_emulators_by_type(self):
169 """Must be able to select a custom emulator type by type."""147 """Must be able to select a custom emulator type by type."""
170148
=== renamed file 'autopilot/tests/unit/test_introspection_object_registry.py' => 'autopilot/tests/unit/introspection_object_registry.py'
=== modified file 'autopilot/tests/unit/test_introspection.py'
--- autopilot/tests/unit/test_introspection.py 2014-07-31 04:42:08 +0000
+++ autopilot/tests/unit/test_introspection.py 2015-07-20 22:41:01 +0000
@@ -25,7 +25,10 @@
25 IsInstance,25 IsInstance,
26 raises,26 raises,
27)27)
28import autopilot.introspection._search as _s28from autopilot.introspection import (
29 _proxy_objects as _po,
30 _search as _s
31)
29from autopilot.introspection.qt import QtObjectProxyMixin32from autopilot.introspection.qt import QtObjectProxyMixin
30import autopilot.introspection as _i33import autopilot.introspection as _i
3134
@@ -35,21 +38,21 @@
35 fake_state_data = (String('/some/path'), dict(foo=123))38 fake_state_data = (String('/some/path'), dict(foo=123))
3639
37 def test_returns_classname(self):40 def test_returns_classname(self):
38 class_name, _, _ = _s._get_details_from_state_data(41 class_name, _, _ = _po._get_details_from_state_data(
39 self.fake_state_data42 self.fake_state_data
40 )43 )
41 self.assertThat(class_name, Equals('path'))44 self.assertThat(class_name, Equals('path'))
4245
43 def test_returns_path(self):46 def test_returns_path(self):
44 _, path, _ = _s._get_details_from_state_data(self.fake_state_data)47 _, path, _ = _po._get_details_from_state_data(self.fake_state_data)
45 self.assertThat(path, Equals(b'/some/path'))48 self.assertThat(path, Equals(b'/some/path'))
4649
47 def test_returned_path_is_bytestring(self):50 def test_returned_path_is_bytestring(self):
48 _, path, _ = _s._get_details_from_state_data(self.fake_state_data)51 _, path, _ = _po._get_details_from_state_data(self.fake_state_data)
49 self.assertThat(path, IsInstance(type(b'')))52 self.assertThat(path, IsInstance(type(b'')))
5053
51 def test_returns_state_dict(self):54 def test_returns_state_dict(self):
52 _, _, state = _s._get_details_from_state_data(self.fake_state_data)55 _, _, state = _po._get_details_from_state_data(self.fake_state_data)
53 self.assertThat(state, Equals(dict(foo=123)))56 self.assertThat(state, Equals(dict(foo=123)))
5457
5558
@@ -119,13 +122,13 @@
119122
120 def test_raises_RuntimeError_when_no_interface_is_found(self):123 def test_raises_RuntimeError_when_no_interface_is_found(self):
121 self.assertThat(124 self.assertThat(
122 lambda: _s._get_proxy_bases_from_introspection_xml(""),125 lambda: _po._get_proxy_bases_from_introspection_xml(""),
123 raises(RuntimeError("Could not find Autopilot interface."))126 raises(RuntimeError("Could not find Autopilot interface."))
124 )127 )
125128
126 def test_returns_ApplicationProxyObject_claws_for_base_interface(self):129 def test_returns_ApplicationProxyObject_claws_for_base_interface(self):
127 self.assertThat(130 self.assertThat(
128 _s._get_proxy_bases_from_introspection_xml(131 _po._get_proxy_bases_from_introspection_xml(
129 self.fake_data_with_ap_interface132 self.fake_data_with_ap_interface
130 ),133 ),
131 Equals(())134 Equals(())
@@ -133,7 +136,7 @@
133136
134 def test_returns_both_base_and_qt_interface(self):137 def test_returns_both_base_and_qt_interface(self):
135 self.assertThat(138 self.assertThat(
136 _s._get_proxy_bases_from_introspection_xml(139 _po._get_proxy_bases_from_introspection_xml(
137 self.fake_data_with_ap_and_qt_interfaces140 self.fake_data_with_ap_and_qt_interfaces
138 ),141 ),
139 Equals((QtObjectProxyMixin,))142 Equals((QtObjectProxyMixin,))
140143
=== modified file 'autopilot/tests/unit/test_introspection_backends.py'
--- autopilot/tests/unit/test_introspection_backends.py 2015-06-09 15:35:08 +0000
+++ autopilot/tests/unit/test_introspection_backends.py 2015-07-20 22:41:01 +0000
@@ -23,6 +23,7 @@
23from testtools.matchers import Equals, Not, NotEquals, IsInstance23from testtools.matchers import Equals, Not, NotEquals, IsInstance
2424
25from autopilot.introspection import (25from autopilot.introspection import (
26 _proxy_objects as _po,
26 _xpathselect as xpathselect,27 _xpathselect as xpathselect,
27 backends,28 backends,
28 dbus,29 dbus,
@@ -325,21 +326,24 @@
325 """Verify that a class has a validation method by default."""326 """Verify that a class has a validation method by default."""
326 self.assertTrue(callable(self.DefaultSelector.validate_dbus_object))327 self.assertTrue(callable(self.DefaultSelector.validate_dbus_object))
327328
328 @patch.object(backends, '_get_proxy_object_class')329 @patch.object(_po, 'get_proxy_object_class')
329 def test_make_introspection_object(self, gpoc):330 def test_make_introspection_object(self, gpoc):
330 """Verify that make_introspection_object makes the right call."""331 """Verify that make_introspection_object makes the right call."""
331 gpoc.return_value = self.DefaultSelector332 gpoc.return_value = self.DefaultSelector
332 fake_id = Mock()333 fake_backend = Mock()
334 cpo_name = self.getUniqueString()
335 path = String('/Object')
336
333 new_fake = backends.make_introspection_object(337 new_fake = backends.make_introspection_object(
334 (String('/Object'), {'id': [0, 42]}),338 (path, {'id': [0, 42]}),
335 None,339 fake_backend,
336 fake_id,340 cpo_name
337 )341 )
338 self.assertThat(new_fake, IsInstance(self.DefaultSelector))342 self.assertThat(new_fake, IsInstance(self.DefaultSelector))
339 gpoc.assert_called_once_with(343 gpoc.assert_called_once_with(
340 fake_id,344 cpo_name,
341 b'/Object',345 path.encode('utf-8'),
342 {'id': [0, 42]}346 fake_backend.ipc_address
343 )347 )
344348
345 def test_validate_dbus_object_matches_on_class_name(self):349 def test_validate_dbus_object_matches_on_class_name(self):
346350
=== modified file 'autopilot/tests/unit/test_introspection_dbus.py'
--- autopilot/tests/unit/test_introspection_dbus.py 2015-07-20 22:41:00 +0000
+++ autopilot/tests/unit/test_introspection_dbus.py 2015-07-20 22:41:01 +0000
@@ -29,50 +29,19 @@
29from testtools.matchers import (29from testtools.matchers import (
30 Equals,30 Equals,
31 Not,31 Not,
32 NotEquals,
33 Raises,32 Raises,
34 raises,33 raises,
35)34)
3635
37from autopilot.exceptions import StateNotFoundError36from autopilot.exceptions import StateNotFoundError
38from autopilot.introspection import CustomEmulatorBase37from autopilot.introspection import (
39from autopilot.introspection import dbus38 _proxy_objects as _po,
39 CustomEmulatorBase,
40 dbus
41)
40from autopilot.utilities import sleep42from autopilot.utilities import sleep
4143
4244
43class IntrospectionFeatureTests(TestCase):
44
45 def test_custom_emulator_base_does_not_have_id(self):
46 self.assertThat(hasattr(CustomEmulatorBase, '_id'), Equals(False))
47
48 def test_derived_emulator_bases_do_have_id(self):
49 class MyEmulatorBase(CustomEmulatorBase):
50 pass
51 self.assertThat(hasattr(MyEmulatorBase, '_id'), Equals(True))
52
53 def test_derived_children_have_same_id(self):
54 class MyEmulatorBase(CustomEmulatorBase):
55 pass
56
57 class MyEmulator(MyEmulatorBase):
58 pass
59
60 class MyEmulator2(MyEmulatorBase):
61 pass
62
63 self.assertThat(MyEmulatorBase._id, Equals(MyEmulator._id))
64 self.assertThat(MyEmulatorBase._id, Equals(MyEmulator2._id))
65
66 def test_children_have_different_ids(self):
67 class MyEmulatorBase(CustomEmulatorBase):
68 pass
69
70 class MyEmulatorBase2(CustomEmulatorBase):
71 pass
72
73 self.assertThat(MyEmulatorBase._id, NotEquals(MyEmulatorBase2._id))
74
75
76class DBusIntrospectionObjectTests(TestCase):45class DBusIntrospectionObjectTests(TestCase):
7746
78 def test_can_access_path_attribute(self):47 def test_can_access_path_attribute(self):
@@ -141,6 +110,27 @@
141 self.assertThat(TestCPO.get_type_query_name(), Equals("TestCPO"))110 self.assertThat(TestCPO.get_type_query_name(), Equals("TestCPO"))
142111
143112
113class ProxyObjectCreationTests(TestCase):
114 def test_default_class_mentioned_in_attr_error(self):
115 name = self.getUniqueString()
116 fake_class = _po._get_default_proxy_class(name, ())
117 fake_object = fake_class(
118 dict(id=[0, 123], path=[0, '/some/path']),
119 b'/root',
120 Mock()
121 )
122
123 expected_msg = "Generated Class '{}' has no attribute 'foo'.".format(
124 name
125 )
126
127 with fake_object.no_automatic_refreshing():
128 self.assertThat(
129 lambda: fake_object.foo(),
130 raises(AttributeError(expected_msg))
131 )
132
133
144class ProxyObjectPrintTreeTests(TestCase):134class ProxyObjectPrintTreeTests(TestCase):
145135
146 def _print_test_fake_object(self):136 def _print_test_fake_object(self):
147137
=== modified file 'autopilot/tests/unit/test_introspection_search.py'
--- autopilot/tests/unit/test_introspection_search.py 2015-05-06 10:00:49 +0000
+++ autopilot/tests/unit/test_introspection_search.py 2015-07-20 22:41:01 +0000
@@ -35,7 +35,6 @@
35from autopilot.utilities import sleep35from autopilot.utilities import sleep
36from autopilot.introspection import _search as _s36from autopilot.introspection import _search as _s
3737
38from autopilot.introspection import CustomEmulatorBase
39from autopilot.introspection.constants import AUTOPILOT_PATH38from autopilot.introspection.constants import AUTOPILOT_PATH
4039
4140
@@ -732,95 +731,3 @@
732 _s._find_matching_connections(bus, lambda *args: True)731 _s._find_matching_connections(bus, lambda *args: True)
733732
734 dedupe.assert_called_once_with(["conn1"], bus)733 dedupe.assert_called_once_with(["conn1"], bus)
735
736
737class ActualBaseClassTests(TestCase):
738
739 def test_dont_raise_passed_base_when_is_only_base(self):
740 class ActualBase(CustomEmulatorBase):
741 pass
742
743 try:
744 _s._raise_if_base_class_not_actually_base(ActualBase)
745 except ValueError:
746 self.fail('Unexpected ValueError exception')
747
748 def test_raises_if_passed_incorrect_base_class(self):
749 class ActualBase(CustomEmulatorBase):
750 pass
751
752 class InheritedCPO(ActualBase):
753 pass
754
755 self.assertRaises(
756 ValueError,
757 _s._raise_if_base_class_not_actually_base,
758 InheritedCPO
759 )
760
761 def test_raises_parent_with_simple_non_ap_multi_inheritance(self):
762 """When mixing in non-customproxy classes must return the base."""
763
764 class ActualBase(CustomEmulatorBase):
765 pass
766
767 class InheritedCPO(ActualBase):
768 pass
769
770 class TrickyOne(object):
771 pass
772
773 class FinalForm(InheritedCPO, TrickyOne):
774 pass
775
776 self.assertRaises(
777 ValueError,
778 _s._raise_if_base_class_not_actually_base,
779 FinalForm
780 )
781
782 def test_raises_parent_with_non_ap_multi_inheritance(self):
783
784 class ActualBase(CustomEmulatorBase):
785 pass
786
787 class InheritedCPO(ActualBase):
788 pass
789
790 class TrickyOne(object):
791 pass
792
793 class FinalForm(TrickyOne, InheritedCPO):
794 pass
795
796 self.assertRaises(
797 ValueError,
798 _s._raise_if_base_class_not_actually_base,
799 FinalForm
800 )
801
802 def test_dont_raise_when_using_default_emulator_base(self):
803 # _make_proxy_object potentially creates a default base.
804 DefaultBase = _s._make_default_emulator_base()
805 try:
806 _s._raise_if_base_class_not_actually_base(DefaultBase)
807 except ValueError:
808 self.fail('Unexpected ValueError exception')
809
810 def test_exception_message_contains_useful_information(self):
811 class ActualBase(CustomEmulatorBase):
812 pass
813
814 class InheritedCPO(ActualBase):
815 pass
816
817 try:
818 _s._raise_if_base_class_not_actually_base(InheritedCPO)
819 except ValueError as err:
820 self.assertEqual(
821 str(err),
822 _s.WRONG_CPO_CLASS_MSG.format(
823 passed=InheritedCPO,
824 actual=ActualBase
825 )
826 )
827734
=== added file 'autopilot/tests/unit/test_proxy_object.py'
--- autopilot/tests/unit/test_proxy_object.py 1970-01-01 00:00:00 +0000
+++ autopilot/tests/unit/test_proxy_object.py 2015-07-20 22:41:01 +0000
@@ -0,0 +1,81 @@
1# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
2#
3# Autopilot Functional Test Tool
4# Copyright (C) 2015 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
20from unittest.mock import (
21 Mock,
22 patch
23)
24from testtools import TestCase
25
26from autopilot.introspection import _proxy_objects as _po
27
28
29class IsCpoClassTests(TestCase):
30
31 def test_returns_False_when_None(self):
32 self.assertFalse(_po._is_cpo_class(None))
33
34 def test_returns_False_when_is_string(self):
35 self.assertFalse(_po._is_cpo_class('foo'))
36
37 def test_returns_True_when_is_class(self):
38 class Foo():
39 pass
40 self.assertTrue(_po._is_cpo_class(Foo))
41
42
43class GetApplicableClassNameTests(TestCase):
44
45 @patch.object(_po, 'get_classname_from_path')
46 def test_queries_path_when_name_unknown(self, p_gcfp):
47 path = self.getUniqueString()
48 _po._get_applicable_class_name(None, path)
49 p_gcfp.assert_called_once_with(path)
50
51 @patch.object(_po, 'get_classname_from_path')
52 def test_returns_path_name_when_passed_None(self, p_gcfp):
53 name = self.getUniqueString()
54 p_gcfp.return_value = bytes(name, 'utf-8')
55
56 self.assertEqual(name, _po._get_applicable_class_name(None, ''))
57
58 @patch.object(_po, 'get_classname_from_path')
59 def test_returns_dbus_name_when_passed_star(self, p_gcfp):
60 name = self.getUniqueString()
61 path = ''
62 p_gcfp.return_value = bytes(name, 'utf-8')
63
64 self.assertEqual(
65 name,
66 _po._get_applicable_class_name('*', path)
67 )
68
69 def test_returns_string_from_classname(self):
70 name = self.getUniqueString()
71 fake_dbus_backend = Mock()
72 with patch.object(_po, 'get_classname_from_path') as from_path:
73 from_path.return_value = bytes(name, 'utf-8')
74 self.assertEqual(
75 _po._get_applicable_class_name('*', fake_dbus_backend),
76 name
77 )
78
79 def test_returns_name_of_string(self):
80 name = self.getUniqueString()
81 self.assertEqual(name, _po._get_applicable_class_name(name, None))

Subscribers

People subscribed via source and target branches