827895d32f
The C-7 KF6 sed migration script shipped in commit ae749ffb2
was a stub with three structural problems that made it
unrunnable:
1. Called 'repo cook $recipe_dir' with a path, but the
cookbook CLI takes bare names — this would have failed
with 'Package name invalid' on first run.
2. Step 2 created an empty pristine_dir via mktemp -d but
never populated it, so the diff was always empty
(zero-byte output, 'no diff' branch taken, no patch
written).
3. Step 4 was 'SKIP — manual rewrite pending', so the
script wrote no patch even when the inline sed chains
actually edited the source.
Replace the stub with a working v2 that:
- Uses 'repo cook $name' (bare names) throughout
- Snapshots source/ → source-pristine/ BEFORE the cook
so the pristine state is real, not empty
- Runs the full cook (with -i || true so a build failure
after the sed step doesn't abort the migration — we
only need the post-sed source state)
- diffs the real pristine vs post-cook tree, with
--exclude='.git' and --exclude='target' so the diff
is the actual sed edits
- Saves the diff as
local/patches/<name>/01-initial-migration.patch with
a header explaining provenance and the cookbook_apply_patches
invocation the recipe should use
- Cleans up source-pristine/ + runs 'repo unfetch $name' so
the next migration run starts from a clean slate
Add a --dry-run mode that lists candidates without fetching,
for safe CI / smoke testing. Add --recipe=<name> and
--limit=N for targeted runs. Add --help.
Add a test escape hatch via REDBEAR_MIGRATE_RECIPES_DIR and
REDBEAR_MIGRATE_PATCHES_DIR env vars so the candidate
discovery can be exercised on a synthetic tree without
touching the live project. Also gate the cookbook-binary
check on DRY_RUN != 1 so --dry-run doesn't require a
pre-built ./target/release/repo.
13 unit tests in local/scripts/tests/test_migrate_kf6_seds.py:
TestCandidateDiscovery (7):
- discovers sed+tar recipe
- skips recipe without sed
- skips recipe with git source (Rule 1 in-tree, not
sed-migration candidates)
- --limit=N caps results
- --recipe=<name> filters
- existing patch triggers SKIP branch (via static analysis)
- --help output describes the script
TestScriptStructure (6):
- regression: uses bare names, not paths
- uses release/repo binary
- creates patches dir
- diff includes .git/target excludes
- unfetches after capture
- idempotent SKIP when patch exists
Test count: 86/86 → 99/99 (all in <1s).
The actual migration run still requires the full KF6 dep
chain to be built (qtbase, qtdeclarative, kf6-extra-cmake-modules,
plus the recipe's own deps). The 56 recipes are now
discoverable + scriptable; the recipe-by-recipe verification
+ patch validity check remains a per-recipe manual step
(open the patch, confirm the diff matches the inline sed
chain, edit [build].script to call cookbook_apply_patches,
re-cook, byte-compare stage.pkgar).
188 lines
6.7 KiB
Python
188 lines
6.7 KiB
Python
"""Tests for local/scripts/migrate-kf6-seds-to-patches.sh.
|
|
|
|
The migration script is bash; these tests validate the candidate
|
|
discovery logic in a language with proper unit test infrastructure.
|
|
The script itself is exercised manually with --dry-run on the
|
|
live tree.
|
|
"""
|
|
|
|
import os
|
|
import re
|
|
import subprocess
|
|
import tempfile
|
|
import textwrap
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
SCRIPT = Path(__file__).resolve().parent.parent / "migrate-kf6-seds-to-patches.sh"
|
|
|
|
|
|
def _make_recipe(
|
|
root: Path,
|
|
category: str,
|
|
name: str,
|
|
*,
|
|
has_sed: bool = True,
|
|
has_tar: bool = True,
|
|
) -> Path:
|
|
"""Create a recipe.toml in the synthetic tree under root/local/recipes/<cat>/<name>."""
|
|
d = root / "local" / "recipes" / category / name
|
|
d.mkdir(parents=True, exist_ok=True)
|
|
body = ["[source]"]
|
|
if has_tar:
|
|
body += [
|
|
'tar = "https://example.com/foo.tar.xz"',
|
|
'blake3 = "deadbeef"',
|
|
]
|
|
body += ["", "[build]"]
|
|
if has_sed:
|
|
body += [
|
|
'script = """',
|
|
'sed -i \'s/foo/bar/\' CMakeLists.txt',
|
|
"make install",
|
|
'"""',
|
|
]
|
|
else:
|
|
body += ['script = "cmake -B build"', ""]
|
|
(d / "recipe.toml").write_text("\n".join(body) + "\n")
|
|
return d
|
|
|
|
|
|
def _run_dry_run(root: Path, extra: list[str] | None = None) -> subprocess.CompletedProcess:
|
|
if extra is None:
|
|
extra = []
|
|
env = os.environ.copy()
|
|
env["MIGRATION_LOG_DIR"] = str(root / "logs")
|
|
env["REDBEAR_MIGRATE_RECIPES_DIR"] = str(root / "local" / "recipes")
|
|
env["REDBEAR_MIGRATE_PATCHES_DIR"] = str(root / "local" / "patches")
|
|
# The script exits 1 when no candidates are found (legitimate
|
|
# "nothing to migrate" signal). Don't raise — let the test
|
|
# inspect stdout/stderr to assert on the outcome.
|
|
return subprocess.run(
|
|
[str(SCRIPT), "--dry-run", *extra],
|
|
cwd=root,
|
|
env=env,
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=30,
|
|
check=False,
|
|
)
|
|
|
|
|
|
class TestCandidateDiscovery(unittest.TestCase):
|
|
def setUp(self):
|
|
self.tmp = tempfile.TemporaryDirectory()
|
|
self.root = Path(self.tmp.name)
|
|
|
|
def tearDown(self):
|
|
self.tmp.cleanup()
|
|
|
|
def test_discovers_sed_tar_recipe(self):
|
|
_make_recipe(self.root, "kde", "kf6-foo")
|
|
result = _run_dry_run(self.root)
|
|
self.assertIn("kf6-foo", result.stdout)
|
|
self.assertIn("Found 1 candidate", result.stdout)
|
|
|
|
def test_skips_recipe_without_sed(self):
|
|
_make_recipe(self.root, "kde", "kf6-clean", has_sed=False, has_tar=True)
|
|
result = _run_dry_run(self.root)
|
|
# The script exits 1 with a "no candidates" message to stderr.
|
|
self.assertEqual(result.returncode, 1)
|
|
self.assertIn("No sed-bearing tar-sourced recipes found", result.stderr)
|
|
|
|
def test_skips_recipe_with_git_source(self):
|
|
_make_recipe(self.root, "kde", "kf6-git", has_sed=True, has_tar=False)
|
|
recipe = self.root / "local" / "recipes" / "kde" / "kf6-git" / "recipe.toml"
|
|
text = recipe.read_text()
|
|
text = text.replace(
|
|
'tar = "https://example.com/foo.tar.xz"',
|
|
'git = "https://example.com/foo.git"',
|
|
)
|
|
text = text.replace('blake3 = "deadbeef"', 'rev = "main"')
|
|
recipe.write_text(text)
|
|
result = _run_dry_run(self.root)
|
|
self.assertEqual(result.returncode, 1)
|
|
self.assertIn("No sed-bearing tar-sourced recipes found", result.stderr)
|
|
|
|
def test_limit_caps_results(self):
|
|
for i in range(5):
|
|
_make_recipe(self.root, "kde", f"kf6-r{i}")
|
|
result = _run_dry_run(self.root, ["--limit=2"])
|
|
self.assertIn("Found 2 candidate", result.stdout)
|
|
self.assertNotIn("kf6-r2", result.stdout)
|
|
self.assertNotIn("kf6-r3", result.stdout)
|
|
|
|
def test_recipe_filter_picks_specific_name(self):
|
|
_make_recipe(self.root, "kde", "kf6-a")
|
|
_make_recipe(self.root, "kde", "kf6-b")
|
|
result = _run_dry_run(self.root, ["--recipe=kf6-b"])
|
|
self.assertIn("Found 1 candidate", result.stdout)
|
|
self.assertIn("kf6-b", result.stdout)
|
|
self.assertNotIn("kf6-a", result.stdout)
|
|
|
|
def test_skips_existing_patch(self):
|
|
_make_recipe(self.root, "kde", "kf6-existing")
|
|
patch_dir = self.root / "local" / "patches" / "kf6-existing"
|
|
patch_dir.mkdir(parents=True)
|
|
(patch_dir / "01-initial-migration.patch").write_text("# existing")
|
|
# We can't easily exercise the SKIP path without network;
|
|
# the dry-run mode short-circuits before the SKIP check.
|
|
# Validate the script source has the skip branch instead.
|
|
script_text = SCRIPT.read_text()
|
|
self.assertIn('if [ -e "$patch_file" ]', script_text)
|
|
self.assertIn("SKIP — patch already exists", script_text)
|
|
|
|
def test_help_output_describes_script(self):
|
|
result = subprocess.run(
|
|
[str(SCRIPT), "--help"],
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=5,
|
|
)
|
|
self.assertEqual(result.returncode, 0)
|
|
self.assertIn("C-7 KF6 sed migration", result.stdout)
|
|
self.assertIn("--dry-run", result.stdout)
|
|
self.assertIn("--recipe=", result.stdout)
|
|
self.assertIn("--limit=", result.stdout)
|
|
|
|
|
|
class TestScriptStructure(unittest.TestCase):
|
|
def test_uses_repo_cook_bare_names(self):
|
|
# The original v1 of this script called `repo cook
|
|
# <recipe_dir>` with a path, which is wrong. The v2 must
|
|
# use bare names. This regression test catches the
|
|
# "use paths instead of names" mistake.
|
|
text = SCRIPT.read_text()
|
|
self.assertIn('release/repo cook "$name"', text)
|
|
self.assertIn('release/repo fetch "$name"', text)
|
|
self.assertNotIn('repo cook "$recipe_dir"', text)
|
|
self.assertNotIn('repo fetch "$recipe_dir"', text)
|
|
|
|
def test_uses_release_repo_binary(self):
|
|
text = SCRIPT.read_text()
|
|
self.assertIn("./target/release/repo", text)
|
|
|
|
def test_creates_patches_dir(self):
|
|
text = SCRIPT.read_text()
|
|
self.assertIn("mkdir -p \"$patch_dir\"", text)
|
|
|
|
def test_diff_includes_target_exclude(self):
|
|
text = SCRIPT.read_text()
|
|
self.assertIn("--exclude='.git'", text)
|
|
self.assertIn("--exclude='target'", text)
|
|
|
|
def test_unfetch_after_capture(self):
|
|
# After capturing the diff, the script should uncook
|
|
# (unfetch) so the source is clean for the next run.
|
|
text = SCRIPT.read_text()
|
|
self.assertIn('release/repo unfetch "$name"', text)
|
|
|
|
def test_idempotent_skip(self):
|
|
# If a patch already exists, the script reports SKIP.
|
|
text = SCRIPT.read_text()
|
|
self.assertIn("SKIP — patch already exists", text)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|