""" Tests for cookbook_apply_patches end-to-end These tests extract the cookbook_apply_patches function from src/cook/script.rs and run it against real recipe sources + migration patches. Verifies: 1. First apply: patch is applied successfully 2. Idempotency: second apply reports "already applied" 3. The post-patch source matches the expected state (e.g. the ecm_install_po_files_as_qm line is commented out) 4. The 4-level path resolution works This is the integration test for C-7 step 2 — the recipe edit that replaces inline `sed -i` chains with a `cookbook_apply_patches` call. """ import os import shutil import subprocess import tempfile import unittest from pathlib import Path REPO_ROOT = Path(__file__).parent.parent.parent.parent SCRIPT_RS = REPO_ROOT / "src" / "cook" / "script.rs" COOKBOOK_RECIPE = REPO_ROOT / "local" / "recipes" / "kde" / "kf6-karchive" COOKBOOK_SOURCE = COOKBOOK_RECIPE / "source" COOKBOOK_PRISTINE = COOKBOOK_RECIPE / "source-pristine" PATCH_DIR = REPO_ROOT / "local" / "patches" / "kf6-karchive" PATCH_FILE = PATCH_DIR / "01-initial-migration.patch" def extract_cookbook_helper() -> str: """Extract the cookbook_apply_patches function from src/cook/script.rs and return it as a bash source string. """ text = SCRIPT_RS.read_text() lines = text.splitlines() start = end = None for idx, line in enumerate(lines): if line.startswith("function cookbook_apply_patches {"): start = idx continue if start is not None and line.startswith("}"): end = idx + 1 break if start is None or end is None: raise RuntimeError("could not find cookbook_apply_patches in script.rs") extracted = lines[start:end] return "\n".join(extracted) def run_helper_in_subshell(args: dict[str, str]) -> tuple[int, str]: """Run the cookbook helper in a subshell with the given env vars. Returns (exit_code, output). """ helper_src = extract_cookbook_helper() env_assignments = " ".join(f'{k}="{v}"' for k, v in args.items()) cmd = f'{env_assignments} bash -c \'{helper_src.replace(chr(39), chr(39) + chr(92) + chr(39) + chr(39))}; cookbook_apply_patches "$1" 2>&1\' _ "{args["COOKBOOK_PATCHES_DIR"]}"' proc = subprocess.run( cmd, shell=True, capture_output=True, text=True, cwd=args["COOKBOOK_SOURCE"], ) return proc.returncode, proc.stdout + proc.stderr class TestCookbookApplyPatches(unittest.TestCase): @classmethod def setUpClass(cls): # Verify the test fixtures exist. if not COOKBOOK_PRISTINE.exists(): raise unittest.SkipTest( f"pristine dir missing: {COOKBOOK_PRISTINE}. " "Run `repo fetch kf6-karchive` first." ) if not PATCH_FILE.exists(): raise unittest.SkipTest(f"patch missing: {PATCH_FILE}") def setUp(self): # Reset source to pristine before each test. if COOKBOOK_SOURCE.exists(): shutil.rmtree(COOKBOOK_SOURCE) shutil.copytree(COOKBOOK_PRISTINE, COOKBOOK_SOURCE) def test_first_apply_succeeds(self): exit_code, output = run_helper_in_subshell( { "COOKBOOK_RECIPE": str(COOKBOOK_RECIPE), "COOKBOOK_SOURCE": str(COOKBOOK_SOURCE), "COOKBOOK_BUILD": str(COOKBOOK_SOURCE / "build"), "COOKBOOK_PATCHES_DIR": str(PATCH_DIR), } ) self.assertEqual(exit_code, 0, f"helper failed: {output}") self.assertIn("applying 01-initial-migration.patch", output) self.assertIn("applied=1", output) self.assertIn("skipped=0", output) self.assertIn("failed=0", output) def test_idempotency_second_apply_skips(self): common_args = { "COOKBOOK_RECIPE": str(COOKBOOK_RECIPE), "COOKBOOK_SOURCE": str(COOKBOOK_SOURCE), "COOKBOOK_BUILD": str(COOKBOOK_SOURCE / "build"), "COOKBOOK_PATCHES_DIR": str(PATCH_DIR), } # First apply. exit1, out1 = run_helper_in_subshell(common_args) self.assertEqual(exit1, 0, f"first apply failed: {out1}") # Second apply. exit2, out2 = run_helper_in_subshell(common_args) self.assertEqual(exit2, 0, f"second apply failed: {out2}") self.assertIn("already applied, skipping", out2) self.assertIn("applied=0", out2) self.assertIn("skipped=1", out2) self.assertIn("failed=0", out2) def test_apply_modifies_source_correctly(self): run_helper_in_subshell( { "COOKBOOK_RECIPE": str(COOKBOOK_RECIPE), "COOKBOOK_SOURCE": str(COOKBOOK_SOURCE), "COOKBOOK_BUILD": str(COOKBOOK_SOURCE / "build"), "COOKBOOK_PATCHES_DIR": str(PATCH_DIR), } ) # The patch comments out the # ecm_install_po_files_as_qm line. cmake = (COOKBOOK_SOURCE / "CMakeLists.txt").read_text() self.assertIn("#ecm_install_po_files_as_qm", cmake) self.assertNotIn("^ecm_install_po_files_as_qm", cmake) def test_four_level_path_resolution(self): # The recipe at local/recipes/kde/kf6-karchive is 4 # levels deep. The path # ${COOKBOOK_RECIPE}/../../../../local/patches/ # must resolve to the patches dir. from pathlib import Path expected = (COOKBOOK_RECIPE / "../../../../local/patches/kf6-karchive").resolve() self.assertEqual(expected, PATCH_DIR.resolve()) if __name__ == "__main__": unittest.main()