Merge lp:~barry/ubuntu-system-image/citrain-24.0u1 into lp:~ubuntu-managed-branches/ubuntu-system-image/system-image

Proposed by Barry Warsaw on 2014-09-17
Status: Merged
Approved by: Barry Warsaw on 2014-09-19
Approved revision: 239
Merged at revision: 237
Proposed branch: lp:~barry/ubuntu-system-image/citrain-24.0u1
Merge into: lp:~ubuntu-managed-branches/ubuntu-system-image/system-image
Diff against target: 2708 lines (+1013/-217)
53 files modified
NEWS.rst (+18/-0)
PKG-INFO (+1/-1)
cli-manpage.rst (+2/-9)
coverage.ini (+15/-0)
debian/changelog (+27/-0)
debian/control (+0/-1)
debian/rules (+4/-15)
debian/system-image-common.postinst (+2/-0)
debian/system-image-common.postrm (+2/-0)
ini-manpage.rst (+21/-6)
setup.cfg (+2/-2)
system_image.egg-info/PKG-INFO (+1/-1)
system_image.egg-info/SOURCES.txt (+6/-0)
systemimage/api.py (+1/-1)
systemimage/bag.py (+1/-1)
systemimage/bindings.py (+2/-2)
systemimage/candidates.py (+1/-1)
systemimage/channel.py (+1/-1)
systemimage/config.py (+17/-4)
systemimage/dbus.py (+77/-15)
systemimage/device.py (+1/-1)
systemimage/download.py (+6/-5)
systemimage/gpg.py (+2/-2)
systemimage/helpers.py (+25/-42)
systemimage/image.py (+1/-1)
systemimage/logging.py (+36/-34)
systemimage/main.py (+15/-9)
systemimage/reactor.py (+1/-1)
systemimage/reboot.py (+1/-1)
systemimage/scores.py (+1/-1)
systemimage/service.py (+9/-7)
systemimage/settings.py (+1/-1)
systemimage/state.py (+6/-0)
systemimage/testing/controller.py (+8/-4)
systemimage/testing/dbus.py (+71/-1)
systemimage/testing/helpers.py (+2/-1)
systemimage/testing/nose.py (+11/-3)
systemimage/testing/service.py (+50/-0)
systemimage/tests/data/channel_06.ini (+8/-0)
systemimage/tests/data/channel_07.ini (+8/-0)
systemimage/tests/data/config_03.ini (+1/-1)
systemimage/tests/data/config_09.ini (+27/-0)
systemimage/tests/data/config_10.ini (+35/-0)
systemimage/tests/test_api.py (+12/-0)
systemimage/tests/test_config.py (+63/-2)
systemimage/tests/test_dbus.py (+161/-10)
systemimage/tests/test_download.py (+45/-18)
systemimage/tests/test_helpers.py (+72/-4)
systemimage/tests/test_main.py (+32/-2)
systemimage/tests/test_state.py (+82/-2)
systemimage/version.txt (+1/-1)
tox.ini (+16/-2)
unittest.cfg (+2/-1)
To merge this branch: bzr merge lp:~barry/ubuntu-system-image/citrain-24.0u1
Reviewer Review Type Date Requested Status
Ubuntu CI managed package branches 2014-09-17 Pending
Review via email: mp+234983@code.launchpad.net

Description of the change

