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

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

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

Commit message

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

Description of the change

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

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

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

585. By Christopher Lee

Remove un-used method

586. By Christopher Lee

Remove question comment and update docstring.

587. By Christopher Lee

Fix whitespace issue.

588. By Christopher Lee

Remove un-used test class.

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

The new changes seem reasonable to me.

review: Approve
589. By Christopher Lee

Merge pre-req. updates.

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

Is this actually going to land?

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

Hi Nicholas,

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

Unmerged revisions

589. By Christopher Lee

Merge pre-req. updates.

588. By Christopher Lee

Remove un-used test class.

587. By Christopher Lee

Fix whitespace issue.

586. By Christopher Lee

Remove question comment and update docstring.

585. By Christopher Lee

Remove un-used method

584. By Christopher Lee

Remove changes that accidently got in

583. By Christopher Lee

Merge pre-req. changes

582. By Christopher Lee

Remove conversion notes

581. By Christopher Lee

Initial renaming from emulator_base to cpo_class

580. By Christopher Lee

Fix tests due to updated code.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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))

Subscribers

People subscribed via source and target branches