~ubuntu-managed-branches/ubuntu-system-image/system-image

227.1.3 by Barry Warsaw
Bump copyright years.
1
# Copyright (C) 2013-2014 Canonical Ltd.
13 by Barry Warsaw
* get_candidates() returns the set of all candidate upgrades, without regard
2
# Author: Barry Warsaw <barry@ubuntu.com>
3
4
# This program is free software: you can redistribute it and/or modify
5
# it under the terms of the GNU General Public License as published by
6
# the Free Software Foundation; version 3 of the License.
7
#
8
# This program is distributed in the hope that it will be useful,
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
# GNU General Public License for more details.
12
#
13
# You should have received a copy of the GNU General Public License
14
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
15
16
"""Test the candidate upgrade path algorithm."""
17
18
__all__ = [
150 by Barry Warsaw
* Add `system-image-cli --filter` option to allow for forcing full or delta
19
    'TestCandidateDownloads',
20
    'TestCandidateFilters',
13 by Barry Warsaw
* get_candidates() returns the set of all candidate upgrades, without regard
21
    'TestCandidates',
155.1.1 by Barry Warsaw
* Support the new version number regime, which uses sequential version
22
    'TestNewVersionRegime',
13 by Barry Warsaw
* get_candidates() returns the set of all candidate upgrades, without regard
23
    ]
24
25
26
import unittest
27
22.1.5 by Barry Warsaw
Add a bunch of candidate tests for various index files.
28
from operator import attrgetter
150 by Barry Warsaw
* Add `system-image-cli --filter` option to allow for forcing full or delta
29
from systemimage.candidates import (
30
    delta_filter, full_filter, get_candidates, iter_path)
67 by Barry Warsaw
* Fix distutils packaging bugs exposed by Debian packaging work.
31
from systemimage.scores import WeightedScorer
136.1.1 by Barry Warsaw
* Use nose as the test runner. This allows us to pre-initialize the logging
32
from systemimage.testing.helpers import configuration, get_index
13 by Barry Warsaw
* get_candidates() returns the set of all candidate upgrades, without regard
33
34
155.1.1 by Barry Warsaw
* Support the new version number regime, which uses sequential version
35
def _descriptions(path):
36
    descriptions = []
37
    for image in path:
38
        # There's only one description per image so order doesn't
39
        # matter.
40
        descriptions.extend(image.descriptions.values())
41
    return descriptions
42
43
13 by Barry Warsaw
* get_candidates() returns the set of all candidate upgrades, without regard
44
class TestCandidates(unittest.TestCase):
22.1.4 by Barry Warsaw
Remove old style JSON indexes.
45
    def test_no_images(self):
46
        # If there are no images defined, there are no candidates.
47
        index = get_index('index_01.json')
