def test_forked_paths(self): # We have a fork in the road. There is a full update, but two deltas # with different versions point to the same base. This will give us # two upgrade paths, both of which include the full. index = get_index('candidates.index_07.json') candidates = get_candidates(index, 1200) self.assertEqual(len(candidates), 2) # We can sort the paths by length. paths = sorted(candidates, key=len) # The shortest path gets us to 1302 in two steps. self.assertEqual(len(paths[0]), 2) self.assertEqual([image.version for image in paths[0]], [1300, 1302]) descriptions = [] for image in paths[0]: # There's only one description per image so order doesn't matter. descriptions.extend(image.descriptions.values()) self.assertEqual(descriptions, ['Full 1', 'Delta 2']) # The longer path gets us to 1302 in three steps. self.assertEqual(len(paths[1]), 3) self.assertEqual([image.version for image in paths[1]], [1300, 1301, 1302]) descriptions = [] for image in paths[1]: # There's only one description per image so order doesn't matter. descriptions.extend(image.descriptions.values()) self.assertEqual(descriptions, ['Full 1', 'Delta 1', 'Delta 3'])
def test_candidates(self): # Path B will win; it has one full and two deltas. index = get_index('candidates.index_13.json') candidates = get_candidates(index, 0) self.assertEqual(len(candidates), 3) path0 = candidates[0] self.assertEqual(descriptions(path0), ['Full A', 'Delta A.1', 'Delta A.2']) path1 = candidates[1] self.assertEqual(descriptions(path1), ['Full B', 'Delta B.1', 'Delta B.2']) path2 = candidates[2] self.assertEqual(descriptions(path2), ['Full C', 'Delta C.1']) # The version numbers use the new regime. self.assertEqual(path0[0].version, 300) self.assertEqual(path0[1].base, 300) self.assertEqual(path0[1].version, 301) self.assertEqual(path0[2].base, 301) self.assertEqual(path0[2].version, 304) winner = WeightedScorer().choose(candidates, 'devel') self.assertEqual(descriptions(winner), ['Full B', 'Delta B.1', 'Delta B.2']) self.assertEqual(winner[0].version, 200) self.assertEqual(winner[1].base, 200) self.assertEqual(winner[1].version, 201) self.assertEqual(winner[2].base, 201) self.assertEqual(winner[2].version, 304)
def test_filter_for_fulls_with_just_delta_candidates(self): # A candidate path that contains only deltas will have no filtered # paths if all the images are delta updates. index = get_index('candidates.index_11.json') candidates = get_candidates(index, 100) self.assertEqual(len(candidates), 1) filtered = full_filter(candidates) self.assertEqual(len(filtered), 0)
def test_filter_for_deltas(self): # Filter the candidates, where the only available path is a delta path. index = get_index('candidates.index_11.json') candidates = get_candidates(index, 100) self.assertEqual(len(candidates), 1) filtered = delta_filter(candidates) self.assertEqual(len(filtered), 1) self.assertEqual(candidates, filtered)
def test_filter_for_deltas_none_available(self): # Run a filter over the candidates, such that the only ones left are # those that start with and contain only deltas. Since none of the # paths do so, tere are no candidates left. index = get_index('candidates.index_08.json') candidates = get_candidates(index, 600) filtered = delta_filter(candidates) self.assertEqual(len(filtered), 0)
def test_tied_candidates(self): # LP: #1206866 - TypeError when two candidate paths scored equal. # # index_04.json was captured from real data causing the traceback. index = get_index('scores.index_04.json') candidates = get_candidates(index, 1) path = self.scorer.choose(candidates, 'devel') self.assertEqual(len(path), 1) self.assertEqual(path[0].version, 1800)
def test_filter_for_multiple_deltas(self): # The candidate path has multiple deltas. All are preserved. index = get_index('candidates.index_12.json') candidates = get_candidates(index, 100) filtered = delta_filter(candidates) self.assertEqual(len(filtered), 1) path = filtered[0] self.assertEqual(len(path), 3) self.assertEqual(descriptions(path), ['Delta A', 'Delta B', 'Delta C'])
def test_pulled_update_insanely_positive_randint(self): # When the final image on an update path has a phase percentage of # zero, then regardless of the device's percentage (even if randint # returned some insane value), the candidate path is not okay. In this # case, the `Full B` has phase of 0%. index = get_index('scores.index_01.json') candidates = get_candidates(index, 100) with patch('systemimage.scores.phased_percentage', return_value=1000): winner = self.scorer.choose(candidates, 'devel') self.assertEqual(len(winner), 0)
def test_pulled_update(self): # When the final image on an update path has a phase percentage of # zero, then regardless of the device's percentage, the candidate path # is not okay. In this case, the `Full B` has phase of 0%. index = get_index('scores.index_01.json') candidates = get_candidates(index, 100) with patch('systemimage.scores.phased_percentage', return_value=0): winner = self.scorer.choose(candidates, 'devel') self.assertEqual(descriptions(winner), ['Full A', 'Delta A.1', 'Delta A.2'])
def test_equal_phase_gets_update(self): # When the final image on an update path has a phase percentage exactly # equal to the device percentage, the candidate path is okay. In this # case, the `Full B` has phase of 50%. index = get_index('scores.index_05.json') candidates = get_candidates(index, 100) with patch('systemimage.scores.phased_percentage', return_value=50): winner = self.scorer.choose(candidates, 'devel') self.assertEqual(descriptions(winner), ['Full B', 'Delta B.1', 'Delta B.2'])
def test_one_path_with_full_and_deltas(self): # There's one path to upgrade from our version to the final version. # This one starts at a full and includes several deltas. index = get_index('candidates.index_06.json') candidates = get_candidates(index, 1000) self.assertEqual(len(candidates), 1) path = candidates[0] self.assertEqual(len(path), 3) self.assertEqual([image.version for image in path], [1300, 1301, 1302]) self.assertEqual(descriptions(path), ['Full 1', 'Delta 1', 'Delta 2'])
def test_outside_phase_gets_update(self): # When the final image on an update path has a phase percentage lower # than the device percentage, the scorer falls back to the next # candidate path. index = get_index('scores.index_05.json') candidates = get_candidates(index, 100) with patch('systemimage.scores.phased_percentage', return_value=66): winner = self.scorer.choose(candidates, 'devel') self.assertEqual(descriptions(winner), ['Full A', 'Delta A.1', 'Delta A.2'])
def test_one_delta_based_on_us(self): # There is one delta in the test data that is based on us. index = get_index('candidates.index_04.json') candidates = get_candidates(index, 500) self.assertEqual(len(candidates), 1) path = candidates[0] # The path has exactly one image. self.assertEqual(len(path), 1) image = path[0] self.assertEqual(list(image.descriptions.values()), ['Delta 2'])
def test_one_path_with_deltas(self): # Similar to above, except that because we're upgrading from the # version of the full, the path is only two images long, i.e. the # deltas. index = get_index('candidates.index_06.json') candidates = get_candidates(index, 1300) self.assertEqual(len(candidates), 1) path = candidates[0] self.assertEqual(len(path), 2) self.assertEqual([image.version for image in path], [1301, 1302]) self.assertEqual(descriptions(path), ['Delta 1', 'Delta 2'])
def test_one_path(self): index = get_index('scores.index_02.json') candidates = get_candidates(index, 600) # There's only one path. scores = self.scorer.score(candidates) # The score is 200 for the two extra bootme flags. self.assertEqual(scores, [200]) # And we upgrade to the only path available. winner = self.scorer.choose(candidates, 'devel') # There are two images in the winning path. self.assertEqual(len(winner), 2) self.assertEqual([image.version for image in winner], [1300, 1301])
def test_inside_phase_gets_update(self): # When the final image on an update path has a phase percentage higher # than the device percentage, the candidate path is okay. In this # case, the `Full B` has phase of 50%. index = get_index('scores.index_05.json') candidates = get_candidates(index, 100) with patch('systemimage.scores.phased_percentage', return_value=22): winner = self.scorer.choose(candidates, 'devel') descriptions = [] for image in winner: descriptions.extend(image.descriptions.values()) self.assertEqual(descriptions, ['Full B', 'Delta B.1', 'Delta B.2'])
def test_one_higher_full(self): # Our device is between the minversions of the two available fulls, so # the older one can be upgraded too. index = get_index('candidates.index_02.json') candidates = get_candidates(index, 800) # There is exactly one upgrade path. self.assertEqual(len(candidates), 1) path = candidates[0] # The path has exactly one image. self.assertEqual(len(path), 1) image = path[0] self.assertEqual(list(image.descriptions.values()), ['New full build 1'])
def test_two_deltas_based_on_us(self): # There are two deltas that are based on us, so both are candidates. # They get us to different final versions. index = get_index('candidates.index_05.json') candidates = get_candidates(index, 1100) self.assertEqual(len(candidates), 2) # Both candidate paths have exactly one image in them. We can't sort # these paths, so just test them both. path0, path1 = candidates self.assertEqual(len(path0), 1) self.assertEqual(len(path1), 1) # One path gets us to version 1300 and the other 1400. images = sorted([path0[0], path1[0]], key=attrgetter('version')) self.assertEqual(descriptions(images), ['Delta 2', 'Delta 1'])
def test_no_version_detail(self): # The index.json file has three paths for updates, but only one is # selected. The winning path lands on an image without a # version_detail key. index = get_index('scores.index_07.json') candidates = get_candidates(index, 600) scores = self.scorer.score(candidates) self.assertEqual(scores, [300, 200, 9401]) winner = self.scorer.choose(candidates, 'devel') self.assertEqual(len(winner), 3) self.assertEqual([image.version for image in winner], [1200, 1201, 1304]) self.assertEqual(descriptions(winner), ['Full B', 'Delta B.1', 'Delta B.2']) self.assertEqual(winner[-1].version_detail, '')
def test_filter_for_fulls(self): # Run a filter over the candidates, such that the only ones left are # those that contain only full upgrades. This can truncate any paths # that start with some fulls and then contain some deltas. index = get_index('candidates.index_08.json') candidates = get_candidates(index, 600) filtered = full_filter(candidates) # Since all images start with a full update, we're still left with # three candidates. self.assertEqual(len(filtered), 3) self.assertEqual([image.type for image in filtered[0]], ['full']) self.assertEqual([image.type for image in filtered[1]], ['full']) self.assertEqual([image.type for image in filtered[2]], ['full']) self.assertEqual(descriptions(filtered[0]), ['Full A']) self.assertEqual(descriptions(filtered[1]), ['Full B']) self.assertEqual(descriptions(filtered[2]), ['Full C'])
def test_fulls_with_no_minversion(self): # Like the previous test, there are two full upgrades, but because # neither of them have minversions, both are candidates. index = get_index('candidates.index_03.json') candidates = get_candidates(index, 400) self.assertEqual(len(candidates), 2) # Both candidate paths have exactly one image in them. We can't sort # these paths, so just test them both. path0, path1 = candidates self.assertEqual(len(path0), 1) self.assertEqual(len(path1), 1) # One path gets us to version 1300 and the other 1400. images = sorted([path0[0], path1[0]], key=attrgetter('version')) self.assertEqual(list(images[0].descriptions.values()), ['New full build 1']) self.assertEqual(list(images[1].descriptions.values()), ['New full build 2'])
def _calculate_winner(self): """Given an index, calculate the paths and score a winner.""" # If we were tracking a channel alias, and that channel alias has # changed, squash the build number to 0 before calculating the # winner. Otherwise, trust the configured build number. channel = self.channels[config.channel] # channel_target is the channel we're on based on the alias mapping in # our config files. channel_alias is the alias mapping in the # channel.json file, i.e. the channel an update will put us on. channel_target = getattr(config.service, 'channel_target', None) channel_alias = getattr(channel, 'alias', None) if (channel_alias is None or channel_target is None or channel_alias == channel_target): build_number = config.build_number else: # This is a channel switch caused by a new alias. Unless the # build number has been explicitly overridden on the command line # via --build/-b, use build number 0 to force a full update. build_number = (config.build_number if config.build_number_override else 0) self.channel_switch = (channel_target, channel_alias) candidates = get_candidates(self.index, build_number) log.debug('Candidates from build# {}: {}'.format( build_number, len(candidates))) if self.candidate_filter is not None: candidates = self.candidate_filter(candidates) self.winner = config.hooks.scorer().choose( candidates, (channel_target if channel_alias is None else channel_alias)) if len(self.winner) == 0: log.info('Already up-to-date') return winning_path = [str(image.version) for image in self.winner] log.info('Upgrade path is {}'.format(COLON.join(winning_path))) # Now filter the winning path to cap the maximum version number. if (self.winner_filter is not None and self.winner_filter.maximum_version is not None): log.info('Upgrade path capped at version {}'.format( self.winner_filter.maximum_version)) self.winner = self.winner_filter(self.winner) if len(self.winner) == 0: log.info('Capped upgrade leaves device up-to-date') return self._next.append(self._download_files)
def test_get_downloads(self): # Path B will win; it has one full and two deltas, none of which have # a bootme flag. Download all their files. index = get_index('candidates.index_08.json') candidates = get_candidates(index, 600) winner = WeightedScorer().choose(candidates, 'devel') descriptions = [] for image in winner: # There's only one description per image so order doesn't matter. descriptions.extend(image.descriptions.values()) self.assertEqual(descriptions, ['Full B', 'Delta B.1', 'Delta B.2']) downloads = list(iter_path(winner)) paths = set(filerec.path for (n, filerec) in downloads) self.assertEqual( paths, set([ '/3/4/5.txt', '/4/5/6.txt', '/5/6/7.txt', '/6/7/8.txt', '/7/8/9.txt', '/8/9/a.txt', '/9/a/b.txt', '/e/d/c.txt', '/f/e/d.txt', ])) signatures = set(filerec.signature for (n, filerec) in downloads) self.assertEqual( signatures, set([ '/3/4/5.txt.asc', '/4/5/6.txt.asc', '/5/6/7.txt.asc', '/6/7/8.txt.asc', '/7/8/9.txt.asc', '/8/9/a.txt.asc', '/9/a/b.txt.asc', '/e/d/c.txt.asc', '/f/e/d.txt.asc', ]))
def test_get_downloads_with_bootme(self): # Path B will win; it has one full and two deltas. The first delta # has a bootme flag so the second delta's files are not downloaded. index = get_index('candidates.index_09.json') candidates = get_candidates(index, 600) winner = WeightedScorer().choose(candidates, 'devel') descriptions = [] for image in winner: # There's only one description per image so order doesn't matter. descriptions.extend(image.descriptions.values()) self.assertEqual(descriptions, ['Full B', 'Delta B.1', 'Delta B.2']) downloads = iter_path(winner) paths = set(filerec.path for (n, filerec) in downloads) self.assertEqual( paths, set([ '/3/4/5.txt', '/4/5/6.txt', '/5/6/7.txt', '/6/7/8.txt', '/7/8/9.txt', '/8/9/a.txt', ]))
def test_calculate_candidates(self): # Calculate the candidate paths. setup_keyrings() state = State() # Run the state machine until we get an index file. state.run_until('calculate_winner') candidates = get_candidates(state.index, 100) # There are three candidate upgrade paths. self.assertEqual(len(candidates), 3) descriptions = [] for image in candidates[0]: # There's only one description per image so order doesn't matter. descriptions.extend(image.descriptions.values()) self.assertEqual(descriptions, ['Full A', 'Delta A.1', 'Delta A.2']) descriptions = [] for image in candidates[1]: # There's only one description per image so order doesn't matter. descriptions.extend(image.descriptions.values()) self.assertEqual(descriptions, ['Full B', 'Delta B.1', 'Delta B.2']) descriptions = [] for image in candidates[2]: # There's only one description per image so order doesn't matter. descriptions.extend(image.descriptions.values()) self.assertEqual(descriptions, ['Full C', 'Delta C.1'])
def test_three_paths(self): # - Path A requires three extra reboots, is the smallest total # download and leaves you at the highest available version. # Score: 300 # # - Path B requires one extra reboot, but is 100MiB bigger and leaves # you at the highest available version. Score: 200 # # - Path C requires no extra reboots, but is 400MiB bigger and leaves # you at 1303 instead of the highest 1304. For that reason, it gets # a huge score making it impossible to win. # # Path B wins. index = get_index('scores.index_03.json') candidates = get_candidates(index, 600) # There are three paths. The scores are as above. scores = self.scorer.score(candidates) self.assertEqual(scores, [300, 200, 9401]) winner = self.scorer.choose(candidates, 'devel') self.assertEqual(len(winner), 3) self.assertEqual([image.version for image in winner], [1200, 1201, 1304]) self.assertEqual(descriptions(winner), ['Full B', 'Delta B.1', 'Delta B.2'])
def test_no_deltas_based_on_us(self): # There are deltas in the test data, but no fulls. None of the deltas # have a base equal to our build number. index = get_index('candidates.index_04.json') candidates = get_candidates(index, 100) self.assertEqual(candidates, [])
def test_filter_for_deltas_one_candidate(self): # Filter for delta updates, but the only candidate is a full. index = get_index('candidates.index_10.json') candidates = get_candidates(index, 600) filtered = delta_filter(candidates) self.assertEqual(len(filtered), 0)
def test_only_higher_fulls(self): # All the full images have a minversion greater than our version, so # we cannot upgrade to any of them. index = get_index('candidates.index_02.json') candidates = get_candidates(index, 100) self.assertEqual(candidates, [])
def test_no_images(self): # If there are no images defined, there are no candidates. index = get_index('candidates.index_01.json') candidates = get_candidates(index, 1400) self.assertEqual(candidates, [])