forked from roots/phyllomeos
Replace the hardcoded TEMPLATES dict and the ksflatten-relative wrapper with a single data-driven pipeline: - recipe_templates.yaml becomes the single source of truth with three sections: base (always included), choices (exactly-one per category, the invariant pykickstart cannot check) and features (additive flags or one-of values) - generate_recipe.py is a pure YAML consumer: it resolves ingredients, renders dishes via pykickstart in-process (no ksflatten binary, no %include path munging), lints the invariants pykickstart cannot check (exactly-one per choice category, known keys, existing fragments, unique filenames) and validates every generated dish - dish names are derived by a generic rule with canonical category order from the templates, independent of YAML key order; no committed generated files means no name burn-in - recipes/ and dishes/ become gitignored build products; the 68 tracked generated files (already out of sync with the manifest) are removed - delete dead ingredients (section-data/, validation/, initial-setup/ desktop/, live/hypervisor.ks, rpmfusion-nonfree.ks), the stale all-ingredients.cfg (replaced by 'make inventory') and the bin/generate-recipe and bin/ksflatten-relative wrappers - Makefile: single 'make all' step plus lint, validate, inventory, test - add 16 pytest tests covering expansion, selection, naming, lint and flatten/validate round-trips
273 lines
10 KiB
Python
273 lines
10 KiB
Python
"""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 |