Merge lp:~veebers/autopilot/remove_object_registry-CPO_creation_explicit into lp:autopilot
- remove_object_registry-CPO_creation_explicit
- Merge into trunk
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 |
Related bugs: |
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.
PS Jenkins bot (ps-jenkins) wrote : | # |
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.
PS Jenkins bot (ps-jenkins) wrote : | # |
PASSED: Continuous integration, rev:588
http://
Executed test runs:
SUCCESS: http://
deb: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
deb: http://
FAILURE: http://
FAILURE: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
Max Brustkern (nuclearbob) wrote : | # |
The new changes seem reasonable to me.
- 589. By Christopher Lee
-
Merge pre-req. updates.
PS Jenkins bot (ps-jenkins) wrote : | # |
PASSED: Continuous integration, rev:589
http://
Executed test runs:
SUCCESS: http://
deb: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
deb: http://
FAILURE: http://
FAILURE: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
platform-qa-bot (platform-qa-bot) wrote : | # |
PASSED: Continuous integration, rev:589
https:/
Executed test runs:
None: https:/
Click here to trigger a rebuild:
https:/
Nicholas Skaggs (nskaggs) wrote : | # |
Is this actually going to land?
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
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-07-20 22:41:01 +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-07-20 22:41:01 +0000 |
139 | @@ -0,0 +1,212 @@ |
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 | +def get_proxy_object_class(cpo_or_string, path, dbus_backend): |
299 | + """Return the proxy class to use with the correct bases added |
300 | + |
301 | + If passed a CPO returns that, if passed a string contructs a default class |
302 | + to use instead. |
303 | + |
304 | + """ |
305 | + |
306 | + extension_classes = _get_extension_classes(dbus_backend) |
307 | + |
308 | + if _is_cpo_class(cpo_or_string): |
309 | + _apply_mixin_bases(cpo_or_string, extension_classes) |
310 | + return cpo_or_string |
311 | + else: |
312 | + cpo_name = _get_applicable_class_name(cpo_or_string, path) |
313 | + return _get_default_proxy_class(cpo_name, extension_classes) |
314 | + |
315 | + |
316 | +def _get_extension_classes(dbus_backend): |
317 | + """Return any extension classes that this object will need.""" |
318 | + |
319 | + intro_xml = _get_introspection_xml_from_backend(dbus_backend) |
320 | + try: |
321 | + return _get_proxy_bases_from_introspection_xml(intro_xml) |
322 | + except RuntimeError as e: |
323 | + e.args = ( |
324 | + "Could not find Autopilot interface on dbus address '%s'." |
325 | + % dbus_backend, |
326 | + ) |
327 | + |
328 | + |
329 | +def _is_cpo_class(cpo_or_string): |
330 | + return cpo_or_string is not None and not isinstance(cpo_or_string, str) |
331 | + |
332 | + |
333 | +def _get_applicable_class_name(cpo_name, path): |
334 | + """Return correct name for class. |
335 | + |
336 | + Determine what the actual name should be if cpo_name isn't a class (and |
337 | + thus has its own name.) |
338 | + It is possible that the cpo_name is '*' which is what was used in the |
339 | + original query. |
340 | + |
341 | + """ |
342 | + if cpo_name is None or cpo_name == '*': |
343 | + cls_name = get_classname_from_path(path) |
344 | + return cls_name.decode('utf-8') |
345 | + else: |
346 | + return cpo_name |
347 | + |
348 | + |
349 | +def _apply_mixin_bases(target_class, mixin_classes): |
350 | + to_add = tuple(set(mixin_classes).difference(target_class.__bases__)) |
351 | + target_class.__bases__ += to_add |
352 | |
353 | === modified file 'autopilot/introspection/_search.py' |
354 | --- autopilot/introspection/_search.py 2015-05-06 10:03:43 +0000 |
355 | +++ autopilot/introspection/_search.py 2015-07-20 22:41:01 +0000 |
356 | @@ -33,7 +33,7 @@ |
357 | from autopilot.introspection import backends |
358 | from autopilot.introspection import constants |
359 | from autopilot.introspection import dbus as ap_dbus |
360 | -from autopilot.introspection import _object_registry |
361 | +from autopilot.introspection import _proxy_objects as _po |
362 | from autopilot.introspection._xpathselect import get_classname_from_path |
363 | from autopilot.introspection.backends import WireProtocolVersionMismatch |
364 | from autopilot.introspection.utilities import ( |
365 | @@ -76,8 +76,12 @@ |
366 | Searches the given bus (supplied by the kwarg **dbus_bus**) for an |
367 | application matching the search criteria (also supplied in kwargs, see |
368 | further down for explaination on what these can be.) |
369 | - Returns a proxy object created using the supplied custom emulator |
370 | - **emulator_base** (which defaults to None). |
371 | + Returns a proxy object created using the supplied custom proxy object class |
372 | + **cpo_class** (which defaults to None). |
373 | + |
374 | + Note: for backwards compatibility this method will continue to accept |
375 | + emulator_base instead of cpo_class. Currently will issue a warning, in |
376 | + the future this will potentially be an error. |
377 | |
378 | This function take kwargs arguments containing search parameter values to |
379 | use when searching for the target application. |
380 | @@ -105,9 +109,9 @@ |
381 | Must be a string containing either 'session', 'system' or the |
382 | custom buses name (i.e. 'unix:abstract=/tmp/dbus-IgothuMHNk'). |
383 | Defaults to 'session' |
384 | - :param emulator_base: The custom emulator to use when creating the |
385 | + :param cpo_class: The custom proxy object class to use when creating the |
386 | resulting proxy object. |
387 | - Defaults to None |
388 | + Defaults to None resulting in a generated default object. |
389 | |
390 | **Exceptions possibly thrown by this function:** |
391 | |
392 | @@ -154,7 +158,15 @@ |
393 | # Pop off non-search stuff. |
394 | dbus_bus = _get_dbus_bus_from_string(kwargs.pop('dbus_bus', 'session')) |
395 | process = kwargs.pop('process', None) |
396 | + cpo_class = kwargs.pop('cpo_class', None) |
397 | emulator_base = kwargs.pop('emulator_base', None) |
398 | + if emulator_base is not None: |
399 | + logger.warning( |
400 | + 'The use of emulator_base is deprecated. Please use ' |
401 | + 'cpo_class instead.' |
402 | + ) |
403 | + |
404 | + cpo_class = cpo_class if cpo_class else emulator_base |
405 | |
406 | # Force default object_path |
407 | kwargs['object_path'] = kwargs.get('object_path', constants.AUTOPILOT_PATH) |
408 | @@ -180,7 +192,7 @@ |
409 | connection_name = connections[0] |
410 | return _make_proxy_object( |
411 | _get_dbus_address_object(connection_name, object_path, dbus_bus), |
412 | - emulator_base |
413 | + cpo_class |
414 | ) |
415 | |
416 | |
417 | @@ -377,47 +389,25 @@ |
418 | ]) |
419 | |
420 | |
421 | -def _make_proxy_object(dbus_address, emulator_base): |
422 | +def _make_proxy_object(dbus_address, cpo_class): |
423 | """Returns a root proxy object given a DBus service name. |
424 | |
425 | :param dbus_address: The DBusAddress object we're querying. |
426 | - :param emulator_base: The emulator base object (or None), as provided by |
427 | - the user. |
428 | + :param cpo_class: The Custom Proxy Object class (or None) to use, as |
429 | + provided by the user. |
430 | """ |
431 | - # make sure we always have an emulator base. Either use the one the user |
432 | - # gave us, or make one: |
433 | - emulator_base = emulator_base or _make_default_emulator_base() |
434 | - _raise_if_base_class_not_actually_base(emulator_base) |
435 | - |
436 | - # Get the dbus introspection Xml for the backend. |
437 | - intro_xml = _get_introspection_xml_from_backend(dbus_address) |
438 | - try: |
439 | - # Figure out if the backend has any extension methods, and return |
440 | - # classes that understand how to use each of those extensions: |
441 | - extension_classes = _get_proxy_bases_from_introspection_xml(intro_xml) |
442 | - |
443 | - # Register those base classes for everything that will derive from this |
444 | - # emulator base class. |
445 | - _object_registry.register_extension_classes_for_proxy_base( |
446 | - emulator_base, |
447 | - extension_classes, |
448 | - ) |
449 | - except RuntimeError as e: |
450 | - e.args = ( |
451 | - "Could not find Autopilot interface on dbus address '%s'." |
452 | - % dbus_address, |
453 | - ) |
454 | - raise e |
455 | - |
456 | - cls_name, path, cls_state = _get_proxy_object_class_name_and_state( |
457 | + |
458 | + # make sure we always have an cpo class. Either use the one the user gave |
459 | + # us, or make one: |
460 | + # This will return a default class or the CPO with the correct additions |
461 | + # made. |
462 | + cls_name, path, cls_state = _po._get_proxy_object_class_name_and_state( |
463 | dbus_address |
464 | ) |
465 | + proxy_class = _po.get_proxy_object_class( |
466 | + cpo_class, path, dbus_address |
467 | + ) |
468 | |
469 | - proxy_class = _object_registry._get_proxy_object_class( |
470 | - emulator_base._id, |
471 | - path, |
472 | - cls_state |
473 | - ) |
474 | # For this object only, add the ApplicationProxy class, since it's the |
475 | # root of the tree. Ideally this would be nicer... |
476 | if ApplicationProxyObject not in proxy_class.__bases__: |
477 | @@ -425,11 +415,6 @@ |
478 | return proxy_class(cls_state, path, backends.Backend(dbus_address)) |
479 | |
480 | |
481 | -def _make_default_emulator_base(): |
482 | - """Make a default base class for all proxy classes to derive from.""" |
483 | - return type("DefaultEmulatorBase", (ap_dbus.DBusIntrospectionObject,), {}) |
484 | - |
485 | - |
486 | WRONG_CPO_CLASS_MSG = '''\ |
487 | base_class: {passed} does not appear to be the actual base CPO class. |
488 | Perhaps you meant to use: {actual}.''' |
489 | @@ -482,20 +467,22 @@ |
490 | def build_proxy(introspection_xml, cls_name, path, cls_state): |
491 | # Figure out if the backend has any extension methods, and return |
492 | # classes that understand how to use each of those extensions: |
493 | - extension_classes = _get_proxy_bases_from_introspection_xml( |
494 | + extension_classes = _po._get_proxy_bases_from_introspection_xml( |
495 | introspection_xml |
496 | ) |
497 | - # Register those base classes for everything that will derive from this |
498 | - # emulator base class. |
499 | - _object_registry.register_extension_classes_for_proxy_base( |
500 | - emulator_base, |
501 | - extension_classes, |
502 | - ) |
503 | - proxy_class = _object_registry._get_proxy_object_class( |
504 | - emulator_base._id, |
505 | - path, |
506 | - cls_state |
507 | - ) |
508 | + |
509 | + cpo_base = _po._get_applicable_class_name(emulator_base, path) |
510 | + |
511 | + if not _po._is_cpo_class(cpo_base): |
512 | + proxy_class = _po._get_default_proxy_class( |
513 | + cpo_base, |
514 | + extension_classes |
515 | + ) |
516 | + else: |
517 | + # It's actually a CPO class. |
518 | + _po._apply_mixin_bases(cpo_base, extension_classes) |
519 | + proxy_class = cpo_base |
520 | + |
521 | reply_handler( |
522 | proxy_class(cls_state, path, backends.Backend(data_source)) |
523 | ) |
524 | @@ -503,7 +490,7 @@ |
525 | # Phase 2: We recieve the introspection string, and make an asynchronous |
526 | # dbus call to get the state information for the root of this applicaiton. |
527 | def get_root_state(introspection_xml): |
528 | - _get_proxy_object_class_name_and_state( |
529 | + _po._get_proxy_object_class_name_and_state( |
530 | data_source, |
531 | reply_handler=partial(build_proxy, introspection_xml), |
532 | error_handler=error_handler, |
533 | @@ -511,123 +498,13 @@ |
534 | |
535 | # Phase 1: Make an asynchronous dbus call to get the introspection xml |
536 | # from the data_source provided for us. |
537 | - emulator_base = emulator_base or _make_default_emulator_base() |
538 | - |
539 | - _get_introspection_xml_from_backend( |
540 | + _po._get_introspection_xml_from_backend( |
541 | data_source, |
542 | reply_handler=get_root_state, |
543 | error_handler=error_handler |
544 | ) |
545 | |
546 | |
547 | -def _get_introspection_xml_from_backend( |
548 | - backend, reply_handler=None, error_handler=None): |
549 | - """Get DBus Introspection xml from a backend. |
550 | - |
551 | - :param backend: The backend object to query. |
552 | - :param reply_handler: If set, makes a dbus async call, and the result will |
553 | - be sent to reply_handler. This must be a callable object. |
554 | - :param error_handler: If set, this callable will recieve any errors, and |
555 | - the call will be made asyncronously. |
556 | - :returns: A string containing introspection xml, if called synchronously. |
557 | - :raises ValueError: if one, but not both of 'reply_handler' and |
558 | - 'error_handler' are set. |
559 | - |
560 | - """ |
561 | - if callable(reply_handler) and callable(error_handler): |
562 | - backend.dbus_introspection_iface.Introspect( |
563 | - reply_handler=reply_handler, |
564 | - error_handler=error_handler, |
565 | - ) |
566 | - elif reply_handler or error_handler: |
567 | - raise ValueError( |
568 | - "Both 'reply_handler' and 'error_handler' must be set." |
569 | - ) |
570 | - else: |
571 | - return backend.dbus_introspection_iface.Introspect() |
572 | - |
573 | - |
574 | -def _get_proxy_object_class_name_and_state( |
575 | - backend, reply_handler=None, error_handler=None): |
576 | - """Get details about this autopilot backend via a dbus GetState call. |
577 | - |
578 | - :param reply_handler: A callable that must accept three positional |
579 | - arguments, which correspond to the return value of this function when |
580 | - called synchronously. |
581 | - |
582 | - :param error_handler: A callable which will recieve any dbus errors, should |
583 | - they occur. |
584 | - |
585 | - :raises ValueError: if one, but not both of reply_handler and error_handler |
586 | - are set. |
587 | - |
588 | - :returns: A tuple containing the class name of the root of the |
589 | - introspection tree, the full path to the root of the introspection |
590 | - tree, and the state dictionary of the root node in the introspection |
591 | - tree. |
592 | - |
593 | - """ |
594 | - if callable(reply_handler) and callable(error_handler): |
595 | - # Async call: |
596 | - # Since we get an array of state, and we only care about the first one |
597 | - # we use a lambda to unpack it and get the details we want. |
598 | - backend.introspection_iface.GetState( |
599 | - "/", |
600 | - reply_handler=lambda r: reply_handler( |
601 | - *_get_details_from_state_data(r[0]) |
602 | - ), |
603 | - error_handler=error_handler, |
604 | - ) |
605 | - elif reply_handler or error_handler: |
606 | - raise ValueError( |
607 | - "Both 'reply_handler' and 'error_handler' must be set." |
608 | - ) |
609 | - else: |
610 | - # Sync call |
611 | - state = backend.introspection_iface.GetState("/")[0] |
612 | - return _get_details_from_state_data(state) |
613 | - |
614 | - |
615 | -def _get_details_from_state_data(state_data): |
616 | - """Get details from a state data array. |
617 | - |
618 | - Returns class name, path, and state dictionary. |
619 | - """ |
620 | - object_path, object_state = state_data |
621 | - return ( |
622 | - get_classname_from_path(object_path), |
623 | - object_path.encode('utf-8'), |
624 | - object_state, |
625 | - ) |
626 | - |
627 | - |
628 | -def _get_proxy_bases_from_introspection_xml(introspection_xml): |
629 | - """Return tuple of the base classes to use when creating a proxy object. |
630 | - |
631 | - Currently this works by looking for certain interface names in the XML. In |
632 | - the future we may want to parse the XML and perform more rigerous checks. |
633 | - |
634 | - :param introspection_xml: An xml string that describes the exported object |
635 | - on the dbus backend. This determines which capabilities are present in |
636 | - the backend, and therefore which base classes should be used to create |
637 | - the proxy object. |
638 | - |
639 | - :raises RuntimeError: if the autopilot interface cannot be found. |
640 | - |
641 | - """ |
642 | - |
643 | - bases = [] |
644 | - |
645 | - if constants.AP_INTROSPECTION_IFACE not in introspection_xml: |
646 | - raise RuntimeError("Could not find Autopilot interface.") |
647 | - |
648 | - if constants.QT_AUTOPILOT_IFACE in introspection_xml: |
649 | - from autopilot.introspection.qt import QtObjectProxyMixin |
650 | - bases.append(QtObjectProxyMixin) |
651 | - |
652 | - return tuple(bases) |
653 | - |
654 | - |
655 | class ApplicationProxyObject(object): |
656 | """A class that better supports query data from an application.""" |
657 | |
658 | |
659 | === modified file 'autopilot/introspection/backends.py' |
660 | --- autopilot/introspection/backends.py 2015-06-09 15:35:08 +0000 |
661 | +++ autopilot/introspection/backends.py 2015-07-20 22:41:01 +0000 |
662 | @@ -51,7 +51,7 @@ |
663 | _pid_is_running, |
664 | _get_bus_connections_pid, |
665 | ) |
666 | -from autopilot.introspection._object_registry import _get_proxy_object_class |
667 | +from autopilot.introspection import _proxy_objects |
668 | |
669 | |
670 | _logger = logging.getLogger(__name__) |
671 | @@ -245,14 +245,14 @@ |
672 | ) |
673 | return data |
674 | |
675 | - def execute_query_get_proxy_instances(self, query, id): |
676 | + def execute_query_get_proxy_instances(self, query, cpo_class): |
677 | """Execute 'query', returning proxy instances.""" |
678 | data = self.execute_query_get_data(query) |
679 | objects = [ |
680 | make_introspection_object( |
681 | t, |
682 | type(self)(self.ipc_address), |
683 | - id, |
684 | + cpo_class, |
685 | ) |
686 | for t in data |
687 | ] |
688 | @@ -288,7 +288,7 @@ |
689 | return self.fake_ipc_return_data |
690 | |
691 | |
692 | -def make_introspection_object(dbus_tuple, backend, object_id): |
693 | +def make_introspection_object(dbus_tuple, backend, cpo_class): |
694 | """Make an introspection object given a DBus tuple of |
695 | (path, state_dict). |
696 | |
697 | @@ -303,7 +303,11 @@ |
698 | """ |
699 | path, state = dbus_tuple |
700 | path = path.encode('utf-8') |
701 | - class_object = _get_proxy_object_class(object_id, path, state) |
702 | + class_object = _proxy_objects.get_proxy_object_class( |
703 | + cpo_class, |
704 | + path, |
705 | + backend.ipc_address |
706 | + ) |
707 | return class_object(state, path, backend) |
708 | |
709 | |
710 | |
711 | === modified file 'autopilot/introspection/dbus.py' |
712 | --- autopilot/introspection/dbus.py 2015-07-20 22:41:00 +0000 |
713 | +++ autopilot/introspection/dbus.py 2015-07-20 22:41:01 +0000 |
714 | @@ -30,9 +30,6 @@ |
715 | import logging |
716 | |
717 | from autopilot.exceptions import StateNotFoundError |
718 | -from autopilot.introspection._object_registry import ( |
719 | - DBusIntrospectionObjectBase, |
720 | -) |
721 | from autopilot.introspection.types import create_value_instance |
722 | from autopilot.introspection.utilities import translate_state_keys |
723 | from autopilot.introspection import _xpathselect as xpathselect |
724 | @@ -42,7 +39,7 @@ |
725 | _logger = logging.getLogger(__name__) |
726 | |
727 | |
728 | -class DBusIntrospectionObject(DBusIntrospectionObjectBase): |
729 | +class DBusIntrospectionObject(object): |
730 | """A class that supports transparent data retrieval from the application |
731 | under test. |
732 | |
733 | @@ -83,11 +80,16 @@ |
734 | self.id |
735 | ) |
736 | |
737 | - def _execute_query(self, query): |
738 | - """Execute query object 'query' and return the result.""" |
739 | + def _execute_query(self, query, cpo_class=None): |
740 | + """Execute query object 'query' and return the result. |
741 | + |
742 | + Returns a proxy object created from ``cpo_class``. If ``cpo_class`` is |
743 | + None the proxy object will be a generated default. |
744 | + |
745 | + """ |
746 | return self._backend.execute_query_get_proxy_instances( |
747 | query, |
748 | - getattr(self, '_id', None), |
749 | + cpo_class, |
750 | ) |
751 | |
752 | def _set_properties(self, state_dict): |
753 | @@ -151,7 +153,7 @@ |
754 | kwargs |
755 | ) |
756 | |
757 | - return self._execute_query(new_query) |
758 | + return self._execute_query(new_query, desired_type) |
759 | |
760 | def get_properties(self): |
761 | """Returns a dictionary of all the properties on this class. |
762 | @@ -175,9 +177,12 @@ |
763 | This returns a list of all children. To return only children of a |
764 | specific type, use :meth:`get_children_by_type`. To get objects |
765 | further down the introspection tree (i.e.- nodes that may not |
766 | - necessarily be immeadiate children), use :meth:`select_single` and |
767 | + necessarily be immediate children), use :meth:`select_single` and |
768 | :meth:`select_many`. |
769 | |
770 | + Note. Any proxy objects return from this call will be generated |
771 | + defaults and will not use any Custom Proxy Object classes. |
772 | + |
773 | """ |
774 | # Thomi: 2014-03-20: There used to be a call to 'self.refresh_state()' |
775 | # here. That's not needed, since the only thing we use is the proxy |
776 | @@ -191,6 +196,9 @@ |
777 | If this object has no parent (i.e.- it is the root of the introspection |
778 | tree). Then it returns itself. |
779 | |
780 | + Note. Any proxy object return from this call will be generated defaults |
781 | + and will not use any Custom Proxy Object classes. |
782 | + |
783 | """ |
784 | new_query = self._query.select_parent() |
785 | return self._execute_query(new_query)[0] |
786 | @@ -237,7 +245,7 @@ |
787 | type_name_str, |
788 | kwargs |
789 | ) |
790 | - instances = self._execute_query(new_query) |
791 | + instances = self._execute_query(new_query, type_name) |
792 | if len(instances) > 1: |
793 | raise ValueError("More than one item was returned for query") |
794 | if not instances: |
795 | @@ -351,7 +359,7 @@ |
796 | 'any type' |
797 | if type_name_str == '*' else 'type ' + type_name_str, kwargs |
798 | ) |
799 | - return self._execute_query(new_query) |
800 | + return self._execute_query(new_query, type_name) |
801 | |
802 | def refresh_state(self): |
803 | """Refreshes the object's state. |
804 | @@ -410,10 +418,14 @@ |
805 | if self.__refresh_on_attribute: |
806 | self.refresh_state() |
807 | return self.__state[name] |
808 | + |
809 | + exception_msg = "{}Class '{}' has no attribute '{}'.".format( |
810 | + 'Generated ' if hasattr(self.__class__, '__generated') else '', |
811 | + self.__class__.__name__, |
812 | + name |
813 | + ) |
814 | # attribute not found. |
815 | - raise AttributeError( |
816 | - "Class '%s' has no attribute '%s'." % |
817 | - (self.__class__.__name__, name)) |
818 | + raise AttributeError(exception_msg) |
819 | |
820 | def _get_new_state(self): |
821 | """Retrieve a new state dictionary for this class instance. |
822 | @@ -538,6 +550,7 @@ |
823 | :returns: Whether this class is appropriate for the dbus object |
824 | |
825 | """ |
826 | + |
827 | state_name = xpathselect.get_classname_from_path(path) |
828 | if isinstance(state_name, str): |
829 | state_name = state_name.encode('utf-8') |
830 | |
831 | === modified file 'autopilot/tests/functional/test_dbus_query.py' |
832 | --- autopilot/tests/functional/test_dbus_query.py 2015-06-09 15:35:08 +0000 |
833 | +++ autopilot/tests/functional/test_dbus_query.py 2015-07-20 22:41:01 +0000 |
834 | @@ -217,6 +217,12 @@ |
835 | self.assertThat(abs(end_time - start_time), GreaterThan(9)) |
836 | self.assertThat(abs(end_time - start_time), LessThan(11)) |
837 | |
838 | + def test_selecting_without_classname_sets_proxy_classname(self): |
839 | + app = self.start_fully_featured_app() |
840 | + test_object = app.select_single(windowTitle='Default Window Title') |
841 | + |
842 | + self.assertThat(test_object.__class__.__name__, Equals('QMainWindow')) |
843 | + |
844 | |
845 | @skipIf(platform.model() != "Desktop", "Only suitable on Desktop (WinMocker)") |
846 | class DbusCustomBusTests(AutopilotTestCase): |
847 | |
848 | === modified file 'autopilot/tests/functional/test_introspection_features.py' |
849 | --- autopilot/tests/functional/test_introspection_features.py 2015-07-20 22:41:00 +0000 |
850 | +++ autopilot/tests/functional/test_introspection_features.py 2015-07-20 22:41:01 +0000 |
851 | @@ -36,7 +36,6 @@ |
852 | StartsWith, |
853 | ) |
854 | from textwrap import dedent |
855 | -from unittest.mock import patch |
856 | from io import StringIO |
857 | |
858 | from autopilot import platform |
859 | @@ -46,7 +45,6 @@ |
860 | from autopilot.tests.functional.fixtures import TempDesktopFile |
861 | from autopilot.introspection import CustomEmulatorBase |
862 | from autopilot.introspection import _object_registry as object_registry |
863 | -from autopilot.introspection import _search |
864 | from autopilot.introspection.qt import QtObjectProxyMixin |
865 | from autopilot.display import Display |
866 | |
867 | @@ -94,15 +92,9 @@ |
868 | return path == b'/window-mocker' |
869 | |
870 | # verify that the initial proxy object we get back is the correct type: |
871 | - app = self.start_mock_app(EmulatorBase) |
872 | + app = self.start_mock_app(WindowMockerApp) |
873 | self.assertThat(type(app), Equals(WindowMockerApp)) |
874 | |
875 | - # verify that we get the correct type from get_root_instance: |
876 | - self.assertThat( |
877 | - type(app.get_root_instance()), |
878 | - Equals(WindowMockerApp) |
879 | - ) |
880 | - |
881 | def test_customised_proxy_classes_have_extension_classes(self): |
882 | class WindowMockerApp(EmulatorBase): |
883 | @classmethod |
884 | @@ -113,21 +105,20 @@ |
885 | self.assertThat(app.__class__.__bases__, Contains(QtObjectProxyMixin)) |
886 | |
887 | def test_customised_proxy_classes_have_multiple_extension_classes(self): |
888 | - with object_registry.patch_registry({}): |
889 | - class SecondEmulatorBase(CustomEmulatorBase): |
890 | - pass |
891 | - |
892 | - class WindowMockerApp(EmulatorBase, SecondEmulatorBase): |
893 | - @classmethod |
894 | - def validate_dbus_object(cls, path, _state): |
895 | - return path == b'/window-mocker' |
896 | - |
897 | - app = self.start_mock_app(EmulatorBase) |
898 | - self.assertThat(app.__class__.__bases__, Contains(EmulatorBase)) |
899 | - self.assertThat( |
900 | - app.__class__.__bases__, |
901 | - Contains(SecondEmulatorBase) |
902 | - ) |
903 | + class SecondEmulatorBase(CustomEmulatorBase): |
904 | + pass |
905 | + |
906 | + class WindowMockerApp(EmulatorBase, SecondEmulatorBase): |
907 | + @classmethod |
908 | + def validate_dbus_object(cls, path, _state): |
909 | + return path == b'/window-mocker' |
910 | + |
911 | + app = self.start_mock_app(WindowMockerApp) |
912 | + self.assertThat(app.__class__.__bases__, Contains(EmulatorBase)) |
913 | + self.assertThat( |
914 | + app.__class__.__bases__, |
915 | + Contains(SecondEmulatorBase) |
916 | + ) |
917 | |
918 | def test_handles_using_app_cpo_base_class(self): |
919 | # This test replicates an issue found in an application test suite |
920 | @@ -140,30 +131,17 @@ |
921 | |
922 | self.start_mock_app(WindowMockerApp) |
923 | |
924 | - def test_warns_when_using_incorrect_cpo_base_class(self): |
925 | - # Ensure the warning method is called when launching a proxy. |
926 | - with object_registry.patch_registry({}): |
927 | - class TestCPO(CustomEmulatorBase): |
928 | - pass |
929 | - |
930 | - class WindowMockerApp(TestCPO): |
931 | - @classmethod |
932 | - def validate_dbus_object(cls, path, _state): |
933 | - return path == b'/window-mocker' |
934 | - |
935 | - with patch.object(_search, 'logger') as p_logger: |
936 | - self.start_mock_app(WindowMockerApp) |
937 | - self.assertTrue(p_logger.warning.called) |
938 | - |
939 | - def test_can_select_custom_emulators_by_name(self): |
940 | - """Must be able to select a custom emulator type by name.""" |
941 | + def test_selecting_using_string_creates_default_cpo(self): |
942 | + """When not passing CPO must return generated default.""" |
943 | + |
944 | class MouseTestWidget(EmulatorBase): |
945 | pass |
946 | |
947 | app = self.start_mock_app(EmulatorBase) |
948 | test_widget = app.select_single('MouseTestWidget') |
949 | |
950 | - self.assertThat(type(test_widget), Equals(MouseTestWidget)) |
951 | + self.assertThat(type(test_widget), Not(Equals(MouseTestWidget))) |
952 | + self.assertTrue(getattr(test_widget, '__generated')) |
953 | |
954 | def test_can_select_custom_emulators_by_type(self): |
955 | """Must be able to select a custom emulator type by type.""" |
956 | |
957 | === renamed file 'autopilot/tests/unit/test_introspection_object_registry.py' => 'autopilot/tests/unit/introspection_object_registry.py' |
958 | === modified file 'autopilot/tests/unit/test_introspection.py' |
959 | --- autopilot/tests/unit/test_introspection.py 2014-07-31 04:42:08 +0000 |
960 | +++ autopilot/tests/unit/test_introspection.py 2015-07-20 22:41:01 +0000 |
961 | @@ -25,7 +25,10 @@ |
962 | IsInstance, |
963 | raises, |
964 | ) |
965 | -import autopilot.introspection._search as _s |
966 | +from autopilot.introspection import ( |
967 | + _proxy_objects as _po, |
968 | + _search as _s |
969 | +) |
970 | from autopilot.introspection.qt import QtObjectProxyMixin |
971 | import autopilot.introspection as _i |
972 | |
973 | @@ -35,21 +38,21 @@ |
974 | fake_state_data = (String('/some/path'), dict(foo=123)) |
975 | |
976 | def test_returns_classname(self): |
977 | - class_name, _, _ = _s._get_details_from_state_data( |
978 | + class_name, _, _ = _po._get_details_from_state_data( |
979 | self.fake_state_data |
980 | ) |
981 | self.assertThat(class_name, Equals('path')) |
982 | |
983 | def test_returns_path(self): |
984 | - _, path, _ = _s._get_details_from_state_data(self.fake_state_data) |
985 | + _, path, _ = _po._get_details_from_state_data(self.fake_state_data) |
986 | self.assertThat(path, Equals(b'/some/path')) |
987 | |
988 | def test_returned_path_is_bytestring(self): |
989 | - _, path, _ = _s._get_details_from_state_data(self.fake_state_data) |
990 | + _, path, _ = _po._get_details_from_state_data(self.fake_state_data) |
991 | self.assertThat(path, IsInstance(type(b''))) |
992 | |
993 | def test_returns_state_dict(self): |
994 | - _, _, state = _s._get_details_from_state_data(self.fake_state_data) |
995 | + _, _, state = _po._get_details_from_state_data(self.fake_state_data) |
996 | self.assertThat(state, Equals(dict(foo=123))) |
997 | |
998 | |
999 | @@ -119,13 +122,13 @@ |
1000 | |
1001 | def test_raises_RuntimeError_when_no_interface_is_found(self): |
1002 | self.assertThat( |
1003 | - lambda: _s._get_proxy_bases_from_introspection_xml(""), |
1004 | + lambda: _po._get_proxy_bases_from_introspection_xml(""), |
1005 | raises(RuntimeError("Could not find Autopilot interface.")) |
1006 | ) |
1007 | |
1008 | def test_returns_ApplicationProxyObject_claws_for_base_interface(self): |
1009 | self.assertThat( |
1010 | - _s._get_proxy_bases_from_introspection_xml( |
1011 | + _po._get_proxy_bases_from_introspection_xml( |
1012 | self.fake_data_with_ap_interface |
1013 | ), |
1014 | Equals(()) |
1015 | @@ -133,7 +136,7 @@ |
1016 | |
1017 | def test_returns_both_base_and_qt_interface(self): |
1018 | self.assertThat( |
1019 | - _s._get_proxy_bases_from_introspection_xml( |
1020 | + _po._get_proxy_bases_from_introspection_xml( |
1021 | self.fake_data_with_ap_and_qt_interfaces |
1022 | ), |
1023 | Equals((QtObjectProxyMixin,)) |
1024 | |
1025 | === modified file 'autopilot/tests/unit/test_introspection_backends.py' |
1026 | --- autopilot/tests/unit/test_introspection_backends.py 2015-06-09 15:35:08 +0000 |
1027 | +++ autopilot/tests/unit/test_introspection_backends.py 2015-07-20 22:41:01 +0000 |
1028 | @@ -23,6 +23,7 @@ |
1029 | from testtools.matchers import Equals, Not, NotEquals, IsInstance |
1030 | |
1031 | from autopilot.introspection import ( |
1032 | + _proxy_objects as _po, |
1033 | _xpathselect as xpathselect, |
1034 | backends, |
1035 | dbus, |
1036 | @@ -325,21 +326,24 @@ |
1037 | """Verify that a class has a validation method by default.""" |
1038 | self.assertTrue(callable(self.DefaultSelector.validate_dbus_object)) |
1039 | |
1040 | - @patch.object(backends, '_get_proxy_object_class') |
1041 | + @patch.object(_po, 'get_proxy_object_class') |
1042 | def test_make_introspection_object(self, gpoc): |
1043 | """Verify that make_introspection_object makes the right call.""" |
1044 | gpoc.return_value = self.DefaultSelector |
1045 | - fake_id = Mock() |
1046 | + fake_backend = Mock() |
1047 | + cpo_name = self.getUniqueString() |
1048 | + path = String('/Object') |
1049 | + |
1050 | new_fake = backends.make_introspection_object( |
1051 | - (String('/Object'), {'id': [0, 42]}), |
1052 | - None, |
1053 | - fake_id, |
1054 | + (path, {'id': [0, 42]}), |
1055 | + fake_backend, |
1056 | + cpo_name |
1057 | ) |
1058 | self.assertThat(new_fake, IsInstance(self.DefaultSelector)) |
1059 | gpoc.assert_called_once_with( |
1060 | - fake_id, |
1061 | - b'/Object', |
1062 | - {'id': [0, 42]} |
1063 | + cpo_name, |
1064 | + path.encode('utf-8'), |
1065 | + fake_backend.ipc_address |
1066 | ) |
1067 | |
1068 | def test_validate_dbus_object_matches_on_class_name(self): |
1069 | |
1070 | === modified file 'autopilot/tests/unit/test_introspection_dbus.py' |
1071 | --- autopilot/tests/unit/test_introspection_dbus.py 2015-07-20 22:41:00 +0000 |
1072 | +++ autopilot/tests/unit/test_introspection_dbus.py 2015-07-20 22:41:01 +0000 |
1073 | @@ -29,50 +29,19 @@ |
1074 | from testtools.matchers import ( |
1075 | Equals, |
1076 | Not, |
1077 | - NotEquals, |
1078 | Raises, |
1079 | raises, |
1080 | ) |
1081 | |
1082 | from autopilot.exceptions import StateNotFoundError |
1083 | -from autopilot.introspection import CustomEmulatorBase |
1084 | -from autopilot.introspection import dbus |
1085 | +from autopilot.introspection import ( |
1086 | + _proxy_objects as _po, |
1087 | + CustomEmulatorBase, |
1088 | + dbus |
1089 | +) |
1090 | from autopilot.utilities import sleep |
1091 | |
1092 | |
1093 | -class IntrospectionFeatureTests(TestCase): |
1094 | - |
1095 | - def test_custom_emulator_base_does_not_have_id(self): |
1096 | - self.assertThat(hasattr(CustomEmulatorBase, '_id'), Equals(False)) |
1097 | - |
1098 | - def test_derived_emulator_bases_do_have_id(self): |
1099 | - class MyEmulatorBase(CustomEmulatorBase): |
1100 | - pass |
1101 | - self.assertThat(hasattr(MyEmulatorBase, '_id'), Equals(True)) |
1102 | - |
1103 | - def test_derived_children_have_same_id(self): |
1104 | - class MyEmulatorBase(CustomEmulatorBase): |
1105 | - pass |
1106 | - |
1107 | - class MyEmulator(MyEmulatorBase): |
1108 | - pass |
1109 | - |
1110 | - class MyEmulator2(MyEmulatorBase): |
1111 | - pass |
1112 | - |
1113 | - self.assertThat(MyEmulatorBase._id, Equals(MyEmulator._id)) |
1114 | - self.assertThat(MyEmulatorBase._id, Equals(MyEmulator2._id)) |
1115 | - |
1116 | - def test_children_have_different_ids(self): |
1117 | - class MyEmulatorBase(CustomEmulatorBase): |
1118 | - pass |
1119 | - |
1120 | - class MyEmulatorBase2(CustomEmulatorBase): |
1121 | - pass |
1122 | - |
1123 | - self.assertThat(MyEmulatorBase._id, NotEquals(MyEmulatorBase2._id)) |
1124 | - |
1125 | - |
1126 | class DBusIntrospectionObjectTests(TestCase): |
1127 | |
1128 | def test_can_access_path_attribute(self): |
1129 | @@ -141,6 +110,27 @@ |
1130 | self.assertThat(TestCPO.get_type_query_name(), Equals("TestCPO")) |
1131 | |
1132 | |
1133 | +class ProxyObjectCreationTests(TestCase): |
1134 | + def test_default_class_mentioned_in_attr_error(self): |
1135 | + name = self.getUniqueString() |
1136 | + fake_class = _po._get_default_proxy_class(name, ()) |
1137 | + fake_object = fake_class( |
1138 | + dict(id=[0, 123], path=[0, '/some/path']), |
1139 | + b'/root', |
1140 | + Mock() |
1141 | + ) |
1142 | + |
1143 | + expected_msg = "Generated Class '{}' has no attribute 'foo'.".format( |
1144 | + name |
1145 | + ) |
1146 | + |
1147 | + with fake_object.no_automatic_refreshing(): |
1148 | + self.assertThat( |
1149 | + lambda: fake_object.foo(), |
1150 | + raises(AttributeError(expected_msg)) |
1151 | + ) |
1152 | + |
1153 | + |
1154 | class ProxyObjectPrintTreeTests(TestCase): |
1155 | |
1156 | def _print_test_fake_object(self): |
1157 | |
1158 | === modified file 'autopilot/tests/unit/test_introspection_search.py' |
1159 | --- autopilot/tests/unit/test_introspection_search.py 2015-05-06 10:00:49 +0000 |
1160 | +++ autopilot/tests/unit/test_introspection_search.py 2015-07-20 22:41:01 +0000 |
1161 | @@ -35,7 +35,6 @@ |
1162 | from autopilot.utilities import sleep |
1163 | from autopilot.introspection import _search as _s |
1164 | |
1165 | -from autopilot.introspection import CustomEmulatorBase |
1166 | from autopilot.introspection.constants import AUTOPILOT_PATH |
1167 | |
1168 | |
1169 | @@ -732,95 +731,3 @@ |
1170 | _s._find_matching_connections(bus, lambda *args: True) |
1171 | |
1172 | dedupe.assert_called_once_with(["conn1"], bus) |
1173 | - |
1174 | - |
1175 | -class ActualBaseClassTests(TestCase): |
1176 | - |
1177 | - def test_dont_raise_passed_base_when_is_only_base(self): |
1178 | - class ActualBase(CustomEmulatorBase): |
1179 | - pass |
1180 | - |
1181 | - try: |
1182 | - _s._raise_if_base_class_not_actually_base(ActualBase) |
1183 | - except ValueError: |
1184 | - self.fail('Unexpected ValueError exception') |
1185 | - |
1186 | - def test_raises_if_passed_incorrect_base_class(self): |
1187 | - class ActualBase(CustomEmulatorBase): |
1188 | - pass |
1189 | - |
1190 | - class InheritedCPO(ActualBase): |
1191 | - pass |
1192 | - |
1193 | - self.assertRaises( |
1194 | - ValueError, |
1195 | - _s._raise_if_base_class_not_actually_base, |
1196 | - InheritedCPO |
1197 | - ) |
1198 | - |
1199 | - def test_raises_parent_with_simple_non_ap_multi_inheritance(self): |
1200 | - """When mixing in non-customproxy classes must return the base.""" |
1201 | - |
1202 | - class ActualBase(CustomEmulatorBase): |
1203 | - pass |
1204 | - |
1205 | - class InheritedCPO(ActualBase): |
1206 | - pass |
1207 | - |
1208 | - class TrickyOne(object): |
1209 | - pass |
1210 | - |
1211 | - class FinalForm(InheritedCPO, TrickyOne): |
1212 | - pass |
1213 | - |
1214 | - self.assertRaises( |
1215 | - ValueError, |
1216 | - _s._raise_if_base_class_not_actually_base, |
1217 | - FinalForm |
1218 | - ) |
1219 | - |
1220 | - def test_raises_parent_with_non_ap_multi_inheritance(self): |
1221 | - |
1222 | - class ActualBase(CustomEmulatorBase): |
1223 | - pass |
1224 | - |
1225 | - class InheritedCPO(ActualBase): |
1226 | - pass |
1227 | - |
1228 | - class TrickyOne(object): |
1229 | - pass |
1230 | - |
1231 | - class FinalForm(TrickyOne, InheritedCPO): |
1232 | - pass |
1233 | - |
1234 | - self.assertRaises( |
1235 | - ValueError, |
1236 | - _s._raise_if_base_class_not_actually_base, |
1237 | - FinalForm |
1238 | - ) |
1239 | - |
1240 | - def test_dont_raise_when_using_default_emulator_base(self): |
1241 | - # _make_proxy_object potentially creates a default base. |
1242 | - DefaultBase = _s._make_default_emulator_base() |
1243 | - try: |
1244 | - _s._raise_if_base_class_not_actually_base(DefaultBase) |
1245 | - except ValueError: |
1246 | - self.fail('Unexpected ValueError exception') |
1247 | - |
1248 | - def test_exception_message_contains_useful_information(self): |
1249 | - class ActualBase(CustomEmulatorBase): |
1250 | - pass |
1251 | - |
1252 | - class InheritedCPO(ActualBase): |
1253 | - pass |
1254 | - |
1255 | - try: |
1256 | - _s._raise_if_base_class_not_actually_base(InheritedCPO) |
1257 | - except ValueError as err: |
1258 | - self.assertEqual( |
1259 | - str(err), |
1260 | - _s.WRONG_CPO_CLASS_MSG.format( |
1261 | - passed=InheritedCPO, |
1262 | - actual=ActualBase |
1263 | - ) |
1264 | - ) |
1265 | |
1266 | === added file 'autopilot/tests/unit/test_proxy_object.py' |
1267 | --- autopilot/tests/unit/test_proxy_object.py 1970-01-01 00:00:00 +0000 |
1268 | +++ autopilot/tests/unit/test_proxy_object.py 2015-07-20 22:41:01 +0000 |
1269 | @@ -0,0 +1,81 @@ |
1270 | +# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- |
1271 | +# |
1272 | +# Autopilot Functional Test Tool |
1273 | +# Copyright (C) 2015 Canonical |
1274 | +# |
1275 | +# This program is free software: you can redistribute it and/or modify |
1276 | +# it under the terms of the GNU General Public License as published by |
1277 | +# the Free Software Foundation, either version 3 of the License, or |
1278 | +# (at your option) any later version. |
1279 | +# |
1280 | +# This program is distributed in the hope that it will be useful, |
1281 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
1282 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
1283 | +# GNU General Public License for more details. |
1284 | +# |
1285 | +# You should have received a copy of the GNU General Public License |
1286 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
1287 | +# |
1288 | + |
1289 | +from unittest.mock import ( |
1290 | + Mock, |
1291 | + patch |
1292 | +) |
1293 | +from testtools import TestCase |
1294 | + |
1295 | +from autopilot.introspection import _proxy_objects as _po |
1296 | + |
1297 | + |
1298 | +class IsCpoClassTests(TestCase): |
1299 | + |
1300 | + def test_returns_False_when_None(self): |
1301 | + self.assertFalse(_po._is_cpo_class(None)) |
1302 | + |
1303 | + def test_returns_False_when_is_string(self): |
1304 | + self.assertFalse(_po._is_cpo_class('foo')) |
1305 | + |
1306 | + def test_returns_True_when_is_class(self): |
1307 | + class Foo(): |
1308 | + pass |
1309 | + self.assertTrue(_po._is_cpo_class(Foo)) |
1310 | + |
1311 | + |
1312 | +class GetApplicableClassNameTests(TestCase): |
1313 | + |
1314 | + @patch.object(_po, 'get_classname_from_path') |
1315 | + def test_queries_path_when_name_unknown(self, p_gcfp): |
1316 | + path = self.getUniqueString() |
1317 | + _po._get_applicable_class_name(None, path) |
1318 | + p_gcfp.assert_called_once_with(path) |
1319 | + |
1320 | + @patch.object(_po, 'get_classname_from_path') |
1321 | + def test_returns_path_name_when_passed_None(self, p_gcfp): |
1322 | + name = self.getUniqueString() |
1323 | + p_gcfp.return_value = bytes(name, 'utf-8') |
1324 | + |
1325 | + self.assertEqual(name, _po._get_applicable_class_name(None, '')) |
1326 | + |
1327 | + @patch.object(_po, 'get_classname_from_path') |
1328 | + def test_returns_dbus_name_when_passed_star(self, p_gcfp): |
1329 | + name = self.getUniqueString() |
1330 | + path = '' |
1331 | + p_gcfp.return_value = bytes(name, 'utf-8') |
1332 | + |
1333 | + self.assertEqual( |
1334 | + name, |
1335 | + _po._get_applicable_class_name('*', path) |
1336 | + ) |
1337 | + |
1338 | + def test_returns_string_from_classname(self): |
1339 | + name = self.getUniqueString() |
1340 | + fake_dbus_backend = Mock() |
1341 | + with patch.object(_po, 'get_classname_from_path') as from_path: |
1342 | + from_path.return_value = bytes(name, 'utf-8') |
1343 | + self.assertEqual( |
1344 | + _po._get_applicable_class_name('*', fake_dbus_backend), |
1345 | + name |
1346 | + ) |
1347 | + |
1348 | + def test_returns_name_of_string(self): |
1349 | + name = self.getUniqueString() |
1350 | + self.assertEqual(name, _po._get_applicable_class_name(name, None)) |
PASSED: Continuous integration, rev:584 jenkins. qa.ubuntu. com/job/ autopilot- ci/1092/ jenkins. qa.ubuntu. com/job/ autopilot- wily-amd64- ci/23 jenkins. qa.ubuntu. com/job/ autopilot- wily-amd64- ci/23/artifact/ work/output/ *zip*/output. zip jenkins. qa.ubuntu. com/job/ autopilot- wily-armhf- ci/22 jenkins. qa.ubuntu. com/job/ autopilot- wily-armhf- ci/22/artifact/ work/output/ *zip*/output. zip jenkins. qa.ubuntu. com/job/ autopilot- wily-i386- ci/23 jenkins. qa.ubuntu. com/job/ autopilot- wily-i386- ci/23/artifact/ work/output/ *zip*/output. zip jenkins. qa.ubuntu. com/job/ generic- deb-autopilot- vivid-touch/ 3280 jenkins. qa.ubuntu. com/job/ generic- deb-autopilot- runner- vivid-mako/ 2737 jenkins. qa.ubuntu. com/job/ generic- mediumtests- builder- vivid-armhf/ 3278 jenkins. qa.ubuntu. com/job/ generic- mediumtests- builder- vivid-armhf/ 3278/artifact/ work/output/ *zip*/output. zip s-jenkins. ubuntu- ci:8080/ job/touch- flash-device/ 21379
http://
Executed test runs:
SUCCESS: http://
deb: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
deb: http://
UNSTABLE: http://
UNSTABLE: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild: s-jenkins. ubuntu- ci:8080/ job/autopilot- ci/1092/ rebuild
http://