201 by Barry Warsaw
* Remove support for old version numbers. (LP: #1220238)
48
        candidates = get_candidates(index, 1400)
22.1.5 by Barry Warsaw
Add a bunch of candidate tests for various index files.
49
        self.assertEqual(candidates, [])
22.1.4 by Barry Warsaw
Remove old style JSON indexes.
50
51
    def test_only_higher_fulls(self):
52
        # All the full images have a minversion greater than our version, so
53
        # we cannot upgrade to any of them.
22.1.5 by Barry Warsaw
Add a bunch of candidate tests for various index files.
54
        index = get_index('index_02.json')
201 by Barry Warsaw
* Remove support for old version numbers. (LP: #1220238)
55
        candidates = get_candidates(index, 100)
22.1.5 by Barry Warsaw
Add a bunch of candidate tests for various index files.
56
        self.assertEqual(candidates, [])
57
58
    def test_one_higher_full(self):
59
        # Our device is between the minversions of the two available fulls, so
60
        # the older one can be upgraded too.
61
        index = get_index('index_02.json')
201 by Barry Warsaw
* Remove support for old version numbers. (LP: #1220238)
62
        candidates = get_candidates(index, 800)
22.1.5 by Barry Warsaw
Add a bunch of candidate tests for various index files.
63
        # There is exactly one upgrade path.
64
        self.assertEqual(len(candidates), 1)
65
        path = candidates[0]
66
        # The path has exactly one image.
67
        self.assertEqual(len(path), 1)
68
        image = path[0]
69.1.6 by Barry Warsaw
Added an Update class, an instance of which is the thing that
69
        self.assertEqual(list(image.descriptions.values()),
70
                         ['New full build 1'])
22.1.5 by Barry Warsaw
Add a bunch of candidate tests for various index files.
71
72
    def test_fulls_with_no_minversion(self):
73
        # Like the previous test, there are two full upgrades, but because
74
        # neither of them have minversions, both are candidates.
75
        index = get_index('index_05.json')
201 by Barry Warsaw
* Remove support for old version numbers. (LP: #1220238)
76
        candidates = get_candidates(index, 400)
22.1.5 by Barry Warsaw
Add a bunch of candidate tests for various index files.
77
        self.assertEqual(len(candidates), 2)
78
        # Both candidate paths have exactly one image in them.  We can't sort
79
        # these paths, so just test them both.
80
        path0, path1 = candidates
81
        self.assertEqual(len(path0), 1)
82
        self.assertEqual(len(path1), 1)
201 by Barry Warsaw
* Remove support for old version numbers. (LP: #1220238)
83
        # One path gets us to version 1300 and the other 1400.
22.1.5 by Barry Warsaw
Add a bunch of candidate tests for various index files.
84
        images = sorted([path0[0], path1[0]], key=attrgetter('version'))
69.1.6 by Barry Warsaw
Added an Update class, an instance of which is the thing that
85
        self.assertEqual(list(images[0].descriptions.values()),
86
                         ['New full build 1'])
87
        self.assertEqual(list(images[1].descriptions.values()),
88
                         ['New full build 2'])
22.1.5 by Barry Warsaw
Add a bunch of candidate tests for various index files.
89
90
    def test_no_deltas_based_on_us(self):
91
        # There are deltas in the test data, but no fulls.  None of the deltas
92
        # have a base equal to our build number.
93
        index = get_index('index_03.json')
201 by Barry Warsaw
* Remove support for old version numbers. (LP: #1220238)
94
        candidates = get_candidates(index, 100)
22.1.5 by Barry Warsaw
Add a bunch of candidate tests for various index files.
95
        self.assertEqual(candidates, [])
96
97
    def test_one_delta_based_on_us(self):
98
        # There is one delta in the test data that is based on us.
99
        index = get_index('index_03.json')
201 by Barry Warsaw
* Remove support for old version numbers. (LP: #1220238)
100
        candidates = get_candidates(index, 500)
22.1.5 by Barry Warsaw
Add a bunch of candidate tests for various index files.
101
        self.assertEqual(len(candidates), 1)
102
        path = candidates[0]
103
        # The path has exactly one image.
104
        self.assertEqual(len(path), 1)
105
        image = path[0]
69.1.6 by Barry Warsaw
Added an Update class, an instance of which is the thing that
106
        self.assertEqual(list(image.descriptions.values()), ['Delta 2'])
22.1.5 by Barry Warsaw
Add a bunch of candidate tests for various index files.
107
108
    def test_two_deltas_based_on_us(self):
109
        # There are two deltas that are based on us, so both are candidates.
110
        # They get us to different final versions.
111
        index = get_index('index_04.json')
201 by Barry Warsaw
* Remove support for old version numbers. (LP: #1220238)
112
        candidates = get_candidates(index, 1100)
22.1.5 by Barry Warsaw
Add a bunch of candidate tests for various index files.
113
        self.assertEqual(len(candidates), 2)
114
        # Both candidate paths have exactly one image in them.  We can't sort
115
        # these paths, so just test them both.
116
        path0, path1 = candidates
117
        self.assertEqual(len(path0), 1)
118
        self.assertEqual(len(path1), 1)
201 by Barry Warsaw
* Remove support for old version numbers. (LP: #1220238)
119
        # One path gets us to version 1300 and the other 1400.
22.1.5 by Barry Warsaw
Add a bunch of candidate tests for various index files.
120
        images = sorted([path0[0], path1[0]], key=attrgetter('version'))
155.1.1 by Barry Warsaw
* Support the new version number regime, which uses sequential version
121
        self.assertEqual(_descriptions(images), ['Delta 2', 'Delta 1'])
22.1.6 by Barry Warsaw
Tests for various other upgrade path cases.
122
123
    def test_one_path_with_full_and_deltas(self):
124
        # There's one path to upgrade from our version to the final version.
125
        # This one starts at a full and includes several deltas.
126
        index = get_index('index_06.json')
201 by Barry Warsaw
* Remove support for old version numbers. (LP: #1220238)
127
        candidates = get_candidates(index, 1000)
22.1.6 by Barry Warsaw
Tests for various other upgrade path cases.
128
        self.assertEqual(len(candidates), 1)
129
        path = candidates[0]
130
        self.assertEqual(len(path), 3)
131
        self.assertEqual([image.version for image in path],
201 by Barry Warsaw
* Remove support for old version numbers. (LP: #1220238)
132
                         [1300, 1301, 1302])
155.1.1 by Barry Warsaw
* Support the new version number regime, which uses sequential version
133
        self.assertEqual(_descriptions(path), ['Full 1', 'Delta 1', 'Delta 2'])
22.1.6 by Barry Warsaw
Tests for various other upgrade path cases.
134
135
    def test_one_path_with_deltas(self):
136
        # Similar to above, except that because we're upgrading from the
137
        # version of the full, the path is only two images long, i.e. the
138
        # deltas.
139
        index = get_index('index_06.json')
201 by Barry Warsaw
* Remove support for old version numbers. (LP: #1220238)
140
        candidates = get_candidates(index, 1300)
22.1.6 by Barry Warsaw
Tests for various other upgrade path cases.
141
        self.assertEqual(len(candidates), 1)
142
        path = candidates[0]
143
        self.assertEqual(len(path), 2)
201 by Barry Warsaw
* Remove support for old version numbers. (LP: #1220238)
144
        self.assertEqual([image.version for image in path], [1301, 1302])
155.1.1 by Barry Warsaw
* Support the new version number regime, which uses sequential version
145
        self.assertEqual(_descriptions(path), ['Delta 1', 'Delta 2'])
22.1.6 by Barry Warsaw
Tests for various other upgrade path cases.
146
147
    def test_forked_paths(self):
148
        # We have a fork in the road.  There is a full update, but two deltas
149
        # with different versions point to the same base.  This will give us
150
        # two upgrade paths, both of which include the full.
151
        index = get_index('index_07.json')
201 by Barry Warsaw
* Remove support for old version numbers. (LP: #1220238)
152
        candidates = get_candidates(index, 1200)
22.1.6 by Barry Warsaw
Tests for various other upgrade path cases.
153
        self.assertEqual(len(candidates), 2)
154
        # We can sort the paths by length.
155
        paths = sorted(candidates, key=len)
201 by Barry Warsaw
* Remove support for old version numbers. (LP: #1220238)
156
        # The shortest path gets us to 1302 in two steps.
22.1.6 by Barry Warsaw
Tests for various other upgrade path cases.
157
        self.assertEqual(len(paths[0]), 2)
201 by Barry Warsaw
* Remove support for old version numbers. (LP: #1220238)
158
        self.assertEqual([image.version for image in paths[0]], [1300, 1302])
69.1.6 by Barry Warsaw
Added an Update class, an instance of which is the thing that
159
        descriptions = []
160
        for image in paths[0]:
161
            # There's only one description per image so order doesn't matter.
162
            descriptions.extend(image.descriptions.values())
163
        self.assertEqual(descriptions, ['Full 1', 'Delta 2'])
201 by Barry Warsaw
* Remove support for old version numbers. (LP: #1220238)
164
        # The longer path gets us to 1302 in three steps.
22.1.6 by Barry Warsaw
Tests for various other upgrade path cases.
165
        self.assertEqual(len(paths[1]), 3)
166
        self.assertEqual([image.version for image in paths[1]],
201 by Barry Warsaw
* Remove support for old version numbers. (LP: #1220238)
167
                         [1300, 1301, 1302])
69.1.6 by Barry Warsaw
Added an Update class, an instance of which is the thing that
168
        descriptions = []
169
        for image in paths[1]:
170
            # There's only one description per image so order doesn't matter.
171
            descriptions.extend(image.descriptions.values())
172
        self.assertEqual(descriptions, ['Full 1', 'Delta 1', 'Delta 3'])
26 by Barry Warsaw
Calculate the downloadable files, taking into account both the signature and
173
174
175
class TestCandidateDownloads(unittest.TestCase):
60 by Barry Warsaw
* Verify the checksums of the files that were downloaded.
176
    maxDiff = None
26 by Barry Warsaw
Calculate the downloadable files, taking into account both the signature and
177
136.1.1 by Barry Warsaw
* Use nose as the test runner. This allows us to pre-initialize the logging
178
    @configuration
26 by Barry Warsaw
Calculate the downloadable files, taking into account both the signature and
179
    def test_get_downloads(self):
180
        # Path B will win; it has one full and two deltas, none of which have
181
        # a bootme flag.  Download all their files.
182
        index = get_index('index_10.json')
201 by Barry Warsaw
* Remove support for old version numbers. (LP: #1220238)
183
        candidates = get_candidates(index, 600)
26 by Barry Warsaw
Calculate the downloadable files, taking into account both the signature and
184
        winner = WeightedScorer().choose(candidates)
69.1.6 by Barry Warsaw
Added an Update class, an instance of which is the thing that
185
        descriptions = []
186
        for image in winner:
187
            # There's only one description per image so order doesn't matter.
188
            descriptions.extend(image.descriptions.values())
189
        self.assertEqual(descriptions, ['Full B', 'Delta B.1', 'Delta B.2'])
77 by Barry Warsaw
get_downloads() -> iter_path()
190
        downloads = list(iter_path(winner))
78 by Barry Warsaw
* Fix ubuntu_command file ordering. (LP: #1199986)
191
        paths = set(filerec.path for (n, filerec) in downloads)
60 by Barry Warsaw
* Verify the checksums of the files that were downloaded.
192
        self.assertEqual(paths, set([
43 by Barry Warsaw
- Update TODO list with priorities.
193
            '/3/4/5.txt',
194
            '/4/5/6.txt',
195
            '/5/6/7.txt',
196
            '/6/7/8.txt',
197
            '/7/8/9.txt',
198
            '/8/9/a.txt',
199
            '/9/a/b.txt',
60 by Barry Warsaw
* Verify the checksums of the files that were downloaded.
200
            '/e/d/c.txt',
201
            '/f/e/d.txt',
202
            ]))
78 by Barry Warsaw
* Fix ubuntu_command file ordering. (LP: #1199986)
203
        signatures = set(filerec.signature for (n, filerec) in downloads)
60 by Barry Warsaw
* Verify the checksums of the files that were downloaded.
204
        self.assertEqual(signatures, set([
205
            '/3/4/5.txt.asc',
206
            '/4/5/6.txt.asc',
207
            '/5/6/7.txt.asc',
208
            '/6/7/8.txt.asc',
209
            '/7/8/9.txt.asc',
210
            '/8/9/a.txt.asc',
43 by Barry Warsaw
- Update TODO list with priorities.
211
            '/9/a/b.txt.asc',
212
            '/e/d/c.txt.asc',
213
            '/f/e/d.txt.asc',
26 by Barry Warsaw
Calculate the downloadable files, taking into account both the signature and
214
            ]))
215
136.1.1 by Barry Warsaw
* Use nose as the test runner. This allows us to pre-initialize the logging
216
    @configuration
26 by Barry Warsaw
Calculate the downloadable files, taking into account both the signature and
217
    def test_get_downloads_with_bootme(self):
218
        # Path B will win; it has one full and two deltas.  The first delta
219
        # has a bootme flag so the second delta's files are not downloaded.
220
        index = get_index('index_11.json')
201 by Barry Warsaw
* Remove support for old version numbers. (LP: #1220238)
221
        candidates = get_candidates(index, 600)
26 by Barry Warsaw
Calculate the downloadable files, taking into account both the signature and
222
        winner = WeightedScorer().choose(candidates)
69.1.6 by Barry Warsaw
Added an Update class, an instance of which is the thing that
223
        descriptions = []
224
        for image in winner:
225
            # There's only one description per image so order doesn't matter.
226
            descriptions.extend(image.descriptions.values())
227
        self.assertEqual(descriptions, ['Full B', 'Delta B.1', 'Delta B.2'])
77 by Barry Warsaw
get_downloads() -> iter_path()
228
        downloads = iter_path(winner)
78 by Barry Warsaw
* Fix ubuntu_command file ordering. (LP: #1199986)
229
        paths = set(filerec.path for (n, filerec) in downloads)
60 by Barry Warsaw
* Verify the checksums of the files that were downloaded.
230
        self.assertEqual(paths, set([
43 by Barry Warsaw
- Update TODO list with priorities.
231
            '/3/4/5.txt',
232
            '/4/5/6.txt',
233
            '/5/6/7.txt',
234
            '/6/7/8.txt',
235
            '/7/8/9.txt',
236
            '/8/9/a.txt',
26 by Barry Warsaw
Calculate the downloadable files, taking into account both the signature and
237
            ]))
150 by Barry Warsaw
* Add `system-image-cli --filter` option to allow for forcing full or delta
238
239
240
class TestCandidateFilters(unittest.TestCase):
241
    def test_filter_for_fulls(self):
242
        # Run a filter over the candidates, such that the only ones left are
243
        # those that contain only full upgrades.  This can truncate any paths
244
        # that start with some fulls and then contain some deltas.
245
        index = get_index('index_10.json')
201 by Barry Warsaw
* Remove support for old version numbers. (LP: #1220238)
246
        candidates = get_candidates(index, 600)
150 by Barry Warsaw
* Add `system-image-cli --filter` option to allow for forcing full or delta
247
        filtered = full_filter(candidates)
248
        # Since all images start with a full update, we're still left with
249
        # three candidates.
250
        self.assertEqual(len(filtered), 3)
251
        self.assertEqual([image.type for image in filtered[0]], ['full'])
252
        self.assertEqual([image.type for image in filtered[1]], ['full'])
253
        self.assertEqual([image.type for image in filtered[2]], ['full'])
254
        self.assertEqual(_descriptions(filtered[0]), ['Full A'])
255
        self.assertEqual(_descriptions(filtered[1]), ['Full B'])
256
        self.assertEqual(_descriptions(filtered[2]), ['Full C'])
257
258
    def test_filter_for_fulls_one_candidate(self):
259
        # Filter for full updates, where the only candidate has one full image.
260
        index = get_index('index_13.json')
201 by Barry Warsaw
* Remove support for old version numbers. (LP: #1220238)
261
        candidates = get_candidates(index, 600)
150 by Barry Warsaw
* Add `system-image-cli --filter` option to allow for forcing full or delta
262
        filtered = full_filter(candidates)
263
        self.assertEqual(filtered, candidates)
264
265
    def test_filter_for_fulls_with_just_delta_candidates(self):
266
        # A candidate path that contains only deltas will have no filtered
267
        # paths if all the images are delta updates.
268
        index = get_index('index_15.json')
201 by Barry Warsaw
* Remove support for old version numbers. (LP: #1220238)
269
        candidates = get_candidates(index, 100)
150 by Barry Warsaw
* Add `system-image-cli --filter` option to allow for forcing full or delta
270
        self.assertEqual(len(candidates), 1)
271
        filtered = full_filter(candidates)
272
        self.assertEqual(len(filtered), 0)
273
274
    def test_filter_for_deltas(self):
275
        # Filter the candidates, where the only available path is a delta path.
276
        index = get_index('index_15.json')
201 by Barry Warsaw
* Remove support for old version numbers. (LP: #1220238)
277
        candidates = get_candidates(index, 100)
150 by Barry Warsaw
* Add `system-image-cli --filter` option to allow for forcing full or delta
278
        self.assertEqual(len(candidates), 1)
279
        filtered = delta_filter(candidates)
280
        self.assertEqual(len(filtered), 1)
281
        self.assertEqual(candidates, filtered)
282
283
    def test_filter_for_deltas_none_available(self):
284
        # Run a filter over the candidates, such that the only ones left are
285
        # those that start with and contain only deltas.  Since none of the
286
        # paths do so, tere are no candidates left.
287
        index = get_index('index_10.json')
201 by Barry Warsaw
* Remove support for old version numbers. (LP: #1220238)
288
        candidates = get_candidates(index, 600)
150 by Barry Warsaw
* Add `system-image-cli --filter` option to allow for forcing full or delta
289
        filtered = delta_filter(candidates)
290
        self.assertEqual(len(filtered), 0)
291
292
    def test_filter_for_deltas_one_candidate(self):
293
        # Filter for delta updates, but the only candidate is a full.
294
        index = get_index('index_13.json')
201 by Barry Warsaw
* Remove support for old version numbers. (LP: #1220238)
295
        candidates = get_candidates(index, 600)
150 by Barry Warsaw
* Add `system-image-cli --filter` option to allow for forcing full or delta
296
        filtered = delta_filter(candidates)
297
        self.assertEqual(len(filtered), 0)
298
299
    def test_filter_for_multiple_deltas(self):
300
        # The candidate path has multiple deltas.  All are preserved.
301
        index = get_index('index_19.json')
201 by Barry Warsaw
* Remove support for old version numbers. (LP: #1220238)
302
        candidates = get_candidates(index, 100)
150 by Barry Warsaw
* Add `system-image-cli --filter` option to allow for forcing full or delta
303
        filtered = delta_filter(candidates)
304
        self.assertEqual(len(filtered), 1)
305
        path = filtered[0]
306
        self.assertEqual(len(path), 3)
307
        self.assertEqual(_descriptions(path),
308
                         ['Delta A', 'Delta B', 'Delta C'])
155.1.1 by Barry Warsaw
* Support the new version number regime, which uses sequential version
309
310
311
class TestNewVersionRegime(unittest.TestCase):
312
    """LP: #1218612"""
313
314
    def test_candidates(self):
315
        # Path B will win; it has one full and two deltas.
316
        index = get_index('index_20.json')
317
        candidates = get_candidates(index, 0)
318
        self.assertEqual(len(candidates), 3)
319
        path0 = candidates[0]
320
        self.assertEqual(_descriptions(path0),
321
                         ['Full A', 'Delta A.1', 'Delta A.2'])
322
        path1 = candidates[1]
323
        self.assertEqual(_descriptions(path1),
324
                         ['Full B', 'Delta B.1', 'Delta B.2'])
325
        path2 = candidates[2]
326
        self.assertEqual(_descriptions(path2), ['Full C', 'Delta C.1'])
327
        # The version numbers use the new regime.
328
        self.assertEqual(path0[0].version, 300)
329
        self.assertEqual(path0[1].base, 300)
330
        self.assertEqual(path0[1].version, 301)
331
        self.assertEqual(path0[2].base, 301)
332
        self.assertEqual(path0[2].version, 304)
333
        winner = WeightedScorer().choose(candidates)
334
        self.assertEqual(_descriptions(winner),
335
                         ['Full B', 'Delta B.1', 'Delta B.2'])
336
        self.assertEqual(winner[0].version, 200)
337
        self.assertEqual(winner[1].base, 200)
338
        self.assertEqual(winner[1].version, 201)
339
        self.assertEqual(winner[2].base, 201)
340
        self.assertEqual(winner[2].version, 304)