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

Proposed by Natalia Bidart
Status: Merged
Approved by: Roberto Alsina
Approved revision: 216
Merged at revision: 218
Proposed branch: lp:~nataliabidart/ubuntuone-control-panel/handle-errors
Merge into: lp:ubuntuone-control-panel
Diff against target: 1063 lines (+396/-127)
21 files modified
data/qt/signin.ui (+0/-7)
ubuntuone/controlpanel/cache.py (+7/-2)
ubuntuone/controlpanel/gui/qt/__init__.py (+49/-0)
ubuntuone/controlpanel/gui/qt/account.py (+1/-0)
ubuntuone/controlpanel/gui/qt/addfolder.py (+2/-0)
ubuntuone/controlpanel/gui/qt/controlpanel.py (+1/-0)
ubuntuone/controlpanel/gui/qt/device.py (+11/-2)
ubuntuone/controlpanel/gui/qt/devices.py (+1/-0)
ubuntuone/controlpanel/gui/qt/folders.py (+1/-0)
ubuntuone/controlpanel/gui/qt/preferences.py (+1/-0)
ubuntuone/controlpanel/gui/qt/signin.py (+9/-10)
ubuntuone/controlpanel/gui/qt/tests/__init__.py (+43/-5)
ubuntuone/controlpanel/gui/qt/tests/test_addfolder.py (+20/-23)
ubuntuone/controlpanel/gui/qt/tests/test_common.py (+166/-3)
ubuntuone/controlpanel/gui/qt/tests/test_device.py (+19/-11)
ubuntuone/controlpanel/gui/qt/tests/test_devices.py (+0/-2)
ubuntuone/controlpanel/gui/qt/tests/test_folders.py (+14/-31)
ubuntuone/controlpanel/gui/qt/tests/test_signin.py (+14/-30)
ubuntuone/controlpanel/gui/qt/tests/test_ubuntuonebin.py (+19/-1)
ubuntuone/controlpanel/gui/qt/ubuntuonebin.py (+11/-0)
ubuntuone/controlpanel/tests/test_cache.py (+7/-0)
To merge this branch: bzr merge lp:~nataliabidart/ubuntuone-control-panel/handle-errors
Reviewer Review Type Date Requested Status
Roberto Alsina (community) Approve
Diego Sarmentero (community) Approve
Review via email: mp+74490@code.launchpad.net

Commit message

