Merge lp:~veebers/autopilot/remove_object_registry-CPO_creation_explicit into lp:autopilot
- remove_object_registry-CPO_creation_explicit
- Merge into trunk
Status: | Superseded |
---|---|
Proposed branch: | lp:~veebers/autopilot/remove_object_registry-CPO_creation_explicit |
Merge into: | lp:autopilot |
Diff against target: |
1472 lines (+568/-464) 13 files modified
autopilot/introspection/_object_registry.py (+0/-110) autopilot/introspection/_proxy_objects.py (+211/-0) autopilot/introspection/_search.py (+34/-151) autopilot/introspection/backends.py (+11/-5) autopilot/introspection/dbus.py (+77/-13) autopilot/tests/functional/test_dbus_query.py (+6/-0) autopilot/tests/functional/test_input_stack.py (+1/-1) autopilot/tests/functional/test_introspection_features.py (+56/-40) autopilot/tests/unit/test_introspection.py (+11/-8) autopilot/tests/unit/test_introspection_backends.py (+9/-7) autopilot/tests/unit/test_introspection_dbus.py (+68/-36) autopilot/tests/unit/test_introspection_search.py (+0/-93) autopilot/tests/unit/test_proxy_object.py (+84/-0) |
To merge this branch: | bzr merge lp:~veebers/autopilot/remove_object_registry-CPO_creation_explicit |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Autopilot Hackers | Pending | ||
Review via email: mp+261809@code.launchpad.net |
This proposal has been superseded by a proposal from 2015-06-16.
Commit message
Remove the idea of (and actual) the object registry
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.
- 567. By Christopher Lee
-
Initial changes for async proxy creation (autopilot vis) not stable.
- 568. By Christopher Lee
-
Fix pyflake errors
- 569. By Christopher Lee
-
Merge and fix conflicts from push branch
- 570. By Christopher Lee
-
Another pyflake fix
- 571. By Christopher Lee
-
Remove question comment, has been answered
- 572. By Christopher Lee
-
Test for error message (error message has been improved.
- 573. By Christopher Lee
-
Further tests and fixes for an issue found
- 574. By Christopher Lee
-
Pyflake fixes.
- 575. By Christopher Lee
-
Make vis work
- 576. By Christopher Lee
-
Cleanup for vis make_proxy
- 577. By Christopher Lee
-
Slight cleanup and removal of unneeded method
- 578. By Christopher Lee
-
Remove comment and clear up question from comment.
- 579. By Christopher Lee
-
Clean up docstring
- 580. By Christopher Lee
-
Fix tests due to updated code.
- 581. By Christopher Lee
-
Initial renaming from emulator_base to cpo_class
- 582. By Christopher Lee
-
Remove conversion notes
- 583. By Christopher Lee
-
Merge pre-req. changes
- 584. By Christopher Lee
-
Remove changes that accidently got in
- 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.
- 589. By Christopher Lee
-
Merge pre-req. updates.
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
1 | === modified file 'autopilot/introspection/_object_registry.py' |
2 | --- autopilot/introspection/_object_registry.py 2015-05-06 10:00:49 +0000 |
3 | +++ autopilot/introspection/_object_registry.py 2015-06-15 09:43:19 +0000 |
4 | @@ -42,10 +42,6 @@ |
5 | |
6 | """ |
7 | |
8 | -from uuid import uuid4 |
9 | - |
10 | -from autopilot.introspection._xpathselect import get_classname_from_path |
11 | -from autopilot.utilities import get_debug_logger |
12 | from contextlib import contextmanager |
13 | |
14 | _object_registry = {} |
15 | @@ -62,92 +58,6 @@ |
16 | return _proxy_extensions.get(id, ()) |
17 | |
18 | |
19 | -class IntrospectableObjectMetaclass(type): |
20 | - """Metaclass to insert appropriate classes into the object registry.""" |
21 | - |
22 | - def __new__(cls, classname, bases, classdict): |
23 | - """Create a new proxy class, possibly adding it to the object registry. |
24 | - |
25 | - Test authors may derive a class from DBusIntrospectionObject or the |
26 | - CustomEmulatorBase alias and use this as their 'emulator base'. This |
27 | - class will be given a unique '_id' attribute. That attribute is the |
28 | - first level index into the object registry. It's used so we can have |
29 | - custom proxy classes for more than one process at the same time and |
30 | - avoid clashes in the dictionary. |
31 | - """ |
32 | - cls_id = None |
33 | - |
34 | - for base in bases: |
35 | - if hasattr(base, '_id'): |
36 | - cls_id = base._id |
37 | - break |
38 | - else: |
39 | - # Ignore classes that are in the autopilot class heirarchy: |
40 | - if classname not in ( |
41 | - 'ApplicationProxyObject', |
42 | - 'CustomEmulatorBase', |
43 | - 'DBusIntrospectionObject', |
44 | - 'DBusIntrospectionObjectBase', |
45 | - ): |
46 | - # Add the '_id' attribute as a class attr: |
47 | - cls_id = classdict['_id'] = uuid4() |
48 | - |
49 | - # use the bases passed to us, but extend it with whatever is stored in |
50 | - # the proxy_extensions dictionary. |
51 | - extensions = _get_proxy_bases_for_id(cls_id) |
52 | - for extension in extensions: |
53 | - if extension not in bases: |
54 | - bases += (extension,) |
55 | - # make the object. Nothing special here. |
56 | - class_object = type.__new__(cls, classname, bases, classdict) |
57 | - |
58 | - if not classdict.get('__generated', False): |
59 | - # If the newly made object has an id, add it to the object |
60 | - # registry. |
61 | - if getattr(class_object, '_id', None) is not None: |
62 | - if class_object._id in _object_registry: |
63 | - _object_registry[class_object._id][classname] = \ |
64 | - class_object |
65 | - else: |
66 | - _object_registry[class_object._id] = \ |
67 | - {classname: class_object} |
68 | - # in all cases, return the class unchanged. |
69 | - return class_object |
70 | - |
71 | - |
72 | -DBusIntrospectionObjectBase = IntrospectableObjectMetaclass( |
73 | - 'DBusIntrospectionObjectBase', |
74 | - (object,), |
75 | - {} |
76 | -) |
77 | - |
78 | - |
79 | -def _get_proxy_object_class(object_id, path, state): |
80 | - """Return a custom proxy class, from the object registry or the default. |
81 | - |
82 | - This function first inspects the object registry using the object_id passed |
83 | - in. The object_id will be unique to all custom proxy classes for the same |
84 | - application. |
85 | - |
86 | - If that fails, we create a class on the fly based on the default class. |
87 | - |
88 | - :param object_id: The _id attribute of the class doing the lookup. This is |
89 | - used to index into the object registry to retrieve the dict of proxy |
90 | - classes to try. |
91 | - :param path: dbus path |
92 | - :param state: dbus state |
93 | - :returns: appropriate custom proxy class |
94 | - :raises ValueError: if more than one class in the dict matches |
95 | - |
96 | - """ |
97 | - class_type = _try_custom_proxy_classes(object_id, path, state) |
98 | - |
99 | - return class_type or _get_default_proxy_class( |
100 | - object_id, |
101 | - get_classname_from_path(path) |
102 | - ) |
103 | - |
104 | - |
105 | def _try_custom_proxy_classes(object_id, path, state): |
106 | """Identify which custom proxy class matches the dbus path and state. |
107 | |
108 | @@ -239,26 +149,6 @@ |
109 | return order |
110 | |
111 | |
112 | -def _get_default_proxy_class(id, name): |
113 | - """Return a custom proxy object class of the default or a base class. |
114 | - |
115 | - We want the object to inherit from the class that is set as the emulator |
116 | - base class, not the class that is doing the selecting. Using the passed id |
117 | - we retrieve the relevant bases from the object registry. |
118 | - |
119 | - :param id: The object id (_id attribute) of the class doing the lookup. |
120 | - :param name: name of new class |
121 | - :returns: custom proxy object class |
122 | - |
123 | - """ |
124 | - get_debug_logger().warning( |
125 | - "Generating introspection instance for type '%s' based on generic " |
126 | - "class.", name) |
127 | - if isinstance(name, bytes): |
128 | - name = name.decode('utf-8') |
129 | - return type(name, _get_proxy_bases_for_id(id), dict(__generated=True)) |
130 | - |
131 | - |
132 | @contextmanager |
133 | def patch_registry(new_registry): |
134 | """A utility context manager that allows us to patch the object registry. |
135 | |
136 | === added file 'autopilot/introspection/_proxy_objects.py' |
137 | --- autopilot/introspection/_proxy_objects.py 1970-01-01 00:00:00 +0000 |
138 | +++ autopilot/introspection/_proxy_objects.py 2015-06-15 09:43:19 +0000 |
139 | @@ -0,0 +1,211 @@ |
140 | +# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- |
141 | +# |
142 | +# Autopilot Functional Test Tool |
143 | +# Copyright (C) 2015 Canonical |
144 | +# |
145 | +# This program is free software: you can redistribute it and/or modify |
146 | +# it under the terms of the GNU General Public License as published by |
147 | +# the Free Software Foundation, either version 3 of the License, or |
148 | +# (at your option) any later version. |
149 | +# |
150 | +# This program is distributed in the hope that it will be useful, |
151 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
152 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
153 | +# GNU General Public License for more details. |
154 | +# |
155 | +# You should have received a copy of the GNU General Public License |
156 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
157 | +# |
158 | + |
159 | +"""Functions for dealing with proxy classes and objects.""" |
160 | + |
161 | + |
162 | +from autopilot.introspection import ( |
163 | + constants, |
164 | + dbus as ap_dbus |
165 | +) |
166 | +from autopilot.introspection._xpathselect import get_classname_from_path |
167 | +from autopilot.utilities import get_debug_logger |
168 | + |
169 | + |
170 | +def _get_introspection_xml_from_backend( |
171 | + backend, reply_handler=None, error_handler=None): |
172 | + """Get DBus Introspection xml from a backend. |
173 | + |
174 | + :param backend: The backend object to query. |
175 | + :param reply_handler: If set, makes a dbus async call, and the result will |
176 | + be sent to reply_handler. This must be a callable object. |
177 | + :param error_handler: If set, this callable will recieve any errors, and |
178 | + the call will be made asyncronously. |
179 | + :returns: A string containing introspection xml, if called synchronously. |
180 | + :raises ValueError: if one, but not both of 'reply_handler' and |
181 | + 'error_handler' are set. |
182 | + |
183 | + """ |
184 | + if callable(reply_handler) and callable(error_handler): |
185 | + backend.dbus_introspection_iface.Introspect( |
186 | + reply_handler=reply_handler, |
187 | + error_handler=error_handler, |
188 | + ) |
189 | + elif reply_handler or error_handler: |
190 | + raise ValueError( |
191 | + "Both 'reply_handler' and 'error_handler' must be set." |
192 | + ) |
193 | + else: |
194 | + return backend.dbus_introspection_iface.Introspect() |
195 | + |
196 | + |
197 | +def _get_proxy_bases_from_introspection_xml(introspection_xml): |
198 | + """Return tuple of the base classes to use when creating a proxy object. |
199 | + |
200 | + Currently this works by looking for certain interface names in the XML. In |
201 | + the future we may want to parse the XML and perform more rigerous checks. |
202 | + |
203 | + :param introspection_xml: An xml string that describes the exported object |
204 | + on the dbus backend. This determines which capabilities are present in |
205 | + the backend, and therefore which base classes should be used to create |
206 | + the proxy object. |
207 | + |
208 | + :raises RuntimeError: if the autopilot interface cannot be found. |
209 | + |
210 | + """ |
211 | + |
212 | + bases = [] |
213 | + |
214 | + if constants.AP_INTROSPECTION_IFACE not in introspection_xml: |
215 | + raise RuntimeError("Could not find Autopilot interface.") |
216 | + |
217 | + if constants.QT_AUTOPILOT_IFACE in introspection_xml: |
218 | + from autopilot.introspection.qt import QtObjectProxyMixin |
219 | + bases.append(QtObjectProxyMixin) |
220 | + |
221 | + return tuple(bases) |
222 | + |
223 | + |
224 | +def _get_proxy_object_class_name_and_state( |
225 | + backend, reply_handler=None, error_handler=None): |
226 | + """Get details about this autopilot backend via a dbus GetState call. |
227 | + |
228 | + :param reply_handler: A callable that must accept three positional |
229 | + arguments, which correspond to the return value of this function when |
230 | + called synchronously. |
231 | + |
232 | + :param error_handler: A callable which will recieve any dbus errors, should |
233 | + they occur. |
234 | + |
235 | + :raises ValueError: if one, but not both of reply_handler and error_handler |
236 | + are set. |
237 | + |
238 | + :returns: A tuple containing the class name of the root of the |
239 | + introspection tree, the full path to the root of the introspection |
240 | + tree, and the state dictionary of the root node in the introspection |
241 | + tree. |
242 | + |
243 | + """ |
244 | + if callable(reply_handler) and callable(error_handler): |
245 | + # Async call: |
246 | + # Since we get an array of state, and we only care about the first one |
247 | + # we use a lambda to unpack it and get the details we want. |
248 | + backend.introspection_iface.GetState( |
249 | + "/", |
250 | + reply_handler=lambda r: reply_handler( |
251 | + *_get_details_from_state_data(r[0]) |
252 | + ), |
253 | + error_handler=error_handler, |
254 | + ) |
255 | + elif reply_handler or error_handler: |
256 | + raise ValueError( |
257 | + "Both 'reply_handler' and 'error_handler' must be set." |
258 | + ) |
259 | + else: |
260 | + # Sync call |
261 | + state = backend.introspection_iface.GetState("/")[0] |
262 | + return _get_details_from_state_data(state) |
263 | + |
264 | + |
265 | +def _get_details_from_state_data(state_data): |
266 | + """Get details from a state data array. |
267 | + |
268 | + Returns class name, path, and state dictionary. |
269 | + """ |
270 | + object_path, object_state = state_data |
271 | + return ( |
272 | + get_classname_from_path(object_path), |
273 | + object_path.encode('utf-8'), |
274 | + object_state, |
275 | + ) |
276 | + |
277 | + |
278 | +def _get_default_proxy_class(name, bases): |
279 | + """Return a default custom proxy object class. |
280 | + |
281 | + :param name: name of new class |
282 | + :param bases: Tuple of base classes to add to the base. |
283 | + :returns: custom proxy object class |
284 | + |
285 | + """ |
286 | + get_debug_logger().warning( |
287 | + "Generating introspection instance for type '%s' based on generic " |
288 | + "class.", name) |
289 | + if isinstance(name, bytes): |
290 | + name = name.decode('utf-8') |
291 | + return type( |
292 | + name, |
293 | + (ap_dbus.DBusIntrospectionObject,) + bases, |
294 | + dict(__generated=True) |
295 | + ) |
296 | + |
297 | + |
298 | +# vvveebers this probably shouldn't be 'private'. |
299 | +def _get_proxy_object_class(cpo_or_string, path, dbus_backend): |
300 | + """Return the proxy class to use with the correct bases added |
301 | + |
302 | + If passed a CPO returns that, if passed a string contructs a default class |
303 | + to use instead. |
304 | + |
305 | + """ |
306 | + |
307 | + extension_classes = _get_extension_classes(dbus_backend) |
308 | + |
309 | + if _is_cpo_class(cpo_or_string): |
310 | + _apply_mixin_bases(cpo_or_string, extension_classes) |
311 | + return cpo_or_string |
312 | + else: |
313 | + cpo_name = _get_applicable_class_name(cpo_or_string, path) |
314 | + return _get_default_proxy_class(cpo_name, extension_classes) |
315 | + |
316 | + |
317 | +def _get_extension_classes(dbus_backend): |
318 | + """Return any extension classes that this object will need.""" |
319 | + |
320 | + intro_xml = _get_introspection_xml_from_backend(dbus_backend) |
321 | + try: |
322 | + return _get_proxy_bases_from_introspection_xml(intro_xml) |
323 | + except RuntimeError as e: |
324 | + e.args = ( |
325 | + "Could not find Autopilot interface on dbus address '%s'." |
326 | + % dbus_backend, |
327 | + ) |
328 | + |
329 | + |
330 | +def _is_cpo_class(cpo_or_string): |
331 | + return cpo_or_string is not None and not isinstance(cpo_or_string, str) |
332 | + |
333 | + |
334 | +def _get_applicable_class_name(cpo_name, path): |
335 | + """Return correct name for class. |
336 | + |
337 | + It is possible that the cpo_name is '*' which is what was used in the |
338 | + query. Determine what the actual name should be. |
339 | + |
340 | + """ |
341 | + if cpo_name is None or cpo_name == '*': |
342 | + cls_name = get_classname_from_path(path) |
343 | + return cls_name |
344 | + else: |
345 | + return cpo_name |
346 | + |
347 | + |
348 | +def _apply_mixin_bases(target_class, mixin_classes): |
349 | + to_add = tuple(set(mixin_classes).difference(target_class.__bases__)) |
350 | + target_class.__bases__ += to_add |
351 | |
352 | === modified file 'autopilot/introspection/_search.py' |
353 | --- autopilot/introspection/_search.py 2015-05-06 10:03:43 +0000 |
354 | +++ autopilot/introspection/_search.py 2015-06-15 09:43:19 +0000 |
355 | @@ -33,7 +33,7 @@ |
356 | from autopilot.introspection import backends |
357 | from autopilot.introspection import constants |
358 | from autopilot.introspection import dbus as ap_dbus |
359 | -from autopilot.introspection import _object_registry |
360 | +from autopilot.introspection import _proxy_objects as _po |
361 | from autopilot.introspection._xpathselect import get_classname_from_path |
362 | from autopilot.introspection.backends import WireProtocolVersionMismatch |
363 | from autopilot.introspection.utilities import ( |
364 | @@ -384,40 +384,22 @@ |
365 | :param emulator_base: The emulator base object (or None), as provided by |
366 | the user. |
367 | """ |
368 | + |
369 | + # vvveebers emulator_base will become proxy_class (to be explicit as the |
370 | + # term emulator has long gone.). |
371 | + |
372 | # make sure we always have an emulator base. Either use the one the user |
373 | # gave us, or make one: |
374 | - emulator_base = emulator_base or _make_default_emulator_base() |
375 | - _raise_if_base_class_not_actually_base(emulator_base) |
376 | - |
377 | - # Get the dbus introspection Xml for the backend. |
378 | - intro_xml = _get_introspection_xml_from_backend(dbus_address) |
379 | - try: |
380 | - # Figure out if the backend has any extension methods, and return |
381 | - # classes that understand how to use each of those extensions: |
382 | - extension_classes = _get_proxy_bases_from_introspection_xml(intro_xml) |
383 | - |
384 | - # Register those base classes for everything that will derive from this |
385 | - # emulator base class. |
386 | - _object_registry.register_extension_classes_for_proxy_base( |
387 | - emulator_base, |
388 | - extension_classes, |
389 | - ) |
390 | - except RuntimeError as e: |
391 | - e.args = ( |
392 | - "Could not find Autopilot interface on dbus address '%s'." |
393 | - % dbus_address, |
394 | - ) |
395 | - raise e |
396 | - |
397 | - cls_name, path, cls_state = _get_proxy_object_class_name_and_state( |
398 | + # This will return a default class or the CPO with the correct stuff added. |
399 | + |
400 | + cls_name, path, cls_state = _po._get_proxy_object_class_name_and_state( |
401 | dbus_address |
402 | ) |
403 | |
404 | - proxy_class = _object_registry._get_proxy_object_class( |
405 | - emulator_base._id, |
406 | - path, |
407 | - cls_state |
408 | + proxy_class = _po._get_proxy_object_class( |
409 | + emulator_base, path, dbus_address |
410 | ) |
411 | + |
412 | # For this object only, add the ApplicationProxy class, since it's the |
413 | # root of the tree. Ideally this would be nicer... |
414 | if ApplicationProxyObject not in proxy_class.__bases__: |
415 | @@ -426,6 +408,7 @@ |
416 | |
417 | |
418 | def _make_default_emulator_base(): |
419 | + # vvveebers this will be removed (make_proxy_async needs updated.) |
420 | """Make a default base class for all proxy classes to derive from.""" |
421 | return type("DefaultEmulatorBase", (ap_dbus.DBusIntrospectionObject,), {}) |
422 | |
423 | @@ -471,6 +454,8 @@ |
424 | reply_handler will be called with a single argument: The proxy object. |
425 | |
426 | """ |
427 | + # vvveebers emulator_base name will change to proxy_class. |
428 | + |
429 | # Note: read this function backwards! |
430 | # |
431 | # Due to the callbacks, I need to define the end of the callback chain |
432 | @@ -482,20 +467,26 @@ |
433 | def build_proxy(introspection_xml, cls_name, path, cls_state): |
434 | # Figure out if the backend has any extension methods, and return |
435 | # classes that understand how to use each of those extensions: |
436 | - extension_classes = _get_proxy_bases_from_introspection_xml( |
437 | + extension_classes = _po._get_proxy_bases_from_introspection_xml( |
438 | introspection_xml |
439 | ) |
440 | - # Register those base classes for everything that will derive from this |
441 | - # emulator base class. |
442 | - _object_registry.register_extension_classes_for_proxy_base( |
443 | - emulator_base, |
444 | - extension_classes, |
445 | - ) |
446 | - proxy_class = _object_registry._get_proxy_object_class( |
447 | - emulator_base._id, |
448 | - path, |
449 | - cls_state |
450 | - ) |
451 | + |
452 | + # XXX ERROR! |
453 | + emulator_base = None |
454 | + if emulator_base is None: |
455 | + emulator_base = cls_name |
456 | + |
457 | + if isinstance(emulator_base, str): |
458 | + cpo_name = emulator_base |
459 | + proxy_class = _po._get_default_proxy_class( |
460 | + cpo_name, |
461 | + extension_classes |
462 | + ) |
463 | + else: |
464 | + # It's actually a CPO class. |
465 | + _po._apply_mixin_bases(emulator_base, extension_classes) |
466 | + proxy_class = emulator_base |
467 | + |
468 | reply_handler( |
469 | proxy_class(cls_state, path, backends.Backend(data_source)) |
470 | ) |
471 | @@ -503,7 +494,7 @@ |
472 | # Phase 2: We recieve the introspection string, and make an asynchronous |
473 | # dbus call to get the state information for the root of this applicaiton. |
474 | def get_root_state(introspection_xml): |
475 | - _get_proxy_object_class_name_and_state( |
476 | + _po._get_proxy_object_class_name_and_state( |
477 | data_source, |
478 | reply_handler=partial(build_proxy, introspection_xml), |
479 | error_handler=error_handler, |
480 | @@ -513,121 +504,13 @@ |
481 | # from the data_source provided for us. |
482 | emulator_base = emulator_base or _make_default_emulator_base() |
483 | |
484 | - _get_introspection_xml_from_backend( |
485 | + _po._get_introspection_xml_from_backend( |
486 | data_source, |
487 | reply_handler=get_root_state, |
488 | error_handler=error_handler |
489 | ) |
490 | |
491 | |
492 | -def _get_introspection_xml_from_backend( |
493 | - backend, reply_handler=None, error_handler=None): |
494 | - """Get DBus Introspection xml from a backend. |
495 | - |
496 | - :param backend: The backend object to query. |
497 | - :param reply_handler: If set, makes a dbus async call, and the result will |
498 | - be sent to reply_handler. This must be a callable object. |
499 | - :param error_handler: If set, this callable will recieve any errors, and |
500 | - the call will be made asyncronously. |
501 | - :returns: A string containing introspection xml, if called synchronously. |
502 | - :raises ValueError: if one, but not both of 'reply_handler' and |
503 | - 'error_handler' are set. |
504 | - |
505 | - """ |
506 | - if callable(reply_handler) and callable(error_handler): |
507 | - backend.dbus_introspection_iface.Introspect( |
508 | - reply_handler=reply_handler, |
509 | - error_handler=error_handler, |
510 | - ) |
511 | - elif reply_handler or error_handler: |
512 | - raise ValueError( |
513 | - "Both 'reply_handler' and 'error_handler' must be set." |
514 | - ) |
515 | - else: |
516 | - return backend.dbus_introspection_iface.Introspect() |
517 | - |
518 | - |
519 | -def _get_proxy_object_class_name_and_state( |
520 | - backend, reply_handler=None, error_handler=None): |
521 | - """Get details about this autopilot backend via a dbus GetState call. |
522 | - |
523 | - :param reply_handler: A callable that must accept three positional |
524 | - arguments, which correspond to the return value of this function when |
525 | - called synchronously. |
526 | - |
527 | - :param error_handler: A callable which will recieve any dbus errors, should |
528 | - they occur. |
529 | - |
530 | - :raises ValueError: if one, but not both of reply_handler and error_handler |
531 | - are set. |
532 | - |
533 | - :returns: A tuple containing the class name of the root of the |
534 | - introspection tree, the full path to the root of the introspection |
535 | - tree, and the state dictionary of the root node in the introspection |
536 | - tree. |
537 | - |
538 | - """ |
539 | - if callable(reply_handler) and callable(error_handler): |
540 | - # Async call: |
541 | - # Since we get an array of state, and we only care about the first one |
542 | - # we use a lambda to unpack it and get the details we want. |
543 | - backend.introspection_iface.GetState( |
544 | - "/", |
545 | - reply_handler=lambda r: reply_handler( |
546 | - *_get_details_from_state_data(r[0]) |
547 | - ), |
548 | - error_handler=error_handler, |
549 | - ) |
550 | - elif reply_handler or error_handler: |
551 | - raise ValueError( |
552 | - "Both 'reply_handler' and 'error_handler' must be set." |
553 | - ) |
554 | - else: |
555 | - # Sync call |
556 | - state = backend.introspection_iface.GetState("/")[0] |
557 | - return _get_details_from_state_data(state) |
558 | - |
559 | - |
560 | -def _get_details_from_state_data(state_data): |
561 | - """Get details from a state data array. |
562 | - |
563 | - Returns class name, path, and state dictionary. |
564 | - """ |
565 | - object_path, object_state = state_data |
566 | - return ( |
567 | - get_classname_from_path(object_path), |
568 | - object_path.encode('utf-8'), |
569 | - object_state, |
570 | - ) |
571 | - |
572 | - |
573 | -def _get_proxy_bases_from_introspection_xml(introspection_xml): |
574 | - """Return tuple of the base classes to use when creating a proxy object. |
575 | - |
576 | - Currently this works by looking for certain interface names in the XML. In |
577 | - the future we may want to parse the XML and perform more rigerous checks. |
578 | - |
579 | - :param introspection_xml: An xml string that describes the exported object |
580 | - on the dbus backend. This determines which capabilities are present in |
581 | - the backend, and therefore which base classes should be used to create |
582 | - the proxy object. |
583 | - |
584 | - :raises RuntimeError: if the autopilot interface cannot be found. |
585 | - |
586 | - """ |
587 | - |
588 | - bases = [] |
589 | - |
590 | - if constants.AP_INTROSPECTION_IFACE not in introspection_xml: |
591 | - raise RuntimeError("Could not find Autopilot interface.") |
592 | - |
593 | - if constants.QT_AUTOPILOT_IFACE in introspection_xml: |
594 | - from autopilot.introspection.qt import QtObjectProxyMixin |
595 | - bases.append(QtObjectProxyMixin) |
596 | - |
597 | - return tuple(bases) |
598 | - |
599 | - |
600 | class ApplicationProxyObject(object): |
601 | """A class that better supports query data from an application.""" |
602 | |
603 | |
604 | === modified file 'autopilot/introspection/backends.py' |
605 | --- autopilot/introspection/backends.py 2015-06-09 15:35:08 +0000 |
606 | +++ autopilot/introspection/backends.py 2015-06-15 09:43:19 +0000 |
607 | @@ -51,7 +51,7 @@ |
608 | _pid_is_running, |
609 | _get_bus_connections_pid, |
610 | ) |
611 | -from autopilot.introspection._object_registry import _get_proxy_object_class |
612 | +from autopilot.introspection import _proxy_objects |
613 | |
614 | |
615 | _logger = logging.getLogger(__name__) |
616 | @@ -245,14 +245,14 @@ |
617 | ) |
618 | return data |
619 | |
620 | - def execute_query_get_proxy_instances(self, query, id): |
621 | + def execute_query_get_proxy_instances(self, query, cpo_class): |
622 | """Execute 'query', returning proxy instances.""" |
623 | data = self.execute_query_get_data(query) |
624 | objects = [ |
625 | make_introspection_object( |
626 | t, |
627 | type(self)(self.ipc_address), |
628 | - id, |
629 | + cpo_class, |
630 | ) |
631 | for t in data |
632 | ] |
633 | @@ -288,7 +288,7 @@ |
634 | return self.fake_ipc_return_data |
635 | |
636 | |
637 | -def make_introspection_object(dbus_tuple, backend, object_id): |
638 | +def make_introspection_object(dbus_tuple, backend, cpo_class): |
639 | """Make an introspection object given a DBus tuple of |
640 | (path, state_dict). |
641 | |
642 | @@ -303,7 +303,13 @@ |
643 | """ |
644 | path, state = dbus_tuple |
645 | path = path.encode('utf-8') |
646 | - class_object = _get_proxy_object_class(object_id, path, state) |
647 | + # import ipdb; ipdb.set_trace() |
648 | + # Path seems correct here. |
649 | + class_object = _proxy_objects._get_proxy_object_class( |
650 | + cpo_class, |
651 | + path, |
652 | + backend.ipc_address |
653 | + ) |
654 | return class_object(state, path, backend) |
655 | |
656 | |
657 | |
658 | === modified file 'autopilot/introspection/dbus.py' |
659 | --- autopilot/introspection/dbus.py 2014-07-31 04:42:08 +0000 |
660 | +++ autopilot/introspection/dbus.py 2015-06-15 09:43:19 +0000 |
661 | @@ -30,9 +30,6 @@ |
662 | import logging |
663 | |
664 | from autopilot.exceptions import StateNotFoundError |
665 | -from autopilot.introspection._object_registry import ( |
666 | - DBusIntrospectionObjectBase, |
667 | -) |
668 | from autopilot.introspection.types import create_value_instance |
669 | from autopilot.introspection.utilities import translate_state_keys |
670 | from autopilot.introspection import _xpathselect as xpathselect |
671 | @@ -42,7 +39,7 @@ |
672 | _logger = logging.getLogger(__name__) |
673 | |
674 | |
675 | -class DBusIntrospectionObject(DBusIntrospectionObjectBase): |
676 | +class DBusIntrospectionObject(object): |
677 | """A class that supports transparent data retrieval from the application |
678 | under test. |
679 | |
680 | @@ -83,11 +80,15 @@ |
681 | self.id |
682 | ) |
683 | |
684 | - def _execute_query(self, query): |
685 | + def _execute_query(self, query, cpo_class=None): |
686 | + # Default to None because it's ok to create a default CPO oh or perhaps |
687 | + # default to a default dbus object? |
688 | """Execute query object 'query' and return the result.""" |
689 | + # vvveebers this will need to be updated to take any CPO passed in. |
690 | + # as well as updating the backend code so that it can handle it. |
691 | return self._backend.execute_query_get_proxy_instances( |
692 | query, |
693 | - getattr(self, '_id', None), |
694 | + cpo_class, |
695 | ) |
696 | |
697 | def _set_properties(self, state_dict): |
698 | @@ -146,6 +147,9 @@ |
699 | Tutorial Section :ref:`custom_proxy_classes` |
700 | |
701 | """ |
702 | + # vvveebers this will need updating (docs, and select_child). |
703 | + # Will def need to pass in the CPO class. |
704 | + # (seems that this will reside in execute_query. |
705 | new_query = self._query.select_child( |
706 | get_type_name(desired_type), |
707 | kwargs |
708 | @@ -175,10 +179,13 @@ |
709 | This returns a list of all children. To return only children of a |
710 | specific type, use :meth:`get_children_by_type`. To get objects |
711 | further down the introspection tree (i.e.- nodes that may not |
712 | - necessarily be immeadiate children), use :meth:`select_single` and |
713 | + necessarily be immediate children), use :meth:`select_single` and |
714 | :meth:`select_many`. |
715 | |
716 | """ |
717 | + # vvveebers this will need a note stating that any objects returned |
718 | + # will be just defaults. |
719 | + |
720 | # Thomi: 2014-03-20: There used to be a call to 'self.refresh_state()' |
721 | # here. That's not needed, since the only thing we use is the proxy |
722 | # path, which isn't affected by the current state. |
723 | @@ -192,6 +199,8 @@ |
724 | tree). Then it returns itself. |
725 | |
726 | """ |
727 | + # vvveebers This might also be an issue as there will be no record of |
728 | + # what CPO to use. |
729 | new_query = self._query.select_parent() |
730 | return self._execute_query(new_query)[0] |
731 | |
732 | @@ -237,7 +246,7 @@ |
733 | type_name_str, |
734 | kwargs |
735 | ) |
736 | - instances = self._execute_query(new_query) |
737 | + instances = self._execute_query(new_query, type_name) |
738 | if len(instances) > 1: |
739 | raise ValueError("More than one item was returned for query") |
740 | if not instances: |
741 | @@ -351,7 +360,7 @@ |
742 | 'any type' |
743 | if type_name_str == '*' else 'type ' + type_name_str, kwargs |
744 | ) |
745 | - return self._execute_query(new_query) |
746 | + return self._execute_query(new_query, type_name) |
747 | |
748 | def refresh_state(self): |
749 | """Refreshes the object's state. |
750 | @@ -385,6 +394,11 @@ |
751 | :return: List (possibly empty) of class instances. |
752 | |
753 | """ |
754 | + # vvveebers this will need a revamp: |
755 | + # - getting cls_name like this will fail for CPOs with different |
756 | + # names. |
757 | + # - this would currently produce a default proxy as the CPO isn't |
758 | + # passed in: either pass self in (or similar) or perhaps 'from_cpo()'. |
759 | cls_name = type(self).__name__ |
760 | return self._execute_query( |
761 | xpathselect.Query.whole_tree_search(cls_name) |
762 | @@ -397,6 +411,10 @@ |
763 | introspection tree. |
764 | |
765 | """ |
766 | + # vvveebers undecided if this should take a cpo argument so the |
767 | + # returned object can be instatiated instead of a default. |
768 | + # It appears that currently in use that the expectation is that it's a |
769 | + # default. |
770 | query = xpathselect.Query.pseudo_tree_root() |
771 | return self._execute_query(query)[0] |
772 | |
773 | @@ -410,10 +428,14 @@ |
774 | if self.__refresh_on_attribute: |
775 | self.refresh_state() |
776 | return self.__state[name] |
777 | + |
778 | + exception_msg = "{}Class '{}' has no attribute '{}'.".format( |
779 | + 'Generated ' if hasattr(self.__class__, '__generated') else '', |
780 | + self.__class__.__name__, |
781 | + name |
782 | + ) |
783 | # attribute not found. |
784 | - raise AttributeError( |
785 | - "Class '%s' has no attribute '%s'." % |
786 | - (self.__class__.__name__, name)) |
787 | + raise AttributeError(exception_msg) |
788 | |
789 | def _get_new_state(self): |
790 | """Retrieve a new state dictionary for this class instance. |
791 | @@ -538,12 +560,47 @@ |
792 | :returns: Whether this class is appropriate for the dbus object |
793 | |
794 | """ |
795 | + |
796 | + # vvveebers this won't be needed with the newer design. Is there |
797 | + # perhaps a need for something along this line so CPOs are created for |
798 | + # the expected bits? |
799 | + |
800 | state_name = xpathselect.get_classname_from_path(path) |
801 | if isinstance(state_name, str): |
802 | state_name = state_name.encode('utf-8') |
803 | class_name = cls.__name__.encode('utf-8') |
804 | return state_name == class_name |
805 | |
806 | + @classmethod |
807 | + def get_type_query_name(cls): |
808 | + """Return the Type node name to use within the search query. |
809 | + |
810 | + This allows for a Custom Proxy Object to be named differently to the |
811 | + underlying node type name. |
812 | + |
813 | + For instance if you have a QML type defined in the file RedRect.qml:: |
814 | + |
815 | + import QtQuick 2.0 |
816 | + Rectangle { |
817 | + color: red; |
818 | + } |
819 | + |
820 | + You can then define a Custom Proxy Object for this type like so:: |
821 | + |
822 | + class RedRect(DBusIntrospectionObject): |
823 | + @classmethod |
824 | + def get_type_query_name(cls): |
825 | + return 'QQuickRectangle' |
826 | + |
827 | + This is due to the qml engine storing 'RedRect' as a QQuickRectangle in |
828 | + the UI tree and the xpathquery query needs a node type to query for. |
829 | + By default the query will use the class name (in this case RedRect) but |
830 | + this will not match any node type in the tree. |
831 | + |
832 | + """ |
833 | + |
834 | + return cls.__name__ |
835 | + |
836 | |
837 | # TODO - can we add a deprecation warning around this somehow? |
838 | CustomEmulatorBase = DBusIntrospectionObject |
839 | @@ -557,5 +614,12 @@ |
840 | |
841 | """ |
842 | if not isinstance(maybe_string_or_class, str): |
843 | - return maybe_string_or_class.__name__ |
844 | + return _get_class_type_name(maybe_string_or_class) |
845 | return maybe_string_or_class |
846 | + |
847 | + |
848 | +def _get_class_type_name(maybe_cpo_class): |
849 | + if hasattr(maybe_cpo_class, 'get_type_query_name'): |
850 | + return maybe_cpo_class.get_type_query_name() |
851 | + else: |
852 | + return maybe_cpo_class.__name__ |
853 | |
854 | === modified file 'autopilot/tests/functional/test_dbus_query.py' |
855 | --- autopilot/tests/functional/test_dbus_query.py 2015-06-09 15:35:08 +0000 |
856 | +++ autopilot/tests/functional/test_dbus_query.py 2015-06-15 09:43:19 +0000 |
857 | @@ -217,6 +217,12 @@ |
858 | self.assertThat(abs(end_time - start_time), GreaterThan(9)) |
859 | self.assertThat(abs(end_time - start_time), LessThan(11)) |
860 | |
861 | + def test_selecting_without_classname_sets_proxy_classname(self): |
862 | + app = self.start_fully_featured_app() |
863 | + test_object = app.select_single(windowTitle='Default Window Title') |
864 | + |
865 | + self.assertThat(test_object.__class__.__name__, Equals('QMainWindow')) |
866 | + |
867 | |
868 | @skipIf(platform.model() != "Desktop", "Only suitable on Desktop (WinMocker)") |
869 | class DbusCustomBusTests(AutopilotTestCase): |
870 | |
871 | === modified file 'autopilot/tests/functional/test_input_stack.py' |
872 | --- autopilot/tests/functional/test_input_stack.py 2015-05-08 04:32:51 +0000 |
873 | +++ autopilot/tests/functional/test_input_stack.py 2015-06-15 09:43:19 +0000 |
874 | @@ -379,7 +379,7 @@ |
875 | screen_geometry = Display.create().get_screen_geometry(0) |
876 | target_x = screen_geometry[0] + 10 |
877 | target_y = screen_geometry[1] + 10.6 |
878 | - self.device.move(target_x, target_y) |
879 | + device.move(target_x, target_y) |
880 | self.assertEqual(device.position(), (target_x, int(target_y))) |
881 | |
882 | @patch('autopilot.platform.model', new=lambda *args: "Not Desktop", ) |
883 | |
884 | === modified file 'autopilot/tests/functional/test_introspection_features.py' |
885 | --- autopilot/tests/functional/test_introspection_features.py 2015-04-30 03:34:13 +0000 |
886 | +++ autopilot/tests/functional/test_introspection_features.py 2015-06-15 09:43:19 +0000 |
887 | @@ -36,16 +36,15 @@ |
888 | StartsWith, |
889 | ) |
890 | from textwrap import dedent |
891 | -from unittest.mock import patch |
892 | from io import StringIO |
893 | |
894 | from autopilot import platform |
895 | from autopilot.matchers import Eventually |
896 | from autopilot.testcase import AutopilotTestCase |
897 | +from autopilot.tests.functional import QmlScriptRunnerMixin |
898 | from autopilot.tests.functional.fixtures import TempDesktopFile |
899 | from autopilot.introspection import CustomEmulatorBase |
900 | from autopilot.introspection import _object_registry as object_registry |
901 | -from autopilot.introspection import _search |
902 | from autopilot.introspection.qt import QtObjectProxyMixin |
903 | from autopilot.display import Display |
904 | |
905 | @@ -93,14 +92,16 @@ |
906 | return path == b'/window-mocker' |
907 | |
908 | # verify that the initial proxy object we get back is the correct type: |
909 | - app = self.start_mock_app(EmulatorBase) |
910 | + app = self.start_mock_app(WindowMockerApp) |
911 | self.assertThat(type(app), Equals(WindowMockerApp)) |
912 | |
913 | # verify that we get the correct type from get_root_instance: |
914 | - self.assertThat( |
915 | - type(app.get_root_instance()), |
916 | - Equals(WindowMockerApp) |
917 | - ) |
918 | + # vvveebers behaviour has changed, get_root_instance currently returns |
919 | + # a default cpo. |
920 | + # self.assertThat( |
921 | + # type(app.get_root_instance()), |
922 | + # Equals(WindowMockerApp) |
923 | + # ) |
924 | |
925 | def test_customised_proxy_classes_have_extension_classes(self): |
926 | class WindowMockerApp(EmulatorBase): |
927 | @@ -112,21 +113,22 @@ |
928 | self.assertThat(app.__class__.__bases__, Contains(QtObjectProxyMixin)) |
929 | |
930 | def test_customised_proxy_classes_have_multiple_extension_classes(self): |
931 | - with object_registry.patch_registry({}): |
932 | - class SecondEmulatorBase(CustomEmulatorBase): |
933 | - pass |
934 | - |
935 | - class WindowMockerApp(EmulatorBase, SecondEmulatorBase): |
936 | - @classmethod |
937 | - def validate_dbus_object(cls, path, _state): |
938 | - return path == b'/window-mocker' |
939 | - |
940 | - app = self.start_mock_app(EmulatorBase) |
941 | - self.assertThat(app.__class__.__bases__, Contains(EmulatorBase)) |
942 | - self.assertThat( |
943 | - app.__class__.__bases__, |
944 | - Contains(SecondEmulatorBase) |
945 | - ) |
946 | + # vvveebers this test isn't really testing anything anymore as it's |
947 | + # handled by python (i.e. class inheritence) |
948 | + class SecondEmulatorBase(CustomEmulatorBase): |
949 | + pass |
950 | + |
951 | + class WindowMockerApp(EmulatorBase, SecondEmulatorBase): |
952 | + @classmethod |
953 | + def validate_dbus_object(cls, path, _state): |
954 | + return path == b'/window-mocker' |
955 | + |
956 | + app = self.start_mock_app(WindowMockerApp) |
957 | + self.assertThat(app.__class__.__bases__, Contains(EmulatorBase)) |
958 | + self.assertThat( |
959 | + app.__class__.__bases__, |
960 | + Contains(SecondEmulatorBase) |
961 | + ) |
962 | |
963 | def test_handles_using_app_cpo_base_class(self): |
964 | # This test replicates an issue found in an application test suite |
965 | @@ -139,30 +141,16 @@ |
966 | |
967 | self.start_mock_app(WindowMockerApp) |
968 | |
969 | - def test_warns_when_using_incorrect_cpo_base_class(self): |
970 | - # Ensure the warning method is called when launching a proxy. |
971 | - with object_registry.patch_registry({}): |
972 | - class TestCPO(CustomEmulatorBase): |
973 | - pass |
974 | - |
975 | - class WindowMockerApp(TestCPO): |
976 | - @classmethod |
977 | - def validate_dbus_object(cls, path, _state): |
978 | - return path == b'/window-mocker' |
979 | - |
980 | - with patch.object(_search, 'logger') as p_logger: |
981 | - self.start_mock_app(WindowMockerApp) |
982 | - self.assertTrue(p_logger.warning.called) |
983 | - |
984 | - def test_can_select_custom_emulators_by_name(self): |
985 | - """Must be able to select a custom emulator type by name.""" |
986 | + def test_selecting_using_string_creates_default_cpo(self): |
987 | + """When not passing CPO must return generated default.""" |
988 | class MouseTestWidget(EmulatorBase): |
989 | pass |
990 | |
991 | app = self.start_mock_app(EmulatorBase) |
992 | test_widget = app.select_single('MouseTestWidget') |
993 | |
994 | - self.assertThat(type(test_widget), Equals(MouseTestWidget)) |
995 | + self.assertThat(type(test_widget), Not(Equals(MouseTestWidget))) |
996 | + self.assertTrue(getattr(test_widget, '__generated')) |
997 | |
998 | def test_can_select_custom_emulators_by_type(self): |
999 | """Must be able to select a custom emulator type by type.""" |
1000 | @@ -397,3 +385,31 @@ |
1001 | [e[1] for e in result2.decorated.errors] |
1002 | ) |
1003 | ) |
1004 | + |
1005 | + |
1006 | +class CustomCPOTest(AutopilotTestCase, QmlScriptRunnerMixin): |
1007 | + |
1008 | + def launch_simple_qml_script(self): |
1009 | + simple_script = dedent(""" |
1010 | + import QtQuick 2.0 |
1011 | + Rectangle { |
1012 | + objectName: "ExampleRectangle" |
1013 | + } |
1014 | + """) |
1015 | + return self.start_qml_script(simple_script) |
1016 | + |
1017 | + def test_cpo_can_be_named_different_to_underlying_type(self): |
1018 | + """A CPO with the correct name match method must be matched if the |
1019 | + class name is different to the Type name. |
1020 | + |
1021 | + """ |
1022 | + with object_registry.patch_registry({}): |
1023 | + class RandomNamedCPORectangle(CustomEmulatorBase): |
1024 | + @classmethod |
1025 | + def get_type_query_name(cls): |
1026 | + return 'QQuickRectangle' |
1027 | + |
1028 | + app = self.launch_simple_qml_script() |
1029 | + rectangle = app.select_single(RandomNamedCPORectangle) |
1030 | + |
1031 | + self.assertThat(rectangle.objectName, Equals('ExampleRectangle')) |
1032 | |
1033 | === renamed file 'autopilot/tests/unit/test_introspection_object_registry.py' => 'autopilot/tests/unit/introspection_object_registry.py' |
1034 | === modified file 'autopilot/tests/unit/test_introspection.py' |
1035 | --- autopilot/tests/unit/test_introspection.py 2014-07-31 04:42:08 +0000 |
1036 | +++ autopilot/tests/unit/test_introspection.py 2015-06-15 09:43:19 +0000 |
1037 | @@ -25,7 +25,10 @@ |
1038 | IsInstance, |
1039 | raises, |
1040 | ) |
1041 | -import autopilot.introspection._search as _s |
1042 | +from autopilot.introspection import ( |
1043 | + _proxy_objects as _po, |
1044 | + _search as _s |
1045 | +) |
1046 | from autopilot.introspection.qt import QtObjectProxyMixin |
1047 | import autopilot.introspection as _i |
1048 | |
1049 | @@ -35,21 +38,21 @@ |
1050 | fake_state_data = (String('/some/path'), dict(foo=123)) |
1051 | |
1052 | def test_returns_classname(self): |
1053 | - class_name, _, _ = _s._get_details_from_state_data( |
1054 | + class_name, _, _ = _po._get_details_from_state_data( |
1055 | self.fake_state_data |
1056 | ) |
1057 | self.assertThat(class_name, Equals('path')) |
1058 | |
1059 | def test_returns_path(self): |
1060 | - _, path, _ = _s._get_details_from_state_data(self.fake_state_data) |
1061 | + _, path, _ = _po._get_details_from_state_data(self.fake_state_data) |
1062 | self.assertThat(path, Equals(b'/some/path')) |
1063 | |
1064 | def test_returned_path_is_bytestring(self): |
1065 | - _, path, _ = _s._get_details_from_state_data(self.fake_state_data) |
1066 | + _, path, _ = _po._get_details_from_state_data(self.fake_state_data) |
1067 | self.assertThat(path, IsInstance(type(b''))) |
1068 | |
1069 | def test_returns_state_dict(self): |
1070 | - _, _, state = _s._get_details_from_state_data(self.fake_state_data) |
1071 | + _, _, state = _po._get_details_from_state_data(self.fake_state_data) |
1072 | self.assertThat(state, Equals(dict(foo=123))) |
1073 | |
1074 | |
1075 | @@ -119,13 +122,13 @@ |
1076 | |
1077 | def test_raises_RuntimeError_when_no_interface_is_found(self): |
1078 | self.assertThat( |
1079 | - lambda: _s._get_proxy_bases_from_introspection_xml(""), |
1080 | + lambda: _po._get_proxy_bases_from_introspection_xml(""), |
1081 | raises(RuntimeError("Could not find Autopilot interface.")) |
1082 | ) |
1083 | |
1084 | def test_returns_ApplicationProxyObject_claws_for_base_interface(self): |
1085 | self.assertThat( |
1086 | - _s._get_proxy_bases_from_introspection_xml( |
1087 | + _po._get_proxy_bases_from_introspection_xml( |
1088 | self.fake_data_with_ap_interface |
1089 | ), |
1090 | Equals(()) |
1091 | @@ -133,7 +136,7 @@ |
1092 | |
1093 | def test_returns_both_base_and_qt_interface(self): |
1094 | self.assertThat( |
1095 | - _s._get_proxy_bases_from_introspection_xml( |
1096 | + _po._get_proxy_bases_from_introspection_xml( |
1097 | self.fake_data_with_ap_and_qt_interfaces |
1098 | ), |
1099 | Equals((QtObjectProxyMixin,)) |
1100 | |
1101 | === modified file 'autopilot/tests/unit/test_introspection_backends.py' |
1102 | --- autopilot/tests/unit/test_introspection_backends.py 2015-06-09 15:35:08 +0000 |
1103 | +++ autopilot/tests/unit/test_introspection_backends.py 2015-06-15 09:43:19 +0000 |
1104 | @@ -23,6 +23,7 @@ |
1105 | from testtools.matchers import Equals, Not, NotEquals, IsInstance |
1106 | |
1107 | from autopilot.introspection import ( |
1108 | + _proxy_objects as _po, |
1109 | _xpathselect as xpathselect, |
1110 | backends, |
1111 | dbus, |
1112 | @@ -325,21 +326,22 @@ |
1113 | """Verify that a class has a validation method by default.""" |
1114 | self.assertTrue(callable(self.DefaultSelector.validate_dbus_object)) |
1115 | |
1116 | - @patch.object(backends, '_get_proxy_object_class') |
1117 | + @patch.object(_po, '_get_proxy_object_class') |
1118 | def test_make_introspection_object(self, gpoc): |
1119 | """Verify that make_introspection_object makes the right call.""" |
1120 | gpoc.return_value = self.DefaultSelector |
1121 | - fake_id = Mock() |
1122 | + fake_backend = Mock() |
1123 | + cpo_name = self.getUniqueString() |
1124 | + |
1125 | new_fake = backends.make_introspection_object( |
1126 | (String('/Object'), {'id': [0, 42]}), |
1127 | - None, |
1128 | - fake_id, |
1129 | + fake_backend, |
1130 | + cpo_name |
1131 | ) |
1132 | self.assertThat(new_fake, IsInstance(self.DefaultSelector)) |
1133 | gpoc.assert_called_once_with( |
1134 | - fake_id, |
1135 | - b'/Object', |
1136 | - {'id': [0, 42]} |
1137 | + cpo_name, |
1138 | + fake_backend.ipc_address |
1139 | ) |
1140 | |
1141 | def test_validate_dbus_object_matches_on_class_name(self): |
1142 | |
1143 | === modified file 'autopilot/tests/unit/test_introspection_dbus.py' |
1144 | --- autopilot/tests/unit/test_introspection_dbus.py 2014-07-22 02:30:19 +0000 |
1145 | +++ autopilot/tests/unit/test_introspection_dbus.py 2015-06-15 09:43:19 +0000 |
1146 | @@ -29,50 +29,19 @@ |
1147 | from testtools.matchers import ( |
1148 | Equals, |
1149 | Not, |
1150 | - NotEquals, |
1151 | Raises, |
1152 | raises, |
1153 | ) |
1154 | |
1155 | from autopilot.exceptions import StateNotFoundError |
1156 | -from autopilot.introspection import CustomEmulatorBase |
1157 | -from autopilot.introspection import dbus |
1158 | +from autopilot.introspection import ( |
1159 | + _proxy_objects as _po, |
1160 | + CustomEmulatorBase, |
1161 | + dbus |
1162 | +) |
1163 | from autopilot.utilities import sleep |
1164 | |
1165 | |
1166 | -class IntrospectionFeatureTests(TestCase): |
1167 | - |
1168 | - def test_custom_emulator_base_does_not_have_id(self): |
1169 | - self.assertThat(hasattr(CustomEmulatorBase, '_id'), Equals(False)) |
1170 | - |
1171 | - def test_derived_emulator_bases_do_have_id(self): |
1172 | - class MyEmulatorBase(CustomEmulatorBase): |
1173 | - pass |
1174 | - self.assertThat(hasattr(MyEmulatorBase, '_id'), Equals(True)) |
1175 | - |
1176 | - def test_derived_children_have_same_id(self): |
1177 | - class MyEmulatorBase(CustomEmulatorBase): |
1178 | - pass |
1179 | - |
1180 | - class MyEmulator(MyEmulatorBase): |
1181 | - pass |
1182 | - |
1183 | - class MyEmulator2(MyEmulatorBase): |
1184 | - pass |
1185 | - |
1186 | - self.assertThat(MyEmulatorBase._id, Equals(MyEmulator._id)) |
1187 | - self.assertThat(MyEmulatorBase._id, Equals(MyEmulator2._id)) |
1188 | - |
1189 | - def test_children_have_different_ids(self): |
1190 | - class MyEmulatorBase(CustomEmulatorBase): |
1191 | - pass |
1192 | - |
1193 | - class MyEmulatorBase2(CustomEmulatorBase): |
1194 | - pass |
1195 | - |
1196 | - self.assertThat(MyEmulatorBase._id, NotEquals(MyEmulatorBase2._id)) |
1197 | - |
1198 | - |
1199 | class DBusIntrospectionObjectTests(TestCase): |
1200 | |
1201 | def test_can_access_path_attribute(self): |
1202 | @@ -118,6 +87,49 @@ |
1203 | ), |
1204 | ) |
1205 | |
1206 | + def test_base_class_provides_correct_query_name(self): |
1207 | + self.assertThat( |
1208 | + dbus.DBusIntrospectionObject.get_type_query_name(), |
1209 | + Equals('ProxyBase') |
1210 | + ) |
1211 | + |
1212 | + def test_inherited_uses_default_get_node_name(self): |
1213 | + class TestCPO(dbus.DBusIntrospectionObject): |
1214 | + pass |
1215 | + |
1216 | + self.assertThat( |
1217 | + TestCPO.get_type_query_name(), |
1218 | + Equals('TestCPO') |
1219 | + ) |
1220 | + |
1221 | + def test_inherited_overwrites_node_name_is_correct(self): |
1222 | + class TestCPO(dbus.DBusIntrospectionObject): |
1223 | + @classmethod |
1224 | + def get_type_query_name(cls): |
1225 | + return "TestCPO" |
1226 | + self.assertThat(TestCPO.get_type_query_name(), Equals("TestCPO")) |
1227 | + |
1228 | + |
1229 | +class ProxyObjectCreationTests(TestCase): |
1230 | + def test_default_class_mentioned_in_attr_error(self): |
1231 | + name = self.getUniqueString() |
1232 | + fake_class = _po._get_default_proxy_class(name, ()) |
1233 | + fake_object = fake_class( |
1234 | + dict(id=[0, 123], path=[0, '/some/path']), |
1235 | + b'/root', |
1236 | + Mock() |
1237 | + ) |
1238 | + |
1239 | + expected_msg = "Generated Class '{}' has no attribute 'foo'.".format( |
1240 | + name |
1241 | + ) |
1242 | + |
1243 | + with fake_object.no_automatic_refreshing(): |
1244 | + self.assertThat( |
1245 | + lambda: fake_object.foo(), |
1246 | + raises(AttributeError(expected_msg)) |
1247 | + ) |
1248 | + |
1249 | |
1250 | class ProxyObjectPrintTreeTests(TestCase): |
1251 | |
1252 | @@ -222,3 +234,23 @@ |
1253 | class FooBarBaz(object): |
1254 | pass |
1255 | self.assertEqual("FooBarBaz", dbus.get_type_name(FooBarBaz)) |
1256 | + |
1257 | + def test_get_type_name_returns_classname(self): |
1258 | + class CustomCPO(dbus.DBusIntrospectionObject): |
1259 | + pass |
1260 | + |
1261 | + type_name = dbus.get_type_name(CustomEmulatorBase) |
1262 | + self.assertThat(type_name, Equals('ProxyBase')) |
1263 | + |
1264 | + def test_get_type_name_returns_custom_node_name(self): |
1265 | + class CustomCPO(dbus.DBusIntrospectionObject): |
1266 | + @classmethod |
1267 | + def get_type_query_name(cls): |
1268 | + return 'TestingCPO' |
1269 | + type_name = dbus.get_type_name(CustomCPO) |
1270 | + self.assertThat(type_name, Equals('TestingCPO')) |
1271 | + |
1272 | + def test_get_type_name_returns_classname_of_non_proxybase_classes(self): |
1273 | + class Foo(object): |
1274 | + pass |
1275 | + self.assertEqual('Foo', dbus.get_type_name(Foo)) |
1276 | |
1277 | === modified file 'autopilot/tests/unit/test_introspection_search.py' |
1278 | --- autopilot/tests/unit/test_introspection_search.py 2015-05-06 10:00:49 +0000 |
1279 | +++ autopilot/tests/unit/test_introspection_search.py 2015-06-15 09:43:19 +0000 |
1280 | @@ -35,7 +35,6 @@ |
1281 | from autopilot.utilities import sleep |
1282 | from autopilot.introspection import _search as _s |
1283 | |
1284 | -from autopilot.introspection import CustomEmulatorBase |
1285 | from autopilot.introspection.constants import AUTOPILOT_PATH |
1286 | |
1287 | |
1288 | @@ -732,95 +731,3 @@ |
1289 | _s._find_matching_connections(bus, lambda *args: True) |
1290 | |
1291 | dedupe.assert_called_once_with(["conn1"], bus) |
1292 | - |
1293 | - |
1294 | -class ActualBaseClassTests(TestCase): |
1295 | - |
1296 | - def test_dont_raise_passed_base_when_is_only_base(self): |
1297 | - class ActualBase(CustomEmulatorBase): |
1298 | - pass |
1299 | - |
1300 | - try: |
1301 | - _s._raise_if_base_class_not_actually_base(ActualBase) |
1302 | - except ValueError: |
1303 | - self.fail('Unexpected ValueError exception') |
1304 | - |
1305 | - def test_raises_if_passed_incorrect_base_class(self): |
1306 | - class ActualBase(CustomEmulatorBase): |
1307 | - pass |
1308 | - |
1309 | - class InheritedCPO(ActualBase): |
1310 | - pass |
1311 | - |
1312 | - self.assertRaises( |
1313 | - ValueError, |
1314 | - _s._raise_if_base_class_not_actually_base, |
1315 | - InheritedCPO |
1316 | - ) |
1317 | - |
1318 | - def test_raises_parent_with_simple_non_ap_multi_inheritance(self): |
1319 | - """When mixing in non-customproxy classes must return the base.""" |
1320 | - |
1321 | - class ActualBase(CustomEmulatorBase): |
1322 | - pass |
1323 | - |
1324 | - class InheritedCPO(ActualBase): |
1325 | - pass |
1326 | - |
1327 | - class TrickyOne(object): |
1328 | - pass |
1329 | - |
1330 | - class FinalForm(InheritedCPO, TrickyOne): |
1331 | - pass |
1332 | - |
1333 | - self.assertRaises( |
1334 | - ValueError, |
1335 | - _s._raise_if_base_class_not_actually_base, |
1336 | - FinalForm |
1337 | - ) |
1338 | - |
1339 | - def test_raises_parent_with_non_ap_multi_inheritance(self): |
1340 | - |
1341 | - class ActualBase(CustomEmulatorBase): |
1342 | - pass |
1343 | - |
1344 | - class InheritedCPO(ActualBase): |
1345 | - pass |
1346 | - |
1347 | - class TrickyOne(object): |
1348 | - pass |
1349 | - |
1350 | - class FinalForm(TrickyOne, InheritedCPO): |
1351 | - pass |
1352 | - |
1353 | - self.assertRaises( |
1354 | - ValueError, |
1355 | - _s._raise_if_base_class_not_actually_base, |
1356 | - FinalForm |
1357 | - ) |
1358 | - |
1359 | - def test_dont_raise_when_using_default_emulator_base(self): |
1360 | - # _make_proxy_object potentially creates a default base. |
1361 | - DefaultBase = _s._make_default_emulator_base() |
1362 | - try: |
1363 | - _s._raise_if_base_class_not_actually_base(DefaultBase) |
1364 | - except ValueError: |
1365 | - self.fail('Unexpected ValueError exception') |
1366 | - |
1367 | - def test_exception_message_contains_useful_information(self): |
1368 | - class ActualBase(CustomEmulatorBase): |
1369 | - pass |
1370 | - |
1371 | - class InheritedCPO(ActualBase): |
1372 | - pass |
1373 | - |
1374 | - try: |
1375 | - _s._raise_if_base_class_not_actually_base(InheritedCPO) |
1376 | - except ValueError as err: |
1377 | - self.assertEqual( |
1378 | - str(err), |
1379 | - _s.WRONG_CPO_CLASS_MSG.format( |
1380 | - passed=InheritedCPO, |
1381 | - actual=ActualBase |
1382 | - ) |
1383 | - ) |
1384 | |
1385 | === added file 'autopilot/tests/unit/test_proxy_object.py' |
1386 | --- autopilot/tests/unit/test_proxy_object.py 1970-01-01 00:00:00 +0000 |
1387 | +++ autopilot/tests/unit/test_proxy_object.py 2015-06-15 09:43:19 +0000 |
1388 | @@ -0,0 +1,84 @@ |
1389 | +# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- |
1390 | +# |
1391 | +# Autopilot Functional Test Tool |
1392 | +# Copyright (C) 2015 Canonical |
1393 | +# |
1394 | +# This program is free software: you can redistribute it and/or modify |
1395 | +# it under the terms of the GNU General Public License as published by |
1396 | +# the Free Software Foundation, either version 3 of the License, or |
1397 | +# (at your option) any later version. |
1398 | +# |
1399 | +# This program is distributed in the hope that it will be useful, |
1400 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
1401 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
1402 | +# GNU General Public License for more details. |
1403 | +# |
1404 | +# You should have received a copy of the GNU General Public License |
1405 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
1406 | +# |
1407 | + |
1408 | +from unittest.mock import ( |
1409 | + Mock, |
1410 | + patch |
1411 | +) |
1412 | +from testtools import TestCase |
1413 | + |
1414 | +from autopilot.introspection import _proxy_objects as _po |
1415 | + |
1416 | + |
1417 | +class GetProxyObjectClassesTests(TestCase): |
1418 | + pass |
1419 | + |
1420 | + |
1421 | +class IsCpoClassTests(TestCase): |
1422 | + |
1423 | + def test_returns_False_when_None(self): |
1424 | + self.assertFalse(_po._is_cpo_class(None)) |
1425 | + |
1426 | + def test_returns_False_when_is_string(self): |
1427 | + self.assertFalse(_po._is_cpo_class('foo')) |
1428 | + |
1429 | + def test_returns_True_when_is_class(self): |
1430 | + class Foo(): pass |
1431 | + self.assertTrue(_po._is_cpo_class(Foo)) |
1432 | + |
1433 | +class GetApplicableClassNameTests(TestCase): |
1434 | + |
1435 | + @patch.object(_po, '_get_proxy_object_class_name_and_state') |
1436 | + def test_queries_dbus_name_unknown(self, po_cs): |
1437 | + fake_dbus_backend = Mock() |
1438 | + po_cs.return_value = (None, None, None) |
1439 | + _po._get_applicable_class_name(None, fake_dbus_backend) |
1440 | + po_cs.assert_called_once_with(fake_dbus_backend) |
1441 | + |
1442 | + @patch.object(_po, '_get_proxy_object_class_name_and_state') |
1443 | + def test_returns_dbus_name_when_passed_None(self, po_cs): |
1444 | + name = self.getUniqueString() |
1445 | + po_cs.return_value = (name, None, None) |
1446 | + fake_dbus_backend = Mock() |
1447 | + |
1448 | + self.assertEqual( |
1449 | + name, |
1450 | + _po._get_applicable_class_name( |
1451 | + None, |
1452 | + fake_dbus_backend |
1453 | + ) |
1454 | + ) |
1455 | + |
1456 | + @patch.object(_po, '_get_proxy_object_class_name_and_state') |
1457 | + def test_returns_dbus_name_when_passed_star(self, po_cs): |
1458 | + name = self.getUniqueString() |
1459 | + po_cs.return_value = (name, None, None) |
1460 | + fake_dbus_backend = Mock() |
1461 | + |
1462 | + self.assertEqual( |
1463 | + name, |
1464 | + _po._get_applicable_class_name( |
1465 | + '*', |
1466 | + fake_dbus_backend |
1467 | + ) |
1468 | + ) |
1469 | + |
1470 | + def test_returns_name_of_string(self): |
1471 | + name = self.getUniqueString() |
1472 | + self.assertEqual(name, _po._get_applicable_class_name(name, None)) |