Merge lp:~mterry/update-manager/continue-on-error into lp:update-manager

Proposed by Michael Terry
Status: Merged
Merged at revision: 2566
Proposed branch: lp:~mterry/update-manager/continue-on-error
Merge into: lp:update-manager
Diff against target: 417 lines (+150/-45)
10 files modified
UpdateManager/Dialogs.py (+21/-10)
UpdateManager/InstallProgress.py (+4/-1)
UpdateManager/UpdateManager.py (+15/-9)
UpdateManager/UpdateProgress.py (+6/-2)
UpdateManager/UpdatesAvailable.py (+4/-2)
UpdateManager/backend/InstallBackendAptdaemon.py (+13/-11)
UpdateManager/backend/InstallBackendSynaptic.py (+2/-1)
UpdateManager/backend/__init__.py (+3/-1)
tests/test_stop_update.py (+11/-8)
tests/test_update_error.py (+71/-0)
To merge this branch: bzr merge lp:~mterry/update-manager/continue-on-error
Reviewer Review Type Date Requested Status
Michael Vogt (community) Approve
Review via email: mp+139789@code.launchpad.net

Description of the change

When an update error occurs, currently Software Updater just stops completely with the error. This branch implements the part of the spec [1] that covers errors during update. Specifically:

* Show a dialog with the error, and Settings, Try Again, and OK buttons.
* If OK is pressed with updates, show a sub-header that notes there were some errors.
* If OK is pressed with no updates, show a slightly different message than normal (one less confident that system is up to date).

Tests added too for most of the code paths.

[1] Spec not quite updated yet, but see mpt's comments: https://bugs.launchpad.net/ubuntu/+source/update-manager/+bug/1049046/comments/5

To post a comment you must log in.
Revision history for this message
Michael Vogt (mvo) wrote :

