Merge lp:~ubuntu-managed-branches/ubuntu-system-image/251-0u1 into lp:~ubuntu-managed-branches/ubuntu-system-image/system-image

Proposed by Barry Warsaw
Status: Needs review
Proposed branch: lp:~ubuntu-managed-branches/ubuntu-system-image/251-0u1
Merge into: lp:~ubuntu-managed-branches/ubuntu-system-image/system-image
Diff against target: 2373 lines (+960/-412)
34 files modified
MANIFEST.in (+1/-1)
NEWS.rst (+15/-1)
PKG-INFO (+1/-1)
cli-manpage.rst (+7/-2)
coverage.ini (+2/-0)
dbus-manpage.rst (+2/-2)
debian/README.Debian (+13/-0)
debian/changelog (+20/-0)
ini-manpage.rst (+2/-2)
system_image.egg-info/PKG-INFO (+1/-1)
system_image.egg-info/SOURCES.txt (+3/-1)
systemimage/config.py (+15/-0)
systemimage/download.py (+12/-4)
systemimage/helpers.py (+11/-18)
systemimage/index.py (+1/-4)
systemimage/main.py (+12/-1)
systemimage/scores.py (+29/-13)
systemimage/state.py (+4/-1)
systemimage/testing/controller.py (+7/-5)
systemimage/testing/helpers.py (+10/-0)
systemimage/testing/service.py (+8/-0)
systemimage/tests/data/index_22.json (+2/-3)
systemimage/tests/data/index_26.json (+245/-0)
systemimage/tests/test_candidates.py (+16/-24)
systemimage/tests/test_config.py (+35/-0)
systemimage/tests/test_helpers.py (+51/-43)
systemimage/tests/test_index.py (+0/-42)
systemimage/tests/test_main.py (+209/-195)
systemimage/tests/test_scores.py (+83/-10)
systemimage/tests/test_state.py (+130/-36)
systemimage/tests/test_winner.py (+1/-1)
systemimage/version.txt (+1/-1)
tools/runme.sh (+10/-0)
tox.ini (+1/-0)
To merge this branch: bzr merge lp:~ubuntu-managed-branches/ubuntu-system-image/251-0u1
Reviewer Review Type Date Requested Status
Ubuntu CI managed package branches Pending
Review via email: mp+240034@code.launchpad.net

Commit message

LP: #1383539 - fixes to phased updates for rtm.

Description of the change

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

  * New upstream release.
    - LP: #1383539 - Make phased upgrade percentage calculation idempotent
      for each tuple of (channel, target-build-number, machine-id). Also,
      modify the candidate upgrade path selection process such that if the
      lowest scored candidate path has a phased percentage greater than the
      device's percentage, the candidate will be ignored, and the next
      lowest scored candidate will be checked until either a winner is found
      or no candidates are left, in which case the device is deemed to be
      up-to-date.
    - system-image-cli options -p/--percentage were added to allow command
      line override of the device's phased percentage.
    - system-image-cli --dry-run now also displays the phase percentage of the
      winning candidate upgrade path.

 -- Barry Warsaw <email address hidden> Wed, 29 Oct 2014 14:31:18 -0400

To post a comment you must log in.
241. By Barry Warsaw

* New upstream release.
  - LP: #1383539 - Make phased upgrade percentage calculation idempotent
    for each tuple of (channel, target-build-number, machine-id). Also,
    modify the candidate upgrade path selection process such that if the
    lowest scored candidate path has a phased percentage greater than the
    device's percentage, the candidate will be ignored, and the next
    lowest scored candidate will be checked until either a winner is found
    or no candidates are left, in which case the device is deemed to be
    up-to-date.
  - system-image-cli options -p/--percentage were added to allow command
    line override of the device's phased percentage.
  - system-image-cli --dry-run now also displays the phase percentage of the
    winning candidate upgrade path.
* debian/README.Debian: Added to explain how to build the source package
  if you get hit by a stupid setuptools bug.

Unmerged revisions

241. By Barry Warsaw

* New upstream release.
  - LP: #1383539 - Make phased upgrade percentage calculation idempotent
    for each tuple of (channel, target-build-number, machine-id). Also,
    modify the candidate upgrade path selection process such that if the
    lowest scored candidate path has a phased percentage greater than the
    device's percentage, the candidate will be ignored, and the next
    lowest scored candidate will be checked until either a winner is found
    or no candidates are left, in which case the device is deemed to be
    up-to-date.
  - system-image-cli options -p/--percentage were added to allow command
    line override of the device's phased percentage.
  - system-image-cli --dry-run now also displays the phase percentage of the
    winning candidate upgrade path.
* debian/README.Debian: Added to explain how to build the source package
  if you get hit by a stupid setuptools bug.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'MANIFEST.in'
