Merge lp:~nataliabidart/ubuntuone-control-panel/handle-sd-timeout into lp:ubuntuone-control-panel
- handle-sd-timeout
- Merge into trunk
Status: | Merged | ||||
---|---|---|---|---|---|
Approved by: | Natalia Bidart | ||||
Approved revision: | 28 | ||||
Merged at revision: | 27 | ||||
Proposed branch: | lp:~nataliabidart/ubuntuone-control-panel/handle-sd-timeout | ||||
Merge into: | lp:ubuntuone-control-panel | ||||
Diff against target: |
941 lines (+309/-136) 17 files modified
ubuntuone/controlpanel/backend.py (+1/-0) ubuntuone/controlpanel/dbus_client.py (+8/-0) ubuntuone/controlpanel/dbus_service.py (+66/-10) ubuntuone/controlpanel/gtk/tests/__init__.py (+0/-16) ubuntuone/controlpanel/gtk/tests/test_gui.py (+2/-3) ubuntuone/controlpanel/gtk/tests/test_widgets.py (+7/-6) ubuntuone/controlpanel/gtk/widgets.py (+1/-0) ubuntuone/controlpanel/integrationtests/__init__.py (+1/-0) ubuntuone/controlpanel/integrationtests/test_dbus_client_sso.py (+1/-0) ubuntuone/controlpanel/integrationtests/test_dbus_service.py (+82/-4) ubuntuone/controlpanel/integrationtests/test_webclient.py (+2/-1) ubuntuone/controlpanel/tests/__init__.py (+17/-0) ubuntuone/controlpanel/tests/test_backend.py (+24/-35) ubuntuone/controlpanel/tests/test_utils.py (+62/-2) ubuntuone/controlpanel/tests/testcase.py (+0/-54) ubuntuone/controlpanel/utils.py (+29/-0) ubuntuone/controlpanel/webclient.py (+6/-5) |
||||
To merge this branch: | bzr merge lp:~nataliabidart/ubuntuone-control-panel/handle-sd-timeout | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
John Lenton (community) | Approve | ||
Review via email: mp+42549@code.launchpad.net |
Commit message
Handling properly any failure between dbus calls and errbacking deferreds (LP: #683760).
Description of the change
When passing DBus error signals to deferred's errback, we need to transform the twisted Failures into string-string dicts. Otherwise, we get traces like:
Unhandled error in Deferred:
Traceback (most recent call last):
File "/usr/lib/
_inlineCall
File "/usr/lib/
deferred.
File "/usr/lib/
self.
File "/usr/lib/
self.
--- <exception caught here> ---
File "/usr/lib/
self.result = callback(
File "/usr/lib/
message.
exceptions.
To reproduce:
* Temporally make syncdaemon dbus service unavailable (edit /usr/share/
* from trunk, run:
in terminal 1) PYTHONPATH=. ./bin/ubuntuone
in terminal 2) PYTHONPATH=. ./bin/ubuntuone
* you'll see the trace I pasted above on terminal 1.
From this branch, reproduce the test case and this time you'll get, on terminal 1:
ERROR:ubuntuone
Preview Diff
1 | === modified file 'ubuntuone/controlpanel/backend.py' |
2 | --- ubuntuone/controlpanel/backend.py 2010-11-17 21:28:55 +0000 |
3 | +++ ubuntuone/controlpanel/backend.py 2010-12-02 20:41:22 +0000 |
4 | @@ -1,6 +1,7 @@ |
5 | # -*- coding: utf-8 -*- |
6 | |
7 | # Authors: Alejandro J. Cura <alecu@canonical.com> |
8 | +# Authors: Natalia B. Bidart <nataliabidart@canonical.com> |
9 | # |
10 | # Copyright 2010 Canonical Ltd. |
11 | # |
12 | |
13 | === modified file 'ubuntuone/controlpanel/dbus_client.py' |
14 | --- ubuntuone/controlpanel/dbus_client.py 2010-11-17 21:28:55 +0000 |
15 | +++ ubuntuone/controlpanel/dbus_client.py 2010-12-02 20:41:22 +0000 |
16 | @@ -1,6 +1,7 @@ |
17 | # -*- coding: utf-8 -*- |
18 | |
19 | # Authors: Alejandro J. Cura <alecu@canonical.com> |
20 | +# Authors: Natalia B. Bidart <nataliabidart@canonical.com> |
21 | # |
22 | # Copyright 2010 Canonical Ltd. |
23 | # |
24 | @@ -26,6 +27,11 @@ |
25 | from ubuntuone.clientdefs import APP_NAME |
26 | from ubuntuone.syncdaemon import dbus_interface as sd_dbus_iface |
27 | |
28 | +from ubuntuone.controlpanel.logger import setup_logging |
29 | + |
30 | + |
31 | +logger = setup_logging('dbus_client') |
32 | + |
33 | |
34 | class CredentialsError(Exception): |
35 | """No credentials could be retrieved.""" |
36 | @@ -78,6 +84,8 @@ |
37 | |
38 | def get_syncdaemon_proxy(object_path, dbus_interface): |
39 | """Get a DBus proxy for syncdaemon at 'object_path':'dbus_interface'.""" |
40 | + logger.debug('get_syncdaemon_proxy: object_path %r, dbus_interface %r', |
41 | + object_path, dbus_interface) |
42 | bus = dbus.SessionBus() |
43 | obj = bus.get_object(bus_name=sd_dbus_iface.DBUS_IFACE_NAME, |
44 | object_path=object_path, |
45 | |
46 | === modified file 'ubuntuone/controlpanel/dbus_service.py' |
47 | --- ubuntuone/controlpanel/dbus_service.py 2010-11-15 21:05:30 +0000 |
48 | +++ ubuntuone/controlpanel/dbus_service.py 2010-12-02 20:41:22 +0000 |
49 | @@ -1,6 +1,7 @@ |
50 | # -*- coding: utf-8 -*- |
51 | |
52 | # Authors: Alejandro J. Cura <alecu@canonical.com> |
53 | +# Authors: Natalia B. Bidart <nataliabidart@canonical.com> |
54 | # |
55 | # Copyright 2010 Canonical Ltd. |
56 | # |
57 | @@ -24,9 +25,61 @@ |
58 | from dbus.mainloop.glib import DBusGMainLoop |
59 | from dbus.service import method, signal |
60 | |
61 | +from twisted.python.failure import Failure |
62 | + |
63 | from ubuntuone.controlpanel import (DBUS_BUS_NAME, DBUS_PREFERENCES_PATH, |
64 | - DBUS_PREFERENCES_IFACE) |
65 | + DBUS_PREFERENCES_IFACE, utils) |
66 | from ubuntuone.controlpanel.backend import ControlBackend |
67 | +from ubuntuone.controlpanel.logger import setup_logging |
68 | + |
69 | + |
70 | +logger = setup_logging('dbus_service') |
71 | + |
72 | + |
73 | +def make_unicode(anything): |
74 | + """Transform 'anything' on an unicode.""" |
75 | + if not isinstance(anything, unicode): |
76 | + anything = str(anything).decode('utf8', 'replace') |
77 | + |
78 | + return anything |
79 | + |
80 | + |
81 | +def error_handler(error): |
82 | + """Handle 'error' properly to be able to call a dbus error signal. |
83 | + If 'error' is a Failure, then transform the exception in it to a error |
84 | + dict. If 'error' is a regular Exception, transform it. |
85 | + |
86 | + If 'error' is already a string-string dict, just pass it along. Build a |
87 | + generic error dict in any other case. |
88 | + |
89 | + """ |
90 | + result = {} |
91 | + if isinstance(error, Failure): |
92 | + result = utils.failure_to_error_dict(error) |
93 | + elif isinstance(error, Exception): |
94 | + result = utils.exception_to_error_dict(error) |
95 | + elif isinstance(error, dict): |
96 | + # ensure that both keys and values are unicodes |
97 | + result = dict(map(make_unicode, i) for i in error.iteritems()) |
98 | + else: |
99 | + msg = 'Got unexpected error argument %r' % error |
100 | + result = {utils.ERROR_TYPE: 'UnknownError', utils.ERROR_MESSAGE: msg} |
101 | + |
102 | + return result |
103 | + |
104 | + |
105 | +def transform_failure(f): |
106 | + """Decorator to apply to DBus error signals. |
107 | + |
108 | + With this call, a Failure is transformed into a string-string dict. |
109 | + |
110 | + """ |
111 | + def inner(error, *a): |
112 | + """Do the Failure transformation.""" |
113 | + error_dict = error_handler(error) |
114 | + return f(error_dict) |
115 | + |
116 | + return inner |
117 | |
118 | |
119 | class ControlPanelBackend(dbus.service.Object): |
120 | @@ -36,6 +89,7 @@ |
121 | """Create this instance of the backend.""" |
122 | super(ControlPanelBackend, self).__init__(*args, **kwargs) |
123 | self.backend = backend |
124 | + logger.debug('ControlPanelBackend created with %r, %r', args, kwargs) |
125 | |
126 | # pylint: disable=C0103 |
127 | |
128 | @@ -44,7 +98,7 @@ |
129 | """Find out the account info for the current logged in user.""" |
130 | d = self.backend.account_info() |
131 | d.addCallback(self.AccountInfoReady) |
132 | - d.addErrback(self.AccountInfoError) |
133 | + d.addErrback(transform_failure(self.AccountInfoError)) |
134 | |
135 | @signal(dbus_interface=DBUS_PREFERENCES_IFACE, signature="a{ss}") |
136 | def AccountInfoReady(self, info): |
137 | @@ -61,7 +115,7 @@ |
138 | """Find out the devices info for the logged in user.""" |
139 | d = self.backend.devices_info() |
140 | d.addCallback(self.DevicesInfoReady) |
141 | - d.addErrback(self.DevicesInfoError) |
142 | + d.addErrback(transform_failure(self.DevicesInfoError)) |
143 | |
144 | @signal(dbus_interface=DBUS_PREFERENCES_IFACE, signature="aa{ss}") |
145 | def DevicesInfoReady(self, info): |
146 | @@ -78,7 +132,7 @@ |
147 | """Configure a given device.""" |
148 | d = self.backend.change_device_settings(token, settings) |
149 | d.addCallback(self.DeviceSettingsChanged) |
150 | - d.addErrback(self.DeviceSettingsChangeError) |
151 | + d.addErrback(transform_failure(self.DeviceSettingsChangeError)) |
152 | |
153 | @signal(dbus_interface=DBUS_PREFERENCES_IFACE, signature="s") |
154 | def DeviceSettingsChanged(self, token): |
155 | @@ -95,7 +149,7 @@ |
156 | """Remove a given device.""" |
157 | d = self.backend.remove_device(token) |
158 | d.addCallback(self.DeviceRemoved) |
159 | - d.addErrback(self.DeviceRemovalError) |
160 | + d.addErrback(transform_failure(self.DeviceRemovalError)) |
161 | |
162 | @signal(dbus_interface=DBUS_PREFERENCES_IFACE, signature="s") |
163 | def DeviceRemoved(self, token): |
164 | @@ -112,7 +166,7 @@ |
165 | """Get the status of the file sync service.""" |
166 | d = self.backend.file_sync_status() |
167 | d.addCallback(lambda args: self.FileSyncStatusReady(*args)) |
168 | - d.addErrback(self.FileSyncStatusError) |
169 | + d.addErrback(transform_failure(self.FileSyncStatusError)) |
170 | |
171 | @signal(dbus_interface=DBUS_PREFERENCES_IFACE, signature="bs") |
172 | def FileSyncStatusReady(self, enabled, status): |
173 | @@ -129,12 +183,14 @@ |
174 | """Find out the volumes info for the logged in user.""" |
175 | d = self.backend.volumes_info() |
176 | d.addCallback(self.VolumesInfoReady) |
177 | - d.addErrback(self.VolumesInfoError) |
178 | + d.addErrback(transform_failure(self.VolumesInfoError)) |
179 | |
180 | + @utils.log_call(logger.info) |
181 | @signal(dbus_interface=DBUS_PREFERENCES_IFACE, signature="aa{ss}") |
182 | def VolumesInfoReady(self, info): |
183 | """The info for the volumes is available right now.""" |
184 | |
185 | + @utils.log_call(logger.error) |
186 | @signal(dbus_interface=DBUS_PREFERENCES_IFACE, signature="a{ss}") |
187 | def VolumesInfoError(self, error): |
188 | """The info for the volumes is currently unavailable.""" |
189 | @@ -146,7 +202,7 @@ |
190 | """Configure a given volume.""" |
191 | d = self.backend.change_volume_settings(volume_id, settings) |
192 | d.addCallback(self.VolumeSettingsChanged) |
193 | - d.addErrback(self.VolumeSettingsChangeError) |
194 | + d.addErrback(transform_failure(self.VolumeSettingsChangeError)) |
195 | |
196 | @signal(dbus_interface=DBUS_PREFERENCES_IFACE, signature="s") |
197 | def VolumeSettingsChanged(self, token): |
198 | @@ -163,7 +219,7 @@ |
199 | """Check if the extension to sync bookmarks is installed.""" |
200 | d = self.backend.query_bookmark_extension() |
201 | d.addCallback(self.QueryBookmarksResult) |
202 | - d.addErrback(self.QueryBookmarksError) |
203 | + d.addErrback(transform_failure(self.QueryBookmarksError)) |
204 | |
205 | @signal(dbus_interface=DBUS_PREFERENCES_IFACE, signature="b") |
206 | def QueryBookmarksResult(self, enabled): |
207 | @@ -180,7 +236,7 @@ |
208 | """Install the extension to sync bookmarks.""" |
209 | d = self.backend.install_bookmarks_extension() |
210 | d.addCallback(lambda _: self.InstallBookmarksSuccess()) |
211 | - d.addErrback(self.InstallBookmarksError) |
212 | + d.addErrback(transform_failure(self.InstallBookmarksError)) |
213 | |
214 | @signal(dbus_interface=DBUS_PREFERENCES_IFACE, signature="") |
215 | def InstallBookmarksSuccess(self): |
216 | |
217 | === modified file 'ubuntuone/controlpanel/gtk/tests/__init__.py' |
218 | --- ubuntuone/controlpanel/gtk/tests/__init__.py 2010-11-30 19:51:01 +0000 |
219 | +++ ubuntuone/controlpanel/gtk/tests/__init__.py 2010-12-02 20:41:22 +0000 |
220 | @@ -17,19 +17,3 @@ |
221 | # with this program. If not, see <http://www.gnu.org/licenses/>. |
222 | |
223 | """The test suite for the GTK UI for the control panel for Ubuntu One.""" |
224 | - |
225 | -from twisted.trial.unittest import TestCase |
226 | - |
227 | - |
228 | -class BaseTestCase(TestCase): |
229 | - """Basics for testing.""" |
230 | - |
231 | - def setUp(self): |
232 | - self._called = False |
233 | - |
234 | - def tearDown(self): |
235 | - self._called = False |
236 | - |
237 | - def _set_called(self, *args, **kwargs): |
238 | - """Store 'args' and 'kwargs' for test assertions.""" |
239 | - self._called = (args, kwargs) |
240 | |
241 | === modified file 'ubuntuone/controlpanel/gtk/tests/test_gui.py' |
242 | --- ubuntuone/controlpanel/gtk/tests/test_gui.py 2010-11-30 20:13:10 +0000 |
243 | +++ ubuntuone/controlpanel/gtk/tests/test_gui.py 2010-12-02 20:41:22 +0000 |
244 | @@ -27,8 +27,7 @@ |
245 | from ubuntuone.devtools.handlers import MementoHandler |
246 | |
247 | from ubuntuone.controlpanel.gtk import gui |
248 | -from ubuntuone.controlpanel.tests import TOKEN |
249 | -from ubuntuone.controlpanel.gtk import tests |
250 | +from ubuntuone.controlpanel.tests import TOKEN, TestCase |
251 | |
252 | # Attribute 'yyy' defined outside __init__ |
253 | # pylint: disable=W0201 |
254 | @@ -133,7 +132,7 @@ |
255 | return FakedSSOBackend(obj, dbus_interface, *args, **kwargs) |
256 | |
257 | |
258 | -class BaseTestCase(tests.BaseTestCase): |
259 | +class BaseTestCase(TestCase): |
260 | """Basics for testing.""" |
261 | |
262 | # self.klass is not callable |
263 | |
264 | === modified file 'ubuntuone/controlpanel/gtk/tests/test_widgets.py' |
265 | --- ubuntuone/controlpanel/gtk/tests/test_widgets.py 2010-11-30 20:36:38 +0000 |
266 | +++ ubuntuone/controlpanel/gtk/tests/test_widgets.py 2010-12-02 20:41:22 +0000 |
267 | @@ -18,10 +18,11 @@ |
268 | |
269 | """The test suite for the extra widgets.""" |
270 | |
271 | -from ubuntuone.controlpanel.gtk import widgets, tests |
272 | - |
273 | - |
274 | -class LoadingTestCase(tests.BaseTestCase): |
275 | +from ubuntuone.controlpanel.tests import TestCase |
276 | +from ubuntuone.controlpanel.gtk import widgets |
277 | + |
278 | + |
279 | +class LoadingTestCase(TestCase): |
280 | """Test suite for the Loading widget (a spinner plus a label).""" |
281 | |
282 | def setUp(self): |
283 | @@ -59,7 +60,7 @@ |
284 | widgets.gtk.gdk.Color(expected)) |
285 | |
286 | |
287 | -class LabelLoadingTestCase(tests.BaseTestCase): |
288 | +class LabelLoadingTestCase(TestCase): |
289 | """Test suite for the LabelLoading widget. |
290 | |
291 | (a label that shows a Loading until the text is set). |
292 | @@ -155,7 +156,7 @@ |
293 | self.assertEqual(actual, widgets.gtk.gdk.Color(expected)) |
294 | |
295 | |
296 | -class PanelTitleTestCase(tests.BaseTestCase): |
297 | +class PanelTitleTestCase(TestCase): |
298 | """Tets case for a special title for each management panel.""" |
299 | |
300 | TITLE = '<b>Foo Bar</b>' |
301 | |
302 | === modified file 'ubuntuone/controlpanel/gtk/widgets.py' |
303 | --- ubuntuone/controlpanel/gtk/widgets.py 2010-11-30 20:36:38 +0000 |
304 | +++ ubuntuone/controlpanel/gtk/widgets.py 2010-12-02 20:41:22 +0000 |
305 | @@ -1,5 +1,6 @@ |
306 | # -*- coding: utf-8 -*- |
307 | |
308 | +# Authors: Natalia B. Bidart <nataliabidart@canonical.com> |
309 | # Authors: Evan Dandrea <evan.dandrea@canonical.com> |
310 | # |
311 | # Copyright 2009-2010 Canonical Ltd. |
312 | |
313 | === modified file 'ubuntuone/controlpanel/integrationtests/__init__.py' |
314 | --- ubuntuone/controlpanel/integrationtests/__init__.py 2010-11-17 21:28:55 +0000 |
315 | +++ ubuntuone/controlpanel/integrationtests/__init__.py 2010-12-02 20:41:22 +0000 |
316 | @@ -1,5 +1,6 @@ |
317 | # -*- coding: utf-8 -*- |
318 | |
319 | +# Authors: Natalia B. Bidart <nataliabidart@canonical.com> |
320 | # Authors: Alejandro J. Cura <alecu@canonical.com> |
321 | # |
322 | # Copyright 2010 Canonical Ltd. |
323 | |
324 | === modified file 'ubuntuone/controlpanel/integrationtests/test_dbus_client_sso.py' |
325 | --- ubuntuone/controlpanel/integrationtests/test_dbus_client_sso.py 2010-11-15 16:00:23 +0000 |
326 | +++ ubuntuone/controlpanel/integrationtests/test_dbus_client_sso.py 2010-12-02 20:41:22 +0000 |
327 | @@ -1,6 +1,7 @@ |
328 | # -*- coding: utf-8 -*- |
329 | |
330 | # Authors: Alejandro J. Cura <alecu@canonical.com> |
331 | +# Authors: Natalia B. Bidart <nataliabidart@canonical.com> |
332 | # |
333 | # Copyright 2010 Canonical Ltd. |
334 | # |
335 | |
336 | === modified file 'ubuntuone/controlpanel/integrationtests/test_dbus_service.py' |
337 | --- ubuntuone/controlpanel/integrationtests/test_dbus_service.py 2010-11-17 22:05:32 +0000 |
338 | +++ ubuntuone/controlpanel/integrationtests/test_dbus_service.py 2010-12-02 20:41:22 +0000 |
339 | @@ -1,6 +1,7 @@ |
340 | # -*- coding: utf-8 -*- |
341 | |
342 | # Authors: Alejandro J. Cura <alecu@canonical.com> |
343 | +# Authors: Natalia B. Bidart <nataliabidart@canonical.com> |
344 | # |
345 | # Copyright 2010 Canonical Ltd. |
346 | # |
347 | @@ -23,11 +24,11 @@ |
348 | |
349 | from twisted.internet import defer |
350 | from twisted.python.failure import Failure |
351 | -from ubuntuone.devtools.testcase import DBusTestCase as TestCase |
352 | |
353 | from ubuntuone.controlpanel import dbus_service |
354 | from ubuntuone.controlpanel import (DBUS_BUS_NAME, DBUS_PREFERENCES_PATH, |
355 | DBUS_PREFERENCES_IFACE) |
356 | +from ubuntuone.controlpanel.integrationtests import TestCase |
357 | |
358 | |
359 | SAMPLE_ACCOUNT_INFO = { |
360 | @@ -120,7 +121,7 @@ |
361 | """Process the request with the given result.""" |
362 | if self.exception: |
363 | # pylint: disable=E1102 |
364 | - return defer.fail(self.exception()) |
365 | + return defer.fail(self.exception(result)) |
366 | return defer.succeed(result) |
367 | |
368 | def account_info(self): |
369 | @@ -189,12 +190,63 @@ |
370 | busname = dbus_service.get_busname() |
371 | self.assertEqual(busname.get_name(), DBUS_BUS_NAME) |
372 | |
373 | - |
374 | -class OperationsTestCase(DBusServiceTestCase): |
375 | + def test_error_handler_with_failure(self): |
376 | + """Ensure to build a string-string dict to pass to error signals.""" |
377 | + error = dbus_service.Failure(TypeError('oh no!')) |
378 | + expected = dbus_service.utils.failure_to_error_dict(error) |
379 | + |
380 | + result = dbus_service.error_handler(error) |
381 | + |
382 | + self.assertEqual(expected, result) |
383 | + |
384 | + def test_error_handler_with_exception(self): |
385 | + """Ensure to build a string-string dict to pass to error signals.""" |
386 | + error = TypeError('oh no, no again!') |
387 | + expected = dbus_service.utils.exception_to_error_dict(error) |
388 | + |
389 | + result = dbus_service.error_handler(error) |
390 | + |
391 | + self.assertEqual(expected, result) |
392 | + |
393 | + def test_error_handler_with_string_dict(self): |
394 | + """Ensure to build a string-string dict to pass to error signals.""" |
395 | + expected = {'test': 'me'} |
396 | + |
397 | + result = dbus_service.error_handler(expected) |
398 | + |
399 | + self.assertEqual(expected, result) |
400 | + |
401 | + def test_error_handler_with_non_string_dict(self): |
402 | + """Ensure to build a string-string dict to pass to error signals.""" |
403 | + expected = {'test': 0, 'qué?': None, |
404 | + 10: 'foo\xffbar', True: u'ñoño'} |
405 | + |
406 | + result = dbus_service.error_handler(expected) |
407 | + expected = dict(map(lambda x: x if isinstance(x, unicode) else |
408 | + str(x).decode('utf8', 'replace'), i) |
409 | + for i in expected.iteritems()) |
410 | + |
411 | + self.assertEqual(expected, result) |
412 | + |
413 | + def test_error_handler_default(self): |
414 | + """Ensure to build a string-string dict to pass to error signals.""" |
415 | + msg = 'Got unexpected error argument %r' % None |
416 | + expected = {dbus_service.utils.ERROR_TYPE: 'UnknownError', |
417 | + dbus_service.utils.ERROR_MESSAGE: msg} |
418 | + |
419 | + result = dbus_service.error_handler(None) |
420 | + |
421 | + self.assertEqual(expected, result) |
422 | + |
423 | + |
424 | +class OperationsTestCase(TestCase): |
425 | """Test for the DBus service operations.""" |
426 | |
427 | + timeout = 3 |
428 | + |
429 | def setUp(self): |
430 | super(OperationsTestCase, self).setUp() |
431 | + dbus_service.init_mainloop() |
432 | be = dbus_service.publish_backend(MockBackend()) |
433 | self.addCleanup(be.remove_from_connection) |
434 | bus = dbus.SessionBus() |
435 | @@ -381,3 +433,29 @@ |
436 | args = ("InstallBookmarksSuccess", "InstallBookmarksError", got_signal, |
437 | self.backend.install_bookmarks_extension) |
438 | return self.assert_correct_method_call(*args) |
439 | + |
440 | + |
441 | +class OperationsErrorTestCase(OperationsTestCase): |
442 | + """Test for the DBus service operations when there is an error.""" |
443 | + |
444 | + def setUp(self): |
445 | + super(OperationsErrorTestCase, self).setUp() |
446 | + self.patch(MockBackend, 'exception', AssertionError) |
447 | + |
448 | + def assert_correct_method_call(self, success_sig, error_sig, success_cb, |
449 | + method, *args): |
450 | + """Call parent instance swapping success_sig with error_sig. |
451 | + |
452 | + This is because we want to succeed the test when the error signal was |
453 | + received. |
454 | + |
455 | + """ |
456 | + |
457 | + def got_error_signal(error_dict): |
458 | + """The error signal was received.""" |
459 | + self.assertEqual(error_dict[dbus_service.utils.ERROR_TYPE], |
460 | + 'AssertionError') |
461 | + self.deferred.callback("success") |
462 | + |
463 | + return super(OperationsErrorTestCase, self).assert_correct_method_call( |
464 | + error_sig, success_sig, got_error_signal, method, *args) |
465 | |
466 | === modified file 'ubuntuone/controlpanel/integrationtests/test_webclient.py' |
467 | --- ubuntuone/controlpanel/integrationtests/test_webclient.py 2010-11-10 15:54:29 +0000 |
468 | +++ ubuntuone/controlpanel/integrationtests/test_webclient.py 2010-12-02 20:41:22 +0000 |
469 | @@ -1,6 +1,7 @@ |
470 | # -*- coding: utf-8 -*- |
471 | |
472 | # Authors: Alejandro J. Cura <alecu@canonical.com> |
473 | +# Authors: Natalia B. Bidart <nataliabidart@canonical.com> |
474 | # |
475 | # Copyright 2010 Canonical Ltd. |
476 | # |
477 | @@ -22,9 +23,9 @@ |
478 | from twisted.internet import defer |
479 | from twisted.internet.defer import inlineCallbacks |
480 | from twisted.web import server, resource |
481 | -from ubuntuone.devtools.testcase import DBusTestCase as TestCase |
482 | |
483 | from ubuntuone.controlpanel import webclient |
484 | +from ubuntuone.controlpanel.integrationtests import TestCase |
485 | |
486 | |
487 | SAMPLE_KEY = "result" |
488 | |
489 | === modified file 'ubuntuone/controlpanel/tests/__init__.py' |
490 | --- ubuntuone/controlpanel/tests/__init__.py 2010-10-07 20:13:07 +0000 |
491 | +++ ubuntuone/controlpanel/tests/__init__.py 2010-12-02 20:41:22 +0000 |
492 | @@ -18,8 +18,25 @@ |
493 | |
494 | """The test suite for the control panel for Ubuntu One.""" |
495 | |
496 | +from twisted.trial import unittest |
497 | + |
498 | + |
499 | TOKEN = {u'consumer_key': u'xQ7xDAz', |
500 | u'consumer_secret': u'KzCJWCTNbbntwfyCKKjomJDzlgqxLy', |
501 | u'token_name': u'test', |
502 | u'token': u'GkInOfSMGwTXAUoVQwLUoPxElEEUdhsLVNTPhxHJDUIeHCPNEo', |
503 | u'token_secret': u'qFYImEtlczPbsCnYyuwLoPDlPEnvNcIktZphPQklAWrvyfFMV'} |
504 | + |
505 | + |
506 | +class TestCase(unittest.TestCase): |
507 | + """Basics for testing.""" |
508 | + |
509 | + def setUp(self): |
510 | + self._called = False |
511 | + |
512 | + def tearDown(self): |
513 | + self._called = False |
514 | + |
515 | + def _set_called(self, *args, **kwargs): |
516 | + """Store 'args' and 'kwargs' for test assertions.""" |
517 | + self._called = (args, kwargs) |
518 | |
519 | === modified file 'ubuntuone/controlpanel/tests/test_backend.py' |
520 | --- ubuntuone/controlpanel/tests/test_backend.py 2010-11-17 21:28:55 +0000 |
521 | +++ ubuntuone/controlpanel/tests/test_backend.py 2010-12-02 20:41:22 +0000 |
522 | @@ -1,6 +1,7 @@ |
523 | # -*- coding: utf-8 -*- |
524 | |
525 | # Authors: Alejandro J. Cura <alecu@canonical.com> |
526 | +# Authors: Natalia B. Bidart <nataliabidart@canonical.com> |
527 | # |
528 | # Copyright 2010 Canonical Ltd. |
529 | # |
530 | @@ -22,11 +23,11 @@ |
531 | |
532 | from twisted.internet import defer |
533 | from twisted.internet.defer import inlineCallbacks |
534 | -from twisted.trial.unittest import TestCase |
535 | |
536 | from ubuntuone.controlpanel import backend |
537 | from ubuntuone.controlpanel.backend import (ACCOUNT_API, QUOTA_API, |
538 | DEVICES_API, DEVICE_REMOVE_API) |
539 | +from ubuntuone.controlpanel.tests import TestCase |
540 | from ubuntuone.controlpanel.webclient import WebClientError |
541 | |
542 | SAMPLE_CREDENTIALS = {"token": "ABC1234DEF"} |
543 | @@ -191,17 +192,16 @@ |
544 | self.patch(backend, "WebClient", MockWebClient) |
545 | self.patch(backend, "dbus_client", MockDBusClient()) |
546 | self.local_token = "Computer" + SAMPLE_CREDENTIALS["token"] |
547 | + self.be = backend.ControlBackend() |
548 | |
549 | def test_backend_creation(self): |
550 | """The backend instance is successfully created.""" |
551 | - be = backend.ControlBackend() |
552 | - self.assertEqual(be.wc.__class__, MockWebClient) |
553 | + self.assertEqual(self.be.wc.__class__, MockWebClient) |
554 | |
555 | @inlineCallbacks |
556 | def test_get_token(self): |
557 | """The get_token method returns the right token.""" |
558 | - be = backend.ControlBackend() |
559 | - token = yield be.get_token() |
560 | + token = yield self.be.get_token() |
561 | self.assertEqual(token, SAMPLE_CREDENTIALS["token"]) |
562 | |
563 | |
564 | @@ -211,20 +211,18 @@ |
565 | @inlineCallbacks |
566 | def test_account_info(self): |
567 | """The account_info method exercises its callback.""" |
568 | - be = backend.ControlBackend() |
569 | # pylint: disable=E1101 |
570 | - be.wc.results[ACCOUNT_API] = SAMPLE_ACCOUNT_JSON |
571 | - be.wc.results[QUOTA_API] = SAMPLE_QUOTA_JSON |
572 | - result = yield be.account_info() |
573 | + self.be.wc.results[ACCOUNT_API] = SAMPLE_ACCOUNT_JSON |
574 | + self.be.wc.results[QUOTA_API] = SAMPLE_QUOTA_JSON |
575 | + result = yield self.be.account_info() |
576 | self.assertEqual(result, EXPECTED_ACCOUNT_INFO) |
577 | |
578 | @inlineCallbacks |
579 | def test_account_info_fails(self): |
580 | """The account_info method exercises its errback.""" |
581 | - be = backend.ControlBackend() |
582 | # pylint: disable=E1101 |
583 | - be.wc.failure = 404 |
584 | - yield self.assertFailure(be.account_info(), WebClientError) |
585 | + self.be.wc.failure = 404 |
586 | + yield self.assertFailure(self.be.account_info(), WebClientError) |
587 | |
588 | |
589 | class BackendDevicesTestCase(BackendBasicTestCase): |
590 | @@ -233,58 +231,52 @@ |
591 | @inlineCallbacks |
592 | def test_devices_info(self): |
593 | """The devices_info method exercises its callback.""" |
594 | - be = backend.ControlBackend() |
595 | # pylint: disable=E1101 |
596 | - be.wc.results[DEVICES_API] = SAMPLE_DEVICES_JSON |
597 | - result = yield be.devices_info() |
598 | + self.be.wc.results[DEVICES_API] = SAMPLE_DEVICES_JSON |
599 | + result = yield self.be.devices_info() |
600 | self.assertEqual(result, EXPECTED_DEVICES_INFO) |
601 | |
602 | @inlineCallbacks |
603 | def test_devices_info_fails(self): |
604 | """The devices_info method exercises its errback.""" |
605 | - be = backend.ControlBackend() |
606 | # pylint: disable=E1101 |
607 | - be.wc.failure = 404 |
608 | - yield self.assertFailure(be.devices_info(), WebClientError) |
609 | + self.be.wc.failure = 404 |
610 | + yield self.assertFailure(self.be.devices_info(), WebClientError) |
611 | |
612 | @inlineCallbacks |
613 | def test_remove_device(self): |
614 | """The remove_device method calls the right api.""" |
615 | - be = backend.ControlBackend() |
616 | dtype, did = "Computer", "SAMPLE-TOKEN" |
617 | device_id = dtype + did |
618 | apiurl = DEVICE_REMOVE_API % (dtype, did) |
619 | # pylint: disable=E1101 |
620 | - be.wc.results[apiurl] = SAMPLE_DEVICES_JSON |
621 | - result = yield be.remove_device(device_id) |
622 | + self.be.wc.results[apiurl] = SAMPLE_DEVICES_JSON |
623 | + result = yield self.be.remove_device(device_id) |
624 | self.assertEqual(result, device_id) |
625 | |
626 | @inlineCallbacks |
627 | def test_remove_device_fails(self): |
628 | """The remove_device method fails as expected.""" |
629 | - be = backend.ControlBackend() |
630 | # pylint: disable=E1101 |
631 | - be.wc.failure = 404 |
632 | - yield self.assertFailure(be.devices_info(), WebClientError) |
633 | + self.be.wc.failure = 404 |
634 | + yield self.assertFailure(self.be.devices_info(), WebClientError) |
635 | |
636 | @inlineCallbacks |
637 | def test_change_limit_bandwidth(self): |
638 | """The device settings are updated.""" |
639 | - be = backend.ControlBackend() |
640 | backend.dbus_client.throttling = False |
641 | - yield be.change_device_settings(self.local_token, |
642 | + yield self.be.change_device_settings(self.local_token, |
643 | {"limit_bandwidth": "1"}) |
644 | self.assertEqual(backend.dbus_client.throttling, True) |
645 | - yield be.change_device_settings(self.local_token, |
646 | + yield self.be.change_device_settings(self.local_token, |
647 | {"limit_bandwidth": "0"}) |
648 | self.assertEqual(backend.dbus_client.throttling, False) |
649 | |
650 | @inlineCallbacks |
651 | def test_change_upload_speed_limit(self): |
652 | """The device settings are updated.""" |
653 | - be = backend.ControlBackend() |
654 | backend.dbus_client.limits = {"download": -1, "upload": -1} |
655 | - yield be.change_device_settings(self.local_token, |
656 | + yield self.be.change_device_settings(self.local_token, |
657 | {"max_upload_speed": "1111"}) |
658 | self.assertEqual(backend.dbus_client.limits["upload"], 1111) |
659 | self.assertEqual(backend.dbus_client.limits["download"], -1) |
660 | @@ -292,9 +284,8 @@ |
661 | @inlineCallbacks |
662 | def test_change_download_speed_limit(self): |
663 | """The device settings are updated.""" |
664 | - be = backend.ControlBackend() |
665 | backend.dbus_client.limits = {"download": -1, "upload": -1} |
666 | - yield be.change_device_settings(self.local_token, |
667 | + yield self.be.change_device_settings(self.local_token, |
668 | {"max_download_speed": "99"}) |
669 | self.assertEqual(backend.dbus_client.limits["upload"], -1) |
670 | self.assertEqual(backend.dbus_client.limits["download"], 99) |
671 | @@ -302,7 +293,6 @@ |
672 | @inlineCallbacks |
673 | def test_changing_settings_for_wrong_id_has_no_effect(self): |
674 | """If the id is wrong, no settings are changed.""" |
675 | - be = backend.ControlBackend() |
676 | backend.dbus_client.throttling = False |
677 | backend.dbus_client.limits = {"download": -1, "upload": -1} |
678 | new_settings = { |
679 | @@ -310,7 +300,7 @@ |
680 | "max_upload_speed": "99", |
681 | "limit_bandwidth": "1", |
682 | } |
683 | - yield be.change_device_settings("wrong token!", new_settings) |
684 | + yield self.be.change_device_settings("wrong token!", new_settings) |
685 | self.assertEqual(backend.dbus_client.throttling, False) |
686 | self.assertEqual(backend.dbus_client.limits["upload"], -1) |
687 | self.assertEqual(backend.dbus_client.limits["download"], -1) |
688 | @@ -322,6 +312,5 @@ |
689 | @inlineCallbacks |
690 | def test_volumes_info(self): |
691 | """The volumes_info method exercises its callback.""" |
692 | - be = backend.ControlBackend() |
693 | - result = yield be.volumes_info() |
694 | + result = yield self.be.volumes_info() |
695 | self.assertEqual(result, SAMPLE_VOLUMES) |
696 | |
697 | === modified file 'ubuntuone/controlpanel/tests/test_utils.py' |
698 | --- ubuntuone/controlpanel/tests/test_utils.py 2010-11-10 19:50:29 +0000 |
699 | +++ ubuntuone/controlpanel/tests/test_utils.py 2010-12-02 20:41:22 +0000 |
700 | @@ -21,10 +21,10 @@ |
701 | import logging |
702 | import sys |
703 | |
704 | -from twisted.trial.unittest import TestCase |
705 | from ubuntuone.devtools.handlers import MementoHandler |
706 | |
707 | from ubuntuone.controlpanel import utils |
708 | +from ubuntuone.controlpanel.tests import TestCase |
709 | |
710 | |
711 | CONSTANTS_MODULE = 'ubuntuone.controlpanel.constants' |
712 | @@ -32,11 +32,18 @@ |
713 | |
714 | |
715 | class FakedConstantsModule(object): |
716 | - """FAke the 'ubuntuone.controlpanel.constants' module.""" |
717 | + """Fake the 'ubuntuone.controlpanel.constants' module.""" |
718 | |
719 | PROJECT_DIR = '/tmp/foo/bar' |
720 | |
721 | |
722 | +class FakedFailure(object): |
723 | + """Fake a twisted Failure.""" |
724 | + |
725 | + def __init__(self, value): |
726 | + self.value = value |
727 | + |
728 | + |
729 | class GetProjectDirTestCase(TestCase): |
730 | """Test case for get_project_dir when constants module is not defined.""" |
731 | |
732 | @@ -106,3 +113,56 @@ |
733 | result = utils.get_data_file(dummy_file) |
734 | expected = utils.os.path.join(dummy_dir, dummy_file) |
735 | self.assertEqual(expected, result) |
736 | + |
737 | + |
738 | +class ExceptionHandligTestCase(TestCase): |
739 | + """Test cases for exception handling.""" |
740 | + |
741 | + def test_is_dbus_no_reply(self): |
742 | + """The failure is a dbus no_reply error.""" |
743 | + exc = utils.dbus.exceptions.DBusException() |
744 | + exc._dbus_error_name = utils.DBUS_NO_REPLY |
745 | + |
746 | + result = utils.is_dbus_no_reply(FakedFailure(value=exc)) |
747 | + |
748 | + self.assertTrue(result) |
749 | + |
750 | + def test_other_dbus_error_is_not_dbus_no_reply(self): |
751 | + """Another dbus exception is not a dbus no_reply error.""" |
752 | + exc = utils.dbus.exceptions.DBusException() |
753 | + exc._dbus_error_name = utils.DBUS_SERVICE_UNKNOWN |
754 | + |
755 | + result = utils.is_dbus_no_reply(FakedFailure(value=exc)) |
756 | + |
757 | + self.assertFalse(result) |
758 | + |
759 | + def test_no_dbus_exception_is_not_dbus_no_reply(self): |
760 | + """A non dbus exception is not a dbus no_reply error.""" |
761 | + exc = AssertionError(utils.DBUS_NO_REPLY) |
762 | + |
763 | + result = utils.is_dbus_no_reply(FakedFailure(value=exc)) |
764 | + |
765 | + self.assertFalse(result) |
766 | + |
767 | + def test_exception_to_error_dict(self): |
768 | + """Transform a regular Exception into a string-string dictionary.""" |
769 | + msg = 'Something went wrong.' |
770 | + exc = AssertionError(msg) |
771 | + |
772 | + result = utils.exception_to_error_dict(exc) |
773 | + expected = {utils.ERROR_TYPE: exc.__class__.__name__, |
774 | + utils.ERROR_MESSAGE: unicode(exc)} |
775 | + |
776 | + self.assertEqual(expected, result) |
777 | + |
778 | + def test_failure_to_error_dict(self): |
779 | + """Transform a Failure into a string-string dictionary.""" |
780 | + msg = 'Something went wrong.' |
781 | + exc = AssertionError(msg) |
782 | + failure = FakedFailure(value=exc) |
783 | + |
784 | + result = utils.failure_to_error_dict(failure) |
785 | + expected = {utils.ERROR_TYPE: exc.__class__.__name__, |
786 | + utils.ERROR_MESSAGE: unicode(exc)} |
787 | + |
788 | + self.assertEqual(expected, result) |
789 | |
790 | === removed file 'ubuntuone/controlpanel/tests/testcase.py' |
791 | --- ubuntuone/controlpanel/tests/testcase.py 2010-09-20 17:17:06 +0000 |
792 | +++ ubuntuone/controlpanel/tests/testcase.py 1970-01-01 00:00:00 +0000 |
793 | @@ -1,54 +0,0 @@ |
794 | -# Author: Guillermo Gonzalez <guillermo.gonzalez@canonical.com> |
795 | -# |
796 | -# Copyright 2009-2010 Canonical Ltd. |
797 | -# |
798 | -# This program is free software: you can redistribute it and/or modify it |
799 | -# under the terms of the GNU General Public License version 3, as published |
800 | -# by the Free Software Foundation. |
801 | -# |
802 | -# This program is distributed in the hope that it will be useful, but |
803 | -# WITHOUT ANY WARRANTY; without even the implied warranties of |
804 | -# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR |
805 | -# PURPOSE. See the GNU General Public License for more details. |
806 | -# |
807 | -# You should have received a copy of the GNU General Public License along |
808 | -# with this program. If not, see <http://www.gnu.org/licenses/>. |
809 | -"""Test utilities.""" |
810 | - |
811 | -import logging |
812 | - |
813 | - |
814 | -class MementoHandler(logging.Handler): |
815 | - """ A handler class which store logging records in a list """ |
816 | - |
817 | - def __init__(self, *args, **kwargs): |
818 | - """ Create the instance, and add a records attribute. """ |
819 | - logging.Handler.__init__(self, *args, **kwargs) |
820 | - self.records = [] |
821 | - |
822 | - def emit(self, record): |
823 | - """ Just add the record to self.records. """ |
824 | - self.records.append(record) |
825 | - |
826 | - def check(self, level, *msgs): |
827 | - """Verifies that the msgs are logged in the specified level.""" |
828 | - for rec in self.records: |
829 | - if rec.levelno == level and all(m in rec.message for m in msgs): |
830 | - return True |
831 | - return False |
832 | - |
833 | - def check_debug(self, *msgs): |
834 | - """Shortcut for checking in DEBUG.""" |
835 | - return self.check(logging.DEBUG, *msgs) |
836 | - |
837 | - def check_info(self, *msgs): |
838 | - """Shortcut for checking in INFO.""" |
839 | - return self.check(logging.INFO, *msgs) |
840 | - |
841 | - def check_warning(self, *msgs): |
842 | - """Shortcut for checking in WARNING.""" |
843 | - return self.check(logging.WARNING, *msgs) |
844 | - |
845 | - def check_error(self, *msgs): |
846 | - """Shortcut for checking in ERROR.""" |
847 | - return self.check(logging.ERROR, *msgs) |
848 | |
849 | === modified file 'ubuntuone/controlpanel/utils.py' |
850 | --- ubuntuone/controlpanel/utils.py 2010-11-10 19:50:29 +0000 |
851 | +++ ubuntuone/controlpanel/utils.py 2010-12-02 20:41:22 +0000 |
852 | @@ -22,12 +22,21 @@ |
853 | |
854 | from functools import wraps |
855 | |
856 | +import dbus |
857 | + |
858 | from ubuntuone.controlpanel.logger import setup_logging |
859 | |
860 | |
861 | logger = setup_logging('utils') |
862 | + |
863 | DATA_SUFFIX = 'data' |
864 | |
865 | +DBUS_NO_REPLY = 'org.freedesktop.DBus.Error.NoReply' |
866 | +DBUS_SERVICE_UNKNOWN = 'org.freedesktop.DBus.Error.ServiceUnknown' |
867 | + |
868 | +ERROR_TYPE = 'error_type' |
869 | +ERROR_MESSAGE = 'error_msg' |
870 | + |
871 | |
872 | def get_project_dir(): |
873 | """Return the absolute path to this project's data/ dir. |
874 | @@ -75,3 +84,23 @@ |
875 | return inner |
876 | |
877 | return middle |
878 | + |
879 | + |
880 | +def is_dbus_no_reply(failure): |
881 | + """Decide if 'failure' is a DBus NoReply Error.""" |
882 | + exc = failure.value |
883 | + res = (isinstance(exc, dbus.exceptions.DBusException) and |
884 | + exc.get_dbus_name() == DBUS_NO_REPLY) |
885 | + return res |
886 | + |
887 | + |
888 | +def exception_to_error_dict(exc): |
889 | + """Transform a regular Exception into a dictionary.""" |
890 | + result = {ERROR_TYPE: exc.__class__.__name__, ERROR_MESSAGE: unicode(exc)} |
891 | + |
892 | + return result |
893 | + |
894 | + |
895 | +def failure_to_error_dict(failure): |
896 | + """Transform a twisted Failure into a dictionary.""" |
897 | + return exception_to_error_dict(failure.value) |
898 | |
899 | === modified file 'ubuntuone/controlpanel/webclient.py' |
900 | --- ubuntuone/controlpanel/webclient.py 2010-10-08 17:38:57 +0000 |
901 | +++ ubuntuone/controlpanel/webclient.py 2010-12-02 20:41:22 +0000 |
902 | @@ -1,4 +1,7 @@ |
903 | +# -*- coding: utf-8 -*- |
904 | + |
905 | # Authors: Alejandro J. Cura <alecu@canonical.com> |
906 | +# Authors: Natalia B. Bidart <nataliabidart@canonical.com> |
907 | # |
908 | # Copyright 2010 Canonical Ltd. |
909 | # |
910 | @@ -45,7 +48,7 @@ |
911 | |
912 | def _handler(self, session, msg, d): |
913 | """Handle the result of an http message.""" |
914 | - logger.debug("got http response: %d", msg.status_code) |
915 | + logger.debug("WebClient: got http response: %d", msg.status_code) |
916 | if msg.status_code == 200: |
917 | result = simplejson.loads(msg.response_body.data) |
918 | d.callback(result) |
919 | @@ -55,20 +58,18 @@ |
920 | |
921 | def _call_api_with_creds(self, credentials, api_name): |
922 | """Get a given url from the webservice with credentials.""" |
923 | - logger.debug("got credentials") |
924 | url = self.base_url + api_name |
925 | method = "GET" |
926 | msg = Soup.Message.new(method, url) |
927 | add_oauth_headers(msg.request_headers.append, method, url, credentials) |
928 | d = defer.Deferred() |
929 | - logger.debug("getting url: %s", url) |
930 | + logger.debug("WebClient: getting url: %s", url) |
931 | self.session.queue_message(msg, self._handler, d) |
932 | return d |
933 | |
934 | def call_api(self, api_name): |
935 | """Get a given url from the webservice.""" |
936 | - logger.debug("calling api: %s", api_name) |
937 | - logger.debug("getting credentials") |
938 | + logger.debug("WebClient: calling api: %s", api_name) |
939 | d = self.get_credentials() |
940 | d.addCallback(self._call_api_with_creds, api_name) |
941 | return d |
As advertised.