mmd_resolver: rewrite function for finding alternatives

Previous version was assuming that number of alternatives is static
which is wrong, they can be appearing depending on solvables.

Also it was checking even impossible combinations (due to using
itertools.product).

Now we check only real possibilities.

Signed-off-by: Igor Gnatenko <ignatenko@redhat.com>
This commit is contained in:
Igor Gnatenko
2018-02-28 19:07:49 +01:00
committed by mprahl
parent 4ff944477d
commit 9ce5dffb14
2 changed files with 97 additions and 89 deletions

View File

@@ -24,8 +24,85 @@
# Igor Gnatenko <ignatenko@redhat.com>
import solv
from module_build_service import log, conf
import itertools
from module_build_service import log
def _gather_alternatives(pool, favor=None, tested=None, level=1, transactions=None):
if tested is None:
tested = set()
if transactions is None:
transactions = []
solver = pool.Solver()
jobs = []
jobs.extend(pool.Job(solv.Job.SOLVER_FAVOR | solv.Job.SOLVER_SOLVABLE, s.id)
for s in favor or [])
jobs.extend(pool.Job(solv.Job.SOLVER_DISFAVOR | solv.Job.SOLVER_SOLVABLE, s.id)
for s in tested)
assert not solver.solve(jobs)
newsolvables = solver.transaction().newsolvables()
transactions.append(newsolvables)
alternatives = solver.all_alternatives()
if not alternatives:
return transactions
if [alt for alt in alternatives if alt.type != solv.Alternative.SOLVER_ALTERNATIVE_TYPE_RULE]:
assert False, "Recommends alternative rule"
log.debug("Jobs:")
for job in pool.getpooljobs():
log.debug(" * %s [pool]", job)
for job in jobs:
log.debug(" * %s", job)
log.debug("Transaction:")
for s in newsolvables:
log.debug(" * %s", s)
log.debug("Alternatives:")
auto_minimized = False
for alt in alternatives:
raw_choices = alt.choices_raw()
log.debug(" %d: %s", alt.level, alt)
for choice in alt.choices():
if choice == alt.chosen:
sign = "+"
elif -choice.id in raw_choices:
sign = "-"
auto_minimized = True
else:
sign = " "
log.debug(" * %s%s", sign, choice)
if auto_minimized:
raise NotImplementedError
current_alternatives = [alt for alt in alternatives if alt.level == level]
if len(current_alternatives) > 1:
raise NotImplementedError
alternative = current_alternatives[0]
raw_choices = alternative.choices_raw()
tested.add(alternative.chosen)
for choice in (choice for choice in alternative.choices() if choice not in tested):
_gather_alternatives(pool,
favor=favor,
tested=tested,
level=level,
transactions=transactions)
max_level = max(alt.level for alt in alternatives)
if level == max_level:
return transactions
next_favor = [alt.chosen for alt in alternatives if alt.level <= level]
next_tested = set(alt.chosen for alt in alternatives if alt.level == level + 1)
_gather_alternatives(pool,
favor=next_favor,
tested=next_tested,
level=level + 1,
transactions=transactions)
return transactions
class MMDResolver(object):
"""
@@ -53,7 +130,7 @@ class MMDResolver(object):
"""
solvable = repo.add_solvable()
solvable.name = "%s:%s:%d:%s" % (mmd.get_name(), mmd.get_stream(),
mmd.get_version(), mmd.get_context())
mmd.get_version(), mmd.get_context())
solvable.evr = "%s-%d" % (mmd.get_stream(), mmd.get_version())
solvable.arch = "x86_64"
@@ -114,83 +191,6 @@ class MMDResolver(object):
"""
self._create_solvable(self.available_repo, mmd)
def _solve(self, module_name, alternative_with=None):
"""
Solves the dependencies of module `module_name`. If there is an
alternative solution to dependency solving, it will prefer the one
which brings in the package in `alternative_with` list (if set).
:rtype: solv.Solver
:return: Solver object with dependencies resolved.
"""
solver = self.pool.Solver()
# Try to select the module we are interested in.
flags = solv.Selection.SELECTION_PROVIDES
sel = self.pool.select("module(%s)" % module_name, flags)
if sel.isempty():
raise ValueError(
"Cannot find module %s while resolving "
"dependencies" % module_name)
# Prepare the job including the solution for problems from previous calls.
jobs = sel.jobs(solv.Job.SOLVER_INSTALL)
if alternative_with:
for name in alternative_with:
sel = self.pool.select("module(%s)" % name, flags)
if sel.isempty():
raise ValueError(
"Cannot find module %s while resolving "
"dependencies" % name)
jobs += sel.jobs(solv.Job.SOLVER_FAVOR)
# Try to solve the dependencies.
problems = solver.solve(jobs)
# In case we have some problems, return early here with the problems.
if len(problems) != 0:
# TODO: Add more info.
raise ValueError(
"Dependencies between modules are not satisfied")
return solver
def _solve_recurse(self, solvable, alternatives=None, alternatives_tried=None):
"""
Solves dependencies of module defined by `solvable` object and all its
alternatives recursively.
:return: set of frozensets of n:s:v:c of modules which satisfied the
dependency solving.
"""
if not alternatives:
alternatives = set()
if not alternatives_tried:
alternatives_tried = set()
solver = self._solve(solvable.name, alternatives)
if not solver:
return set([])
ret = set([])
ret.add(
frozenset([s.name for s in solver.transaction().newsolvables()
if s.name != solvable.name]))
choices = []
for alt in solver.all_alternatives():
l = []
for alt_choice in alt.choices():
if alt_choice.name.split(":")[0] in self.alternatives_whitelist:
l.append(alt_choice.name)
if l:
choices.append(l)
choices_combinations = list(itertools.product(*choices))
for choices_combination in choices_combinations:
if choices_combination not in alternatives_tried:
alternatives_tried.add(choices_combination)
ret = ret.union(self._solve_recurse(
solvable, choices_combination, alternatives_tried))
return ret
def solve(self, mmd):
"""
Solves dependencies of module defined by `mmd` object. Returns set
@@ -203,5 +203,10 @@ class MMDResolver(object):
solvable = self._create_solvable(self.build_repo, mmd)
self.pool.createwhatprovides()
alternatives = self._solve_recurse(solvable)
return alternatives
new_job = self.pool.Job(solv.Job.SOLVER_INSTALL | solv.Job.SOLVER_SOLVABLE, solvable.id)
jobs = self.pool.getpooljobs()
self.pool.setpooljobs(jobs + [new_job])
alternatives = _gather_alternatives(self.pool)
self.pool.setpooljobs(jobs)
return set(frozenset(s.name for s in trans if s != solvable) for trans in alternatives)

