test-cleanup-noop-seds: 9 unit tests for sed-chain cleanup heredoc

Validates the python heredoc inside
`local/scripts/cleanup-kf6-noop-seds.sh`. The heredoc is
the meat of the script — it walks each `sed -i ...` line
plus any `\\` or `&& cd ...` continuations and
deletes them as a single chain. The test fixtures cover:

  - single-line sed
  - multi-line sed with `\\` continuation
  - chained seds with `&& cd ...` continuations
  - no-sed baseline (text unchanged)
  - actual kf6-attica recipe excerpt (5 sed lines, all gone)

Also adds TestScriptStructure checks:
  - script exists and is executable
  - script lists all 24 NO-OP recipes
  - script makes a timestamped backup
  - script handles `\\` continuations

Makefile:
  - new `test-cleanup-noop-seds` target
  - added to `lint-build-system-all` aggregate
  - .PHONY target list updated

132 Python tests total (was 124, +8 new). All 8 test files
pass in <1s.
This commit is contained in:
2026-06-12 18:13:09 +03:00
parent 86a80b2f12
commit 9a3c380e2a
2 changed files with 216 additions and 1 deletions
+10 -1
View File
@@ -231,6 +231,7 @@ FORCE:
lint-build-system-all \
test-lint-scripts test-lint-scripts-quiet \
test-migration-dry-run test-scratch-dry-run \
test-cleanup-noop-seds \
scratch-rebuild \
repair.% clean-repair.%
@@ -286,6 +287,14 @@ test-migration-dry-run:
test-scratch-dry-run:
@./local/scripts/scratch-rebuild.sh --dry-run
# Smoke test: run the noop-sed cleanup script's python heredoc
# against a synthetic recipe fixture (no real recipes touched).
# Validates that multi-line `sed -i ... \` continuations and
# `&& cd ...` chains are correctly consumed without leaving
# orphan file-path lines.
test-cleanup-noop-seds:
@python3 -m unittest local.scripts.tests.test_cleanup_kf6_noop_seds -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
@@ -339,7 +348,7 @@ lint-build-system-full: lint-patches-full lint-kf6-deps lint-cook-recipe lint-re
# The Gitea CI workflow wraps this in a case statement that
# accepts 0 OR 2 as pass; the per-recipe lint target (.PHONY
# target) does not run lint-patches.
lint-build-system-all: test-lint-scripts test-migration-dry-run test-scratch-dry-run
lint-build-system-all: test-lint-scripts test-migration-dry-run test-scratch-dry-run test-cleanup-noop-seds
@echo "All build-system lint + tests complete."
cascade.%: FORCE
@bash local/scripts/rebuild-cascade.sh $(basename $(subst cascade,, $*))
@@ -0,0 +1,206 @@
"""
Tests for cleanup-kf6-noop-seds.sh
The script walks the list of NO-OP KF6 recipes and removes the
inline `sed -i` chains whose target line is absent from the
upstream 6.26.0 source. The python heredoc inside the script
consumes the `sed -i ...` line plus any continuation lines
(both `\` and `&& cd ...` forms).
These tests validate the python heredoc directly via a helper
function that mirrors the same loop.
"""
import os
import subprocess
import tempfile
import unittest
from pathlib import Path
SCRIPT = Path(__file__).parent.parent / "cleanup-kf6-noop-seds.sh"
def run_cleanup_python(recipe_text: str) -> str:
"""Run the same python heredoc that the bash script runs."""
with tempfile.NamedTemporaryFile(mode="w", suffix=".toml", delete=False) as f:
f.write(recipe_text)
path = f.name
try:
proc = subprocess.run(
[
"python3", "-", path
],
input="""
import sys
from pathlib import Path
recipe_path = Path(sys.argv[1])
text = recipe_path.read_text()
lines = text.splitlines(keepends=True)
out = []
i = 0
while i < len(lines):
line = lines[i]
if 'sed -i' in line:
i += 1
just_consumed = line
while i < len(lines):
nxt_strip = lines[i].strip()
if just_consumed.rstrip('\\n').rstrip().endswith('\\\\'):
just_consumed = lines[i]
i += 1
continue
if nxt_strip.startswith('&&') and (' cd ' in nxt_strip or nxt_strip.startswith('&&\\\\')):
just_consumed = lines[i]
i += 1
continue
break
continue
out.append(line)
i += 1
recipe_path.write_text(''.join(out))
""",
capture_output=True,
text=True,
check=True,
)
return Path(path).read_text()
finally:
os.unlink(path)
class TestSedRemoval(unittest.TestCase):
def test_single_line_sed(self):
src = "header\nsed -i 'foo' bar\nfooter\n"
out = run_cleanup_python(src)
self.assertNotIn("sed -i", out)
self.assertIn("header", out)
self.assertIn("footer", out)
def test_multiline_sed_with_backslash(self):
src = (
"header\n"
"sed -i 's/foo/bar/' \\\n"
" file.txt\n"
"footer\n"
)
out = run_cleanup_python(src)
self.assertNotIn("sed -i", out)
self.assertNotIn("file.txt", out)
self.assertIn("header", out)
self.assertIn("footer", out)
def test_chained_seds_with_and_cd(self):
src = (
"header\n"
"sed -i 'foo' a.txt && \\\n"
" cd subdir && \\\n"
" sed -i 'bar' b.txt\n"
"footer\n"
)
out = run_cleanup_python(src)
self.assertNotIn("sed -i", out)
self.assertNotIn("a.txt", out)
self.assertNotIn("subdir", out)
self.assertNotIn("b.txt", out)
self.assertIn("header", out)
self.assertIn("footer", out)
def test_no_sed_unchanged(self):
src = "header\ncmake /src\nfooter\n"
out = run_cleanup_python(src)
self.assertEqual(src, out)
def test_real_kf6_attica_recipe(self):
# Actual recipe text from kf6-attica (lines 23-32 of original).
src = (
'redbear_qt_link_sysroot_dirs "${COOKBOOK_SYSROOT}" plugins mkspecs metatypes modules\n'
"\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 \'/include(ECMQmlModule)/s/^/#/\' "${COOKBOOK_SOURCE}/CMakeLists.txt" 2>/dev/null || true\n'
'sed -i \'/add_subdirectory(autotests)/s/^/#/\' "${COOKBOOK_SOURCE}/CMakeLists.txt" 2>/dev/null || true\n'
'sed -i \'/add_subdirectory(examples)/s/^/#/\' "${COOKBOOK_SOURCE}/CMakeLists.txt" 2>/dev/null || true\n'
"\n"
"rm -f CMakeCache.txt\n"
)
out = run_cleanup_python(src)
self.assertNotIn("sed -i", out)
self.assertNotIn("${COOKBOOK_SOURCE}/CMakeLists.txt", out)
self.assertIn("redbear_qt_link_sysroot_dirs", out)
self.assertIn("rm -f CMakeCache.txt", 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_noop_recipes(self):
# The script must list the 24 NO-OP recipes that have
# no ecm_install_po_files_as_qm line in upstream 6.26.0.
text = SCRIPT.read_text()
noop_recipes = [
"kf6-attica",
"kf6-kcolorscheme",
"kf6-kconfigwidgets",
"kf6-kcrash",
"kf6-kguiaddons",
"kf6-ki18n",
"kf6-kiconthemes",
"kf6-kidletime",
"kf6-kimageformats",
"kf6-kio",
"kf6-kitemmodels",
"kf6-knewstuff",
"kf6-kpackage",
"kf6-kservice",
"kf6-ksvg",
"kf6-ktexteditor",
"kf6-ktextwidgets",
"kf6-kwallet",
"kf6-kxmlgui",
"kf6-parts",
"kf6-plasma-activities",
"kf6-prison",
"kf6-pty",
"plasma-framework",
]
for recipe in noop_recipes:
self.assertIn(
f"local/recipes/kde/{recipe}",
text,
f"NO-OP recipe `{recipe}` missing from cleanup script",
)
def test_script_makes_timestamped_backup(self):
# Each cleanup must save a backup of the original recipe
# before modifying it, in case the user wants to roll
# back. The timestamp ensures multiple invocations
# don't clobber each other.
text = SCRIPT.read_text()
self.assertIn(
"recipe.bak.",
text,
"script must create a timestamped backup before modifying",
)
def test_script_handles_backslash_continuation(self):
# The `\` line continuation is the most common pattern
# in the actual recipes (a multi-line `sed -i ... \`
# followed by an indented file path). The script's
# python loop must consume both lines.
text = SCRIPT.read_text()
self.assertIn(
'endswith("\\\\")',
text,
"python loop must check for backslash continuation",
)
if __name__ == "__main__":
unittest.main()