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:
@@ -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()
|
||||
Reference in New Issue
Block a user