- Added decorator to handle errors from the backend (LP: #807021).

To post a comment you must log in.
216. By Natalia Bidart on 2011-09-08

Forgotten is_processing = False.

Diego Sarmentero (diegosarmentero) wrote :

+1

review: Approve
Roberto Alsina (ralsina) wrote :

+1

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'data/qt/signin.ui'
2--- data/qt/signin.ui 2011-08-30 23:18:35 +0000
3+++ data/qt/signin.ui 2011-09-08 01:17:23 +0000
4@@ -41,13 +41,6 @@
5 </widget>
6 </item>
7 <item>
8- <widget class="QLabel" name="warning_label">
9- <property name="text">
10- <string>warning label!</string>
11- </property>
12- </widget>
13- </item>
14- <item>
15 <layout class="QHBoxLayout" name="horizontalLayout">
16 <item>
17 <layout class="QVBoxLayout" name="verticalLayout_2">
18
19=== modified file 'ubuntuone/controlpanel/cache.py'
20--- ubuntuone/controlpanel/cache.py 2011-09-02 17:59:39 +0000
21+++ ubuntuone/controlpanel/cache.py 2011-09-08 01:17:23 +0000
22@@ -33,13 +33,18 @@
23 if self.logger is not None:
24 self.logger.debug('%s: started.', self.__class__.__name__)
25
26- @property
27- def backend(self):
28+ def get_backend(self):
29 """A cached ControlBackend instance."""
30 if not self._shared_objects:
31 self._shared_objects['backend'] = backend.ControlBackend()
32 return self._shared_objects['backend']
33
34+ def set_backend(self, new_value):
35+ """Set a new ControlBackend instance."""
36+ self._shared_objects['backend'] = new_value
37+
38+ backend = property(fget=get_backend, fset=set_backend)
39+
40 def clear(self):
41 """Clear all cached objects."""
42 self._shared_objects = {}
43
44=== modified file 'ubuntuone/controlpanel/gui/qt/__init__.py'
45--- ubuntuone/controlpanel/gui/qt/__init__.py 2011-09-01 17:11:16 +0000
46+++ ubuntuone/controlpanel/gui/qt/__init__.py 2011-09-08 01:17:23 +0000
47@@ -18,7 +18,13 @@
48
49 """The Qt graphical interface for the control panel for Ubuntu One."""
50
51+import collections
52+import logging
53+
54+from functools import wraps
55+
56 from PyQt4 import QtGui, QtCore
57+from twisted.internet import defer
58
59
60 def uri_hook(uri):
61@@ -53,3 +59,46 @@
62 icon.icon_name = icon_name
63
64 return icon
65+
66+
67+def handle_errors(error_handler=None, logger=None):
68+ """Decorator to handle errors when calling a function.
69+
70+ if 'error_handler' is not None, it will be yielded on if any error happens.
71+
72+ """
73+ if logger is None:
74+ logger = logging.getLogger()
75+
76+ def middle(f):
77+ """Decorator to handle errors when calling 'f'."""
78+
79+ @defer.inlineCallbacks
80+ @wraps(f)
81+ def inner(*args, **kwargs):
82+ """Call 'f' passing 'args' and 'kwargs'.
83+
84+ Catch any error and show a error message.
85+
86+ """
87+ try:
88+ res = yield f(*args, **kwargs)
89+ except Exception, e: # pylint: disable=W0703
90+ logger.exception(f.__name__)
91+ else:
92+ defer.returnValue(res)
93+
94+ # this code will only be executed if there was an exception
95+ if error_handler is not None:
96+ yield error_handler()
97+
98+ if len(e.args) > 0 and isinstance(e.args[0], collections.Mapping):
99+ msgs = e.args[0].itervalues()
100+ else:
101+ msgs = [e.__class__.__name__] + map(repr, e.args)
102+ msg = '\n'.join(msgs)
103+ QtGui.QMessageBox.warning(None, '', msg, QtGui.QMessageBox.Close)
104+
105+ return inner
106+
107+ return middle
108
109=== modified file 'ubuntuone/controlpanel/gui/qt/account.py'
110--- ubuntuone/controlpanel/gui/qt/account.py 2011-08-31 17:18:56 +0000
111+++ ubuntuone/controlpanel/gui/qt/account.py 2011-09-08 01:17:23 +0000
112@@ -45,6 +45,7 @@
113 self.ui.edit_profile_button.uri = EDIT_PROFILE_LINK
114 self.ui.edit_services_button.uri = EDIT_ACCOUNT_LINK
115
116+ # pylint: disable=E0202
117 @defer.inlineCallbacks
118 def load(self):
119 """Load info."""
120
121=== modified file 'ubuntuone/controlpanel/gui/qt/addfolder.py'
122--- ubuntuone/controlpanel/gui/qt/addfolder.py 2011-09-02 17:59:39 +0000
123+++ ubuntuone/controlpanel/gui/qt/addfolder.py 2011-09-08 01:17:23 +0000
124@@ -28,6 +28,7 @@
125 from ubuntuone.controlpanel import cache
126 from ubuntuone.controlpanel.logger import setup_logging
127 from ubuntuone.controlpanel.gui import FOLDER_INVALID_PATH
128+from ubuntuone.controlpanel.gui.qt import handle_errors
129
130
131 logger = setup_logging('qt.addfolder')
132@@ -50,6 +51,7 @@
133 self.clicked.connect(self.on_clicked)
134
135 @QtCore.pyqtSlot()
136+ @handle_errors(logger=logger)
137 @defer.inlineCallbacks
138 def on_clicked(self):
139 """The 'Sync another folder' button was clicked."""
140
141=== modified file 'ubuntuone/controlpanel/gui/qt/controlpanel.py'
142--- ubuntuone/controlpanel/gui/qt/controlpanel.py 2011-09-01 17:11:16 +0000
143+++ ubuntuone/controlpanel/gui/qt/controlpanel.py 2011-09-08 01:17:23 +0000
144@@ -72,6 +72,7 @@
145 self.ui.switcher.setCurrentWidget(self.ui.management)
146 self.is_processing = False
147
148+ # pylint: disable=E0202
149 @defer.inlineCallbacks
150 def load(self):
151 """Load info."""
152
153=== modified file 'ubuntuone/controlpanel/gui/qt/device.py'
154--- ubuntuone/controlpanel/gui/qt/device.py 2011-09-02 17:59:39 +0000
155+++ ubuntuone/controlpanel/gui/qt/device.py 2011-09-08 01:17:23 +0000
156@@ -27,8 +27,13 @@
157 )
158 from ubuntuone.controlpanel import cache
159 from ubuntuone.controlpanel.gui import DEVICE_CONFIRM_REMOVE
160-from ubuntuone.controlpanel.gui.qt import icon_from_name, pixmap_from_name
161+from ubuntuone.controlpanel.gui.qt import (
162+ handle_errors,
163+ icon_from_name,
164+ pixmap_from_name,
165+)
166 from ubuntuone.controlpanel.gui.qt.ui import device_ui
167+from ubuntuone.controlpanel.logger import setup_logging
168
169 COMPUTER_ICON = "computer"
170 PHONE_ICON = "phone"
171@@ -44,6 +49,9 @@
172 YES = QtGui.QMessageBox.Yes
173
174
175+logger = setup_logging('qt.device')
176+
177+
178 def icon_name_from_type(device_type):
179 """Get the icon name for the device."""
180 icon_name = DEVICE_TYPE_TO_ICON_MAP.get(device_type, DEFAULT_ICON)
181@@ -70,8 +78,9 @@
182 pixmap = pixmap_from_name(icon_name)
183 self.ui.device_icon_label.setPixmap(pixmap)
184
185+ @QtCore.pyqtSlot()
186+ @handle_errors(logger=logger)
187 @defer.inlineCallbacks
188- @QtCore.pyqtSlot()
189 def on_remove_device_button_clicked(self):
190 """The user wants to remove this device."""
191 msg = DEVICE_CONFIRM_REMOVE
192
193=== modified file 'ubuntuone/controlpanel/gui/qt/devices.py'
194--- ubuntuone/controlpanel/gui/qt/devices.py 2011-09-02 17:59:39 +0000
195+++ ubuntuone/controlpanel/gui/qt/devices.py 2011-09-08 01:17:23 +0000
196@@ -47,6 +47,7 @@
197 super(DevicesPanel, self)._setup()
198 self.ui.manage_devices_button.uri = EDIT_DEVICES_LINK
199
200+ # pylint: disable=E0202
201 @defer.inlineCallbacks
202 def load(self):
203 """Load info."""
204
205=== modified file 'ubuntuone/controlpanel/gui/qt/folders.py'
206--- ubuntuone/controlpanel/gui/qt/folders.py 2011-08-31 17:18:56 +0000
207+++ ubuntuone/controlpanel/gui/qt/folders.py 2011-09-08 01:17:23 +0000
208@@ -82,6 +82,7 @@
209 icon = icon_from_name('external_icon_orange')
210 self.ui.share_publish_button.setIcon(icon)
211
212+ # pylint: disable=E0202
213 @defer.inlineCallbacks
214 def load(self):
215 """Load specific tab info."""
216
217=== modified file 'ubuntuone/controlpanel/gui/qt/preferences.py'
218--- ubuntuone/controlpanel/gui/qt/preferences.py 2011-08-31 17:18:56 +0000
219+++ ubuntuone/controlpanel/gui/qt/preferences.py 2011-09-08 01:17:23 +0000
220@@ -64,6 +64,7 @@
221 ui_class = preferences_ui
222 logger = logger
223
224+ # pylint: disable=E0202
225 @defer.inlineCallbacks
226 def load(self):
227 """Load info."""
228
229=== modified file 'ubuntuone/controlpanel/gui/qt/signin.py'
230--- ubuntuone/controlpanel/gui/qt/signin.py 2011-08-31 17:18:56 +0000
231+++ ubuntuone/controlpanel/gui/qt/signin.py 2011-09-08 01:17:23 +0000
232@@ -19,12 +19,10 @@
233 """The signin page."""
234
235 from PyQt4 import QtCore
236-
237 from twisted.internet import defer
238-from ubuntuone.platform.credentials import CredentialsError
239
240 from ubuntuone.controlpanel.gui import RESET_PASSWORD_LINK
241-from ubuntuone.controlpanel.gui.qt import icon_from_name
242+from ubuntuone.controlpanel.gui.qt import icon_from_name, handle_errors
243 from ubuntuone.controlpanel.gui.qt.ubuntuonebin import UbuntuOneBin
244 from ubuntuone.controlpanel.gui.qt.ui import signin_ui
245 from ubuntuone.controlpanel.logger import setup_logging, log_call
246@@ -45,7 +43,6 @@
247 def _setup(self):
248 """Do some extra setupping for the UI."""
249 super(SignInPanel, self)._setup()
250- self.ui.warning_label.setText("")
251
252 self.ui.forgot_password_button.uri = RESET_PASSWORD_LINK
253 icon = icon_from_name('external_icon_orange')
254@@ -64,21 +61,23 @@
255 self.ui.signin_button.style().unpolish(self.ui.signin_button)
256 self.ui.signin_button.style().polish(self.ui.signin_button)
257
258+ # pylint: disable=E0202
259+ @defer.inlineCallbacks
260+ def load(self):
261+ """Load specific tab info."""
262+ yield self.backend.get_credentials()
263+
264+ @QtCore.pyqtSlot()
265+ @handle_errors(logger=logger)
266 @log_call(logger.debug)
267 @defer.inlineCallbacks
268- @QtCore.pyqtSlot()
269 def on_signin_button_clicked(self):
270 """The 'Sign in' button was clicked."""
271- self.ui.warning_label.setText("")
272 email = unicode(self.ui.email_entry.text())
273 password = unicode(self.ui.password_entry.text())
274 self.is_processing = True
275 try:
276 result = yield self.backend.login(email=email, password=password)
277- except CredentialsError, e:
278- logger.info('on_signin_button_clicked: %r', e)
279- self.ui.warning_label.setText(e.args[0]['message'])
280- else:
281 logger.info('Emitting credentialsFound for email %r.', email)
282 self.credentialsFound.emit(result)
283 finally:
284
285=== modified file 'ubuntuone/controlpanel/gui/qt/tests/__init__.py'
286--- ubuntuone/controlpanel/gui/qt/tests/__init__.py 2011-09-02 17:59:39 +0000
287+++ ubuntuone/controlpanel/gui/qt/tests/__init__.py 2011-09-08 01:17:23 +0000
288@@ -22,7 +22,7 @@
289 import os
290 import urllib
291
292-from PyQt4 import QtCore
293+from PyQt4 import QtGui, QtCore
294 from ubuntuone.devtools.handlers import MementoHandler
295
296 from ubuntuone.controlpanel import backend, cache
297@@ -126,9 +126,32 @@
298 return TOKEN
299
300
301-class FakedConfirmDialog(object):
302+class CrashyBackendException(Exception):
303+ """A faked backend crash."""
304+
305+
306+class CrashyBackend(FakedControlPanelBackend):
307+ """A faked backend that crashes."""
308+
309+ def __init__(self, *args, **kwargs):
310+ super(CrashyBackend, self).__init__(*args, **kwargs)
311+ for i in self.exposed_methods + ['get_credentials']:
312+ setattr(self, i, self._fail(i))
313+
314+ def _fail(self, f):
315+ """Crash boom bang."""
316+
317+ def inner(*args, **kwargs):
318+ """Raise a custom exception."""
319+ raise CrashyBackendException(f)
320+
321+ return inner
322+
323+
324+class FakedDialog(object):
325 """Fake a confirmation dialog."""
326
327+ Close = 0
328 response = args = kwargs = None
329
330 @classmethod
331@@ -163,6 +186,7 @@
332 innerclass_name = None
333 class_ui = None
334 kwargs = {}
335+ logger = None
336
337 def setUp(self):
338 cache.Cache._shared_objects = {}
339@@ -180,11 +204,25 @@
340 if getattr(self.ui, 'backend', None) is not None:
341 self.addCleanup(self.ui.backend._called.clear)
342
343- if getattr(self.ui, 'logger', None) is not None:
344+ logger = self.logger if self.logger is not None else \
345+ getattr(self.ui, 'logger', None)
346+ self.memento = None
347+ if logger is not None:
348 self.memento = MementoHandler()
349 self.memento.setLevel(logging.DEBUG)
350- self.ui.logger.addHandler(self.memento)
351- self.addCleanup(self.ui.logger.removeHandler, self.memento)
352+ logger.addHandler(self.memento)
353+ self.addCleanup(logger.removeHandler, self.memento)
354+
355+ # default response if user does nothing
356+ FakedFileDialog.response = QtCore.QString('')
357+ FakedFileDialog.args = None
358+ FakedFileDialog.kwargs = None
359+ self.patch(QtGui, 'QFileDialog', FakedFileDialog)
360+
361+ FakedDialog.response = None
362+ FakedDialog.args = None
363+ FakedDialog.kwargs = None
364+ self.patch(QtGui, 'QMessageBox', FakedDialog)
365
366 def get_pixmap_data(self, pixmap):
367 """Get the raw data of a QPixmap."""
368
369=== modified file 'ubuntuone/controlpanel/gui/qt/tests/test_addfolder.py'
370--- ubuntuone/controlpanel/gui/qt/tests/test_addfolder.py 2011-08-29 13:28:58 +0000
371+++ ubuntuone/controlpanel/gui/qt/tests/test_addfolder.py 2011-09-08 01:17:23 +0000
372@@ -29,7 +29,9 @@
373 from ubuntuone.controlpanel.gui.qt import addfolder as gui
374 from ubuntuone.controlpanel.gui.qt.tests import (
375 BaseTestCase,
376- FakedConfirmDialog,
377+ CrashyBackend,
378+ CrashyBackendException,
379+ FakedDialog,
380 FakedFileDialog,
381 set_path_on_file_dialog,
382 )
383@@ -53,20 +55,6 @@
384 os.environ['HOME'] = USER_HOME
385 self.addCleanup(lambda: os.environ.__setitem__('HOME', old_home))
386
387- # default response if user does nothing
388- FakedFileDialog.response = gui.QtCore.QString('')
389- FakedFileDialog.args = None
390- FakedFileDialog.kwargs = None
391- self.patch(gui.QtGui, 'QFileDialog', FakedFileDialog)
392-
393- FakedConfirmDialog.response = None
394- FakedConfirmDialog.args = None
395- FakedConfirmDialog.kwargs = None
396- self.patch(gui.QtGui, 'QMessageBox', FakedConfirmDialog)
397-
398- # reset backend state
399- self.ui.backend._called.clear()
400-
401 @defer.inlineCallbacks
402 def assert_does_nothing(self, method_call):
403 """Nothing happens.
404@@ -103,8 +91,8 @@
405 yield self.assert_does_nothing(self.ui.click)
406
407 # the warning dialog was not opened
408- self.assertEqual(FakedConfirmDialog.args, None)
409- self.assertEqual(FakedConfirmDialog.kwargs, None)
410+ self.assertEqual(FakedDialog.args, None)
411+ self.assertEqual(FakedDialog.kwargs, None)
412
413 @defer.inlineCallbacks
414 def test_opens_warning_if_folder_path_not_valid(self):
415@@ -116,9 +104,9 @@
416
417 args = {'folder_path': folder_path, 'home_folder': USER_HOME}
418 msg = gui.FOLDER_INVALID_PATH % args
419- self.assertEqual(FakedConfirmDialog.args,
420+ self.assertEqual(FakedDialog.args,
421 (self.ui, '', msg, gui.CLOSE))
422- self.assertEqual(FakedConfirmDialog.kwargs, {})
423+ self.assertEqual(FakedDialog.kwargs, {})
424
425 yield self.assert_does_nothing(self.ui.click)
426
427@@ -129,8 +117,8 @@
428 yield self.ui.click()
429
430 # no warning
431- self.assertEqual(FakedConfirmDialog.args, None)
432- self.assertEqual(FakedConfirmDialog.kwargs, None)
433+ self.assertEqual(FakedDialog.args, None)
434+ self.assertEqual(FakedDialog.kwargs, None)
435 # backend called
436 self.assert_backend_called('create_folder', folder_path=folder)
437
438@@ -141,8 +129,8 @@
439 yield self.ui.click()
440
441 # no warning
442- self.assertEqual(FakedConfirmDialog.args, None)
443- self.assertEqual(FakedConfirmDialog.kwargs, None)
444+ self.assertEqual(FakedDialog.args, None)
445+ self.assertEqual(FakedDialog.kwargs, None)
446 # backend called
447 self.assert_backend_called('create_folder', folder_path=folder)
448
449@@ -170,3 +158,12 @@
450 yield self.ui.click()
451
452 self.assertEqual(self._called, ((), {}))
453+
454+ @defer.inlineCallbacks
455+ def test_backend_error_is_handled(self):
456+ """Any error from the backend is properly handled."""
457+ set_path_on_file_dialog()
458+ self.patch(self.ui, 'backend', CrashyBackend())
459+ yield self.ui.click()
460+
461+ self.assertTrue(self.memento.check_exception(CrashyBackendException))
462
463=== renamed file 'ubuntuone/controlpanel/gui/qt/tests/test_urihook.py' => 'ubuntuone/controlpanel/gui/qt/tests/test_common.py'
464--- ubuntuone/controlpanel/gui/qt/tests/test_urihook.py 2011-09-01 17:11:16 +0000
465+++ ubuntuone/controlpanel/gui/qt/tests/test_common.py 2011-09-08 01:17:23 +0000
466@@ -18,12 +18,17 @@
467
468 """Tests for the uri_hook helper."""
469
470+import logging
471+
472 from PyQt4 import QtGui, QtCore
473-
474 from twisted.internet import defer
475
476-from ubuntuone.controlpanel.gui.qt import uri_hook
477-from ubuntuone.controlpanel.gui.qt.tests import BaseTestCase
478+from ubuntuone.controlpanel.logger import setup_logging
479+from ubuntuone.controlpanel.gui.qt import uri_hook, handle_errors
480+from ubuntuone.controlpanel.gui.qt.tests import (
481+ BaseTestCase,
482+ FakedDialog,
483+)
484
485
486 class UriHookTestCase(BaseTestCase):
487@@ -45,3 +50,161 @@
488 expected = 'http://foo.bar/?next=https://one.ubuntu.com/'
489 uri_hook(expected)
490 self.assertEqual(self._called, ((QtCore.QUrl(expected),), {}))
491+
492+
493+class HandleErrorTestCase(BaseTestCase):
494+ """Test suite for the generic error handler."""
495+
496+ error_handler = None
497+ use_logger = False
498+ logger = logging.getLogger() # root logger
499+
500+ @defer.inlineCallbacks
501+ def setUp(self):
502+ yield super(HandleErrorTestCase, self).setUp()
503+ self.called = None
504+ self.result = None
505+ self.failure = None
506+ self.error_handler_called = None
507+
508+ if self.use_logger:
509+ logger = self.logger
510+ else:
511+ logger = None
512+
513+ self.decorate_me = handle_errors(error_handler=self.error_handler,
514+ logger=logger)(self._decorate_me)
515+
516+ @defer.inlineCallbacks
517+ def _decorate_me(self, *args, **kwargs):
518+ """Helper to test thye decorator."""
519+ if self.failure:
520+ # Raising only classes, instances or string are allowed
521+ # pylint: disable=E0702
522+ raise self.failure
523+
524+ yield
525+ self.called = (args, kwargs)
526+ defer.returnValue(self.result)
527+
528+ @defer.inlineCallbacks
529+ def test_is_decorator(self):
530+ """Is a decorator."""
531+ yield self.decorate_me()
532+
533+ @defer.inlineCallbacks
534+ def test_params_are_passed(self):
535+ """Named and unnamed arguments are passed."""
536+ args = ({}, object(), 'foo')
537+ kwargs = {'1': 1, 'test': None, '0': ['a']}
538+ yield self.decorate_me(*args, **kwargs)
539+
540+ self.assertTrue(self.called, (args, kwargs))
541+
542+ @defer.inlineCallbacks
543+ def test_result_is_returned(self):
544+ """Result is returned."""
545+ self.result = object()
546+ result = yield self.decorate_me()
547+
548+ self.assertEqual(self.result, result)
549+
550+ def test_name_is_preserved(self):
551+ """The method name is not masqueraded."""
552+ self.assertEqual(self.decorate_me.__name__, self._decorate_me.__name__)
553+
554+ @defer.inlineCallbacks
555+ def test_exeptions_are_handled(self):
556+ """Any exception is handled and logged in the root logger."""
557+ msg = 'This is me failing badly.'
558+ self.failure = Exception(msg)
559+
560+ yield self.decorate_me()
561+
562+ logged = self.memento.check_exception(self.failure.__class__, msg)
563+ recs = '\n'.join(rec.exc_text for rec in self.memento.records)
564+ self.assertTrue(logged, 'Exception must be logged, got:\n%s' % recs)
565+
566+ @defer.inlineCallbacks
567+ def test_show_error_message(self):
568+ """On error, show an error message."""
569+ self.failure = Exception()
570+
571+ yield self.decorate_me()
572+
573+ msg = self.failure.__class__.__name__
574+ self.assertEqual(FakedDialog.args,
575+ (None, '', msg, QtGui.QMessageBox.Close))
576+ self.assertEqual(FakedDialog.kwargs, {})
577+
578+ @defer.inlineCallbacks
579+ def test_show_error_message_if_args(self):
580+ """On error, show an error message."""
581+ msg1 = 'This is me failing badly.'
582+ msg2 = 'More details about what went wrong.'
583+ obj = object()
584+ self.failure = Exception(msg1, msg2, obj)
585+
586+ yield self.decorate_me()
587+
588+ msg = '\n'.join(map(repr, (msg1, msg2, obj)))
589+ msg = self.failure.__class__.__name__ + '\n' + msg
590+ self.assertEqual(FakedDialog.args,
591+ (None, '', msg, QtGui.QMessageBox.Close))
592+ self.assertEqual(FakedDialog.kwargs, {})
593+
594+ @defer.inlineCallbacks
595+ def test_show_error_message_if_mapping(self):
596+ """On error, show an error message."""
597+ msg1 = 'This is me failing badly.'
598+ msg2 = 'More details about what went wrong.'
599+ errdict = {'foo': msg1, 'bar': msg2}
600+ self.failure = Exception(errdict)
601+
602+ yield self.decorate_me()
603+
604+ msg = '\n'.join((msg1, msg2))
605+ self.assertEqual(FakedDialog.args,
606+ (None, '', msg, QtGui.QMessageBox.Close))
607+ self.assertEqual(FakedDialog.kwargs, {})
608+
609+ @defer.inlineCallbacks
610+ def test_no_error_message_if_no_exception(self):
611+ """On success, do not show an error message."""
612+ yield self.decorate_me()
613+
614+ self.assertEqual(FakedDialog.args, None)
615+ self.assertEqual(FakedDialog.kwargs, None)
616+
617+ @defer.inlineCallbacks
618+ def test_call_error_handler(self):
619+ """On success, do not execute error_handler."""
620+ yield self.decorate_me()
621+ self.assertFalse(self.error_handler_called)
622+
623+
624+class HandleErrorWithCustomLoggerTestCase(HandleErrorTestCase):
625+ """Test suite for the generic error handler."""
626+
627+ use_logger = True
628+ logger = setup_logging('HandleErrorWithoutLoggerTestCase') # custom logger
629+
630+
631+class HandleErrorWithHandlerTestCase(HandleErrorTestCase):
632+ """Test suite for the generic error handler when using a custom handler."""
633+
634+ @defer.inlineCallbacks
635+ def error_handler(self, *a, **kw):
636+ """Implement an error handler."""
637+ self.error_handler_called = (a, kw)
638+ yield
639+
640+ @defer.inlineCallbacks
641+ def test_call_error_handler(self):
642+ """On error, execute error_handler."""
643+ msg = 'This is me failing badly.'
644+ self.failure = Exception(msg)
645+
646+ yield self.decorate_me()
647+
648+ self.assertEqual(self.error_handler_called, ((), {}))
649
650=== modified file 'ubuntuone/controlpanel/gui/qt/tests/test_device.py'
651--- ubuntuone/controlpanel/gui/qt/tests/test_device.py 2011-09-02 17:59:39 +0000
652+++ ubuntuone/controlpanel/gui/qt/tests/test_device.py 2011-09-08 01:17:23 +0000
653@@ -23,7 +23,9 @@
654 from ubuntuone.controlpanel.gui.qt import device as gui
655 from ubuntuone.controlpanel.gui.qt.tests import (
656 BaseTestCase,
657- FakedConfirmDialog,
658+ CrashyBackend,
659+ CrashyBackendException,
660+ FakedDialog,
661 SAMPLE_COMPUTER_INFO,
662 SAMPLE_PHONE_INFO,
663 )
664@@ -41,6 +43,7 @@
665 class_ui = gui.DeviceWidget
666 device_id = 'zaraza'
667 kwargs = {'device_id': device_id}
668+ logger = gui.logger
669
670 def test_has_id(self):
671 """The device as an id, None by default."""
672@@ -98,10 +101,7 @@
673 @defer.inlineCallbacks
674 def setUp(self):
675 yield super(RemoveDeviceTestCase, self).setUp()
676- FakedConfirmDialog.response = gui.NO
677- FakedConfirmDialog.args = None
678- FakedConfirmDialog.kwargs = None
679- self.patch(gui.QtGui, 'QMessageBox', FakedConfirmDialog)
680+ FakedDialog.response = gui.YES
681
682 def test_remove_device_opens_confirmation_dialog(self):
683 """A confirmation dialog is opened when user clicks 'delete device'."""
684@@ -109,13 +109,13 @@
685
686 msg = gui.DEVICE_CONFIRM_REMOVE
687 buttons = gui.YES | gui.NO
688- self.assertEqual(FakedConfirmDialog.args,
689+ self.assertEqual(FakedDialog.args,
690 (self.ui, '', msg, buttons, gui.NO))
691- self.assertEqual(FakedConfirmDialog.kwargs, {})
692+ self.assertEqual(FakedDialog.kwargs, {})
693
694 def test_remove_device_does_not_remove_if_answer_is_no(self):
695 """The device is not removed is answer is No."""
696- FakedConfirmDialog.response = gui.NO
697+ FakedDialog.response = gui.NO
698 self.ui.removed.connect(self._set_called)
699 self.ui.ui.remove_device_button.click()
700
701@@ -124,7 +124,7 @@
702
703 def test_remove_device_does_remove_if_answer_is_yes(self):
704 """The device is removed is answer is Yes."""
705- FakedConfirmDialog.response = gui.YES
706+ FakedDialog.response = gui.YES
707 self.ui.ui.remove_device_button.click()
708
709 self.assert_backend_called('remove_device', device_id=self.device_id)
710@@ -138,7 +138,7 @@
711 """Fire deferred when the device was removed."""
712 d.callback(device_id)
713
714- FakedConfirmDialog.response = gui.YES
715+ FakedDialog.response = gui.YES
716 self.ui.removed.connect(self._set_called)
717 self.patch(self.ui.backend, 'remove_device', check)
718 self.ui.ui.remove_device_button.click()
719@@ -148,8 +148,16 @@
720
721 def test_remove_device_emits_signal_when_not_removed(self):
722 """The signal 'removeCanceled' is emitted when user cancels removal."""
723- FakedConfirmDialog.response = gui.NO
724+ FakedDialog.response = gui.NO
725 self.ui.removeCanceled.connect(self._set_called)
726 self.ui.ui.remove_device_button.click()
727
728 self.assertEqual(self._called, ((), {}))
729+
730+ @defer.inlineCallbacks
731+ def test_backend_error_is_handled(self):
732+ """Any error from the backend is properly handled."""
733+ self.patch(self.ui, 'backend', CrashyBackend())
734+ yield self.ui.ui.remove_device_button.click()
735+
736+ self.assertTrue(self.memento.check_exception(CrashyBackendException))
737
738=== modified file 'ubuntuone/controlpanel/gui/qt/tests/test_devices.py'
739--- ubuntuone/controlpanel/gui/qt/tests/test_devices.py 2011-09-01 17:11:16 +0000
740+++ ubuntuone/controlpanel/gui/qt/tests/test_devices.py 2011-09-08 01:17:23 +0000
741@@ -22,7 +22,6 @@
742
743 from ubuntuone.controlpanel.gui.qt import devices as gui
744 from ubuntuone.controlpanel.gui.qt.tests import (
745- FakedConfirmDialog,
746 SAMPLE_DEVICES_INFO,
747 )
748 from ubuntuone.controlpanel.gui.qt.tests.test_ubuntuonebin import (
749@@ -41,7 +40,6 @@
750 def setUp(self):
751 yield super(DevicesPanelTestCase, self).setUp()
752 self.ui.backend.next_result = SAMPLE_DEVICES_INFO
753- self.patch(gui.QtGui, 'QMessageBox', FakedConfirmDialog)
754
755 def test_is_processing_while_asking_info(self):
756 """The ui is processing while the contents are loaded."""
757
758=== modified file 'ubuntuone/controlpanel/gui/qt/tests/test_folders.py'
759--- ubuntuone/controlpanel/gui/qt/tests/test_folders.py 2011-09-01 17:11:16 +0000
760+++ ubuntuone/controlpanel/gui/qt/tests/test_folders.py 2011-09-08 01:17:23 +0000
761@@ -33,8 +33,7 @@
762 )
763 from ubuntuone.controlpanel.gui.qt import folders as gui
764 from ubuntuone.controlpanel.gui.qt.tests import (
765- FakedConfirmDialog,
766- FakedFileDialog,
767+ FakedDialog,
768 )
769 from ubuntuone.controlpanel.gui.qt.tests.test_ubuntuonebin import (
770 UbuntuOneBinTestCase,
771@@ -320,17 +319,6 @@
772 # reset backend state
773 self.ui.backend._called.clear()
774
775- # default response if user does nothing
776- FakedFileDialog.response = gui.QtCore.QString('')
777- FakedFileDialog.args = None
778- FakedFileDialog.kwargs = None
779- self.patch(gui.QtGui, 'QFileDialog', FakedFileDialog)
780-
781- FakedConfirmDialog.response = None
782- FakedConfirmDialog.args = None
783- FakedConfirmDialog.kwargs = None
784- self.patch(gui.QtGui, 'QMessageBox', FakedConfirmDialog)
785-
786 def test_not_is_processing(self):
787 """Before clicking the add folder button, the UI is not processing."""
788 self.assertFalse(self.ui.is_processing, 'ui must not be processing')
789@@ -354,10 +342,7 @@
790 def setUp(self):
791 yield super(FoldersPanelSubscriptionTestCase, self).setUp()
792 self.patch(gui.os.path, 'exists', lambda path: True)
793- FakedConfirmDialog.response = gui.YES
794- FakedConfirmDialog.args = None
795- FakedConfirmDialog.kwargs = None
796- self.patch(gui.QtGui, 'QMessageBox', FakedConfirmDialog)
797+ FakedDialog.response = gui.YES
798
799 self.ui.process_info(FAKE_VOLUMES_MINIMAL_INFO)
800 # the music folder
801@@ -436,9 +421,9 @@
802 volume_path = self.item.volume_path
803 msg = gui.FOLDERS_CONFIRM_MERGE % {'folder_path': volume_path}
804 buttons = gui.YES | gui.NO | gui.CANCEL
805- self.assertEqual(FakedConfirmDialog.args,
806+ self.assertEqual(FakedDialog.args,
807 (self.ui, '', msg, buttons, gui.YES))
808- self.assertEqual(FakedConfirmDialog.kwargs, {})
809+ self.assertEqual(FakedDialog.kwargs, {})
810
811 @defer.inlineCallbacks
812 def test_confirm_dialog_if_path_does_not_exist(self):
813@@ -452,14 +437,13 @@
814
815 yield self.ui.on_folders_itemChanged(self.item)
816
817- self.assertEqual(FakedConfirmDialog.args, None)
818- self.assertEqual(FakedConfirmDialog.kwargs, None)
819+ self.assertEqual(FakedDialog.args, None)
820+ self.assertEqual(FakedDialog.kwargs, None)
821
822 @defer.inlineCallbacks
823 def test_subscribe_does_not_call_backend_if_dialog_closed(self):
824 """Backend is not called if users closes the confirmation dialog."""
825- FakedConfirmDialog.response = gui.CANCEL
826- self.patch(gui.QtGui, 'QMessageBox', FakedConfirmDialog)
827+ FakedDialog.response = gui.CANCEL
828
829 # make sure the item is subscribed
830 self.ui.is_processing = True
831@@ -468,7 +452,7 @@
832
833 yield self.ui.on_folders_itemChanged(self.item)
834
835- self.assertFalse(FakedConfirmDialog.args is None, 'warning was called')
836+ self.assertFalse(FakedDialog.args is None, 'warning was called')
837 self.assertNotIn('change_volume_settings', self.ui.backend._called)
838 self.assertFalse(self.ui.is_processing)
839
840@@ -478,8 +462,7 @@
841 @defer.inlineCallbacks
842 def test_subscribe_does_not_call_backend_if_answer_is_no(self):
843 """Backend is not called if users clicks on 'No'."""
844- FakedConfirmDialog.response = gui.NO
845- self.patch(gui.QtGui, 'QMessageBox', FakedConfirmDialog)
846+ FakedDialog.response = gui.NO
847
848 # make sure the item is subscribed
849 self.ui.is_processing = True
850@@ -488,7 +471,7 @@
851
852 yield self.ui.on_folders_itemChanged(self.item)
853
854- self.assertFalse(FakedConfirmDialog.args is None, 'warning was called')
855+ self.assertFalse(FakedDialog.args is None, 'warning was called')
856 self.assertNotIn('change_volume_settings', self.ui.backend._called)
857 self.assertFalse(self.ui.is_processing)
858
859@@ -504,10 +487,10 @@
860 self.ui.is_processing = False
861
862 # the confirm dialog was not called so far
863- assert FakedConfirmDialog.args is None
864- assert FakedConfirmDialog.kwargs is None
865+ assert FakedDialog.args is None
866+ assert FakedDialog.kwargs is None
867
868 yield self.ui.on_folders_itemChanged(self.item)
869
870- self.assertTrue(FakedConfirmDialog.args is None, 'dialog was not run')
871- self.assertTrue(FakedConfirmDialog.kwargs is None, 'dialog was hid')
872+ self.assertTrue(FakedDialog.args is None, 'dialog was not run')
873+ self.assertTrue(FakedDialog.kwargs is None, 'dialog was hid')
874
875=== modified file 'ubuntuone/controlpanel/gui/qt/tests/test_signin.py'
876--- ubuntuone/controlpanel/gui/qt/tests/test_signin.py 2011-08-31 17:18:56 +0000
877+++ ubuntuone/controlpanel/gui/qt/tests/test_signin.py 2011-09-08 01:17:23 +0000
878@@ -22,6 +22,10 @@
879
880 from ubuntuone.controlpanel.gui import qt
881 from ubuntuone.controlpanel.gui.qt import signin as gui
882+from ubuntuone.controlpanel.gui.qt.tests import (
883+ CrashyBackend,
884+ CrashyBackendException,
885+)
886 from ubuntuone.controlpanel.gui.qt.tests.test_ubuntuonebin import (
887 UbuntuOneBinTestCase,
888 )
889@@ -36,7 +40,7 @@
890
891 def fail(*a, **kw):
892 """Emit CredentialsError."""
893- raise gui.CredentialsError(MSG)
894+ raise TypeError(MSG)
895
896
897 class BaseSignInPanelTestCase(UbuntuOneBinTestCase):
898@@ -81,10 +85,6 @@
899 yield self.ui.ui.signin_button.click()
900 self.assertFalse(self.ui.is_processing)
901
902- def test_warning_label_empty(self):
903- """The warning_label is empty at startup."""
904- self.assertEqual('', unicode(self.ui.ui.warning_label.text()))
905-
906 def test_signin_disabled_at_startup(self):
907 """The signin_button is disabled at startup."""
908 self.assertFalse(self.ui.ui.signin_button.isEnabled())
909@@ -139,31 +139,7 @@
910 yield self.ui.ui.signin_button.click()
911
912 self.assertEqual(self._called, ((TOKEN,), {}))
913- self.assertEqual('', unicode(self.ui.ui.warning_label.text()))
914- self.assertFalse(self.ui.is_processing)
915-
916- @defer.inlineCallbacks
917- def test_signin_credentials_error(self):
918- """Show error when CredentialsError was raised from the backend."""
919- self.patch(self.ui.backend, 'login', fail)
920- self.ui.credentialsFound.connect(self._set_called)
921- yield self.ui.ui.signin_button.click()
922-
923- self.assertFalse(self._called, 'credentialsFound must not be emitted.')
924- self.assertEqual(MSG['message'],
925- unicode(self.ui.ui.warning_label.text()))
926- self.assertFalse(self.ui.is_processing)
927- self.assertTrue(self.memento.check_info('signin_button_clicked',
928- repr(MSG)))
929-
930- @defer.inlineCallbacks
931- def test_signin_success_after_error(self):
932- """Emit credentialsFound on signin success."""
933- self.patch(self.ui.backend, 'login', fail) # login failed
934- yield self.ui.ui.signin_button.click()
935-
936- self.patch(self.ui.backend, 'login', lambda *a, **kw: TOKEN) # success
937- yield self.test_signin_success()
938+ self.assertFalse(self.ui.is_processing)
939
940 def test_signin_enabled_if_email_and_password(self):
941 """Enable signin_button if email and password are non empty."""
942@@ -182,3 +158,11 @@
943 self.assertEqual(1, receivers)
944
945 self._called = False
946+
947+ @defer.inlineCallbacks
948+ def test_backend_error_is_handled(self):
949+ """Any error from the backend is properly handled."""
950+ self.patch(self.ui, 'backend', CrashyBackend())
951+ yield self.ui.ui.signin_button.click()
952+
953+ self.assertTrue(self.memento.check_exception(CrashyBackendException))
954
955=== modified file 'ubuntuone/controlpanel/gui/qt/tests/test_ubuntuonebin.py'
956--- ubuntuone/controlpanel/gui/qt/tests/test_ubuntuonebin.py 2011-09-02 17:59:39 +0000
957+++ ubuntuone/controlpanel/gui/qt/tests/test_ubuntuonebin.py 2011-09-08 01:17:23 +0000
958@@ -18,10 +18,15 @@
959
960 """Tests for the Ubuntu One Bin."""
961
962+from twisted.internet import defer
963 from ubuntuone.devtools.testcase import skipIfOS
964
965 from ubuntuone.controlpanel.gui.qt import ubuntuonebin as gui
966-from ubuntuone.controlpanel.gui.qt.tests import BaseTestCase
967+from ubuntuone.controlpanel.gui.qt.tests import (
968+ BaseTestCase,
969+ CrashyBackend,
970+ CrashyBackendException,
971+)
972
973 # Attribute 'yyy' defined outside __init__, access to a protected member
974 # pylint: disable=W0201, W0212
975@@ -45,6 +50,7 @@
976 def test_is_enabled_if_not_processing(self):
977 """If not processing, the UI is enabled."""
978 self.ui.show() # need to show to test widgets visibility
979+ self.addCleanup(self.ui.hide)
980
981 self.ui.is_processing = False
982
983@@ -55,8 +61,20 @@
984 def test_is_not_enabled_if_processing(self):
985 """If processing, the UI is disabled."""
986 self.ui.show() # need to show to test widgets visibility
987+ self.addCleanup(self.ui.hide)
988
989 self.ui.is_processing = True
990
991 self.assertFalse(self.ui.isEnabled())
992 self.assertTrue(self.ui.overlay.isVisible())
993+
994+ @defer.inlineCallbacks
995+ def test_backend_error_is_handled(self):
996+ """Any error from the backend is properly handled."""
997+ self.patch(self.ui, 'backend', CrashyBackend())
998+ yield self.ui.load()
999+
1000+ self.assertFalse(self.ui.is_processing)
1001+ if self.memento:
1002+ logged = self.memento.check_exception(CrashyBackendException)
1003+ self.assertTrue(logged)
1004
1005=== modified file 'ubuntuone/controlpanel/gui/qt/ubuntuonebin.py'
1006--- ubuntuone/controlpanel/gui/qt/ubuntuonebin.py 2011-09-02 17:59:39 +0000
1007+++ ubuntuone/controlpanel/gui/qt/ubuntuonebin.py 2011-09-08 01:17:23 +0000
1008@@ -21,6 +21,7 @@
1009 from PyQt4 import QtGui
1010
1011 from ubuntuone.controlpanel import cache
1012+from ubuntuone.controlpanel.gui.qt import handle_errors
1013 from ubuntuone.controlpanel.gui.qt.loadingoverlay import LoadingOverlay
1014
1015
1016@@ -45,6 +46,11 @@
1017 self._is_processing = None
1018 self.is_processing = False
1019
1020+ # pylint: disable=E0202
1021+ handler = handle_errors(logger=self.logger,
1022+ error_handler=self._error_handler)
1023+ self.load = handler(self.load)
1024+
1025 self._setup()
1026
1027 def _get_is_processing(self):
1028@@ -64,6 +70,10 @@
1029
1030 is_processing = property(fget=_get_is_processing, fset=_set_is_processing)
1031
1032+ def _error_handler(self):
1033+ """Custom error handler, unset is_processing."""
1034+ self.is_processing = False
1035+
1036 def _setup(self):
1037 """Do some extra setupping for the UI."""
1038
1039@@ -82,5 +92,6 @@
1040
1041 # pylint: enable=C0103
1042
1043+ # pylint: disable=E0202
1044 def load(self):
1045 """Load the widget with specific info."""
1046
1047=== modified file 'ubuntuone/controlpanel/tests/test_cache.py'
1048--- ubuntuone/controlpanel/tests/test_cache.py 2011-09-02 18:10:13 +0000
1049+++ ubuntuone/controlpanel/tests/test_cache.py 2011-09-08 01:17:23 +0000
1050@@ -39,6 +39,13 @@
1051 """The backend instance is successfully created."""
1052 self.assertIsInstance(self.obj.backend, cache.backend.ControlBackend)
1053
1054+ def test_set_backend(self):
1055+ """The backend instance is successfully assigned."""
1056+ expected = object()
1057+ assert self.obj.backend is not expected
1058+ self.obj.backend = expected
1059+ self.assertTrue(self.obj.backend is expected)
1060+
1061 def test_backend_is_cached(self):
1062 """The backend instance is cached."""
1063 obj2 = cache.Cache()

Subscribers

People subscribed via source and target branches