Extract a BinaryPackageUniverse from the InstallabilityTester

The InstallabilityTester is suffering from a lack of clear purpose
because it serves multiple.  This commit extracts most of one of these
purposes into the BinaryPackageUniverse class while retaining the
original API of the InstallabilityTester.

Signed-off-by: Niels Thykier <niels@thykier.net>
ubuntu/rebased
Niels Thykier 6 years ago
parent b7fe352713
commit 530db5d3f7
No known key found for this signature in database
GPG Key ID: A65B78DBE67C7AAC

@ -18,6 +18,7 @@ from itertools import product
from britney2.utils import ifilter_except, iter_except, get_dependency_solvers from britney2.utils import ifilter_except, iter_except, get_dependency_solvers
from britney2.installability.solver import InstallabilitySolver from britney2.installability.solver import InstallabilitySolver
from britney2.installability.universe import BinaryPackageRelation, BinaryPackageUniverse
def build_installability_tester(suite_info, archs): def build_installability_tester(suite_info, archs):
@ -305,10 +306,11 @@ class InstallabilityTesterBuilder(object):
if b in reverse_package_table: if b in reverse_package_table:
del reverse_package_table[b] del reverse_package_table[b]
eqv_table = self._build_eqv_packages_table(package_table, reverse_package_table) relations, eqv_table = self._build_eqv_packages_table(package_table, reverse_package_table)
return InstallabilitySolver(package_table, universe = BinaryPackageUniverse(relations)
reverse_package_table,
return InstallabilitySolver(universe,
self._testing, self._testing,
self._broken, self._broken,
self._essentials, self._essentials,
@ -369,6 +371,8 @@ class InstallabilityTesterBuilder(object):
find_eqv_table = defaultdict(list) find_eqv_table = defaultdict(list)
eqv_table = {} eqv_table = {}
relations = {}
emptyset = frozenset()
for pkg in reverse_package_table: for pkg in reverse_package_table:
rdeps = reverse_package_table[pkg][2] rdeps = reverse_package_table[pkg][2]
@ -380,12 +384,21 @@ class InstallabilityTesterBuilder(object):
ekey = (deps, con, rdeps) ekey = (deps, con, rdeps)
find_eqv_table[ekey].append(pkg) find_eqv_table[ekey].append(pkg)
for pkg_list in find_eqv_table.values(): for pkg_relations, pkg_list in find_eqv_table.items():
rel = BinaryPackageRelation(pkg_relations[0], pkg_relations[1], reverse_package_table[pkg_list[0]][0])
if len(pkg_list) < 2: if len(pkg_list) < 2:
relations[pkg_list[0]] = rel
continue continue
eqv_set = frozenset(pkg_list) eqv_set = frozenset(pkg_list)
for pkg in pkg_list: for pkg in pkg_list:
eqv_table[pkg] = eqv_set eqv_table[pkg] = eqv_set
relations[pkg] = rel
for pkg, forward_relations in package_table.items():
if pkg in relations:
continue
rel = BinaryPackageRelation(forward_relations[0], forward_relations[1], emptyset)
relations[pkg] = rel
return eqv_table return relations, eqv_table

@ -132,15 +132,10 @@ def compute_scc(graph):
class InstallabilitySolver(InstallabilityTester): class InstallabilitySolver(InstallabilityTester):
def __init__(self, universe, revuniverse, testing, broken, essentials, def __init__(self, universe, testing, broken, essentials, eqv_table):
eqv_table):
"""Create a new installability solver """Create a new installability solver
universe is a dict mapping package tuples to their universe is a BinaryPackageUniverse.
dependencies and conflicts.
revuniverse is a dict mapping package tuples to their reverse
dependencies and reverse conflicts.
testing is a (mutable) set of package tuples that determines testing is a (mutable) set of package tuples that determines
which of the packages in universe are currently in testing. which of the packages in universe are currently in testing.
@ -152,13 +147,11 @@ class InstallabilitySolver(InstallabilityTester):
- NB: arch:all packages are "re-mapped" to given architecture. - NB: arch:all packages are "re-mapped" to given architecture.
(simplifies caches and dependency checking) (simplifies caches and dependency checking)
""" """
super().__init__(universe, revuniverse, testing, super().__init__(universe, testing, broken, essentials, eqv_table)
broken, essentials, eqv_table)
def solve_groups(self, groups): def solve_groups(self, groups):
sat_in_testing = self._testing.isdisjoint sat_in_testing = self._testing.isdisjoint
universe = self._universe universe = self._universe
revuniverse = self._revuniverse
result = [] result = []
emitted = set() emitted = set()
queue = deque() queue = deque()
@ -191,9 +184,9 @@ class InstallabilitySolver(InstallabilityTester):
oldcons = set() oldcons = set()
newcons = set() newcons = set()
for r in rms: for r in rms:
oldcons.update(universe[r][1]) oldcons.update(universe.negative_dependencies_of(r))
for a in adds: for a in adds:
newcons.update(universe[a][1]) newcons.update(universe.negative_dependencies_of(a))
current = newcons & oldcons current = newcons & oldcons
oldcons -= current oldcons -= current
newcons -= current newcons -= current
@ -213,11 +206,11 @@ class InstallabilitySolver(InstallabilityTester):
order[key]['before'].add(other) order[key]['before'].add(other)
order[other]['after'].add(key) order[other]['after'].add(key)
for r in ifilter_only(revuniverse, rms): for r in rms:
# The binaries have reverse dependencies in testing; # The binaries have reverse dependencies in testing;
# check if we can/should migrate them first. # check if we can/should migrate them first.
for rdep in revuniverse[r][0]: for rdep in universe.reverse_dependencies_of(r):
for depgroup in universe[rdep][0]: for depgroup in universe.dependencies_of(rdep):
rigid = depgroup - going_out rigid = depgroup - going_out
if not sat_in_testing(rigid): if not sat_in_testing(rigid):
# (partly) satisfied by testing, assume it is okay # (partly) satisfied by testing, assume it is okay
@ -236,7 +229,7 @@ class InstallabilitySolver(InstallabilityTester):
# Check if this item should migrate before others # Check if this item should migrate before others
# (e.g. because they depend on a new [version of a] # (e.g. because they depend on a new [version of a]
# binary provided by this item). # binary provided by this item).
for depgroup in universe[a][0]: for depgroup in universe.dependencies_of(a):
rigid = depgroup - going_out rigid = depgroup - going_out
if not sat_in_testing(rigid): if not sat_in_testing(rigid):
# (partly) satisfied by testing, assume it is okay # (partly) satisfied by testing, assume it is okay

@ -22,15 +22,10 @@ from britney2.utils import iter_except
class InstallabilityTester(object): class InstallabilityTester(object):
def __init__(self, universe, revuniverse, testing, broken, essentials, def __init__(self, universe, testing, broken, essentials, eqv_table):
eqv_table):
"""Create a new installability tester """Create a new installability tester
universe is a dict mapping package ids to their universe is a BinaryPackageUniverse
dependencies and conflicts.
revuniverse is a table containing all packages with reverse
relations mapping them to their reverse relations.
testing is a (mutable) set of package ids that determines testing is a (mutable) set of package ids that determines
which of the packages in universe are currently in testing. which of the packages in universe are currently in testing.
@ -49,7 +44,6 @@ class InstallabilityTester(object):
self._testing = testing self._testing = testing
self._broken = broken self._broken = broken
self._essentials = essentials self._essentials = essentials
self._revuniverse = revuniverse
self._eqv_table = eqv_table self._eqv_table = eqv_table
self._stats = InstallabilityStats() self._stats = InstallabilityStats()
logger_name = ".".join((self.__class__.__module__, self.__class__.__name__)) logger_name = ".".join((self.__class__.__module__, self.__class__.__name__))
@ -122,10 +116,7 @@ class InstallabilityTester(object):
:return: A set containing the package ids all of the reverse :return: A set containing the package ids all of the reverse
dependencies of the input package. The result is suite agnostic. dependencies of the input package. The result is suite agnostic.
""" """
revuniverse = self._revuniverse return self._universe.reverse_dependencies_of(pkg_id)
if pkg_id not in revuniverse:
return frozenset()
return revuniverse[pkg_id][0]
def negative_dependencies_of(self, pkg_id): def negative_dependencies_of(self, pkg_id):
"""Returns the set of negative dependencies of a given package """Returns the set of negative dependencies of a given package
@ -138,7 +129,7 @@ class InstallabilityTester(object):
:return: A set containing the package ids all of the negative :return: A set containing the package ids all of the negative
dependencies of the input package. The result is suite agnostic. dependencies of the input package. The result is suite agnostic.
""" """
return self._universe[pkg_id][1] return self._universe.negative_dependencies_of(pkg_id)
def dependencies_of(self, pkg_id): def dependencies_of(self, pkg_id):
"""Returns the set of dependencies of a given package """Returns the set of dependencies of a given package
@ -147,7 +138,7 @@ class InstallabilityTester(object):
:return: A set containing the package ids all of the dependencies :return: A set containing the package ids all of the dependencies
of the input package. The result is suite agnostic. of the input package. The result is suite agnostic.
""" """
return self._universe[pkg_id][0] return self._universe.dependencies_of(pkg_id)
def any_of_these_are_in_testing(self, pkgs): def any_of_these_are_in_testing(self, pkgs):
"""Test if at least one package of a given set is in testing """Test if at least one package of a given set is in testing
@ -214,7 +205,7 @@ class InstallabilityTester(object):
# Removes a package from the "pseudo-essential set" # Removes a package from the "pseudo-essential set"
del self._cache_ess[pkg_id.architecture] del self._cache_ess[pkg_id.architecture]
if pkg_id not in self._revuniverse: if not self._universe.reverse_dependencies_of(pkg_id):
# no reverse relations - safe # no reverse relations - safe
return True return True
if pkg_id not in self._broken and pkg_id in self._cache_inst: if pkg_id not in self._broken and pkg_id in self._cache_inst:
@ -483,9 +474,9 @@ class InstallabilityTester(object):
# While we have guaranteed dependencies (in check), examine all # While we have guaranteed dependencies (in check), examine all
# of them. # of them.
for cur in iter_except(check.pop, IndexError): for cur in iter_except(check.pop, IndexError):
(deps, cons) = universe[cur] relations = universe.relations_of(cur)
if cons: if relations.negative_dependencies:
# Conflicts? # Conflicts?
if cur in never: if cur in never:
# cur adds a (reverse) conflict, so check if cur # cur adds a (reverse) conflict, so check if cur
@ -498,11 +489,11 @@ class InstallabilityTester(object):
return False return False
# We must install cur for the package to be installable, # We must install cur for the package to be installable,
# so "obviously" we can never choose any of its conflicts # so "obviously" we can never choose any of its conflicts
never.update(cons & testing) never.update(relations.negative_dependencies & testing)
# depgroup can be satisfied by picking something that is # depgroup can be satisfied by picking something that is
# already in musts - lets pick that (again). :) # already in musts - lets pick that (again). :)
for depgroup in not_satisfied(deps): for depgroup in not_satisfied(relations.dependencies):
# Of all the packages listed in the relation remove those that # Of all the packages listed in the relation remove those that
# are either: # are either:
@ -587,8 +578,9 @@ class InstallabilityTester(object):
for choice in not_satisfied(ess_choices): for choice in not_satisfied(ess_choices):
b = False b = False
for c in choice: for c in choice:
if universe[c][1] <= ess_never and \ relations = universe.relations_of(c)
not any(not_satisfied(universe[c][0])): if relations.negative_dependencies <= ess_never and \
not any(not_satisfied(relations.dependencies)):
ess_base.append(c) ess_base.append(c)
b = True b = True
break break
@ -599,7 +591,7 @@ class InstallabilityTester(object):
break break
for x in start: for x in start:
ess_never.update(universe[x][1]) ess_never.update(universe.negative_dependencies_of(x))
self._cache_ess[arch] = (frozenset(start), frozenset(ess_never), frozenset(ess_choices)) self._cache_ess[arch] = (frozenset(start), frozenset(ess_never), frozenset(ess_choices))
return self._cache_ess[arch] return self._cache_ess[arch]
@ -612,7 +604,7 @@ class InstallabilityTester(object):
for pkg in universe: for pkg in universe:
(pkg_name, pkg_version, pkg_arch) = pkg (pkg_name, pkg_version, pkg_arch) = pkg
deps, con = universe[pkg] relations = universe.relations_of(pkg)
arch_stats = graph_stats[pkg_arch] arch_stats = graph_stats[pkg_arch]
arch_stats.nodes += 1 arch_stats.nodes += 1
@ -621,8 +613,8 @@ class InstallabilityTester(object):
eqv = [e for e in eqv_table[pkg] if e.architecture == pkg_arch] eqv = [e for e in eqv_table[pkg] if e.architecture == pkg_arch]
arch_stats.eqv_nodes += len(eqv) arch_stats.eqv_nodes += len(eqv)
arch_stats.add_dep_edges(deps) arch_stats.add_dep_edges(relations.dependencies)
arch_stats.add_con_edges(con) arch_stats.add_con_edges(relations.negative_dependencies)
for stat in graph_stats.values(): for stat in graph_stats.values():
stat.compute_all() stat.compute_all()

@ -0,0 +1,115 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2012 Niels Thykier <niels@thykier.net>
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
class BinaryPackageRelation(object):
"""All relations of a given binary package"""
__slots__ = ['dependencies', 'negative_dependencies', 'reverse_dependencies']
def __init__(self, dependencies, negative_dependencies, reverse_dependencies):
self.dependencies = dependencies
self.negative_dependencies = negative_dependencies
self.reverse_dependencies = reverse_dependencies
class BinaryPackageUniverse(object):
"""A "universe" of all binary packages and their relations
The package universe is a read-only ("immutable") data structure
that knows of all binary packages and their internal relations.
The relations are either in Conjunctive Normal Form (CNF) represented
via sets of sets of the package ids or simply sets of package ids.
Being immutable, the universe does *not* track stateful data such
as "which package is in what suite?" nor "is this package installable
in that suite?".
"""
def __init__(self, relations):
self._relations = relations
def dependencies_of(self, pkg_id):
"""Returns the set of dependencies of a given package
:param pkg_id: The BinaryPackageId of a binary package.
:return: A set containing the package ids all of the dependencies
of the input package in CNF.
"""
return self._relations[pkg_id].dependencies
def negative_dependencies_of(self, pkg_id):
"""Returns the set of negative dependencies of a given package
Note that there is no "reverse_negative_dependencies_of" method,
since negative dependencies have no "direction" unlike positive
dependencies.
:param pkg_id: The BinaryPackageId of a binary package.
:return: A set containing the package ids all of the negative
dependencies of the input package.
"""
return self._relations[pkg_id].negative_dependencies
def reverse_dependencies_of(self, pkg_id):
"""Returns the set of reverse dependencies of a given package
Note that a package is considered a reverse dependency of the
given package as long as at least one of its dependency relations
*could* be satisfied by the given package.
:param pkg_id: The BinaryPackageId of a binary package.
:return: A set containing the package ids all of the reverse
dependencies of the input package.
"""
return self._relations[pkg_id].reverse_dependencies
def are_equivalent(self, pkg_id1, pkg_id2):
"""Test if pkg_id1 and pkg_id2 are equivalent
:param pkg_id1 The id of the first package
:param pkg_id2 The id of the second package
:return: True if pkg_id1 and pkg_id2 have the same "signature" in
the package dependency graph (i.e. relations can not tell
them apart semantically except for their name). Otherwise False.
Note that this can return True even if pkg_id1 and pkg_id2 can
tell each other apart.
"""
return pkg_id2 in self.packages_equivalent_to(pkg_id1)
def packages_equivalent_to(self, pkg_id):
"""Determine which packages are equivalent to a given package
:param pkg_id: The BinaryPackageId of a binary package.
:return: A frozenset of all package ids that are equivalent to the
input package. Note that this set always includes the input
package assuming it is a known package.
"""
return NotImplemented
def relations_of(self, pkg_id):
"""Get the direct relations of a given packge
:param pkg_id: The BinaryPackageId of a binary package.
:return: A BinaryPackageRelation describing all known direct
relations for the package.
"""
return self._relations[pkg_id]
def __contains__(self, pkg_id):
return pkg_id in self._relations
def __iter__(self):
yield from self._relations
Loading…
Cancel
Save