2--- MANIFEST.in 2014-01-30 16:56:57 +0000
3+++ MANIFEST.in 2014-10-29 20:07:23 +0000
4@@ -1,5 +1,5 @@
5 include *.py MANIFEST.in
6-global-include *.txt *.rst *.json *.ini *.gpg *.pem *.service *.in *.conf *.cfg
7+global-include *.txt *.rst *.json *.ini *.gpg *.pem *.service *.in *.conf *.cfg *.sh
8 prune build
9 prune dist
10 prune .tox
11
12=== modified file 'NEWS.rst'
13--- NEWS.rst 2014-09-26 14:36:34 +0000
14+++ NEWS.rst 2014-10-29 20:07:23 +0000
15@@ -2,7 +2,21 @@
16 NEWS for system-image updater
17 =============================
18
19-2.5 (2014-XX-XX)
20+2.5.1 (2014-10-21)
21+==================
22+ * Make phased upgrade percentage calculation idempotent for each tuple of
23+ (channel, target-build-number, machine-id). Also, modify the candidate
24+ upgrade path selection process such that if the lowest scored candidate
25+ path has a phased percentage greater than the device's percentage, the
26+ candidate will be ignored, and the next lowest scored candidate will be
27+ checked until either a winner is found or no candidates are left, in which
28+ case the device is deemed to be up-to-date. (LP: #1383539)
29+ * `system-image-cli -p/--percentage` is added to allow command line override
30+ of the device's phased percentage.
31+ * `system-image-cli --dry-run` now also displays the phase percentage of the
32+ winning candidate upgrade path.
33+
34+2.5 (2014-09-29)
35 ================
36 * Remove the previously deprecated `system-image-cli --dbus` command line
37 switch. (LP: #1369717)
38
39=== modified file 'PKG-INFO'
40--- PKG-INFO 2014-09-26 14:36:34 +0000
41+++ PKG-INFO 2014-10-29 20:07:23 +0000
42@@ -1,6 +1,6 @@
43 Metadata-Version: 1.0
44 Name: system-image
45-Version: 2.5
46+Version: 2.5.1
47 Summary: Ubuntu System Image Based Upgrades
48 Home-page: UNKNOWN
49 Author: Barry Warsaw
50
51=== modified file 'cli-manpage.rst'
52--- cli-manpage.rst 2014-09-17 13:41:31 +0000
53+++ cli-manpage.rst 2014-10-29 20:07:23 +0000
54@@ -7,9 +7,9 @@
55 ------------------------------------------------
56
57 :Author: Barry Warsaw <barry@ubuntu.com>
58-:Date: 2014-09-16
59+:Date: 2014-10-23
60 :Copyright: 2013-2014 Canonical Ltd.
61-:Version: 2.4
62+:Version: 2.5.1
63 :Manual section: 1
64
65
66@@ -68,6 +68,11 @@
67
68 -n, --dry-run
69 Calculate and print the upgrade path, but do not download or apply it.
70+ *New in system-image 2.5.1: output displays the target phase percentage*
71+
72+-p VALUE, --percentage VALUE
73+ For testing purposes, force a device specific phase percentage. The value
74+ must be an integer between 0 and 100. *New in system-image 2.5.1*
75
76 --no-reboot
77 Downloads all files and prepares for a reboot into recovery, but doesn't
78
79=== modified file 'coverage.ini'
80--- coverage.ini 2014-09-17 02:58:58 +0000
81+++ coverage.ini 2014-10-29 20:07:23 +0000
82@@ -8,6 +8,8 @@
83 systemimage/testing/*
84 systemimage/tests/*
85 /usr/lib/*
86+ .tox/coverage/lib/python3.4/distutils/*
87+ .tox/coverage/lib/python3.4/site-packages/pkg_resources*
88
89 [paths]
90 source =
91
92=== modified file 'dbus-manpage.rst'
93--- dbus-manpage.rst 2014-09-26 14:36:34 +0000
94+++ dbus-manpage.rst 2014-10-29 20:07:23 +0000
95@@ -7,9 +7,9 @@
96 -----------------------------------------
97
98 :Author: Barry Warsaw <barry@ubuntu.com>
99-:Date: 2014-07-15
100+:Date: 2014-09-29
101 :Copyright: 2013-2014 Canonical Ltd.
102-:Version: 2.3
103+:Version: 2.5
104 :Manual section: 8
105
106
107
108=== added file 'debian/README.Debian'
109--- debian/README.Debian 1970-01-01 00:00:00 +0000
110+++ debian/README.Debian 2014-10-29 20:07:23 +0000
111@@ -0,0 +1,13 @@
112+You might have problems building the source package because of this issue:
113+
114+https://bitbucket.org/pypa/setuptools/issue/280/egg_info-command-must-write-setupcfg
115+
116+If that happens, create the source package with:
117+
118+$ bzr bd -S -- --source-option="--auto-commit"
119+
120+This will include the stupid setup.cfg non-change to be included in a quilt
121+patch so dpkg-source --abort-on-upstream-changes won't fail the source package
122+build. Sigh.
123+
124+ -- Barry Warsaw <barry@ubuntu.com>, Wed, 29 Oct 2014 15:54:20 -0400
125
126=== modified file 'debian/changelog'
127--- debian/changelog 2014-09-29 19:02:48 +0000
128+++ debian/changelog 2014-10-29 20:07:23 +0000
129@@ -1,3 +1,23 @@
130+system-image (2.5.1-0ubuntu1) UNRELEASED; urgency=medium
131+
132+ * New upstream release.
133+ - LP: #1383539 - Make phased upgrade percentage calculation idempotent
134+ for each tuple of (channel, target-build-number, machine-id). Also,
135+ modify the candidate upgrade path selection process such that if the
136+ lowest scored candidate path has a phased percentage greater than the
137+ device's percentage, the candidate will be ignored, and the next
138+ lowest scored candidate will be checked until either a winner is found
139+ or no candidates are left, in which case the device is deemed to be
140+ up-to-date.
141+ - system-image-cli options -p/--percentage were added to allow command
142+ line override of the device's phased percentage.
143+ - system-image-cli --dry-run now also displays the phase percentage of the
144+ winning candidate upgrade path.
145+ * debian/README.Debian: Added to explain how to build the source package
146+ if you get hit by a stupid setuptools bug.
147+
148+ -- Barry Warsaw <barry@ubuntu.com> Wed, 29 Oct 2014 14:40:51 -0400
149+
150 system-image (2.5-0ubuntu1) utopic; urgency=medium
151
152 [ Barry Warsaw ]
153
154=== modified file 'ini-manpage.rst'
155--- ini-manpage.rst 2014-09-17 13:41:31 +0000
156+++ ini-manpage.rst 2014-10-29 20:07:23 +0000
157@@ -8,9 +8,9 @@
158 -----------------------------------------------
159
160 :Author: Barry Warsaw <barry@ubuntu.com>
161-:Date: 2014-09-11
162+:Date: 2014-09-29
163 :Copyright: 2013-2014 Canonical Ltd.
164-:Version: 2.4
165+:Version: 2.5
166 :Manual section: 5
167
168
169
170=== modified file 'system_image.egg-info/PKG-INFO'
171--- system_image.egg-info/PKG-INFO 2014-09-26 14:36:34 +0000
172+++ system_image.egg-info/PKG-INFO 2014-10-29 20:07:23 +0000
173@@ -1,6 +1,6 @@
174 Metadata-Version: 1.0
175 Name: system-image
176-Version: 2.5
177+Version: 2.5.1
178 Summary: Ubuntu System Image Based Upgrades
179 Home-page: UNKNOWN
180 Author: Barry Warsaw
181
182=== modified file 'system_image.egg-info/SOURCES.txt'
183--- system_image.egg-info/SOURCES.txt 2014-09-26 14:36:34 +0000
184+++ system_image.egg-info/SOURCES.txt 2014-10-29 20:07:23 +0000
185@@ -137,10 +137,12 @@
186 systemimage/tests/data/index_23.json
187 systemimage/tests/data/index_24.json
188 systemimage/tests/data/index_25.json
189+systemimage/tests/data/index_26.json
190 systemimage/tests/data/key.pem
191 systemimage/tests/data/master-secring.gpg
192 systemimage/tests/data/nasty_cert.pem
193 systemimage/tests/data/nasty_key.pem
194 systemimage/tests/data/spare.gpg
195 systemimage/tests/data/sprint_nexus7_index_01.json
196-tools/demo.ini
197\ No newline at end of file
198+tools/demo.ini
199+tools/runme.sh
200\ No newline at end of file
201
202=== modified file 'systemimage/config.py'
203--- systemimage/config.py 2014-09-17 13:41:31 +0000
204+++ systemimage/config.py 2014-10-29 20:07:23 +0000
205@@ -75,6 +75,9 @@
206 self._device = None
207 self._build_number = None
208 self._channel = None
209+ # This is used only to override the phased percentage via command line
210+ # and the property setter.
211+ self._phase_override = None
212 self._tempdir = None
213 self._resources = ExitStack()
214 atexit.register(self._resources.close)
215@@ -203,6 +206,18 @@
216 self._channel = value
217
218 @property
219+ def phase_override(self):
220+ return self._phase_override
221+
222+ @phase_override.setter
223+ def phase_override(self, value):
224+ self._phase_override = max(0, min(100, int(value)))
225+
226+ @phase_override.deleter
227+ def phase_override(self):
228+ self._phase_override = None
229+
230+ @property
231 def tempdir(self):
232 if self._tempdir is None:
233 makedirs(self.system.tempdir)
234
235=== modified file 'systemimage/download.py'
236--- systemimage/download.py 2014-09-17 13:41:31 +0000
237+++ systemimage/download.py 2014-10-29 20:07:23 +0000
238@@ -98,6 +98,7 @@
239 self.canceled = False
240 self.received = 0
241 self.total = 0
242+ self.local_paths = None
243 self.react_to('canceled')
244 self.react_to('error')
245 self.react_to('finished')
246@@ -111,6 +112,7 @@
247
248 def _do_finished(self, signal, path, local_paths):
249 _print('FINISHED:', local_paths)
250+ self.local_paths = local_paths
251 self.quit()
252
253 def _do_error(self, signal, path, error_message):
254@@ -292,10 +294,16 @@
255 raise Canceled
256 if reactor.timed_out:
257 raise TimeoutError
258- # For sanity.
259- for record in records:
260- assert os.path.exists(record.destination), (
261- 'Missing destination: {}'.format(record))
262+ # Sanity check the downloaded results.
263+ # First, every requested destination file must exist, otherwise
264+ # udm would not have given us a `finished` signal.
265+ missing = [record.destination for record in records
266+ if not os.path.exists(record.destination)]
267+ if len(missing) > 0: # pragma: no cover
268+ local_paths = sorted(reactor.local_paths)
269+ raise AssertionError(
270+ 'Missing destination files: {}\nlocal_paths: {}'.format(
271+ missing, local_paths))
272
273 @staticmethod
274 def _set_gsm(iface, *, allow_gsm):
275
276=== modified file 'systemimage/helpers.py'
277--- systemimage/helpers.py 2014-09-17 13:41:31 +0000
278+++ systemimage/helpers.py 2014-10-29 20:07:23 +0000
279@@ -34,7 +34,6 @@
280
281 import os
282 import re
283-import time
284 import random
285 import shutil
286 import logging
287@@ -46,8 +45,8 @@
288 from importlib import import_module
289
290
291+UNIQUE_MACHINE_ID_FILE = '/var/lib/dbus/machine-id'
292 LAST_UPDATE_FILE = '/userdata/.last_update'
293-UNIQUE_MACHINE_ID_FILE = '/var/lib/dbus/machine-id'
294 DEFAULT_DIRMODE = 0o02700
295 MiB = 1 << 20
296 EMPTYSTRING = ''
297@@ -270,19 +269,13 @@
298 return details
299
300
301-_pp_cache = None
302-
303-def phased_percentage(*, reset=False):
304- global _pp_cache
305- if _pp_cache is None:
306- with open(UNIQUE_MACHINE_ID_FILE, 'rb') as fp:
307- data = fp.read()
308- now = str(time.time()).encode('us-ascii')
309- r = random.Random()
310- r.seed(data + now)
311- _pp_cache = r.randint(0, 100)
312- try:
313- return _pp_cache
314- finally:
315- if reset:
316- _pp_cache = None
317+def phased_percentage(channel, target):
318+ # Avoid circular imports.
319+ from systemimage.config import config
320+ if config.phase_override is not None:
321+ return config.phase_override
322+ with open(UNIQUE_MACHINE_ID_FILE, 'r', encoding='utf-8') as fp:
323+ machine_id = fp.read().strip()
324+ r = random.Random()
325+ r.seed('{}.{}.{}'.format(channel, target, machine_id))
326+ return r.randint(0, 100)
327
328=== modified file 'systemimage/index.py'
329--- systemimage/index.py 2014-02-20 23:03:24 +0000
330+++ systemimage/index.py 2014-10-29 20:07:23 +0000
331@@ -24,7 +24,6 @@
332
333 from datetime import datetime, timezone
334 from systemimage.bag import Bag
335-from systemimage.helpers import phased_percentage
336 from systemimage.image import Image
337
338
339@@ -49,7 +48,6 @@
340 global_ = Bag(generated_at=generated_at)
341 # Parse the images.
342 images = []
343- percentage = phased_percentage()
344 for image_data in mapping['images']:
345 # Descriptions can be any of:
346 #
347@@ -70,6 +68,5 @@
348 image = Image(files=bundles,
349 descriptions=descriptions,
350 **image_data)
351- if percentage <= image.phased_percentage:
352- images.append(image)
353+ images.append(image)
354 return cls(global_=global_, images=images)
355
356=== modified file 'systemimage/main.py'
357--- systemimage/main.py 2014-09-26 14:36:34 +0000
358+++ systemimage/main.py 2014-10-29 20:07:23 +0000
359@@ -30,7 +30,8 @@
360 from pkg_resources import resource_string as resource_bytes
361 from systemimage.candidates import delta_filter, full_filter
362 from systemimage.config import config
363-from systemimage.helpers import last_update_date, makedirs, version_detail
364+from systemimage.helpers import (
365+ last_update_date, makedirs, phased_percentage, version_detail)
366 from systemimage.logging import initialize
367 from systemimage.reboot import factory_reset
368 from systemimage.settings import Settings
369@@ -91,6 +92,10 @@
370 default=False, action='store_true',
371 help="""Calculate and print the upgrade path, but do
372 not download or apply it""")
373+ parser.add_argument('-p', '--percentage',
374+ default=None, action='store',
375+ help="""Override the device's phased percentage value
376+ during upgrade candidate calculation.""")
377 parser.add_argument('-v', '--verbose',
378 default=0, action='count',
379 help='Increase verbosity')
380@@ -213,6 +218,8 @@
381 config.channel = args.channel
382 if args.device is not None:
383 config.device = args.device
384+ if args.percentage is not None:
385+ config.phase_override = args.percentage
386
387 if args.info:
388 alias = getattr(config.service, 'channel_target', None)
389@@ -296,15 +303,19 @@
390 else:
391 winning_path = [str(image.version) for image in state.winner]
392 kws = dict(path=COLON.join(winning_path))
393+ target_build = state.winner[-1].version
394 if state.channel_switch is None:
395 # We're not switching channels due to an alias change.
396 template = 'Upgrade path is {path}'
397+ percentage = phased_percentage(config.channel, target_build)
398 else:
399 # This upgrade changes the channel that our alias is mapped
400 # to, so include that information in the output.
401 template = 'Upgrade path is {path} ({from} -> {to})'
402 kws['from'], kws['to'] = state.channel_switch
403+ percentage = phased_percentage(kws['to'], target_build)
404 print(template.format(**kws))
405+ print('Target phase: {}%'.format(percentage))
406 return
407 else:
408 # Run the state machine to conclusion. Suppress all exceptions, but
409
410=== modified file 'systemimage/scores.py'
411--- systemimage/scores.py 2014-09-17 13:41:31 +0000
412+++ systemimage/scores.py 2014-10-29 20:07:23 +0000
413@@ -26,9 +26,8 @@
414
415 import logging
416
417-from io import StringIO
418 from itertools import count
419-from systemimage.helpers import MiB
420+from systemimage.helpers import MiB, phased_percentage
421
422 log = logging.getLogger('systemimage')
423
424@@ -38,7 +37,7 @@
425 class Scorer:
426 """Abstract base class providing an API for candidate selection."""
427
428- def choose(self, candidates):
429+ def choose(self, candidates, channel):
430 """Choose the candidate upgrade paths.
431
432 Lowest score wins.
433@@ -47,6 +46,9 @@
434 the device from the current version to the latest version, sorted
435 in order from oldest verson to newest.
436 :type candidates: list of lists
437+ :param channel: The channel being upgraded to. This is used in the
438+ phased update calculate.
439+ :type channel: str
440 :return: The chosen path.
441 :rtype: list
442 """
443@@ -68,17 +70,31 @@
444 # Be sure that after all is said and done we return the list of Images
445 # though!
446 scores = sorted(zip(self.score(candidates), count(), candidates))
447- fp = StringIO()
448- print('{} path scores (last one wins):'.format(
449- self.__class__.__name__),
450- file=fp)
451- for score, i, candidate in reversed(scores):
452- print('\t[{:4d}] -> {}'.format(
453+ # Calculate the phase percentage for the device. Use the highest
454+ # available build number as input into the random seed.
455+ max_target_number = -1
456+ for score, i, path in scores:
457+ # The last image will be the target image.
458+ assert len(path) > 0, 'Empty upgrade candidate path?'
459+ max_target_number = max(max_target_number, path[-1].version)
460+ assert max_target_number != -1, 'No max target version?'
461+ device_percentage = phased_percentage(channel, max_target_number)
462+ log.debug('Device phased percentage: {}%'.format(device_percentage))
463+ log.debug('{} path scores:'.format(self.__class__.__name__))
464+ # Log the candidate paths, their scores, and their phases.
465+ for score, i, path in reversed(scores):
466+ log.debug('\t[{:4d}] -> {} ({}%)'.format(
467 score,
468- COLON.join(str(image.version) for image in candidate)),
469- file=fp)
470- log.debug('{}'.format(fp.getvalue()))
471- return scores[0][2]
472+ COLON.join(str(image.version) for image in path),
473+ (path[-1].phased_percentage if len(path) > 0 else '--')
474+ ))
475+ for score, i, path in scores:
476+ image_percentage = path[-1].phased_percentage
477+ # An image percentage of 0 means that it's been pulled.
478+ if image_percentage > 0 and device_percentage <= image_percentage:
479+ return path
480+ # No upgrade path.
481+ return []
482
483 def score(self, candidates): # pragma: no cover
484 """Like `choose()` except returns the candidate path scores.
485
486=== modified file 'systemimage/state.py'
487--- systemimage/state.py 2014-09-17 13:41:31 +0000
488+++ systemimage/state.py 2014-10-29 20:07:23 +0000
489@@ -428,7 +428,10 @@
490 candidates = get_candidates(self.index, build_number)
491 if self._filter is not None:
492 candidates = self._filter(candidates)
493- self.winner = config.hooks.scorer().choose(candidates)
494+ self.winner = config.hooks.scorer().choose(
495+ candidates, (channel_target
496+ if channel_alias is None
497+ else channel_alias))
498 # If there is no winning upgrade candidate, then there's nothing more
499 # to do. We can skip everything between downloading the files and
500 # doing the reboot.
501
502=== modified file 'systemimage/testing/controller.py'
503--- systemimage/testing/controller.py 2014-09-17 13:41:31 +0000
504+++ systemimage/testing/controller.py 2014-10-29 20:07:23 +0000
505@@ -41,6 +41,13 @@
506 OVERRIDE = os.environ.get('SYSTEMIMAGE_DBUS_DAEMON_HUP_SLEEP_SECONDS')
507 HUP_SLEEP = (0 if OVERRIDE is None else int(OVERRIDE))
508
509+DLSERVICE = os.environ.get(
510+ 'SYSTEMIMAGE_DLSERVICE',
511+ '/usr/bin/ubuntu-download-manager'
512+ # For debugging the in-tree version of u-d-m.
513+ #'/bin/sh $HOME/projects/phone/runme.sh'
514+ )
515+
516
517 def start_system_image(controller):
518 bus = dbus.SystemBus()
519@@ -105,11 +112,6 @@
520 process.wait(60)
521
522
523-DLSERVICE = '/usr/bin/ubuntu-download-manager'
524-# For debugging the in-tree version of u-d-m.
525-#DLSERVICE = '/bin/sh /home/barry/projects/phone/runme'
526-
527-
528 SERVICES = [
529 ('com.canonical.SystemImage',
530 '{python} -m {self.MODULE} -C {self.ini_path} --testing {self.mode}',
531
532=== modified file 'systemimage/testing/helpers.py'
533--- systemimage/testing/helpers.py 2014-09-17 13:41:31 +0000
534+++ systemimage/testing/helpers.py 2014-10-29 20:07:23 +0000
535@@ -23,6 +23,7 @@
536 'data_path',
537 'debug',
538 'debuggable',
539+ 'descriptions',
540 'find_dbus_process',
541 'get_channels',
542 'get_index',
543@@ -544,3 +545,12 @@
544 dict(type='device-signing'),
545 os.path.join(self._serverdir, self.CHANNEL, self.DEVICE,
546 'device-signing.tar.xz'))
547+
548+
549+def descriptions(path):
550+ descriptions = []
551+ for image in path:
552+ # There's only one description per image so order doesn't
553+ # matter.
554+ descriptions.extend(image.descriptions.values())
555+ return descriptions
556
557=== modified file 'systemimage/testing/service.py'
558--- systemimage/testing/service.py 2014-09-17 02:58:58 +0000
559+++ systemimage/testing/service.py 2014-10-29 20:07:23 +0000
560@@ -21,6 +21,14 @@
561
562 import os
563
564+# Set this environment variable if the controller won't start. There's no
565+# other good way to get debugging information about the D-Bus activated
566+# process, since their stderr just seems to get lost.
567+if os.environ.get('SYSTEMIMAGE_DEBUG_DBUS_ACTIVATION'):
568+ import sys
569+ sys.stderr = open('/tmp/debug.log', 'a', encoding='utf-8')
570+
571+
572 # It's okay if this module isn't available.
573 try:
574 from coverage.control import coverage as _Coverage
575
576=== modified file 'systemimage/tests/data/index_22.json'
577--- systemimage/tests/data/index_22.json 2014-01-30 15:41:03 +0000
578+++ systemimage/tests/data/index_22.json 2014-10-29 20:07:23 +0000
579@@ -120,7 +120,6 @@
580 }
581 ],
582 "type": "full",
583- "phased-percentage": 50,
584 "version": 200
585 },
586 {
587@@ -180,7 +179,8 @@
588 }
589 ],
590 "type": "delta",
591- "version": 304
592+ "version": 304,
593+ "phased-percentage": 50
594 },
595
596 {
597@@ -209,7 +209,6 @@
598 }
599 ],
600 "type": "full",
601- "phased-percentage": 75,
602 "version": 100
603 },
604 {
605
606=== added file 'systemimage/tests/data/index_26.json'
607--- systemimage/tests/data/index_26.json 1970-01-01 00:00:00 +0000
608+++ systemimage/tests/data/index_26.json 2014-10-29 20:07:23 +0000
609@@ -0,0 +1,245 @@
610+{
611+ "global": {
612+ "generated_at": "Mon Apr 29 18:45:27 UTC 2013"
613+ },
614+ "images": [
615+ {
616+ "bootme": true,
617+ "description": "Full A",
618+ "files": [
619+ {
620+ "checksum": "abc",
621+ "order": 1,
622+ "path": "/a/b/c.txt",
623+ "signature": "/a/b/c.txt.asc",
624+ "size": 104857600
625+
626+ },
627+ {
628+ "checksum": "bcd",
629+ "order": 1,
630+ "path": "/b/c/d.txt",
631+ "signature": "/b/c/d.txt.asc",
632+ "size": 104857600
633+ },
634+ {
635+ "checksum": "cde",
636+ "order": 1,
637+ "path": "/c/d/e.txt",
638+ "signature": "/c/d/e.txt.asc",
639+ "size": 104857600
640+ }
641+ ],
642+ "type": "full",
643+ "version": 300
644+ },
645+ {
646+ "base": 300,
647+ "bootme": true,
648+ "description": "Delta A.1",
649+ "files": [
650+ {
651+ "checksum": "def",
652+ "order": 1,
653+ "path": "/d/e/f.txt",
654+ "signature": "/d/e/f.txt.asc",
655+ "size": 104857600
656+ },
657+ {
658+ "checksum": "ef0",
659+ "order": 1,
660+ "path": "/e/f/0.txt",
661+ "signature": "/e/f/0.txt.asc",
662+ "size": 104857600
663+ },
664+ {
665+ "checksum": "f01",
666+ "order": 1,
667+ "path": "/f/e/1.txt",
668+ "signature": "/f/e/1.txt.asc",
669+ "size": 104857600
670+ }
671+ ],
672+ "type": "delta",
673+ "version": 301
674+ },
675+ {
676+ "base": 301,
677+ "bootme": true,
678+ "description": "Delta A.2",
679+ "files": [
680+ {
681+ "checksum": "012",
682+ "order": 1,
683+ "path": "/0/1/2.txt",
684+ "signature": "/0/1/2.txt.asc",
685+ "size": 104857600
686+ },
687+ {
688+ "checksum": "123",
689+ "order": 1,
690+ "path": "/1/2/3.txt",
691+ "signature": "/1/2/3.txt.asc",
692+ "size": 104857600
693+ },
694+ {
695+ "checksum": "234",
696+ "order": 1,
697+ "path": "/2/3/4.txt",
698+ "signature": "/2/3/4.txt.asc",
699+ "size": 104857600
700+ }
701+ ],
702+ "type": "delta",
703+ "version": 304
704+ },
705+
706+ {
707+ "description": "Full B",
708+ "files": [
709+ {
710+ "checksum": "345",
711+ "order": 1,
712+ "path": "/3/4/5.txt",
713+ "signature": "/3/4/5.txt.asc",
714+ "size": 104857600
715+ },
716+ {
717+ "checksum": "456",
718+ "order": 1,
719+ "path": "/4/5/6.txt",
720+ "signature": "/4/5/6.txt.asc",
721+ "size": 104857600
722+ },
723+ {
724+ "checksum": "567",
725+ "order": 1,
726+ "path": "/5/6/7.txt",
727+ "signature": "/5/6/7.txt.asc",
728+ "size": 104857600
729+ }
730+ ],
731+ "type": "full",
732+ "version": 200
733+ },
734+ {
735+ "base": 200,
736+ "description": "Delta B.1",
737+ "files": [
738+ {
739+ "checksum": "678",
740+ "order": 1,
741+ "path": "/6/7/8.txt",
742+ "signature": "/6/7/8.txt.asc",
743+ "size": 104857600
744+ },
745+ {
746+ "checksum": "789",
747+ "order": 1,
748+ "path": "/7/8/9.txt",
749+ "signature": "/7/8/9.txt.asc",
750+ "size": 104857600
751+ },
752+ {
753+ "checksum": "89a",
754+ "order": 1,
755+ "path": "/8/9/a.txt",
756+ "signature": "/8/9/a.txt.asc",
757+ "size": 104857600
758+ }
759+ ],
760+ "type": "delta",
761+ "version": 201
762+ },
763+ {
764+ "base": 201,
765+ "description": "Delta B.2",
766+ "files": [
767+ {
768+ "checksum": "9ab",
769+ "order": 1,
770+ "path": "/9/a/b.txt",
771+ "signature": "/9/a/b.txt.asc",
772+ "size": 104857600
773+ },
774+ {
775+ "checksum": "fed",
776+ "order": 1,
777+ "path": "/f/e/d.txt",
778+ "signature": "/f/e/d.txt.asc",
779+ "size": 104857600
780+ },
781+ {
782+ "checksum": "edc",
783+ "order": 1,
784+ "path": "/e/d/c.txt",
785+ "signature": "/e/d/c.txt.asc",
786+ "size": 209715200
787+
788+ }
789+ ],
790+ "type": "delta",
791+ "version": 304,
792+ "phased-percentage": 0
793+ },
794+
795+ {
796+ "description": "Full C",
797+ "files": [
798+ {
799+ "checksum": "dcb",
800+ "order": 1,
801+ "path": "/d/c/b.txt",
802+ "signature": "/d/c/b.txt.asc",
803+ "size": 104857600
804+ },
805+ {
806+ "checksum": "cba",
807+ "order": 1,
808+ "path": "/c/b/a.txt",
809+ "signature": "/c/b/a.txt.asc",
810+ "size": 104857600
811+ },
812+ {
813+ "checksum": "ba9",
814+ "order": 1,
815+ "path": "/b/a/9.txt",
816+ "signature": "/b/a/9.txt.asc",
817+ "size": 104857600
818+ }
819+ ],
820+ "type": "full",
821+ "version": 100
822+ },
823+ {
824+ "base": 100,
825+ "description": "Delta C.1",
826+ "files": [
827+ {
828+ "checksum": "a98",
829+ "order": 1,
830+ "path": "/a/9/8.txt",
831+ "signature": "/a/9/8.txt.asc",
832+ "size": 104857600
833+ },
834+ {
835+ "checksum": "987",
836+ "order": 1,
837+ "path": "/9/8/7.txt",
838+ "signature": "/9/8/7.txt.asc",
839+ "size": 104857600
840+ },
841+ {
842+ "checksum": "876",
843+ "order": 1,
844+ "path": "/8/7/6.txt",
845+ "signature": "/8/7/6.txt.asc",
846+ "size": 838860800
847+
848+ }
849+ ],
850+ "type": "delta",
851+ "version": 303
852+ }
853+ ]
854+}
855
856=== modified file 'systemimage/tests/test_candidates.py'
857--- systemimage/tests/test_candidates.py 2014-02-20 23:03:24 +0000
858+++ systemimage/tests/test_candidates.py 2014-10-29 20:07:23 +0000
859@@ -29,16 +29,8 @@
860 from systemimage.candidates import (
861 delta_filter, full_filter, get_candidates, iter_path)
862 from systemimage.scores import WeightedScorer
863-from systemimage.testing.helpers import configuration, get_index
864-
865-
866-def _descriptions(path):
867- descriptions = []
868- for image in path:
869- # There's only one description per image so order doesn't
870- # matter.
871- descriptions.extend(image.descriptions.values())
872- return descriptions
873+from systemimage.testing.helpers import (
874+ configuration, descriptions, get_index)
875
876
877 class TestCandidates(unittest.TestCase):
878@@ -118,7 +110,7 @@
879 self.assertEqual(len(path1), 1)
880 # One path gets us to version 1300 and the other 1400.
881 images = sorted([path0[0], path1[0]], key=attrgetter('version'))
882- self.assertEqual(_descriptions(images), ['Delta 2', 'Delta 1'])
883+ self.assertEqual(descriptions(images), ['Delta 2', 'Delta 1'])
884
885 def test_one_path_with_full_and_deltas(self):
886 # There's one path to upgrade from our version to the final version.
887@@ -130,7 +122,7 @@
888 self.assertEqual(len(path), 3)
889 self.assertEqual([image.version for image in path],
890 [1300, 1301, 1302])
891- self.assertEqual(_descriptions(path), ['Full 1', 'Delta 1', 'Delta 2'])
892+ self.assertEqual(descriptions(path), ['Full 1', 'Delta 1', 'Delta 2'])
893
894 def test_one_path_with_deltas(self):
895 # Similar to above, except that because we're upgrading from the
896@@ -142,7 +134,7 @@
897 path = candidates[0]
898 self.assertEqual(len(path), 2)
899 self.assertEqual([image.version for image in path], [1301, 1302])
900- self.assertEqual(_descriptions(path), ['Delta 1', 'Delta 2'])
901+ self.assertEqual(descriptions(path), ['Delta 1', 'Delta 2'])
902
903 def test_forked_paths(self):
904 # We have a fork in the road. There is a full update, but two deltas
905@@ -181,7 +173,7 @@
906 # a bootme flag. Download all their files.
907 index = get_index('index_10.json')
908 candidates = get_candidates(index, 600)
909- winner = WeightedScorer().choose(candidates)
910+ winner = WeightedScorer().choose(candidates, 'devel')
911 descriptions = []
912 for image in winner:
913 # There's only one description per image so order doesn't matter.
914@@ -219,7 +211,7 @@
915 # has a bootme flag so the second delta's files are not downloaded.
916 index = get_index('index_11.json')
917 candidates = get_candidates(index, 600)
918- winner = WeightedScorer().choose(candidates)
919+ winner = WeightedScorer().choose(candidates, 'devel')
920 descriptions = []
921 for image in winner:
922 # There's only one description per image so order doesn't matter.
923@@ -251,9 +243,9 @@
924 self.assertEqual([image.type for image in filtered[0]], ['full'])
925 self.assertEqual([image.type for image in filtered[1]], ['full'])
926 self.assertEqual([image.type for image in filtered[2]], ['full'])
927- self.assertEqual(_descriptions(filtered[0]), ['Full A'])
928- self.assertEqual(_descriptions(filtered[1]), ['Full B'])
929- self.assertEqual(_descriptions(filtered[2]), ['Full C'])
930+ self.assertEqual(descriptions(filtered[0]), ['Full A'])
931+ self.assertEqual(descriptions(filtered[1]), ['Full B'])
932+ self.assertEqual(descriptions(filtered[2]), ['Full C'])
933
934 def test_filter_for_fulls_one_candidate(self):
935 # Filter for full updates, where the only candidate has one full image.
936@@ -304,7 +296,7 @@
937 self.assertEqual(len(filtered), 1)
938 path = filtered[0]
939 self.assertEqual(len(path), 3)
940- self.assertEqual(_descriptions(path),
941+ self.assertEqual(descriptions(path),
942 ['Delta A', 'Delta B', 'Delta C'])
943
944
945@@ -317,21 +309,21 @@
946 candidates = get_candidates(index, 0)
947 self.assertEqual(len(candidates), 3)
948 path0 = candidates[0]
949- self.assertEqual(_descriptions(path0),
950+ self.assertEqual(descriptions(path0),
951 ['Full A', 'Delta A.1', 'Delta A.2'])
952 path1 = candidates[1]
953- self.assertEqual(_descriptions(path1),
954+ self.assertEqual(descriptions(path1),
955 ['Full B', 'Delta B.1', 'Delta B.2'])
956 path2 = candidates[2]
957- self.assertEqual(_descriptions(path2), ['Full C', 'Delta C.1'])
958+ self.assertEqual(descriptions(path2), ['Full C', 'Delta C.1'])
959 # The version numbers use the new regime.
960 self.assertEqual(path0[0].version, 300)
961 self.assertEqual(path0[1].base, 300)
962 self.assertEqual(path0[1].version, 301)
963 self.assertEqual(path0[2].base, 301)
964 self.assertEqual(path0[2].version, 304)
965- winner = WeightedScorer().choose(candidates)
966- self.assertEqual(_descriptions(winner),
967+ winner = WeightedScorer().choose(candidates, 'devel')
968+ self.assertEqual(descriptions(winner),
969 ['Full B', 'Delta B.1', 'Delta B.2'])
970 self.assertEqual(winner[0].version, 200)
971 self.assertEqual(winner[1].base, 200)
972
973=== modified file 'systemimage/tests/test_config.py'
974--- systemimage/tests/test_config.py 2014-09-17 13:41:31 +0000
975+++ systemimage/tests/test_config.py 2014-10-29 20:07:23 +0000
976@@ -378,3 +378,38 @@
977 config.load(data_path('channel_07.ini'), override=True)
978 with _patch_device_hook():
979 self.assertEqual(config.device, '?')
980+
981+ @configuration
982+ def test_phased_percentage(self, ini_file):
983+ # By default, the phased percentage override is None.
984+ config = Configuration(ini_file)
985+ self.assertIsNone(config.phase_override)
986+
987+ @configuration
988+ def test_phased_percentage_override(self, ini_file):
989+ # The phased percentage for the device can be overridden.
990+ config = Configuration(ini_file)
991+ self.assertIsNone(config.phase_override)
992+ config.phase_override = 33
993+ self.assertEqual(config.phase_override, 33)
994+ # It can also be reset.
995+ del config.phase_override
996+ self.assertIsNone(config.phase_override)
997+
998+ @configuration
999+ def test_phased_percentage_override_int(self, ini_file):
1000+ # When overriding the phased percentage, the new value must be an int.
1001+ config = Configuration(ini_file)
1002+ self.assertRaises(ValueError, setattr, config, 'phase_override', '!')
1003+
1004+ @configuration
1005+ def test_crazy_phase(self, ini_file):
1006+ config = Configuration(ini_file)
1007+ config.phase_override = -100
1008+ self.assertEqual(config.phase_override, 0)
1009+ config.phase_override = 108
1010+ self.assertEqual(config.phase_override, 100)
1011+ config.phase_override = 0
1012+ self.assertEqual(config.phase_override, 0)
1013+ config.phase_override = 100
1014+ self.assertEqual(config.phase_override, 100)
1015
1016=== modified file 'systemimage/tests/test_helpers.py'
1017--- systemimage/tests/test_helpers.py 2014-09-17 13:41:31 +0000
1018+++ systemimage/tests/test_helpers.py 2014-10-29 20:07:23 +0000
1019@@ -275,53 +275,61 @@
1020
1021 class TestPhasedPercentage(unittest.TestCase):
1022 def setUp(self):
1023- phased_percentage(reset=True)
1024+ self._resources = ExitStack()
1025+ tmpdir = self._resources.enter_context(temporary_directory())
1026+ self._mid_path = os.path.join(tmpdir, 'machine-id')
1027+ self._resources.enter_context(patch(
1028+ 'systemimage.helpers.UNIQUE_MACHINE_ID_FILE', self._mid_path))
1029
1030 def tearDown(self):
1031- phased_percentage(reset=True)
1032+ self._resources.close()
1033+
1034+ def _set_machine_id(self, machine_id):
1035+ with open(self._mid_path, 'w', encoding='utf-8') as fp:
1036+ fp.write(machine_id)
1037
1038 def test_phased_percentage(self):
1039- # This function returns a percentage between 0 and 100. If this value
1040- # is greater than a similar value in the index.json's 'image' section,
1041- # that image is completely ignored.
1042- with ExitStack() as stack:
1043- tmpdir = stack.enter_context(temporary_directory())
1044- path = os.path.join(tmpdir, 'machine-id')
1045- stack.enter_context(patch(
1046- 'systemimage.helpers.UNIQUE_MACHINE_ID_FILE',
1047- path))
1048- stack.enter_context(patch(
1049- 'systemimage.helpers.time.time',
1050- return_value=1380659512.983512))
1051- with open(path, 'wb') as fp:
1052- fp.write(b'0123456789abcdef\n')
1053- self.assertEqual(phased_percentage(), 81)
1054- # The value is cached, so it's always the same for the life of the
1055- # process, at least until we reset it.
1056- self.assertEqual(phased_percentage(), 81)
1057-
1058- def test_phased_percentage_reset(self):
1059- # Test the reset API.
1060- with ExitStack() as stack:
1061- tmpdir = stack.enter_context(temporary_directory())
1062- path = os.path.join(tmpdir, 'machine-id')
1063- stack.enter_context(patch(
1064- 'systemimage.helpers.UNIQUE_MACHINE_ID_FILE',
1065- path))
1066- stack.enter_context(patch(
1067- 'systemimage.helpers.time.time',
1068- return_value=1380659512.983512))
1069- with open(path, 'wb') as fp:
1070- fp.write(b'0123456789abcdef\n')
1071- self.assertEqual(phased_percentage(), 81)
1072- # The value is cached, so it's always the same for the life of the
1073- # process, at least until we reset it.
1074- with open(path, 'wb') as fp:
1075- fp.write(b'x0123456789abcde\n')
1076- self.assertEqual(phased_percentage(reset=True), 81)
1077- # The next one will have a different value.
1078- self.assertEqual(phased_percentage(), 17)
1079-
1080+ # The phased percentage is used to determine whether a calculated
1081+ # winning path is to be applied or not. It returns a number between 0
1082+ # and 100 based on the machine's unique machine id (as kept in a
1083+ # file), the update channel, and the target build number.
1084+ self._set_machine_id('0123456789abcdef')
1085+ self.assertEqual(phased_percentage(channel='ubuntu', target=11), 51)
1086+ # The phased percentage is always the same, given the same
1087+ # machine-id, channel, and target.
1088+ self.assertEqual(phased_percentage(channel='ubuntu', target=11), 51)
1089+
1090+ def test_phased_percentage_different_machine_id(self):
1091+ # All else being equal, a different machine_id gives different %.
1092+ self._set_machine_id('0123456789abcdef')
1093+ self.assertEqual(phased_percentage(channel='ubuntu', target=11), 51)
1094+ self._set_machine_id('fedcba9876543210')
1095+ self.assertEqual(phased_percentage(channel='ubuntu', target=11), 25)
1096+
1097+ def test_phased_percentage_different_channel(self):
1098+ # All else being equal, a different channel gives different %.
1099+ self._set_machine_id('0123456789abcdef')
1100+ self.assertEqual(phased_percentage(channel='ubuntu', target=11), 51)
1101+ self._set_machine_id('0123456789abcdef')
1102+ self.assertEqual(phased_percentage(channel='devel', target=11), 96)
1103+
1104+ def test_phased_percentage_different_target(self):
1105+ # All else being equal, a different target gives different %.
1106+ self._set_machine_id('0123456789abcdef')
1107+ self.assertEqual(phased_percentage(channel='ubuntu', target=11), 51)
1108+ self._set_machine_id('0123456789abcdef')
1109+ self.assertEqual(phased_percentage(channel='ubuntu', target=12), 1)
1110+
1111+ @configuration
1112+ def test_phased_percentage_override(self):
1113+ # The phased percentage can be overridden.
1114+ self._set_machine_id('0123456789abcdef')
1115+ self.assertEqual(phased_percentage(channel='ubuntu', target=11), 51)
1116+ config.phase_override = 33
1117+ self.assertEqual(phased_percentage(channel='ubuntu', target=11), 33)
1118+ # And reset.
1119+ del config.phase_override
1120+ self.assertEqual(phased_percentage(channel='ubuntu', target=11), 51)
1121
1122 class TestSignature(unittest.TestCase):
1123 def test_calculate_signature(self):
1124
1125=== modified file 'systemimage/tests/test_index.py'
1126--- systemimage/tests/test_index.py 2014-02-20 23:03:24 +0000
1127+++ systemimage/tests/test_index.py 2014-10-29 20:07:23 +0000
1128@@ -33,9 +33,6 @@
1129 configuration, copy, get_index, make_http_server, makedirs,
1130 setup_keyring_txz, setup_keyrings, sign)
1131 from systemimage.testing.nose import SystemImagePlugin
1132-# FIXME
1133-from systemimage.tests.test_candidates import _descriptions
1134-from unittest.mock import patch
1135
1136
1137 class TestIndex(unittest.TestCase):
1138@@ -113,45 +110,6 @@
1139 'description-xx_CC': 'This hyar is the delta B.2',
1140 })
1141
1142- def test_image_phased_percentage(self):
1143- # This index has two full updates with a phased-percentage value and
1144- # one without (which defaults to 100). We'll set the system's
1145- # percentage right in the middle of the two so that the one with 50%
1146- # will not show up in the list of images.
1147- with patch('systemimage.index.phased_percentage', return_value=66):
1148- index = get_index('index_22.json')
1149- descriptions = set(_descriptions(index.images))
1150- # This one does not have a phased-percentage, so using the default of
1151- # 100, it gets in.
1152- self.assertIn('Full A', descriptions)
1153- # This one has a phased-percentage of 50 so it gets ignored.
1154- self.assertNotIn('Full B', descriptions)
1155- # This one has a phased-percentage of 75 so it gets added.
1156- self.assertIn('Full C', descriptions)
1157-
1158- def test_image_phased_percentage_100(self):
1159- # Like above, but with a system percentage of 100, so nothing but the
1160- # default gets in.
1161- with patch('systemimage.index.phased_percentage', return_value=100):
1162- index = get_index('index_22.json')
1163- descriptions = set(_descriptions(index.images))
1164- # This one does not have a phased-percentage, so using the default of
1165- # 100, it gets in.
1166- self.assertIn('Full A', descriptions)
1167- # This one has a phased-percentage of 50 so it gets ignored.
1168- self.assertNotIn('Full B', descriptions)
1169- # This one has a phased-percentage of 75 so it gets added.
1170- self.assertNotIn('Full C', descriptions)
1171-
1172- def test_image_phased_percentage_0(self):
1173- # Like above, but with a system percentage of 0, everything gets in.
1174- with patch('systemimage.index.phased_percentage', return_value=0):
1175- index = get_index('index_22.json')
1176- descriptions = set(_descriptions(index.images))
1177- self.assertIn('Full A', descriptions)
1178- self.assertIn('Full B', descriptions)
1179- self.assertIn('Full C', descriptions)
1180-
1181
1182 class TestDownloadIndex(unittest.TestCase):
1183 maxDiff = None
1184
1185=== modified file 'systemimage/tests/test_main.py'
1186--- systemimage/tests/test_main.py 2014-09-17 13:41:31 +0000
1187+++ systemimage/tests/test_main.py 2014-10-29 20:07:23 +0000
1188@@ -71,6 +71,27 @@
1189 os.umask(old_mask)
1190
1191
1192+def machine_id(mid):
1193+ with ExitStack() as resources:
1194+ tempdir = resources.enter_context(temporary_directory())
1195+ path = os.path.join(tempdir, 'machine-id')
1196+ with open(path, 'w', encoding='utf-8') as fp:
1197+ print(mid, file=fp)
1198+ resources.enter_context(
1199+ patch('systemimage.helpers.UNIQUE_MACHINE_ID_FILE', path))
1200+ return resources.pop_all()
1201+
1202+
1203+def capture_print(fp):
1204+ return patch('builtins.print', partial(print, file=fp))
1205+
1206+
1207+def argv(*args):
1208+ args = list(args)
1209+ args.insert(0, 'argv0')
1210+ return patch('systemimage.main.sys.argv', args)
1211+
1212+
1213 class TestCLIMain(unittest.TestCase):
1214 def setUp(self):
1215 super().setUp()
1216@@ -81,11 +102,12 @@
1217 # We patch builtin print() rather than sys.stdout because the
1218 # latter can mess with pdb output should we need to trace through
1219 # the code.
1220- self._resources.enter_context(
1221- patch('builtins.print', partial(print, file=self._stdout)))
1222+ self._resources.enter_context(capture_print(self._stdout))
1223 # Patch argparse's stderr to capture its error messages.
1224 self._resources.enter_context(
1225 patch('argparse._sys.stderr', self._stderr))
1226+ self._resources.push(
1227+ machine_id('feedfacebeefbacafeedfacebeefbaca'))
1228 except:
1229 self._resources.close()
1230 raise
1231@@ -96,8 +118,7 @@
1232
1233 def test_config_file_good_path(self):
1234 # The default configuration file exists.
1235- self._resources.enter_context(
1236- patch('systemimage.main.sys.argv', ['argv0', '--info']))
1237+ self._resources.enter_context(argv('--info'))
1238 # Patch default configuration file.
1239 tempdir = self._resources.enter_context(temporary_directory())
1240 ini_path = os.path.join(tempdir, 'client.ini')
1241@@ -114,8 +135,7 @@
1242
1243 def test_missing_default_config_file(self):
1244 # The default configuration file is missing.
1245- self._resources.enter_context(
1246- patch('systemimage.main.sys.argv', ['argv0']))
1247+ self._resources.enter_context(argv())
1248 # Patch default configuration file.
1249 self._resources.enter_context(
1250 patch('systemimage.main.DEFAULT_CONFIG_FILE',
1251@@ -129,9 +149,7 @@
1252
1253 def test_missing_explicit_config_file(self):
1254 # An explicit configuration file given with -C is missing.
1255- self._resources.enter_context(
1256- patch('systemimage.main.sys.argv',
1257- ['argv0', '-C', '/does/not/exist.ini']))
1258+ self._resources.enter_context(argv('-C', '/does/not/exist.ini'))
1259 with self.assertRaises(SystemExit) as cm:
1260 cli_main()
1261 self.assertEqual(cm.exception.code, 2)
1262@@ -155,9 +173,7 @@
1263 with open(config_ini, 'wt', encoding='utf-8') as fp:
1264 fp.write(configuration)
1265 # Invoking main() creates the directories.
1266- self._resources.enter_context(patch(
1267- 'systemimage.main.sys.argv',
1268- ['argv0', '-C', config_ini, '--info']))
1269+ self._resources.enter_context(argv('-C', config_ini, '--info'))
1270 self.assertFalse(os.path.exists(tmpdir))
1271 cli_main()
1272 self.assertTrue(os.path.exists(tmpdir))
1273@@ -182,9 +198,7 @@
1274 config = Configuration(config_ini)
1275 self.assertFalse(os.path.exists(config.system.tempdir))
1276 self.assertFalse(os.path.exists(config.system.logfile))
1277- self._resources.enter_context(patch(
1278- 'systemimage.main.sys.argv',
1279- ['argv0', '-C', config_ini, '--info']))
1280+ self._resources.enter_context(argv('-C', config_ini, '--info'))
1281 cli_main()
1282 mode = os.stat(config.system.tempdir).st_mode
1283 self.assertEqual(stat.filemode(mode), 'drwx--S---')
1284@@ -197,9 +211,7 @@
1285 def test_info(self, ini_file):
1286 # -i/--info gives information about the device, including the current
1287 # build number, channel, and device name.
1288- self._resources.enter_context(
1289- patch('systemimage.main.sys.argv',
1290- ['argv0', '-C', ini_file, '--info']))
1291+ self._resources.enter_context(argv('-C', ini_file, '--info'))
1292 # Set up the build number.
1293 touch_build(1701, TIMESTAMP)
1294 cli_main()
1295@@ -217,9 +229,7 @@
1296 channel_ini = os.path.join(os.path.dirname(ini_file), 'channel.ini')
1297 head, tail = os.path.split(channel_ini)
1298 copy('channel_01.ini', head, tail)
1299- self._resources.enter_context(
1300- patch('systemimage.main.sys.argv',
1301- ['argv0', '-C', ini_file, '--info']))
1302+ self._resources.enter_context(argv('-C', ini_file, '--info'))
1303 # Set up the build number.
1304 config = Configuration(ini_file)
1305 touch_build(1701)
1306@@ -240,9 +250,7 @@
1307 # --info's last update date falls back to the mtime of
1308 # /etc/ubuntu-build when no channel.ini file exists.
1309 channel_ini = os.path.join(os.path.dirname(ini_file), 'channel.ini')
1310- self._resources.enter_context(
1311- patch('systemimage.main.sys.argv',
1312- ['argv0', '-C', ini_file, '--info']))
1313+ self._resources.enter_context(argv('-C', ini_file, '--info'))
1314 # Set up the build number.
1315 config = Configuration(ini_file)
1316 touch_build(1701)
1317@@ -263,10 +271,7 @@
1318 touch_build(1701, TIMESTAMP)
1319 # Use --build to override the default build number.
1320 self._resources.enter_context(
1321- patch('systemimage.main.sys.argv',
1322- ['argv0', '-C', ini_file,
1323- '--build', '20250801',
1324- '--info']))
1325+ argv('-C', ini_file, '--build', '20250801', '--info'))
1326 cli_main()
1327 self.assertEqual(self._stdout.getvalue(), dedent("""\
1328 current build number: 20250801
1329@@ -280,10 +285,7 @@
1330 # -d/--device overrides the device type.
1331 touch_build(1701, TIMESTAMP)
1332 self._resources.enter_context(
1333- patch('systemimage.main.sys.argv',
1334- ['argv0', '-C', ini_file,
1335- '--device', 'phablet',
1336- '--info']))
1337+ argv('-C', ini_file, '--device', 'phablet', '--info'))
1338 cli_main()
1339 self.assertEqual(self._stdout.getvalue(), dedent("""\
1340 current build number: 1701
1341@@ -297,10 +299,7 @@
1342 # -c/--channel overrides the channel.
1343 touch_build(1701, TIMESTAMP)
1344 self._resources.enter_context(
1345- patch('systemimage.main.sys.argv',
1346- ['argv0', '-C', ini_file,
1347- '--channel', 'daily-proposed',
1348- '--info']))
1349+ argv('-C', ini_file, '--channel', 'daily-proposed', '--info'))
1350 cli_main()
1351 self.assertEqual(self._stdout.getvalue(), dedent("""\
1352 current build number: 1701
1353@@ -317,9 +316,7 @@
1354 head, tail = os.path.split(channel_ini)
1355 copy('channel_05.ini', head, tail)
1356 touch_build(300, TIMESTAMP)
1357- self._resources.enter_context(
1358- patch('systemimage.main.sys.argv',
1359- ['argv0', '-C', ini_file, '--info']))
1360+ self._resources.enter_context(argv('-C', ini_file, '--info'))
1361 cli_main()
1362 self.assertEqual(self._stdout.getvalue(), dedent("""\
1363 current build number: 300
1364@@ -335,12 +332,8 @@
1365 touch_build(1701, TIMESTAMP)
1366 # Use --build to override the default build number.
1367 self._resources.enter_context(
1368- patch('systemimage.main.sys.argv',
1369- ['argv0', '-C', ini_file,
1370- '-b', '20250801',
1371- '-c', 'daily-proposed',
1372- '-d', 'phablet',
1373- '--info']))
1374+ argv('-C', ini_file, '-b', '20250801',
1375+ '-c', 'daily-proposed', '-d', 'phablet', '--info'))
1376 cli_main()
1377 self.assertEqual(self._stdout.getvalue(), dedent("""\
1378 current build number: 20250801
1379@@ -352,9 +345,7 @@
1380 @configuration
1381 def test_bad_build_number_override(self, ini_file):
1382 # -b/--build requires an integer.
1383- self._resources.enter_context(
1384- patch('systemimage.main.sys.argv',
1385- ['argv0', '-C', ini_file, '--build', 'bogus']))
1386+ self._resources.enter_context(argv('-C', ini_file, '--build', 'bogus'))
1387 with self.assertRaises(SystemExit) as cm:
1388 cli_main()
1389 self.assertEqual(cm.exception.code, 2)
1390@@ -366,9 +357,7 @@
1391 def test_channel_ini_override_build_number(self, ini_file):
1392 # The channel.ini file can override the build number.
1393 copy('channel_01.ini', os.path.dirname(ini_file), 'channel.ini')
1394- self._resources.enter_context(
1395- patch('systemimage.main.sys.argv',
1396- ['argv0', '-C', ini_file, '-i']))
1397+ self._resources.enter_context(argv('-C', ini_file, '-i'))
1398 # Set up the build number.
1399 touch_build(1701, TIMESTAMP)
1400 cli_main()
1401@@ -386,9 +375,7 @@
1402 head, tail = os.path.split(channel_ini)
1403 copy('channel_01.ini', head, tail)
1404 os.utime(channel_ini, (TIMESTAMP, TIMESTAMP))
1405- self._resources.enter_context(
1406- patch('systemimage.main.sys.argv',
1407- ['argv0', '-C', ini_file, '-i']))
1408+ self._resources.enter_context(argv('-C', ini_file, '-i'))
1409 cli_main()
1410 self.assertEqual(self._stdout.getvalue(), dedent("""\
1411 current build number: 1833
1412@@ -403,9 +390,7 @@
1413 # `system-image-cli -b 0 --channel <channel>`.
1414 touch_build(801, TIMESTAMP)
1415 self._resources.enter_context(
1416- patch('systemimage.main.sys.argv',
1417- ['argv0', '-C', ini_file, '--switch', 'utopic-proposed',
1418- '--info']))
1419+ argv('-C', ini_file, '--switch', 'utopic-proposed', '--info'))
1420 cli_main()
1421 self.assertEqual(self._stdout.getvalue(), dedent("""\
1422 current build number: 0
1423@@ -420,9 +405,8 @@
1424 # given explicitly, they override the convenience.
1425 touch_build(801, TIMESTAMP)
1426 self._resources.enter_context(
1427- patch('systemimage.main.sys.argv',
1428- ['argv0', '-C', ini_file, '--switch', 'utopic-proposed',
1429- '-b', '1', '-c', 'utopic', '--info']))
1430+ argv('-C', ini_file, '--switch', 'utopic-proposed',
1431+ '-b', '1', '-c', 'utopic', '--info'))
1432 cli_main()
1433 self.assertEqual(self._stdout.getvalue(), dedent("""\
1434 current build number: 1
1435@@ -443,9 +427,7 @@
1436 return self
1437 def __next__(self):
1438 raise StopIteration
1439- self._resources.enter_context(
1440- patch('systemimage.main.sys.argv',
1441- ['argv0', '-C', ini_file]))
1442+ self._resources.enter_context(argv('-C', ini_file))
1443 self._resources.enter_context(
1444 patch('systemimage.main.State', FakeState))
1445 cli_main()
1446@@ -474,9 +456,7 @@
1447 tmpdir = self._resources.enter_context(temporary_directory())
1448 self._resources.enter_context(
1449 patch('systemimage.logging.xdg_cache_home', tmpdir))
1450- self._resources.enter_context(
1451- patch('systemimage.main.sys.argv',
1452- ['argv0', '-C', ini_file, '--dry-run']))
1453+ self._resources.enter_context(argv('-C', ini_file, '--dry-run'))
1454 cli_main()
1455 # There should now be nothing in the system log file, and something in
1456 # the fallback log file.
1457@@ -490,8 +470,7 @@
1458 def test_bad_filter_type(self, ini_file):
1459 # --filter option where value is not `full` or `delta` is an error.
1460 self._resources.enter_context(
1461- patch('systemimage.main.sys.argv',
1462- ['argv0', '-C', ini_file, '--filter', 'bogus']))
1463+ argv('-C', ini_file, '--filter', 'bogus'))
1464 with self.assertRaises(SystemExit) as cm:
1465 cli_main()
1466 self.assertEqual(cm.exception.code, 2)
1467@@ -506,9 +485,7 @@
1468 head, tail = os.path.split(channel_ini)
1469 copy('channel_03.ini', head, tail)
1470 os.utime(channel_ini, (TIMESTAMP, TIMESTAMP))
1471- self._resources.enter_context(
1472- patch('systemimage.main.sys.argv',
1473- ['argv0', '-C', ini_file, '-i']))
1474+ self._resources.enter_context(argv('-C', ini_file, '-i'))
1475 cli_main()
1476 self.assertEqual(self._stdout.getvalue(), dedent("""\
1477 current build number: 1833
1478@@ -527,9 +504,7 @@
1479 head, tail = os.path.split(channel_ini)
1480 copy('channel_01.ini', head, tail)
1481 os.utime(channel_ini, (TIMESTAMP, TIMESTAMP))
1482- self._resources.enter_context(
1483- patch('systemimage.main.sys.argv',
1484- ['argv0', '-C', ini_file, '-i']))
1485+ self._resources.enter_context(argv('-C', ini_file, '-i'))
1486 cli_main()
1487 self.assertEqual(self._stdout.getvalue(), dedent("""\
1488 current build number: 1833
1489@@ -543,8 +518,7 @@
1490 # If an exception happens during the state machine run, the error is
1491 # logged and main exits with code 1.
1492 config = Configuration(ini_file)
1493- self._resources.enter_context(
1494- patch('systemimage.main.sys.argv', ['argv0', '-C', ini_file]))
1495+ self._resources.enter_context(argv('-C', ini_file))
1496 # Making the cache directory unwritable is a good way to trigger a
1497 # crash. Be sure to set it back though!
1498 with chmod(config.updater.cache_partition, 0):
1499@@ -557,9 +531,7 @@
1500 config = Configuration(ini_file)
1501 # Making the cache directory unwritable is a good way to trigger a
1502 # crash. Be sure to set it back though!
1503- self._resources.enter_context(
1504- patch('systemimage.main.sys.argv',
1505- ['argv0', '-C', ini_file, '--dry-run']))
1506+ self._resources.enter_context(argv('-C', ini_file, '--dry-run'))
1507 with chmod(config.updater.cache_partition, 0):
1508 exit_code = cli_main()
1509 self.assertEqual(exit_code, 1)
1510@@ -580,14 +552,15 @@
1511 # the code.
1512 capture = StringIO()
1513 with ExitStack() as resources:
1514- resources.enter_context(
1515- patch('builtins.print', partial(print, file=capture)))
1516- resources.enter_context(
1517- patch('systemimage.main.sys.argv',
1518- ['argv0', '-C', ini_file, '--dry-run']))
1519+ resources.enter_context(capture_print(capture))
1520+ resources.push(machine_id('0000000000000000aaaaaaaaaaaaaaaa'))
1521+ resources.enter_context(argv('-C', ini_file, '--dry-run'))
1522 cli_main()
1523- self.assertEqual(capture.getvalue(),
1524- 'Upgrade path is 1200:1201:1304\n')
1525+ self.assertEqual(
1526+ capture.getvalue(), """\
1527+Upgrade path is 1200:1201:1304
1528+Target phase: 12%
1529+""")
1530
1531 @configuration
1532 def test_dry_run_no_update(self, ini_file):
1533@@ -600,11 +573,8 @@
1534 # Set up the build number.
1535 touch_build(1701)
1536 with ExitStack() as resources:
1537- resources.enter_context(
1538- patch('builtins.print', partial(print, file=capture)))
1539- resources.enter_context(
1540- patch('systemimage.main.sys.argv',
1541- ['argv0', '-C', ini_file, '--dry-run']))
1542+ resources.enter_context(capture_print(capture))
1543+ resources.enter_context(argv('-C', ini_file, '--dry-run'))
1544 cli_main()
1545 self.assertEqual(capture.getvalue(), 'Already up-to-date\n')
1546
1547@@ -618,17 +588,99 @@
1548 # the code.
1549 capture = StringIO()
1550 with ExitStack() as resources:
1551- resources.enter_context(
1552- patch('builtins.print', partial(print, file=capture)))
1553+ resources.enter_context(capture_print(capture))
1554 # Use --build to override the default build number.
1555 resources.enter_context(
1556- patch('systemimage.main.sys.argv', [
1557- 'argv0', '-C', ini_file,
1558- '--channel', 'daily-proposed',
1559- '--dry-run']))
1560+ argv('-C', ini_file, '--channel', 'daily-proposed',
1561+ '--dry-run'))
1562 cli_main()
1563 self.assertEqual(capture.getvalue(), 'Already up-to-date\n')
1564
1565+ @configuration
1566+ def test_percentage(self, ini_file):
1567+ # --percentage overrides the device's target percentage.
1568+ self._setup_server_keyrings()
1569+ capture = StringIO()
1570+ with ExitStack() as resources:
1571+ resources.enter_context(capture_print(capture))
1572+ resources.push(machine_id('0000000000000000aaaaaaaaaaaaaaaa'))
1573+ resources.enter_context(argv('-C', ini_file, '--dry-run'))
1574+ cli_main()
1575+ self.assertEqual(
1576+ capture.getvalue(), """\
1577+Upgrade path is 1200:1201:1304
1578+Target phase: 12%
1579+""")
1580+ capture = StringIO()
1581+ with ExitStack() as resources:
1582+ resources.enter_context(capture_print(capture))
1583+ resources.push(machine_id('0000000000000000aaaaaaaaaaaaaaaa'))
1584+ resources.enter_context(
1585+ argv('-C', ini_file, '--dry-run', '--percentage', '81'))
1586+ cli_main()
1587+ self.assertEqual(
1588+ capture.getvalue(), """\
1589+Upgrade path is 1200:1201:1304
1590+Target phase: 81%
1591+""")
1592+
1593+ @configuration
1594+ def test_p(self, ini_file):
1595+ # -p overrides the device's target percentage.
1596+ self._setup_server_keyrings()
1597+ capture = StringIO()
1598+ with ExitStack() as resources:
1599+ resources.enter_context(capture_print(capture))
1600+ resources.push(machine_id('0000000000000000aaaaaaaaaaaaaaaa'))
1601+ resources.enter_context(argv('-C', ini_file, '--dry-run'))
1602+ cli_main()
1603+ self.assertEqual(
1604+ capture.getvalue(), """\
1605+Upgrade path is 1200:1201:1304
1606+Target phase: 12%
1607+""")
1608+ capture = StringIO()
1609+ with ExitStack() as resources:
1610+ resources.enter_context(capture_print(capture))
1611+ resources.push(machine_id('0000000000000000aaaaaaaaaaaaaaaa'))
1612+ resources.enter_context(
1613+ argv('-C', ini_file, '--dry-run', '--p', '81'))
1614+ cli_main()
1615+ self.assertEqual(
1616+ capture.getvalue(), """\
1617+Upgrade path is 1200:1201:1304
1618+Target phase: 81%
1619+""")
1620+
1621+ @configuration
1622+ def test_crazy_p(self, ini_file):
1623+ # --percentage/-p value is floored at 0% and ceilinged at 100%.
1624+ self._setup_server_keyrings()
1625+ capture = StringIO()
1626+ with ExitStack() as resources:
1627+ resources.enter_context(capture_print(capture))
1628+ resources.push(machine_id('0000000000000000aaaaaaaaaaaaaaaa'))
1629+ resources.enter_context(
1630+ argv('-C', ini_file, '--dry-run', '--p', '10000'))
1631+ cli_main()
1632+ self.assertEqual(
1633+ capture.getvalue(), """\
1634+Upgrade path is 1200:1201:1304
1635+Target phase: 100%
1636+""")
1637+ capture = StringIO()
1638+ with ExitStack() as resources:
1639+ resources.enter_context(capture_print(capture))
1640+ resources.push(machine_id('0000000000000000aaaaaaaaaaaaaaaa'))
1641+ resources.enter_context(
1642+ argv('-C', ini_file, '--dry-run', '--p', '-10'))
1643+ cli_main()
1644+ self.assertEqual(
1645+ capture.getvalue(), """\
1646+Upgrade path is 1200:1201:1304
1647+Target phase: 0%
1648+""")
1649+
1650
1651 class TestCLIMainDryRunAliases(ServerTestBase):
1652 INDEX_FILE = 'index_20.json'
1653@@ -645,20 +697,23 @@
1654 head, tail = os.path.split(channel_ini)
1655 copy('channel_05.ini', head, tail)
1656 capture = StringIO()
1657- self._resources.enter_context(
1658- patch('builtins.print', partial(print, file=capture)))
1659- self._resources.enter_context(
1660- patch('systemimage.main.sys.argv',
1661- ['argv0', '-C', ini_file, '--dry-run']))
1662- # Do not use self._resources to manage the check_output mock. Because
1663- # of the nesting order of the @configuration decorator and the base
1664- # class's tearDown(), using self._resources causes the mocks to be
1665- # unwound in the wrong order, affecting future tests.
1666- with patch('systemimage.device.check_output', return_value='manta'):
1667+ with ExitStack() as resources:
1668+ resources.enter_context(capture_print(capture))
1669+ resources.enter_context(argv('-C', ini_file, '--dry-run'))
1670+ # Patch the machine id.
1671+ resources.push(machine_id('0000000000000000aaaaaaaaaaaaaaaa'))
1672+ # Do not use self._resources to manage the check_output mock.
1673+ # Because of the nesting order of the @configuration decorator and
1674+ # the base class's tearDown(), using self._resources causes the
1675+ # mocks to be unwound in the wrong order, affecting future tests.
1676+ resources.enter_context(
1677+ patch('systemimage.device.check_output', return_value='manta'))
1678 cli_main()
1679 self.assertEqual(
1680- capture.getvalue(),
1681- 'Upgrade path is 200:201:304 (saucy -> tubular)\n')
1682+ capture.getvalue(), """\
1683+Upgrade path is 200:201:304 (saucy -> tubular)
1684+Target phase: 25%
1685+""")
1686
1687
1688 class TestCLIListChannels(ServerTestBase):
1689@@ -676,11 +731,8 @@
1690 head, tail = os.path.split(channel_ini)
1691 copy('channel_05.ini', head, tail)
1692 capture = StringIO()
1693- self._resources.enter_context(
1694- patch('builtins.print', partial(print, file=capture)))
1695- self._resources.enter_context(
1696- patch('systemimage.main.sys.argv',
1697- ['argv0', '-C', ini_file, '--list-channels']))
1698+ self._resources.enter_context(capture_print(capture))
1699+ self._resources.enter_context(argv('-C', ini_file, '--list-channels'))
1700 # Do not use self._resources to manage the check_output mock. Because
1701 # of the nesting order of the @configuration decorator and the base
1702 # class's tearDown(), using self._resources causes the mocks to be
1703@@ -703,11 +755,8 @@
1704 head, tail = os.path.split(channel_ini)
1705 copy('channel_05.ini', head, tail)
1706 capture = StringIO()
1707- self._resources.enter_context(
1708- patch('builtins.print', partial(print, file=capture)))
1709- self._resources.enter_context(
1710- patch('systemimage.main.sys.argv',
1711- ['argv0', '-C', ini_file, '--list-channels']))
1712+ self._resources.enter_context(capture_print(capture))
1713+ self._resources.enter_context(argv('-C', ini_file, '--list-channels'))
1714 # Do not use self._resources to manage the check_output mock. Because
1715 # of the nesting order of the @configuration decorator and the base
1716 # class's tearDown(), using self._resources causes the mocks to be
1717@@ -741,12 +790,9 @@
1718 # Set up the build number.
1719 touch_build(100)
1720 with ExitStack() as resources:
1721- resources.enter_context(
1722- patch('builtins.print', partial(print, file=capture)))
1723- resources.enter_context(
1724- patch('systemimage.main.sys.argv', [
1725- 'argv0', '-C', ini_file, '--dry-run',
1726- '--filter', 'full']))
1727+ resources.enter_context(capture_print(capture))
1728+ resources.enter_context(
1729+ argv('-C', ini_file, '--dry-run', '--filter', 'full'))
1730 cli_main()
1731 self.assertMultiLineEqual(capture.getvalue(), 'Already up-to-date\n')
1732
1733@@ -761,14 +807,15 @@
1734 # Set up the build number.
1735 touch_build(100)
1736 with ExitStack() as resources:
1737- resources.enter_context(
1738- patch('builtins.print', partial(print, file=capture)))
1739- resources.enter_context(
1740- patch('systemimage.main.sys.argv', [
1741- 'argv0', '-C', ini_file, '--dry-run',
1742- '--filter', 'delta']))
1743+ resources.enter_context(capture_print(capture))
1744+ resources.enter_context(
1745+ argv('-C', ini_file, '--dry-run', '--filter', 'delta'))
1746+ resources.push(machine_id('0000000000000000aaaaaaaaaaaaaaaa'))
1747 cli_main()
1748- self.assertMultiLineEqual(capture.getvalue(), 'Upgrade path is 1600\n')
1749+ self.assertMultiLineEqual(capture.getvalue(), """\
1750+Upgrade path is 1600
1751+Target phase: 80%
1752+""")
1753
1754
1755 class TestCLIDuplicateDestinations(ServerTestBase):
1756@@ -786,8 +833,7 @@
1757 # an exception.
1758 self._setup_server_keyrings()
1759 with ExitStack() as resources:
1760- resources.enter_context(
1761- patch('systemimage.main.sys.argv', ['argv0', '-C', ini_file]))
1762+ resources.enter_context(argv('-C', ini_file))
1763 exit_code = cli_main()
1764 self.assertEqual(exit_code, 1)
1765 # 2013-11-12 BAW: IWBNI we could assert something about the log
1766@@ -812,12 +858,9 @@
1767 # reboot into recovery.
1768 self._setup_server_keyrings()
1769 capture = StringIO()
1770- self._resources.enter_context(
1771- patch('builtins.print', partial(print, file=capture)))
1772- self._resources.enter_context(
1773- patch('systemimage.main.sys.argv',
1774- ['argv0', '-C', ini_file, '--no-reboot',
1775- '-b', 0, '-c', 'daily']))
1776+ self._resources.enter_context(capture_print(capture))
1777+ self._resources.enter_context(
1778+ argv('-C', ini_file, '--no-reboot', '-b', 0, '-c', 'daily'))
1779 mock = self._resources.enter_context(
1780 patch('systemimage.reboot.Reboot.reboot'))
1781 # Do not use self._resources to manage the check_output mock. Because
1782@@ -869,11 +912,9 @@
1783 # recovery.
1784 self._setup_server_keyrings()
1785 capture = StringIO()
1786- self._resources.enter_context(
1787- patch('builtins.print', partial(print, file=capture)))
1788- self._resources.enter_context(
1789- patch('systemimage.main.sys.argv',
1790- ['argv0', '-C', ini_file, '-g', '-b', 0, '-c', 'daily']))
1791+ self._resources.enter_context(capture_print(capture))
1792+ self._resources.enter_context(
1793+ argv('-C', ini_file, '-g', '-b', 0, '-c', 'daily'))
1794 mock = self._resources.enter_context(
1795 patch('systemimage.reboot.Reboot.reboot'))
1796 # Do not use self._resources to manage the check_output mock. Because
1797@@ -925,13 +966,11 @@
1798 # not download anything the second time, but does issue a reboot.
1799 self._setup_server_keyrings()
1800 capture = StringIO()
1801- self._resources.enter_context(
1802- patch('builtins.print', partial(print, file=capture)))
1803+ self._resources.enter_context(capture_print(capture))
1804 mock = self._resources.enter_context(
1805 patch('systemimage.reboot.Reboot.reboot'))
1806 self._resources.enter_context(
1807- patch('systemimage.main.sys.argv',
1808- ['argv0', '-C', ini_file, '-g', '-b', 0, '-c', 'daily']))
1809+ argv('-C', ini_file, '-g', '-b', 0, '-c', 'daily'))
1810 # Do not use self._resources to manage the check_output mock. Because
1811 # of the nesting order of the @configuration decorator and the base
1812 # class's tearDown(), using self._resources causes the mocks to be
1813@@ -945,8 +984,7 @@
1814 shutil.rmtree(os.path.join(self._serverdir, '3'))
1815 shutil.rmtree(os.path.join(self._serverdir, '4'))
1816 shutil.rmtree(os.path.join(self._serverdir, '5'))
1817- with patch('systemimage.main.sys.argv',
1818- ['argv0', '-C', ini_file, '-b', 0, '-c', 'daily']):
1819+ with argv('-C', ini_file, '-b', 0, '-c', 'daily'):
1820 cli_main()
1821 # The reboot method was never called.
1822 self.assertTrue(mock.called)
1823@@ -960,13 +998,10 @@
1824 # system-image-cli --factory-reset
1825 capture = StringIO()
1826 with ExitStack() as resources:
1827- resources.enter_context(
1828- patch('builtins.print', partial(print, file=capture)))
1829+ resources.enter_context(capture_print(capture))
1830 mock = resources.enter_context(
1831 patch('systemimage.reboot.Reboot.reboot'))
1832- resources.enter_context(
1833- patch('systemimage.main.sys.argv',
1834- ['argv0', '-C', ini_file, '--factory-reset']))
1835+ resources.enter_context(argv('-C', ini_file, '--factory-reset'))
1836 cli_main()
1837 # A reboot was issued.
1838 self.assertTrue(mock.called)
1839@@ -990,8 +1025,7 @@
1840 # We patch builtin print() rather than sys.stdout because the
1841 # latter can mess with pdb output should we need to trace through
1842 # the code.
1843- self._resources.enter_context(
1844- patch('builtins.print', partial(print, file=self._stdout)))
1845+ self._resources.enter_context(capture_print(self._stdout))
1846 # Patch argparse's stderr to capture its error messages.
1847 self._resources.enter_context(
1848 patch('argparse._sys.stderr', self._stderr))
1849@@ -1011,9 +1045,7 @@
1850 settings.set('peart', 'neil')
1851 settings.set('lee', 'geddy')
1852 settings.set('lifeson', 'alex')
1853- self._resources.enter_context(
1854- patch('systemimage.main.sys.argv',
1855- ['argv0', '-C', ini_file, '--show-settings']))
1856+ self._resources.enter_context(argv('-C', ini_file, '--show-settings'))
1857 cli_main()
1858 self.assertMultiLineEqual(self._stdout.getvalue(), dedent("""\
1859 lee=geddy
1860@@ -1026,9 +1058,7 @@
1861 # `system-image-cli --get key` prints the key's value.
1862 settings = Settings()
1863 settings.set('ant', 'aunt')
1864- self._resources.enter_context(
1865- patch('systemimage.main.sys.argv',
1866- ['argv0', '-C', ini_file, '--get', 'ant']))
1867+ self._resources.enter_context(argv('-C', ini_file, '--get', 'ant'))
1868 cli_main()
1869 self.assertMultiLineEqual(self._stdout.getvalue(), dedent("""\
1870 aunt
1871@@ -1042,9 +1072,7 @@
1872 settings.set('t', 'trusty')
1873 settings.set('u', 'utopic')
1874 self._resources.enter_context(
1875- patch('systemimage.main.sys.argv',
1876- ['argv0', '-C', ini_file,
1877- '--get', 's', '--get', 'u', '--get', 't']))
1878+ argv('-C', ini_file, '--get', 's', '--get', 'u', '--get', 't'))
1879 cli_main()
1880 self.assertMultiLineEqual(self._stdout.getvalue(), dedent("""\
1881 saucy
1882@@ -1057,9 +1085,7 @@
1883 # Since by definition a missing key has a default value, you can get
1884 # missing keys. Note that `auto_download` is the one weirdo.
1885 self._resources.enter_context(
1886- patch('systemimage.main.sys.argv',
1887- ['argv0', '-C', ini_file,
1888- '--get', 'missing', '--get', 'auto_download']))
1889+ argv('-C', ini_file, '--get', 'missing', '--get', 'auto_download'))
1890 cli_main()
1891 # This produces a blank line, since `missing` returns the empty
1892 # string. For better readability, don't indent the results.
1893@@ -1071,9 +1097,7 @@
1894 @configuration
1895 def test_set_key(self, ini_file):
1896 # `system-image-cli --set key=value` sets a key/value pair.
1897- self._resources.enter_context(
1898- patch('systemimage.main.sys.argv',
1899- ['argv0', '-C', ini_file, '--set', 'bass=4']))
1900+ self._resources.enter_context(argv('-C', ini_file, '--set', 'bass=4'))
1901 cli_main()
1902 self.assertEqual(Settings().get('bass'), '4')
1903
1904@@ -1084,9 +1108,7 @@
1905 settings.set('a', 'ant')
1906 settings.set('b', 'bee')
1907 settings.set('c', 'cat')
1908- self._resources.enter_context(
1909- patch('systemimage.main.sys.argv',
1910- ['argv0', '-C', ini_file, '--set', 'b=bat']))
1911+ self._resources.enter_context(argv('-C', ini_file, '--set', 'b=bat'))
1912 cli_main()
1913 self.assertEqual(settings.get('a'), 'ant')
1914 self.assertEqual(settings.get('b'), 'bat')
1915@@ -1096,11 +1118,10 @@
1916 def test_set_keys(self, ini_file):
1917 # `--set key=value` can be used multiple times.
1918 self._resources.enter_context(
1919- patch('systemimage.main.sys.argv',
1920- ['argv0', '-C', ini_file,
1921- '--set', 'a=ant',
1922- '--set', 'b=bee',
1923- '--set', 'c=cat']))
1924+ argv('-C', ini_file,
1925+ '--set', 'a=ant',
1926+ '--set', 'b=bee',
1927+ '--set', 'c=cat'))
1928 cli_main()
1929 settings = Settings()
1930 self.assertEqual(settings.get('a'), 'ant')
1931@@ -1114,9 +1135,7 @@
1932 settings.set('ant', 'insect')
1933 settings.set('bee', 'insect')
1934 settings.set('cat', 'mammal')
1935- self._resources.enter_context(
1936- patch('systemimage.main.sys.argv',
1937- ['argv0', '-C', ini_file, '--del', 'bee']))
1938+ self._resources.enter_context(argv('-C', ini_file, '--del', 'bee'))
1939 cli_main()
1940 settings = Settings()
1941 self.assertEqual(settings.get('ant'), 'insect')
1942@@ -1132,8 +1151,7 @@
1943 settings.set('bee', 'insect')
1944 settings.set('cat', 'mammal')
1945 self._resources.enter_context(
1946- patch('systemimage.main.sys.argv',
1947- ['argv0', '-C', ini_file, '--del', 'bee', '--del', 'cat']))
1948+ argv('-C', ini_file, '--del', 'bee', '--del', 'cat'))
1949 cli_main()
1950 settings = Settings()
1951 self.assertEqual(settings.get('ant'), 'insect')
1952@@ -1145,9 +1163,7 @@
1953 def test_del_missing_key(self, ini_file):
1954 # When asked to delete a key that's not in the database, nothing
1955 # much happens.
1956- self._resources.enter_context(
1957- patch('systemimage.main.sys.argv',
1958- ['argv0', '-C', ini_file, '--del', 'missing']))
1959+ self._resources.enter_context(argv('-C', ini_file, '--del', 'missing'))
1960 cli_main()
1961 self.assertEqual(Settings().get('missing'), '')
1962
1963@@ -1157,12 +1173,10 @@
1964 # mixing and matching database arguments would be arbitrary, it is not
1965 # allowed to mix them.
1966 capture = StringIO()
1967- self._resources.enter_context(
1968- patch('builtins.print', partial(print, file=capture)))
1969- self._resources.enter_context(
1970- patch('systemimage.main.sys.argv',
1971- ['argv0', '-C', ini_file,
1972- '--set', 'c=cat', '--del', 'bee', '--get', 'dog']))
1973+ self._resources.enter_context(capture_print(capture))
1974+ self._resources.enter_context(
1975+ argv('-C', ini_file,
1976+ '--set', 'c=cat', '--del', 'bee', '--get', 'dog'))
1977 with self.assertRaises(SystemExit) as cm:
1978 cli_main()
1979 self.assertEqual(cm.exception.code, 2)
1980
1981=== modified file 'systemimage/tests/test_scores.py'
1982--- systemimage/tests/test_scores.py 2014-02-20 23:03:24 +0000
1983+++ systemimage/tests/test_scores.py 2014-10-29 20:07:23 +0000
1984@@ -14,6 +14,7 @@
1985 # along with this program. If not, see <http://www.gnu.org/licenses/>.
1986
1987 __all__ = [
1988+ 'TestPhasedUpdates',
1989 'TestWeightedScorer',
1990 ]
1991
1992@@ -22,7 +23,8 @@
1993
1994 from systemimage.candidates import get_candidates
1995 from systemimage.scores import WeightedScorer
1996-from systemimage.testing.helpers import get_index
1997+from systemimage.testing.helpers import descriptions, get_index
1998+from unittest.mock import patch
1999
2000
2001 class TestWeightedScorer(unittest.TestCase):
2002@@ -31,7 +33,7 @@
2003
2004 def test_choose_no_candidates(self):
2005 # If there are no candidates, then there is no path to upgrade.
2006- self.assertEqual(self.scorer.choose([]), [])
2007+ self.assertEqual(self.scorer.choose([], 'devel'), [])
2008
2009 def test_score_no_candidates(self):
2010 self.assertEqual(self.scorer.score([]), [])
2011@@ -44,7 +46,7 @@
2012 # The score is 200 for the two extra bootme flags.
2013 self.assertEqual(scores, [200])
2014 # And we upgrade to the only path available.
2015- winner = self.scorer.choose(candidates)
2016+ winner = self.scorer.choose(candidates, 'devel')
2017 # There are two images in the winning path.
2018 self.assertEqual(len(winner), 2)
2019 self.assertEqual([image.version for image in winner], [1300, 1301])
2020@@ -67,15 +69,12 @@
2021 # There are three paths. The scores are as above.
2022 scores = self.scorer.score(candidates)
2023 self.assertEqual(scores, [300, 200, 9401])
2024- winner = self.scorer.choose(candidates)
2025+ winner = self.scorer.choose(candidates, 'devel')
2026 self.assertEqual(len(winner), 3)
2027 self.assertEqual([image.version for image in winner],
2028 [1200, 1201, 1304])
2029- descriptions = []
2030- for image in winner:
2031- # There's only one description per image so order doesn't matter.
2032- descriptions.extend(image.descriptions.values())
2033- self.assertEqual(descriptions, ['Full B', 'Delta B.1', 'Delta B.2'])
2034+ self.assertEqual(descriptions(winner),
2035+ ['Full B', 'Delta B.1', 'Delta B.2'])
2036
2037 def test_tied_candidates(self):
2038 # LP: #1206866 - TypeError when two candidate paths scored equal.
2039@@ -83,6 +82,80 @@
2040 # index_17.json was captured from real data causing the traceback.
2041 index = get_index('index_17.json')
2042 candidates = get_candidates(index, 1)
2043- path = self.scorer.choose(candidates)
2044+ path = self.scorer.choose(candidates, 'devel')
2045 self.assertEqual(len(path), 1)
2046 self.assertEqual(path[0].version, 1800)
2047+
2048+
2049+class TestPhasedUpdates(unittest.TestCase):
2050+ def setUp(self):
2051+ self.scorer = WeightedScorer()
2052+
2053+ def test_inside_phase_gets_update(self):
2054+ # When the final image on an update path has a phase percentage higher
2055+ # than the device percentage, the candidate path is okay. In this
2056+ # case, the `Full B` has phase of 50%.
2057+ index = get_index('index_22.json')
2058+ candidates = get_candidates(index, 100)
2059+ with patch('systemimage.scores.phased_percentage', return_value=22):
2060+ winner = self.scorer.choose(candidates, 'devel')
2061+ descriptions = []
2062+ for image in winner:
2063+ descriptions.extend(image.descriptions.values())
2064+ self.assertEqual(descriptions, ['Full B', 'Delta B.1', 'Delta B.2'])
2065+
2066+ def test_outside_phase_gets_update(self):
2067+ # When the final image on an update path has a phase percentage lower
2068+ # than the device percentage, the scorer falls back to the next
2069+ # candidate path.
2070+ index = get_index('index_22.json')
2071+ candidates = get_candidates(index, 100)
2072+ with patch('systemimage.scores.phased_percentage', return_value=66):
2073+ winner = self.scorer.choose(candidates, 'devel')
2074+ self.assertEqual(descriptions(winner),
2075+ ['Full A', 'Delta A.1', 'Delta A.2'])
2076+
2077+ def test_equal_phase_gets_update(self):
2078+ # When the final image on an update path has a phase percentage exactly
2079+ # equal to the device percentage, the candidate path is okay. In this
2080+ # case, the `Full B` has phase of 50%.
2081+ index = get_index('index_22.json')
2082+ candidates = get_candidates(index, 100)
2083+ with patch('systemimage.scores.phased_percentage', return_value=50):
2084+ winner = self.scorer.choose(candidates, 'devel')
2085+ self.assertEqual(descriptions(winner),
2086+ ['Full B', 'Delta B.1', 'Delta B.2'])
2087+
2088+ def test_pulled_update(self):
2089+ # When the final image on an update path has a phase percentage of
2090+ # zero, then regardless of the device's percentage, the candidate path
2091+ # is not okay. In this case, the `Full B` has phase of 0%.
2092+ index = get_index('index_26.json')
2093+ candidates = get_candidates(index, 100)
2094+ with patch('systemimage.scores.phased_percentage', return_value=0):
2095+ winner = self.scorer.choose(candidates, 'devel')
2096+ self.assertEqual(descriptions(winner),
2097+ ['Full A', 'Delta A.1', 'Delta A.2'])
2098+
2099+ def test_pulled_update_insanely_negative_randint(self):
2100+ # When the final image on an update path has a phase percentage of
2101+ # zero, then regardless of the device's percentage (even if randint
2102+ # returned some insane value), the candidate path is not okay. In this
2103+ # case, the `Full B` has phase of 0%.
2104+ index = get_index('index_26.json')
2105+ candidates = get_candidates(index, 100)
2106+ with patch('systemimage.scores.phased_percentage', return_value=-100):
2107+ winner = self.scorer.choose(candidates, 'devel')
2108+ self.assertEqual(descriptions(winner),
2109+ ['Full A', 'Delta A.1', 'Delta A.2'])
2110+
2111+ def test_pulled_update_insanely_positive_randint(self):
2112+ # When the final image on an update path has a phase percentage of
2113+ # zero, then regardless of the device's percentage (even if randint
2114+ # returned some insane value), the candidate path is not okay. In this
2115+ # case, the `Full B` has phase of 0%.
2116+ index = get_index('index_26.json')
2117+ candidates = get_candidates(index, 100)
2118+ with patch('systemimage.scores.phased_percentage', return_value=1000):
2119+ winner = self.scorer.choose(candidates, 'devel')
2120+ self.assertEqual(len(winner), 0)
2121
2122=== modified file 'systemimage/tests/test_state.py'
2123--- systemimage/tests/test_state.py 2014-09-17 13:41:31 +0000
2124+++ systemimage/tests/test_state.py 2014-10-29 20:07:23 +0000
2125@@ -48,12 +48,10 @@
2126 from systemimage.state import ChecksumError, State
2127 from systemimage.testing.demo import DemoDevice
2128 from systemimage.testing.helpers import (
2129- ServerTestBase, configuration, copy, data_path, get_index,
2130+ ServerTestBase, configuration, copy, data_path, descriptions, get_index,
2131 make_http_server, setup_keyring_txz, setup_keyrings, sign,
2132 temporary_directory, touch_build)
2133 from systemimage.testing.nose import SystemImagePlugin
2134-# FIXME
2135-from systemimage.tests.test_candidates import _descriptions
2136 from unittest.mock import call, patch
2137
2138 BAD_SIGNATURE = 'f' * 64
2139@@ -939,25 +937,59 @@
2140 INDEX_FILE = 'index_22.json'
2141
2142 @configuration
2143- def test_phased_updates(self):
2144- # With our threshold at 66, the "Full B" image is suppressed, thus the
2145- # upgrade path is different than it normally would be. In this case,
2146- # the 'A' path is taken (in fact, the B path isn't even considered).
2147- self._setup_server_keyrings()
2148- config.channel = 'daily'
2149- state = State()
2150- self._resources.enter_context(
2151- patch('systemimage.index.phased_percentage', return_value=66))
2152- # Do not use self._resources to manage the check_output mock. Because
2153- # of the nesting order of the @configuration decorator and the base
2154- # class's tearDown(), using self._resources causes the mocks to be
2155- # unwound in the wrong order, affecting future tests.
2156- with patch('systemimage.device.check_output', return_value='manta'):
2157- state.run_thru('calculate_winner')
2158- self.assertEqual(_descriptions(state.winner),
2159+ def test_inside_phased_updates_0(self):
2160+ # With our threshold at 22, the normal upgrade to "Full B" image is ok.
2161+ self._setup_server_keyrings()
2162+ config.channel = 'daily'
2163+ state = State()
2164+ self._resources.enter_context(
2165+ patch('systemimage.scores.phased_percentage', return_value=22))
2166+ # Do not use self._resources to manage the check_output mock. Because
2167+ # of the nesting order of the @configuration decorator and the base
2168+ # class's tearDown(), using self._resources causes the mocks to be
2169+ # unwound in the wrong order, affecting future tests.
2170+ with patch('systemimage.device.check_output', return_value='manta'):
2171+ state.run_thru('calculate_winner')
2172+ self.assertEqual(descriptions(state.winner),
2173+ ['Full B', 'Delta B.1', 'Delta B.2'])
2174+
2175+ @configuration
2176+ def test_outside_phased_updates(self):
2177+ # With our threshold at 66, the normal upgrade to "Full B" image is
2178+ # discarded, and the previous Full A update is chosen instead.
2179+ self._setup_server_keyrings()
2180+ config.channel = 'daily'
2181+ state = State()
2182+ self._resources.enter_context(
2183+ patch('systemimage.scores.phased_percentage', return_value=66))
2184+ # Do not use self._resources to manage the check_output mock. Because
2185+ # of the nesting order of the @configuration decorator and the base
2186+ # class's tearDown(), using self._resources causes the mocks to be
2187+ # unwound in the wrong order, affecting future tests.
2188+ with patch('systemimage.device.check_output', return_value='manta'):
2189+ state.run_thru('calculate_winner')
2190+ self.assertEqual(descriptions(state.winner),
2191 ['Full A', 'Delta A.1', 'Delta A.2'])
2192
2193 @configuration
2194+ def test_equal_phased_updates_0(self):
2195+ # With our threshold at 50, i.e. exactly equal to the image's
2196+ # percentage, the normal upgrade to "Full B" image is ok.
2197+ self._setup_server_keyrings()
2198+ config.channel = 'daily'
2199+ state = State()
2200+ self._resources.enter_context(
2201+ patch('systemimage.scores.phased_percentage', return_value=50))
2202+ # Do not use self._resources to manage the check_output mock. Because
2203+ # of the nesting order of the @configuration decorator and the base
2204+ # class's tearDown(), using self._resources causes the mocks to be
2205+ # unwound in the wrong order, affecting future tests.
2206+ with patch('systemimage.device.check_output', return_value='manta'):
2207+ state.run_thru('calculate_winner')
2208+ self.assertEqual(descriptions(state.winner),
2209+ ['Full B', 'Delta B.1', 'Delta B.2'])
2210+
2211+ @configuration
2212 def test_phased_updates_0(self):
2213 # With our threshold at 0, all images are good, so it's a "normal"
2214 # update path.
2215@@ -965,33 +997,95 @@
2216 config.channel = 'daily'
2217 state = State()
2218 self._resources.enter_context(
2219- patch('systemimage.index.phased_percentage', return_value=0))
2220+ patch('systemimage.scores.phased_percentage', return_value=0))
2221 # Do not use self._resources to manage the check_output mock. Because
2222 # of the nesting order of the @configuration decorator and the base
2223 # class's tearDown(), using self._resources causes the mocks to be
2224 # unwound in the wrong order, affecting future tests.
2225 with patch('systemimage.device.check_output', return_value='manta'):
2226 state.run_thru('calculate_winner')
2227- self.assertEqual(_descriptions(state.winner),
2228+ self.assertEqual(descriptions(state.winner),
2229 ['Full B', 'Delta B.1', 'Delta B.2'])
2230
2231 @configuration
2232 def test_phased_updates_100(self):
2233- # With our threshold at 100, only the image without a specific
2234- # phased-percentage key is allowed. That's the 'A' path again.
2235- self._setup_server_keyrings()
2236- config.channel = 'daily'
2237- state = State()
2238- self._resources.enter_context(
2239- patch('systemimage.index.phased_percentage', return_value=77))
2240- # Do not use self._resources to manage the check_output mock. Because
2241- # of the nesting order of the @configuration decorator and the base
2242- # class's tearDown(), using self._resources causes the mocks to be
2243- # unwound in the wrong order, affecting future tests.
2244- with patch('systemimage.device.check_output', return_value='manta'):
2245- state.run_thru('calculate_winner')
2246- self.assertEqual(_descriptions(state.winner),
2247- ['Full A', 'Delta A.1', 'Delta A.2'])
2248+ # With our threshold at 100, the "Full B" image is discarded and the
2249+ # backup "Full A" image is chosen.
2250+ self._setup_server_keyrings()
2251+ config.channel = 'daily'
2252+ state = State()
2253+ self._resources.enter_context(
2254+ patch('systemimage.scores.phased_percentage', return_value=77))
2255+ # Do not use self._resources to manage the check_output mock. Because
2256+ # of the nesting order of the @configuration decorator and the base
2257+ # class's tearDown(), using self._resources causes the mocks to be
2258+ # unwound in the wrong order, affecting future tests.
2259+ with patch('systemimage.device.check_output', return_value='manta'):
2260+ state.run_thru('calculate_winner')
2261+ self.assertEqual(descriptions(state.winner),
2262+ ['Full A', 'Delta A.1', 'Delta A.2'])
2263+
2264+
2265+class TestPhasedUpdatesPulled(ServerTestBase):
2266+ CHANNEL_FILE = 'channels_10.json'
2267+ CHANNEL = 'daily'
2268+ DEVICE = 'manta'
2269+ INDEX_FILE = 'index_26.json'
2270+
2271+ @configuration
2272+ def test_pulled_update(self):
2273+ # Regardless of the device's phase percentage, when the image has a
2274+ # percentage of 0, it will never be considered. In this case Full B
2275+ # has a phased percentage of 0, so the fallback Full A is chosen.
2276+ self._setup_server_keyrings()
2277+ config.channel = 'daily'
2278+ state = State()
2279+ self._resources.enter_context(
2280+ patch('systemimage.scores.phased_percentage', return_value=0))
2281+ # Do not use self._resources to manage the check_output mock. Because
2282+ # of the nesting order of the @configuration decorator and the base
2283+ # class's tearDown(), using self._resources causes the mocks to be
2284+ # unwound in the wrong order, affecting future tests.
2285+ with patch('systemimage.device.check_output', return_value='manta'):
2286+ state.run_thru('calculate_winner')
2287+ self.assertEqual(descriptions(state.winner),
2288+ ['Full A', 'Delta A.1', 'Delta A.2'])
2289+
2290+ @configuration
2291+ def test_pulled_update_insanely_negative_randint(self):
2292+ # Regardless of the device's phase percentage, when the image has a
2293+ # percentage of 0, it will never be considered. In this case Full B
2294+ # has a phased percentage of 0, so the fallback Full A is chosen.
2295+ self._setup_server_keyrings()
2296+ config.channel = 'daily'
2297+ state = State()
2298+ self._resources.enter_context(
2299+ patch('systemimage.scores.phased_percentage', return_value=-100))
2300+ # Do not use self._resources to manage the check_output mock. Because
2301+ # of the nesting order of the @configuration decorator and the base
2302+ # class's tearDown(), using self._resources causes the mocks to be
2303+ # unwound in the wrong order, affecting future tests.
2304+ with patch('systemimage.device.check_output', return_value='manta'):
2305+ state.run_thru('calculate_winner')
2306+ self.assertEqual(descriptions(state.winner),
2307+ ['Full A', 'Delta A.1', 'Delta A.2'])
2308+
2309+ @configuration
2310+ def test_pulled_update_insanely_positive_randint(self):
2311+ # Regardless of the device's phase percentage, when the image has a
2312+ # percentage of 0, it will never be considered.
2313+ self._setup_server_keyrings()
2314+ config.channel = 'daily'
2315+ state = State()
2316+ self._resources.enter_context(
2317+ patch('systemimage.scores.phased_percentage', return_value=1000))
2318+ # Do not use self._resources to manage the check_output mock. Because
2319+ # of the nesting order of the @configuration decorator and the base
2320+ # class's tearDown(), using self._resources causes the mocks to be
2321+ # unwound in the wrong order, affecting future tests.
2322+ with patch('systemimage.device.check_output', return_value='manta'):
2323+ state.run_thru('calculate_winner')
2324+ self.assertEqual(len(state.winner), 0)
2325
2326
2327 class TestCachedFiles(ServerTestBase):
2328
2329=== modified file 'systemimage/tests/test_winner.py'
2330--- systemimage/tests/test_winner.py 2014-07-23 22:51:19 +0000
2331+++ systemimage/tests/test_winner.py 2014-10-29 20:07:23 +0000
2332@@ -28,7 +28,7 @@
2333 from systemimage.config import config
2334 from systemimage.gpg import SignatureError
2335 from systemimage.helpers import temporary_directory
2336-from systemimage.state import ChecksumError, State
2337+from systemimage.state import State
2338 from systemimage.testing.helpers import (
2339 configuration, copy, make_http_server, setup_index, setup_keyring_txz,
2340 setup_keyrings, sign, touch_build)
2341
2342=== modified file 'systemimage/version.txt'
2343--- systemimage/version.txt 2014-09-26 14:36:34 +0000
2344+++ systemimage/version.txt 2014-10-29 20:07:23 +0000
2345@@ -1,1 +1,1 @@
2346-2.5
2347+2.5.1
2348
2349=== added file 'tools/runme.sh'
2350--- tools/runme.sh 1970-01-01 00:00:00 +0000
2351+++ tools/runme.sh 2014-10-29 20:07:23 +0000
2352@@ -0,0 +1,10 @@
2353+where=udm/build
2354+root=$HOME/projects/phone/${where}/src/downloads/daemon
2355+logfile=$HOME/.cache/ubuntu-download-manager/ubuntu-download-manager.INFO
2356+# export GLOG_logtostderr=1
2357+# export GLOG_v=100
2358+echo -n `date --rfc-3339=ns` >> ${logfile}
2359+echo -n " " >> ${logfile}
2360+echo $* >> ${logfile}
2361+#exec env -u DBUS_SESSION_BUS_ADDRESS ${root}/ubuntu-download-manager $*
2362+exec ${root}/ubuntu-download-manager $*
2363
2364=== modified file 'tox.ini'
2365--- tox.ini 2014-09-17 13:41:31 +0000
2366+++ tox.ini 2014-10-29 20:07:23 +0000
2367@@ -5,6 +5,7 @@
2368 [testenv]
2369 commands = python -m nose2 -v
2370 sitepackages = True
2371+usedevelop=True
2372 setenv =
2373 SYSTEMIMAGE_REACTOR_TIMEOUT=60
2374

Subscribers

People subscribed via source and target branches