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

Proposed by Christopher Lee
Status: Superseded
Proposed branch: lp:~veebers/autopilot/remove_object_registry-CPO_creation_explicit
Merge into: lp:autopilot
Diff against target: 1472 lines (+568/-464)
13 files modified
autopilot/introspection/_object_registry.py (+0/-110)
autopilot/introspection/_proxy_objects.py (+211/-0)
autopilot/introspection/_search.py (+34/-151)
autopilot/introspection/backends.py (+11/-5)
autopilot/introspection/dbus.py (+77/-13)
autopilot/tests/functional/test_dbus_query.py (+6/-0)
autopilot/tests/functional/test_input_stack.py (+1/-1)
autopilot/tests/functional/test_introspection_features.py (+56/-40)
autopilot/tests/unit/test_introspection.py (+11/-8)
autopilot/tests/unit/test_introspection_backends.py (+9/-7)
autopilot/tests/unit/test_introspection_dbus.py (+68/-36)
autopilot/tests/unit/test_introspection_search.py (+0/-93)
autopilot/tests/unit/test_proxy_object.py (+84/-0)
To merge this branch: bzr merge lp:~veebers/autopilot/remove_object_registry-CPO_creation_explicit
Reviewer Review Type Date Requested Status
Autopilot Hackers Pending
Review via email: mp+261809@code.launchpad.net

This proposal has been superseded by a proposal from 2015-06-16.

Commit message

Remove the idea of (and actual) the object registry

Description of the change

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

To post a comment you must log in.
567. By Christopher Lee

Initial changes for async proxy creation (autopilot vis) not stable.

568. By Christopher Lee

Fix pyflake errors

569. By Christopher Lee

Merge and fix conflicts from push branch

570. By Christopher Lee

Another pyflake fix

571. By Christopher Lee

Remove question comment, has been answered

572. By Christopher Lee