This looks good, +1. Nice tests as well.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'UpdateManager/Dialogs.py'
2--- UpdateManager/Dialogs.py 2012-12-13 19:56:07 +0000
3+++ UpdateManager/Dialogs.py 2012-12-13 20:47:25 +0000
4@@ -112,9 +112,12 @@
5
6
7 class NoUpdatesDialog(Dialog):
8- def __init__(self, window_main):
9+ def __init__(self, window_main, error_occurred=False):
10 Dialog.__init__(self, window_main)
11- self.set_header(_("The software on this computer is up to date."))
12+ if error_occurred:
13+ self.set_header(_("No software updates are available."))
14+ else:
15+ self.set_header(_("The software on this computer is up to date."))
16 self.add_settings_button()
17 self.focus_button = self.add_button(Gtk.STOCK_OK, self.close)
18
19@@ -198,15 +201,23 @@
20 def main(self):
21 Dialog.main(self)
22 # The label likes to start selecting everything (b/c it got focus
23- # before
24- # we switched to our default button).
25+ # before we switched to our default button).
26 self.label_desc.select_region(0, 0)
27- # Since errors usually are outside the normal flow, we'll guarantee
28- # that
29- # we don't continue with normal code flow by running our own loop here.
30- # We won't screw anything up because the only thing this dialog will do
31- # is exit.
32- Gtk.main()
33+
34+
35+class UpdateErrorDialog(ErrorDialog):
36+ def __init__(self, window_main, header, desc=None):
37+ ErrorDialog.__init__(self, window_main, header, desc)
38+ # Get rid of normal error dialog button before adding our own
39+ self.focus_button.destroy()
40+ self.add_button(_("_Try Again"), self.update)
41+ self.focus_button = self.add_button(Gtk.STOCK_OK, self.available)
42+
43+ def update(self):
44+ self.window_main.start_update()
45+
46+ def available(self):
47+ self.window_main.start_available(error_occurred=True)
48
49
50 class NeedRestartDialog(Dialog):
51
52=== modified file 'UpdateManager/InstallProgress.py'
53--- UpdateManager/InstallProgress.py 2012-07-06 19:25:03 +0000
54+++ UpdateManager/InstallProgress.py 2012-12-13 20:47:25 +0000
55@@ -81,7 +81,8 @@
56 pkgs_upgrade.append(pkg.name)
57 self.install_backend.commit(pkgs_install, pkgs_upgrade, close_on_done)
58
59- def _on_backend_done(self, backend, action, authorized, success):
60+ def _on_backend_done(self, backend, action, authorized, success,
61+ error_string, error_desc):
62 # Allow suspend after synaptic is finished
63 if self.sleep_cookie:
64 allow_sleep(self.sleep_dev, self.sleep_cookie)
65@@ -90,6 +91,8 @@
66 # Either launch main dialog and continue or quit altogether
67 if success:
68 self.window_main.start_available()
69+ elif error_string:
70+ self.window_main.start_error(False, error_string, error_desc)
71 else:
72 sys.exit(0)
73
74
75=== modified file 'UpdateManager/UpdateManager.py'
76--- UpdateManager/UpdateManager.py 2012-11-20 13:09:01 +0000
77+++ UpdateManager/UpdateManager.py 2012-12-13 20:47:25 +0000
78@@ -48,7 +48,8 @@
79 NoUpdatesDialog,
80 StoppedUpdatesDialog,
81 PartialUpgradeDialog,
82- UnsupportedDialog)
83+ UnsupportedDialog,
84+ UpdateErrorDialog)
85 from .InstallProgress import InstallProgress
86 from .MetaReleaseGObject import MetaRelease
87 from .UpdateProgress import UpdateProgress
88@@ -163,17 +164,17 @@
89
90 self._start_pane(UpdateProgress(self))
91
92- def start_available(self, cancelled_update=False):
93+ def start_available(self, cancelled_update=False, error_occurred=True):
94 self._look_busy()
95 self.refresh_cache()
96
97 pane = self._make_available_pane(self.cache.install_count,
98 os.path.exists(REBOOT_REQUIRED_FILE),
99- cancelled_update)
100+ cancelled_update, error_occurred)
101 self._start_pane(pane)
102
103- def _make_available_pane(self, install_count, need_reboot,
104- cancelled_update):
105+ def _make_available_pane(self, install_count, need_reboot=False,
106+ cancelled_update=False, error_occurred=False):
107 if install_count == 0:
108 # Need Restart > New Release > No Updates
109 if need_reboot:
110@@ -184,11 +185,13 @@
111 elif cancelled_update:
112 return StoppedUpdatesDialog(self)
113 else:
114- return NoUpdatesDialog(self)
115+ return NoUpdatesDialog(self, error_occurred=error_occurred)
116 else:
117 header = None
118 desc = None
119- if cancelled_update:
120+ if error_occurred:
121+ desc = _("Some software couldn’t be checked for updates.")
122+ elif cancelled_update:
123 header = _("You stopped the check for updates.")
124 desc = _("Updated software is available from "
125 "a previous check.")
126@@ -197,8 +200,11 @@
127 def start_install(self):
128 self._start_pane(InstallProgress(self))
129
130- def start_error(self, header, desc):
131- self._start_pane(ErrorDialog(self, header, desc))
132+ def start_error(self, is_update_error, header, desc):
133+ if is_update_error:
134+ self._start_pane(UpdateErrorDialog(self, header, desc))
135+ else:
136+ self._start_pane(ErrorDialog(self, header, desc))
137
138 def _start_pane(self, pane):
139 self._look_busy()
140
141=== modified file 'UpdateManager/UpdateProgress.py'
142--- UpdateManager/UpdateProgress.py 2012-08-24 20:29:12 +0000
143+++ UpdateManager/UpdateProgress.py 2012-12-13 20:47:25 +0000
144@@ -65,13 +65,17 @@
145
146 self.install_backend.update()
147
148- def _on_backend_done(self, backend, action, authorized, success):
149+ def _on_backend_done(self, backend, action, authorized, success,
150+ error_string, error_desc):
151 # Allow suspend after synaptic is finished
152 if self.sleep_cookie:
153 allow_sleep(self.sleep_dev, self.sleep_cookie)
154 self.sleep_cookie = self.sleep_dev = None
155
156- self.window_main.start_available(not success)
157+ if error_string:
158+ self.window_main.start_error(True, error_string, error_desc)
159+ else:
160+ self.window_main.start_available(not success)
161
162 def main(self):
163 self.invoke_manager()
164
165=== modified file 'UpdateManager/UpdatesAvailable.py'
166--- UpdateManager/UpdatesAvailable.py 2012-11-19 16:33:28 +0000
167+++ UpdateManager/UpdatesAvailable.py 2012-12-13 20:47:25 +0000
168@@ -468,7 +468,6 @@
169
170 if self.custom_header is not None:
171 text_header = self.custom_header
172- text_desc = self.custom_desc
173 # show different text on first run (UX team suggestion)
174 elif self.settings.get_boolean("first-run"):
175 flavor = self.window_main.meta_release.flavor_name
176@@ -481,6 +480,9 @@
177 text_header = _("Updated software is available for this "
178 "computer. Do you want to install it now?")
179
180+ if self.custom_desc is not None:
181+ text_desc = self.custom_desc
182+
183 self.notebook_details.set_sensitive(True)
184 self.treeview_update.set_sensitive(True)
185 self.button_install.grab_default()
186@@ -556,7 +558,7 @@
187 self.cache.checkFreeSpace()
188 except NotEnoughFreeSpaceError as e:
189 for req in e.free_space_required_list:
190- self.window_main.start_error(err_sum,
191+ self.window_main.start_error(False, err_sum,
192 err_long % (req.size_total,
193 req.dir,
194 req.size_needed,
195
196=== modified file 'UpdateManager/backend/InstallBackendAptdaemon.py'
197--- UpdateManager/backend/InstallBackendAptdaemon.py 2012-08-20 02:39:19 +0000
198+++ UpdateManager/backend/InstallBackendAptdaemon.py 2012-12-13 20:47:25 +0000
199@@ -13,12 +13,13 @@
200 from aptdaemon.gtk3widgets import (AptCancelButton,
201 AptConfigFileConflictDialog,
202 AptDetailsExpander,
203- AptErrorDialog,
204 AptMediumRequiredDialog,
205 AptProgressBar)
206 from aptdaemon.enums import (EXIT_SUCCESS,
207 EXIT_FAILED,
208 STATUS_COMMITTING,
209+ get_error_description_from_enum,
210+ get_error_string_from_enum,
211 get_status_string_from_enum)
212
213 from UpdateManager.backend import InstallBackend
214@@ -62,9 +63,9 @@
215 _("Checking for updates…"),
216 False, False)
217 except errors.NotAuthorizedError:
218- self.emit("action-done", self.UPDATE, False, False)
219+ self.emit("action-done", self.UPDATE, False, False, None, None)
220 except:
221- self.emit("action-done", self.UPDATE, True, False)
222+ self.emit("action-done", self.UPDATE, True, False, None, None)
223 raise
224
225 @inline_callbacks
226@@ -84,14 +85,14 @@
227 _("Installing updates…"),
228 True, close_on_done)
229 except errors.NotAuthorizedError as e:
230- self.emit("action-done", self.INSTALL, False, False)
231+ self.emit("action-done", self.INSTALL, False, False, None, None)
232 except dbus.DBusException as e:
233 #print(e, e.get_dbus_name())
234 if e.get_dbus_name() != "org.freedesktop.DBus.Error.NoReply":
235 raise
236- self.emit("action-done", self.INSTALL, False, False)
237+ self.emit("action-done", self.INSTALL, False, False, None, None)
238 except Exception as e:
239- self.emit("action-done", self.INSTALL, True, False)
240+ self.emit("action-done", self.INSTALL, True, False, None, None)
241 raise
242
243 def _on_progress_changed(self, trans, progress):
244@@ -223,16 +224,17 @@
245 transaction.resolve_config_file_conflict(old, "keep")
246
247 def _on_finished(self, trans, status, action, close_on_done):
248+ error_string = None
249+ error_desc = None
250 if status == EXIT_FAILED:
251- err_dia = AptErrorDialog(trans.error, self.window_main)
252- err_dia.run()
253- err_dia.hide()
254- sys.exit(0)
255+ error_string = get_error_string_from_enum(trans.error.code)
256+ error_desc = get_error_description_from_enum(trans.error.code)
257 elif status == EXIT_SUCCESS and close_on_done:
258 sys.exit(0)
259 # tell unity to hide the progress again
260 self.unity.set_progress(-1)
261- self.emit("action-done", action, True, status == EXIT_SUCCESS)
262+ self.emit("action-done", action, True, status == EXIT_SUCCESS,
263+ error_string, error_desc)
264
265 if __name__ == "__main__":
266 b = InstallBackendAptdaemon(None)
267
268=== modified file 'UpdateManager/backend/InstallBackendSynaptic.py'
269--- UpdateManager/backend/InstallBackendSynaptic.py 2012-06-28 19:29:15 +0000
270+++ UpdateManager/backend/InstallBackendSynaptic.py 2012-12-13 20:47:25 +0000
271@@ -42,7 +42,8 @@
272 action, tempf = data
273 if tempf:
274 tempf.close()
275- self.emit("action-done", action, True, os.WEXITSTATUS(condition) == 0)
276+ self.emit("action-done", action, True, os.WEXITSTATUS(condition) == 0,
277+ None, None)
278
279 def update(self):
280 opt = ["--update-at-startup"]
281
282=== modified file 'UpdateManager/backend/__init__.py'
283--- UpdateManager/backend/__init__.py 2012-06-28 00:10:23 +0000
284+++ UpdateManager/backend/__init__.py 2012-12-13 20:47:25 +0000
285@@ -18,7 +18,9 @@
286 None,
287 (GObject.TYPE_INT, # action id
288 GObject.TYPE_BOOLEAN, # authorized
289- GObject.TYPE_BOOLEAN) # success
290+ GObject.TYPE_BOOLEAN, # success
291+ GObject.TYPE_STRING, # error string
292+ GObject.TYPE_STRING) # error desc
293 ),
294 }
295
296
297=== modified file 'tests/test_stop_update.py'
298--- tests/test_stop_update.py 2012-08-24 23:04:09 +0000
299+++ tests/test_stop_update.py 2012-12-13 20:47:25 +0000
300@@ -5,6 +5,7 @@
301 import sys
302 import unittest
303 from mock import patch
304+from gettext import gettext as _
305
306 from UpdateManager.UpdateManager import UpdateManager
307 from UpdateManager.UpdatesAvailable import UpdatesAvailable
308@@ -24,23 +25,25 @@
309 self.manager.datadir = os.path.join(CURDIR, '..', 'data')
310
311 def test_stop_no_updates(self):
312- # install_count, need_reboot, cancelled_update
313- p = UpdateManager._make_available_pane(self.manager, 0, False, True)
314+ p = UpdateManager._make_available_pane(self.manager, 0,
315+ cancelled_update=True)
316 self.assertIsInstance(p, Dialogs.StoppedUpdatesDialog)
317
318 def test_no_stop_no_updates(self):
319- # install_count, need_reboot, cancelled_update
320- p = UpdateManager._make_available_pane(self.manager, 0, False, False)
321+ p = UpdateManager._make_available_pane(self.manager, 0,
322+ cancelled_update=False)
323 self.assertNotIsInstance(p, Dialogs.StoppedUpdatesDialog)
324
325 def test_stop_updates(self):
326- # install_count, need_reboot, cancelled_update
327- p = UpdateManager._make_available_pane(self.manager, 1, False, True)
328+ p = UpdateManager._make_available_pane(self.manager, 1,
329+ cancelled_update=True)
330 self.assertIsInstance(p, UpdatesAvailable)
331- self.assertIsNotNone(p.custom_header)
332+ self.assertEqual(p.custom_header,
333+ _("You stopped the check for updates."))
334
335 def test_no_stop_updates(self):
336- p = UpdateManager._make_available_pane(self.manager, 1, False, False)
337+ p = UpdateManager._make_available_pane(self.manager, 1,
338+ cancelled_update=False)
339 self.assertIsInstance(p, UpdatesAvailable)
340 self.assertIsNone(p.custom_header)
341
342
343=== added file 'tests/test_update_error.py'
344--- tests/test_update_error.py 1970-01-01 00:00:00 +0000
345+++ tests/test_update_error.py 2012-12-13 20:47:25 +0000
346@@ -0,0 +1,71 @@
347+#!/usr/bin/python3
348+# -*- Mode: Python; indent-tabs-mode: nil; tab-width: 4; coding: utf-8 -*-
349+
350+import logging
351+import mock
352+import sys
353+import unittest
354+from gettext import gettext as _
355+from mock import patch
356+
357+from UpdateManager.Dialogs import NoUpdatesDialog
358+from UpdateManager.UpdateManager import UpdateManager
359+from UpdateManager.UpdateProgress import UpdateProgress
360+from UpdateManager.UpdatesAvailable import UpdatesAvailable
361+
362+import os
363+CURDIR = os.path.dirname(os.path.abspath(__file__))
364+
365+
366+class TestUpdateManagerError(unittest.TestCase):
367+
368+ def setUp(self):
369+ patcher = patch('UpdateManager.UpdateManager.UpdateManager')
370+ self.addCleanup(patcher.stop)
371+ self.manager = patcher.start()
372+ self.manager._check_meta_release.return_value = False
373+ self.manager.datadir = os.path.join(CURDIR, '..', 'data')
374+
375+ def test_error_no_updates(self):
376+ p = UpdateManager._make_available_pane(self.manager, 0,
377+ error_occurred=True)
378+ self.assertIsInstance(p, NoUpdatesDialog)
379+ self.assertEqual(p.label_header.get_label(),
380+ _("No software updates are available."))
381+
382+ def test_error_with_updates(self):
383+ p = UpdateManager._make_available_pane(self.manager, 1,
384+ error_occurred=True)
385+ self.assertIsInstance(p, UpdatesAvailable)
386+ self.assertEqual(p.custom_desc,
387+ _("Some software couldn’t be checked for updates."))
388+
389+
390+class TestBackendError(unittest.TestCase):
391+
392+ def setUp(self):
393+ os.environ['UPDATE_MANAGER_FORCE_BACKEND_APTDAEMON'] = '1'
394+
395+ def clear_environ():
396+ del os.environ['UPDATE_MANAGER_FORCE_BACKEND_APTDAEMON']
397+
398+ self.addCleanup(clear_environ)
399+
400+ @patch('UpdateManager.backend.InstallBackendAptdaemon.'
401+ 'InstallBackendAptdaemon.update')
402+ def test_backend_error(self, update):
403+ main = mock.MagicMock()
404+ main.datadir = os.path.join(CURDIR, '..', 'data')
405+
406+ progress = UpdateProgress(main)
407+ update.side_effect = lambda: progress.install_backend.emit(
408+ "action-done", progress.install_backend.UPDATE, True, False,
409+ "string", "desc")
410+
411+ progress.main()
412+ main.start_error.assert_called_once_with(True, "string", "desc")
413+
414+if __name__ == '__main__':
415+ if len(sys.argv) > 1 and sys.argv[1] == "-v":
416+ logging.basicConfig(level=logging.DEBUG)
417+ unittest.main()

Subscribers

People subscribed via source and target branches

to status/vote changes: