diff --git a/bin/comps_filter b/bin/comps_filter index 4b6c5ccf..f2e6f1e7 100755 --- a/bin/comps_filter +++ b/bin/comps_filter @@ -29,6 +29,13 @@ def main(): help="remove all environment sections") parser.add_argument("--keep-empty-group", default=[], action="append", metavar="GROUPID", help="keep groups even if they are empty") + parser.add_argument( + "--lookaside-group", + default=[], + action="append", + metavar="GROUPID", + help="keep this group in environments even if they are not defined in the comps", + ) parser.add_argument("--no-cleanup", default=False, action="store_true", help="don't remove empty groups and categories") parser.add_argument("--no-reindent", default=False, action="store_true", @@ -46,7 +53,7 @@ def main(): f.filter_environments(opts.arch, opts.variant, opts.arch_only_environments) if not opts.no_cleanup: - f.cleanup(opts.keep_empty_group) + f.cleanup(opts.keep_empty_group, opts.lookaside_group) if opts.remove_categories: f.remove_categories() diff --git a/pungi/phases/init.py b/pungi/phases/init.py index 00cdc281..35e2ebb9 100644 --- a/pungi/phases/init.py +++ b/pungi/phases/init.py @@ -107,6 +107,19 @@ def write_arch_comps(compose, arch): UNMATCHED_GROUP_MSG = 'Variant %s.%s requires comps group %s which does not match anything in input comps file' +def get_lookaside_groups(compose, variant): + """Find all groups listed in parent variant.""" + groups = set() + if variant.parent: + groups.update(g["name"] for g in variant.parent.groups) + + for var, lookaside in compose.conf.get("variant_as_lookaside", []): + if var == variant.uid: + lookaside_variant = compose.all_variants[lookaside] + groups.update(g["name"] for g in lookaside_variant.groups) + return groups + + def write_variant_comps(compose, arch, variant): comps_file = compose.paths.work.comps(arch=arch, variant=variant) msg = "Writing comps file (arch: %s, variant: %s): %s" % (arch, variant, comps_file) @@ -123,10 +136,18 @@ def write_variant_comps(compose, arch, variant): return compose.log_debug(msg) - run(["comps_filter", "--arch=%s" % arch, "--keep-empty-group=conflicts", - "--keep-empty-group=conflicts-%s" % variant.uid.lower(), - "--variant=%s" % variant.uid, - "--output=%s" % comps_file, compose.paths.work.comps(arch="global")]) + cmd = [ + "comps_filter", + "--arch=%s" % arch, + "--keep-empty-group=conflicts", + "--keep-empty-group=conflicts-%s" % variant.uid.lower(), + "--variant=%s" % variant.uid, + "--output=%s" % comps_file, + compose.paths.work.comps(arch="global") + ] + for group in get_lookaside_groups(compose, variant): + cmd.append("--lookaside-group=%s" % group) + run(cmd) comps = CompsWrapper(comps_file) if variant.groups or variant.modules is not None or variant.type != 'variant': diff --git a/pungi/wrappers/comps.py b/pungi/wrappers/comps.py index 2ace7b21..3c26a566 100644 --- a/pungi/wrappers/comps.py +++ b/pungi/wrappers/comps.py @@ -169,11 +169,11 @@ class CompsFilter(object): for i in self.tree.xpath("//*[@xml:lang]"): i.getparent().remove(i) - def filter_environment_groups(self): + def filter_environment_groups(self, lookaside_groups=[]): """ Remove undefined groups from environments. """ - all_groups = self.tree.xpath("/comps/group/id/text()") + all_groups = self.tree.xpath("/comps/group/id/text()") + lookaside_groups for environment in self.tree.xpath("/comps/environment"): for group in environment.xpath("grouplist/groupid"): if group.text not in all_groups: @@ -199,15 +199,18 @@ class CompsFilter(object): self.tree.write(file_obj, pretty_print=self.reindent, xml_declaration=True, encoding=self.encoding) file_obj.write(b"\n") - def cleanup(self, keep_groups=[]): + def cleanup(self, keep_groups=[], lookaside_groups=[]): """ Remove empty groups, categories and environment from the comps file. Groups given in ``keep_groups`` will be preserved even if empty. + Lookaside groups are groups that are available in parent variant and + can be referenced in environments even if they are not directly defined + in the same comps file. """ self.remove_empty_groups(keep_groups) self.filter_category_groups() self.remove_empty_categories() - self.filter_environment_groups() + self.filter_environment_groups(lookaside_groups) self.remove_empty_environments() @@ -319,7 +322,7 @@ class CompsWrapper(object): append_grouplist(doc, cat_node, groups) for environment in sorted(self.comps.environments, key=attrgetter('id')): - groups = set(x.name for x in environment.group_ids) & set(self.get_comps_groups()) + groups = set(x.name for x in environment.group_ids) if not groups: continue env_node = doc.createElement("environment") diff --git a/tests/data/dummy-comps.xml b/tests/data/dummy-comps.xml index 92e99086..72ed7738 100644 --- a/tests/data/dummy-comps.xml +++ b/tests/data/dummy-comps.xml @@ -135,6 +135,17 @@ + + foobar + Foo Bar + Referencing a group from parent variant + 10 + + resilient-storage + text-internet + + + diff --git a/tests/data/dummy-variants.xml b/tests/data/dummy-variants.xml index ad485647..f8427369 100644 --- a/tests/data/dummy-variants.xml +++ b/tests/data/dummy-variants.xml @@ -9,6 +9,9 @@ resilient-storage + + foobar + diff --git a/tests/fixtures/comps-formatted.xml b/tests/fixtures/comps-formatted.xml index 58c60483..2ad7e255 100644 --- a/tests/fixtures/comps-formatted.xml +++ b/tests/fixtures/comps-formatted.xml @@ -99,6 +99,15 @@ text-internet + + empty + Empty + Should not appear in the repos. + 10 + + does-not-exist + + minimal Minimal install diff --git a/tests/fixtures/comps-group.xml b/tests/fixtures/comps-group.xml index f2985afa..d3fa8e85 100644 --- a/tests/fixtures/comps-group.xml +++ b/tests/fixtures/comps-group.xml @@ -65,6 +65,15 @@ text-internet + + empty + Empty + Should not appear in the repos. + 10 + + does-not-exist + + minimal Minimal install diff --git a/tests/test_initphase.py b/tests/test_initphase.py index 8712af3f..250c3ed3 100644 --- a/tests/test_initphase.py +++ b/tests/test_initphase.py @@ -293,6 +293,48 @@ class TestWriteVariantComps(PungiTestCase): [mock.call(variant.environments)]) self.assertEqual(comps.write_comps.mock_calls, [mock.call()]) + @mock.patch("pungi.phases.init.get_lookaside_groups") + @mock.patch("pungi.phases.init.run") + @mock.patch("pungi.phases.init.CompsWrapper") + def test_run_with_lookaside_groups(self, CompsWrapper, run, glg): + compose = DummyCompose(self.topdir, {}) + variant = compose.variants["Server"] + comps = CompsWrapper.return_value + comps.filter_groups.return_value = [] + glg.return_value = ["foo", "bar"] + + init.write_variant_comps(compose, "x86_64", variant) + + self.assertEqual( + run.mock_calls, + [ + mock.call( + [ + "comps_filter", + "--arch=x86_64", + "--keep-empty-group=conflicts", + "--keep-empty-group=conflicts-server", + "--variant=Server", + "--output=%s/work/x86_64/comps/comps-Server.x86_64.xml" % self.topdir, + self.topdir + "/work/global/comps/comps-global.xml", + "--lookaside-group=foo", + "--lookaside-group=bar", + ] + ), + ], + ) + self.assertEqual( + CompsWrapper.call_args_list, + [mock.call(self.topdir + "/work/x86_64/comps/comps-Server.x86_64.xml")], + ) + self.assertEqual( + comps.filter_groups.call_args_list, [mock.call(variant.groups)] + ) + self.assertEqual( + comps.filter_environments.mock_calls, [mock.call(variant.environments)] + ) + self.assertEqual(comps.write_comps.mock_calls, [mock.call()]) + @mock.patch('pungi.phases.init.run') @mock.patch('pungi.phases.init.CompsWrapper') def test_run_no_filter_without_groups(self, CompsWrapper, run): @@ -389,6 +431,33 @@ class TestWriteVariantComps(PungiTestCase): self.assertEqual(comps.write_comps.mock_calls, []) +class TestGetLookasideGroups(PungiTestCase): + def test_toplevel_variant(self): + compose = DummyCompose(self.topdir, {}) + self.assertItemsEqual( + init.get_lookaside_groups(compose, compose.variants["Server"]), [] + ) + + def test_classic_addon(self): + compose = DummyCompose(self.topdir, {}) + compose.setup_addon() + compose.variants["Server"].groups = [{"name": "foo"}] + self.assertItemsEqual( + init.get_lookaside_groups(compose, compose.all_variants["Server-HA"]), + ["foo"], + ) + + def test_variant_as_lookaside(self): + compose = DummyCompose( + self.topdir, {"variant_as_lookaside": [("Server", "Client")]} + ) + compose.variants["Client"].groups = [{"name": "foo"}] + self.assertItemsEqual( + init.get_lookaside_groups(compose, compose.variants["Server"]), + ["foo"], + ) + + @mock.patch("shutil.copytree") @mock.patch("pungi.phases.init.get_dir_from_scm") class TestWriteModuleDefaults(PungiTestCase):