"""Tests for the Phyllome OS recipe generator (cook/generate_recipe.py).""" import os import pytest import yaml import generate_recipe as gen COOK_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) @pytest.fixture def templates(): return gen.load_yaml(os.path.join(COOK_DIR, "recipe_templates.yaml")) BASE_FRAGMENTS = [ "core/base.ks", "core/locale.ks", "core/network.ks", "core/services.ks", "packages/core.ks", "packages/fedora-remix.ks", "packages/hand-picked.ks", ] def desktop_group(variants): return {"recipes": [{"name": "desktop", "variants": variants}]} # --------------------------------------------------------------------------- # expand_variants # --------------------------------------------------------------------------- def test_expand_variants_cartesian_count(): config = { "repository": ["43", "rawhide"], "desktop": "gnome", "storage": ["standard", "encrypted"], "bootloader": ["grub", "systemd-boot"], "hardware-support": [True, False], "guest-agents": [True, False], } variants = gen.expand_variants(config) assert len(variants) == 2 * 2 * 2 * 2 * 2 # Every variant keeps the scalar key and resolves one value per list key. for variant in variants: assert variant["desktop"] == "gnome" assert variant["repository"] in ("43", "rawhide") assert variant["storage"] in ("standard", "encrypted") assert variant["bootloader"] in ("grub", "systemd-boot") assert isinstance(variant["hardware-support"], bool) assert isinstance(variant["guest-agents"], bool) def test_expand_variants_all_combos_present(): config = { "storage": ["standard", "encrypted"], "bootloader": ["grub", "systemd-boot"], } combos = {(v["storage"], v["bootloader"]) for v in gen.expand_variants(config)} assert combos == { ("standard", "grub"), ("standard", "systemd-boot"), ("encrypted", "grub"), ("encrypted", "systemd-boot"), } # --------------------------------------------------------------------------- # collect_fragments # --------------------------------------------------------------------------- def test_collect_fragments_gnome_standard(templates): variant = { "repository": "43", "desktop": "gnome", "storage": "standard", "bootloader": "grub", "hardware-support": False, "guest-agents": False, } fragments, problems = gen.collect_fragments(templates, variant) assert problems == [] assert fragments[:7] == BASE_FRAGMENTS assert "repo/fedora-43-mirrors.ks" in fragments assert "storage/standard.ks" in fragments assert "storage/encrypted.ks" not in fragments assert "bootloader/grub.ks" in fragments assert "core/security/enabled.ks" in fragments assert "core/security/disabled.ks" not in fragments assert "initial-setup/server/config.ks" in fragments assert "guest-agents/base.ks" not in fragments assert "packages/hardware-support.ks" not in fragments for slug in ("desktop/gnome/config.ks", "desktop/gnome/packages.ks", "desktop/gnome/post-scripts.ks"): assert slug in fragments def test_collect_fragments_choices_default_to_first(templates): variant = {"repository": "43"} fragments, problems = gen.collect_fragments(templates, variant) assert problems == [] # Omitted choices fall back to their first declared value. assert "storage/standard.ks" in fragments assert "bootloader/grub.ks" in fragments assert "core/security/enabled.ks" in fragments assert "initial-setup/server/config.ks" in fragments def test_collect_fragments_flags(templates): off, off_problems = gen.collect_fragments( templates, {"repository": "43", "hardware-support": False, "guest-agents": False}) on, on_problems = gen.collect_fragments( templates, {"repository": "43", "hardware-support": True, "guest-agents": True}) assert off_problems == [] and on_problems == [] assert "packages/hardware-support.ks" not in off assert "guest-agents/base.ks" not in off assert "packages/hardware-support.ks" in on assert "guest-agents/base.ks" in on def test_collect_fragments_unknown_choice_reports_problem(templates): _, problems = gen.collect_fragments(templates, {"repository": "sid"}) assert any("repository" in p and "sid" in p for p in problems) def test_collect_fragments_unknown_feature_reports_problem(templates): _, problems = gen.collect_fragments(templates, {"repository": "43", "desktop": "kde"}) assert any("desktop" in p and "kde" in p for p in problems) def test_collect_fragments_dedups(templates): variant = {"repository": "43", "desktop": "gnome"} fragments, _ = gen.collect_fragments(templates, variant) assert len(fragments) == len(set(fragments)) # --------------------------------------------------------------------------- # render_filename # --------------------------------------------------------------------------- def test_render_filename(templates): manifest = desktop_group([ {"repository": "43", "desktop": "gnome", "storage": "standard", "bootloader": "grub", "hardware-support": True, "guest-agents": False}, ]) group = manifest["recipes"][0] name = gen.render_filename(templates, group["name"], gen.expand_variants(group["variants"][0])[0]) assert name == "desktop_43_standard_grub_gnome_hardware-support.cfg" def test_render_filename_key_order_irrelevant(templates): # Naming must not depend on the order keys appear in the manifest. a = {"repository": "43", "storage": "standard", "bootloader": "grub", "desktop": "gnome"} b = {"desktop": "gnome", "bootloader": "grub", "storage": "standard", "repository": "43"} assert gen.render_filename(templates, "desktop", a) == \ gen.render_filename(templates, "desktop", b) def test_render_filenames_unique_across_matrix(templates): manifest = desktop_group([ {"repository": ["43", "rawhide"], "desktop": "gnome", "storage": ["standard", "encrypted"], "bootloader": ["grub", "systemd-boot"], "hardware-support": [True, False], "guest-agents": [True, False]}, ]) names = [] for group in manifest["recipes"]: for config in group["variants"]: for variant in gen.expand_variants(config): names.append(gen.render_filename(templates, group["name"], variant)) assert len(names) == 32 assert len(set(names)) == 32 # --------------------------------------------------------------------------- # lint_manifest # --------------------------------------------------------------------------- def test_lint_clean_manifest(templates): manifest = desktop_group([ {"repository": "43", "desktop": "gnome", "storage": "standard", "bootloader": "grub", "hardware-support": False, "guest-agents": False}, ]) problems = gen.lint_manifest(manifest, templates, os.path.join(COOK_DIR, "ingredients")) assert problems == [] def test_lint_catches_missing_ingredient(templates, tmp_path): # Point lint at an empty ingredients dir so every fragment is missing. manifest = desktop_group([ {"repository": "43", "desktop": "gnome", "storage": "standard", "bootloader": "grub"}, ]) problems = gen.lint_manifest(manifest, templates, str(tmp_path)) assert any("missing ingredient fragment" in p for p in problems) def test_lint_catches_unknown_key_and_duplicates(templates): manifest = { "recipes": [ {"name": "desktop", "variants": [ {"repository": "43", "storage": "btrfs", "bootloader": "grub", "desktop": "kde", "typo-key": True}]}, {"name": "desktop", "variants": [ {"repository": "43", "storage": "standard", "bootloader": "grub"}]}, ] } problems = gen.lint_manifest(manifest, templates, os.path.join(COOK_DIR, "ingredients")) text = "\n".join(problems) assert "unknown variant key 'typo-key'" in text assert "unknown value 'btrfs'" in text assert "unknown value 'kde'" in text assert "duplicate group name 'desktop'" in text # --------------------------------------------------------------------------- # flatten / validate via pykickstart # --------------------------------------------------------------------------- def test_flatten_and_validate(tmp_path): ingredients = tmp_path / "ingredients" recipes = tmp_path / "recipes" ingredients.mkdir() recipes.mkdir() (ingredients / "core.ks").write_text("text\nrootpw --lock\n") (ingredients / "locale.ks").write_text("lang en_US.UTF-8\n") recipe = recipes / "desktop_43.cfg" recipe.write_text( "%include ../ingredients/core.ks\n%include ../ingredients/locale.ks\n") dish = gen.flatten_recipe(str(recipe)) assert "text" in dish assert "rootpw --lock" in dish assert "lang en_US.UTF-8" in dish # The flattened output is standalone: no %include may survive. assert "%include" not in dish dish_path = tmp_path / "desktop_43.dish" dish_path.write_text(dish) ok, error = gen.validate_dish(str(dish_path)) assert ok, error def test_generate_writes_recipes_and_dishes(tmp_path, templates): manifest = tmp_path / "manifest.yaml" manifest.write_text(yaml.safe_dump(desktop_group([ {"repository": "43", "desktop": "gnome", "storage": "standard", "bootloader": "grub", "hardware-support": False, "guest-agents": False}, ]))) # recipes/ must be a sibling of ingredients/ for %include resolution, so # symlink the real ingredients tree next to the generated recipes. (tmp_path / "ingredients").symlink_to(os.path.join(COOK_DIR, "ingredients"), target_is_directory=True) recipes = tmp_path / "recipes" dishes = tmp_path / "dishes" recipes.mkdir() dishes.mkdir() code = gen.generate(str(manifest), os.path.join(COOK_DIR, "recipe_templates.yaml"), str(recipes), str(dishes), "ingredients") assert code == 0 assert len(list(recipes.glob("*.cfg"))) == 1 assert len(list(dishes.glob("*.cfg"))) == 1 dish = (dishes / "desktop_43_standard_grub_gnome.cfg").read_text() assert "%include" not in dish