Test for error message (error message has been improved.

573. By Christopher Lee

Further tests and fixes for an issue found

574. By Christopher Lee

Pyflake fixes.

575. By Christopher Lee

Make vis work

576. By Christopher Lee

Cleanup for vis make_proxy

577. By Christopher Lee

Slight cleanup and removal of unneeded method

578. By Christopher Lee

Remove comment and clear up question from comment.

579. By Christopher Lee

Clean up docstring

580. By Christopher Lee

Fix tests due to updated code.

581. By Christopher Lee

Initial renaming from emulator_base to cpo_class

582. By Christopher Lee

Remove conversion notes

583. By Christopher Lee

Merge pre-req. changes

584. By Christopher Lee

Remove changes that accidently got in

585. By Christopher Lee

Remove un-used method

586. By Christopher Lee

Remove question comment and update docstring.

587. By Christopher Lee

Fix whitespace issue.

588. By Christopher Lee

Remove un-used test class.

589. By Christopher Lee

Merge pre-req. updates.

Unmerged revisions

589. By Christopher Lee

Merge pre-req. updates.

588. By Christopher Lee

Remove un-used test class.

587. By Christopher Lee

Fix whitespace issue.

586. By Christopher Lee

Remove question comment and update docstring.

585. By Christopher Lee

Remove un-used method

584. By Christopher Lee

Remove changes that accidently got in

583. By Christopher Lee

Merge pre-req. changes

582. By Christopher Lee

Remove conversion notes

581. By Christopher Lee

Initial renaming from emulator_base to cpo_class

580. By Christopher Lee

Fix tests due to updated code.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'autopilot/introspection/_object_registry.py'
--- autopilot/introspection/_object_registry.py 2015-05-06 10:00:49 +0000
+++ autopilot/introspection/_object_registry.py 2015-06-15 09:43:19 +0000
@@ -42,10 +42,6 @@
4242
43"""43"""
4444
45from uuid import uuid4
46
47from autopilot.introspection._xpathselect import get_classname_from_path
48from autopilot.utilities import get_debug_logger
49from contextlib import contextmanager45from contextlib import contextmanager
5046
51_object_registry = {}47_object_registry = {}
@@ -62,92 +58,6 @@
62 return _proxy_extensions.get(id, ())58 return _proxy_extensions.get(id, ())
6359
6460
65class IntrospectableObjectMetaclass(type):
66 """Metaclass to insert appropriate classes into the object registry."""
67
68 def __new__(cls, classname, bases, classdict):
69 """Create a new proxy class, possibly adding it to the object registry.
70
71 Test authors may derive a class from DBusIntrospectionObject or the
72 CustomEmulatorBase alias and use this as their 'emulator base'. This
73 class will be given a unique '_id' attribute. That attribute is the
74 first level index into the object registry. It's used so we can have
75 custom proxy classes for more than one process at the same time and
76 avoid clashes in the dictionary.
77 """
78 cls_id = None
79
80 for base in bases:
81 if hasattr(base, '_id'):
82 cls_id = base._id
83 break
84 else:
85 # Ignore classes that are in the autopilot class heirarchy:
86 if classname not in (
87 'ApplicationProxyObject',
88 'CustomEmulatorBase',
89 'DBusIntrospectionObject',
90 'DBusIntrospectionObjectBase',
91 ):
92 # Add the '_id' attribute as a class attr:
93 cls_id = classdict['_id'] = uuid4()
94
95 # use the bases passed to us, but extend it with whatever is stored in
96 # the proxy_extensions dictionary.
97 extensions = _get_proxy_bases_for_id(cls_id)
98 for extension in extensions:
99 if extension not in bases:
100 bases += (extension,)
101 # make the object. Nothing special here.
102 class_object = type.__new__(cls, classname, bases, classdict)
103
104 if not classdict.get('__generated', False):
105 # If the newly made object has an id, add it to the object
106 # registry.
107 if getattr(class_object, '_id', None) is not None:
108 if class_object._id in _object_registry:
109 _object_registry[class_object._id][classname] = \
110 class_object
111 else:
112 _object_registry[class_object._id] = \
113 {classname: class_object}
114 # in all cases, return the class unchanged.
115 return class_object
116
117
118DBusIntrospectionObjectBase = IntrospectableObjectMetaclass(
119 'DBusIntrospectionObjectBase',
120 (object,),
121 {}
122)
123
124
125def _get_proxy_object_class(object_id, path, state):
126 """Return a custom proxy class, from the object registry or the default.
127
128 This function first inspects the object registry using the object_id passed
129 in. The object_id will be unique to all custom proxy classes for the same
130 application.
131
132 If that fails, we create a class on the fly based on the default class.
133
134 :param object_id: The _id attribute of the class doing the lookup. This is
135 used to index into the object registry to retrieve the dict of proxy
136 classes to try.
137 :param path: dbus path
138 :param state: dbus state
139 :returns: appropriate custom proxy class
140 :raises ValueError: if more than one class in the dict matches
141
142 """
143 class_type = _try_custom_proxy_classes(object_id, path, state)
144
145 return class_type or _get_default_proxy_class(
146 object_id,
147 get_classname_from_path(path)
148 )
149
150
151def _try_custom_proxy_classes(object_id, path, state):61def _try_custom_proxy_classes(object_id, path, state):
152 """Identify which custom proxy class matches the dbus path and state.62 """Identify which custom proxy class matches the dbus path and state.
15363
@@ -239,26 +149,6 @@
239 return order149 return order
240150
241151
242def _get_default_proxy_class(id, name):
243 """Return a custom proxy object class of the default or a base class.
244
245 We want the object to inherit from the class that is set as the emulator
246 base class, not the class that is doing the selecting. Using the passed id
247 we retrieve the relevant bases from the object registry.
248
249 :param id: The object id (_id attribute) of the class doing the lookup.
250 :param name: name of new class
251 :returns: custom proxy object class
252
253 """
254 get_debug_logger().warning(
255 "Generating introspection instance for type '%s' based on generic "
256 "class.", name)
257 if isinstance(name, bytes):
258 name = name.decode('utf-8')
259 return type(name, _get_proxy_bases_for_id(id), dict(__generated=True))
260
261
262@contextmanager152@contextmanager
263def patch_registry(new_registry):153def patch_registry(new_registry):
264 """A utility context manager that allows us to patch the object registry.154 """A utility context manager that allows us to patch the object registry.
265155
=== added file 'autopilot/introspection/_proxy_objects.py'
--- autopilot/introspection/_proxy_objects.py 1970-01-01 00:00:00 +0000
+++ autopilot/introspection/_proxy_objects.py 2015-06-15 09:43:19 +0000
@@ -0,0 +1,211 @@
1# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
2#
3# Autopilot Functional Test Tool
4# Copyright (C) 2015 Canonical
5#
6# This program is free software: you can redistribute it and/or modify
7# it under the terms of the GNU General Public License as published by
8# the Free Software Foundation, either version 3 of the License, or
9# (at your option) any later version.
10#
11# This program is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14# GNU General Public License for more details.
15#
16# You should have received a copy of the GNU General Public License
17# along with this program. If not, see <http://www.gnu.org/licenses/>.
18#
19
20"""Functions for dealing with proxy classes and objects."""
21
22
23from autopilot.introspection import (
24 constants,
25 dbus as ap_dbus
26)
27from autopilot.introspection._xpathselect import get_classname_from_path
28from autopilot.utilities import get_debug_logger
29
30
31def _get_introspection_xml_from_backend(
32 backend, reply_handler=None, error_handler=None):
33 """Get DBus Introspection xml from a backend.
34
35 :param backend: The backend object to query.
36 :param reply_handler: If set, makes a dbus async call, and the result will
37 be sent to reply_handler. This must be a callable object.
38 :param error_handler: If set, this callable will recieve any errors, and
39 the call will be made asyncronously.
40 :returns: A string containing introspection xml, if called synchronously.
41 :raises ValueError: if one, but not both of 'reply_handler' and
42 'error_handler' are set.
43
44 """
45 if callable(reply_handler) and callable(error_handler):
46 backend.dbus_introspection_iface.Introspect(
47 reply_handler=reply_handler,
48 error_handler=error_handler,
49 )
50 elif reply_handler or error_handler:
51 raise ValueError(
52 "Both 'reply_handler' and 'error_handler' must be set."
53 )
54 else:
55 return backend.dbus_introspection_iface.Introspect()
56
57
58def _get_proxy_bases_from_introspection_xml(introspection_xml):
59 """Return tuple of the base classes to use when creating a proxy object.
60
61 Currently this works by looking for certain interface names in the XML. In
62 the future we may want to parse the XML and perform more rigerous checks.
63
64 :param introspection_xml: An xml string that describes the exported object
65 on the dbus backend. This determines which capabilities are present in
66 the backend, and therefore which base classes should be used to create
67 the proxy object.
68
69 :raises RuntimeError: if the autopilot interface cannot be found.
70
71 """
72
73 bases = []
74
75 if constants.AP_INTROSPECTION_IFACE not in introspection_xml:
76 raise RuntimeError("Could not find Autopilot interface.")
77
78 if constants.QT_AUTOPILOT_IFACE in introspection_xml:
79 from autopilot.introspection.qt import QtObjectProxyMixin
80 bases.append(QtObjectProxyMixin)
81
82 return tuple(bases)
83
84
85def _get_proxy_object_class_name_and_state(
86 backend, reply_handler=None, error_handler=None):
87 """Get details about this autopilot backend via a dbus GetState call.
88
89 :param reply_handler: A callable that must accept three positional
90 arguments, which correspond to the return value of this function when
91 called synchronously.
92
93 :param error_handler: A callable which will recieve any dbus errors, should
94 they occur.
95
96 :raises ValueError: if one, but not both of reply_handler and error_handler
97 are set.
98
99 :returns: A tuple containing the class name of the root of the
100 introspection tree, the full path to the root of the introspection
101 tree, and the state dictionary of the root node in the introspection
102 tree.
103
104 """
105 if callable(reply_handler) and callable(error_handler):
106 # Async call:
107 # Since we get an array of state, and we only care about the first one
108 # we use a lambda to unpack it and get the details we want.
109 backend.introspection_iface.GetState(
110 "/",
111 reply_handler=lambda r: reply_handler(
112 *_get_details_from_state_data(r[0])
113 ),
114 error_handler=error_handler,
115 )
116 elif reply_handler or error_handler:
117 raise ValueError(
118 "Both 'reply_handler' and 'error_handler' must be set."
119 )
120 else:
121 # Sync call
122 state = backend.introspection_iface.GetState("/")[0]
123 return _get_details_from_state_data(state)
124
125
126def _get_details_from_state_data(state_data):
127 """Get details from a state data array.
128
129 Returns class name, path, and state dictionary.
130 """
131 object_path, object_state = state_data
132 return (
133 get_classname_from_path(object_path),
134 object_path.encode('utf-8'),
135 object_state,
136 )
137
138
139def _get_default_proxy_class(name, bases):
140 """Return a default custom proxy object class.
141
142 :param name: name of new class
143 :param bases: Tuple of base classes to add to the base.
144 :returns: custom proxy object class
145
146 """
147 get_debug_logger().warning(
148 "Generating introspection instance for type '%s' based on generic "
149 "class.", name)
150 if isinstance(name, bytes):
151 name = name.decode('utf-8')
152 return type(
153 name,
154 (ap_dbus.DBusIntrospectionObject,) + bases,
155 dict(__generated=True)
156 )
157
158
159# vvveebers this probably shouldn't be 'private'.
160def _get_proxy_object_class(cpo_or_string, path, dbus_backend):
161 """Return the proxy class to use with the correct bases added
162
163 If passed a CPO returns that, if passed a string contructs a default class
164 to use instead.
165
166 """
167
168 extension_classes = _get_extension_classes(dbus_backend)
169
170 if _is_cpo_class(cpo_or_string):
171 _apply_mixin_bases(cpo_or_string, extension_classes)
172 return cpo_or_string
173 else:
174 cpo_name = _get_applicable_class_name(cpo_or_string, path)
175 return _get_default_proxy_class(cpo_name, extension_classes)
176
177
178def _get_extension_classes(dbus_backend):
179 """Return any extension classes that this object will need."""
180
181 intro_xml = _get_introspection_xml_from_backend(dbus_backend)
182 try:
183 return _get_proxy_bases_from_introspection_xml(intro_xml)
184 except RuntimeError as e:
185 e.args = (
186 "Could not find Autopilot interface on dbus address '%s'."
187 % dbus_backend,
188 )
189
190
191def _is_cpo_class(cpo_or_string):
192 return cpo_or_string is not None and not isinstance(cpo_or_string, str)
193
194
195def _get_applicable_class_name(cpo_name, path):
196 """Return correct name for class.
197
198 It is possible that the cpo_name is '*' which is what was used in the
199 query. Determine what the actual name should be.
200
201 """
202 if cpo_name is None or cpo_name == '*':
203 cls_name = get_classname_from_path(path)
204 return cls_name
205 else:
206 return cpo_name
207
208
209def _apply_mixin_bases(target_class, mixin_classes):
210 to_add = tuple(set(mixin_classes).difference(target_class.__bases__))
211 target_class.__bases__ += to_add
0212
=== modified file 'autopilot/introspection/_search.py'
--- autopilot/introspection/_search.py 2015-05-06 10:03:43 +0000
+++ autopilot/introspection/_search.py 2015-06-15 09:43:19 +0000
@@ -33,7 +33,7 @@
33from autopilot.introspection import backends33from autopilot.introspection import backends
34from autopilot.introspection import constants34from autopilot.introspection import constants
35from autopilot.introspection import dbus as ap_dbus35from autopilot.introspection import dbus as ap_dbus
36from autopilot.introspection import _object_registry36from autopilot.introspection import _proxy_objects as _po
37from autopilot.introspection._xpathselect import get_classname_from_path37from autopilot.introspection._xpathselect import get_classname_from_path
38from autopilot.introspection.backends import WireProtocolVersionMismatch38from autopilot.introspection.backends import WireProtocolVersionMismatch
39from autopilot.introspection.utilities import (39from autopilot.introspection.utilities import (
@@ -384,40 +384,22 @@
384 :param emulator_base: The emulator base object (or None), as provided by384 :param emulator_base: The emulator base object (or None), as provided by
385 the user.385 the user.
386 """386 """
387
388 # vvveebers emulator_base will become proxy_class (to be explicit as the
389 # term emulator has long gone.).
390
387 # make sure we always have an emulator base. Either use the one the user391 # make sure we always have an emulator base. Either use the one the user
388 # gave us, or make one:392 # gave us, or make one:
389 emulator_base = emulator_base or _make_default_emulator_base()393 # This will return a default class or the CPO with the correct stuff added.
390 _raise_if_base_class_not_actually_base(emulator_base)394
391395 cls_name, path, cls_state = _po._get_proxy_object_class_name_and_state(
392 # Get the dbus introspection Xml for the backend.
393 intro_xml = _get_introspection_xml_from_backend(dbus_address)
394 try:
395 # Figure out if the backend has any extension methods, and return
396 # classes that understand how to use each of those extensions:
397 extension_classes = _get_proxy_bases_from_introspection_xml(intro_xml)
398
399 # Register those base classes for everything that will derive from this
400 # emulator base class.
401 _object_registry.register_extension_classes_for_proxy_base(
402 emulator_base,
403 extension_classes,
404 )
405 except RuntimeError as e:
406 e.args = (
407 "Could not find Autopilot interface on dbus address '%s'."
408 % dbus_address,
409 )
410 raise e
411
412 cls_name, path, cls_state = _get_proxy_object_class_name_and_state(
413 dbus_address396 dbus_address
414 )397 )
415398
416 proxy_class = _object_registry._get_proxy_object_class(399 proxy_class = _po._get_proxy_object_class(
417 emulator_base._id,400 emulator_base, path, dbus_address
418 path,
419 cls_state
420 )401 )
402
421 # For this object only, add the ApplicationProxy class, since it's the403 # For this object only, add the ApplicationProxy class, since it's the
422 # root of the tree. Ideally this would be nicer...404 # root of the tree. Ideally this would be nicer...
423 if ApplicationProxyObject not in proxy_class.__bases__:405 if ApplicationProxyObject not in proxy_class.__bases__:
@@ -426,6 +408,7 @@
426408
427409
428def _make_default_emulator_base():410def _make_default_emulator_base():
411 # vvveebers this will be removed (make_proxy_async needs updated.)
429 """Make a default base class for all proxy classes to derive from."""412 """Make a default base class for all proxy classes to derive from."""
430 return type("DefaultEmulatorBase", (ap_dbus.DBusIntrospectionObject,), {})413 return type("DefaultEmulatorBase", (ap_dbus.DBusIntrospectionObject,), {})
431414
@@ -471,6 +454,8 @@
471 reply_handler will be called with a single argument: The proxy object.454 reply_handler will be called with a single argument: The proxy object.
472455
473 """456 """
457 # vvveebers emulator_base name will change to proxy_class.
458
474 # Note: read this function backwards!459 # Note: read this function backwards!
475 #460 #
476 # Due to the callbacks, I need to define the end of the callback chain461 # Due to the callbacks, I need to define the end of the callback chain
@@ -482,20 +467,26 @@
482 def build_proxy(introspection_xml, cls_name, path, cls_state):467 def build_proxy(introspection_xml, cls_name, path, cls_state):
483 # Figure out if the backend has any extension methods, and return468 # Figure out if the backend has any extension methods, and return
484 # classes that understand how to use each of those extensions:469 # classes that understand how to use each of those extensions:
485 extension_classes = _get_proxy_bases_from_introspection_xml(470 extension_classes = _po._get_proxy_bases_from_introspection_xml(
486 introspection_xml471 introspection_xml
487 )472 )
488 # Register those base classes for everything that will derive from this473
489 # emulator base class.474 # XXX ERROR!
490 _object_registry.register_extension_classes_for_proxy_base(475 emulator_base = None
491 emulator_base,476 if emulator_base is None:
492 extension_classes,477 emulator_base = cls_name
493 )478
494 proxy_class = _object_registry._get_proxy_object_class(479 if isinstance(emulator_base, str):
495 emulator_base._id,480 cpo_name = emulator_base
496 path,481 proxy_class = _po._get_default_proxy_class(
497 cls_state482 cpo_name,
498 )483 extension_classes
484 )
485 else:
486 # It's actually a CPO class.
487 _po._apply_mixin_bases(emulator_base, extension_classes)
488 proxy_class = emulator_base
489
499 reply_handler(490 reply_handler(
500 proxy_class(cls_state, path, backends.Backend(data_source))491 proxy_class(cls_state, path, backends.Backend(data_source))
501 )492 )
@@ -503,7 +494,7 @@
503 # Phase 2: We recieve the introspection string, and make an asynchronous494 # Phase 2: We recieve the introspection string, and make an asynchronous
504 # dbus call to get the state information for the root of this applicaiton.495 # dbus call to get the state information for the root of this applicaiton.
505 def get_root_state(introspection_xml):496 def get_root_state(introspection_xml):
506 _get_proxy_object_class_name_and_state(497 _po._get_proxy_object_class_name_and_state(
507 data_source,498 data_source,
508 reply_handler=partial(build_proxy, introspection_xml),499 reply_handler=partial(build_proxy, introspection_xml),
509 error_handler=error_handler,500 error_handler=error_handler,
@@ -513,121 +504,13 @@
513 # from the data_source provided for us.504 # from the data_source provided for us.
514 emulator_base = emulator_base or _make_default_emulator_base()505 emulator_base = emulator_base or _make_default_emulator_base()
515506
516 _get_introspection_xml_from_backend(507 _po._get_introspection_xml_from_backend(
517 data_source,508 data_source,
518 reply_handler=get_root_state,509 reply_handler=get_root_state,
519 error_handler=error_handler510 error_handler=error_handler
520 )511 )
521512
522513
523def _get_introspection_xml_from_backend(
524 backend, reply_handler=None, error_handler=None):
525 """Get DBus Introspection xml from a backend.
526
527 :param backend: The backend object to query.
528 :param reply_handler: If set, makes a dbus async call, and the result will
529 be sent to reply_handler. This must be a callable object.
530 :param error_handler: If set, this callable will recieve any errors, and
531 the call will be made asyncronously.
532 :returns: A string containing introspection xml, if called synchronously.
533 :raises ValueError: if one, but not both of 'reply_handler' and
534 'error_handler' are set.
535
536 """
537 if callable(reply_handler) and callable(error_handler):
538 backend.dbus_introspection_iface.Introspect(
539 reply_handler=reply_handler,
540 error_handler=error_handler,
541 )
542 elif reply_handler or error_handler:
543 raise ValueError(
544 "Both 'reply_handler' and 'error_handler' must be set."
545 )
546 else:
547 return backend.dbus_introspection_iface.Introspect()
548
549
550def _get_proxy_object_class_name_and_state(
551 backend, reply_handler=None, error_handler=None):
552 """Get details about this autopilot backend via a dbus GetState call.
553
554 :param reply_handler: A callable that must accept three positional
555 arguments, which correspond to the return value of this function when
556 called synchronously.
557
558 :param error_handler: A callable which will recieve any dbus errors, should
559 they occur.
560
561 :raises ValueError: if one, but not both of reply_handler and error_handler
562 are set.
563
564 :returns: A tuple containing the class name of the root of the
565 introspection tree, the full path to the root of the introspection
566 tree, and the state dictionary of the root node in the introspection
567 tree.
568
569 """
570 if callable(reply_handler) and callable(error_handler):
571 # Async call:
572 # Since we get an array of state, and we only care about the first one
573 # we use a lambda to unpack it and get the details we want.
574 backend.introspection_iface.GetState(
575 "/",
576 reply_handler=lambda r: reply_handler(
577 *_get_details_from_state_data(r[0])
578 ),
579 error_handler=error_handler,
580 )
581 elif reply_handler or error_handler:
582 raise ValueError(
583 "Both 'reply_handler' and 'error_handler' must be set."
584 )
585 else:
586 # Sync call
587 state = backend.introspection_iface.GetState("/")[0]
588 return _get_details_from_state_data(state)
589
590
591def _get_details_from_state_data(state_data):
592 """Get details from a state data array.
593
594 Returns class name, path, and state dictionary.
595 """
596 object_path, object_state = state_data
597 return (
598 get_classname_from_path(object_path),
599 object_path.encode('utf-8'),
600 object_state,
601 )
602
603
604def _get_proxy_bases_from_introspection_xml(introspection_xml):
605 """Return tuple of the base classes to use when creating a proxy object.
606
607 Currently this works by looking for certain interface names in the XML. In
608 the future we may want to parse the XML and perform more rigerous checks.
609
610 :param introspection_xml: An xml string that describes the exported object
611 on the dbus backend. This determines which capabilities are present in
612 the backend, and therefore which base classes should be used to create
613 the proxy object.
614
615 :raises RuntimeError: if the autopilot interface cannot be found.
616
617 """
618
619 bases = []
620
621 if constants.AP_INTROSPECTION_IFACE not in introspection_xml:
622 raise RuntimeError("Could not find Autopilot interface.")
623
624 if constants.QT_AUTOPILOT_IFACE in introspection_xml:
625 from autopilot.introspection.qt import QtObjectProxyMixin
626 bases.append(QtObjectProxyMixin)
627
628 return tuple(bases)
629
630
631class ApplicationProxyObject(object):514class ApplicationProxyObject(object):
632 """A class that better supports query data from an application."""515 """A class that better supports query data from an application."""
633516
634517
=== modified file 'autopilot/introspection/backends.py'
--- autopilot/introspection/backends.py 2015-06-09 15:35:08 +0000
+++ autopilot/introspection/backends.py 2015-06-15 09:43:19 +0000
@@ -51,7 +51,7 @@
51 _pid_is_running,51 _pid_is_running,
52 _get_bus_connections_pid,52 _get_bus_connections_pid,
53)53)
54from autopilot.introspection._object_registry import _get_proxy_object_class54from autopilot.introspection import _proxy_objects
5555
5656
57_logger = logging.getLogger(__name__)57_logger = logging.getLogger(__name__)
@@ -245,14 +245,14 @@
245 )245 )
246 return data246 return data
247247
248 def execute_query_get_proxy_instances(self, query, id):248 def execute_query_get_proxy_instances(self, query, cpo_class):
249 """Execute 'query', returning proxy instances."""249 """Execute 'query', returning proxy instances."""
250 data = self.execute_query_get_data(query)250 data = self.execute_query_get_data(query)
251 objects = [251 objects = [
252 make_introspection_object(252 make_introspection_object(
253 t,253 t,
254 type(self)(self.ipc_address),254 type(self)(self.ipc_address),
255 id,255 cpo_class,
256 )256 )
257 for t in data257 for t in data
258 ]258 ]
@@ -288,7 +288,7 @@
288 return self.fake_ipc_return_data288 return self.fake_ipc_return_data
289289
290290
291def make_introspection_object(dbus_tuple, backend, object_id):291def make_introspection_object(dbus_tuple, backend, cpo_class):
292 """Make an introspection object given a DBus tuple of292 """Make an introspection object given a DBus tuple of
293 (path, state_dict).293 (path, state_dict).
294294
@@ -303,7 +303,13 @@
303 """303 """
304 path, state = dbus_tuple304 path, state = dbus_tuple
305 path = path.encode('utf-8')305 path = path.encode('utf-8')
306 class_object = _get_proxy_object_class(object_id, path, state)306 # import ipdb; ipdb.set_trace()
307 # Path seems correct here.
308 class_object = _proxy_objects._get_proxy_object_class(
309 cpo_class,
310 path,
311 backend.ipc_address
312 )
307 return class_object(state, path, backend)313 return class_object(state, path, backend)
308314
309315
310316
=== modified file 'autopilot/introspection/dbus.py'
--- autopilot/introspection/dbus.py 2014-07-31 04:42:08 +0000
+++ autopilot/introspection/dbus.py 2015-06-15 09:43:19 +0000
@@ -30,9 +30,6 @@
30import logging30import logging
3131
32from autopilot.exceptions import StateNotFoundError32from autopilot.exceptions import StateNotFoundError
33from autopilot.introspection._object_registry import (
34 DBusIntrospectionObjectBase,
35)
36from autopilot.introspection.types import create_value_instance33from autopilot.introspection.types import create_value_instance
37from autopilot.introspection.utilities import translate_state_keys34from autopilot.introspection.utilities import translate_state_keys
38from autopilot.introspection import _xpathselect as xpathselect35from autopilot.introspection import _xpathselect as xpathselect
@@ -42,7 +39,7 @@
42_logger = logging.getLogger(__name__)39_logger = logging.getLogger(__name__)
4340
4441
45class DBusIntrospectionObject(DBusIntrospectionObjectBase):42class DBusIntrospectionObject(object):
46 """A class that supports transparent data retrieval from the application43 """A class that supports transparent data retrieval from the application
47 under test.44 under test.
4845
@@ -83,11 +80,15 @@
83 self.id80 self.id
84 )81 )
8582
86 def _execute_query(self, query):83 def _execute_query(self, query, cpo_class=None):
84 # Default to None because it's ok to create a default CPO oh or perhaps
85 # default to a default dbus object?
87 """Execute query object 'query' and return the result."""86 """Execute query object 'query' and return the result."""
87 # vvveebers this will need to be updated to take any CPO passed in.
88 # as well as updating the backend code so that it can handle it.
88 return self._backend.execute_query_get_proxy_instances(89 return self._backend.execute_query_get_proxy_instances(
89 query,90 query,
90 getattr(self, '_id', None),91 cpo_class,
91 )92 )
9293
93 def _set_properties(self, state_dict):94 def _set_properties(self, state_dict):
@@ -146,6 +147,9 @@
146 Tutorial Section :ref:`custom_proxy_classes`147 Tutorial Section :ref:`custom_proxy_classes`
147148
148 """149 """
150 # vvveebers this will need updating (docs, and select_child).
151 # Will def need to pass in the CPO class.
152 # (seems that this will reside in execute_query.
149 new_query = self._query.select_child(153 new_query = self._query.select_child(
150 get_type_name(desired_type),154 get_type_name(desired_type),
151 kwargs155 kwargs
@@ -175,10 +179,13 @@
175 This returns a list of all children. To return only children of a179 This returns a list of all children. To return only children of a
176 specific type, use :meth:`get_children_by_type`. To get objects180 specific type, use :meth:`get_children_by_type`. To get objects
177 further down the introspection tree (i.e.- nodes that may not181 further down the introspection tree (i.e.- nodes that may not
178 necessarily be immeadiate children), use :meth:`select_single` and182 necessarily be immediate children), use :meth:`select_single` and
179 :meth:`select_many`.183 :meth:`select_many`.
180184
181 """185 """
186 # vvveebers this will need a note stating that any objects returned
187 # will be just defaults.
188
182 # Thomi: 2014-03-20: There used to be a call to 'self.refresh_state()'189 # Thomi: 2014-03-20: There used to be a call to 'self.refresh_state()'
183 # here. That's not needed, since the only thing we use is the proxy190 # here. That's not needed, since the only thing we use is the proxy
184 # path, which isn't affected by the current state.191 # path, which isn't affected by the current state.
@@ -192,6 +199,8 @@
192 tree). Then it returns itself.199 tree). Then it returns itself.
193200
194 """201 """
202 # vvveebers This might also be an issue as there will be no record of
203 # what CPO to use.
195 new_query = self._query.select_parent()204 new_query = self._query.select_parent()
196 return self._execute_query(new_query)[0]205 return self._execute_query(new_query)[0]
197206
@@ -237,7 +246,7 @@
237 type_name_str,246 type_name_str,
238 kwargs247 kwargs
239 )248 )
240 instances = self._execute_query(new_query)249 instances = self._execute_query(new_query, type_name)
241 if len(instances) > 1:250 if len(instances) > 1:
242 raise ValueError("More than one item was returned for query")251 raise ValueError("More than one item was returned for query")
243 if not instances:252 if not instances:
@@ -351,7 +360,7 @@
351 'any type'360 'any type'
352 if type_name_str == '*' else 'type ' + type_name_str, kwargs361 if type_name_str == '*' else 'type ' + type_name_str, kwargs
353 )362 )
354 return self._execute_query(new_query)363 return self._execute_query(new_query, type_name)
355364
356 def refresh_state(self):365 def refresh_state(self):
357 """Refreshes the object's state.366 """Refreshes the object's state.
@@ -385,6 +394,11 @@
385 :return: List (possibly empty) of class instances.394 :return: List (possibly empty) of class instances.
386395
387 """396 """
397 # vvveebers this will need a revamp:
398 # - getting cls_name like this will fail for CPOs with different
399 # names.
400 # - this would currently produce a default proxy as the CPO isn't
401 # passed in: either pass self in (or similar) or perhaps 'from_cpo()'.
388 cls_name = type(self).__name__402 cls_name = type(self).__name__
389 return self._execute_query(403 return self._execute_query(
390 xpathselect.Query.whole_tree_search(cls_name)404 xpathselect.Query.whole_tree_search(cls_name)
@@ -397,6 +411,10 @@
397 introspection tree.411 introspection tree.
398412
399 """413 """
414 # vvveebers undecided if this should take a cpo argument so the
415 # returned object can be instatiated instead of a default.
416 # It appears that currently in use that the expectation is that it's a
417 # default.
400 query = xpathselect.Query.pseudo_tree_root()418 query = xpathselect.Query.pseudo_tree_root()
401 return self._execute_query(query)[0]419 return self._execute_query(query)[0]
402420
@@ -410,10 +428,14 @@
410 if self.__refresh_on_attribute:428 if self.__refresh_on_attribute:
411 self.refresh_state()429 self.refresh_state()
412 return self.__state[name]430 return self.__state[name]
431
432 exception_msg = "{}Class '{}' has no attribute '{}'.".format(
433 'Generated ' if hasattr(self.__class__, '__generated') else '',
434 self.__class__.__name__,
435 name
436 )
413 # attribute not found.437 # attribute not found.
414 raise AttributeError(438 raise AttributeError(exception_msg)
415 "Class '%s' has no attribute '%s'." %
416 (self.__class__.__name__, name))
417439
418 def _get_new_state(self):440 def _get_new_state(self):
419 """Retrieve a new state dictionary for this class instance.441 """Retrieve a new state dictionary for this class instance.
@@ -538,12 +560,47 @@
538 :returns: Whether this class is appropriate for the dbus object560 :returns: Whether this class is appropriate for the dbus object
539561
540 """562 """
563
564 # vvveebers this won't be needed with the newer design. Is there
565 # perhaps a need for something along this line so CPOs are created for
566 # the expected bits?
567
541 state_name = xpathselect.get_classname_from_path(path)568 state_name = xpathselect.get_classname_from_path(path)
542 if isinstance(state_name, str):569 if isinstance(state_name, str):
543 state_name = state_name.encode('utf-8')570 state_name = state_name.encode('utf-8')
544 class_name = cls.__name__.encode('utf-8')571 class_name = cls.__name__.encode('utf-8')
545 return state_name == class_name572 return state_name == class_name
546573
574 @classmethod
575 def get_type_query_name(cls):
576 """Return the Type node name to use within the search query.
577
578 This allows for a Custom Proxy Object to be named differently to the
579 underlying node type name.
580
581 For instance if you have a QML type defined in the file RedRect.qml::
582
583 import QtQuick 2.0
584 Rectangle {
585 color: red;
586 }
587
588 You can then define a Custom Proxy Object for this type like so::
589
590 class RedRect(DBusIntrospectionObject):
591 @classmethod
592 def get_type_query_name(cls):
593 return 'QQuickRectangle'
594
595 This is due to the qml engine storing 'RedRect' as a QQuickRectangle in
596 the UI tree and the xpathquery query needs a node type to query for.
597 By default the query will use the class name (in this case RedRect) but
598 this will not match any node type in the tree.
599
600 """
601
602 return cls.__name__
603
547604
548# TODO - can we add a deprecation warning around this somehow?605# TODO - can we add a deprecation warning around this somehow?
549CustomEmulatorBase = DBusIntrospectionObject606CustomEmulatorBase = DBusIntrospectionObject
@@ -557,5 +614,12 @@
557614
558 """615 """
559 if not isinstance(maybe_string_or_class, str):616 if not isinstance(maybe_string_or_class, str):
560 return maybe_string_or_class.__name__617 return _get_class_type_name(maybe_string_or_class)
561 return maybe_string_or_class618 return maybe_string_or_class
619
620
621def _get_class_type_name(maybe_cpo_class):
622 if hasattr(maybe_cpo_class, 'get_type_query_name'):
623 return maybe_cpo_class.get_type_query_name()
624 else:
625 return maybe_cpo_class.__name__
562626
=== modified file 'autopilot/tests/functional/test_dbus_query.py'
--- autopilot/tests/functional/test_dbus_query.py 2015-06-09 15:35:08 +0000
+++ autopilot/tests/functional/test_dbus_query.py 2015-06-15 09:43:19 +0000
@@ -217,6 +217,12 @@
217 self.assertThat(abs(end_time - start_time), GreaterThan(9))217 self.assertThat(abs(end_time - start_time), GreaterThan(9))
218 self.assertThat(abs(end_time - start_time), LessThan(11))218 self.assertThat(abs(end_time - start_time), LessThan(11))
219219
220 def test_selecting_without_classname_sets_proxy_classname(self):
221 app = self.start_fully_featured_app()
222 test_object = app.select_single(windowTitle='Default Window Title')
223
224 self.assertThat(test_object.__class__.__name__, Equals('QMainWindow'))
225
220226
221@skipIf(platform.model() != "Desktop", "Only suitable on Desktop (WinMocker)")227@skipIf(platform.model() != "Desktop", "Only suitable on Desktop (WinMocker)")
222class DbusCustomBusTests(AutopilotTestCase):228class DbusCustomBusTests(AutopilotTestCase):
223229
=== modified file 'autopilot/tests/functional/test_input_stack.py'
--- autopilot/tests/functional/test_input_stack.py 2015-05-08 04:32:51 +0000
+++ autopilot/tests/functional/test_input_stack.py 2015-06-15 09:43:19 +0000
@@ -379,7 +379,7 @@
379 screen_geometry = Display.create().get_screen_geometry(0)379 screen_geometry = Display.create().get_screen_geometry(0)
380 target_x = screen_geometry[0] + 10380 target_x = screen_geometry[0] + 10
381 target_y = screen_geometry[1] + 10.6381 target_y = screen_geometry[1] + 10.6
382 self.device.move(target_x, target_y)382 device.move(target_x, target_y)
383 self.assertEqual(device.position(), (target_x, int(target_y)))383 self.assertEqual(device.position(), (target_x, int(target_y)))
384384
385 @patch('autopilot.platform.model', new=lambda *args: "Not Desktop", )385 @patch('autopilot.platform.model', new=lambda *args: "Not Desktop", )
386386
=== modified file 'autopilot/tests/functional/test_introspection_features.py'
--- autopilot/tests/functional/test_introspection_features.py 2015-04-30 03:34:13 +0000
+++ autopilot/tests/functional/test_introspection_features.py 2015-06-15 09:43:19 +0000
@@ -36,16 +36,15 @@
36 StartsWith,36 StartsWith,
37)37)
38from textwrap import dedent38from textwrap import dedent
39from unittest.mock import patch
40from io import StringIO39from io import StringIO
4140
42from autopilot import platform41from autopilot import platform
43from autopilot.matchers import Eventually42from autopilot.matchers import Eventually
44from autopilot.testcase import AutopilotTestCase43from autopilot.testcase import AutopilotTestCase
44from autopilot.tests.functional import QmlScriptRunnerMixin
45from autopilot.tests.functional.fixtures import TempDesktopFile45from autopilot.tests.functional.fixtures import TempDesktopFile
46from autopilot.introspection import CustomEmulatorBase46from autopilot.introspection import CustomEmulatorBase
47from autopilot.introspection import _object_registry as object_registry47from autopilot.introspection import _object_registry as object_registry
48from autopilot.introspection import _search
49from autopilot.introspection.qt import QtObjectProxyMixin48from autopilot.introspection.qt import QtObjectProxyMixin
50from autopilot.display import Display49from autopilot.display import Display
5150
@@ -93,14 +92,16 @@
93 return path == b'/window-mocker'92 return path == b'/window-mocker'
9493
95 # verify that the initial proxy object we get back is the correct type:94 # verify that the initial proxy object we get back is the correct type:
96 app = self.start_mock_app(EmulatorBase)95 app = self.start_mock_app(WindowMockerApp)
97 self.assertThat(type(app), Equals(WindowMockerApp))96 self.assertThat(type(app), Equals(WindowMockerApp))
9897
99 # verify that we get the correct type from get_root_instance:98 # verify that we get the correct type from get_root_instance:
100 self.assertThat(99 # vvveebers behaviour has changed, get_root_instance currently returns
101 type(app.get_root_instance()),100 # a default cpo.
102 Equals(WindowMockerApp)101 # self.assertThat(
103 )102 # type(app.get_root_instance()),
103 # Equals(WindowMockerApp)
104 # )
104105
105 def test_customised_proxy_classes_have_extension_classes(self):106 def test_customised_proxy_classes_have_extension_classes(self):
106 class WindowMockerApp(EmulatorBase):107 class WindowMockerApp(EmulatorBase):
@@ -112,21 +113,22 @@
112 self.assertThat(app.__class__.__bases__, Contains(QtObjectProxyMixin))113 self.assertThat(app.__class__.__bases__, Contains(QtObjectProxyMixin))
113114
114 def test_customised_proxy_classes_have_multiple_extension_classes(self):115 def test_customised_proxy_classes_have_multiple_extension_classes(self):
115 with object_registry.patch_registry({}):116 # vvveebers this test isn't really testing anything anymore as it's
116 class SecondEmulatorBase(CustomEmulatorBase):117 # handled by python (i.e. class inheritence)
117 pass118 class SecondEmulatorBase(CustomEmulatorBase):
118119 pass
119 class WindowMockerApp(EmulatorBase, SecondEmulatorBase):120
120 @classmethod121 class WindowMockerApp(EmulatorBase, SecondEmulatorBase):
121 def validate_dbus_object(cls, path, _state):122 @classmethod
122 return path == b'/window-mocker'123 def validate_dbus_object(cls, path, _state):
123124 return path == b'/window-mocker'
124 app = self.start_mock_app(EmulatorBase)125
125 self.assertThat(app.__class__.__bases__, Contains(EmulatorBase))126 app = self.start_mock_app(WindowMockerApp)
126 self.assertThat(127 self.assertThat(app.__class__.__bases__, Contains(EmulatorBase))
127 app.__class__.__bases__,128 self.assertThat(
128 Contains(SecondEmulatorBase)129 app.__class__.__bases__,
129 )130 Contains(SecondEmulatorBase)
131 )
130132
131 def test_handles_using_app_cpo_base_class(self):133 def test_handles_using_app_cpo_base_class(self):
132 # This test replicates an issue found in an application test suite134 # This test replicates an issue found in an application test suite
@@ -139,30 +141,16 @@
139141
140 self.start_mock_app(WindowMockerApp)142 self.start_mock_app(WindowMockerApp)
141143
142 def test_warns_when_using_incorrect_cpo_base_class(self):144 def test_selecting_using_string_creates_default_cpo(self):
143 # Ensure the warning method is called when launching a proxy.145 """When not passing CPO must return generated default."""
144 with object_registry.patch_registry({}):
145 class TestCPO(CustomEmulatorBase):
146 pass
147
148 class WindowMockerApp(TestCPO):
149 @classmethod
150 def validate_dbus_object(cls, path, _state):
151 return path == b'/window-mocker'
152
153 with patch.object(_search, 'logger') as p_logger:
154 self.start_mock_app(WindowMockerApp)
155 self.assertTrue(p_logger.warning.called)
156
157 def test_can_select_custom_emulators_by_name(self):
158 """Must be able to select a custom emulator type by name."""
159 class MouseTestWidget(EmulatorBase):146 class MouseTestWidget(EmulatorBase):
160 pass147 pass
161148
162 app = self.start_mock_app(EmulatorBase)149 app = self.start_mock_app(EmulatorBase)
163 test_widget = app.select_single('MouseTestWidget')150 test_widget = app.select_single('MouseTestWidget')
164151
165 self.assertThat(type(test_widget), Equals(MouseTestWidget))152 self.assertThat(type(test_widget), Not(Equals(MouseTestWidget)))
153 self.assertTrue(getattr(test_widget, '__generated'))
166154
167 def test_can_select_custom_emulators_by_type(self):155 def test_can_select_custom_emulators_by_type(self):
168 """Must be able to select a custom emulator type by type."""156 """Must be able to select a custom emulator type by type."""
@@ -397,3 +385,31 @@
397 [e[1] for e in result2.decorated.errors]385 [e[1] for e in result2.decorated.errors]
398 )386 )
399 )387 )
388
389
390class CustomCPOTest(AutopilotTestCase, QmlScriptRunnerMixin):
391
392 def launch_simple_qml_script(self):
393 simple_script = dedent("""
394 import QtQuick 2.0
395 Rectangle {
396 objectName: "ExampleRectangle"
397 }
398 """)
399 return self.start_qml_script(simple_script)
400
401 def test_cpo_can_be_named_different_to_underlying_type(self):
402 """A CPO with the correct name match method must be matched if the
403 class name is different to the Type name.
404
405 """
406 with object_registry.patch_registry({}):
407 class RandomNamedCPORectangle(CustomEmulatorBase):
408 @classmethod
409 def get_type_query_name(cls):
410 return 'QQuickRectangle'
411
412 app = self.launch_simple_qml_script()
413 rectangle = app.select_single(RandomNamedCPORectangle)
414
415 self.assertThat(rectangle.objectName, Equals('ExampleRectangle'))
400416
=== renamed file 'autopilot/tests/unit/test_introspection_object_registry.py' => 'autopilot/tests/unit/introspection_object_registry.py'
=== modified file 'autopilot/tests/unit/test_introspection.py'
--- autopilot/tests/unit/test_introspection.py 2014-07-31 04:42:08 +0000
+++ autopilot/tests/unit/test_introspection.py 2015-06-15 09:43:19 +0000
@@ -25,7 +25,10 @@
25 IsInstance,25 IsInstance,
26 raises,26 raises,
27)27)
28import autopilot.introspection._search as _s28from autopilot.introspection import (
29 _proxy_objects as _po,
30 _search as _s
31)
29from autopilot.introspection.qt import QtObjectProxyMixin32from autopilot.introspection.qt import QtObjectProxyMixin
30import autopilot.introspection as _i33import autopilot.introspection as _i
3134
@@ -35,21 +38,21 @@
35 fake_state_data = (String('/some/path'), dict(foo=123))38 fake_state_data = (String('/some/path'), dict(foo=123))
3639
37 def test_returns_classname(self):40 def test_returns_classname(self):
38 class_name, _, _ = _s._get_details_from_state_data(41 class_name, _, _ = _po._get_details_from_state_data(
39 self.fake_state_data42 self.fake_state_data
40 )43 )
41 self.assertThat(class_name, Equals('path'))44 self.assertThat(class_name, Equals('path'))
4245
43 def test_returns_path(self):46 def test_returns_path(self):
44 _, path, _ = _s._get_details_from_state_data(self.fake_state_data)47 _, path, _ = _po._get_details_from_state_data(self.fake_state_data)
45 self.assertThat(path, Equals(b'/some/path'))48 self.assertThat(path, Equals(b'/some/path'))
4649
47 def test_returned_path_is_bytestring(self):50 def test_returned_path_is_bytestring(self):
48 _, path, _ = _s._get_details_from_state_data(self.fake_state_data)51 _, path, _ = _po._get_details_from_state_data(self.fake_state_data)
49 self.assertThat(path, IsInstance(type(b'')))52 self.assertThat(path, IsInstance(type(b'')))
5053
51 def test_returns_state_dict(self):54 def test_returns_state_dict(self):
52 _, _, state = _s._get_details_from_state_data(self.fake_state_data)55 _, _, state = _po._get_details_from_state_data(self.fake_state_data)
53 self.assertThat(state, Equals(dict(foo=123)))56 self.assertThat(state, Equals(dict(foo=123)))
5457
5558
@@ -119,13 +122,13 @@
119122
120 def test_raises_RuntimeError_when_no_interface_is_found(self):123 def test_raises_RuntimeError_when_no_interface_is_found(self):
121 self.assertThat(124 self.assertThat(
122 lambda: _s._get_proxy_bases_from_introspection_xml(""),125 lambda: _po._get_proxy_bases_from_introspection_xml(""),
123 raises(RuntimeError("Could not find Autopilot interface."))126 raises(RuntimeError("Could not find Autopilot interface."))
124 )127 )
125128
126 def test_returns_ApplicationProxyObject_claws_for_base_interface(self):129 def test_returns_ApplicationProxyObject_claws_for_base_interface(self):
127 self.assertThat(130 self.assertThat(
128 _s._get_proxy_bases_from_introspection_xml(131 _po._get_proxy_bases_from_introspection_xml(
129 self.fake_data_with_ap_interface132 self.fake_data_with_ap_interface
130 ),133 ),
131 Equals(())134 Equals(())
@@ -133,7 +136,7 @@
133136
134 def test_returns_both_base_and_qt_interface(self):137 def test_returns_both_base_and_qt_interface(self):
135 self.assertThat(138 self.assertThat(
136 _s._get_proxy_bases_from_introspection_xml(139 _po._get_proxy_bases_from_introspection_xml(
137 self.fake_data_with_ap_and_qt_interfaces140 self.fake_data_with_ap_and_qt_interfaces
138 ),141 ),
139 Equals((QtObjectProxyMixin,))142 Equals((QtObjectProxyMixin,))
140143
=== modified file 'autopilot/tests/unit/test_introspection_backends.py'
--- autopilot/tests/unit/test_introspection_backends.py 2015-06-09 15:35:08 +0000
+++ autopilot/tests/unit/test_introspection_backends.py 2015-06-15 09:43:19 +0000
@@ -23,6 +23,7 @@
23from testtools.matchers import Equals, Not, NotEquals, IsInstance23from testtools.matchers import Equals, Not, NotEquals, IsInstance
2424
25from autopilot.introspection import (25from autopilot.introspection import (
26 _proxy_objects as _po,
26 _xpathselect as xpathselect,27 _xpathselect as xpathselect,
27 backends,28 backends,
28 dbus,29 dbus,
@@ -325,21 +326,22 @@
325 """Verify that a class has a validation method by default."""326 """Verify that a class has a validation method by default."""
326 self.assertTrue(callable(self.DefaultSelector.validate_dbus_object))327 self.assertTrue(callable(self.DefaultSelector.validate_dbus_object))
327328
328 @patch.object(backends, '_get_proxy_object_class')329 @patch.object(_po, '_get_proxy_object_class')
329 def test_make_introspection_object(self, gpoc):330 def test_make_introspection_object(self, gpoc):
330 """Verify that make_introspection_object makes the right call."""331 """Verify that make_introspection_object makes the right call."""
331 gpoc.return_value = self.DefaultSelector332 gpoc.return_value = self.DefaultSelector
332 fake_id = Mock()333 fake_backend = Mock()
334 cpo_name = self.getUniqueString()
335
333 new_fake = backends.make_introspection_object(336 new_fake = backends.make_introspection_object(
334 (String('/Object'), {'id': [0, 42]}),337 (String('/Object'), {'id': [0, 42]}),
335 None,338 fake_backend,
336 fake_id,339 cpo_name
337 )340 )
338 self.assertThat(new_fake, IsInstance(self.DefaultSelector))341 self.assertThat(new_fake, IsInstance(self.DefaultSelector))
339 gpoc.assert_called_once_with(342 gpoc.assert_called_once_with(
340 fake_id,343 cpo_name,
341 b'/Object',344 fake_backend.ipc_address
342 {'id': [0, 42]}
343 )345 )
344346
345 def test_validate_dbus_object_matches_on_class_name(self):347 def test_validate_dbus_object_matches_on_class_name(self):
346348
=== modified file 'autopilot/tests/unit/test_introspection_dbus.py'
--- autopilot/tests/unit/test_introspection_dbus.py 2014-07-22 02:30:19 +0000
+++ autopilot/tests/unit/test_introspection_dbus.py 2015-06-15 09:43:19 +0000
@@ -29,50 +29,19 @@
29from testtools.matchers import (29from testtools.matchers import (
30 Equals,30 Equals,
31 Not,31 Not,
32 NotEquals,
33 Raises,32 Raises,
34 raises,33 raises,
35)34)
3635
37from autopilot.exceptions import StateNotFoundError36from autopilot.exceptions import StateNotFoundError
38from autopilot.introspection import CustomEmulatorBase37from autopilot.introspection import (
39from autopilot.introspection import dbus38 _proxy_objects as _po,
39 CustomEmulatorBase,
40 dbus
41)
40from autopilot.utilities import sleep42from autopilot.utilities import sleep
4143
4244
43class IntrospectionFeatureTests(TestCase):
44
45 def test_custom_emulator_base_does_not_have_id(self):
46 self.assertThat(hasattr(CustomEmulatorBase, '_id'), Equals(False))
47
48 def test_derived_emulator_bases_do_have_id(self):
49 class MyEmulatorBase(CustomEmulatorBase):
50 pass
51 self.assertThat(hasattr(MyEmulatorBase, '_id'), Equals(True))
52
53 def test_derived_children_have_same_id(self):
54 class MyEmulatorBase(CustomEmulatorBase):
55 pass
56
57 class MyEmulator(MyEmulatorBase):
58 pass
59
60 class MyEmulator2(MyEmulatorBase):
61 pass
62
63 self.assertThat(MyEmulatorBase._id, Equals(MyEmulator._id))
64 self.assertThat(MyEmulatorBase._id, Equals(MyEmulator2._id))
65
66 def test_children_have_different_ids(self):
67 class MyEmulatorBase(CustomEmulatorBase):
68 pass
69
70 class MyEmulatorBase2(CustomEmulatorBase):
71 pass
72
73 self.assertThat(MyEmulatorBase._id, NotEquals(MyEmulatorBase2._id))
74
75
76class DBusIntrospectionObjectTests(TestCase):45class DBusIntrospectionObjectTests(TestCase):
7746
78 def test_can_access_path_attribute(self):47 def test_can_access_path_attribute(self):
@@ -118,6 +87,49 @@
118 ),87 ),
119 )88 )
12089
90 def test_base_class_provides_correct_query_name(self):
91 self.assertThat(
92 dbus.DBusIntrospectionObject.get_type_query_name(),
93 Equals('ProxyBase')
94 )
95
96 def test_inherited_uses_default_get_node_name(self):
97 class TestCPO(dbus.DBusIntrospectionObject):
98 pass
99
100 self.assertThat(
101 TestCPO.get_type_query_name(),
102 Equals('TestCPO')
103 )
104
105 def test_inherited_overwrites_node_name_is_correct(self):
106 class TestCPO(dbus.DBusIntrospectionObject):
107 @classmethod
108 def get_type_query_name(cls):
109 return "TestCPO"
110 self.assertThat(TestCPO.get_type_query_name(), Equals("TestCPO"))
111
112
113class ProxyObjectCreationTests(TestCase):
114 def test_default_class_mentioned_in_attr_error(self):
115 name = self.getUniqueString()
116 fake_class = _po._get_default_proxy_class(name, ())
117 fake_object = fake_class(
118 dict(id=[0, 123], path=[0, '/some/path']),
119 b'/root',
120 Mock()
121 )
122
123 expected_msg = "Generated Class '{}' has no attribute 'foo'.".format(
124 name
125 )
126
127 with fake_object.no_automatic_refreshing():
128 self.assertThat(
129 lambda: fake_object.foo(),
130 raises(AttributeError(expected_msg))
131 )
132
121133
122class ProxyObjectPrintTreeTests(TestCase):134class ProxyObjectPrintTreeTests(TestCase):
123135
@@ -222,3 +234,23 @@
222 class FooBarBaz(object):234 class FooBarBaz(object):
223 pass235 pass
224 self.assertEqual("FooBarBaz", dbus.get_type_name(FooBarBaz))236 self.assertEqual("FooBarBaz", dbus.get_type_name(FooBarBaz))
237
238 def test_get_type_name_returns_classname(self):
239 class CustomCPO(dbus.DBusIntrospectionObject):
240 pass
241
242 type_name = dbus.get_type_name(CustomEmulatorBase)
243 self.assertThat(type_name, Equals('ProxyBase'))
244
245 def test_get_type_name_returns_custom_node_name(self):
246 class CustomCPO(dbus.DBusIntrospectionObject):
247 @classmethod
248 def get_type_query_name(cls):
249 return 'TestingCPO'
250 type_name = dbus.get_type_name(CustomCPO)
251 self.assertThat(type_name, Equals('TestingCPO'))
252
253 def test_get_type_name_returns_classname_of_non_proxybase_classes(self):
254 class Foo(object):
255 pass
256 self.assertEqual('Foo', dbus.get_type_name(Foo))
225257
=== modified file 'autopilot/tests/unit/test_introspection_search.py'
--- autopilot/tests/unit/test_introspection_search.py 2015-05-06 10:00:49 +0000
+++ autopilot/tests/unit/test_introspection_search.py 2015-06-15 09:43:19 +0000
@@ -35,7 +35,6 @@
35from autopilot.utilities import sleep35from autopilot.utilities import sleep
36from autopilot.introspection import _search as _s36from autopilot.introspection import _search as _s
3737
38from autopilot.introspection import CustomEmulatorBase
39from autopilot.introspection.constants import AUTOPILOT_PATH38from autopilot.introspection.constants import AUTOPILOT_PATH
4039
4140
@@ -732,95 +731,3 @@
732 _s._find_matching_connections(bus, lambda *args: True)731 _s._find_matching_connections(bus, lambda *args: True)
733732
734 dedupe.assert_called_once_with(["conn1"], bus)733 dedupe.assert_called_once_with(["conn1"], bus)
735
736
737class ActualBaseClassTests(TestCase):
738
739 def test_dont_raise_passed_base_when_is_only_base(self):
740 class ActualBase(CustomEmulatorBase):
741 pass
742
743 try:
744 _s._raise_if_base_class_not_actually_base(ActualBase)
745 except ValueError:
746 self.fail('Unexpected ValueError exception')
747
748 def test_raises_if_passed_incorrect_base_class(self):
749 class ActualBase(CustomEmulatorBase):
750 pass
751
752 class InheritedCPO(ActualBase):
753 pass
754
755 self.assertRaises(
756 ValueError,
757 _s._raise_if_base_class_not_actually_base,
758 InheritedCPO
759 )
760
761 def test_raises_parent_with_simple_non_ap_multi_inheritance(self):
762 """When mixing in non-customproxy classes must return the base."""
763
764 class ActualBase(CustomEmulatorBase):
765 pass
766
767 class InheritedCPO(ActualBase):
768 pass
769
770 class TrickyOne(object):
771 pass
772
773 class FinalForm(InheritedCPO, TrickyOne):
774 pass
775
776 self.assertRaises(
777 ValueError,
778 _s._raise_if_base_class_not_actually_base,
779 FinalForm
780 )
781
782 def test_raises_parent_with_non_ap_multi_inheritance(self):
783
784 class ActualBase(CustomEmulatorBase):
785 pass
786
787 class InheritedCPO(ActualBase):
788 pass
789
790 class TrickyOne(object):
791 pass
792
793 class FinalForm(TrickyOne, InheritedCPO):
794 pass
795
796 self.assertRaises(
797 ValueError,
798 _s._raise_if_base_class_not_actually_base,
799 FinalForm
800 )
801
802 def test_dont_raise_when_using_default_emulator_base(self):
803 # _make_proxy_object potentially creates a default base.
804 DefaultBase = _s._make_default_emulator_base()
805 try:
806 _s._raise_if_base_class_not_actually_base(DefaultBase)
807 except ValueError:
808 self.fail('Unexpected ValueError exception')
809
810 def test_exception_message_contains_useful_information(self):
811 class ActualBase(CustomEmulatorBase):
812 pass
813
814 class InheritedCPO(ActualBase):
815 pass
816
817 try:
818 _s._raise_if_base_class_not_actually_base(InheritedCPO)
819 except ValueError as err:
820 self.assertEqual(
821 str(err),
822 _s.WRONG_CPO_CLASS_MSG.format(
823 passed=InheritedCPO,
824 actual=ActualBase
825 )
826 )
827734
=== added file 'autopilot/tests/unit/test_proxy_object.py'
--- autopilot/tests/unit/test_proxy_object.py 1970-01-01 00:00:00 +0000
+++ autopilot/tests/unit/test_proxy_object.py 2015-06-15 09:43:19 +0000
@@ -0,0 +1,84 @@
1# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
2#
3# Autopilot Functional Test Tool
4# Copyright (C) 2015 Canonical
5#
6# This program is free software: you can redistribute it and/or modify
7# it under the terms of the GNU General Public License as published by
8# the Free Software Foundation, either version 3 of the License, or
9# (at your option) any later version.
10#
11# This program is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14# GNU General Public License for more details.
15#
16# You should have received a copy of the GNU General Public License
17# along with this program. If not, see <http://www.gnu.org/licenses/>.
18#
19
20from unittest.mock import (
21 Mock,
22 patch
23)
24from testtools import TestCase
25
26from autopilot.introspection import _proxy_objects as _po
27
28
29class GetProxyObjectClassesTests(TestCase):
30 pass
31
32
33class IsCpoClassTests(TestCase):
34
35 def test_returns_False_when_None(self):
36 self.assertFalse(_po._is_cpo_class(None))
37
38 def test_returns_False_when_is_string(self):
39 self.assertFalse(_po._is_cpo_class('foo'))
40
41 def test_returns_True_when_is_class(self):
42 class Foo(): pass
43 self.assertTrue(_po._is_cpo_class(Foo))
44
45class GetApplicableClassNameTests(TestCase):
46
47 @patch.object(_po, '_get_proxy_object_class_name_and_state')
48 def test_queries_dbus_name_unknown(self, po_cs):
49 fake_dbus_backend = Mock()
50 po_cs.return_value = (None, None, None)
51 _po._get_applicable_class_name(None, fake_dbus_backend)
52 po_cs.assert_called_once_with(fake_dbus_backend)
53
54 @patch.object(_po, '_get_proxy_object_class_name_and_state')
55 def test_returns_dbus_name_when_passed_None(self, po_cs):
56 name = self.getUniqueString()
57 po_cs.return_value = (name, None, None)
58 fake_dbus_backend = Mock()
59
60 self.assertEqual(
61 name,
62 _po._get_applicable_class_name(
63 None,
64 fake_dbus_backend
65 )
66 )
67
68 @patch.object(_po, '_get_proxy_object_class_name_and_state')
69 def test_returns_dbus_name_when_passed_star(self, po_cs):
70 name = self.getUniqueString()
71 po_cs.return_value = (name, None, None)
72 fake_dbus_backend = Mock()
73
74 self.assertEqual(
75 name,
76 _po._get_applicable_class_name(
77 '*',
78 fake_dbus_backend
79 )
80 )
81
82 def test_returns_name_of_string(self):
83 name = self.getUniqueString()
84 self.assertEqual(name, _po._get_applicable_class_name(name, None))

Subscribers

People subscribed via source and target branches