Manifest groups get a tier; only `default` is generated (make all TIER=..., --tier). Default: phyllomeos (GNOME + virt-manager) and phyllomeos-headless, Fedora 44, systemd-boot, CPU-agnostic hypervisor ingredient (hypervisor_type: any). guest tier: guest-server, guest-desktop. experimental: biosboot/grub, encrypted, rawhide. Generator clears stale recipes/dishes before writing. build-image.sh: --dish and --tier; deploy.sh: --tier. build-iso.yaml replaced by build-image.yaml: raw images for both editions on the fedora:host runner (TMPDIR=/var/tmp, max-parallel 1), attached with the flattened kickstarts to tagged releases. Live edition no longer built. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
355 lines
14 KiB
Python
355 lines
14 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-explicit.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
|
|
|
|
|
|
@pytest.mark.parametrize("release", ["43", "44"])
|
|
def test_collect_fragments_release_repository(templates, release):
|
|
variant = {"repository": release}
|
|
fragments, problems = gen.collect_fragments(templates, variant)
|
|
assert problems == []
|
|
assert f"repo/fedora-{release}-mirrors.ks" 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
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# tiers and the real manifest
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def real_manifest():
|
|
return gen.load_yaml(os.path.join(COOK_DIR, "recipes_manifest.yaml"))
|
|
|
|
|
|
def dish_names(manifest, templates, tiers):
|
|
return sorted(
|
|
gen.render_filename(templates, group["name"], variant)
|
|
for group in gen.select_groups(manifest, tiers)
|
|
for config in group["variants"]
|
|
for variant in gen.expand_variants(config))
|
|
|
|
|
|
def test_select_groups_defaults_untiered_groups_to_default():
|
|
manifest = {"recipes": [{"name": "a"}, {"name": "b", "tier": "guest"}]}
|
|
assert [g["name"] for g in gen.select_groups(manifest, ("default",))] == ["a"]
|
|
assert [g["name"] for g in gen.select_groups(manifest, ("guest",))] == ["b"]
|
|
assert [g["name"] for g in gen.select_groups(manifest, ("all",))] == ["a", "b"]
|
|
|
|
|
|
def test_real_manifest_lints_clean_across_all_tiers(templates):
|
|
problems = gen.lint_manifest(real_manifest(), templates,
|
|
os.path.join(COOK_DIR, "ingredients"))
|
|
assert problems == []
|
|
|
|
|
|
def test_default_tier_is_the_two_editions(templates):
|
|
names = dish_names(real_manifest(), templates, ("default",))
|
|
assert len(names) == 2
|
|
assert sorted(n.split("_")[0] for n in names) == ["phyllomeos", "phyllomeos-headless"]
|
|
|
|
|
|
def test_guest_tier_is_server_and_desktop(templates):
|
|
names = dish_names(real_manifest(), templates, ("guest",))
|
|
assert sorted(n.split("_")[0] for n in names) == ["guest-desktop", "guest-server"]
|
|
assert all("guest-agents" in n for n in names)
|
|
|
|
|
|
def test_shipping_dishes_contents(templates):
|
|
manifest = real_manifest()
|
|
for group in gen.select_groups(manifest, ("default",)):
|
|
variant = gen.expand_variants(group["variants"][0])[0]
|
|
fragments, problems = gen.collect_fragments(templates, variant)
|
|
assert problems == []
|
|
assert "hypervisor/cpu-agnostic.ks" in fragments
|
|
assert "bootloader/systemd-boot.ks" in fragments
|
|
assert "storage/standard.ks" in fragments
|
|
assert ("desktop/gnome/config.ks" in fragments) == (group["name"] == "phyllomeos")
|
|
|
|
|
|
def test_generate_only_writes_requested_tier_and_clears_stale(tmp_path):
|
|
(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()
|
|
(dishes / "stale.cfg").write_text("stale")
|
|
(recipes / "stale.cfg").write_text("stale")
|
|
templates_path = os.path.join(COOK_DIR, "recipe_templates.yaml")
|
|
manifest_path = os.path.join(COOK_DIR, "recipes_manifest.yaml")
|
|
|
|
code = gen.generate(manifest_path, templates_path, str(recipes), str(dishes),
|
|
"ingredients", tiers=("guest",))
|
|
assert code == 0
|
|
names = sorted(p.name.split("_")[0] for p in dishes.glob("*.cfg"))
|
|
assert names == ["guest-desktop", "guest-server"]
|
|
assert not (dishes / "stale.cfg").exists()
|
|
assert not (recipes / "stale.cfg").exists()
|