test-edit-kf6-recipes: 11 unit tests for the edit script heredoc
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).
This commit is contained in:
@@ -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/<arch>/{build,sysroot,stage.tmp}/ per
|
||||
# recipe in the closure and re-cooks in dep order. Use after
|
||||
|
||||
@@ -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/<name>/ 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<int>(mode))/g\' \\\n'
|
||||
' "${COOKBOOK_SOURCE}/src/karchive.cpp" 2>/dev/null || true\n'
|
||||
'sed -i \'s/[.]arg(d->mode)/.arg(static_cast<int>(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/<name>/` (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()
|
||||
Reference in New Issue
Block a user