From 879d0daca0a044fc3f495602a652d25a7d580e22 Mon Sep 17 00:00:00 2001 From: Chenxiong Qi Date: Nov 06 2018 12:27:43 +0000 Subject: 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 --- diff --git a/module_build_service/mmd_resolver.py b/module_build_service/mmd_resolver.py index 7601428..759eabc 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 4c32457..7a9f6c8 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)