""" 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()