Detect transitive stream collision

For an explanation of transitive stream collision, please refer to the
source code docstring.

When such collision is detected, error is raised with proper error
message.

Signed-off-by: Chenxiong Qi <cqi@redhat.com>
This commit is contained in:
Chenxiong Qi
2018-10-29 19:13:25 +08:00
parent ba3bef9f46
commit 879d0daca0
2 changed files with 105 additions and 17 deletions

View File

@@ -526,8 +526,14 @@ class MMDResolver(object):
# Solve the deps and log the dependency issues.
problems = solver.solve(jobs)
if problems:
raise RuntimeError("Problems were found during solve(): %s" % ", ".join(
str(p) for p in problems))
problem_str = self._detect_transitive_stream_collision(problems)
if problem_str:
err_msg = problem_str
else:
err_msg = ', '.join(str(p) for p in problems)
raise RuntimeError(
'Problems were found during module dependency resolution: {}'
.format(err_msg))
# Find out what was actually resolved by libsolv to be installed as a result
# of our jobs - those are the modules we are looking for.
newsolvables = solver.transaction().newsolvables()
@@ -596,3 +602,39 @@ class MMDResolver(object):
return set(frozenset(s2nsvc(s) for s in transactions[0])
for src_alternatives in alternatives.values()
for transactions in src_alternatives.values())
@staticmethod
def _detect_transitive_stream_collision(problems):
"""Return problem description if transitive stream collision happens
Transitive stream collision could happen if different buildrequired
modules requires same module but with different streams. For example,
app:1 --br--> gtk:1 --req--> baz:1* -------- req --------> platform:f29
| ^
+--br--> foo:1 --req--> bar:1 --req--> baz:2* --req---|
as a result, ``baz:1`` will conflicts with ``baz:2``.
:param problems: list of problems returned from ``solv.Solver.solve``.
:return: a string of problem description if transitive stream collision
is detected. The description is provided by libsolv without
changed. If no such collision, None is returned.
:rtype: str or None
"""
def find_conflicts_pairs():
for problem in problems:
for rule in problem.findallproblemrules():
info = rule.info()
if info.type == solv.Solver.SOLVER_RULE_PKG_CONFLICTS:
pair = [info.solvable.name, info.othersolvable.name]
pair.sort() # only for pretty print
yield pair
formatted_conflicts_pairs = ', '.join(
'{} and {}'.format(*item) for item in find_conflicts_pairs()
)
if formatted_conflicts_pairs:
return 'The module has conflicting buildrequires of: {}'.format(
formatted_conflicts_pairs)

View File

@@ -261,22 +261,68 @@ class TestMMDResolver:
assert expanded == expected
def test_solve_stream_conflicts(self):
# app requires both gtk:1 and foo:1.
# gtk:1 requires bar:1
# foo:1 requires bar:2.
# We cannot install both bar:1 and bar:2 in the same time.
# Therefore the solving should fail.
modules = (
("platform:f29:0:c0", {}),
('gtk:1:1:c2', {'bar': ['1']}),
('foo:1:1:c2', {'bar': ['2']}),
('bar:1:0:c2', {'platform': ['f29']}),
('bar:2:0:c2', {'platform': ['f29']}),
)
@pytest.mark.parametrize('app_buildrequires, modules, err_msg_regex', (
# app --br--> gtk:1 --req--> bar:1* ---req---> platform:f29
# \--br--> foo:1 --req--> bar:2* ---req--/
(
{'gtk': '1', 'foo': '1'},
(
('platform:f29:0:c0', {}),
('gtk:1:1:c01', {'bar': ['1']}),
('bar:1:0:c02', {'platform': ['f29']}),
('foo:1:1:c03', {'bar': ['2']}),
('bar:2:0:c04', {'platform': ['f29']}),
),
'bar:1:0:c02 and bar:2:0:c04',
),
# app --br--> gtk:1 --req--> bar:1* ----------req----------> platform:f29
# \--br--> foo:1 --req--> baz:1 --req--> bar:2* --req--/
(
{'gtk': '1', 'foo': '1'},
(
('platform:f29:0:c0', {}),
('gtk:1:1:c01', {'bar': ['1']}),
('bar:1:0:c02', {'platform': ['f29']}),
('foo:1:1:c03', {'baz': ['1']}),
('baz:1:1:c04', {'bar': ['2']}),
('bar:2:0:c05', {'platform': ['f29']}),
),
'bar:1:0:c02 and bar:2:0:c05',
),
# Test multiple conflicts pairs are detected.
# app --br--> gtk:1 --req--> bar:1* ---------req-----------\
# \--br--> foo:1 --req--> baz:1 --req--> bar:2* ---req---> platform:f29
# \--br--> pkga:1 --req--> perl:5' -------req-----------/
# \--br--> pkgb:1 --req--> perl:6' -------req-----------/
(
{'gtk': '1', 'foo': '1', 'pkga': '1', 'pkgb': '1'},
(
('platform:f29:0:c0', {}),
('gtk:1:1:c01', {'bar': ['1']}),
('bar:1:0:c02', {'platform': ['f29']}),
('foo:1:1:c03', {'baz': ['1']}),
('baz:1:1:c04', {'bar': ['2']}),
('bar:2:0:c05', {'platform': ['f29']}),
('pkga:1:0:c06', {'perl': ['5']}),
('perl:5:0:c07', {'platform': ['f29']}),
('pkgb:1:0:c08', {'perl': ['6']}),
('perl:6:0:c09', {'platform': ['f29']}),
),
# MMD Resolver should still catch a conflict
'bar:1:0:c02 and bar:2:0:c05',
),
))
def test_solve_stream_conflicts(self, app_buildrequires, modules, err_msg_regex):
for n, req in modules:
self.mmd_resolver.add_modules(self._make_mmd(n, req))
app = self._make_mmd("app:1:0", {'gtk': '1', 'foo': '1'})
with pytest.raises(RuntimeError):
app = self._make_mmd("app:1:0", app_buildrequires)
with pytest.raises(RuntimeError, match=err_msg_regex):
self.mmd_resolver.solve(app)