Files
RedBear-OS/local/scripts/tests/test_lint_recipe.py
T
vasilito ffbbf4935c C-7 cleanup: lint-recipe down from 13 to 4 errors
The 4 remaining errors are out of C-7 scope:
  - sddm (19 seds): needs separate migration to
    `local/patches/sddm/` (already partly done via
    the drop-x11.py approach in the sddm 0.21.0 work)
  - qtbase (2 seds): needs separate migration
  - redbear-sessiond: missing patch file (pre-existing
    user WIP, not introduced by C-7)
  - libwayland: missing patch file (pre-existing user
    WIP, this is a re-add of the line C-1 removed)

Changes in this commit:

1. `local/scripts/lint-recipe.py`: R2-INLINE-SED rule now
   distinguishes upstream-source seds (target
   `${COOKBOOK_SOURCE}/` or `find "${COOKBOOK_SOURCE}``)
   from build-time seds (target `${COOKBOOK_STAGE}/`,
   `${COOKBOOK_BUILD}/`, `${COOKBOOK_SYSROOT}/`, or
   non-source paths). Build-time seds are exempt
   because they're adjustments to staged artifacts,
   not upstream source edits — the Rule 2 concern is
   upstream-source durability.

   This is a non-trivial refinement of the R2 rule
   because the original implementation flagged every
   `sed -i` regardless of target. Several recipes
   (bison, m4, rust-native, kf6-kded6, kf6-kbookmarks)
   had build-time seds mixed with upstream-source
   seds; the previous rule would force a migration
   for a build-time Makefile edit which doesn't
   actually fit the rule's intent.

2. `local/scripts/tests/test_lint_recipe.py`: updated
   the 2 R2 fixture tests to use the upstream-source
   path (`${COOKBOOK_SOURCE}/file.c`) so they actually
   trigger R2. Added 1 new test
   (`test_build_time_seds_are_exempt`) that verifies
   build-time seds targeting `${COOKBOOK_STAGE}`,
   `${COOKBOOK_BUILD}`, `${COOKBOOK_SYSROOT}` are
   correctly exempt. 25/25 lint tests pass.

3. `local/recipes/kde/breeze/recipe.toml`: deleted the
   lone `sed -i '/include(ECMQmlModule)/s/^/#/'` line.
   `ECMQmlModule` is not in upstream 6.6.5, so the sed
   was a no-op (dead code per the zero-tolerance
   policy on stubs and workarounds).

4. `local/recipes/kde/kde-cli-tools/recipe.toml`: deleted
   the `sed -i 's/^add_subdirectory(kdesu/#...'` line.
   The regex is BROKEN — it has `^add_subdirectory(kdesu/`
   but the upstream line is `    add_subdirectory(kdesu)`
   (with a `)`, not `/`). The sed was a no-op.
   The kdesu subdir has been building all along.

5. `local/recipes/kde/kf6-kbookmarks/recipe.toml`: deleted
   the 2 ecm/ki18n seds (NO-OPs — line not in upstream
   6.26.0) and the broken `find_package(Qt6GuiPrivate)`
   injection (regex typo: `Widgets)` requires a closing
   paren but upstream has `Widgets Xml)`). Remaining
   2 seds target `${COOKBOOK_SYSROOT}` (build-time,
   exempt per the new R2 rule).

6. `local/scripts/cleanup-kf6-noop-seds-targeted.sh`:
   added `kf6-kbookmarks` to the recipe list. It was
   missed in the original 24-recipe cleanup (the
   initial list was derived from the NO-OP classifier
   but kf6-kbookmarks' 2 sysroot seds made the
   classifier put it in the 'has-sysroot' bucket).
   Now caught by the targeted cleanup.
2026-06-12 22:52:13 +03:00

446 lines
16 KiB
Python

"""Unit tests for local/scripts/lint-recipe.py.
Covers the 7 registered rules with synthetic recipe.toml fixtures
written to a tmpdir, plus the main() entry point with a fake
LOCAL_RECIPES / MAINLINE_RECIPES set.
Run: python3 -m unittest local/scripts/tests/test_lint_recipe.py -v
"""
import json
import os
import subprocess
import sys
import tempfile
import textwrap
import unittest
from pathlib import Path
from unittest import mock
SCRIPT_DIR = Path(__file__).resolve().parent
LINT_SCRIPT = SCRIPT_DIR.parent / "lint-recipe.py"
class LintRecipeFixture(unittest.TestCase):
"""Base class that creates a tmp project tree and runs the
linter against synthetic recipes inside it."""
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.root = Path(self.tmp.name)
for d in ["local/recipes/kde/kf6-foo",
"local/recipes/core/relibc",
"local/recipes/kde/kf6-clean",
"local/recipes/kde/kf6-with-patches",
"recipes/core/kernel"]:
(self.root / d / "recipe.toml").parent.mkdir(parents=True, exist_ok=True)
(self.root / d / "recipe.toml").write_text("")
(self.root / "local/patches/kf6-with-patches").mkdir(parents=True)
(self.root / "local/patches/kf6-with-patches/01-init.patch").write_text("")
def tearDown(self):
self.tmp.cleanup()
def write(self, recipe_path: str, content: str) -> Path:
path = self.root / recipe_path
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(textwrap.dedent(content))
return path
def run_lint(self, recipe_path: Path, extra_args=()):
import importlib.util
spec = importlib.util.spec_from_file_location("lint_recipe", LINT_SCRIPT)
assert spec is not None and spec.loader is not None
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
with mock.patch.object(mod, "PROJECT_ROOT", self.root), \
mock.patch.object(mod, "LOCAL_RECIPES", self.root / "local" / "recipes"), \
mock.patch.object(mod, "MAINLINE_RECIPES", self.root / "recipes"), \
mock.patch.object(mod, "LOCAL_PATCHES", self.root / "local" / "patches"):
return mod.lint_recipe(recipe_path, strict=False)
def findings_by_rule(self, findings):
return {rule_id: (sev, msg) for sev, rule_id, msg in findings}
class TestRule1NoPatchFile(LintRecipeFixture):
def test_missing_patch_file_fires(self):
path = self.write("local/recipes/kde/kf6-foo/recipe.toml", """
[source]
git = "https://example.com/foo.git"
rev = "deadbeef"
patches = ["nope.patch"]
[build]
script = "echo build"
""")
findings = self.run_lint(path)
rules = self.findings_by_rule(findings)
self.assertIn("R1-NO-PATCH-FILE", rules)
sev, msg = rules["R1-NO-PATCH-FILE"]
self.assertEqual(sev, "error")
self.assertIn("nope.patch", msg)
def test_existing_patch_file_passes(self):
(self.root / "local/recipes/kde/kf6-foo/legit.patch").write_text("")
path = self.write("local/recipes/kde/kf6-foo/recipe.toml", """
[source]
git = "https://example.com/foo.git"
rev = "deadbeef"
patches = ["legit.patch"]
[build]
script = "echo build"
""")
findings = self.run_lint(path)
rules = self.findings_by_rule(findings)
self.assertNotIn("R1-NO-PATCH-FILE", rules)
class TestRule1PathSource(LintRecipeFixture):
def test_in_tree_component_with_path_passes(self):
path = self.write("local/recipes/core/relibc/recipe.toml", """
[source]
path = "source"
[build]
script = "make"
""")
findings = self.run_lint(path)
rules = self.findings_by_rule(findings)
self.assertNotIn("R1-PATH-SOURCE", rules)
def test_in_tree_component_with_tar_url_fires(self):
path = self.write("local/recipes/core/relibc/recipe.toml", """
[source]
tar = "https://example.com/relibc.tar.xz"
blake3 = "deadbeef"
[build]
script = "make"
""")
findings = self.run_lint(path)
rules = self.findings_by_rule(findings)
self.assertIn("R1-PATH-SOURCE", rules)
sev, msg = rules["R1-PATH-SOURCE"]
self.assertEqual(sev, "warning")
def test_non_in_tree_component_with_tar_passes(self):
path = self.write("local/recipes/kde/kf6-foo/recipe.toml", """
[source]
tar = "https://example.com/kf6-foo.tar.xz"
blake3 = "deadbeef"
[build]
script = "make"
""")
findings = self.run_lint(path)
rules = self.findings_by_rule(findings)
self.assertNotIn("R1-PATH-SOURCE", rules)
class TestRule2InlineSed(LintRecipeFixture):
def test_sed_without_patches_fires_error(self):
path = self.write("local/recipes/kde/kf6-foo/recipe.toml", """
[source]
tar = "https://example.com/kf6-foo.tar.xz"
[build]
script = '''
sed -i 's/foo/bar/' "${COOKBOOK_SOURCE}/file.c"
sed -i 's/baz/qux/' "${COOKBOOK_SOURCE}/file.c"
make
'''
""")
findings = self.run_lint(path)
rules = self.findings_by_rule(findings)
self.assertIn("R2-INLINE-SED", rules)
sev, msg = rules["R2-INLINE-SED"]
self.assertEqual(sev, "error")
self.assertIn("2 `sed -i`", msg)
def test_sed_with_cookbook_apply_patches_fires_warning(self):
path = self.write("local/recipes/kde/kf6-foo/recipe.toml", """
[source]
tar = "https://example.com/kf6-foo.tar.xz"
[build]
script = '''
cookbook_apply_patches $REDBEAR_PATCHES_DIR
sed -i 's/foo/bar/' "${COOKBOOK_SOURCE}/file.c"
make
'''
""")
findings = self.run_lint(path)
rules = self.findings_by_rule(findings)
self.assertIn("R2-INLINE-SED", rules)
sev, msg = rules["R2-INLINE-SED"]
self.assertEqual(sev, "warning")
self.assertIn("WITH-PATCHES", msg)
def test_build_time_seds_are_exempt(self):
# Seds that target ${COOKBOOK_STAGE}, ${COOKBOOK_BUILD},
# ${COOKBOOK_SYSROOT}, or non-source paths are exempt
# from R2 (those are build-time adjustments, not
# upstream source edits).
path = self.write("local/recipes/kde/kf6-foo/recipe.toml", """
[source]
tar = "https://example.com/kf6-foo.tar.xz"
[build]
script = '''
sed -i 's/foo/bar/' "${COOKBOOK_BUILD}/Makefile"
sed -i 's/baz/qux/' "${COOKBOOK_STAGE}/usr/lib/foo"
sed -i 's/aaa/bbb/' "${COOKBOOK_SYSROOT}/lib/cmake/foo"
make
'''
""")
findings = self.run_lint(path)
rules = self.findings_by_rule(findings)
# No R2 finding — all seds are build-time.
self.assertNotIn("R2-INLINE-SED", rules)
def test_no_sed_passes(self):
path = self.write("local/recipes/kde/kf6-clean/recipe.toml", """
[source]
tar = "https://example.com/clean.tar.xz"
[build]
script = "make"
""")
findings = self.run_lint(path)
rules = self.findings_by_rule(findings)
self.assertNotIn("R2-INLINE-SED", rules)
class TestRule2PatchesDirConsistent(LintRecipeFixture):
def test_patches_dir_with_numbered_files_and_no_apply_fires(self):
path = self.write("local/recipes/kde/kf6-with-patches/recipe.toml", """
[source]
tar = "https://example.com/x.tar.xz"
[build]
script = "make"
""")
findings = self.run_lint(path)
rules = self.findings_by_rule(findings)
self.assertIn("R2-PATCHES-DIR-UNUSED", rules)
sev, msg = rules["R2-PATCHES-DIR-UNUSED"]
self.assertEqual(sev, "error")
self.assertIn("PATCHES-DIR-UNUSED", msg)
def test_apply_patches_without_dir_fires(self):
path = self.write("local/recipes/kde/kf6-foo/recipe.toml", """
[source]
tar = "https://example.com/x.tar.xz"
[build]
script = "cookbook_apply_patches /tmp/nope"
""")
findings = self.run_lint(path)
rules = self.findings_by_rule(findings)
self.assertIn("R2-PATCHES-DIR-UNUSED", rules)
sev, msg = rules["R2-PATCHES-DIR-UNUSED"]
self.assertEqual(sev, "error")
self.assertIn("APPLY-PATCHES-NO-DIR", msg)
def test_patches_dir_used_correctly_passes(self):
path = self.write("local/recipes/kde/kf6-with-patches/recipe.toml", """
[source]
tar = "https://example.com/x.tar.xz"
[build]
script = '''
REDBEAR_PATCHES_DIR=local/patches/kf6-with-patches
cookbook_apply_patches "$REDBEAR_PATCHES_DIR"
make
'''
""")
findings = self.run_lint(path)
rules = self.findings_by_rule(findings)
self.assertNotIn("R2-PATCHES-DIR-UNUSED", rules)
class TestNoLegacyMake(LintRecipeFixture):
def test_make_all_config_name_fires(self):
path = self.write("local/recipes/kde/kf6-foo/recipe.toml", """
[source]
tar = "https://example.com/x.tar.xz"
[build]
script = "make all CONFIG_NAME=redbear-full"
""")
findings = self.run_lint(path)
rules = self.findings_by_rule(findings)
self.assertIn("NO-LEGACY-MAKE", rules)
sev, _ = rules["NO-LEGACY-MAKE"]
self.assertEqual(sev, "warning")
def test_make_live_fires(self):
path = self.write("local/recipes/kde/kf6-foo/recipe.toml", """
[build]
script = "make live CONFIG_NAME=redbear-mini"
""")
findings = self.run_lint(path)
rules = self.findings_by_rule(findings)
self.assertIn("NO-LEGACY-MAKE", rules)
def test_make_something_else_passes(self):
path = self.write("local/recipes/kde/kf6-foo/recipe.toml", """
[build]
script = "make install"
""")
findings = self.run_lint(path)
rules = self.findings_by_rule(findings)
self.assertNotIn("NO-LEGACY-MAKE", rules)
class TestNoApplyPatchesSh(LintRecipeFixture):
def test_apply_patches_sh_reference_fires(self):
path = self.write("local/recipes/kde/kf6-foo/recipe.toml", """
[build]
script = "./apply-patches.sh"
""")
findings = self.run_lint(path)
rules = self.findings_by_rule(findings)
self.assertIn("R1-LEGACY-APPLY-PATCHES", rules)
sev, _ = rules["R1-LEGACY-APPLY-PATCHES"]
self.assertEqual(sev, "error")
def test_no_reference_passes(self):
path = self.write("local/recipes/kde/kf6-foo/recipe.toml", """
[build]
script = "make"
""")
findings = self.run_lint(path)
rules = self.findings_by_rule(findings)
self.assertNotIn("R1-LEGACY-APPLY-PATCHES", rules)
class TestDepsResolve(LintRecipeFixture):
def test_redbear_dep_missing_fires(self):
path = self.write("local/recipes/kde/kf6-foo/recipe.toml", """
[build]
script = "make"
dependencies = ["redbear-nonexistent-daemon"]
""")
findings = self.run_lint(path)
rules = self.findings_by_rule(findings)
self.assertIn("DEP-NOT-FOUND", rules)
sev, msg = rules["DEP-NOT-FOUND"]
self.assertEqual(sev, "error")
self.assertIn("redbear-nonexistent-daemon", msg)
def test_kf6_dep_missing_fires(self):
path = self.write("local/recipes/kde/kf6-foo/recipe.toml", """
[build]
script = "make"
dependencies = ["kf6-bogus-package"]
""")
findings = self.run_lint(path)
rules = self.findings_by_rule(findings)
self.assertIn("DEP-NOT-FOUND", rules)
def test_known_dep_resolves(self):
path = self.write("local/recipes/kde/kf6-foo/recipe.toml", """
[build]
script = "make"
dependencies = ["kf6-clean"]
""")
findings = self.run_lint(path)
rules = self.findings_by_rule(findings)
self.assertNotIn("DEP-NOT-FOUND", rules)
def test_mainline_dep_resolves(self):
path = self.write("local/recipes/kde/kf6-foo/recipe.toml", """
[build]
script = "make"
dependencies = ["kernel"]
""")
findings = self.run_lint(path)
rules = self.findings_by_rule(findings)
self.assertNotIn("DEP-NOT-FOUND", rules)
class TestCleanRecipe(LintRecipeFixture):
"""A well-formed clean recipe should produce zero findings."""
def test_clean_recipe_no_findings(self):
path = self.write("local/recipes/kde/kf6-clean/recipe.toml", """
[source]
tar = "https://example.com/clean.tar.xz"
blake3 = "abc123"
[build]
script = "make install"
""")
findings = self.run_lint(path)
# No rules should fire
self.assertEqual(findings, [], f"Expected no findings, got: {findings}")
class TestRecipeIndexCaching(unittest.TestCase):
"""Verify that build_recipe_index precomputes a usable lookup set."""
def setUp(self):
import importlib.util
spec = importlib.util.spec_from_file_location("lint_recipe", LINT_SCRIPT)
assert spec is not None and spec.loader is not None
self.mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(self.mod)
self.tmp = tempfile.TemporaryDirectory()
self.root = Path(self.tmp.name)
for f in ["local/recipes/kde/kf6-x/recipe.toml",
"recipes/core/kernel/recipe.toml",
"local/recipes/source/should-skip/recipe.toml",
"local/recipes/wip/should-skip/recipe.toml",
"local/recipes/kde/kf6-x/source/sub/recipe.toml"]:
(self.root / f).parent.mkdir(parents=True, exist_ok=True)
(self.root / f).write_text("")
def tearDown(self):
self.tmp.cleanup()
def test_index_includes_pkg_and_cat_pkg(self):
with mock.patch.object(self.mod, "LOCAL_RECIPES", self.root / "local" / "recipes"), \
mock.patch.object(self.mod, "MAINLINE_RECIPES", self.root / "recipes"):
idx = self.mod.build_recipe_index()
self.assertIn("kf6-x", idx)
self.assertIn("kde/kf6-x", idx)
self.assertIn("kernel", idx)
self.assertIn("core/kernel", idx)
self.assertNotIn("should-skip", idx)
class TestExitCodes(LintRecipeFixture):
"""End-to-end: clean recipe produces no findings, errors do."""
def test_clean_recipe_no_findings(self):
self.write("local/recipes/kde/kf6-clean/recipe.toml", """
[source]
tar = "https://example.com/clean.tar.xz"
[build]
script = "make"
""")
path = self.root / "local" / "recipes" / "kde" / "kf6-clean" / "recipe.toml"
findings = self.run_lint(path)
self.assertEqual(findings, [])
def test_error_recipe_exit_1(self):
path = self.write("local/recipes/kde/kf6-foo/recipe.toml", """
[source]
git = "https://example.com/foo.git"
rev = "deadbeef"
patches = ["missing.patch"]
[build]
script = "sed -i 's/a/b/' file && make"
""")
findings = self.run_lint(path)
self.assertTrue(any(s == "error" for s, _, _ in findings))
if __name__ == "__main__":
unittest.main()