system-image (2.4-0ubuntu1) UNRELEASED; urgency=medium

  * New upstream release.
    - LP: #1353178 - The channel.ini file can override the device name by
      setting `[service]device`.
    - LP: #1324241 - Add optional instrumentation to collect code coverage
      data during test suite run via tox.
    - LP: #1279970 - When an exception occurs in a `system-image-dbus`
      D-Bus method, signal, or callback, this exception is logged in the
      standard log file, and the process exits. Also, `[system]loglevel`
      can now take an optional ":level" prefix which can be used to set
      the log level for the D-Bus API methods. By default, they log at
      `ERROR` level, but can be set lower for debugging purposes.
    - LP: #1365646 - Don't crash when releasing an unacquired checking lock.
    - LP: #1365761 - When checking files for `last_update_date()` ignore
      PermissionErrors and just keep checking the fall backs.
    - LP: #1369714 - `system-image-cli --dbus` has been deprecated and
      will be removed in the future.
  * d/control: Remove tox as a build dependency to avoid having to MIR tox,
    virtualenv, and pip.
  * d/rules:
    - Call nose2 explicitly to avoid use of tox.
    - Remove unnecessary override_dh_auto_clean rule.
  * d/system-image-common.post{inst,rm}: `set -e` to make lintian happy.

 -- Barry Warsaw <email address hidden> Tue, 09 Sep 2014 11:05:09 -0400

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-07-31 23:20:10 +0000
3+++ NEWS.rst 2014-09-17 13:42:00 +0000
4@@ -2,6 +2,24 @@
5 NEWS for system-image updater
6 =============================
7
8+2.4 (2014-09-16)
9+================
10+ * The channel.ini file can override the device name by setting
11+ ``[service]device``. (LP: #1353178)
12+ * Add optional instrumentation to collect code coverage data during test
13+ suite run via tox. (LP: #1324241)
14+ * When an exception occurs in a `system-image-dbus` D-Bus method, signal, or
15+ callback, this exception is logged in the standard log file, and the
16+ process exits. Also, `[system]loglevel` can now take an optional ":level"
17+ prefix which can be used to set the log level for the D-Bus API methods.
18+ By default, they log at `ERROR` level, but can be set lower for debugging
19+ purposes. (LP: #1279970)
20+ * Don't crash when releasing an unacquired checking lock. (LP: #1365646)
21+ * When checking files for `last_update_date()` ignore PermissionErrors and
22+ just keep checking the fall backs. (LP: #1365761)
23+ * `system-image-cli --dbus` has been deprecated and will be removed in the
24+ future. (LP: #1369714)
25+
26 2.3.2 (2014-07-31)
27 ==================
28 * When system-image-{cli,dbus} is run as non-root, use a fallback location
29
30=== modified file 'PKG-INFO'
31--- PKG-INFO 2014-07-31 21:06:59 +0000
32+++ PKG-INFO 2014-09-17 13:42:00 +0000
33@@ -1,6 +1,6 @@
34 Metadata-Version: 1.0
35 Name: system-image
36-Version: 2.3.2
37+Version: 2.4
38 Summary: Ubuntu System Image Based Upgrades
39 Home-page: UNKNOWN
40 Author: Barry Warsaw
41
42=== modified file 'cli-manpage.rst'
43--- cli-manpage.rst 2014-07-23 22:51:19 +0000
44+++ cli-manpage.rst 2014-09-17 13:42:00 +0000
45@@ -7,9 +7,9 @@
46 ------------------------------------------------
47
48 :Author: Barry Warsaw <barry@ubuntu.com>
49-:Date: 2013-10-23
50+:Date: 2014-09-16
51 :Copyright: 2013-2014 Canonical Ltd.
52-:Version: 2.3
53+:Version: 2.4
54 :Manual section: 1
55
56
57@@ -103,13 +103,6 @@
58 Deletes the given key from the settings database. If the key does not
59 exist, this is a no-op. May be given multiple times.
60
61---dbus
62- Run in D-Bus client mode. Normally, ``system-image-cli`` runs directly
63- against the internal API. With this switch, it instead acts as a D-Bus
64- client, performing all operations against the ``system-image-dbus``
65- service. This mode more closely mimics how a user interface would perform
66- updates.
67-
68
69 FILES
70 =====
71
72=== added file 'coverage.ini'
73--- coverage.ini 1970-01-01 00:00:00 +0000
74+++ coverage.ini 2014-09-17 13:42:00 +0000
75@@ -0,0 +1,15 @@
76+[run]
77+branch = true
78+parallel = true
79+omit =
80+ setup*
81+ systemimage/data/*
82+ systemimage/docs/*
83+ systemimage/testing/*
84+ systemimage/tests/*
85+ /usr/lib/*
86+
87+[paths]
88+source =
89+ systemimage
90+ .tox/coverage/lib/python*/site-packages/systemimage
91
92=== modified file 'debian/changelog'
93--- debian/changelog 2014-08-01 18:33:39 +0000
94+++ debian/changelog 2014-09-17 13:42:00 +0000
95@@ -1,3 +1,30 @@
96+system-image (2.4-0ubuntu1) UNRELEASED; urgency=medium
97+
98+ * New upstream release.
99+ - LP: #1353178 - The channel.ini file can override the device name by
100+ setting `[service]device`.
101+ - LP: #1324241 - Add optional instrumentation to collect code coverage
102+ data during test suite run via tox.
103+ - LP: #1279970 - When an exception occurs in a `system-image-dbus`
104+ D-Bus method, signal, or callback, this exception is logged in the
105+ standard log file, and the process exits. Also, `[system]loglevel`
106+ can now take an optional ":level" prefix which can be used to set
107+ the log level for the D-Bus API methods. By default, they log at
108+ `ERROR` level, but can be set lower for debugging purposes.
109+ - LP: #1365646 - Don't crash when releasing an unacquired checking lock.
110+ - LP: #1365761 - When checking files for `last_update_date()` ignore
111+ PermissionErrors and just keep checking the fall backs.
112+ - LP: #1369714 - `system-image-cli --dbus` has been deprecated and
113+ will be removed in the future.
114+ * d/control: Remove tox as a build dependency to avoid having to MIR tox,
115+ virtualenv, and pip.
116+ * d/rules:
117+ - Call nose2 explicitly to avoid use of tox.
118+ - Remove unnecessary override_dh_auto_clean rule.
119+ * d/system-image-common.post{inst,rm}: `set -e` to make lintian happy.
120+
121+ -- Barry Warsaw <barry@ubuntu.com> Tue, 16 Sep 2014 22:58:59 -0400
122+
123 system-image (2.3.2-0ubuntu2) utopic; urgency=medium
124
125 [ Barry Warsaw ]
126
127=== modified file 'debian/control'
128--- debian/control 2014-08-01 15:41:36 +0000
129+++ debian/control 2014-09-17 13:42:00 +0000
130@@ -9,7 +9,6 @@
131 debhelper (>= 8),
132 dh-python,
133 python-docutils,
134- python-tox,
135 python3-all (>= 3.3),
136 python3-dbus,
137 python3-gi,
138
139=== modified file 'debian/rules'
140--- debian/rules 2014-07-18 16:32:44 +0000
141+++ debian/rules 2014-09-17 13:42:00 +0000
142@@ -8,18 +8,12 @@
143 %:
144 dh $@ --with python3 --buildsystem=pybuild
145
146-ifeq (,$(filter nocheck,$(DEB_BUILD_OPTIONS)))
147-test-python%:
148- unset http_proxy; unset https_proxy; export HOME=/tmp; \
149+override_dh_auto_test:
150+ unset http_proxy; unset https_proxy; \
151 export SYSTEMIMAGE_REACTOR_TIMEOUT=1200; \
152 export SYSTEMIMAGE_DBUS_DAEMON_HUP_SLEEP_SECONDS=2; \
153- nodot=$(shell echo $* | cut --complement -b 2); \
154- tox -e py$${nodot}
155-
156-override_dh_auto_test: $(PYTHON3:%=test-python%)
157-else
158-override_dh_auto_test:
159-endif
160+ PYBUILD_SYSTEM=custom \
161+ PYBUILD_TEST_ARGS="{interpreter} -m nose2 -v" dh_auto_test
162
163 # pybuild can't yet handle Python 3 packages that don't start with "python3-".
164 # See bug #751908 - In the meantime, this override isn't perfect, but it gets
165@@ -57,8 +51,3 @@
166 rst2man dbus-manpage.rst > debian/tmp/system-image-dbus.man
167 rst2man ini-manpage.rst > debian/tmp/client-ini.man
168 dh_installman
169-
170-override_dh_auto_clean:
171- dh_auto_clean
172- rm -rf build
173- rm -rf *.egg-info
174
175=== modified file 'debian/system-image-common.postinst'
176--- debian/system-image-common.postinst 2013-12-13 13:55:51 +0000
177+++ debian/system-image-common.postinst 2014-09-17 13:42:00 +0000
178@@ -1,5 +1,7 @@
179 #!/bin/sh
180
181+set -e
182+
183 # Tweak directory and file permissions.
184
185 case "$1" in
186
187=== modified file 'debian/system-image-common.postrm'
188--- debian/system-image-common.postrm 2013-12-13 13:55:51 +0000
189+++ debian/system-image-common.postrm 2014-09-17 13:42:00 +0000
190@@ -1,5 +1,7 @@
191 #!/bin/sh
192
193+set -e
194+
195 # On purge, remove the log file and directory, as well as the lib directory
196 # and configuration file.
197
198
199=== modified file 'ini-manpage.rst'
200--- ini-manpage.rst 2014-07-23 22:51:19 +0000
201+++ ini-manpage.rst 2014-09-17 13:42:00 +0000
202@@ -8,9 +8,9 @@
203 -----------------------------------------------
204
205 :Author: Barry Warsaw <barry@ubuntu.com>
206-:Date: 2014-07-15
207+:Date: 2014-09-11
208 :Copyright: 2013-2014 Canonical Ltd.
209-:Version: 2.3
210+:Version: 2.4
211 :Manual section: 5
212
213
214@@ -61,6 +61,10 @@
215 channel
216 The upgrade channel.
217
218+device
219+ The device name. If missing or unset (i.e. the empty string), then the
220+ device is calculated using the ``[hooks]device`` callback.
221+
222 build_number
223 The system's current build number.
224
225@@ -91,10 +95,21 @@
226 The file where logging output will be sent.
227
228 loglevel
229- The level at which logging information will be emitted. This is a string
230- corresponding to the following `log levels`_ from least verbose to most
231- verbose: ``DEBUG``, ``INFO``, ``WARNING``, ``ERROR``, ``CRITICAL``. The
232- value of this variable is case insensitive.
233+ The level at which logging information will be emitted. There are two
234+ loggers which both log messages to `logfile`. "systemimage" is the main
235+ logger, but additional logging can go to the "systemimage.dbus" logger.
236+ The latter is used in debugging situations to get more information about
237+ the D-Bus service.
238+
239+ `loglevel` can be a single case-insensitive string corresponding to the
240+ following `log levels`_ from least verbose to most verbose: ``DEBUG``,
241+ ``INFO``, ``WARNING``, ``ERROR``, ``CRITICAL``. In this case, the
242+ "systemimage" logger will be placed at this level, while the
243+ "systemimage.dbus" logger will be placed at the ``ERROR`` level.
244+
245+ `loglevel` can also describe two levels, separated by a colon. In this
246+ case, the main logger is placed at the first level, while the D-Bus logger
247+ is placed at the second level. For example: ``debug:info``.
248
249 timeout
250 The maximum allowed time interval for downloading the individual files.
251
252=== modified file 'setup.cfg'
253--- setup.cfg 2014-07-31 23:20:10 +0000
254+++ setup.cfg 2014-09-17 13:42:00 +0000
255@@ -4,7 +4,7 @@
256 logging-filter = systemimage
257
258 [egg_info]
259+tag_build =
260+tag_date = 0
261 tag_svn_revision = 0
262-tag_date = 0
263-tag_build =
264
265
266=== modified file 'system_image.egg-info/PKG-INFO'
267--- system_image.egg-info/PKG-INFO 2014-07-31 21:06:59 +0000
268+++ system_image.egg-info/PKG-INFO 2014-09-17 13:42:00 +0000
269@@ -1,6 +1,6 @@
270 Metadata-Version: 1.0
271 Name: system-image
272-Version: 2.3.2
273+Version: 2.4
274 Summary: Ubuntu System Image Based Upgrades
275 Home-page: UNKNOWN
276 Author: Barry Warsaw
277
278=== modified file 'system_image.egg-info/SOURCES.txt'
279--- system_image.egg-info/SOURCES.txt 2014-07-16 22:23:06 +0000
280+++ system_image.egg-info/SOURCES.txt 2014-09-17 13:42:00 +0000
281@@ -2,6 +2,7 @@
282 NEWS.rst
283 README.rst
284 cli-manpage.rst
285+coverage.ini
286 dbus-manpage.rst
287 ini-manpage.rst
288 setup.cfg
289@@ -51,6 +52,7 @@
290 systemimage/testing/demo.py
291 systemimage/testing/helpers.py
292 systemimage/testing/nose.py
293+systemimage/testing/service.py
294 systemimage/tests/__init__.py
295 systemimage/tests/test_api.py
296 systemimage/tests/test_bag.py
297@@ -79,6 +81,8 @@
298 systemimage/tests/data/channel_03.ini
299 systemimage/tests/data/channel_04.ini
300 systemimage/tests/data/channel_05.ini
301+systemimage/tests/data/channel_06.ini
302+systemimage/tests/data/channel_07.ini
303 systemimage/tests/data/channels_01.json
304 systemimage/tests/data/channels_02.json
305 systemimage/tests/data/channels_03.json
306@@ -101,6 +105,8 @@
307 systemimage/tests/data/config_06.ini
308 systemimage/tests/data/config_07.ini
309 systemimage/tests/data/config_08.ini
310+systemimage/tests/data/config_09.ini
311+systemimage/tests/data/config_10.ini
312 systemimage/tests/data/dbus-system.conf.in
313 systemimage/tests/data/device-signing.gpg
314 systemimage/tests/data/expired_cert.pem
315
316=== modified file 'systemimage/api.py'
317--- systemimage/api.py 2014-07-23 22:51:19 +0000
318+++ systemimage/api.py 2014-09-17 13:42:00 +0000
319@@ -80,7 +80,7 @@
320 self._update = None
321 self._callback = callback
322
323- def __repr__(self):
324+ def __repr__(self): # pragma: no cover
325 return '<Mediator at 0x{:x} | State at 0x{:x}>'.format(
326 id(self), id(self._state))
327
328
329=== modified file 'systemimage/bag.py'
330--- systemimage/bag.py 2014-07-23 22:51:19 +0000
331+++ systemimage/bag.py 2014-09-17 13:42:00 +0000
332@@ -70,7 +70,7 @@
333 key += '_'
334 return key, value
335
336- def __repr__(self):
337+ def __repr__(self): # pragma: no cover
338 return '<Bag: {}>'.format(COMMASPACE.join(sorted(
339 key for key in self.__dict__ if not key.startswith('_'))))
340
341
342=== modified file 'systemimage/bindings.py'
343--- systemimage/bindings.py 2014-07-23 22:51:19 +0000
344+++ systemimage/bindings.py 2014-09-17 13:42:00 +0000
345@@ -53,7 +53,7 @@
346
347 def _do_UpdateAvailableStatus(self, signal, path, *args):
348 payload = UASRecord(*args)
349- if payload.error_reason != '':
350+ if payload.error_reason != '': # pragma: no cover
351 # Cancel the download, set the failed flag and log the reason.
352 log.error('CheckForUpdate returned an error: {}',
353 payload.error_reason)
354@@ -64,7 +64,7 @@
355 log.info('No update available')
356 self.quit()
357 return
358- if not payload.downloading:
359+ if not payload.downloading: # pragma: no cover
360 # We should be in auto download mode, so why aren't we downloading
361 # the update? Do it manually.
362 log.info('Update available, downloading manually')
363
364=== modified file 'systemimage/candidates.py'
365--- systemimage/candidates.py 2014-02-20 23:03:24 +0000
366+++ systemimage/candidates.py 2014-09-17 13:42:00 +0000
367@@ -75,7 +75,7 @@
368 fulls.add(image)
369 elif image.type == 'delta':
370 deltas.add(image)
371- else:
372+ else: # pragma: no cover
373 # BAW 2013-04-30: log and ignore.
374 raise AssertionError('unknown image type: {}'.format(image.type))
375 # Load up the roots of candidate upgrade paths.
376
377=== modified file 'systemimage/channel.py'
378--- systemimage/channel.py 2014-02-20 23:03:24 +0000
379+++ systemimage/channel.py 2014-09-17 13:42:00 +0000
380@@ -22,7 +22,7 @@
381
382 import json
383
384-from systemimage.helpers import Bag
385+from systemimage.bag import Bag
386
387
388 def _parse_device_mappings(device_mapping):
389
390=== modified file 'systemimage/config.py'
391--- systemimage/config.py 2014-07-23 22:51:19 +0000
392+++ systemimage/config.py 2014-09-17 13:42:00 +0000
393@@ -28,8 +28,9 @@
394 from configparser import ConfigParser
395 from contextlib import ExitStack
396 from pkg_resources import resource_filename
397+from systemimage.bag import Bag
398 from systemimage.helpers import (
399- Bag, as_loglevel, as_object, as_timedelta, makedirs, temporary_directory)
400+ as_loglevel, as_object, as_timedelta, makedirs, temporary_directory)
401
402
403 DISABLED = object()
404@@ -48,6 +49,10 @@
405 return result
406
407
408+def device_converter(value):
409+ return value.strip()
410+
411+
412 class Configuration:
413 def __init__(self, ini_file=None):
414 # Defaults.
415@@ -82,7 +87,9 @@
416 self.config_file = path
417 self.service.update(converters=dict(http_port=port_value_converter,
418 https_port=port_value_converter,
419- build_number=int),
420+ build_number=int,
421+ device=device_converter,
422+ ),
423 **parser['service'])
424 if (self.service.http_port is DISABLED and
425 self.service.https_port is DISABLED):
426@@ -170,9 +177,15 @@
427
428 @property
429 def device(self):
430- # It's safe to cache this.
431 if self._device is None:
432- self._device = self.hooks.device().get_device()
433+ # Start by looking for a [service]device setting. Use this if it
434+ # exists, otherwise fall back to calling the hook.
435+ self._device = getattr(self.service, 'device', None)
436+ # The key could exist in the channel.ini file, but its value could
437+ # be empty. That's semantically equivalent to a missing
438+ # [service]device setting.
439+ if not self._device:
440+ self._device = self.hooks.device().get_device()
441 return self._device
442
443 @device.setter
444
445=== modified file 'systemimage/dbus.py'
446--- systemimage/dbus.py 2014-07-23 22:51:19 +0000
447+++ systemimage/dbus.py 2014-09-17 13:42:00 +0000
448@@ -18,16 +18,17 @@
449 __all__ = [
450 'Loop',
451 'Service',
452+ 'log_and_exit',
453 ]
454
455
456 import os
457 import sys
458 import logging
459-import traceback
460
461 from datetime import datetime
462 from dbus.service import Object, method, signal
463+from functools import wraps
464 from gi.repository import GLib
465 from systemimage.api import Mediator
466 from systemimage.config import config
467@@ -38,6 +39,29 @@
468
469 EMPTYSTRING = ''
470 log = logging.getLogger('systemimage')
471+dbus_log = logging.getLogger('systemimage.dbus')
472+
473+
474+def log_and_exit(function):
475+ """Decorator for D-Bus methods to handle tracebacks.
476+
477+ Put this *above* the @method or @signal decorator. It will cause
478+ the exception to be logged and the D-Bus service will exit.
479+ """
480+ @wraps(function)
481+ def wrapper(*args, **kws):
482+ try:
483+ dbus_log.info('>>> {}', function.__name__)
484+ retval = function(*args, **kws)
485+ dbus_log.info('<<< {}', function.__name__)
486+ return retval
487+ except:
488+ dbus_log.info('!!! {}', function.__name__)
489+ dbus_log.exception('Error in D-Bus method')
490+ self = args[0]
491+ assert isinstance(self, Service), args[0]
492+ sys.exit(1)
493+ return wrapper
494
495
496 class Loop:
497@@ -81,6 +105,7 @@
498 self._failure_count = 0
499 self._last_error = ''
500
501+ @log_and_exit
502 def _check_for_update(self):
503 # Asynchronous method call.
504 log.info('Enter _check_for_update()')
505@@ -108,6 +133,7 @@
506
507 # 2013-07-25 BAW: should we use the rather underdocumented async_callbacks
508 # argument to @method?
509+ @log_and_exit
510 @method('com.canonical.SystemImage')
511 def CheckForUpdate(self):
512 """Find out whether an update is available.
513@@ -150,6 +176,7 @@
514 # this method can return immediately.
515 GLib.timeout_add(50, self._check_for_update)
516
517+ @log_and_exit
518 def _progress_callback(self, received, total):
519 # Plumb the progress through our own D-Bus API. Our API is defined as
520 # signalling a percentage and an eta. We can calculate the percentage
521@@ -158,6 +185,7 @@
522 eta = 0
523 self.UpdateProgress(percentage, eta)
524
525+ @log_and_exit
526 def _download(self):
527 if self._downloading and self._paused:
528 self._api.resume()
529@@ -186,10 +214,14 @@
530 except Exception:
531 log.exception('Download failed')
532 self._failure_count += 1
533- # This will return both the exception name and the exception
534- # value, but not the traceback.
535- self._last_error = EMPTYSTRING.join(
536- traceback.format_exception_only(*sys.exc_info()[:2]))
537+ # Set the last error string to the exception's class name.
538+ exception, value = sys.exc_info()[:2]
539+ # if there's no meaningful value, omit it.
540+ value_str = str(value)
541+ name = exception.__name__
542+ self._last_error = ('{}'.format(name)
543+ if len(value_str) == 0
544+ else '{}: {}'.format(name, value))
545 self.UpdateFailed(self._failure_count, self._last_error)
546 else:
547 log.info('Update downloaded')
548@@ -198,11 +230,25 @@
549 self._last_error = ''
550 self._rebootable = True
551 self._downloading = False
552- log.info('release checking lock from _download()')
553- self._checking.release()
554+ log.info('releasing checking lock from _download()')
555+ try:
556+ self._checking.release()
557+ except RuntimeError:
558+ # 2014-09-11 BAW: We don't own the lock. There are several reasons
559+ # why this can happen including: 1) the client canceled the
560+ # download while it was in progress, and CancelUpdate() already
561+ # released the lock; 2) the client called DownloadUpdate() without
562+ # first calling CheckForUpdate(); 3) the client called DU()
563+ # multiple times in a row but the update was already downloaded and
564+ # all the file signatures have been verified. I can't think of
565+ # reason why we shouldn't just ignore the double release, so
566+ # that's what we do. See LP: #1365646.
567+ pass
568+ log.info('released checking lock from _download()')
569 # Stop GLib from calling this method again.
570 return False
571
572+ @log_and_exit
573 @method('com.canonical.SystemImage')
574 def DownloadUpdate(self):
575 """Download the available update.
576@@ -214,6 +260,7 @@
577 self._loop.keepalive()
578 GLib.timeout_add(50, self._download)
579
580+ @log_and_exit
581 @method('com.canonical.SystemImage', out_signature='s')
582 def PauseDownload(self):
583 """Pause a downloading update."""
584@@ -226,29 +273,30 @@
585 error_message = 'not downloading'
586 return error_message
587
588+ @log_and_exit
589 @method('com.canonical.SystemImage', out_signature='s')
590 def CancelUpdate(self):
591 """Cancel a download."""
592 self._loop.keepalive()
593+ # During the download, this will cause an UpdateFailed signal to be
594+ # issued, as part of the exception handling in _download(). If we're
595+ # not downloading, then no signal need be sent. There's no need to
596+ # send *another* signal when downloading, because we never will be
597+ # downloading by the time we get past this next call.
598 self._api.cancel()
599 # If we're holding the checking lock, release it.
600 try:
601+ log.info('releasing checking lock from CancelUpdate()')
602 self._checking.release()
603- log.info('release checking lock from CancelUpdate()')
604+ log.info('released checking lock from CancelUpdate()')
605 except RuntimeError:
606 # We're not holding the lock.
607 pass
608- # We're now in a failure state until the next CheckForUpdate.
609- self._failure_count += 1
610- self._last_error = 'Canceled'
611- log.info('CancelUpdate() called')
612- # Only send this signal if we were in the middle of downloading.
613- if self._downloading:
614- self.UpdateFailed(self._failure_count, self._last_error)
615 # XXX 2013-08-22: If we can't cancel the current download, return the
616 # reason in this string.
617 return ''
618
619+ @log_and_exit
620 def _apply_update(self):
621 self._loop.keepalive()
622 if not self._rebootable:
623@@ -264,12 +312,14 @@
624 self._rebootable = False
625 self.Rebooting(True)
626
627+ @log_and_exit
628 @method('com.canonical.SystemImage')
629 def ApplyUpdate(self):
630 """Apply the update, rebooting the device."""
631 GLib.timeout_add(50, self._apply_update)
632 return ''
633
634+ @log_and_exit
635 @method('com.canonical.SystemImage', out_signature='isssa{ss}')
636 def Info(self):
637 self._loop.keepalive()
638@@ -279,6 +329,7 @@
639 last_update_date(),
640 version_detail())
641
642+ @log_and_exit
643 @method('com.canonical.SystemImage', out_signature='a{ss}')
644 def Information(self):
645 self._loop.keepalive()
646@@ -292,6 +343,7 @@
647 last_check_date=settings.get('last_check_date'),
648 )
649
650+ @log_and_exit
651 @method('com.canonical.SystemImage', in_signature='ss')
652 def SetSetting(self, key, value):
653 """Set a key/value setting.
654@@ -321,12 +373,14 @@
655 # Send the signal.
656 self.SettingChanged(key, value)
657
658+ @log_and_exit
659 @method('com.canonical.SystemImage', in_signature='s', out_signature='s')
660 def GetSetting(self, key):
661 """Get a setting."""
662 self._loop.keepalive()
663 return Settings().get(key)
664
665+ @log_and_exit
666 @method('com.canonical.SystemImage')
667 def FactoryReset(self):
668 self._api.factory_reset()
669@@ -334,11 +388,13 @@
670 # reboot procedure.
671 self.Rebooting(True)
672
673+ @log_and_exit
674 @method('com.canonical.SystemImage')
675 def Exit(self):
676 """Quit the daemon immediately."""
677 self._loop.quit()
678
679+ @log_and_exit
680 @signal('com.canonical.SystemImage', signature='bbsiss')
681 def UpdateAvailableStatus(self,
682 is_available, downloading,
683@@ -354,18 +410,21 @@
684 last_update_date, repr(error_reason))
685 self._loop.keepalive()
686
687+ @log_and_exit
688 @signal('com.canonical.SystemImage', signature='id')
689 def UpdateProgress(self, percentage, eta):
690 """Download progress."""
691 log.debug('EMIT UpdateProgress({}, {})', percentage, eta)
692 self._loop.keepalive()
693
694+ @log_and_exit
695 @signal('com.canonical.SystemImage')
696 def UpdateDownloaded(self):
697 """The update has been successfully downloaded."""
698 log.debug('EMIT UpdateDownloaded()')
699 self._loop.keepalive()
700
701+ @log_and_exit
702 @signal('com.canonical.SystemImage', signature='is')
703 def UpdateFailed(self, consecutive_failure_count, last_reason):
704 """The update failed for some reason."""
705@@ -373,18 +432,21 @@
706 consecutive_failure_count, repr(last_reason))
707 self._loop.keepalive()
708
709+ @log_and_exit
710 @signal('com.canonical.SystemImage', signature='i')
711 def UpdatePaused(self, percentage):
712 """The download got paused."""
713 log.debug('EMIT UpdatePaused({})', percentage)
714 self._loop.keepalive()
715
716+ @log_and_exit
717 @signal('com.canonical.SystemImage', signature='ss')
718 def SettingChanged(self, key, new_value):
719 """A setting value has change."""
720 log.debug('EMIT SettingChanged({}, {})', repr(key), repr(new_value))
721 self._loop.keepalive()
722
723+ @log_and_exit
724 @signal('com.canonical.SystemImage', signature='b')
725 def Rebooting(self, status):
726 """The system is rebooting."""
727
728=== modified file 'systemimage/device.py'
729--- systemimage/device.py 2014-02-20 23:03:24 +0000
730+++ systemimage/device.py 2014-09-17 13:42:00 +0000
731@@ -28,7 +28,7 @@
732 class BaseDevice:
733 """Common device calculation actions."""
734
735- def get_device(self):
736+ def get_device(self): # pragma: no cover
737 """Subclasses must override this."""
738 raise NotImplementedError
739
740
741=== modified file 'systemimage/download.py'
742--- systemimage/download.py 2014-07-23 22:51:19 +0000
743+++ systemimage/download.py 2014-09-17 13:42:00 +0000
744@@ -141,7 +141,8 @@
745
746 def _do_paused(self, signal, path, paused):
747 _print('PAUSE:', paused, self._pausable)
748- if self._pausable and config.dbus_service is not None:
749+ send_paused = self._pausable and config.dbus_service is not None
750+ if send_paused: # pragma: no branch
751 # We could plumb through the `service` object from service.py (the
752 # main entry point for system-image-dbus, but that's actually a
753 # bit of a pain, so do the expedient thing and grab the interface
754@@ -155,7 +156,7 @@
755 # There currently is no UpdateResumed() signal.
756
757 def _default(self, *args, **kws):
758- _print('SIGNAL:', args, kws)
759+ _print('SIGNAL:', args, kws) # pragma: no cover
760
761
762 class DBusDownloadManager:
763@@ -171,7 +172,7 @@
764 self._queued_cancel = False
765 self.callback = callback
766
767- def __repr__(self):
768+ def __repr__(self): # pragma: no cover
769 return '<DBusDownloadManager at 0x{:x}>'.format(id(self))
770
771 def get_files(self, downloads, *, pausable=False):
772@@ -316,10 +317,10 @@
773
774 def pause(self):
775 """Pause the download, but only if one is in progress."""
776- if self._iface is not None:
777+ if self._iface is not None: # pragma: no branch
778 self._iface.pause()
779
780 def resume(self):
781 """Resume the download, but only if one is in progress."""
782- if self._iface is not None:
783+ if self._iface is not None: # pragma: no branch
784 self._iface.resume()
785
786=== modified file 'systemimage/gpg.py'
787--- systemimage/gpg.py 2014-07-23 22:51:19 +0000
788+++ systemimage/gpg.py 2014-09-17 13:42:00 +0000
789@@ -128,7 +128,7 @@
790 # condition, but I don't see any good way to eliminate this given
791 # python-gnupg's behavior.
792 for path in self._keyrings:
793- if not os.path.exists(path):
794+ if not os.path.exists(path): # pragma: no cover
795 raise FileNotFoundError(path)
796 if blacklist is not None:
797 if not os.path.exists(blacklist):
798@@ -148,7 +148,7 @@
799 dir=config.tempdir))
800 self._ctx = gnupg.GPG(gnupghome=home, keyring=self._keyrings)
801 self._stack.callback(setattr, self, '_ctx', None)
802- except:
803+ except: # pragma: no cover
804 # Restore all context and re-raise the exception.
805 self._stack.close()
806 raise
807
808=== modified file 'systemimage/helpers.py'
809--- systemimage/helpers.py 2014-07-23 22:51:19 +0000
810+++ systemimage/helpers.py 2014-09-17 13:42:00 +0000
811@@ -17,7 +17,6 @@
812
813 __all__ = [
814 'DEFAULT_DIRMODE',
815- 'ExtendedEncoder',
816 'MiB',
817 'as_loglevel',
818 'as_object',
819@@ -35,7 +34,6 @@
820
821 import os
822 import re
823-import json
824 import time
825 import random
826 import shutil
827@@ -46,13 +44,13 @@
828 from datetime import datetime, timedelta
829 from hashlib import sha256
830 from importlib import import_module
831-from systemimage.bag import Bag
832
833
834 LAST_UPDATE_FILE = '/userdata/.last_update'
835 UNIQUE_MACHINE_ID_FILE = '/var/lib/dbus/machine-id'
836 DEFAULT_DIRMODE = 0o02700
837 MiB = 1 << 20
838+EMPTYSTRING = ''
839
840
841 def calculate_signature(fp, hash_class=None):
842@@ -166,18 +164,15 @@
843 # will properly complain if there's more than one dot.
844 components = sorted(re.findall(r'([\d.]+[smhdw])', value), key=_sortkey)
845 # Complain if the components are out of order.
846- if ''.join(components) != value:
847+ if EMPTYSTRING.join(components) != value:
848 raise ValueError
849 keywords = dict((interval[0].lower(), interval)
850 for interval in ('weeks', 'days', 'hours',
851 'minutes', 'seconds'))
852 keyword_arguments = {}
853 for interval in components:
854- if len(interval) == 0:
855- raise ValueError
856- keyword = keywords.get(interval[-1].lower())
857- if keyword is None:
858- raise ValueError
859+ assert len(interval) > 0, 'Unexpected value: {}'.format(interval)
860+ keyword = keywords[interval[-1].lower()]
861 if keyword in keyword_arguments:
862 raise ValueError
863 if '.' in interval[:-1]:
864@@ -191,28 +186,20 @@
865
866
867 def as_loglevel(value):
868- level = getattr(logging, value.upper(), None)
869- if level is None or not isinstance(level, int):
870- raise ValueError
871- return level
872-
873-
874-class ExtendedEncoder(json.JSONEncoder):
875- """An extended JSON encoder which knows about other data types."""
876-
877- def default(self, obj):
878- if isinstance(obj, datetime):
879- return obj.isoformat()
880- elif isinstance(obj, timedelta):
881- # as_timedelta() does not recognize microseconds, so convert these
882- # to floating seconds, but only if there are any seconds.
883- if obj.seconds > 0 or obj.microseconds > 0:
884- seconds = obj.seconds + obj.microseconds / 1000000.0
885- return '{0}d{1}s'.format(obj.days, seconds)
886- return '{0}d'.format(obj.days)
887- elif isinstance(obj, Bag):
888- return obj.original
889- return json.JSONEncoder.default(self, obj)
890+ # The value can now be a single name, like "info" or two names separated
891+ # by a colon, such as "info:debug". In the later case, the second name is
892+ # used to initialize the systemimage.dbus logger. In the former case, the
893+ # dbus logger defaults to 'error'.
894+ main, colon, dbus = value.upper().partition(':')
895+ if len(dbus) == 0:
896+ dbus = 'ERROR'
897+ main_level = getattr(logging, main, None)
898+ if main_level is None or not isinstance(main_level, int):
899+ raise ValueError
900+ dbus_level = getattr(logging, dbus, None)
901+ if dbus_level is None or not isinstance(dbus_level, int):
902+ raise ValueError
903+ return main_level, dbus_level
904
905
906 @contextmanager
907@@ -234,10 +221,7 @@
908
909
910 def makedirs(dir, mode=DEFAULT_DIRMODE):
911- try:
912- os.makedirs(dir, mode=mode, exist_ok=True)
913- except (PermissionError, FileExistsError):
914- pass
915+ os.makedirs(dir, mode=mode, exist_ok=True)
916
917
918 def last_update_date():
919@@ -263,7 +247,7 @@
920 # Seconds resolution.
921 timestamp = timestamp.replace(microsecond=0)
922 return str(timestamp)
923- except FileNotFoundError:
924+ except (FileNotFoundError, PermissionError):
925 pass
926 else:
927 return 'Unknown'
928@@ -278,12 +262,11 @@
929 if details_string is None:
930 return {}
931 details = {}
932- if details is not None:
933- for item in details_string.strip().split(','):
934- name, equals, version = item.partition('=')
935- if equals != '=':
936- continue
937- details[name] = version
938+ for item in details_string.strip().split(','):
939+ name, equals, version = item.partition('=')
940+ if equals != '=':
941+ continue
942+ details[name] = version
943 return details
944
945
946
947=== modified file 'systemimage/image.py'
948--- systemimage/image.py 2014-02-20 23:03:24 +0000
949+++ systemimage/image.py 2014-09-17 13:42:00 +0000
950@@ -67,7 +67,7 @@
951 def __ne__(self, other):
952 return not self.__eq__(other)
953
954- def __repr__(self):
955+ def __repr__(self): # pragma: no cover
956 return '<Image: {}>'.format(COMMASPACE.join(sorted(
957 key for key in self.__dict__ if not key.startswith('_'))))
958
959
960=== modified file 'systemimage/logging.py'
961--- systemimage/logging.py 2014-07-23 22:51:19 +0000
962+++ systemimage/logging.py 2014-09-17 13:42:00 +0000
963@@ -64,7 +64,7 @@
964 if self.args:
965 msg = msg.format(*self.args)
966 return msg
967- else:
968+ else: # pragma: no cover
969 return super().getMessage()
970
971
972@@ -79,45 +79,47 @@
973
974 def initialize(*, verbosity=0):
975 """Initialize the loggers."""
976- level = {
977- 0: logging.ERROR,
978- 1: logging.INFO,
979- 2: logging.DEBUG,
980- 3: logging.CRITICAL,
981- }.get(verbosity, logging.ERROR)
982- level = min(level, config.system.loglevel)
983- # Make sure our library's logging uses {}-style messages.
984- logging.setLogRecordFactory(FormattingLogRecord)
985- # Now configure the application level logger based on the ini file.
986- log = logging.getLogger('systemimage')
987- try:
988- handler = _make_handler(Path(config.system.logfile))
989- except PermissionError:
990- handler = _make_handler(
991- Path(xdg_cache_home) / 'system-image' / 'client.log')
992- handler.setLevel(level)
993- formatter = logging.Formatter(style='{', fmt=MSG_FMT, datefmt=DATE_FMT)
994- handler.setFormatter(formatter)
995- log.addHandler(handler)
996- log.propagate = False
997- # If we want more verbosity, add a stream handler.
998- if verbosity == 0:
999- # Set the log level.
1000- log.setLevel(level)
1001- return
1002- handler = logging.StreamHandler(stream=sys.stderr)
1003- handler.setLevel(level)
1004- handler.setFormatter(formatter)
1005- log.addHandler(handler)
1006- # Set the overall level on the log object to the minimum level.
1007- log.setLevel(level)
1008+ main, dbus = config.system.loglevel
1009+ for name, loglevel in (('systemimage', main), ('systemimage.dbus', dbus)):
1010+ level = {
1011+ 0: logging.ERROR,
1012+ 1: logging.INFO,
1013+ 2: logging.DEBUG,
1014+ 3: logging.CRITICAL,
1015+ }.get(verbosity, logging.ERROR)
1016+ level = min(level, loglevel)
1017+ # Make sure our library's logging uses {}-style messages.
1018+ logging.setLogRecordFactory(FormattingLogRecord)
1019+ # Now configure the application level logger based on the ini file.
1020+ log = logging.getLogger(name)
1021+ try:
1022+ handler = _make_handler(Path(config.system.logfile))
1023+ except PermissionError:
1024+ handler = _make_handler(
1025+ Path(xdg_cache_home) / 'system-image' / 'client.log')
1026+ handler.setLevel(level)
1027+ formatter = logging.Formatter(style='{', fmt=MSG_FMT, datefmt=DATE_FMT)
1028+ handler.setFormatter(formatter)
1029+ log.addHandler(handler)
1030+ log.propagate = False
1031+ # If we want more verbosity, add a stream handler.
1032+ if verbosity == 0: # pragma: no branch
1033+ # Set the log level.
1034+ log.setLevel(level)
1035+ else: # pragma: no cover
1036+ handler = logging.StreamHandler(stream=sys.stderr)
1037+ handler.setLevel(level)
1038+ handler.setFormatter(formatter)
1039+ log.addHandler(handler)
1040+ # Set the overall level on the log object to the minimum level.
1041+ log.setLevel(level)
1042 # Please be quiet gnupg.
1043 gnupg_log = logging.getLogger('gnupg')
1044 gnupg_log.propagate = False
1045
1046
1047 @contextmanager
1048-def debug_logging():
1049+def debug_logging(): # pragma: no cover
1050 # getEffectiveLevel() is the best we can do, but it's good enough because
1051 # we always set the level of the logger.
1052 log = logging.getLogger('systemimage')
1053
1054=== modified file 'systemimage/main.py'
1055--- systemimage/main.py 2014-07-23 22:51:19 +0000
1056+++ systemimage/main.py 2014-09-17 13:42:00 +0000
1057@@ -25,6 +25,7 @@
1058 import sys
1059 import logging
1060 import argparse
1061+import warnings
1062
1063 from dbus.mainloop.glib import DBusGMainLoop
1064 from pkg_resources import resource_string as resource_bytes
1065@@ -138,7 +139,7 @@
1066 config.load(args.config)
1067 except FileNotFoundError as error:
1068 parser.error('\nConfiguration file not found: {}'.format(error))
1069- assert 'parser.error() does not return'
1070+ assert 'parser.error() does not return' # pragma: no cover
1071 # Load the optional channel.ini file, which must live next to the
1072 # configuration file. It's okay if this file does not exist.
1073 channel_ini = os.path.join(os.path.dirname(args.config), 'channel.ini')
1074@@ -158,7 +159,7 @@
1075 if sum(bool(arg) for arg in
1076 (args.set, args.get, args.delete, args.show_settings)) > 1:
1077 parser.error('Cannot mix and match settings arguments')
1078- assert 'parser.error() does not return'
1079+ assert 'parser.error() does not return' # pragma: no cover
1080
1081 if args.show_settings:
1082 rows = sorted(Settings())
1083@@ -191,7 +192,7 @@
1084 candidate_filter = delta_filter
1085 else:
1086 parser.error('Bad filter type: {}'.format(args.filter))
1087- assert 'parser.error() does not return'
1088+ assert 'parser.error() does not return' # pragma: no cover
1089
1090 # Create the temporary directory if it doesn't exist.
1091 makedirs(config.system.tempdir)
1092@@ -212,7 +213,7 @@
1093 except ValueError:
1094 parser.error(
1095 '-b/--build requires an integer: {}'.format(args.build))
1096- assert 'parser.error() does not return'
1097+ assert 'parser.error() does not return' # pragma: no cover
1098 if args.channel is not None:
1099 config.channel = args.channel
1100 if args.device is not None:
1101@@ -265,8 +266,13 @@
1102 print(' {} (alias for: {})'.format(key, alias))
1103 return 0
1104
1105- # We can either run the API directly or through DBus.
1106- if args.dbus:
1107+ # 2014-09-15 BAW: --dbus is deprecated (LP: #1369714) and will be removed
1108+ # in system-image 2.5 (LP: #1369717).
1109+ if args.dbus: # pragma: no cover
1110+ print('WARNING: --dbus is deprecated and will be removed soon',
1111+ file=sys.stderr)
1112+ warnings.warn('--dbus is deprecated and will be removed soon',
1113+ DeprecationWarning)
1114 client = DBusClient()
1115 client.check_for_update()
1116 if not client.is_available:
1117@@ -288,7 +294,7 @@
1118 # can take a long time to download all the data files. As a compromise,
1119 # we'll output some dots to stderr at verbosity 1, but we won't log these
1120 # dots since they would just be noise. This doesn't have to be perfect.
1121- if args.verbose == 1:
1122+ if args.verbose == 1: # pragma: no cover
1123 dot_count = 0
1124 def callback(received, total):
1125 nonlocal dot_count
1126@@ -338,7 +344,7 @@
1127 state.run_until('reboot')
1128 else:
1129 list(state)
1130- except KeyboardInterrupt:
1131+ except KeyboardInterrupt: # pragma: no cover
1132 return 0
1133 except Exception:
1134 log.exception('system-image-cli exception')
1135@@ -349,5 +355,5 @@
1136 log.info('state machine finished')
1137
1138
1139-if __name__ == '__main__':
1140+if __name__ == '__main__': # pragma: no cover
1141 sys.exit(main())
1142
1143=== modified file 'systemimage/reactor.py'
1144--- systemimage/reactor.py 2014-02-20 23:03:24 +0000
1145+++ systemimage/reactor.py 2014-09-17 13:42:00 +0000
1146@@ -63,7 +63,7 @@
1147 if method is None:
1148 # See if there's a default catch all.
1149 method = getattr(self, '_default', None)
1150- if method is None:
1151+ if method is None: # pragma: no cover
1152 log.info('No handler for signal {}: {} {}', signal, args, kws)
1153 else:
1154 method(signal, path, *args, **kws)
1155
1156=== modified file 'systemimage/reboot.py'
1157--- systemimage/reboot.py 2014-07-23 22:51:19 +0000
1158+++ systemimage/reboot.py 2014-09-17 13:42:00 +0000
1159@@ -35,7 +35,7 @@
1160 class BaseReboot:
1161 """Common reboot actions."""
1162
1163- def reboot(self):
1164+ def reboot(self): # pragma: no cover
1165 """Subclasses must override this."""
1166 raise NotImplementedError
1167
1168
1169=== modified file 'systemimage/scores.py'
1170--- systemimage/scores.py 2014-07-23 22:51:19 +0000
1171+++ systemimage/scores.py 2014-09-17 13:42:00 +0000
1172@@ -80,7 +80,7 @@
1173 log.debug('{}'.format(fp.getvalue()))
1174 return scores[0][2]
1175
1176- def score(self, candidates):
1177+ def score(self, candidates): # pragma: no cover
1178 """Like `choose()` except returns the candidate path scores.
1179
1180 Subclasses are expected to override this method.
1181
1182=== modified file 'systemimage/service.py'
1183--- systemimage/service.py 2014-02-20 23:03:24 +0000
1184+++ systemimage/service.py 2014-09-17 13:42:00 +0000
1185@@ -40,7 +40,7 @@
1186 # the systemimage-dev binary package is installed in Ubuntu.
1187 try:
1188 from systemimage.testing.dbus import instrument, get_service
1189-except ImportError:
1190+except ImportError: # pragma: no cover
1191 instrument = None
1192 get_service = None
1193
1194@@ -51,6 +51,8 @@
1195
1196 def main():
1197 global config
1198+ # If enabled, start code coverage collection as early as possible.
1199+ # Parse arguments.
1200 parser = argparse.ArgumentParser(
1201 prog='system-image-dbus',
1202 description='Ubuntu System Image Upgrader DBus service')
1203@@ -66,7 +68,7 @@
1204 default=0, action='count',
1205 help='Increase verbosity')
1206 # Hidden argument for special setup required by test environment.
1207- if instrument is not None:
1208+ if instrument is not None: # pragma: no branch
1209 parser.add_argument('--testing',
1210 default=False, action='store',
1211 help=argparse.SUPPRESS)
1212@@ -76,7 +78,7 @@
1213 config.load(args.config)
1214 except FileNotFoundError as error:
1215 parser.error('\nConfiguration file not found: {}'.format(error))
1216- assert 'parser.error() does not return'
1217+ assert 'parser.error() does not return' # pragma: no cover
1218 # Load the optional channel.ini file, which must live next to the
1219 # configuration file. It's okay if this file does not exist.
1220 channel_ini = os.path.join(os.path.dirname(args.config), 'channel.ini')
1221@@ -101,7 +103,7 @@
1222 if code == dbus.bus.REQUEST_NAME_REPLY_EXISTS:
1223 # Another instance already owns this name. Exit.
1224 log.error('Cannot get exclusive ownership of bus name.')
1225- sys.exit(2)
1226+ return 2
1227
1228 log.info('SystemImage dbus main loop starting [{}/{}]',
1229 config.channel, config.device)
1230@@ -118,14 +120,14 @@
1231 config.dbus_service = Service(system_bus, '/Service', loop)
1232 try:
1233 loop.run()
1234- except KeyboardInterrupt:
1235+ except KeyboardInterrupt: # pragma: no cover
1236 log.info('SystemImage dbus main loop interrupted')
1237- except:
1238+ except: # pragma: no cover
1239 log.exception('D-Bus loop exception')
1240 raise
1241 else:
1242 log.info('SystemImage dbus main loop exited')
1243
1244
1245-if __name__ == '__main__':
1246+if __name__ == '__main__': # pragma: no cover
1247 sys.exit(main())
1248
1249=== modified file 'systemimage/settings.py'
1250--- systemimage/settings.py 2014-07-31 23:20:10 +0000
1251+++ systemimage/settings.py 2014-09-17 13:42:00 +0000
1252@@ -41,7 +41,7 @@
1253 self._dbpath = None
1254 try:
1255 with self._cursor():
1256- pass
1257+ pass # pragma: no branch
1258 except sqlite3.OperationalError:
1259 self._check_fallback()
1260 with self._cursor() as c:
1261
1262=== modified file 'systemimage/state.py'
1263--- systemimage/state.py 2014-07-23 22:51:19 +0000
1264+++ systemimage/state.py 2014-09-17 13:42:00 +0000
1265@@ -52,6 +52,12 @@
1266 class ChecksumError(Exception):
1267 """Exception raised when a file's checksum does not match."""
1268
1269+ def __init__(self, destination, got, checksum):
1270+ super().__init__()
1271+ self.destination = destination
1272+ self.got = got
1273+ self.expected = checksum
1274+
1275
1276 def _copy_if_missing(src, dstdir):
1277 dst_path = os.path.join(dstdir, os.path.basename(src))
1278
1279=== modified file 'systemimage/testing/controller.py'
1280--- systemimage/testing/controller.py 2014-07-23 22:51:19 +0000
1281+++ systemimage/testing/controller.py 2014-09-17 13:42:00 +0000
1282@@ -112,7 +112,7 @@
1283
1284 SERVICES = [
1285 ('com.canonical.SystemImage',
1286- '{python} -m systemimage.service -C {self.ini_path} --testing {self.mode}',
1287+ '{python} -m {self.MODULE} -C {self.ini_path} --testing {self.mode}',
1288 start_system_image,
1289 stop_system_image,
1290 ),
1291@@ -128,7 +128,9 @@
1292 class Controller:
1293 """Start and stop D-Bus service under test."""
1294
1295- def __init__(self, logfile=None):
1296+ MODULE = 'systemimage.testing.service'
1297+
1298+ def __init__(self, logfile=None, loglevel='info'):
1299 # Non-public.
1300 self._stack = ExitStack()
1301 self._stoppers = []
1302@@ -158,8 +160,10 @@
1303 template = resource_bytes(
1304 'systemimage.tests.data', 'config_03.ini').decode('utf-8')
1305 with open(self.ini_path, 'w', encoding='utf-8') as fp:
1306- print(template.format(tmpdir=ini_tmpdir, vardir=ini_vardir,
1307- logfile=ini_logfile),
1308+ print(template.format(tmpdir=ini_tmpdir,
1309+ vardir=ini_vardir,
1310+ logfile=ini_logfile,
1311+ loglevel=loglevel),
1312 file=fp)
1313
1314 def _configure_services(self):
1315
1316=== modified file 'systemimage/testing/dbus.py'
1317--- systemimage/testing/dbus.py 2014-07-23 22:51:19 +0000
1318+++ systemimage/testing/dbus.py 2014-09-17 13:42:00 +0000
1319@@ -27,7 +27,7 @@
1320 from gi.repository import GLib
1321 from systemimage.api import Mediator
1322 from systemimage.config import config
1323-from systemimage.dbus import Service
1324+from systemimage.dbus import Service, log_and_exit
1325 from systemimage.helpers import MiB, makedirs, safe_remove, version_detail
1326 from unittest.mock import patch
1327
1328@@ -62,6 +62,7 @@
1329 class _LiveTestableService(Service):
1330 """For testing purposes only."""
1331
1332+ @log_and_exit
1333 @method('com.canonical.SystemImage')
1334 def Reset(self):
1335 self._api = Mediator()
1336@@ -77,6 +78,7 @@
1337 del config.build_number
1338 safe_remove(config.system.settings_db)
1339
1340+ @log_and_exit
1341 @method('com.canonical.SystemImage')
1342 def TearDown(self):
1343 # Like CancelUpdate() except it sends a different signal that's only
1344@@ -84,6 +86,7 @@
1345 self._api.cancel()
1346 self.TornDown()
1347
1348+ @log_and_exit
1349 @signal('com.canonical.SystemImage')
1350 def TornDown(self):
1351 pass
1352@@ -106,16 +109,19 @@
1353 self._percentage = 0
1354 self._rebootable = False
1355
1356+ @log_and_exit
1357 @method('com.canonical.SystemImage')
1358 def Reset(self):
1359 self._reset()
1360
1361+ @log_and_exit
1362 @method('com.canonical.SystemImage')
1363 def CheckForUpdate(self):
1364 if self._failure_count > 0:
1365 self._reset()
1366 GLib.timeout_add_seconds(3, self._send_status)
1367
1368+ @log_and_exit
1369 def _send_status(self):
1370 if self._auto_download:
1371 self._downloading = True
1372@@ -132,6 +138,7 @@
1373 self.UpdateDownloaded()
1374 return False
1375
1376+ @log_and_exit
1377 def _send_more_status(self):
1378 if self._canceled:
1379 self._downloading = False
1380@@ -151,6 +158,7 @@
1381 # Continue sending more status.
1382 return True
1383
1384+ @log_and_exit
1385 @method('com.canonical.SystemImage', out_signature='s')
1386 def PauseDownload(self):
1387 if self._downloading:
1388@@ -159,6 +167,7 @@
1389 # Otherwise it's a no-op.
1390 return ''
1391
1392+ @log_and_exit
1393 @method('com.canonical.SystemImage')
1394 def DownloadUpdate(self):
1395 self._paused = False
1396@@ -168,6 +177,7 @@
1397 self.UpdateProgress(0, 50.0)
1398 GLib.timeout_add(500, self._send_more_status)
1399
1400+ @log_and_exit
1401 @method('com.canonical.SystemImage', out_signature='s')
1402 def CancelUpdate(self):
1403 if self._downloading:
1404@@ -175,6 +185,7 @@
1405 # Otherwise it's a no-op.
1406 return ''
1407
1408+ @log_and_exit
1409 @method('com.canonical.SystemImage')
1410 def ApplyUpdate(self):
1411 # Always succeeds.
1412@@ -196,16 +207,24 @@
1413
1414 def _reset(self):
1415 self._failure_count = 1
1416+ self._last_error = 'mock service failed'
1417
1418+ @log_and_exit
1419 @method('com.canonical.SystemImage')
1420 def Reset(self):
1421 self._reset()
1422
1423+ @log_and_exit
1424 @method('com.canonical.SystemImage')
1425 def CheckForUpdate(self):
1426 msg = ('You need some network for downloading'
1427 if self._failure_count > 0
1428 else '')
1429+ # Fake enough of the update status to trick _download() into checking
1430+ # the failure state.
1431+ class Update:
1432+ is_available = True
1433+ self._update = Update()
1434 self.UpdateAvailableStatus(
1435 True, False, '42', 1337 * MiB,
1436 '1983-09-13T12:13:14',
1437@@ -214,6 +233,7 @@
1438 self._failure_count += 1
1439 self.UpdateFailed(self._failure_count, msg)
1440
1441+ @log_and_exit
1442 @method('com.canonical.SystemImage', out_signature='s')
1443 def CancelUpdate(self):
1444 self._failure_count = 0
1445@@ -221,10 +241,12 @@
1446
1447
1448 class _FailApply(Service):
1449+ @log_and_exit
1450 @method('com.canonical.SystemImage')
1451 def Reset(self):
1452 pass
1453
1454+ @log_and_exit
1455 @method('com.canonical.SystemImage')
1456 def CheckForUpdate(self):
1457 self.UpdateAvailableStatus(
1458@@ -233,6 +255,7 @@
1459 '')
1460 self.UpdateDownloaded()
1461
1462+ @log_and_exit
1463 @method('com.canonical.SystemImage')
1464 def ApplyUpdate(self):
1465 # The update cannot be applied.
1466@@ -242,10 +265,12 @@
1467
1468
1469 class _FailResume(Service):
1470+ @log_and_exit
1471 @method('com.canonical.SystemImage')
1472 def Reset(self):
1473 pass
1474
1475+ @log_and_exit
1476 @method('com.canonical.SystemImage')
1477 def CheckForUpdate(self):
1478 self.UpdateAvailableStatus(
1479@@ -254,16 +279,19 @@
1480 '')
1481 self.UpdatePaused(42)
1482
1483+ @log_and_exit
1484 @method('com.canonical.SystemImage')
1485 def DownloadUpdate(self):
1486 self.UpdateFailed(9, 'You need some network for downloading')
1487
1488
1489 class _FailPause(Service):
1490+ @log_and_exit
1491 @method('com.canonical.SystemImage')
1492 def Reset(self):
1493 pass
1494
1495+ @log_and_exit
1496 @method('com.canonical.SystemImage')
1497 def CheckForUpdate(self):
1498 self.UpdateAvailableStatus(
1499@@ -272,20 +300,24 @@
1500 '')
1501 self.UpdateProgress(10, 0)
1502
1503+ @log_and_exit
1504 @method('com.canonical.SystemImage', out_signature='s')
1505 def PauseDownload(self):
1506 return 'no no, not now'
1507
1508
1509 class _NoUpdate(Service):
1510+ @log_and_exit
1511 @method('com.canonical.SystemImage')
1512 def Reset(self):
1513 pass
1514
1515+ @log_and_exit
1516 @method('com.canonical.SystemImage')
1517 def CheckForUpdate(self):
1518 GLib.timeout_add_seconds(3, self._send_status)
1519
1520+ @log_and_exit
1521 def _send_status(self):
1522 self.UpdateAvailableStatus(
1523 False, False, '', 0,
1524@@ -303,15 +335,18 @@
1525 self._version = 'ubuntu=123,mako=456,custom=789'
1526 self._checked = '2099-08-01 04:45:00'
1527
1528+ @log_and_exit
1529 @method('com.canonical.SystemImage')
1530 def Reset(self):
1531 pass
1532
1533+ @log_and_exit
1534 @method('com.canonical.SystemImage', out_signature='isssa{ss}')
1535 def Info(self):
1536 return (self._buildno, self._device, self._channel, self._updated,
1537 version_detail(self._version))
1538
1539+ @log_and_exit
1540 @method('com.canonical.SystemImage', out_signature='a{ss}')
1541 def Information(self):
1542 return dict(current_build_number=str(self._buildno),
1543@@ -322,6 +357,39 @@
1544 last_check_date=self._checked)
1545
1546
1547+class _Crasher(Service):
1548+ @log_and_exit
1549+ @method('com.canonical.SystemImage')
1550+ def Crash(self):
1551+ 1/0
1552+
1553+ @log_and_exit
1554+ @signal('com.canonical.SystemImage')
1555+ def SignalCrash(self):
1556+ 1/0
1557+
1558+ @log_and_exit
1559+ @signal('com.canonical.SystemImage')
1560+ def SignalOkay(self):
1561+ pass
1562+
1563+ @log_and_exit
1564+ @method('com.canonical.SystemImage')
1565+ def CrashSignal(self):
1566+ self.SignalCrash()
1567+
1568+ @log_and_exit
1569+ @method('com.canonical.SystemImage')
1570+ def Okay(self):
1571+ pass
1572+
1573+ @log_and_exit
1574+ @method('com.canonical.SystemImage')
1575+ def CrashAfterSignal(self):
1576+ self.SignalOkay()
1577+ 1/0
1578+
1579+
1580 def get_service(testing_mode, system_bus, object_path, loop):
1581 """Return the appropriate service class for the testing mode."""
1582 if testing_mode == 'live':
1583@@ -342,6 +410,8 @@
1584 ServiceClass = _NoUpdate
1585 elif testing_mode == 'more-info':
1586 ServiceClass = _MoreInfo
1587+ elif testing_mode == 'crasher':
1588+ ServiceClass = _Crasher
1589 else:
1590 raise RuntimeError('Invalid testing mode: {}'.format(testing_mode))
1591 return ServiceClass(system_bus, object_path, loop)
1592
1593=== modified file 'systemimage/testing/helpers.py'
1594--- systemimage/testing/helpers.py 2014-07-23 22:51:19 +0000
1595+++ systemimage/testing/helpers.py 2014-09-17 13:42:00 +0000
1596@@ -461,9 +461,10 @@
1597 # for the specific ini_path for the instance we care about. Yeah, this
1598 # all kind of sucks, but should be effective in finding the one we need to
1599 # track.
1600+ from systemimage.testing.controller import Controller
1601 for process in psutil.process_iter():
1602 cmdline = SPACE.join(process.cmdline())
1603- if 'systemimage.service' in cmdline and ini_path in cmdline:
1604+ if Controller.MODULE in cmdline and ini_path in cmdline:
1605 return process
1606 return None
1607
1608
1609=== modified file 'systemimage/testing/nose.py'
1610--- systemimage/testing/nose.py 2014-07-23 22:51:19 +0000
1611+++ systemimage/testing/nose.py 2014-09-17 13:42:00 +0000
1612@@ -22,9 +22,11 @@
1613
1614 import re
1615 import atexit
1616+import logging
1617
1618 from dbus.mainloop.glib import DBusGMainLoop
1619 from nose2.events import Plugin
1620+from systemimage.config import config
1621 from systemimage.logging import initialize
1622 from systemimage.testing.controller import Controller
1623 from systemimage.testing.helpers import configuration
1624@@ -76,21 +78,26 @@
1625 self.patterns = []
1626 self.verbosity = 0
1627 self.log_file = None
1628+ self.log_level = 'info'
1629 self.addArgument(self.patterns, 'P', 'pattern',
1630 'Add a test matching pattern')
1631 def bump(ignore):
1632 self.verbosity += 1
1633- self.addFlag(bump, 'V', 'Verbosity',
1634+ self.addFlag(bump, 'V', 'verbosity',
1635 'Increase system-image verbosity')
1636 def set_log_file(path):
1637 self.log_file = path[0]
1638 self.addOption(set_log_file, 'L', 'logfile',
1639 'Set the log file for the test run',
1640 nargs=1)
1641+ def set_dbus_loglevel(level):
1642+ self.log_level = 'info:{}'.format(level[0])
1643+ self.addOption(set_dbus_loglevel, 'M', 'loglevel',
1644+ 'Set the systemimage.dbus log level',
1645+ nargs=1)
1646
1647 @configuration
1648 def startTestRun(self, event):
1649- from systemimage.config import config
1650 if self.log_file is not None:
1651 config.system.logfile = self.log_file
1652 DBusGMainLoop(set_as_default=True)
1653@@ -101,7 +108,8 @@
1654 # individual services, and we can write new dbus configuration files
1655 # and HUP the dbus-launch to re-read them, but we cannot change bus
1656 # addresses after the initial one is set.
1657- SystemImagePlugin.controller = Controller(self.log_file)
1658+ SystemImagePlugin.controller = Controller(
1659+ self.log_file, self.log_level)
1660 SystemImagePlugin.controller.start()
1661 atexit.register(SystemImagePlugin.controller.stop)
1662
1663
1664=== added file 'systemimage/testing/service.py'
1665--- systemimage/testing/service.py 1970-01-01 00:00:00 +0000
1666+++ systemimage/testing/service.py 2014-09-17 13:42:00 +0000
1667@@ -0,0 +1,50 @@
1668+# Copyright (C) 2014 Canonical Ltd.
1669+# Author: Barry Warsaw <barry@ubuntu.com>
1670+
1671+# This program is free software: you can redistribute it and/or modify
1672+# it under the terms of the GNU General Public License as published by
1673+# the Free Software Foundation; version 3 of the License.
1674+#
1675+# This program is distributed in the hope that it will be useful,
1676+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1677+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1678+# GNU General Public License for more details.
1679+#
1680+# You should have received a copy of the GNU General Public License
1681+# along with this program. If not, see <http://www.gnu.org/licenses/>.
1682+
1683+"""DBus service testing pre-load module.
1684+
1685+This is arranged so that the test suite can enable code coverage data
1686+collection as early as possible in the private bus D-Bus activated processes.
1687+"""
1688+
1689+import os
1690+
1691+# It's okay if this module isn't available.
1692+try:
1693+ from coverage.control import coverage as _Coverage
1694+except ImportError:
1695+ _Coverage = None
1696+
1697+
1698+def main():
1699+ # Enable code coverage.
1700+ ini_file = os.environ.get('COVERAGE_PROCESS_START')
1701+ if _Coverage is not None and ini_file is not None:
1702+ coverage =_Coverage(config_file=ini_file, auto_data=True)
1703+ # Stolen from coverage.process_startup()
1704+ coverage.erase()
1705+ coverage.start()
1706+ coverage._warn_no_data = False
1707+ coverage._warn_unimported_source = False
1708+ # All systemimage imports happen here so that we have the best possible
1709+ # chance of instrumenting all relevant code.
1710+ from systemimage.service import main as real_main
1711+ # Now run the actual D-Bus service.
1712+ return real_main()
1713+
1714+
1715+if __name__ == '__main__':
1716+ import sys
1717+ sys.exit(main())
1718
1719=== added file 'systemimage/tests/data/channel_06.ini'
1720--- systemimage/tests/data/channel_06.ini 1970-01-01 00:00:00 +0000
1721+++ systemimage/tests/data/channel_06.ini 2014-09-17 13:42:00 +0000
1722@@ -0,0 +1,8 @@
1723+[service]
1724+base: localhost
1725+http_port: 8980
1726+https_port: 8943
1727+channel: daily
1728+build_number: 300
1729+channel_target: saucy
1730+device: shoephone
1731
1732=== added file 'systemimage/tests/data/channel_07.ini'
1733--- systemimage/tests/data/channel_07.ini 1970-01-01 00:00:00 +0000
1734+++ systemimage/tests/data/channel_07.ini 2014-09-17 13:42:00 +0000
1735@@ -0,0 +1,8 @@
1736+[service]
1737+base: localhost
1738+http_port: 8980
1739+https_port: 8943
1740+channel: daily
1741+build_number: 300
1742+channel_target: saucy
1743+device:
1744
1745=== modified file 'systemimage/tests/data/config_03.ini'
1746--- systemimage/tests/data/config_03.ini 2014-02-20 23:03:24 +0000
1747+++ systemimage/tests/data/config_03.ini 2014-09-17 13:42:00 +0000
1748@@ -15,7 +15,7 @@
1749 build_file: {tmpdir}/ubuntu-build
1750 tempdir: {tmpdir}/tmp
1751 logfile: {logfile}
1752-loglevel: info
1753+loglevel: {loglevel}
1754 settings_db: {vardir}/settings.db
1755
1756 [gpg]
1757
1758=== added file 'systemimage/tests/data/config_09.ini'
1759--- systemimage/tests/data/config_09.ini 1970-01-01 00:00:00 +0000
1760+++ systemimage/tests/data/config_09.ini 2014-09-17 13:42:00 +0000
1761@@ -0,0 +1,27 @@
1762+# Bogus configuration file missing the [system] stanza.
1763+
1764+[service]
1765+base: phablet.example.com
1766+# Negative ports are not allowed.
1767+http_port: 80
1768+https_port: disabled
1769+channel: stable
1770+build_number: 0
1771+
1772+[gpg]
1773+archive_master: /etc/phablet/archive-master.tar.xz
1774+image_master: /etc/phablet/image-master.tar.xz
1775+image_signing: /var/lib/phablet/image-signing.tar.xz
1776+device_signing: /var/lib/phablet/device-signing.tar.xz
1777+
1778+[updater]
1779+cache_partition: /android/cache
1780+data_partition: /var/lib/phablet/updater
1781+
1782+[hooks]
1783+device: systemimage.device.SystemProperty
1784+scorer: systemimage.scores.WeightedScorer
1785+reboot: systemimage.reboot.Reboot
1786+
1787+[dbus]
1788+lifetime: 3s
1789
1790=== added file 'systemimage/tests/data/config_10.ini'
1791--- systemimage/tests/data/config_10.ini 1970-01-01 00:00:00 +0000
1792+++ systemimage/tests/data/config_10.ini 2014-09-17 13:42:00 +0000
1793@@ -0,0 +1,35 @@
1794+# Configuration file for specifying relatively static information about the
1795+# upgrade resolution process.
1796+
1797+[service]
1798+base: phablet.example.com
1799+http_port: 80
1800+https_port: 443
1801+channel: stable
1802+build_number: 0
1803+
1804+[system]
1805+timeout: 10s
1806+build_file: /etc/ubuntu-build
1807+tempdir: /tmp
1808+logfile: /var/log/system-image/client.log
1809+loglevel: critical:debug
1810+settings_db: /var/lib/phablet/settings.db
1811+
1812+[gpg]
1813+archive_master: /etc/phablet/archive-master.tar.xz
1814+image_master: /etc/phablet/image-master.tar.xz
1815+image_signing: /var/lib/phablet/image-signing.tar.xz
1816+device_signing: /var/lib/phablet/device-signing.tar.xz
1817+
1818+[updater]
1819+cache_partition: /android/cache
1820+data_partition: /var/lib/phablet/updater
1821+
1822+[hooks]
1823+device: systemimage.device.SystemProperty
1824+scorer: systemimage.scores.WeightedScorer
1825+reboot: systemimage.reboot.Reboot
1826+
1827+[dbus]
1828+lifetime: 2m
1829
1830=== modified file 'systemimage/tests/test_api.py'
1831--- systemimage/tests/test_api.py 2014-07-23 22:51:19 +0000
1832+++ systemimage/tests/test_api.py 2014-09-17 13:42:00 +0000
1833@@ -55,6 +55,18 @@
1834 self.assertTrue(update.is_available)
1835
1836 @configuration
1837+ def test_update_available_cached(self):
1838+ # If we try to check twice on the same mediator object, the second one
1839+ # will return the cached update.
1840+ self._setup_server_keyrings()
1841+ mediator = Mediator()
1842+ update_1 = mediator.check_for_update()
1843+ self.assertTrue(update_1.is_available)
1844+ update_2 = mediator.check_for_update()
1845+ self.assertTrue(update_2.is_available)
1846+ self.assertIs(update_1, update_2)
1847+
1848+ @configuration
1849 def test_update_available_version(self):
1850 # An update is available. What's the target version number?
1851 self._setup_server_keyrings()
1852
1853=== modified file 'systemimage/tests/test_config.py'
1854--- systemimage/tests/test_config.py 2014-07-31 23:20:10 +0000
1855+++ systemimage/tests/test_config.py 2014-09-17 13:42:00 +0000
1856@@ -26,6 +26,7 @@
1857 import logging
1858 import unittest
1859
1860+from contextlib import ExitStack, contextmanager
1861 from datetime import timedelta
1862 from pkg_resources import resource_filename
1863 from subprocess import CalledProcessError, check_output
1864@@ -37,6 +38,21 @@
1865 from unittest.mock import patch
1866
1867
1868+@contextmanager
1869+def _patch_device_hook():
1870+ # The device hook has two things that generally need patching. The first
1871+ # is the logging output, which is just noise for testing purposes, so
1872+ # silence it. The second is that the `getprop` command may actually exist
1873+ # on test system, and we want a consistent environment (i.e. the
1874+ # assumption that the command does not exist).
1875+ with ExitStack() as resources:
1876+ resources.enter_context(patch('systemimage.device.logging.getLogger'))
1877+ resources.enter_context(
1878+ patch('systemimage.device.check_output',
1879+ side_effect=FileNotFoundError))
1880+ yield
1881+
1882+
1883 class TestConfiguration(unittest.TestCase):
1884 def test_defaults(self):
1885 default_ini = resource_filename('systemimage.data', 'client.ini')
1886@@ -54,7 +70,8 @@
1887 self.assertEqual(config.system.build_file, '/etc/ubuntu-build')
1888 self.assertEqual(config.system.logfile,
1889 '/var/log/system-image/client.log')
1890- self.assertEqual(config.system.loglevel, logging.INFO)
1891+ self.assertEqual(config.system.loglevel,
1892+ (logging.INFO, logging.ERROR))
1893 self.assertEqual(config.system.settings_db,
1894 '/var/lib/system-image/settings.db')
1895 # [hooks]
1896@@ -99,7 +116,8 @@
1897 self.assertEqual(config.system.build_file, '/etc/ubuntu-build')
1898 self.assertEqual(config.system.logfile,
1899 '/var/log/system-image/client.log')
1900- self.assertEqual(config.system.loglevel, logging.ERROR)
1901+ self.assertEqual(config.system.loglevel,
1902+ (logging.ERROR, logging.ERROR))
1903 self.assertEqual(config.system.settings_db,
1904 '/var/lib/phablet/settings.db')
1905 self.assertEqual(config.system.timeout, timedelta(seconds=10))
1906@@ -123,6 +141,14 @@
1907 # [dbus]
1908 self.assertEqual(config.dbus.lifetime.total_seconds(), 120)
1909
1910+ def test_special_dbus_logging_level(self):
1911+ # Read a config.ini that has a loglevel value with an explicit dbus
1912+ # logging level.
1913+ ini_file = data_path('config_10.ini')
1914+ config = Configuration(ini_file)
1915+ self.assertEqual(config.system.loglevel,
1916+ (logging.CRITICAL, logging.DEBUG))
1917+
1918 def test_nonstandard_ports(self):
1919 # config_02.ini has non-standard http and https ports.
1920 ini_file = data_path('config_02.ini')
1921@@ -203,6 +229,12 @@
1922 side_effect=CalledProcessError(1, 'ignore')):
1923 self.assertEqual(config.device, '?')
1924
1925+ def test_device_no_getprop_fallback(self):
1926+ # Like above, but a FileNotFoundError occurs instead.
1927+ config = Configuration()
1928+ with _patch_device_hook():
1929+ self.assertEqual(config.device, '?')
1930+
1931 @configuration
1932 def test_get_channel(self, ini_file):
1933 config = Configuration(ini_file)
1934@@ -317,3 +349,32 @@
1935 # LP: #1342183: Configuration constructor takes an ini_file argument.
1936 config = Configuration(data_path('config_01.ini'))
1937 self.assertEqual(config.service.base, 'phablet.example.com')
1938+
1939+ def test_main_ini_file_must_contain_system_stanza(self):
1940+ # It's okay if an override is missing the [system] stanza, but the
1941+ # main ini file (i.e. non-override) must contain it.
1942+ ini_file = data_path('config_09.ini')
1943+ config = Configuration()
1944+ self.assertRaises(KeyError, config.load, ini_file)
1945+
1946+ def test_channel_ini_device_override(self):
1947+ # A channel.ini file can override the device name.
1948+ config = Configuration(data_path('config_01.ini'))
1949+ config.load(data_path('channel_06.ini'), override=True)
1950+ self.assertEqual(config.device, 'shoephone')
1951+
1952+ def test_channel_ini_missing_device_override(self):
1953+ # The channel.ini can omit the [service]device setting, in which case
1954+ # the hook is still used.
1955+ config = Configuration(data_path('config_01.ini'))
1956+ config.load(data_path('channel_05.ini'), override=True)
1957+ with _patch_device_hook():
1958+ self.assertEqual(config.device, '?')
1959+
1960+ def test_channel_ini_empty_device_override(self):
1961+ # The channel.ini can have an empty [service]device setting, in which
1962+ # case the hook is still used.
1963+ config = Configuration(data_path('config_01.ini'))
1964+ config.load(data_path('channel_07.ini'), override=True)
1965+ with _patch_device_hook():
1966+ self.assertEqual(config.device, '?')
1967
1968=== modified file 'systemimage/tests/test_dbus.py'
1969--- systemimage/tests/test_dbus.py 2014-07-23 22:51:19 +0000
1970+++ systemimage/tests/test_dbus.py 2014-09-17 13:42:00 +0000
1971@@ -25,6 +25,8 @@
1972 'TestDBusFactoryReset',
1973 'TestDBusGetSet',
1974 'TestDBusInfo',
1975+ 'TestDBusMiscellaneous',
1976+ 'TestDBusMockCrashers',
1977 'TestDBusMockFailApply',
1978 'TestDBusMockFailPause',
1979 'TestDBusMockFailResume',
1980@@ -48,7 +50,7 @@
1981 import shutil
1982 import unittest
1983
1984-from contextlib import ExitStack
1985+from contextlib import ExitStack, suppress
1986 from datetime import datetime
1987 from dbus.exceptions import DBusException
1988 from functools import partial
1989@@ -103,6 +105,22 @@
1990 self.quit()
1991
1992
1993+class MiscellaneousCancelingReactor(Reactor):
1994+ def __init__(self, iface):
1995+ super().__init__(dbus.SystemBus())
1996+ self._iface = iface
1997+ self.update_failures = []
1998+ self.react_to('UpdateProgress')
1999+ self.react_to('UpdateFailed')
2000+
2001+ def _do_UpdateProgress(self, signal, path, *args, **kws):
2002+ self._iface.CancelUpdate()
2003+
2004+ def _do_UpdateFailed(self, signal, path, *args, **kws):
2005+ self.update_failures.append(args)
2006+ self.quit()
2007+
2008+
2009 class ProgressRecordingReactor(Reactor):
2010 def __init__(self):
2011 super().__init__(dbus.SystemBus())
2012@@ -228,8 +246,11 @@
2013 self.iface = dbus.Interface(service, 'com.canonical.SystemImage')
2014
2015 def tearDown(self):
2016+ self.reset_service()
2017+ super().tearDown()
2018+
2019+ def reset_service(self):
2020 self.iface.Reset()
2021- super().tearDown()
2022
2023 def download_manually(self):
2024 self.iface.SetSetting('auto_download', '0')
2025@@ -574,8 +595,7 @@
2026 failure_count, last_reason = reactor.signals[0]
2027 self.assertEqual(failure_count, 1)
2028 # Don't count on a specific error message.
2029- self.assertEqual(
2030- last_reason[:46], 'systemimage.download.DuplicateDestinationError')
2031+ self.assertEqual(last_reason[:25], 'DuplicateDestinationError')
2032
2033
2034 class TestDBusApply(_LiveTesting):
2035@@ -680,7 +700,7 @@
2036 reactor.run(self.iface.DownloadUpdate)
2037 self.assertEqual(len(reactor.signals), 1)
2038 failure_count, reason = reactor.signals[0]
2039- self.assertEqual(failure_count, 2)
2040+ self.assertEqual(failure_count, 1)
2041 self.assertNotEqual(reason, '')
2042 self.assertFalse(os.path.exists(self.command_file))
2043 # The next check resets the failure count and succeeds.
2044@@ -928,7 +948,7 @@
2045 mode = 'update-failed'
2046
2047 def test_scenario_1(self):
2048- # The server is already in falure mode. A CheckForUpdate() restarts
2049+ # The server is already in failure mode. A CheckForUpdate() restarts
2050 # the check, which returns information about the new update. It
2051 # auto-starts, but this fails.
2052 reactor = MockReactor(self.iface)
2053@@ -947,6 +967,29 @@
2054 self.assertEqual(failure_count, 2)
2055 self.assertEqual(reason, 'You need some network for downloading')
2056
2057+ def test_scenario_2(self):
2058+ # The server starts out in a failure mode. When we ask it to download
2059+ # an update, because it's not already downloading and the failure mode
2060+ # has not been reset, we get an UpdateFailed signal.
2061+ self.iface.CheckForUpdate()
2062+ reactor = SignalCapturingReactor('UpdateFailed')
2063+ reactor.run(self.iface.DownloadUpdate, timeout=10)
2064+ self.assertEqual(len(reactor.signals), 1)
2065+ failure_count, last_error = reactor.signals[0]
2066+ # The failure_count will be three because:
2067+ # 1) it gets set to 1 in the mock's constructor.
2068+ # 2) the mock's CheckForUpdate() bumps it to two.
2069+ # 3) the mock's superclass's DownloadUpdate bumps it to three after it
2070+ # checks to see if downloading is paused (it's not), and if the
2071+ # download is available (it is, though mocked).
2072+ #
2073+ # The code in #3 that terminates with bumping the failure count is the
2074+ # bit we're really trying to test here. An UpdateFailed signal gets
2075+ # sent (the only one in this test, as seen above) and it contains the
2076+ # current failure count as accounted above, and the mock's last error.
2077+ self.assertEqual(failure_count, 3)
2078+ self.assertEqual(last_error, 'mock service failed')
2079+
2080
2081 class TestDBusMockFailApply(_TestBase):
2082 mode = 'fail-apply'
2083@@ -1146,6 +1189,28 @@
2084 # And now there is a command file for the update.
2085 self.assertTrue(os.path.exists(self.command_file))
2086
2087+ def test_lp_1365646(self):
2088+ # After an automatic download is complete, we got three DownloadUpdate
2089+ # calls with no intervening CheckForUpdate. This causes a crash since
2090+ # an unlocked checking lock was released.
2091+ self.download_always()
2092+ # Do a normal automatic download.
2093+ reactor = SignalCapturingReactor('UpdateDownloaded')
2094+ reactor.run(self.iface.CheckForUpdate)
2095+ self.assertEqual(len(reactor.signals), 1)
2096+ # Now, just do a manual DownloadUpdate. We should get an almost
2097+ # immediate UpdateDownloaded in response. Nothing actually gets
2098+ # downloaded, but the files in the cache are still valid. The bug
2099+ # referenced by this method would cause s-i-d to crash, so as long as
2100+ # the process still exists after the signal is received, the bug is
2101+ # fixed. The crash doesn't actually effect any client behavior! But
2102+ # the traceback does show up in the crash reporter.
2103+ process = find_dbus_process(SystemImagePlugin.controller.ini_path)
2104+ reactor = SignalCapturingReactor('UpdateDownloaded')
2105+ reactor.run(self.iface.DownloadUpdate)
2106+ self.assertEqual(len(reactor.signals), 1)
2107+ self.assertTrue(process.is_running())
2108+
2109
2110 class TestDBusGetSet(_TestBase):
2111 """Test the DBus client's key/value settings."""
2112@@ -1457,9 +1522,9 @@
2113 reactor.schedule(self.iface.DownloadUpdate)
2114 reactor.run()
2115 # The only progress we can count on is the first and last ones. All
2116- # will have an eta of 0, since that value is not calculatable right
2117- # now. The first progress will have percentage 0 and the last will
2118- # have percentage 100.
2119+ # will have an eta of 0, since that value is not calculable right now.
2120+ # The first progress will have percentage 0 and the last will have
2121+ # percentage 100.
2122 self.assertGreaterEqual(len(reactor.progress), 2)
2123 percentage, eta = reactor.progress[0]
2124 self.assertEqual(percentage, 0)
2125@@ -1538,6 +1603,12 @@
2126 else:
2127 raise AssertionError('Did not find expected error output')
2128
2129+ def test_must_be_downloading_to_pause(self):
2130+ # You get an error string if you try to pause the download but no
2131+ # download is in progress.
2132+ error_message = self.iface.PauseDownload()
2133+ self.assertEqual(error_message, 'not downloading')
2134+
2135
2136 class TestDBusUseCache(_LiveTesting):
2137 # See LP: #1217098
2138@@ -1754,7 +1825,6 @@
2139
2140
2141 class TestDBusCheckForUpdateWithBrokenIndex(_LiveTesting):
2142-
2143 def test_bad_index_file_crashes_hash(self):
2144 # LP: #1222910. A broken index.json file contained an image with type
2145 # == 'delta' but no base field. This breaks the hash calculation of
2146@@ -1766,3 +1836,84 @@
2147 self.assertEqual(
2148 reactor.signals[0].error_reason,
2149 "'Image' object has no attribute 'base'")
2150+
2151+
2152+class TestDBusMockCrashers(_TestBase):
2153+ """Tests error handling in methods and signals."""
2154+
2155+ mode = 'crasher'
2156+
2157+ def reset_service(self):
2158+ # No-op this so we don't get the tear down .Reset() call messing with
2159+ # our expected results.
2160+ pass
2161+
2162+ def test_method_good_path(self):
2163+ # This tests a wrapped method that does not traceback.
2164+ process = find_dbus_process(SystemImagePlugin.controller.ini_path)
2165+ self.iface.Okay()
2166+ self.assertTrue(process.is_running())
2167+
2168+ def test_method_crasher(self):
2169+ # When this method tracebacks, a log will be written and the process
2170+ # exited. There's no good way to test that the log was written, but
2171+ # it's easy to test that the process exits.
2172+ process = find_dbus_process(SystemImagePlugin.controller.ini_path)
2173+ with suppress(DBusException):
2174+ self.iface.Crash()
2175+ process.wait(5)
2176+ self.assertFalse(process.is_running())
2177+
2178+ def test_signal_crasher(self):
2179+ # Here, it's the signal that tracebacks.
2180+ reactor = SignalCapturingReactor('SignalCrash')
2181+ process = find_dbus_process(SystemImagePlugin.controller.ini_path)
2182+ def safe_run():
2183+ with suppress(DBusException):
2184+ self.iface.CrashSignal()
2185+ reactor.run(safe_run, timeout=5)
2186+ # The signal never made it.
2187+ self.assertEqual(len(reactor.signals), 0)
2188+ process.wait(5)
2189+ self.assertFalse(process.is_running())
2190+
2191+ def test_crash_after_signal(self):
2192+ # Here, the method tracebacks, but not until after it sends the
2193+ # signal, which we should still receive.
2194+ reactor = SignalCapturingReactor('SignalOkay')
2195+ process = find_dbus_process(SystemImagePlugin.controller.ini_path)
2196+ def safe_run():
2197+ with suppress(DBusException):
2198+ self.iface.CrashAfterSignal()
2199+ reactor.run(safe_run, timeout=15)
2200+ # The signal made it.
2201+ self.assertEqual(len(reactor.signals), 1)
2202+ # But the process didn't.
2203+ process.wait(5)
2204+ self.assertFalse(process.is_running())
2205+
2206+
2207+class TestDBusMiscellaneous(_LiveTesting):
2208+ """Various other random tests to improve coverage."""
2209+
2210+ def test_lone_cancel(self):
2211+ # Canceling an update while none is in progress will trigger an
2212+ # ignored exception when the checking lock, which is not acquired, is
2213+ # attempted to be released. That's fine. Note too that since no
2214+ # download is in progress, *no* UpdateFailed signal will be received.
2215+ reactor = SignalCapturingReactor('UpdateFailed')
2216+ reactor.run(self.iface.CancelUpdate, timeout=5)
2217+ self.assertEqual(len(reactor.signals), 0)
2218+
2219+ def test_cancel_while_downloading(self):
2220+ # Wait until we're actually downloading data files, then cancel the
2221+ # update. This tests another code coverage path.
2222+ self.download_always()
2223+ reactor = MiscellaneousCancelingReactor(self.iface)
2224+ reactor.schedule(self.iface.CheckForUpdate)
2225+ reactor.run()
2226+ self.assertEqual(len(reactor.update_failures), 1)
2227+ failure = reactor.update_failures[0]
2228+ # Failure count.
2229+ self.assertEqual(failure[0], 1)
2230+ self.assertEqual(failure[1], 'Canceled')
2231
2232=== modified file 'systemimage/tests/test_download.py'
2233--- systemimage/tests/test_download.py 2014-07-23 22:51:19 +0000
2234+++ systemimage/tests/test_download.py 2014-09-17 13:42:00 +0000
2235@@ -40,10 +40,10 @@
2236 from systemimage.config import Configuration, config
2237 from systemimage.download import (
2238 Canceled, DBusDownloadManager, DuplicateDestinationError, Record)
2239-from systemimage.helpers import MiB, temporary_directory
2240+from systemimage.helpers import temporary_directory
2241 from systemimage.settings import Settings
2242 from systemimage.testing.helpers import (
2243- configuration, data_path, make_http_server)
2244+ configuration, data_path, make_http_server, write_bytes)
2245 from systemimage.testing.nose import SystemImagePlugin
2246 from unittest.mock import patch
2247 from urllib.parse import urljoin
2248@@ -136,6 +136,25 @@
2249 self.assertEqual(total_bytes, 669)
2250
2251 @configuration
2252+ def test_download_with_broken_callback(self):
2253+ # If the callback raises an exception, it is logged and ignored.
2254+ def callback(receive, total):
2255+ raise RuntimeError
2256+ exception = None
2257+ def capture(message):
2258+ nonlocal exception
2259+ exception = message
2260+ downloader = DBusDownloadManager(callback)
2261+ with patch('systemimage.download.log.exception', capture):
2262+ downloader.get_files(_http_pathify([
2263+ ('channels_01.json', 'channels.json'),
2264+ ]))
2265+ # The exception got logged.
2266+ self.assertEqual(exception, 'Exception in progress callback')
2267+ # The file still got downloaded.
2268+ self.assertEqual(os.listdir(config.tempdir), ['channels.json'])
2269+
2270+ @configuration
2271 def test_no_dev_package(self):
2272 # system-image-dev contains the systemimage.testing subpackage, but
2273 # this is not normally installed on the device. When it's missing,
2274@@ -149,6 +168,22 @@
2275 ]))
2276 self.assertEqual(os.listdir(config.tempdir), ['channels.json'])
2277
2278+ @configuration
2279+ def test_timeout(self):
2280+ # If the reactor times out, we get an exception. We fake the timeout
2281+ # by setting the attribute on the reactor, even though it successfully
2282+ # completes its download without timing out.
2283+ def finish_with_timeout(self, *args, **kws):
2284+ self.timed_out = True
2285+ self.quit()
2286+ with patch('systemimage.download.DownloadReactor._do_finished',
2287+ finish_with_timeout):
2288+ self.assertRaises(
2289+ TimeoutError,
2290+ DBusDownloadManager().get_files,
2291+ _http_pathify([('channels_01.json', 'channels.json')])
2292+ )
2293+
2294
2295 class TestHTTPSDownloads(unittest.TestCase):
2296 @classmethod
2297@@ -333,10 +368,8 @@
2298 serverdir = stack.enter_context(temporary_directory())
2299 stack.push(make_http_server(serverdir, 8980))
2300 # Create a couple of big files to download.
2301- with open(os.path.join(serverdir, 'bigfile_1.dat'), 'wb') as fp:
2302- fp.write(b'x' * 10 * MiB)
2303- with open(os.path.join(serverdir, 'bigfile_2.dat'), 'wb') as fp:
2304- fp.write(b'x' * 10 * MiB)
2305+ write_bytes(os.path.join(serverdir, 'bigfile_1.dat'), 10)
2306+ write_bytes(os.path.join(serverdir, 'bigfile_2.dat'), 10)
2307 # The download service doesn't provide reliable cancel
2308 # granularity, so instead, we mock the 'started' signal to
2309 # immediately cancel the download.
2310@@ -364,12 +397,9 @@
2311 serverdir = stack.enter_context(temporary_directory())
2312 stack.push(make_http_server(serverdir, 8980))
2313 # Create a couple of big files to download.
2314- with open(os.path.join(serverdir, 'bigfile_1.dat'), 'wb') as fp:
2315- fp.write(b'x' * 10 * MiB)
2316- with open(os.path.join(serverdir, 'bigfile_2.dat'), 'wb') as fp:
2317- fp.write(b'x' * 10 * MiB)
2318- with open(os.path.join(serverdir, 'bigfile_3.dat'), 'wb') as fp:
2319- fp.write(b'x' * 10 * MiB)
2320+ write_bytes(os.path.join(serverdir, 'bigfile_1.dat'), 10)
2321+ write_bytes(os.path.join(serverdir, 'bigfile_2.dat'), 10)
2322+ write_bytes(os.path.join(serverdir, 'bigfile_3.dat'), 10)
2323 downloads = _http_pathify([
2324 ('bigfile_1.dat', 'bigfile_1.dat'),
2325 ('bigfile_2.dat', 'bigfile_2.dat'),
2326@@ -388,12 +418,9 @@
2327 serverdir = stack.enter_context(temporary_directory())
2328 stack.push(make_http_server(serverdir, 8980))
2329 # Create a couple of big files to download.
2330- with open(os.path.join(serverdir, 'bigfile_1.dat'), 'wb') as fp:
2331- fp.write(b'x' * 10 * MiB)
2332- with open(os.path.join(serverdir, 'bigfile_2.dat'), 'wb') as fp:
2333- fp.write(b'x' * 10 * MiB)
2334- with open(os.path.join(serverdir, 'bigfile_3.dat'), 'wb') as fp:
2335- fp.write(b'x' * 10 * MiB)
2336+ write_bytes(os.path.join(serverdir, 'bigfile_1.dat'), 10)
2337+ write_bytes(os.path.join(serverdir, 'bigfile_2.dat'), 10)
2338+ write_bytes(os.path.join(serverdir, 'bigfile_3.dat'), 10)
2339 downloads = _http_pathify([
2340 ('bigfile_1.dat', 'bigfile_1.dat'),
2341 ('bigfile_2.dat', 'bigfile_2.dat'),
2342
2343=== modified file 'systemimage/tests/test_helpers.py'
2344--- systemimage/tests/test_helpers.py 2014-07-23 22:51:19 +0000
2345+++ systemimage/tests/test_helpers.py 2014-09-17 13:42:00 +0000
2346@@ -19,12 +19,14 @@
2347 __all__ = [
2348 'TestConverters',
2349 'TestLastUpdateDate',
2350+ 'TestMiscellaneous',
2351 'TestPhasedPercentage',
2352 'TestSignature',
2353 ]
2354
2355
2356 import os
2357+import shutil
2358 import hashlib
2359 import logging
2360 import tempfile
2361@@ -32,9 +34,10 @@
2362
2363 from contextlib import ExitStack
2364 from datetime import datetime, timedelta
2365+from systemimage.bag import Bag
2366 from systemimage.config import Configuration, config
2367 from systemimage.helpers import (
2368- Bag, MiB, as_loglevel, as_object, as_timedelta, calculate_signature,
2369+ MiB, as_loglevel, as_object, as_timedelta, calculate_signature,
2370 last_update_date, phased_percentage, temporary_directory, version_detail)
2371 from systemimage.testing.helpers import configuration, data_path, touch_build
2372 from unittest.mock import patch
2373@@ -42,7 +45,7 @@
2374
2375 class TestConverters(unittest.TestCase):
2376 def test_as_object_good_path(self):
2377- self.assertEqual(as_object('systemimage.helpers.Bag'), Bag)
2378+ self.assertEqual(as_object('systemimage.bag.Bag'), Bag)
2379
2380 def test_as_object_no_dot(self):
2381 self.assertRaises(ValueError, as_object, 'foo')
2382@@ -63,6 +66,9 @@
2383 AttributeError,
2384 as_object('systemimage.tests.test_helpers.NoSuchTest'))
2385
2386+ def test_as_object_not_equal(self):
2387+ self.assertNotEqual(as_object('systemimage.bag.Bag'), object())
2388+
2389 def test_as_timedelta_seconds(self):
2390 self.assertEqual(as_timedelta('2s'), timedelta(seconds=2))
2391
2392@@ -75,15 +81,34 @@
2393 def test_as_timedelta_unknown(self):
2394 self.assertRaises(ValueError, as_timedelta, '3x')
2395
2396+ def test_as_timedelta_no_keywords(self):
2397+ self.assertRaises(ValueError, as_timedelta, '')
2398+
2399+ def test_as_timedelta_repeated_interval(self):
2400+ self.assertRaises(ValueError, as_timedelta, '2s2s')
2401+
2402+ def test_as_timedelta_float(self):
2403+ self.assertEqual(as_timedelta('0.5d'), timedelta(hours=12))
2404+
2405 def test_as_loglevel(self):
2406- self.assertEqual(as_loglevel('error'), logging.ERROR)
2407+ # The default D-Bus log level is ERROR.
2408+ self.assertEqual(as_loglevel('critical'),
2409+ (logging.CRITICAL, logging.ERROR))
2410
2411 def test_as_loglevel_uppercase(self):
2412- self.assertEqual(as_loglevel('ERROR'), logging.ERROR)
2413+ self.assertEqual(as_loglevel('CRITICAL'),
2414+ (logging.CRITICAL, logging.ERROR))
2415+
2416+ def test_as_dbus_loglevel(self):
2417+ self.assertEqual(as_loglevel('error:info'),
2418+ (logging.ERROR, logging.INFO))
2419
2420 def test_as_loglevel_unknown(self):
2421 self.assertRaises(ValueError, as_loglevel, 'BADNESS')
2422
2423+ def test_as_bad_dbus_loglevel(self):
2424+ self.assertRaises(ValueError, as_loglevel, 'error:basicConfig')
2425+
2426
2427 class TestLastUpdateDate(unittest.TestCase):
2428 @configuration
2429@@ -184,6 +209,9 @@
2430 self.assertEqual(version_detail('ubuntu=123,mako=456,custom=789'),
2431 dict(ubuntu='123', mako='456', custom='789'))
2432
2433+ def test_no_version_in_version_detail(self):
2434+ self.assertEqual(version_detail('ubuntu,mako,custom'), {})
2435+
2436 @configuration
2437 def test_date_from_userdata_ignoring_fallbacks(self, ini_file):
2438 # Even when /etc/system-image/channel.ini and /etc/ubuntu-build exist,
2439@@ -212,6 +240,38 @@
2440 # Run the test.
2441 self.assertEqual(last_update_date(), '2010-09-08 07:06:05')
2442
2443+ @configuration
2444+ def test_last_date_no_permission(self, ini_file):
2445+ # LP: #1365761 reports a problem where stat'ing /userdata/.last_update
2446+ # results in a PermissionError. In that case it should just use a
2447+ # fall back, in this case the channel.ini file.
2448+ channel_ini = os.path.join(
2449+ os.path.dirname(ini_file), 'channel.ini')
2450+ with open(channel_ini, 'w', encoding='utf-8'):
2451+ pass
2452+ # This creates the ubuntu-build file, but not the channel.ini file.
2453+ timestamp_1 = int(datetime(2022, 1, 2, 3, 4, 5).timestamp())
2454+ touch_build(2, timestamp_1)
2455+ # Now, the channel.ini file.
2456+ timestamp_2 = int(datetime(2022, 3, 4, 5, 6, 7).timestamp())
2457+ os.utime(channel_ini, (timestamp_2, timestamp_2))
2458+ # Now create an stat'able /userdata/.last_update file.
2459+ with ExitStack() as stack:
2460+ tmpdir = stack.enter_context(temporary_directory())
2461+ userdata_path = os.path.join(tmpdir, '.last_update')
2462+ stack.enter_context(patch('systemimage.helpers.LAST_UPDATE_FILE',
2463+ userdata_path))
2464+ timestamp = int(datetime(2012, 11, 10, 9, 8, 7).timestamp())
2465+ with open(userdata_path, 'w'):
2466+ # i.e. touch(1)
2467+ pass
2468+ os.utime(userdata_path, (timestamp, timestamp))
2469+ # Make the file unreadable.
2470+ stack.callback(os.chmod, tmpdir, 0o777)
2471+ os.chmod(tmpdir, 0o000)
2472+ # The last update date will be the date of the channel.ini file.
2473+ self.assertEqual(last_update_date(), '2022-03-04 05:06:07')
2474+
2475
2476 class TestPhasedPercentage(unittest.TestCase):
2477 def setUp(self):
2478@@ -295,3 +355,11 @@
2479 fp.seek(0)
2480 hash2 = hashlib.sha256(fp.read()).hexdigest()
2481 self.assertEqual(hash1, hash2)
2482+
2483+
2484+class TestMiscellaneous(unittest.TestCase):
2485+ def test_temporary_directory_finally_test_coverage(self):
2486+ with temporary_directory() as path:
2487+ shutil.rmtree(path)
2488+ self.assertFalse(os.path.exists(path))
2489+ self.assertFalse(os.path.exists(path))
2490
2491=== modified file 'systemimage/tests/test_main.py'
2492--- systemimage/tests/test_main.py 2014-07-23 22:51:19 +0000
2493+++ systemimage/tests/test_main.py 2014-09-17 13:42:00 +0000
2494@@ -694,6 +694,33 @@
2495 tubular
2496 """))
2497
2498+ @configuration
2499+ def test_list_channels_exception(self, ini_file):
2500+ # If an exception occurs while getting the list of channels, we get a
2501+ # non-zero exit status.
2502+ self._setup_server_keyrings()
2503+ channel_ini = os.path.join(os.path.dirname(ini_file), 'channel.ini')
2504+ head, tail = os.path.split(channel_ini)
2505+ copy('channel_05.ini', head, tail)
2506+ capture = StringIO()
2507+ self._resources.enter_context(
2508+ patch('builtins.print', partial(print, file=capture)))
2509+ self._resources.enter_context(
2510+ patch('systemimage.main.sys.argv',
2511+ ['argv0', '-C', ini_file, '--list-channels']))
2512+ # Do not use self._resources to manage the check_output mock. Because
2513+ # of the nesting order of the @configuration decorator and the base
2514+ # class's tearDown(), using self._resources causes the mocks to be
2515+ # unwound in the wrong order, affecting future tests.
2516+ with ExitStack() as more:
2517+ more.enter_context(
2518+ patch('systemimage.device.check_output', return_value='manta'))
2519+ more.enter_context(
2520+ patch('systemimage.state.State._get_channel',
2521+ side_effect=RuntimeError))
2522+ status = cli_main()
2523+ self.assertEqual(status, 1)
2524+
2525
2526 class TestCLIFilters(ServerTestBase):
2527 INDEX_FILE = 'index_15.json'
2528@@ -1268,7 +1295,10 @@
2529 # Attempt to start a second process on the same system bus.
2530 env = dict(
2531 DBUS_SYSTEM_BUS_ADDRESS=os.environ['DBUS_SYSTEM_BUS_ADDRESS'])
2532- args = (sys.executable, '-m', 'systemimage.service',
2533+ coverage_env = os.environ.get('COVERAGE_PROCESS_START')
2534+ if coverage_env is not None:
2535+ env['COVERAGE_PROCESS_START'] = coverage_env
2536+ args = (sys.executable, '-m', 'systemimage.testing.service',
2537 '-C', self.ini_path)
2538 second = subprocess.Popen(args, universal_newlines=True, env=env)
2539 # Allow a TimeoutExpired exception to fail the test.
2540@@ -1278,5 +1308,5 @@
2541 second.kill()
2542 second.communicate()
2543 raise
2544- self.assertNotEqual(second.pid, proc)
2545+ self.assertNotEqual(second.pid, proc.pid)
2546 self.assertEqual(code, 2)
2547
2548=== modified file 'systemimage/tests/test_state.py'
2549--- systemimage/tests/test_state.py 2014-07-23 22:51:19 +0000
2550+++ systemimage/tests/test_state.py 2014-09-17 13:42:00 +0000
2551@@ -23,6 +23,7 @@
2552 'TestDailyProposed',
2553 'TestFileOrder',
2554 'TestKeyringDoubleChecks',
2555+ 'TestMiscellaneous',
2556 'TestPhasedUpdates',
2557 'TestRebooting',
2558 'TestState',
2559@@ -43,7 +44,8 @@
2560 from systemimage.config import config
2561 from systemimage.download import DuplicateDestinationError
2562 from systemimage.gpg import Context, SignatureError
2563-from systemimage.state import State
2564+from systemimage.helpers import calculate_signature
2565+from systemimage.state import ChecksumError, State
2566 from systemimage.testing.demo import DemoDevice
2567 from systemimage.testing.helpers import (
2568 ServerTestBase, configuration, copy, data_path, get_index,
2569@@ -52,7 +54,9 @@
2570 from systemimage.testing.nose import SystemImagePlugin
2571 # FIXME
2572 from systemimage.tests.test_candidates import _descriptions
2573-from unittest.mock import patch
2574+from unittest.mock import call, patch
2575+
2576+BAD_SIGNATURE = 'f' * 64
2577
2578
2579 class TestState(unittest.TestCase):
2580@@ -1549,3 +1553,79 @@
2581 ['http://localhost:8980/3/4/5.txt.asc',
2582 'http://localhost:8980/5/6/5.txt.asc',
2583 ])
2584+
2585+
2586+class TestMiscellaneous(ServerTestBase):
2587+ """Test a few additional things for full code coverage."""
2588+
2589+ INDEX_FILE = 'index_13.json'
2590+ CHANNEL_FILE = 'channels_06.json'
2591+ CHANNEL = 'stable'
2592+ DEVICE = 'nexus7'
2593+
2594+ @configuration
2595+ def test_checksum_error(self):
2596+ # _download_files() verifies the checksums of all the downloaded
2597+ # files. If any of them fail, you get an exception.
2598+ self._setup_server_keyrings()
2599+ state = State()
2600+ state.run_until('download_files')
2601+ # It's tricky to cause a checksum error. We can't corrupt the local
2602+ # downloaded copy of the data file because _download_files() doesn't
2603+ # give us a good hook into the post-download, pre-checksum logic. We
2604+ # can't corrupt the server file because the lower-level downloading
2605+ # logic will complain. Instead, we mock the calculate_signature()
2606+ # function to produce a broken checksum for one of the files.
2607+ real_signature = None
2608+ def broken_calc(fp, hash_class=None):
2609+ nonlocal real_signature
2610+ signature = calculate_signature(fp, hash_class)
2611+ if os.path.basename(fp.name) == '6.txt':
2612+ real_signature = signature
2613+ return BAD_SIGNATURE
2614+ return signature
2615+ with patch('systemimage.state.calculate_signature', broken_calc):
2616+ with self.assertRaises(ChecksumError) as cm:
2617+ state.run_thru('download_files')
2618+ self.assertEqual(os.path.basename(cm.exception.destination), '6.txt')
2619+ self.assertEqual(cm.exception.got, BAD_SIGNATURE)
2620+ self.assertIsNotNone(real_signature)
2621+ self.assertEqual(cm.exception.expected, real_signature)
2622+
2623+ @configuration
2624+ def test_get_blacklist_2_finds_no_blacklist(self):
2625+ # Getting the blacklist can fail even the second time. That's fine,
2626+ # but output gets logged.
2627+ self._setup_server_keyrings()
2628+ state = State()
2629+ # we want get_blacklist_1 to fail with a SignatureError so that it
2630+ # will try to get the master key and then attempt a refetch of the
2631+ # blacklist. Let's just corrupt the original blacklist file.
2632+ blacklist = os.path.join(self._serverdir, 'gpg', 'blacklist.tar.xz')
2633+ with open(blacklist, 'ba+') as fp:
2634+ fp.write(b'x')
2635+ state.run_until('get_blacklist_2')
2636+ # Now we delete the blacklist file from the server, so as to trigger
2637+ # the expected log message.
2638+ os.remove(blacklist)
2639+ with patch('systemimage.state.log.info') as capture:
2640+ state.run_thru('get_blacklist_2')
2641+ self.assertEqual(capture.call_args,
2642+ call('No blacklist found on second attempt'))
2643+ # Even though there's no blacklist file, everything still gets
2644+ # downloaded correctly.
2645+ state.run_until('reboot')
2646+ path = os.path.join(config.updater.cache_partition, 'ubuntu_command')
2647+ with open(path, 'r', encoding='utf-8') as fp:
2648+ command = fp.read()
2649+ self.assertMultiLineEqual(command, """\
2650+load_keyring image-master.tar.xz image-master.tar.xz.asc
2651+load_keyring image-signing.tar.xz image-signing.tar.xz.asc
2652+load_keyring device-signing.tar.xz device-signing.tar.xz.asc
2653+format system
2654+mount system
2655+update 6.txt 6.txt.asc
2656+update 7.txt 7.txt.asc
2657+update 5.txt 5.txt.asc
2658+unmount system
2659+""")
2660
2661=== modified file 'systemimage/version.txt'
2662--- systemimage/version.txt 2014-07-31 23:20:10 +0000
2663+++ systemimage/version.txt 2014-09-17 13:42:00 +0000
2664@@ -1,1 +1,1 @@
2665-2.3.2
2666+2.4
2667
2668=== modified file 'tox.ini'
2669--- tox.ini 2014-07-23 22:51:19 +0000
2670+++ tox.ini 2014-09-17 13:42:00 +0000
2671@@ -1,8 +1,22 @@
2672 [tox]
2673 envlist = py34
2674+recreate=True
2675
2676 [testenv]
2677 commands = python -m nose2 -v
2678-sitepackages=True
2679-setenv=
2680+sitepackages = True
2681+setenv =
2682 SYSTEMIMAGE_REACTOR_TIMEOUT=60
2683+
2684+[testenv:coverage]
2685+basepython = python3
2686+commands =
2687+ python /usr/bin/python3-coverage run --rcfile={toxinidir}/coverage.ini -m nose2 -v
2688+ python3-coverage combine --rcfile={toxinidir}/coverage.ini
2689+ python3-coverage html --rcfile={toxinidir}/coverage.ini
2690+sitepackages = True
2691+setenv =
2692+ SYSTEMIMAGE_REACTOR_TIMEOUT=120
2693+ COVERAGE_PROCESS_START={toxinidir}/coverage.ini
2694+ COVERAGE_OPTIONS="-p"
2695+ COVERAGE_FILE={toxinidir}/.coverage
2696
2697=== modified file 'unittest.cfg'
2698--- unittest.cfg 2014-07-23 22:51:19 +0000
2699+++ unittest.cfg 2014-09-17 13:42:00 +0000
2700@@ -1,6 +1,7 @@
2701 [unittest]
2702 verbose = 2
2703-plugins = systemimage.testing.nose
2704+plugins =
2705+ systemimage.testing.nose
2706
2707 [systemimage]
2708 always-on = True

Subscribers

People subscribed via source and target branches