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.installability.solver import InstallabilitySolver
from britney2.installability.universe import BinaryPackageRelation, BinaryPackageUniverse
def build_installability_tester(suite_info, archs):
@ -305,10 +306,11 @@ class InstallabilityTesterBuilder(object):
if b in reverse_package_table:
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,
reverse_package_table,
universe = BinaryPackageUniverse(relations)
return InstallabilitySolver(universe,
self._testing,
self._broken,
self._essentials,
@ -369,6 +371,8 @@ class InstallabilityTesterBuilder(object):
find_eqv_table = defaultdict(list)
eqv_table = {}
relations = {}
emptyset = frozenset()
for pkg in reverse_package_table:
rdeps = reverse_package_table[pkg][2]
@ -380,12 +384,21 @@ class InstallabilityTesterBuilder(object):
ekey = (deps, con, rdeps)
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:
relations[pkg_list[0]] = rel
continue
eqv_set = frozenset(pkg_list)
for pkg in pkg_list:
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):
def __init__(self, universe, revuniverse, testing, broken, essentials,
eqv_table):
def __init__(self, universe, testing, broken, essentials, eqv_table):
"""Create a new installability solver
universe is a dict mapping package tuples to their
dependencies and conflicts.
revuniverse is a dict mapping package tuples to their reverse
dependencies and reverse conflicts.
universe is a BinaryPackageUniverse.
testing is a (mutable) set of package tuples that determines
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.
(simplifies caches and dependency checking)
"""
super().__init__(universe, revuniverse, testing,
broken, essentials, eqv_table)
super().__init__(universe, testing, broken, essentials, eqv_table)
def solve_groups(self, groups):
sat_in_testing = self._testing.isdisjoint
universe = self._universe
revuniverse = self._revuniverse
result = []
emitted = set()
queue = deque()
@ -191,9 +184,9 @@ class InstallabilitySolver(InstallabilityTester):
oldcons = set()
newcons = set()
for r in rms:
oldcons.update(universe[r][1])
oldcons.update(universe.negative_dependencies_of(r))
for a in adds:
newcons.update(universe[a][1])
newcons.update(universe.negative_dependencies_of(a))
current = newcons & oldcons
oldcons -= current
newcons -= current
@ -213,11 +206,11 @@ class InstallabilitySolver(InstallabilityTester):
order[key]['before'].add(other)
order[other]['after'].add(key)
for r in ifilter_only(revuniverse, rms):
for r in rms:
# The binaries have reverse dependencies in testing;
# check if we can/should migrate them first.
for rdep in revuniverse[r][0]:
for depgroup in universe[rdep][0]:
for rdep in universe.reverse_dependencies_of(r):
for depgroup in universe.dependencies_of(rdep):
rigid = depgroup - going_out
if not sat_in_testing(rigid):
# (partly) satisfied by testing, assume it is okay
@ -236,7 +229,7 @@ class InstallabilitySolver(InstallabilityTester):
# Check if this item should migrate before others
# (e.g. because they depend on a new [version of a]
# binary provided by this item).
for depgroup in universe[a][0]:
for depgroup in universe.dependencies_of(a):
rigid = depgroup - going_out
if not sat_in_testing(rigid):
# (partly) satisfied by testing, assume it is okay

@ -22,15 +22,10 @@ from britney2.utils import iter_except
class InstallabilityTester(object):
def __init__(self, universe, revuniverse, testing, broken, essentials,
eqv_table):
def __init__(self, universe, testing, broken, essentials, eqv_table):
"""Create a new installability tester
universe is a dict mapping package ids to their
dependencies and conflicts.
revuniverse is a table containing all packages with reverse
relations mapping them to their reverse relations.
universe is a BinaryPackageUniverse
testing is a (mutable) set of package ids that determines
which of the packages in universe are currently in testing.
@ -49,7 +44,6 @@ class InstallabilityTester(object):
self._testing = testing
self._broken = broken
self._essentials = essentials
self._revuniverse = revuniverse
self._eqv_table = eqv_table
self._stats = InstallabilityStats()
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
dependencies of the input package. The result is suite agnostic.
"""
revuniverse = self._revuniverse
if pkg_id not in revuniverse:
return frozenset()
return revuniverse[pkg_id][0]
return self._universe.reverse_dependencies_of(pkg_id)
def negative_dependencies_of(self, pkg_id):
"""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
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):
"""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
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):
"""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"
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
return True
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
# of them.
for cur in iter_except(check.pop, IndexError):
(deps, cons) = universe[cur]
relations = universe.relations_of(cur)
if cons:
if relations.negative_dependencies:
# Conflicts?
if cur in never:
# cur adds a (reverse) conflict, so check if cur
@ -498,11 +489,11 @@ class InstallabilityTester(object):
return False
# We must install cur for the package to be installable,
# 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
# 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
# are either:
@ -587,8 +578,9 @@ class InstallabilityTester(object):
for choice in not_satisfied(ess_choices):
b = False
for c in choice:
if universe[c][1] <= ess_never and \
not any(not_satisfied(universe[c][0])):
relations = universe.relations_of(c)
if relations.negative_dependencies <= ess_never and \
not any(not_satisfied(relations.dependencies)):
ess_base.append(c)
b = True
break
@ -599,7 +591,7 @@ class InstallabilityTester(object):
break
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))
return self._cache_ess[arch]
@ -612,7 +604,7 @@ class InstallabilityTester(object):
for pkg in universe:
(pkg_name, pkg_version, pkg_arch) = pkg
deps, con = universe[pkg]
relations = universe.relations_of(pkg)
arch_stats = graph_stats[pkg_arch]
arch_stats.nodes += 1
@ -621,8 +613,8 @@ class InstallabilityTester(object):
eqv = [e for e in eqv_table[pkg] if e.architecture == pkg_arch]
arch_stats.eqv_nodes += len(eqv)
arch_stats.add_dep_edges(deps)
arch_stats.add_con_edges(con)
arch_stats.add_dep_edges(relations.dependencies)
arch_stats.add_con_edges(relations.negative_dependencies)
for stat in graph_stats.values():
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