Merge lp:~barry/ubuntu-system-image/lp1277589-udm into lp:~registry/ubuntu-system-image/client
- lp1277589-udm
- Merge into client
Proposed by
Barry Warsaw
Status: | Merged | ||||
---|---|---|---|---|---|
Merged at revision: | 234 | ||||
Proposed branch: | lp:~barry/ubuntu-system-image/lp1277589-udm | ||||
Merge into: | lp:~registry/ubuntu-system-image/client | ||||
Diff against target: |
540 lines (+203/-16) 11 files modified
NEWS.rst (+3/-0) systemimage/api.py (+4/-0) systemimage/dbus.py (+35/-3) systemimage/download.py (+35/-1) systemimage/service.py (+2/-1) systemimage/testing/controller.py (+6/-2) systemimage/testing/helpers.py (+8/-5) systemimage/testing/nose.py (+10/-1) systemimage/tests/data/config_03.ini (+1/-1) systemimage/tests/data/index_24.json (+36/-0) systemimage/tests/test_dbus.py (+63/-2) |
||||
To merge this branch: | bzr merge lp:~barry/ubuntu-system-image/lp1277589-udm | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Registry Administrators | Pending | ||
Review via email: mp+207026@code.launchpad.net |
Commit message
Description of the change
More fixes for concurrency protection.
To post a comment you must log in.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'NEWS.rst' |
2 | --- NEWS.rst 2014-02-14 22:47:33 +0000 |
3 | +++ NEWS.rst 2014-02-18 20:26:47 +0000 |
4 | @@ -17,6 +17,9 @@ |
5 | * Return the empty string from `ApplyUpdate()` D-Bus method. This restores |
6 | the original API (patch merged from Ubuntu package, given by Didier |
7 | Roche). (LP: #1260768) |
8 | + * Request ubuntu-download-manager to download all files to temporary |
9 | + destinations, then atomically rename them into place. This avoids |
10 | + clobbering by multiple processes and mimic changes coming in u-d-m. |
11 | |
12 | 2.0.5 (2014-01-30) |
13 | ================== |
14 | |
15 | === modified file 'systemimage/api.py' |
16 | --- systemimage/api.py 2014-02-13 18:16:17 +0000 |
17 | +++ systemimage/api.py 2014-02-18 20:26:47 +0000 |
18 | @@ -73,6 +73,10 @@ |
19 | self._update = None |
20 | self._callback = callback |
21 | |
22 | + def __repr__(self): |
23 | + return '<Mediator at 0x{:x} | State at 0x{:x}>'.format( |
24 | + id(self), id(self._state)) |
25 | + |
26 | def cancel(self): |
27 | self._state.downloader.cancel() |
28 | |
29 | |
30 | === modified file 'systemimage/dbus.py' |
31 | --- systemimage/dbus.py 2014-02-14 22:47:33 +0000 |
32 | +++ systemimage/dbus.py 2014-02-18 20:26:47 +0000 |
33 | @@ -23,6 +23,7 @@ |
34 | |
35 | import os |
36 | import sys |
37 | +import logging |
38 | import traceback |
39 | |
40 | from dbus.service import Object, method, signal |
41 | @@ -35,6 +36,7 @@ |
42 | |
43 | |
44 | EMPTYSTRING = '' |
45 | +log = logging.getLogger('systemimage') |
46 | |
47 | |
48 | class Loop: |
49 | @@ -69,6 +71,7 @@ |
50 | super().__init__(bus, object_path) |
51 | self._loop = loop |
52 | self._api = Mediator(self._progress_callback) |
53 | + log.info('Mediator created {}', self._api) |
54 | self._checking = Lock() |
55 | self._update = None |
56 | self._downloading = False |
57 | @@ -79,17 +82,22 @@ |
58 | |
59 | def _check_for_update(self): |
60 | # Asynchronous method call. |
61 | + log.info('Checking for update') |
62 | self._update = self._api.check_for_update() |
63 | # Do we have an update and can we auto-download it? |
64 | downloading = False |
65 | if self._update.is_available: |
66 | settings = Settings() |
67 | auto = settings.get('auto_download') |
68 | + log.info('Update available; auto-download: {}', auto) |
69 | if auto in ('1', '2'): |
70 | # XXX When we have access to the download service, we can |
71 | # check if we're on the wifi (auto == '1'). |
72 | - GLib.timeout_add(50, self._download) |
73 | + GLib.timeout_add(50, self._download, self._checking.release) |
74 | downloading = True |
75 | + else: |
76 | + log.info('release checking lock from _check_for_update()') |
77 | + self._checking.release() |
78 | self.UpdateAvailableStatus( |
79 | self._update.is_available, |
80 | downloading, |
81 | @@ -100,7 +108,6 @@ |
82 | # array of dictionaries data type. LP: #1215586 |
83 | #self._update.descriptions, |
84 | "") |
85 | - self._checking.release() |
86 | # Stop GLib from calling this method again. |
87 | return False |
88 | |
89 | @@ -122,12 +129,16 @@ |
90 | """ |
91 | self._loop.keepalive() |
92 | # Check-and-acquire the lock. |
93 | + log.info('test and acquire checking lock') |
94 | if not self._checking.acquire(blocking=False): |
95 | # Check is already in progress, so there's nothing more to do. |
96 | + log.info('checking lock not acquired') |
97 | return |
98 | + log.info('checking lock acquired') |
99 | # We've now acquired the lock. Reset any failure or in-progress |
100 | # state. Get a new mediator to reset any of its state. |
101 | self._api = Mediator(self._progress_callback) |
102 | + log.info('Mediator recreated {}', self._api) |
103 | self._failure_count = 0 |
104 | self._last_error = '' |
105 | # Arrange for the actual check to happen in a little while, so that |
106 | @@ -142,21 +153,25 @@ |
107 | eta = 0 |
108 | self.UpdateProgress(percentage, eta) |
109 | |
110 | - def _download(self): |
111 | + def _download(self, release_checking=None): |
112 | if self._downloading and self._paused: |
113 | self._api.resume() |
114 | self._paused = False |
115 | + log.info('Download previously paused') |
116 | return |
117 | if (self._downloading # Already in progress. |
118 | or self._update is None # Not yet checked. |
119 | or not self._update.is_available # No update available. |
120 | ): |
121 | + log.info('Download already in progress or not available') |
122 | return |
123 | if self._failure_count > 0: |
124 | self._failure_count += 1 |
125 | self.UpdateFailed(self._failure_count, self._last_error) |
126 | + log.info('Update failure count: {}', self._failure_count) |
127 | return |
128 | self._downloading = True |
129 | + log.info('Update is downloading') |
130 | try: |
131 | # Always start by sending a UpdateProgress(0, 0). This is |
132 | # enough to get the u/i's attention. |
133 | @@ -168,13 +183,20 @@ |
134 | # value, but not the traceback. |
135 | self._last_error = EMPTYSTRING.join( |
136 | traceback.format_exception_only(*sys.exc_info()[:2])) |
137 | + log.info('Update failed: {}', self._last_error) |
138 | self.UpdateFailed(self._failure_count, self._last_error) |
139 | else: |
140 | + log.info('Update downloaded') |
141 | self.UpdateDownloaded() |
142 | self._failure_count = 0 |
143 | self._last_error = '' |
144 | self._rebootable = True |
145 | self._downloading = False |
146 | + log.info('release checking lock from _download()') |
147 | + if release_checking is not None: |
148 | + # We were auto-downloading, so we now have to release the checking |
149 | + # lock. If we were manually downloading, there would be no lock. |
150 | + release_checking() |
151 | # Stop GLib from calling this method again. |
152 | return False |
153 | |
154 | @@ -297,31 +319,40 @@ |
155 | #descriptions, |
156 | error_reason): |
157 | """Signal sent in response to a CheckForUpdate().""" |
158 | + log.info('EMIT UpdateAvailableStatus({}, {}, {}, {}, {}, {})', |
159 | + is_available, downloading, available_version, update_size, |
160 | + last_update_date, repr(error_reason)) |
161 | self._loop.keepalive() |
162 | |
163 | @signal('com.canonical.SystemImage', signature='id') |
164 | def UpdateProgress(self, percentage, eta): |
165 | """Download progress.""" |
166 | + log.info('EMIT UpdateProgress({}, {})', percentage, eta) |
167 | self._loop.keepalive() |
168 | |
169 | @signal('com.canonical.SystemImage') |
170 | def UpdateDownloaded(self): |
171 | """The update has been successfully downloaded.""" |
172 | + log.info('EMIT UpdateDownloaded()') |
173 | self._loop.keepalive() |
174 | |
175 | @signal('com.canonical.SystemImage', signature='is') |
176 | def UpdateFailed(self, consecutive_failure_count, last_reason): |
177 | """The update failed for some reason.""" |
178 | + log.info('EMIT UpdateFailed({}, {})', |
179 | + consecutive_failure_count, repr(last_reason)) |
180 | self._loop.keepalive() |
181 | |
182 | @signal('com.canonical.SystemImage', signature='i') |
183 | def UpdatePaused(self, percentage): |
184 | """The download got paused.""" |
185 | + log.info('EMIT UpdatePaused({})', percentage) |
186 | self._loop.keepalive() |
187 | |
188 | @signal('com.canonical.SystemImage', signature='ss') |
189 | def SettingChanged(self, key, new_value): |
190 | """A setting value has change.""" |
191 | + log.info('EMIT SettingChanged({}, {})', repr(key), repr(new_value)) |
192 | self._loop.keepalive() |
193 | |
194 | @signal('com.canonical.SystemImage', signature='b') |
195 | @@ -329,3 +360,4 @@ |
196 | """The system is rebooting.""" |
197 | # We don't need to keep the loop alive since we're probably just going |
198 | # to shutdown anyway. |
199 | + log.info('EMIT Rebooting({})', status) |
200 | |
201 | === modified file 'systemimage/download.py' |
202 | --- systemimage/download.py 2014-02-13 18:16:17 +0000 |
203 | +++ systemimage/download.py 2014-02-18 20:26:47 +0000 |
204 | @@ -22,12 +22,16 @@ |
205 | ] |
206 | |
207 | |
208 | +import os |
209 | import dbus |
210 | import logging |
211 | +import tempfile |
212 | |
213 | +from contextlib import ExitStack |
214 | from io import StringIO |
215 | from pprint import pformat |
216 | from systemimage.config import config |
217 | +from systemimage.helpers import safe_remove |
218 | from systemimage.reactor import Reactor |
219 | |
220 | |
221 | @@ -191,8 +195,25 @@ |
222 | for url, dst in downloads: |
223 | print('\t{} -> {}'.format(url, dst), file=fp) |
224 | log.info('{}'.format(fp.getvalue())) |
225 | + # As a workaround for LP: #1277589, ask u-d-m to download the files to |
226 | + # .tmp files, and if they succeed, then atomically move them into |
227 | + # their real location. |
228 | + renames = [] |
229 | + requests = [] |
230 | + for url, dst in downloads: |
231 | + head, tail = os.path.split(dst) |
232 | + fd, path = tempfile.mkstemp(suffix='.tmp', prefix='', dir=head) |
233 | + os.close(fd) |
234 | + renames.append((path, dst)) |
235 | + requests.append((url, path, '')) |
236 | + # mkstemp() creates the file system path, but if the files exist when |
237 | + # the group download is requested, ubuntu-download-manager will |
238 | + # complain and return an error. So, delete all temporary files now so |
239 | + # udm has a clear path to download to. |
240 | + for path, dst in renames: |
241 | + os.remove(path) |
242 | object_path = iface.createDownloadGroup( |
243 | - [(url, dst, '') for url, dst in downloads], |
244 | + requests, # The temporary requests. |
245 | '', # No hashes yet. |
246 | False, # Don't allow GSM yet. |
247 | # https://bugs.freedesktop.org/show_bug.cgi?id=55594 |
248 | @@ -220,6 +241,19 @@ |
249 | raise Canceled |
250 | if reactor.timed_out: |
251 | raise TimeoutError |
252 | + # Now that everything succeeded, rename the temporary files. Just to |
253 | + # be extra cautious, set up a context manager to safely remove all |
254 | + # temporary files in case of an error. If there are no errors, then |
255 | + # there will be nothing to remove. |
256 | + with ExitStack() as resources: |
257 | + for tmp, dst in renames: |
258 | + resources.callback(safe_remove, tmp) |
259 | + for tmp, dst in renames: |
260 | + os.rename(tmp, dst) |
261 | + # We only get here if all the renames succeeded, so there will be |
262 | + # no temporary files to remove, so we can throw away the new |
263 | + # ExitStack, which holds all the removals. |
264 | + resources.pop_all() |
265 | |
266 | def cancel(self): |
267 | """Cancel any current downloads.""" |
268 | |
269 | === modified file 'systemimage/service.py' |
270 | --- systemimage/service.py 2014-02-14 20:10:42 +0000 |
271 | +++ systemimage/service.py 2014-02-18 20:26:47 +0000 |
272 | @@ -30,7 +30,7 @@ |
273 | from dbus.mainloop.glib import DBusGMainLoop |
274 | from pkg_resources import resource_string as resource_bytes |
275 | from systemimage.config import config |
276 | -from systemimage.dbus import Loop, Service |
277 | +from systemimage.dbus import Loop |
278 | from systemimage.helpers import makedirs |
279 | from systemimage.logging import initialize |
280 | from systemimage.main import DEFAULT_CONFIG_FILE |
281 | @@ -114,6 +114,7 @@ |
282 | config.dbus_service = get_service( |
283 | testing_mode, system_bus, '/Service', loop) |
284 | else: |
285 | + from systemimage.dbus import Service |
286 | config.dbus_service = Service(system_bus, '/Service', loop) |
287 | try: |
288 | loop.run() |
289 | |
290 | === modified file 'systemimage/testing/controller.py' |
291 | --- systemimage/testing/controller.py 2014-02-13 18:16:17 +0000 |
292 | +++ systemimage/testing/controller.py 2014-02-18 20:26:47 +0000 |
293 | @@ -125,7 +125,7 @@ |
294 | class Controller: |
295 | """Start and stop D-Bus service under test.""" |
296 | |
297 | - def __init__(self): |
298 | + def __init__(self, logfile=None): |
299 | # Non-public. |
300 | self._stack = ExitStack() |
301 | self._stoppers = [] |
302 | @@ -148,11 +148,15 @@ |
303 | # We need a client.ini file for the subprocess. |
304 | ini_tmpdir = self._stack.enter_context(temporary_directory()) |
305 | ini_vardir = self._stack.enter_context(temporary_directory()) |
306 | + ini_logfile = (os.path.join(ini_tmpdir, 'client.log') |
307 | + if logfile is None |
308 | + else logfile) |
309 | self.ini_path = os.path.join(self.tmpdir, 'client.ini') |
310 | template = resource_bytes( |
311 | 'systemimage.tests.data', 'config_03.ini').decode('utf-8') |
312 | with open(self.ini_path, 'w', encoding='utf-8') as fp: |
313 | - print(template.format(tmpdir=ini_tmpdir, vardir=ini_vardir), |
314 | + print(template.format(tmpdir=ini_tmpdir, vardir=ini_vardir, |
315 | + logfile=ini_logfile), |
316 | file=fp) |
317 | |
318 | def _configure_services(self): |
319 | |
320 | === modified file 'systemimage/testing/helpers.py' |
321 | --- systemimage/testing/helpers.py 2014-02-13 21:44:16 +0000 |
322 | +++ systemimage/testing/helpers.py 2014-02-18 20:26:47 +0000 |
323 | @@ -347,7 +347,7 @@ |
324 | setup_keyring_txz(keyring + '.gpg', signing_kr, json_data, dst) |
325 | |
326 | |
327 | -def setup_index(index, todir, keyring): |
328 | +def setup_index(index, todir, keyring, write_callback=None): |
329 | for image in get_index(index).images: |
330 | for filerec in image.files: |
331 | path = (filerec.path[1:] |
332 | @@ -355,10 +355,13 @@ |
333 | else filerec.path) |
334 | dst = os.path.join(todir, path) |
335 | makedirs(os.path.dirname(dst)) |
336 | - contents = EMPTYSTRING.join( |
337 | - os.path.splitext(filerec.path)[0].split('/')) |
338 | - with open(dst, 'w', encoding='utf-8') as fp: |
339 | - fp.write(contents) |
340 | + if write_callback is None: |
341 | + contents = EMPTYSTRING.join( |
342 | + os.path.splitext(filerec.path)[0].split('/')) |
343 | + with open(dst, 'w', encoding='utf-8') as fp: |
344 | + fp.write(contents) |
345 | + else: |
346 | + write_callback(dst) |
347 | # Sign with the specified signing key. |
348 | sign(dst, keyring) |
349 | |
350 | |
351 | === modified file 'systemimage/testing/nose.py' |
352 | --- systemimage/testing/nose.py 2014-02-14 20:12:08 +0000 |
353 | +++ systemimage/testing/nose.py 2014-02-18 20:26:47 +0000 |
354 | @@ -75,15 +75,24 @@ |
355 | super().__init__() |
356 | self.patterns = [] |
357 | self.verbosity = 0 |
358 | + self.log_file = None |
359 | self.addArgument(self.patterns, 'P', 'pattern', |
360 | 'Add a test matching pattern') |
361 | def bump(ignore): |
362 | self.verbosity += 1 |
363 | self.addFlag(bump, 'V', 'Verbosity', |
364 | 'Increase system-image verbosity') |
365 | + def set_log_file(path): |
366 | + self.log_file = path[0] |
367 | + self.addOption(set_log_file, 'L', 'logfile', |
368 | + 'Set the log file for the test run', |
369 | + nargs=1) |
370 | |
371 | @configuration |
372 | def startTestRun(self, event): |
373 | + from systemimage.config import config |
374 | + if self.log_file is not None: |
375 | + config.system.logfile = self.log_file |
376 | DBusGMainLoop(set_as_default=True) |
377 | initialize(verbosity=self.verbosity) |
378 | # We need to set up the dbus service controller, since all the tests |
379 | @@ -92,7 +101,7 @@ |
380 | # individual services, and we can write new dbus configuration files |
381 | # and HUP the dbus-launch to re-read them, but we cannot change bus |
382 | # addresses after the initial one is set. |
383 | - SystemImagePlugin.controller = Controller() |
384 | + SystemImagePlugin.controller = Controller(self.log_file) |
385 | SystemImagePlugin.controller.start() |
386 | atexit.register(SystemImagePlugin.controller.stop) |
387 | |
388 | |
389 | === modified file 'systemimage/tests/data/config_03.ini' |
390 | --- systemimage/tests/data/config_03.ini 2013-11-12 19:57:39 +0000 |
391 | +++ systemimage/tests/data/config_03.ini 2014-02-18 20:26:47 +0000 |
392 | @@ -14,7 +14,7 @@ |
393 | timeout: 1s |
394 | build_file: {tmpdir}/ubuntu-build |
395 | tempdir: {tmpdir}/tmp |
396 | -logfile: {tmpdir}/client.log |
397 | +logfile: {logfile} |
398 | loglevel: info |
399 | settings_db: {vardir}/settings.db |
400 | |
401 | |
402 | === added file 'systemimage/tests/data/index_24.json' |
403 | --- systemimage/tests/data/index_24.json 1970-01-01 00:00:00 +0000 |
404 | +++ systemimage/tests/data/index_24.json 2014-02-18 20:26:47 +0000 |
405 | @@ -0,0 +1,36 @@ |
406 | +{ |
407 | + "global": { |
408 | + "generated_at": "Thu Aug 01 08:01:00 UTC 2013" |
409 | + }, |
410 | + "images": [ |
411 | + { |
412 | + "description": "Full", |
413 | + "files": [ |
414 | + { |
415 | + "checksum": "5b05b298e974f3b9e40f0a1a8188f50984a4f18fb329e050324296632d3d9dfc", |
416 | + "order": 3, |
417 | + "path": "/3/4/5.txt", |
418 | + "signature": "/3/4/5.txt.asc", |
419 | + "size": 104857600 |
420 | + }, |
421 | + { |
422 | + "checksum": "5b05b298e974f3b9e40f0a1a8188f50984a4f18fb329e050324296632d3d9dfc", |
423 | + "order": 1, |
424 | + "path": "/4/5/6.txt", |
425 | + "signature": "/4/5/6.txt.asc", |
426 | + "size": 104857600 |
427 | + }, |
428 | + { |
429 | + "checksum": "5b05b298e974f3b9e40f0a1a8188f50984a4f18fb329e050324296632d3d9dfc", |
430 | + "order": 2, |
431 | + "path": "/5/6/7.txt", |
432 | + "signature": "/5/6/7.txt.asc", |
433 | + "size": 104857600 |
434 | + } |
435 | + ], |
436 | + "type": "full", |
437 | + "version": 1600, |
438 | + "bootme": true |
439 | + } |
440 | + ] |
441 | +} |
442 | |
443 | === modified file 'systemimage/tests/test_dbus.py' |
444 | --- systemimage/tests/test_dbus.py 2014-02-14 17:10:38 +0000 |
445 | +++ systemimage/tests/test_dbus.py 2014-02-18 20:26:47 +0000 |
446 | @@ -23,6 +23,7 @@ |
447 | 'TestDBusGetSet', |
448 | 'TestDBusInfo', |
449 | 'TestDBusInfoNoDetails', |
450 | + 'TestDBusLP1277589', |
451 | 'TestDBusMockFailApply', |
452 | 'TestDBusMockFailPause', |
453 | 'TestDBusMockFailResume', |
454 | @@ -122,6 +123,23 @@ |
455 | self.quit() |
456 | |
457 | |
458 | +class DoubleCheckingReactor(Reactor): |
459 | + def __init__(self, iface): |
460 | + super().__init__(dbus.SystemBus()) |
461 | + self.iface = iface |
462 | + self.uas_signals = [] |
463 | + self.react_to('UpdateAvailableStatus') |
464 | + self.react_to('UpdateDownloaded') |
465 | + self.schedule(self.iface.CheckForUpdate) |
466 | + |
467 | + def _do_UpdateAvailableStatus(self, signal, path, *args, **kws): |
468 | + self.uas_signals.append(args) |
469 | + self.schedule(self.iface.CheckForUpdate) |
470 | + |
471 | + def _do_UpdateDownloaded(self, *args, **kws): |
472 | + self.quit() |
473 | + |
474 | + |
475 | class _TestBase(unittest.TestCase): |
476 | """Base class for all DBus testing.""" |
477 | |
478 | @@ -262,13 +280,14 @@ |
479 | safe_remove(self.reboot_log) |
480 | super().tearDown() |
481 | |
482 | - def _prepare_index(self, index_file): |
483 | + def _prepare_index(self, index_file, write_callback=None): |
484 | serverdir = SystemImagePlugin.controller.serverdir |
485 | index_path = os.path.join(serverdir, 'stable', 'nexus7', 'index.json') |
486 | head, tail = os.path.split(index_path) |
487 | copy(index_file, head, tail) |
488 | sign(index_path, 'device-signing.gpg') |
489 | - setup_index(index_file, serverdir, 'device-signing.gpg') |
490 | + setup_index(index_file, serverdir, 'device-signing.gpg', |
491 | + write_callback) |
492 | |
493 | def _touch_build(self, version): |
494 | # Unlike the touch_build() helper, this one uses our own config object |
495 | @@ -1542,3 +1561,45 @@ |
496 | update 5.txt 5.txt.asc |
497 | unmount system |
498 | """) |
499 | + |
500 | + |
501 | +class TestDBusLP1277589(_LiveTesting): |
502 | + def test_multiple_check_for_updates(self): |
503 | + # Log analysis of LP: #1277589 appears to show the following scenario, |
504 | + # reproduced in this test case: |
505 | + # |
506 | + # * Automatic updates are enabled. |
507 | + # * No image signing or image master keys are present. |
508 | + # * A full update is checked. |
509 | + # - A new image master key and image signing key is downloaded. |
510 | + # - Update is available |
511 | + # |
512 | + # Start by creating some big files which will take a while to |
513 | + # download. |
514 | + def write_callback(dst): |
515 | + # Write a 100 MiB sized file. |
516 | + with open(dst, 'wb') as fp: |
517 | + for i in range(25600): |
518 | + fp.write(b'x' * 4096) |
519 | + self._prepare_index('index_24.json', write_callback) |
520 | + # Create a reactor that will exit when the UpdateDownloaded signal is |
521 | + # received. We're going to issue a CheckForUpdate with automatic |
522 | + # updates enabled. As soon as we receive the UpdateAvailableStatus |
523 | + # signal, we'll immediately issue *another* CheckForUpdate, which |
524 | + # should run while the auto-download is working. |
525 | + # |
526 | + # At the end, we should not get another UpdateAvailableStatus signal, |
527 | + # but we should get the UpdateDownloaded signal. |
528 | + reactor = DoubleCheckingReactor(self.iface) |
529 | + reactor.run() |
530 | + self.assertEqual(len(reactor.uas_signals), 1) |
531 | + (is_available, downloading, available_version, update_size, |
532 | + last_update_date, |
533 | + #descriptions, |
534 | + error_reason) = reactor.uas_signals[0] |
535 | + self.assertTrue(is_available) |
536 | + self.assertTrue(downloading) |
537 | + self.assertEqual(available_version, '1600') |
538 | + self.assertEqual(update_size, 314572800) |
539 | + self.assertEqual(last_update_date, 'Unknown') |
540 | + self.assertEqual(error_reason, '') |