Merge lp:~ubuntu-managed-branches/ubuntu-system-image/251-0u1 into lp:~ubuntu-managed-branches/ubuntu-system-image/system-image
- 251-0u1
- Merge into system-image
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 |
Related bugs: |
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-
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
- 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
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 |