diff --git a/module_build_service/mmd_resolver.py b/module_build_service/mmd_resolver.py index 76014284..759eabcd 100644 --- a/module_build_service/mmd_resolver.py +++ b/module_build_service/mmd_resolver.py @@ -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) diff --git a/tests/test_mmd_resolver.py b/tests/test_mmd_resolver.py index 4c324570..7a9f6c8d 100644 --- a/tests/test_mmd_resolver.py +++ b/tests/test_mmd_resolver.py @@ -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)