Files
RedBear-OS/local/scripts/tests/test_migrate_kf6_seds.py
T
kellito 827895d32f build: rewrite C-7 KF6 sed migration script + add 13 tests
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).
2026-06-12 15:37:58 +03:00

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