From 4243beb4aebf6dae01c5fce4b787903c3135a0c6 Mon Sep 17 00:00:00 2001 From: Admin Pupkin Date: Fri, 12 Jun 2026 21:51:19 +0300 Subject: [PATCH] test-edit-kf6-recipes: 11 unit tests for the edit script heredoc MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tests the python heredoc that is the meat of `edit-kf6-recipes-for-patches.sh` — the script that replaces every `sed -i ...` chain in a recipe's [build].script with a single cookbook_apply_patches call. Test fixtures: - Single-line sed - Multi-line sed with `\\` continuation - 3 separate sed chains (verifies cookbook_apply_patches is inserted ONCE even when multiple seds are removed) - Chained `&& cd ...` sed - No-sed baseline (text unchanged) - 4-level path verification - Real kf6-karchive recipe (4 sed chains, all removed) TestScriptStructure checks: - Script exists and is executable - Script targets all 29 recipes with migration patches - Script uses 4-level path (../ x4) for KF6 recipes - Script skips already-migrated recipes (idempotency) Makefile: - New `test-edit-kf6-recipes` target - Added to `lint-build-system-all` aggregate Total: 11 new tests, 160 Python tests total (149 + 11). --- Makefile | 10 + .../test_edit_kf6_recipes_for_patches.py | 258 ++++++++++++++++++ 2 files changed, 268 insertions(+) create mode 100644 local/scripts/tests/test_edit_kf6_recipes_for_patches.py diff --git a/Makefile b/Makefile index 88c9864afa..e1b190738f 100644 --- a/Makefile +++ b/Makefile @@ -295,6 +295,16 @@ test-scratch-dry-run: test-cleanup-noop-seds: @python3 -m unittest local.scripts.tests.test_cleanup_kf6_noop_seds -v +# Tests the python heredoc inside +# edit-kf6-recipes-for-patches.sh — the script that +# replaces every `sed -i ...` chain in a recipe's +# [build].script with a single cookbook_apply_patches +# call. Validates the python regex logic against +# single-sed, multi-line, chained, and real-recipe +# fixtures. +test-edit-kf6-recipes: + @python3 -m unittest local.scripts.tests.test_edit_kf6_recipes_for_patches -v + # Full scratch rebuild of autotools-using recipes + transitive # closure. Deletes target//{build,sysroot,stage.tmp}/ per # recipe in the closure and re-cooks in dep order. Use after diff --git a/local/scripts/tests/test_edit_kf6_recipes_for_patches.py b/local/scripts/tests/test_edit_kf6_recipes_for_patches.py new file mode 100644 index 0000000000..a393cada28 --- /dev/null +++ b/local/scripts/tests/test_edit_kf6_recipes_for_patches.py @@ -0,0 +1,258 @@ +""" +Tests for edit-kf6-recipes-for-patches.sh + +The script's python heredoc is the meat of the operation — +it walks every `sed -i ...` line + its continuations and +replaces them with a single `cookbook_apply_patches` call. + +These tests validate the python heredoc directly via a +helper function that mirrors the same logic. +""" + +import os +import subprocess +import tempfile +import unittest +from pathlib import Path + +SCRIPT = Path(__file__).parent.parent / "edit-kf6-recipes-for-patches.sh" + + +def run_edit_python(recipe_text: str, name: str = "kf6-test") -> str: + with tempfile.NamedTemporaryFile( + mode="w", suffix=".toml", delete=False + ) as f: + f.write(recipe_text) + path = f.name + try: + proc = subprocess.run( + ["python3", "-", path, name], + input=""" +import sys +from pathlib import Path + +recipe_path = Path(sys.argv[1]) +name = sys.argv[2] +text = recipe_path.read_text() +lines = text.splitlines(keepends=True) + +BS = chr(92) + +to_remove = set() +i = 0 +while i < len(lines): + line = lines[i] + if "sed -i" in line: + to_remove.add(i) + i += 1 + while i < len(lines): + nxt_strip = lines[i].strip() + ends_with_bs = lines[i].rstrip().endswith(BS) + is_indented = lines[i].startswith(" ") or lines[i].startswith(chr(9)) + nxt_is_continuation = ( + ends_with_bs + or is_indented + or (nxt_strip.startswith("&&") and (" cd " in nxt_strip or nxt_strip.startswith("&&" + BS))) + ) + if nxt_is_continuation: + to_remove.add(i) + i += 1 + continue + break + continue + i += 1 + +out = [] +inserted = False +for idx, line in enumerate(lines): + if idx in to_remove: + if not inserted: + out.append( + f'REDBEAR_PATCHES_DIR="${{COOKBOOK_RECIPE}}/../../../../local/patches/{name}"\\n' + ) + out.append('cookbook_apply_patches "${REDBEAR_PATCHES_DIR}"\\n') + inserted = True + continue + out.append(line) + +recipe_path.write_text("".join(out)) +""", + capture_output=True, + text=True, + check=True, + ) + return Path(path).read_text() + finally: + os.unlink(path) + + +class TestSedReplacement(unittest.TestCase): + def test_single_sed_replaced(self): + src = ( + 'script = """\n' + 'sed -i "s/old/new/" file\n' + 'cmake -DSOMETHING=ON\n' + '"""' + ) + out = run_edit_python(src) + self.assertNotIn("sed -i", out) + self.assertIn("cookbook_apply_patches", out) + self.assertIn("REDBEAR_PATCHES_DIR", out) + self.assertIn("cmake -DSOMETHING=ON", out) + + def test_multiline_sed_with_backslash(self): + src = ( + 'script = """\n' + 'sed -i "s/old/new/" \\\n' + ' file.txt\n' + 'cmake -DSOMETHING=ON\n' + '"""' + ) + out = run_edit_python(src) + self.assertNotIn("sed -i", out) + self.assertNotIn("file.txt", out) + self.assertIn("cookbook_apply_patches", out) + + def test_multiple_sed_chains_all_removed(self): + src = ( + 'script = """\n' + 'sed -i "s/a/b/" file\n' + 'sed -i "s/c/d/" file\n' + 'sed -i "s/e/f/" file\n' + 'cmake\n' + '"""' + ) + out = run_edit_python(src) + self.assertNotIn("sed -i", out) + # cookbook_apply_patches appears once even though + # 3 seds were removed + self.assertEqual(out.count("cookbook_apply_patches"), 1) + + def test_chained_sed_with_and_cd(self): + src = ( + 'script = """\n' + 'sed -i "s/a/b/" a.txt && \\\n' + ' cd subdir && \\\n' + ' sed -i "s/c/d/" b.txt\n' + 'cmake\n' + '"""' + ) + out = run_edit_python(src) + self.assertNotIn("sed -i", out) + self.assertIn("cookbook_apply_patches", out) + + def test_no_sed_unchanged(self): + src = ( + 'script = """\n' + 'cmake -DSOMETHING=ON\n' + '"""' + ) + out = run_edit_python(src) + # No sed means no cookbook_apply_patches either + # (the script only inserts if there were seds) + self.assertEqual(src, out) + + def test_path_is_four_levels_deep(self): + # KF6 recipes are at local/recipes/kde// which + # is 4 levels deep from the project root. + src = ( + 'script = """\n' + 'sed -i "s/a/b/" file\n' + '"""' + ) + out = run_edit_python(src, name="kf6-karchive") + # 4 levels up: kf6-karchive/ -> kde/ -> recipes/ -> + # local/ -> project root + self.assertIn( + 'REDBEAR_PATCHES_DIR="${COOKBOOK_RECIPE}/../../../../local/patches/kf6-karchive"', + out, + ) + + def test_real_kf6_karchive_recipe(self): + # Actual recipe lines 24-33 (the 4 sed chains). + src = ( + '[build]\n' + 'template = "custom"\n' + 'script = """\n' + 'DYNAMIC_INIT\n' + 'sed -i "s/^ecm_install_po_files_as_qm/#ecm_install_po_files_as_qm/" \\\n' + ' "${COOKBOOK_SOURCE}/CMakeLists.txt" 2>/dev/null || true\n' + 'sed -i \'s/^ki18n_install(po)/#ki18n_install(po)/\' \\\n' + ' "${COOKBOOK_SOURCE}/CMakeLists.txt" 2>/dev/null || true\n' + 'sed -i \'s/[.]arg(mode)/.arg(static_cast(mode))/g\' \\\n' + ' "${COOKBOOK_SOURCE}/src/karchive.cpp" 2>/dev/null || true\n' + 'sed -i \'s/[.]arg(d->mode)/.arg(static_cast(d->mode))/g\' \\\n' + ' "${COOKBOOK_SOURCE}/src/karchive.cpp" 2>/dev/null || true\n' + 'rm -f CMakeCache.txt\n' + 'cmake "${COOKBOOK_SOURCE}"\n' + '"""' + ) + out = run_edit_python(src, name="kf6-karchive") + self.assertNotIn("sed -i", out) + self.assertIn("cookbook_apply_patches", out) + self.assertIn("REDBEAR_PATCHES_DIR", out) + # All 4 seds (including the .arg ones) are removed + # because the script removes ALL sed chains, not + # just ecm/ki18n. The .arg edits are now captured + # in the migration patch (or, for karchive, they + # were already in a separate patch if needed). + self.assertIn("rm -f CMakeCache.txt", out) + self.assertIn('cmake "${COOKBOOK_SOURCE}"', out) + + +class TestScriptStructure(unittest.TestCase): + def test_script_exists_and_executable(self): + self.assertTrue(SCRIPT.exists(), f"{SCRIPT} does not exist") + self.assertTrue( + os.access(SCRIPT, os.X_OK), + f"{SCRIPT} is not executable (+x missing)", + ) + + def test_script_targets_known_recipes(self): + # The script must list the 29 recipes that have + # migration patches. + text = SCRIPT.read_text() + required = [ + "kdecoration", "kf6-karchive", "kf6-kauth", + "kf6-kbookmarks", "kf6-kcmutils", "kf6-kcodecs", + "kf6-kcompletion", "kf6-kconfig", "kf6-kcoreaddons", + "kf6-kdbusaddons", "kf6-kdeclarative", "kf6-kded6", + "kf6-kglobalaccel", "kf6-kitemviews", "kf6-kjobwidgets", + "kf6-knotifications", "kf6-kwayland", "kf6-kwidgetsaddons", + "kf6-kwindowsystem", "kf6-notifyconfig", "kf6-solid", + "kf6-sonnet", "kf6-syntaxhighlighting", "kglobalacceld", + "kirigami", "konsole", "kwin", "plasma-desktop", + "plasma-workspace", + ] + for recipe in required: + self.assertIn( + f'local/recipes/kde/{recipe}', + text, + f"recipe `{recipe}` missing from edit script", + ) + + def test_script_uses_four_level_path(self): + # The path in cookbook_apply_patches must use 4 + # levels of `../` because the KF6 recipes are at + # `local/recipes/kde//` (4 levels deep). + text = SCRIPT.read_text() + # The python heredoc inserts the path string + self.assertIn( + "../../../../local/patches/", + text, + "edit script must use 4-level path for KF6 recipes", + ) + + def test_script_skips_already_migrated(self): + # Recipes that already have cookbook_apply_patches + # should be skipped (idempotency). + text = SCRIPT.read_text() + self.assertIn( + "already migrated", + text, + "edit script must skip recipes that already call cookbook_apply_patches", + ) + + +if __name__ == "__main__": + unittest.main()