Merge lp:~nataliabidart/ubuntuone-control-panel/handle-sd-timeout into lp:ubuntuone-control-panel

Proposed by Natalia Bidart
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
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/python2.6/dist-packages/twisted/internet/defer.py", line 949, in gotResult
    _inlineCallbacks(r, g, deferred)
  File "/usr/lib/python2.6/dist-packages/twisted/internet/defer.py", line 939, in _inlineCallbacks
    deferred.errback()
  File "/usr/lib/python2.6/dist-packages/twisted/internet/defer.py", line 345, in errback
    self._startRunCallbacks(fail)
  File "/usr/lib/python2.6/dist-packages/twisted/internet/defer.py", line 424, in _startRunCallbacks
    self._runCallbacks()
--- <exception caught here> ---
  File "/usr/lib/python2.6/dist-packages/twisted/internet/defer.py", line 441, in _runCallbacks
    self.result = callback(self.result, *args, **kw)
  File "/usr/lib/pymodules/python2.6/dbus/decorators.py", line 309, in emit_signal
    message.append(signature=signature, *args)
exceptions.TypeError: iteration over non-sequence

To reproduce:

* Temporally make syncdaemon dbus service unavailable (edit /usr/share/dbus-1/services/com.ubuntuone.SyncDaemon.service and modify the Name= field so the dbus name doesn't exist)

* from trunk, run:

in terminal 1) PYTHONPATH=. ./bin/ubuntuone-control-panel-backend
in terminal 2) PYTHONPATH=. ./bin/ubuntuone-control-panel-gtk

* 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.controlpanel.dbus_service:VolumesInfoError: args (<ubuntuone.controlpanel.dbus_service.ControlPanelBackend at /preferences at 0x343e550>, {'error_type': 'DBusException', 'error_msg': u'org.freedesktop.DBus.Error.ServiceUnknown: The name com.ubuntuone.SyncDaemon was not provided by any .service files'}), kwargs {}.

To post a comment you must log in.
Revision history for this message
John Lenton (chipaca) wrote :

As advertised.

review: Approve

Preview Diff

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

Subscribers

People subscribed via source and target branches