View File

@@ -27,9 +27,6 @@ import gi
gi.require_version('Modulemd', '1.0') # noqa
from gi.repository import Modulemd
import pytest
from mock import patch
from module_build_service.mmd_resolver import MMDResolver
@@ -82,10 +79,14 @@ class TestMMDResolver:
expanded = self.mmd_resolver.solve(mmds[0])
expected = set([
frozenset(["gtk:1:0:c2", "platform:f28:0:c10", "font:b:0:c8"]),
frozenset(["gtk:1:0:c3", "platform:f29:0:c11", "font:b:0:c9"]),
frozenset(["gtk:2:0:c4", "platform:f28:0:c10", "font:b:0:c8"]),
frozenset(["gtk:2:0:c5", "platform:f29:0:c11", "font:b:0:c9"]),
frozenset(["gtk:1:0:c2", "font:a:0:c6", "platform:f28:0:c10"]),
frozenset(["gtk:1:0:c2", "font:b:0:c8", "platform:f28:0:c10"]),
frozenset(["gtk:1:0:c3", "font:a:0:c7", "platform:f29:0:c11"]),
frozenset(["gtk:1:0:c3", "font:b:0:c9", "platform:f29:0:c11"]),
frozenset(["gtk:2:0:c4", "font:a:0:c6", "platform:f28:0:c10"]),
frozenset(["gtk:2:0:c4", "font:b:0:c8", "platform:f28:0:c10"]),
frozenset(["gtk:2:0:c5", "font:a:0:c7", "platform:f29:0:c11"]),
frozenset(["gtk:2:0:c5", "font:b:0:c9", "platform:f29:0:c11"]),
])
assert expanded == expected
@@ -109,7 +110,9 @@ class TestMMDResolver:
expanded = self.mmd_resolver.solve(mmds[0])
expected = set([
frozenset(["gtk:1:0:c2", "platform:f28:0:c10", "font:a:0:c6"]),
frozenset(["gtk:1:0:c2", "platform:f28:0:c10", "font:b:0:c8"]),
frozenset(["gtk:2:0:c4", "platform:f28:0:c10", "font:a:0:c6"]),
frozenset(["gtk:2:0:c4", "platform:f28:0:c10", "font:b:0:c8"]),
])