Merge lp:~thomir-deactivatedaccount/autopilot/dbus-address-changes into lp:autopilot
- dbus-address-changes
- Merge into trunk
Status: | Merged |
---|---|
Approved by: | Christopher Lee |
Approved revision: | 185 |
Merged at revision: | 180 |
Proposed branch: | lp:~thomir-deactivatedaccount/autopilot/dbus-address-changes |
Merge into: | lp:autopilot |
Diff against target: |
427 lines (+205/-62) 5 files modified
autopilot/introspection/__init__.py (+24/-23) autopilot/introspection/backends.py (+118/-0) autopilot/introspection/dbus.py (+10/-34) autopilot/introspection/qt.py (+1/-5) autopilot/tests/test_backend.py (+52/-0) |
To merge this branch: | bzr merge lp:~thomir-deactivatedaccount/autopilot/dbus-address-changes |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
PS Jenkins bot | continuous-integration | Approve | |
Christopher Lee (community) | Approve | ||
Review via email: mp+161629@code.launchpad.net |
Commit message
Changes to Dbus address backend code to make it easier to get a proxy object from a dbus address.
Description of the change
Several changes to support backing autopilot on to different dbus backend locations. It is now easier to get an autopilot proxy object from a dbus backend - this could even be on a separate machine.
PS Jenkins bot (ps-jenkins) wrote : | # |
- 181. By Thomi Richards
-
Use the DBus address instance in place of service name and object path. Support for dbus introspection interface.
- 182. By Thomi Richards
-
Convert Qt interface.
- 183. By Thomi Richards
-
Fix missing import.
- 184. By Thomi Richards
-
Remove un-used optional parameter.
PS Jenkins bot (ps-jenkins) wrote : | # |
PASSED: Continuous integration, rev:181
http://
Executed test runs:
SUCCESS: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
PS Jenkins bot (ps-jenkins) wrote : | # |
PASSED: Continuous integration, rev:184
http://
Executed test runs:
SUCCESS: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
- 185. By Thomi Richards
-
whitespace fix.
Christopher Lee (veebers) wrote : | # |
Awesome, looks good.
PS Jenkins bot (ps-jenkins) wrote : | # |
PASSED: Continuous integration, rev:185
http://
Executed test runs:
SUCCESS: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
Preview Diff
1 | === modified file 'autopilot/introspection/__init__.py' |
2 | --- autopilot/introspection/__init__.py 2013-04-26 22:41:19 +0000 |
3 | +++ autopilot/introspection/__init__.py 2013-04-30 20:33:25 +0000 |
4 | @@ -24,6 +24,7 @@ |
5 | mechanism, and probably isn't useful to most test authors. |
6 | |
7 | """ |
8 | +from __future__ import absolute_import |
9 | |
10 | import dbus |
11 | import logging |
12 | @@ -32,20 +33,19 @@ |
13 | import os |
14 | |
15 | |
16 | +from autopilot.introspection.backends import DBusAddress |
17 | from autopilot.introspection.constants import ( |
18 | AUTOPILOT_PATH, |
19 | QT_AUTOPILOT_IFACE, |
20 | AP_INTROSPECTION_IFACE, |
21 | - DBUS_INTROSPECTION_IFACE, |
22 | ) |
23 | from autopilot.introspection.dbus import ( |
24 | clear_object_registry, |
25 | DBusIntrospectionObject, |
26 | - object_passes_filters, |
27 | - get_session_bus, |
28 | get_classname_from_path, |
29 | ) |
30 | -from autopilot.utilities import get_debug_logger, addCleanup |
31 | +from autopilot.dbus_handler import get_session_bus |
32 | +from autopilot.utilities import get_debug_logger |
33 | |
34 | |
35 | logger = logging.getLogger(__name__) |
36 | @@ -166,11 +166,6 @@ |
37 | bus_object = session_bus.get_object('org.freedesktop.DBus', '/org/freedesktop/DBus') |
38 | bus_iface = dbus.Interface(bus_object, 'org.freedesktop.DBus') |
39 | |
40 | - # clear the object registry, since it's specific to the dbus service, and we |
41 | - # have just started a new service. We don't want the old types hanging around |
42 | - # in the registry. We need a better method for this however. |
43 | - clear_object_registry() |
44 | - |
45 | logger.info("Looking for autopilot interface for PID %d (and children)", pid) |
46 | # We give the process 10 seconds grace time to get the dbus interface up... |
47 | for i in range(10): |
48 | @@ -214,27 +209,37 @@ |
49 | return result |
50 | |
51 | |
52 | -def make_proxy_object_from_service_name(service_name, obj_path): |
53 | +def make_proxy_object_from_service_name(service_name, obj_path, dbus_addr=None): |
54 | """Returns a root proxy object given a DBus service name.""" |
55 | # parameters can sometimes be dbus.String instances, sometimes QString instances. |
56 | # it's easier to convert them here than at the calling sites. |
57 | service_name = str(service_name) |
58 | obj_path = str(obj_path) |
59 | |
60 | - proxy_bases = get_proxy_object_base_clases(service_name, obj_path) |
61 | - cls_name, cls_state = get_proxy_object_class_name_and_state(service_name, obj_path) |
62 | + if dbus_addr is not None: |
63 | + be = DBusAddress.CustomBus(dbus_addr, service_name, obj_path) |
64 | + else: |
65 | + be = DBusAddress.SessionBus(service_name, obj_path) |
66 | + |
67 | + |
68 | + proxy_bases = get_proxy_object_base_clases(be) |
69 | + cls_name, cls_state = get_proxy_object_class_name_and_state(be) |
70 | |
71 | clsobj = type(str(cls_name), |
72 | proxy_bases, |
73 | - dict(DBUS_SERVICE=service_name, |
74 | - DBUS_OBJECT=obj_path |
75 | + dict(_Backend = be |
76 | ) |
77 | ) |
78 | + |
79 | + # clear the object registry, since it's specific to the dbus service, and we |
80 | + # have just started a new service. We don't want the old types hanging around |
81 | + # in the registry. We need a better method for this however. |
82 | + clear_object_registry(clsobj._Backend) |
83 | proxy = clsobj.get_root_instance() |
84 | return proxy |
85 | |
86 | |
87 | -def get_proxy_object_base_clases(service_name, obj_path): |
88 | +def get_proxy_object_base_clases(backend): |
89 | """Return tuple of the base classes to use when creating a proxy object |
90 | for the given service name & path. |
91 | |
92 | @@ -244,11 +249,9 @@ |
93 | |
94 | bases = [ApplicationProxyObject] |
95 | |
96 | - dbus_object = get_session_bus().get_object(service_name, obj_path) |
97 | - introspection_iface = dbus.Interface(dbus_object, DBUS_INTROSPECTION_IFACE) |
98 | - intro_xml = introspection_iface.Introspect() |
99 | + intro_xml = backend.dbus_introspection_iface.Introspect() |
100 | if AP_INTROSPECTION_IFACE not in intro_xml: |
101 | - raise RuntimeError("Could not find Autopilot interface on service name '%s'" % service_name) |
102 | + raise RuntimeError("Could not find Autopilot interface on DBus backend '%s'" % backend) |
103 | |
104 | if QT_AUTOPILOT_IFACE in intro_xml: |
105 | from autopilot.introspection.qt import QtObjectProxyMixin |
106 | @@ -257,11 +260,9 @@ |
107 | return tuple(bases) |
108 | |
109 | |
110 | -def get_proxy_object_class_name_and_state(service_name, obj_path): |
111 | +def get_proxy_object_class_name_and_state(backend): |
112 | """Return the class name and root state dictionary.""" |
113 | - dbus_object = get_session_bus().get_object(service_name, obj_path) |
114 | - dbus_iface = dbus.Interface(dbus_object, AP_INTROSPECTION_IFACE) |
115 | - object_path, object_state = dbus_iface.GetState("/")[0] |
116 | + object_path, object_state = backend.introspection_iface.GetState("/")[0] |
117 | return get_classname_from_path(object_path), object_state |
118 | |
119 | |
120 | |
121 | === added file 'autopilot/introspection/backends.py' |
122 | --- autopilot/introspection/backends.py 1970-01-01 00:00:00 +0000 |
123 | +++ autopilot/introspection/backends.py 2013-04-30 20:33:25 +0000 |
124 | @@ -0,0 +1,118 @@ |
125 | +# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- |
126 | +# |
127 | +# Autopilot Functional Test Tool |
128 | +# Copyright (C) 2012-2013 Canonical |
129 | +# |
130 | +# This program is free software: you can redistribute it and/or modify |
131 | +# it under the terms of the GNU General Public License as published by |
132 | +# the Free Software Foundation, either version 3 of the License, or |
133 | +# (at your option) any later version. |
134 | +# |
135 | +# This program is distributed in the hope that it will be useful, |
136 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
137 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
138 | +# GNU General Public License for more details. |
139 | +# |
140 | +# You should have received a copy of the GNU General Public License |
141 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
142 | +# |
143 | + |
144 | +"Backend interface for autopilot." |
145 | +from __future__ import absolute_import |
146 | + |
147 | +from dbus._dbus import BusConnection |
148 | +import dbus |
149 | + |
150 | +from autopilot.introspection.constants import ( |
151 | + AP_INTROSPECTION_IFACE, |
152 | + DBUS_INTROSPECTION_IFACE, |
153 | + QT_AUTOPILOT_IFACE, |
154 | + ) |
155 | + |
156 | + |
157 | +class DBusAddress(object): |
158 | + |
159 | + "Store information about an Autopilot dbus backend, from keyword arguments." |
160 | + |
161 | + @staticmethod |
162 | + def SessionBus(connection, object_path): |
163 | + """Construct a DBusAddress that backs on to the session bus.""" |
164 | + return DBusAddress(dbus.SessionBus(), connection, object_path) |
165 | + |
166 | + @staticmethod |
167 | + def SystemBus(connection, object_path): |
168 | + """Construct a DBusAddress that backs on to the system bus.""" |
169 | + return DBusAddress(dbus.SystemBus(), connection, object_path) |
170 | + |
171 | + @staticmethod |
172 | + def CustomBus(bus_address, connection, object_path): |
173 | + """Construct a DBusAddress that backs on to a custom bus. |
174 | + |
175 | + :param bus_address: A string representing the address of the dbus bus to |
176 | + connect to. |
177 | + |
178 | + """ |
179 | + return DBusAddress(BusConnection(bus_address), connection, object_path) |
180 | + |
181 | + def __init__(self, bus, connection, object_path): |
182 | + """Construct a DBusAddress instance. |
183 | + |
184 | + :param bus: A valid DBus bus object. |
185 | + :param connection: A string connection name to look at, or None to search |
186 | + all dbus connections for objects that resemble an autopilot conection. |
187 | + :param object_path: The path to the object that provides the autopilot |
188 | + interface, or None to search for the object. |
189 | + |
190 | + """ |
191 | + # We cannot evaluate kwargs for accuracy now, since this class will be |
192 | + # created at module import time, at which point the bus backend probably |
193 | + # does not exist yet. |
194 | + self._bus = bus |
195 | + self._connection = connection |
196 | + self._object_path = object_path |
197 | + |
198 | + @property |
199 | + def introspection_iface(self): |
200 | + if not isinstance(self._connection, basestring): |
201 | + raise TypeError("Service name must be a string.") |
202 | + if not isinstance(self._object_path, basestring): |
203 | + raise TypeError("Object name must be a string") |
204 | + |
205 | + _debug_proxy_obj = self._bus.get_object(self._connection, self._object_path) |
206 | + return dbus.Interface(_debug_proxy_obj, AP_INTROSPECTION_IFACE) |
207 | + |
208 | + @property |
209 | + def dbus_introspection_iface(self): |
210 | + dbus_object = self._bus.get_object(self._connection, self._object_path) |
211 | + return dbus.Interface(dbus_object, DBUS_INTROSPECTION_IFACE) |
212 | + |
213 | + @property |
214 | + def qt_introspection_iface(self): |
215 | + _debug_proxy_obj = self._bus.get_object(self._connection, self._object_path) |
216 | + return dbus.Interface(_debug_proxy_obj, QT_AUTOPILOT_IFACE) |
217 | + |
218 | + def __hash__(self): |
219 | + return hash((self._bus, self._connection, self._object_path)) |
220 | + |
221 | + def __eq__(self, other): |
222 | + return self._bus == other._bus and \ |
223 | + self._connection == other._connection and \ |
224 | + self._object_path == other._object_path |
225 | + |
226 | + def __ne__(self, other): |
227 | + return self._object_path != other._object_path or \ |
228 | + self._connection != other._connection or \ |
229 | + self._bus != other._bus |
230 | + |
231 | + def __str__(self): |
232 | + return repr(self) |
233 | + |
234 | + def __repr__(self): |
235 | + if self._bus_type == dbus.Bus.TYPE_SESSION: |
236 | + name = "session" |
237 | + elif self._bus_type == dbus.Bus.TYPE_SYSTEM: |
238 | + name = "system" |
239 | + else: |
240 | + name = "custom" |
241 | + return "<%s bus %s %s>" % (name, self._connection, self._object_path) |
242 | + |
243 | |
244 | === modified file 'autopilot/introspection/dbus.py' |
245 | --- autopilot/introspection/dbus.py 2013-04-30 00:04:23 +0000 |
246 | +++ autopilot/introspection/dbus.py 2013-04-30 20:33:25 +0000 |
247 | @@ -34,7 +34,6 @@ |
248 | from time import sleep |
249 | from textwrap import dedent |
250 | |
251 | -from autopilot.dbus_handler import get_session_bus |
252 | from autopilot.introspection.constants import AP_INTROSPECTION_IFACE |
253 | from autopilot.utilities import Timer |
254 | |
255 | @@ -58,16 +57,15 @@ |
256 | def __new__(cls, classname, bases, classdict): |
257 | """Add class name to type registry.""" |
258 | class_object = type.__new__(cls, classname, bases, classdict) |
259 | - _object_registry[classname] = class_object |
260 | + # The DBusIntrospectionObject class always has Backend == None, since it's |
261 | + # not introspectable itself. We need to compensate for this... |
262 | + if class_object._Backend is not None: |
263 | + _object_registry[class_object._Backend] = {classname:class_object} |
264 | return class_object |
265 | |
266 | |
267 | -def clear_object_registry(): |
268 | - """Clear the object registry. |
269 | - |
270 | - .. important:: DO NOT CALL THIS UNLESS YOU REALLY KNOW WHAT YOU ARE DOING! |
271 | - ... and even then, are you *sure*? |
272 | - """ |
273 | +def clear_object_registry(target_backend): |
274 | + """Delete all class objects from the object registry for a single backend.""" |
275 | global _object_registry |
276 | |
277 | # NOTE: We used to do '_object_registry.clear()' here, but that causes issues |
278 | @@ -80,31 +78,13 @@ |
279 | # - Thomi Richards |
280 | to_delete = [] |
281 | for k,v in _object_registry.iteritems(): |
282 | - if v.DBUS_SERVICE != "com.canonical.Unity": |
283 | + if k == target_backend: |
284 | to_delete.append(k) |
285 | |
286 | for k in to_delete: |
287 | del _object_registry[k] |
288 | |
289 | |
290 | -def get_introspection_iface(service_name, object_path): |
291 | - """Get the autopilot introspection interface for the specified service name |
292 | - and object path. |
293 | - |
294 | - :param string service_name: |
295 | - :param string object_name: |
296 | - :raises: **TypeError** on invalid parameter type |
297 | - |
298 | - """ |
299 | - if not isinstance(service_name, basestring): |
300 | - raise TypeError("Service name must be a string.") |
301 | - if not isinstance(object_path, basestring): |
302 | - raise TypeError("Object name must be a string") |
303 | - |
304 | - _debug_proxy_obj = get_session_bus().get_object(service_name, object_path) |
305 | - return Interface(_debug_proxy_obj, AP_INTROSPECTION_IFACE) |
306 | - |
307 | - |
308 | def translate_state_keys(state_dict): |
309 | """Translates the *state_dict* passed in so the keys are usable as python attributes.""" |
310 | return {k.replace('-','_'):v for k,v in state_dict.iteritems() } |
311 | @@ -137,8 +117,7 @@ |
312 | |
313 | __metaclass__ = IntrospectableObjectMetaclass |
314 | |
315 | - DBUS_SERVICE = None |
316 | - DBUS_OBJECT = None |
317 | + _Backend = None |
318 | |
319 | def __init__(self, state_dict, path): |
320 | self.__state = {} |
321 | @@ -443,10 +422,7 @@ |
322 | raise TypeError("XPath query must be a string, not %r", type(piece)) |
323 | |
324 | with Timer("GetState %s" % piece): |
325 | - return get_introspection_iface( |
326 | - cls.DBUS_SERVICE, |
327 | - cls.DBUS_OBJECT |
328 | - ).GetState(piece) |
329 | + return cls._Backend.introspection_iface.GetState(piece) |
330 | |
331 | def get_new_state(self): |
332 | """Retrieve a new state dictionary for this class instance. |
333 | @@ -474,7 +450,7 @@ |
334 | path, state = dbus_tuple |
335 | name = get_classname_from_path(path) |
336 | try: |
337 | - class_type = _object_registry[name] |
338 | + class_type = _object_registry[cls._Backend][name] |
339 | except KeyError: |
340 | logger.warning("Generating introspection instance for type '%s' based on generic class.", name) |
341 | class_type = type(str(name), (cls,), {}) |
342 | |
343 | === modified file 'autopilot/introspection/qt.py' |
344 | --- autopilot/introspection/qt.py 2013-04-23 04:15:54 +0000 |
345 | +++ autopilot/introspection/qt.py 2013-04-30 20:33:25 +0000 |
346 | @@ -23,14 +23,11 @@ |
347 | |
348 | __all__ = ['QtApplicationLauncher'] |
349 | |
350 | -import dbus |
351 | import functools |
352 | |
353 | import logging |
354 | |
355 | from autopilot.introspection import ApplicationLauncher |
356 | -from autopilot.introspection.constants import QT_AUTOPILOT_IFACE |
357 | -from autopilot.introspection.dbus import get_session_bus |
358 | |
359 | |
360 | logger = logging.getLogger(__name__) |
361 | @@ -102,8 +99,7 @@ |
362 | |
363 | """ |
364 | |
365 | - _debug_proxy_obj = get_session_bus().get_object(self.DBUS_SERVICE, self.DBUS_OBJECT) |
366 | - return dbus.Interface(_debug_proxy_obj, QT_AUTOPILOT_IFACE) |
367 | + return self._Backend.qt_introspection_iface |
368 | |
369 | @property |
370 | def slots(self): |
371 | |
372 | === added file 'autopilot/tests/test_backend.py' |
373 | --- autopilot/tests/test_backend.py 1970-01-01 00:00:00 +0000 |
374 | +++ autopilot/tests/test_backend.py 2013-04-30 20:33:25 +0000 |
375 | @@ -0,0 +1,52 @@ |
376 | +# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- |
377 | +# |
378 | +# Autopilot Functional Test Tool |
379 | +# Copyright (C) 2012-2013 Canonical |
380 | +# |
381 | +# This program is free software: you can redistribute it and/or modify |
382 | +# it under the terms of the GNU General Public License as published by |
383 | +# the Free Software Foundation, either version 3 of the License, or |
384 | +# (at your option) any later version. |
385 | +# |
386 | +# This program is distributed in the hope that it will be useful, |
387 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
388 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
389 | +# GNU General Public License for more details. |
390 | +# |
391 | +# You should have received a copy of the GNU General Public License |
392 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
393 | +# |
394 | + |
395 | + |
396 | +from testtools import TestCase |
397 | +from testtools.matchers import Equals, Not, NotEquals |
398 | +from autopilot.introspection.backends import DBusAddress |
399 | + |
400 | +class DBusAddressTests(TestCase): |
401 | + |
402 | + def test_can_construct(self): |
403 | + fake_bus = object() |
404 | + addr = DBusAddress(fake_bus, "conn", "path") |
405 | + |
406 | + def test_can_store_address_in_dictionary(self): |
407 | + fake_bus = object() |
408 | + addr = DBusAddress(fake_bus, "conn", "path") |
409 | + dict(addr=object()) |
410 | + |
411 | + def test_equality_operator(self): |
412 | + fake_bus = object() |
413 | + addr1 = DBusAddress(fake_bus, "conn", "path") |
414 | + |
415 | + self.assertThat(addr1, Equals(DBusAddress(fake_bus, "conn", "path"))) |
416 | + self.assertThat(addr1, Not(Equals(DBusAddress(fake_bus, "conn", "new_path")))) |
417 | + self.assertThat(addr1, Not(Equals(DBusAddress(fake_bus, "conn2", "path")))) |
418 | + self.assertThat(addr1, Not(Equals(DBusAddress(object(), "conn", "path")))) |
419 | + |
420 | + def test_inequality_operator(self): |
421 | + fake_bus = object() |
422 | + addr1 = DBusAddress(fake_bus, "conn", "path") |
423 | + |
424 | + self.assertThat(addr1, Not(NotEquals(DBusAddress(fake_bus, "conn", "path")))) |
425 | + self.assertThat(addr1, NotEquals(DBusAddress(fake_bus, "conn", "new_path"))) |
426 | + self.assertThat(addr1, NotEquals(DBusAddress(fake_bus, "conn2", "path"))) |
427 | + self.assertThat(addr1, NotEquals(DBusAddress(object(), "conn", "path"))) |
PASSED: Continuous integration, rev:180 jenkins. qa.ubuntu. com/job/ autopilot- ci/46/ jenkins. qa.ubuntu. com/job/ autopilot- raring- amd64-ci/ 46 jenkins. qa.ubuntu. com/job/ autopilot- raring- armhf-ci/ 45
http://
Executed test runs:
SUCCESS: http://
SUCCESS: http://
Click here to trigger a rebuild: s-jenkins: 8080/job/ autopilot- ci/46/rebuild
http://