# -*- coding: utf-8 -*- # # Copyright © 2018 Red Hat, Inc. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. # # Written by Jan Kaluža # Igor Gnatenko import collections import pytest from module_build_service.mmd_resolver import MMDResolver from module_build_service import Modulemd from module_build_service import glib class TestMMDResolver: def setup_method(self, test_method): self.mmd_resolver = MMDResolver() def teardown_method(self, test_method): pass @staticmethod def _make_mmd(nsvc, requires, xmd_buildrequires=None, virtual_streams=None): name, stream, version = nsvc.split(":", 2) mmd = Modulemd.Module() mmd.set_mdversion(2) mmd.set_name(name) mmd.set_stream(stream) mmd.set_summary("foo") mmd.set_description("foo") licenses = Modulemd.SimpleSet() licenses.add("GPL") mmd.set_module_licenses(licenses) xmd = glib.from_variant_dict(mmd.get_xmd()) xmd["mbs"] = {} xmd["mbs"]["buildrequires"] = {} if xmd_buildrequires: for ns in xmd_buildrequires: n, s = ns.split(":") xmd["mbs"]["buildrequires"][n] = {"stream": s} if virtual_streams: xmd["mbs"]["virtual_streams"] = virtual_streams mmd.set_xmd(glib.dict_values(xmd)) if ":" in version: version, context = version.split(":") mmd.set_context(context) add_requires = Modulemd.Dependencies.add_requires else: add_requires = Modulemd.Dependencies.add_buildrequires mmd.set_version(int(version)) if not isinstance(requires, list): requires = [requires] else: requires = requires deps_list = [] for reqs in requires: deps = Modulemd.Dependencies() for req_name, req_streams in reqs.items(): add_requires(deps, req_name, req_streams) deps_list.append(deps) mmd.set_dependencies(deps_list) return mmd @pytest.mark.parametrize( "deps, expected", ( ([], "None"), ([{"x": []}], "module(x)"), ([{"x": ["1"]}], "(module(x) with module(x:1))"), ([{"x": ["-1"]}], "(module(x) without module(x:1))"), ([{"x": ["1", "2"]}], "(module(x) with (module(x:1) or module(x:2)))"), ([{"x": ["-1", "2"]}], "(module(x) with module(x:2))"), ([{"x": [], "y": []}], "(module(x) and module(y))"), ([{"x": []}, {"y": []}], "(module(x) or module(y))"), ) ) def test_deps2reqs(self, deps, expected): # Sort by keys here to avoid unordered dicts deps = [collections.OrderedDict(sorted(dep.items())) for dep in deps] reqs = self.mmd_resolver._deps2reqs(deps) assert str(reqs) == expected @pytest.mark.parametrize( "buildrequires, expected", ( ({"platform": []}, [ [["platform:f28:0:c0:x86_64"], ["platform:f29:0:c0:x86_64"]], ]), ({"platform": ["f28"]}, [ [["platform:f28:0:c0:x86_64"]], ]), ({"platform": ["-f28"]}, [ [["platform:f29:0:c0:x86_64"]], ]), ({"gtk": [], "qt": []}, [ [["gtk:3:0:c8:x86_64", "qt:4:0:c8:x86_64", "platform:f28:0:c0:x86_64"], ["gtk:4:0:c8:x86_64", "qt:4:0:c8:x86_64", "platform:f28:0:c0:x86_64"], ["gtk:3:0:c8:x86_64", "qt:5:0:c8:x86_64", "platform:f28:0:c0:x86_64"], ["gtk:4:0:c8:x86_64", "qt:5:0:c8:x86_64", "platform:f28:0:c0:x86_64"]], ]), ({"gtk": [], "qt": [], "platform": []}, [ [["gtk:3:0:c8:x86_64", "qt:4:0:c8:x86_64", "platform:f28:0:c0:x86_64"], ["gtk:4:0:c8:x86_64", "qt:4:0:c8:x86_64", "platform:f28:0:c0:x86_64"], ["gtk:3:0:c8:x86_64", "qt:5:0:c8:x86_64", "platform:f28:0:c0:x86_64"], ["gtk:4:0:c8:x86_64", "qt:5:0:c8:x86_64", "platform:f28:0:c0:x86_64"], ["gtk:3:0:c9:x86_64", "qt:4:0:c9:x86_64", "platform:f29:0:c0:x86_64"], ["gtk:4:0:c9:x86_64", "qt:4:0:c9:x86_64", "platform:f29:0:c0:x86_64"], ["gtk:3:0:c9:x86_64", "qt:5:0:c9:x86_64", "platform:f29:0:c0:x86_64"], ["gtk:4:0:c9:x86_64", "qt:5:0:c9:x86_64", "platform:f29:0:c0:x86_64"]], ]), ([{"qt": [], "platform": ["f28"]}, {"gtk": [], "platform": ["-f28"]}], [ [["qt:4:0:c8:x86_64", "platform:f28:0:c0:x86_64"], ["qt:5:0:c8:x86_64", "platform:f28:0:c0:x86_64"]], [["gtk:3:0:c9:x86_64", "platform:f29:0:c0:x86_64"], ["gtk:4:0:c9:x86_64", "platform:f29:0:c0:x86_64"]], ]), ({"mess": []}, [ [["mess:1:0:c0:x86_64", "gtk:3:0:c8:x86_64", "platform:f28:0:c0:x86_64"]], ]), ({"mess": [], "platform": []}, [ [["mess:1:0:c0:x86_64", "gtk:3:0:c8:x86_64", "platform:f28:0:c0:x86_64"], ["mess:1:0:c0:x86_64", "gtk:4:0:c9:x86_64", "platform:f29:0:c0:x86_64"]], ]), ) ) def test_solve(self, buildrequires, expected): modules = ( ("platform:f28:0:c0", {}), ("platform:f29:0:c0", {}), ("gtk:3:0:c8", {"platform": ["f28"]}), ("gtk:3:0:c9", {"platform": ["f29"]}), ("gtk:4:0:c8", {"platform": ["f28"]}), ("gtk:4:0:c9", {"platform": ["f29"]}), ("qt:4:0:c8", {"platform": ["f28"]}), ("qt:4:0:c9", {"platform": ["f29"]}), ("qt:5:0:c8", {"platform": ["f28"]}), ("qt:5:0:c9", {"platform": ["f29"]}), ("mess:1:0:c0", [{"gtk": ["3"], "platform": ["f28"]}, {"gtk": ["4"], "platform": ["-f28"]}]), ) for n, req in modules: self.mmd_resolver.add_modules(self._make_mmd(n, req)) app = self._make_mmd("app:1:0", buildrequires) expanded = self.mmd_resolver.solve(app) expected = set(frozenset(["app:1:0:%d:src" % c] + e) for c, exp in enumerate(expected) for e in exp) assert expanded == expected @pytest.mark.parametrize( "buildrequires, xmd_buildrequires, expected", ( # BR all platform streams -> build for all platform streams. ({"platform": []}, {}, [ [["platform:el8.2.0:0:c0:x86_64"], ["platform:el8.1.0:0:c0:x86_64"], ["platform:el8.0.0:0:c0:x86_64"], ["platform:el7.6.0:0:c0:x86_64"]], ]), # BR "el8" platform stream -> build for all el8 platform streams. ({"platform": ["el8"]}, {}, [ [["platform:el8.2.0:0:c0:x86_64"], ["platform:el8.1.0:0:c0:x86_64"], ["platform:el8.0.0:0:c0:x86_64"]], ]), # BR "el8.1.0" platfrom stream -> build just for el8.1.0. ({"platform": ["el8"]}, ["platform:el8.1.0"], [ [["platform:el8.1.0:0:c0:x86_64"]], ]), # BR platform:el8.1.0 and gtk:3, which is not built against el8.1.0, # but it is built only against el8.0.0 -> cherry-pick gtk:3 from el8.0.0 # and build once against platform:el8.1.0. ({"platform": ["el8"], "gtk": ["3"]}, ["platform:el8.1.0"], [ [["platform:el8.1.0:0:c0:x86_64", "gtk:3:0:c8:x86_64", ]], ]), # BR platform:el8.2.0 and gtk:3, this time gtk:3 build against el8.2.0 exists # -> use both platform and gtk from el8.2.0 and build once. ({"platform": ["el8"], "gtk": ["3"]}, ["platform:el8.2.0"], [ [["platform:el8.2.0:0:c0:x86_64", "gtk:3:1:c8:x86_64", ]], ]), # BR platform:el8.2.0 and mess:1 which is built against platform:el8.1.0 and # requires gtk:3 which is built against platform:el8.2.0 and platform:el8.0.0 # -> Use platform:el8.2.0 and # -> cherry-pick mess:1 from el8.1.0 and # -> use gtk:3:1 from el8.2.0. ({"platform": ["el8"], "mess": ["1"]}, ["platform:el8.2.0"], [ [["platform:el8.2.0:0:c0:x86_64", "mess:1:0:c0:x86_64", "gtk:3:1:c8:x86_64", ]], ]), # BR platform:el8.1.0 and mess:1 which is built against platform:el8.1.0 and # requires gtk:3 which is built against platform:el8.2.0 and platform:el8.0.0 # -> Use platform:el8.1.0 and # -> Used mess:1 from el8.1.0 and # -> cherry-pick gtk:3:0 from el8.0.0. ({"platform": ["el8"], "mess": ["1"]}, ["platform:el8.1.0"], [ [["platform:el8.1.0:0:c0:x86_64", "mess:1:0:c0:x86_64", "gtk:3:0:c8:x86_64", ]], ]), # BR platform:el8.0.0 and mess:1 which is built against platform:el8.1.0 and # requires gtk:3 which is built against platform:el8.2.0 and platform:el8.0.0 # -> No valid combination, because mess:1 is only available in el8.1.0 and later. ({"platform": ["el8"], "mess": ["1"]}, ["platform:el8.0.0"], []), # This is undefined... it might build just once against latest platform or # against all the platforms... we don't know # ({"platform": ["el8"], "gtk": ["3"]}, {}, [ # [["platform:el8.2.0:0:c0:x86_64", "gtk:3:1:c8:x86_64"]], # ]), ) ) def test_solve_virtual_streams(self, buildrequires, xmd_buildrequires, expected): modules = ( # (nsvc, buildrequires, expanded_buildrequires, virtual_streams) ("platform:el8.0.0:0:c0", {}, {}, ["el8"]), ("platform:el8.1.0:0:c0", {}, {}, ["el8"]), ("platform:el8.2.0:0:c0", {}, {}, ["el8"]), ("platform:el7.6.0:0:c0", {}, {}, ["el7"]), ("gtk:3:0:c8", {"platform": ["el8"]}, {"platform:el8.0.0"}, None), ("gtk:3:1:c8", {"platform": ["el8"]}, {"platform:el8.2.0"}, None), ("mess:1:0:c0", [{"gtk": ["3"], "platform": ["el8"]}], {"platform:el8.1.0"}, None), ) for n, req, xmd_br, virtual_streams in modules: self.mmd_resolver.add_modules(self._make_mmd( n, req, xmd_br, virtual_streams)) app = self._make_mmd("app:1:0", buildrequires, xmd_buildrequires) if not expected: with pytest.raises(RuntimeError): self.mmd_resolver.solve(app) return else: expanded = self.mmd_resolver.solve(app) expected = set(frozenset(["app:1:0:%d:src" % c] + e) for c, exp in enumerate(expected) for e in exp) assert expanded == expected @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", app_buildrequires) with pytest.raises(RuntimeError, match=err_msg_regex): self.mmd_resolver.solve(app)